[TOC] ## 1. 背景 在我们开发微服务架构系统时,虽然说每个微服务都是孤立的可以单独开发,但实际上并非如此,要调试和测试你的服务不仅需要您的微服务启动和运行,还需要它的上下文服务、依赖的基础服务等都要运行;但如果你的系统服务数和依赖比较多呢,那就是一个比较棘手的问题!有没有办法能提高开发效率呢? ![](https://img.kancloud.cn/7a/af/7aaf6e91a6b79a67e672e92fea2192f7_710x319.png) 如上图所示,我们能不能用**服务器把所有的服务都部署**起来,然后开发**只在本地运行自己所负责开发的服务**,因为需要依赖其他服务所以本地启动的服务也需要注册到公共的注册中心里; > 例子中`业务服务B`有3台实例注册到注册中心里 > 分别是:服务器的、开发A与开发B自己本机启动的 但是这样做又会出现新的问题:**服务会冲突乱窜**,意思就是`开发A`在debug自己的`业务服务B`服务的时候可能请求会跳转到其他人的实例上(服务器、开发B) ## 2. 解决思路 解决这个服务乱窜问题有一个比较优雅的方式就是`自定义负载均衡规则`,主要实现以下目标: >1. **普通用户**访问服务器上的页面时,请求的所有路由只调用`服务器上的实例` >2. **开发A**访问时,请求的所有路由优先调用`开发A本机启动的实例`,如果没有则调用`服务器上的实例` >3. **开发B**访问时同上,请求的所有路由优先调用`开发B本机启动的实例`,如果没有则调用`服务器上的实例` ## 3. 具体实现 开启功能需要每个服务都添加以下配置,我们可以直接修改`config/nacos/`下的`config.yml`配置文件更新到nacos上 ~~~ pigframe.ribbon.isolation.enabled=true ~~~ 要实现上面的目标有两个比较关键的问题需要解决 >1. 区分`不同用户的服务实例` >2. 实现`自定义负载均衡规则` ### 3.1. 区分**不同用户的服务实例** #### 3.1.1. 元数据 直接使用注册中心的元数据(metadata)来区分就可以了,主流的注册中心都带有元数据管理 以`Nacos`为例,只需要在配置文件下添加 ~~~ spring: cloud: nacos: discovery: server-addr: localhost:8848 metadata: version: pigframe ~~~ metadata下的`version`就是我添加的元数据`key`是version,`value`是pigframe 经过元数据区分后,目前是下面这个情况 * 服务器的实例`version`为空 * 开发人员自己本地启动的实例`version`为唯一标识(自己的名字) ![](https://img.kancloud.cn/ac/11/ac11461ced2b60e757a514bcf22fa7e0_821x368.png) #### 3.1.2. IP ![](https://img.kancloud.cn/b7/05/b705905bb7bee4f1d7e52bd8bfbfb2db_710x316.png) * **开发人员本机IP**\- 其实就是`客户端IP`,也就是原始请求方的IP:**172.16.20.2** * **服务器IP**\- 可以理解为服务器上的服务所在机器的IP:**172.16.20.1** 主要实现以下目标: >1. **普通用户**访问服务器上的页面时,请求的所有路由只调用`服务器上的实例` >2. **开发A**访问时,请求的所有路由优先调用`开发A本机启动的实例`,如果没有则调用`服务器上的实例` >3. **开发B**访问时同上,请求的所有路由优先调用`开发B本机启动的实例`,如果没有则调用`服务器上的实例` 在找到`IP`的辨识规律后,推导出下面3个**路由规则**来实现上面的目标 >1. 优先匹配`原始请求方的IP`的服务实例 >2. 再者匹配`上游服务所在机器IP`的服务实例 >3. 上面2个逻辑都匹配不到的话使用轮询的方式找一个实例 获取`原始IP`的代码片段如下,只需要在网关上增加一个过滤器获取IP,然后添加到header里面一直传递下去就可以了 ~~~ public class IpUtils { /** * 获取IP地址 * <p> * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 */ public static String getIpAddr(HttpServletRequest request) { String ip = null; try { if (request == null) { return ""; } ip = request.getHeader("x-forwarded-for"); if (checkIp(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (checkIp(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (checkIp(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (checkIp(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (checkIp(ip)) { ip = request.getRemoteAddr(); if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { // 根据网卡取本机配置的IP ip = getLocalAddr(); } } } catch (Exception e) { log.error("IPUtils ERROR, {}", e.getMessage()); } //使用代理,则获取第一个IP地址 if(StringUtils.isNotEmpty(ip) && ip.length() > 15) { if(ip.indexOf(", ") > 0) { ip = ip.substring(0, ip.indexOf(", ")); } } if(StringUtils.isNotEmpty(ip) && ip.length() > 15) { if(ip.indexOf(",") > 0) { ip = ip.substring(0, ip.indexOf(",")); } } return ip; } private static boolean checkIp(String ip) { String unknown = "unknown"; return StringUtils.isEmpty(ip) || ip.length() == 0 || unknown.equalsIgnoreCase(ip); } /** * 获取本机的IP地址 */ private static String getLocalAddr() { try { return InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException e) { log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); } return ""; } } ~~~ **把原始IP添加到header**的`HTTP_X_FORWARDED_FOR`里面传递给下游服务 ~~~ RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); String sourceIp = getIpAddr(request); ctx.getZuulRequestHeaders().put("HTTP_X_FORWARDED_FOR", sourceIp); ~~~ ### 3.2. 实现`自定义负载均衡规则` 首先在`Spring Cloud`微服务框架里实例的负载均衡是由`Ribbon`负责。 ~~~ public class VersionIsolationRule extends RoundRobinRule { private final static String KEY_DEFAULT = "default"; /** * 优先根据版本号取实例 */ @Override public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { return null; } String version; if (key != null && !KEY_DEFAULT.equals(key)) { version = key.toString(); } else { version = LbIsolationContextHolder.getVersion(); } List<Server> targetList = null; List<Server> upList = lb.getReachableServers(); if (StringUtils.isNotEmpty(version)) { //取指定版本号的实例 targetList = upList.stream().filter( server -> version.equals( ((NacosServer) server).getMetadata().get(CommonConstant.METADATA_VERSION) ) ).collect(Collectors.toList()); } if (CollectionUtils.isEmpty(targetList)) { //只取无版本号的实例 targetList = upList.stream().filter( server -> { String metadataVersion = ((NacosServer) server).getMetadata().get(CommonConstant.METADATA_VERSION); return StringUtils.isEmpty(metadataVersion); } ).collect(Collectors.toList()); } if (CollectionUtils.isNotEmpty(targetList)) { return getServer(targetList); } return super.choose(lb, key); } /** * 随机取一个实例 */ private Server getServer(List<Server> upList) { int nextInt = RandomUtil.randomInt(upList.size()); return upList.get(nextInt); } } ~~~ >继承轮询规则`RoundRobinRule`来实现,主要的逻辑为 >1. 根据上游输入的版本号`version`,有值的话则取`服务元信息`中`version`值一样的实例 >2. 上游的版本号`version`没值或者该版本号匹配不到任何服务,则只取`服务元信息`中`version`值为空的实例 **并通过配置开关控制是否开启自定义负载规则** ~~~java @ConditionalOnProperty(value = "pigframe.ribbon.isolation.enabled", havingValue = "true") @RibbonClients(defaultConfiguration = {RuleConfigure.class}) public class LbIsolationAutoConfigure { } ~~~ ## 4. 总结 上面提到的**区分服务实例**和**自定义负载规则**为整个解决思路的核心点,基本实现了服务实例的隔离。 元数据方法**上游的`version`怎样传递呢?**,下面我提供两个思路 * 开发人员自己启动前端工程,通过配置参数,统一在前端工程传递`version` * 通过`postman`调用接口的时候在header参数中添加`pigframe-version:pigframe` 通过`IP`的方案来实现**开发环境**服务实例隔离和策略路由后,可以实现到`开发完全无感知`,既不需要配置`元数据`,也不需要自己去传`version`之类的参数了。但是这个方案其实也是有**局限性**的: >1. 开发服务器必须是只用一台来部署所有的服务,因为如果上游服务和下游服务不在同一个`IP`上就失去了辨识能力了 >2. 因为网络环境比较复杂,不一定能获取到客户端的真实`原始IP` >3. 开发人员启动客户端/前端的机器与启动后台服务必须是同一台电脑上才行;例如如果是`前端开发人员A`启动的客户端,去调试`后台开发人员B`启动的服务就不行了,因为`原始IP`与注册上去的`服务实例IP`匹配不上