聊聊微服务中的多环境隔离

一、背景

在微服务项目开发中,虽然说每个服务都是一个微服务,可以独立工作。但现实中,一个微服务还依赖着它的下游服务,如用户服务、基础服务等。
作为一名开发人员,当我完成了某个需求开发后。我需要对自己开发的代码进行自测以及全流程的验证。此时,有如下几种方案:
a. 公司有容器环境,则独立部署一整套容器环境即可
b. 或者把代码提交到公共的开发环境,直接在上面验证
c. 开发者本地启动所依赖的全套服务,完成功能的验证
    方案a需要类似k8s环境。方案b的在验证阶段会造成这套公共开发环境特别不稳定,而且这套环境可能多个人需要用,会冲突。方案 c 是最常见的,因为在开发者自己验证阶段,产生bug的概率很高,特别方便调试bug。但方案c中,如果涉及到的微服务特别多,开发者启动的应用就会很多,因此对开发者的电脑配置需求高。同时由于需要开发者自己启动服务,效率相对不够高。有没有一种更高效率的方案?能让开发者只需要启动自己开发的微服务,其他的服务都不需要启动。


二、方案概述

本文将提出一种基于流量染色的方案,实现开发者只需要在本地启动自己开发的服务,就能验证所开发的功能在全链路中使用是否正常。
流量染色:指的是对正常的http请求做一点点修改,比如添加一个特定的请求头。
具体的方案为:
  1. 前端提供一个设置统一请求头的方法,设置后往后的发的请求都会带上该请求头;通过设置请求头,对流量染色
  2. 网关识别这个染色的流量,先去找同色的服务进行路由,如果没提供则用基准环境的提供的服务
  3. 微服务内部调用时,如果是染色流量,则先找同色的服务提供者,如果没有,则用基准环境的服务提供者
可以参考下图,染色流量的流转
image.png

结合场景,讲解流量染色在各阶段的作用
开发者自测阶段:
  • 假设小王开发的业务A1涉及到Service A和Service C中代码的修改,因此小王本地启动了Service A和Service C
  • 小王在浏览器或app中设置流量染色头为A1,这样流量就能顺利的流入小王 本地的ServiceA _A1和ServiceC_A1;参考上图中染色环境A1的流量流转
  • 小王能够顺利的完成常规的业务操作,说明全链路下其开发的功能都已正常通过测试
前后端联合调试阶段:
  • 小吴是前端开发人员,开发业务A1相关的前端页面,现在他需要找小王进行联合调试
  • 因为小王上面已经启动了本地的ServiceA_A1和ServiceC_A1,因此小吴只需要在开发工具中,设置流量染色头为A1
  • 这样小吴的前端请求,就可以精准的流入到小王的本地环境;小王未提供的服务接口,由基准环境提供
特别说明:
  • 基准环境的流量是不会流入开发者本地的,因此不用担心开发者本地的服务影响基准环境

三、方案实现介绍

根据上一章节对流量染色方案的介绍,最初的设想如下
  • 服务可以按照流量的染色信息路由到相应的染色服务中
  • 如果未找到相应的染色服务,则由基准环境提供
  • DB、Redis、MQ等中间件,和基准环境共用一套
基于此设想,得出具体需要实现的功能如下:
    前提说明:本方案采用的技术框架为SpringCloud + nacos + SpringCloudGateway ,服务之间采用Feign调用,采用ribbon进行客户端负载均衡
    1. web端、app端支持设置请求头,对流量进行染色
    2. SpringCloudGateway支持对染色流量自定义转发策略,将其转发到染色的服务
    3. 自定义ribbon负载均衡策略,识别到染色请求时优先调用染色的服务
    4. 染色头的透传,保证染色头在各个服务之间传递不丢失
    5. 开发者本地启动的服务注册到自定义的nacos分组中,防止影响基准环境,分组名必须和染色头对应上,如上面的染色头为A1,则注册的分组名也必须为A1
    6. fnacos跨组调用的实现
下面阐述这些功能点的实现

3.1 流量染色的实现

由前端js或者app实现 ,添加一个统一的请求头即可

3.2 网关层的改动

参考ReactiveLoadBalancerClientFilter,自定义Filter,修改choose方法,引入自定义负载均衡规则实现
image.png

自定义的负载均衡策略里,需要引入nacos相关的api,可以直接参考下面的关键代码

	/**
	 * 根据serviceId 筛选可用服务
	 * @param serviceId 服务ID
	 * @param request 当前请求
	 * @return
	 */
	@Override
	public ServiceInstance choose(String serviceId, ServerHttpRequest request) {
		try {
			String propertiesGroup = this.nacosDiscoveryProperties.getGroup();
			NamingService namingService = nacosServiceManager.getNamingService(nacosDiscoveryProperties.getNacosProperties());
			List<Instance> instances;
			String grayGroup = request != null ? request.getHeaders().getFirst(GrayConstant.GRAY_HEADER)
					: null;
			if (StringUtils.isNotBlank(grayGroup)) {
				instances = namingService.selectInstances(serviceId, grayGroup, true);
				if (CollectionUtils.isEmpty(instances)) {
					log.debug("no instance in service {} by group {}", serviceId, grayGroup);
					String defaultGroup = propertiesGroup.equals(grayGroup) ? getNacosDefaultGroup() : propertiesGroup;
					instances = namingService.selectInstances(serviceId, defaultGroup, true);
				}
			} else {
				instances = namingService.selectInstances(serviceId, propertiesGroup, true);
			}
			if (CollectionUtils.isEmpty(instances)) {
				log.error("no instance in service {}", serviceId);
				return null;
			}
			Instance instance = ExtendBalancer.getHostByRandomWeight2(instances);
			return hostToServiceInstance(instance, serviceId);
		}
		catch (Exception e) {
			log.warn("NacosRule error", e);
			return null;
		}
	}

3.3 Feign调用的改动

重写ribbon负载均衡策略,如果是染色流量,优先调用染色流量对应nacos分组中的服务。
继承AbstractLoadBalancerRule这个类,重写choose方法即可。
特别注意: 自定义的这个Rule记得实例化成多例的

image.png


3.4 流量染色头的透传方案介绍

1、普通feign调用
通过Feign拦截器copy请求头,copy染色头
2、网关转发
由于前端有传染色头过来,网关转发过程默认不会丢染色头
3、web拦截器
网关将请求转发到对应的服务后,需要写一个拦截器,将请求中染色头存放在ThreadLocal中用于Feign拦截器获取。在请求结束后,清理ThreadLocal
4、网关层面直接feign调用的处理
在用户身份鉴权的场景,网关是通过feign调用用户中心进行请求的身份认证和鉴权。这种情况下Feign调用也需要copy请求头,因此也需要写一个Filter,将染色头放于ThreadLocal中

3.5 nacos跨组调用的实现

nacos默认同一个namespace + group里面的服务能直接调用的;对于同一个namespace不同的group,默认是隔离的。但这个隔离,只是逻辑隔离的。我们可以在服务发现时,通过传入对于的group,发现其他分组的服务;实现跨group的调用。可参考下图
image.png

有了这个官方api的支持,轻松实现跨分组的服务发现,从而实现跨组调用
String grayGroup = GrayGroupContextHolder.getGrayGroup();
if (StringUtils.isNotBlank(grayGroup)) {
    // 跨分组查询实例
    instances = namingService.selectInstances(serviceName, grayGroup, true);
    if (CollectionUtils.isEmpty(instances)) {
        log.debug("no instance in service {} by group {}", serviceName, grayGroup);
        String defaultGroup = propertiesGroup.equals(grayGroup) ? getNacosDefaultGroup() : propertiesGroup;
        instances = namingService.selectInstances(serviceName, defaultGroup, true);
    }
} else {
    instances = namingService.selectInstances(serviceName, propertiesGroup, true);
}

3.6 最终效果示意图

最终实现的效果如下图,通过流量的染色,请求能精准的调用到后端开发者在本地启动的服务
image.png



四、总结

  1. 通过流量染色的方式,对流量进行识别并让其流入开发者本地服务,完成开发者本地服务的调试,提高开发效率
  2. 通过ThreadLocal + copy请求头的方式,保证染色头的透传
  3. 通过自定义负载均衡策略 + nacos跨组调用实现染色请求调用到染色分组的服务且不影响基准环境

五、未来展望

  1. 全链路染色和隔离,包含Redis/MQ等
  2. 这个染色的方案,可用于实现灰度的功能,只要将流量染色的操作放在网关识别用户信息后就能实现部分用户访问灰度环境服务的功能
posted @ 2023-01-16 15:28  zeng1994  阅读(978)  评论(2编辑  收藏  举报