美文网首页
基于 RabbitMQ 实现 Eureka 服务平滑灰度发布

基于 RabbitMQ 实现 Eureka 服务平滑灰度发布

作者: you的日常 | 来源:发表于2022-06-11 10:45 被阅读0次

    前言

    前段时间一位大龄程序来公司面试,已经是做到技术 leader 的级别,面试开始他比较自豪地向我们介绍的设计技术架构、缓存设计、业务设计等等,他说该项目是他一手打造起来的。在服务高可用方面,声称可以做到 4 个 9。

    因为也是 Spring Boot、Spring Cloud 这套比较流行的东西。�因为我们也是用 Spring Cloud 全家桶,但是有些问题我们还没有解决的,一听到对方说 4 个 9。顿时来了兴趣,准备膜拜一下大神怎么解决高可用问题的。

    他说用是 Eureka、Ribbon 这套机制实现高可用,我们问题是 Eureka、Ribbon 怎么实现高可用的,有什么弊端。对方语塞,开始东拉西扯,大失所望。

    可能有些同学不知道,在使用 Eureka、Ribbon 时候,由于 Eureka、Ribbon 内部的缓存机制。会导致服务上线或者下线的时候会出现服务不可用的情况,极端情况下会出现 90 秒服务不可能。在没有解决这些问题的前提下,使用 Eureka、Ribbon 搭建微服务应用是不可能会有 4 个 9 的。

    对于这种情况,官方也没有给具体的解决方案。在没有解决这个问题之前,很多公司也许会跟我们采取同样的做法,就是在晚上流量比较少情况停服发布。但是试下一想,每次发布都只能在 23 点以后发布,关闭 SLB,停机,执行 SQL,起服务。十几个服务,验证功能,一顿操作一下,至少要搞到两三点。这对于整个开发团队来说都是一种负担。

    本文将分享是如何解决 Eureka、Ribbon 组件使用上的弊端。

    Eureka 注册,服务发现机制原理

    相信熟悉微服务架构的人,�都知道服务注册与发现的作用。在成百上千个微服务中,我们必须需要一个中心来通知生产者与消费者的服务状态变化,以便我们在微服务架构中理清我们的服务调用关系。

    经过微服务架构多年的发展,出现很多种注册中心,如 Naco、Eureka、etc、ZooKeeper 等等,其实大体思想都一样。在服务启动时候向注册中心发送消息告诉注册中心我已经 ready,可以被调用了,然后再持续运行过程通过心跳的方式向注册中心汇报自身的状态。

    注册中心收到服务注册信息后,向调用方推送或者调用方主动拉取需要调用的服务的状态信息。在这个过程中其实并没有很高深的理论与思想。注册中心与各个服务的交互方式无非就是长连或者短连。

    eureka 注册与拉取

    服务注册与续约机制,缓存刷新

    在服务环境完全启动完成之后,集成了 eureka-client 的服务会实例化 com.netflix.discovery.DiscoveryClient实例,DiscoveryClient 实例对 Eureka 客户端来说至关重要,各种线程池初始化、服务注册、续约、刷新缓存都在这个对象完成。DiscoveryClient 有个强大的构造方法,在初始化的构造三个至关的重要的线程池。

    //  线程调度器
    private final ScheduledExecutorService scheduler;
    // 心跳线程池 ,主要用于注册与续约
    private final ThreadPoolExecutor heartbeatExecutor;
    // 本地缓存刷新线程池
    private final ThreadPoolExecutor cacheRefreshExecutor;
    
    

    在构造方法里面同时调用一个重要方法 initScheduledTasks

    private void initScheduledTasks() {
           ......
            if (this.clientConfig.shouldFetchRegistry()) {
               ......
               //  这里用 cacheRefreshExecutor 线程池启动定时任务来定时刷新缓存的操作
                this.scheduler.schedule(new TimedSupervisorTask("cacheRefresh", this.scheduler, this.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.CacheRefreshThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
            }
    
            if (this.clientConfig.shouldRegisterWithEureka()) {
              .......
              //  这里使用了 heartbeatExecutor 线程池来来启动任务进行注册与续约的操作
                this.scheduler.schedule(new TimedSupervisorTask("heartbeat", this.scheduler, this.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.HeartbeatThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
             ..... 
            } else {
                logger.info("Not registering with Eureka server per configuration");
            }
    
        }
    
    

    注册与续约的时候的线程是 HeartbeatThread

     private class HeartbeatThread implements Runnable {
            private HeartbeatThread() {
            }
    
            public void run() {
                if (DiscoveryClient.this.renew()) {
                    DiscoveryClient.this.lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
                }
            }
        }
    
    

    到这里我们基本明白注册与续约都是用了 DiscoveryClient 对象的 renew 方法。

    同理我们看到缓存刷新使用的是 CacheRefreshThread 线程:

    class CacheRefreshThread implements Runnable {
            CacheRefreshThread() {
            }
            public void run() {
                DiscoveryClient.this.refreshRegistry();
            }
        }
    
    

    由代码可以看出,刷新 Eureka 客户端的缓存是通过 DiscoveryClient 对象的 refreshRegistry 方法实现的。

    Ribbon 的负载负载均衡策略

    在使用 Spring Cloud 全家桶的 Ribbon 做负载均衡时候,Ribbon 不同于 F5、Nginx 等等通过软件或者硬件做负载均衡,Ribbon 直接就在应用端做负载均衡。Ribbon 自己实现负载均衡器,根据一定规则,从 Ribbon 的本地缓存的服务列表里面选择服务进行调用。

    值得注意的是,Ribbon 给每个服务都初始化了一个 Spring 容器。Eureka 的缓存列表是通过定时任务去注册中心拉取,Ribbon 的本地服务缓存列表则是通过定时任务通过 Eureka 的缓存列表同步过来。

    打开 org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration 在这个配置类里面,可以看到定义了各种 Ribbon 负载均衡需要的各种对象。其中有定义了默认的负载均衡规则,默认的拉取服务列表的策略。

        @Bean
        @ConditionalOnMissingBean
        public IRule ribbonRule(IClientConfig config) {
            if (this.propertiesFactory.isSet(IRule.class, this.name)) {
                return (IRule)this.propertiesFactory.get(IRule.class, config, this.name);
            } else {
                ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
                rule.initWithNiwsConfig(config);
                return rule;
            }
        }
    
    

    上面的代码给我们定义了 Ribbon 默认的负载均衡策略,在我们没有初始化其他实现 IRule 类的时候执行。

    Ribbon 主要实现了一下几种负载均衡策略:

    • RoundRobinRule:轮询策略
    • RandomRule:随机策略
    • AvailabilityFilteringRule:可用过滤策略
    • WeightedResponseTimeRule:响应时间权重策略
    • RetryRule:轮询失败重试策略
    • BestAvailableRule:并发量最小可用策略
    • ZoneAvoidanceRule:根据 server 所在区域的性能和 server 的可用性

    当然,我们也可以根据业务要求自定义负载均衡规则。

    如下代码,Ribbon 定义了默认的拉取服务列表的方式:

        @Bean
        @ConditionalOnMissingBean
        public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
            return new PollingServerListUpdater(config);
        }
    
    

    Ribbon 实现了两种拉取服务的方式:

    • PollingServerListUpdater:通过线程池定时任务的方式每隔 30s 从 Eureka 缓存拉取
    • EurekaNotificationServerListUpdater:通过监听 Eureka 的缓存更新事件,当 Eureka 客户端从注册中心拉取服务列表的时候,同时同步到 Ribbon 的缓存列表中。上面两个类都是 ServerListUpdater 的实现类,都实现了 ServerListUpdate 的 start 方法。而 start 方法会在 DynamicServerListLoadBalancer(Ribbon 负载均衡器)初始化的时候的被调用。下面可以看下 start 方法的代码实现:
    public class EurekaNotificationServerListUpdater implements ServerListUpdater {
         //......省略代码
        public synchronized void start(final UpdateAction updateAction) {
            if (this.isActive.compareAndSet(false, true)) {
               //  初始化 Eureka 时间监听器
                this.updateListener = new EurekaEventListener() {
                    public void onEvent(EurekaEvent event) {
                      // ......省略代码
                    }
                };
                //......省略代码
                // 把监听器注册到 Eureka 的缓存更新事件中
                this.eurekaClient.registerEventListener(this.updateListener);
            } else {
                logger.info("Update listener already registered, no-op");
            }
            ......省略代码
        }
    
    
    public class PollingServerListUpdater implements ServerListUpdater {
      //...... 省略代码
        public synchronized void start(final UpdateAction updateAction) {
            if (this.isActive.compareAndSet(false, true)) {
            // 初始化线程
                Runnable wrapperRunnable = new Runnable() {
                    public void run() {
                        if (!PollingServerListUpdater.this.isActive.get()) {
                            if (PollingServerListUpdater.this.scheduledFuture != null) {
                                PollingServerListUpdater.this.scheduledFuture.cancel(true);
                            }
                        } else {
                            try {
                                updateAction.doUpdate();
                                PollingServerListUpdater.this.lastUpdated = System.currentTimeMillis();
                            } catch (Exception var2) {
                                PollingServerListUpdater.logger.warn("Failed one update cycle", var2);
                            }
                        }
                    }
                };
                //  启动定时任务更新 Ribbon 缓存
                this.scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(wrapperRunnable, this.initialDelayMs, this.refreshIntervalMs, TimeUnit.MILLISECONDS);
            } else {
                logger.info("Already active, no-op");
            }
        }
    
    

    Eureka、Ribbon 的缓存机制

    通过上面的分析,我们所了解到有两个地方的缓存,一是 Eureka 客户端缓存,二是 Ribbon 负载均衡器的缓存。

    这些缓存机制是否都是必须存在的呢?很明显,这些缓存机制都是有存在的必要的,而且是非常合理的。因为你的服务状态不可能是时时变化的。在 Eureka、Ribbon 这两个组件中还有 eureka-server 也是有缓存中的。加起来三个地方有缓存。

    eureka-server 中用 guava 定义了 eadWriteCacheMapreadOnlyCacheMap 两个缓存。当我们的服务注册到 eureka-server,服务的各类元数据信息会先存储在 readWriteCacheMap,然后定时任务每隔 30s 同步到 readOnlyCacheMap 中,所以服务提供者刚注册到注册中心,服务调用者是拉取不到的。

    综上各级缓存,我们可以看下图比较直观:

    相关文章

      网友评论

          本文标题:基于 RabbitMQ 实现 Eureka 服务平滑灰度发布

          本文链接:https://www.haomeiwen.com/subject/usytgktx.html