基于Nacos+Ribbon+网关的灰度发布(流量控制)实战

流量控制

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

举个栗子:服务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 的服务

图片

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

  1. 流量识别能力Spring Cloud 默认的服务调用是 HTTP 协议,在服务调用的过程中需要识别 HTTP 协议的内容后再决定路由。

  2. 实例打标能力:每个服务实例都需要被标记。

业务场景

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

  • 金丝雀发布

  • 同机房优先路由

  • 标签路由

  • 全链路灰度

金丝雀发布

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

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

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

图片

用机房优先路由

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

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

图片

全链路灰度场景

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

全链路灰度解决的问题:保证特定流量能够路由到所有的特殊灰度版本。

灰度发布实战

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

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

当然不是。

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

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

应用流量控制能力如下:

  1. 流量识别能力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/6ctAchttps://t.zsxq.com/hO1EX

用户请求先打到服务A,再由服务A转发到对应服务B。

图片

先来看下如何流量识别:

  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;
        }
    }

    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 的实例收到请求。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值