「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」
一、前言
流量控制指的是根据一些流量特征,控制其流向下流的动作。
举个栗子:服务A 与 服务B 通讯
- 满足
Query Parameter中x-request=don的流量,打到192.168.1.1的服务 - 满足
Header中x-request=don的流量,打到192.168.1.2的服务 - 满足
Cookie中x-request=don的流量,打到192.168.1.3的服务
流量控制需要这两个能力:
- 流量识别能力:
Spring Cloud默认的服务调用是HTTP协议,在服务调用的过程中需要识别HTTP协议的内容后再决定路由。 - 实例打标能力:每个服务实例都需要被标记。
业务场景
流量控制可以应用在许多业务场景中:
- 金丝雀发布
- 同机房优先路由
- 标签路由
- 全链路灰度
1. 金丝雀发布
金丝雀发布(灰度发布):实时流量逐渐从旧版本迁移到新版本直到更新生效。
例如:应用有新和老的两个版本,根据流量特征(流量比例)将部分流量流入新版本验证。
确定新版本稳定后再全量发布。
2. 用机房优先路由
当公司规模扩大之后,应用会跨机房部署来达到高可用的目的。
场景:由于异地跨机房调用出现的网络延迟问题,需要确保服务消费方能优先调用相同机房的服务消费方。
3. 全链路灰度场景
当公司规模扩大之后,微服务数量增多。灰度发布整个链路非常长。
全链路灰度解决的问题:保证特定流量能够路由到所有的特殊灰度版本。
二、灰度发布实战
使用
Ribbon来完成应用灰度发布。
问题:如何识别下游服务,哪些是灰度,哪些是正常?难道请求下游所有服务?
当然不是。
每个服务发现会在本地缓存一份对应服务实例
Map。之后,交由
Ribbon去选择请求哪个服务实例。
应用流量控制能力如下:
- 流量识别能力:
Ribbon架构下,无论是ILoadBalancer作为路由还是IRule作为负载均衡,组件的定义跟HTTP请求信息解耦,在Ribbon内部无法解析HTTP请求信息。
这是需要通过
ThreadLocal来完成HTTP请求解析结果的透传。
-
实例打标能力:在实例的
metadata(元数据)中加上标签信息。通过IRule获取Server列表并根据这些Server中元数据的标签信息决定路由情况。# 正常实例配置内容 spring.cloud.nacos.discovery.metadata.gray=false # 灰度实例配置内容 spring.cloud.nacos.discovery.metadata.gray=true
实例示图:项目地址
用户请求先打到服务A,再由服务A转发到对应服务B。
先来看下如何流量识别:
- 定义请求拦截器,获取特殊流量特征
- 定义
Ribbon的Rule,实现特定流量导向
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 的实例收到请求。