解决Eureka-Ribbon服务优雅下线

背景

为实现产品升级不中断,准备通过服务的滚动升级确保升级阶段始终有可用服务实例,但是发现滚动升级过程中,老版本服务实例下线后,在eureka上仍然存在,因此此时会出现http 500的现象,流程如下:

在这里插入图片描述升级步骤是:
1、将原有的svc pod replica改为2,两个老版本pod
2、开始滚动升级,podA首先升级为新版本,同时在eureka上注册instanceA对应podA
3、此时被删除的老版本podB,在eureka上仍然存在,请求svc,可能被分配到instanceB,出现http 500

因此要解决的问题是,podB删除后立刻在eureka上生效。

参考这篇文章进行了验证:
https://blog.csdn.net/u010457081/article/details/88627926?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

发现按照文章中的方法实施后并没有成功,且会出现:
1、服务下线后没有立刻解绑,eureka上仍然会存在一段时间
2、原有服务重启后,新的instance在eureka上存在,但是老的instance永远不会刷新失效

最终定位是因为该配置方法中,通过@Configuration只在spring容器中new出了一个对象,而实际通过EurekaNotificationServerListUpdater进行更新时,需要将每个服务都作为一个listener进行注册和监听,所以,基于这篇文章的方法,应该在application.properties中做如下修改:

<service-name>.ribbon.ServerListUpdaterClassName=com.netflix.loadbalancer.EurekaNotificationServerListUpdater

同时,去掉@Configuration的类,重新打包运行验证,服务注册解绑功能正常。

定位过程

在遇到配置完成后不生效,甚至eureka刷新机制停止的现象,所以对整体流程进行了一次梳理:

如图,为了达到服务下线后的快速刷新效果,有3个地方的缓存更新需要关注:
1、EurekaServer内部的readWriteCacheMap/readOnlyCacheMap的更新频率(最大延迟3秒)
resonseCacheUpdateIntervalMs,其实可以考虑将readOnlyCacheMap禁用,但是这样会降低效率,因此通过缩短更新周期的方式,确保性能不下降。

2、EurekaClient从EurekaServer更新本地serverList的更新频率(理论最大延迟毫秒级)
registry-fetch-interval-seconds,决定eurekaClient多久从eurekaServer拉取一次服务列表进行更新,这里可以通过主动触发的方式将更新的延迟降到最低,当人为/监控发现服务下线后,通过反射调用fetchRegistry触发eurekaClient的缓存更新

3、ribbonClient从EurekaClient更新serverList的更新频率(理论最大延迟毫秒级)
通过网上的那篇文章配置出现问题,主要就是出现在这里,ribbonClient没有及时更新缓存甚至失去了更新缓存的能力。

ribbonClient在做服务更新的方式有两种:

1、基于定时任务的拉取服务列表,com.netflix.loadbalancer.PollingServerListUpdater
2、基于Eureka服务事件通知的方式更新,com.netflix.loadbalancer.EurekaNotificationServerListUpdater

ribbonClient默认的方式是采取第一种,定时进行更新,这种情况我们可以修改ribbon的ServerListRefreshInterval配置来缩短从eurekaClient更新的周期,这种方式的实时性不是最好的。
我这次采用的是第二种,通过事件触发进行更新,先对比一下两者的源码:

PollingServerListUpdater

    @Override
    public synchronized void start(final UpdateAction updateAction) {
        if (isActive.compareAndSet(false, true)) {
            //创建定时任务,按照特定的实行周期执行更新操作
            final Runnable wrapperRunnable = new Runnable() {
                @Override
                public void run() {
                    if (!isActive.get()) {
                        if (scheduledFuture != null) {
                            scheduledFuture.cancel(true);
                        }
                        return;
                    }
                    try {
                        //执行update操作 ,更新操作定义在LoadBalancer中
                        updateAction.doUpdate();
                        lastUpdated = System.currentTimeMillis();
                    } catch (Exception e) {
                        logger.warn("Failed one update cycle", e);
                    }
                }
            };
           //定时任务创建
            scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                    wrapperRunnable,
                    initialDelayMs, //初始延迟时间
                    refreshIntervalMs, //内部刷新时间
                    TimeUnit.MILLISECONDS
            );
        } else {
            logger.info("Already active, no-op");
        }
    }

可以看到,通过定时触发updateAction.doUpdate()方法来实现ribbonClient的更新,而updateAction只是一个接口,更新逻辑是在DynamicServerListLoadBalancer内定义的,如下:

    protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
        @Override
        public void doUpdate() {
            updateListOfServers();
        }
    };
    @VisibleForTesting
    public void updateListOfServers() {
    	List<T> servers = new ArrayList<T>();
        if (serverListImpl != null) {
            //这里进行eruekaClient的serverList与ribbonClient的缓存更新逻辑
            servers = serverListImpl.getUpdatedListOfServers();
            LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
                    getIdentifier(), servers);
            if (filter != null) {
                servers = filter.getFilteredListOfServers(servers);
                LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
                        getIdentifier(), servers);
            }
        }
        updateAllServerList(servers);
    }

getUpdatedListOfServers的逻辑是在DiscoveryEnabledNIWSServerList内实现的,如下:

private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
        List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();
        if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {
            logger.warn("EurekaClient has not been initialized yet, returning an empty list");
            return new ArrayList<DiscoveryEnabledServer>();
        }
        EurekaClient eurekaClient = eurekaClientProvider.get();
        if (vipAddresses!=null){
            for (String vipAddress : vipAddresses.split(",")) {
                // if targetRegion is null, it will be interpreted as the same region of client
                List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
......省略代码

因此,通过这部分代码的阅读,可以看到一个服务在eurekaServer上更新,对应的一个ribbonClient会有一个DynamicServerListLoadBalancer定时更新本地缓存,调用的逻辑顺序是:

PollingServerListUpdater.start() ----> DynamicServerListLoadBalancer.updateListOfServers() ----> DiscoveryEnabledNIWSServerList.obtainServersViaDiscovery()

对象的关系也是一对一对一的关系,一个服务对应一个PollingServerListUpdater以及其他对象,确保更新逻辑独立。
而PollingServerListUpdater.start()的触发是在DynamicServerListLoadBalancer的构造函数内进行,也就是当从eurekaServer上获取了服务列表后,就已经开启了对应服务实例的定时线程进行定时的刷新
通过PollingServerListUpdater中的isActive.compareAndSet(false, true)这行代码也保证了同一个服务的更新逻辑不会重复执行(通过@Configuration中的@Bean方式,就会导致每次判断都是true,所以服务无法更新)

EurekaNotificationServerListUpdater

EurekaNotificationServerListUpdater:
    @Override
    public synchronized void start(final UpdateAction updateAction) {
        if (isActive.compareAndSet(false, true)) {
          //创建Eureka时间监听器,当Eureka发生改变后,将触发对应逻辑  
          this.updateListener = new EurekaEventListener() {
                @Override
                public void onEvent(EurekaEvent event) {
                    if (event instanceof CacheRefreshedEvent) {
                        //内部消息队列
                       if (!updateQueued.compareAndSet(false, true)) {  // if an update is already queued
                            logger.info("an update action is already queued, returning as no-op");
                            return;
                        }
                        if (!refreshExecutor.isShutdown()) {
                            try {
                                //提交更新操作请求到消息队列中
                                refreshExecutor.submit(new Runnable() {
                                    @Override
                                    public void run() {
                                        try {
                                            updateAction.doUpdate(); // 执行真正的更新操作
                                            lastUpdated.set(System.currentTimeMillis());
                                        } catch (Exception e) {
                                            logger.warn("Failed to update serverList", e);
                                        } finally {
                                            updateQueued.set(false);
                                        }
                                    }
                                });  // fire and forget
                            } catch (Exception e) {
                                logger.warn("Error submitting update task to executor, skipping one round of updates", e);
                                updateQueued.set(false);  // if submit fails, need to reset updateQueued to false
                            }
                        }
                        else {
                            logger.debug("stopping EurekaNotificationServerListUpdater, as refreshExecutor has been shut down");
                            stop();
                        }
                    }
                }
            };
            //EurekaClient 客户端实例
            if (eurekaClient == null) {
                eurekaClient = eurekaClientProvider.get();
            }
            //基于EeurekaClient注册事件监听器
            if (eurekaClient != null) {
                eurekaClient.registerEventListener(updateListener);
            } else {
                logger.error("Failed to register an updateListener to eureka client, eureka client is null");
                throw new IllegalStateException("Failed to start the updater, unable to register the update listener due to eureka client being null.");
            }
        } else {
            logger.info("Update listener already registered, no-op");
        }
    }

更新逻辑与PollingServerListUpdater一样,都是通过DynamicServerListLoadBalancer进行,并且也是一个服务对应一个EurekaNotificationServerListUpdater,触发逻辑不同,EurekaNotificationServerListUpdater的触发逻辑是通过增加listener到eurekaClient的监听事件集合中,当eurekaClient的serverList发生变化时,会调用到EurekaNotificationServerListUpdater注册的DynamicServerListLoadBalancer的updateAction.doUpdate()逻辑,后面的执行内容就一样了。监听的事件是CacheRefreshedEvent,在eurekaClient中的实现逻辑是这样的:

    protected void fireEvent(final EurekaEvent event) {
        for (EurekaEventListener listener : eventListeners) {
            try {
                listener.onEvent(event);
            } catch (Exception e) {
                logger.info("Event {} throw an exception for listener {}", event, listener, e.getMessage());
            }
        }
    }

所以这种方式能够在服务接收到来自eurekaServer的第一时间同步更新ribbon

以上整个过程,通过remote debug单步跟踪梳理的,顺便说一下remote debug的过程:
1、在IDEA上建一个remote debug,参数-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=30119,同时远程服务中增加java执行参数ENV REMOTE_DEBUG=" -agentlib:jdwp=transport=dt_socket,address=8401,suspend=n,server=y "
2、如果是kubernetes环境需要单独创建svc的nodeport确保端口可联通

那么开始参考文章中的描述,按照@Configuration方式来部署实现为什么不行呢?
猜测这种方式创建后,会将单例放在spring容器进行管理,然后就出现了每次active=true的现象,导致后续监听到的服务在创建DynamicServerListLoadBalancer时,无法向eurekaClient注册自己的listener,创建DynamicServerListLoadBalancer时会先判断spring容器中是否存在serverListUpdater对象,如果有就直接用,如果没有则new一个新的
翻找代码发现创建DynamicServerListLoadBalancer的地方如下:

    public ZoneAwareLoadBalancer<T> buildDynamicServerListLoadBalancerWithUpdater() {
        if (serverListImpl == null) {
            serverListImpl = createServerListFromConfig(config);
        }
        if (rule == null) {
            rule = createRuleFromConfig(config);
        }
  //如果为空,则创建新的
        if (serverListUpdater == null) {
   //通过反射的方式,读取配置“ServerListUpdaterClassName: com.netflix.loadbalancer.EurekaNotificationServerListUpdater”创建对象
            serverListUpdater = createServerListUpdaterFromConfig(config);
        }
        return new ZoneAwareLoadBalancer<T>(config, rule, ping, serverListImpl, serverListFilter, serverListUpdater);
    }

可见@Bean让serverListUpdater在每次创建的时候都不为空,但是这部分从spring容器获取对象的代码没有找到,mark一下。

定位的过程中参考了几篇文章,还不错。
https://blog.csdn.net/zhxdick/article/details/78560993
https://blog.csdn.net/wo18237095579/article/details/83276352
https://www.jianshu.com/p/e459f43ef96d
https://juejin.im/post/5c38b768e51d451bd1663f29
http://lifeinide.com/post/2017-12-07-instant-ribbon-client-init-with-eureka/

发布了20 篇原创文章 · 获赞 0 · 访问量 1679
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览