【Nacos】实战 - 灰度发布(流量控制)

5,368 阅读3分钟

「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战

一、前言

流量控制指的是根据一些流量特征,控制其流向下流的动作。

举个栗子:服务A 与 服务B 通讯

  1. 满足 Query Parameterx-request=don 的流量,打到 192.168.1.1 的服务
  2. 满足 Headerx-request=don 的流量,打到 192.168.1.2 的服务
  3. 满足 Cookiex-request=don 的流量,打到 192.168.1.3 的服务

2022-01-2314-35-48.png

流量控制需要这两个能力:

  1. 流量识别能力Spring Cloud 默认的服务调用是 HTTP 协议,在服务调用的过程中需要识别 HTTP 协议的内容后再决定路由。
  2. 实例打标能力:每个服务实例都需要被标记。

业务场景

流量控制可以应用在许多业务场景中:

  • 金丝雀发布
  • 同机房优先路由
  • 标签路由
  • 全链路灰度

1. 金丝雀发布

金丝雀发布(灰度发布):实时流量逐渐从旧版本迁移到新版本直到更新生效。

例如:应用有新和老的两个版本,根据流量特征(流量比例)将部分流量流入新版本验证。

确定新版本稳定后再全量发布。

2022-01-2314-51-10.png


2. 用机房优先路由

当公司规模扩大之后,应用会跨机房部署来达到高可用的目的。

场景:由于异地跨机房调用出现的网络延迟问题,需要确保服务消费方能优先调用相同机房的服务消费方。

2022-01-2315-17-57.png


3. 全链路灰度场景

当公司规模扩大之后,微服务数量增多。灰度发布整个链路非常长。

全链路灰度解决的问题:保证特定流量能够路由到所有的特殊灰度版本。 2022-01-2315-50-28.png



二、灰度发布实战

使用 Ribbon 来完成应用灰度发布。

问题:如何识别下游服务,哪些是灰度,哪些是正常?难道请求下游所有服务?

当然不是。

每个服务发现会在本地缓存一份对应服务实例 Map

之后,交由 Ribbon 去选择请求哪个服务实例。

应用流量控制能力如下:

  1. 流量识别能力Ribbon 架构下,无论是 ILoadBalancer 作为路由还是 IRule 作为负载均衡,组件的定义跟 HTTP 请求信息解耦,在 Ribbon 内部无法解析 HTTP 请求信息。

这是需要通过 ThreadLocal 来完成 HTTP 请求解析结果的透传。

  1. 实例打标能力:在实例的 metadata (元数据)中加上标签信息。通过 IRule 获取 Server 列表并根据这些 Server 中元数据的标签信息决定路由情况。

    # 正常实例配置内容
    spring.cloud.nacos.discovery.metadata.gray=false
    
    # 灰度实例配置内容
    spring.cloud.nacos.discovery.metadata.gray=true
    

实例示图:项目地址

用户请求先打到服务A,再由服务A转发到对应服务B。 2022-01-2323-16-45.png

先来看下如何流量识别:

  1. 定义请求拦截器,获取特殊流量特征
  2. 定义 RibbonRule ,实现特定流量导向
public class GrayInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes,
                                        ClientHttpRequestExecution execution) 
        throws IOException {
        if (httpRequest.getHeaders().containsKey("Gray")) {
            String value = httpRequest.getHeaders().getFirst("Gray");
            if (Objects.equals(value, "true")) {
                // RibbonRequestContextHolder 其实就是 ThreadLocal
                // 为了传递信息
                RibbonRequestContextHolder.getCurrentContext()
                    .put("Gray", Boolean.TRUE.toString());
            }
        }
        return execution.execute(httpRequest, bytes);
    }
}

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getInterceptors().add(new GrayInterceptor());
        return restTemplate;
    }
}

重点来了:

public class GrayRule extends AbstractLoadBalancerRule {
    private Random random = new Random();

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
    }

    @Override
    public Server choose(Object key) {
        try {
            boolean grayInvocation = false;
            String grayTag = RibbonRequestContextHolder.getCurrentContext().get("Gray");
            if(!StringUtils.isEmpty(grayTag) && grayTag.equals("true")) {
                grayInvocation = true;
            }

            List<Server> serverList = this.getLoadBalancer().getReachableServers();
            List<Server> grayServerList = new ArrayList<>();
            List<Server> normalServerList = new ArrayList<>();
            for(Server server : serverList) {
                NacosServer nacosServer = (NacosServer) server;
                if(nacosServer.getMetadata().containsKey("gray")
                        && nacosServer.getMetadata().get("gray").equals("true")) {
                    grayServerList.add(server);
                } else {
                    normalServerList.add(server);
                }
            }

            if(grayInvocation) {
                return grayServerList.get(random.nextInt(grayServerList.size()));
            } else {
                return normalServerList.get(random.nextInt(normalServerList.size()));
            }
        } finally {
            RibbonRequestContextHolder.clearContext();
        }
    }
}

还需要在启动类上定义:

@RibbonClients(defaultConfiguration = {GrayRule.class})
@SpringBootApplication
public class NacosProviderApplication {

	public static void main(String[] args) {
		SpringApplication.run(NacosProviderApplication.class, args);
	}
}

接口访问:

@RestController
class EchoController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/echo")
    public String echo(HttpServletRequest request) {
        String serviceName = "nacos-consumer";
        HttpHeaders headers = new HttpHeaders();
        if (StringUtils.isNotEmpty(request.getHeader("Gray"))) {
            headers.add("Gray", request.getHeader("Gray").equals("true") 
                        ? "true" : "false");
        }
        HttpEntity<String> entity = new HttpEntity<>(headers);
        return restTemplate.exchange("http://" + serviceName + "/", HttpMethod.GET, 
                                     entity, String.class).getBody();
    }
}

测试结果:开启 Gray=true 的实例收到请求。