流量控制
流量控制指的是根据一些流量特征,控制其流向下流的动作。
举个栗子:服务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
协议的内容后再决定路由。 -
实例打标能力:每个服务实例都需要被标记。
业务场景
流量控制可以应用在许多业务场景中:
-
金丝雀发布
-
同机房优先路由
-
标签路由
-
全链路灰度
金丝雀发布
金丝雀发布(灰度发布):实时流量逐渐从旧版本迁移到新版本直到更新生效。
例如:应用有新和老的两个版本,根据流量特征(流量比例)将部分流量流入新版本验证。
❝确定新版本稳定后再全量发布。
用机房优先路由
❝当公司规模扩大之后,应用会跨机房部署来达到高可用的目的。
场景:由于异地跨机房调用出现的网络延迟问题,需要确保服务消费方能优先调用相同机房的服务消费方。
全链路灰度场景
❝当公司规模扩大之后,微服务数量增多。灰度发布整个链路非常长。
全链路灰度解决的问题:保证特定流量能够路由到所有的特殊灰度版本。
灰度发布实战
❝使用
Ribbon
来完成应用灰度发布。
问题:如何识别下游服务,哪些是灰度,哪些是正常?难道请求下游所有服务?
❝当然不是。
每个服务发现会在本地缓存一份对应服务实例
Map
。之后,交由
Ribbon
去选择请求哪个服务实例。
应用流量控制能力如下:
-
流量识别能力:
Ribbon
架构下,无论是ILoadBalancer
作为路由还是IRule
作为负载均衡,组件的定义跟HTTP
请求信息解耦,在Ribbon
内部无法解析HTTP
请求信息。
❝这是需要通过
ThreadLocal
来完成HTTP
请求解析结果的透传。
2.实例打标能力:在实例的 metadata
(元数据)中加上标签信息。通过 IRule
获取 Server
列表并根据这些 Server
中元数据的标签信息决定路由情况。
# 正常实例配置内容
spring.cloud.nacos.discovery.metadata.gray=false
# 灰度实例配置内容
spring.cloud.nacos.discovery.metadata.gray=true
实例示图:项目地址:https://t.zsxq.com/6ctAc
、https://t.zsxq.com/hO1EX
。
❝用户请求先打到服务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; } }
RibbonRequestContextHolder
的相关实现。package com.donald.ribbonconsumer.gray; /** * @author herman */ public class RibbonRequestContextHolder { private static ThreadLocal<RibbonRequestContext> holder = ThreadLocal.withInitial(RibbonRequestContext::new); public static RibbonRequestContext getCurrentContext() { return holder.get(); } public static void setCurrentContext(RibbonRequestContext context) { holder.set(context); } public static void clearContext() { holder.remove(); } }
重点来了:
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
的实例收到请求。