微服务滚动发布方案 | 优雅且平滑

业务背景

众所周知,在我们迭代业务需求和Fix Bug时往往需要重启我们的服务,在高并发场景下,如果还像N年前一样,通过上机操作敲Linux命令执行Kill操作,在分布式环境下显然会让你敲断手指,并且这种方式重启还会影响用户的操作,导致一些线上事故,所以,在微服务、分布式系统中,平滑滚动发布无疑是非常重要的(主打的就是一个丝滑)。

SpringCloud微服务架构导致发布的一些弊端

1.Nacos组件导致的一些弊端

众所周知,Nacos利用心跳机制每隔一段时间(可配置,defalut:30s)服务端与客户端都会互相发送心跳包,当服务端接收到的客户端心跳包有异常情况时,Nacos服务端会剔除客户端在服务端中的注册信息,意味着这个服务是不可用状态,这种方式其实是有弊端的,这其实也是心跳机制的弊端,比如我服务A重启了,Nacos服务端并没有马上感知服务A的异常情况,这个时候还认为其是可用状态,这个时候如果有流量打进来,通过负载均衡很明显会有流量进入到服务A,无法即时感知客户端异常,就是它的最大弊端。

2.SpringCloud Gateway组件导致的一些弊端

SpringCloud Gateway组件对比以前的Zuul网关,在性能和吞吐提升了很多,主要是因为它是基于Spring5.0+SpringBoot2.0+Spring Reactor,没错看到Reactor你就会想到性能强悍的Netty,SpringCloud Gateway正是用了这一高性能通信框架。

提到网关,第一我们要想到的就是负载均衡机制,之前面试过不少后端的兄弟,你们的系统怎么做的负载均衡,他们中多数人的回答是使用了Nginx的负载均衡机制,我...,咱不废话,接着说SpringCloud Gateway的负载均衡机制,当使用Ribbon作为负载中间件时会使用定时线程从Nacos注册中心拉取服务列表然后放到Gateway服务的本地缓存中,拉取时间默认也是30s,同样的也是无法及时感知Nacos注册中心的服务异常情况,所以其实很多人在使用SpringCloud Gateway中会经常出现一些500,503等问题,下面我将针对这些组件的弊端做一个完善解决方案。

针对Nacos与GateWay弊端的解决方案

Nacos与Gateway之间即时感知弊端的优化切入思路

  1. 在程序停止前通过Nacos上下线事件监听回调操作中主动调用Nacos服务下线操作,让Nacos服务端能即时剔除重启的客户端,避免流量流入
  2. SpringCloud GateWay组件中可以重写其负载均衡策略,也就是在服务上下线回调监听事件中刷新在网关服务本地缓存中的Nacos服务信息,就好比我们业务中更新了数据库要刷一遍缓存一般,操作起来没啥难度。

线上优化实战

  • 问题简介

根据线上日志发现,gateway网关在服务重启时会有调用失败的现象,调用失败可能会导致一些数据的丢失甚至引发一些金钱、充值相关的数据有误,尤其在并发越高的情况下,这种现象表现得越发严重,鄙人曾经的项目中有因为此等原因导致了服务雪崩的情况,所以针对此网关进行一个优化。

  • 优化思路可行性分析
  1. 第一种方式 重写gateway的负载均衡器,从可行性来看问题不大,但是我们的主要问题是针对服务上下线无法及时感知而优化,并不是针对其负载均衡器进行深度优化,所以此方式虽可实现,但开发、结果成本可能稍大,不优先采取
  2. 第二种方式 分析Nacos与Gateway之间的关联关系可知,我们可以通过Nacos上下线的事件监听回调来操作Gateway 令其刷新。
  • 代码
   @Slf4j
   @Component
   public class ApplicationEventListener implements ApplicationListener {

       @Value("${spring.application.name}")
       private String applicationName;

       @Value("${server.port}")
       private int port;

       @Autowired
       private DiscoveryClient discoveryClient;
       @Autowired
       private NacosAutoServiceRegistration nacosAutoServiceRegistration;


       @Override
       public void onApplicationEvent(ApplicationEvent applicationEvent) {
           if (applicationEvent instanceof ApplicationStartedEvent) {
               log.info("【{}】【{}】应用启动", IpUtil.getIntranetIp(), applicationName);

               Executors.newSingleThreadExecutor().execute(() -> checkDiscoveryClient());

           } else if (applicationEvent instanceof ContextClosedEvent) {
               log.info("【{}】【{}】程序已停止...", IpUtil.getIntranetIp(), applicationName);
               ApplicationCheckUtil.setSystemIsNormal(false);
               //优雅停机
               nacosAutoServiceRegistration.stop();
               SpringApplication.exit(SpringContextUtil.getApplicationContext());
               ((ConfigurableApplicationContext) SpringContextUtil.getApplicationContext()).close();
           }
       }

       private void checkDiscoveryClient() {
           try {
               List<ServiceInstance> serviceInstanceList;
               AtomicBoolean currentInstanceHasRegister = new AtomicBoolean(false);
               while (true) {
                   serviceInstanceList = discoveryClient.getInstances(applicationName);
                   serviceInstanceList.forEach(serviceInstance -> {
                       log.info("host:{} | port:{} | serviceId:{}", serviceInstance.getHost(), serviceInstance.getPort(), serviceInstance.getServiceId());
                       if (IpUtil.getIntranetIp().equals(serviceInstance.getHost()) && serviceInstance.getPort() == port) {
                           currentInstanceHasRegister.set(true);
                       }
                   });

                   if (currentInstanceHasRegister.get()) {
                       log.info("当前服务实例已成功注册到Nacos中...");
                       break;
                   }

                   TimeUnit.SECONDS.sleep(1);
               }
           } catch (Exception e) {
               log.error("检测服务注册异常...");
           } finally {
               DingTalkUtil.send(DingTalkType.PUBLISH_NOTICE, String.format("【%s】【%s】应用启动成功", IpUtil.getIntranetIp(), applicationName));
               ApplicationCheckUtil.setSystemIsNormal(true);
           }
       }


   }

主要核心代码就一行:nacosAutoServiceRegistration.stop();

线上优化结果指标

通过此优化 频繁重启了几次服务,并且用测试工具一直并发调用重启的服务,并未发现有调用异常的情况,至此 服务端真正意义上的平滑重启稍微提升了一个层次,但还不够,请接着往下看。

K8S滚动发布更新方案

用户请求服务过程

服务重启预想过程

滚动发布流程

k8s参数配置

服务在滚动更新时,deployment控制器的目的是:给旧版本(old_rs)副本数减少至0、给新版本(new_rs)副本数量增至期望值(replicas)。大家在使用时,通常容易忽视控制速率的特性,以下是kubernetes提供的两个参数:

  1. maxUnavailable:和期望ready的副本数比,不可用副本数最大比例(或最大值),这个值越小,越能保证服务稳定,更新越平滑;
  2. maxSurge:和期望ready的副本数比,超过期望副本数最大比例(或最大值),这个值调的越大,副本更新速度越快。
spec:
  ---副本数量
  replicas: 5
  selector:
    matchLabels:
      app: user-service
  minReadySeconds: 120
  strategy:
    ---滚动更新方式
    type: RollingUpdate
    rollingUpdate:
      ---超过期望副本数最大比例(或最大值)
      maxSurge: 1
      ---不可用副本数最大比例(或最大值)
      maxUnavailable: 3

至此,一套完整高可用、稳定的微服务滚动更新方案已完成,基本已经能够满足目前的使用情况。

后续优化展望

  1. 增加灰度发布方案
  2. 增加长连接服务的平滑发布方案(涉及到的知识点很干,大家敬请期待)
#从0到1千万级直播项目#
从0-1开发千万级直播项目 文章被收录于专栏

文章内容源自本人所在互联网社交企业实战项目,分享、记录从0-1做一个千万级直播项目,内容包括高并发场景下技术选型、架构设计、业务解决方案等。

全部评论

相关推荐

你背过凌晨4点的八股文么:简历挂了的话会是流程终止,像我一样
点赞 评论 收藏
分享
04-11 00:51
已编辑
门头沟学院 Java
先说一下楼主的情况:双非本大三,两段实习,javaer,想要找一个暑期大厂offer,努力了两个月,三月份每天的状态就是算法,八股,项目,四月份更是一个面试没有,最终还是没有结果,心碎了一地。期间面了一些中小厂,大厂只有腾讯约面,其他大厂都投了一遍,但是还是石沉大海。再看一下楼主的面试结果吧,就不说ttl了腾讯s3:三面挂csig:一面挂teg:三面挂wxg:一面挂没错,面了八次腾讯,两次三面挂,当时真的心都碎了。其他中小厂都有面,有的没过,有的oc,但是都没有去。其他大厂投了简历,但是不是简历挂,就是测评挂,都说今年行情好很多,各大厂都扩招,可是问题出在那里呢?学历背景吗?实习经历吗?还是简历不够好看?依稀记得,从年初七就离开了家里,回到学校,早早准备面试,当时自己认为凭借着自己的两段实习经历,以及大二就开始准备的八股算法,拿大厂offer不是问题,但是还是不敢放松,回校的状态每天就是算法,八股,还有查看各种招聘信息,想着尽早投机会多,但是事实证明,投的早,不如投的刚刚好。当时想着,先投一些中小厂开始面试,找找面试感觉,从2.10就开始有面试了,基本都是线下面试,面试的感觉都很不错,觉得自己的状态慢慢回来了,期间也有oc一些中小厂,但是自己的目标并不在此,只是想练一下手,遂拒。后面投了腾讯的暑期实习基地,不久就约面了,第一次面这么大的厂,多少有点紧张,好在运气还不错,遇到的面试官也比较好,一直干到了三面,期间看牛客有不少说一面就挂了的,感觉自己还是比较幸运的,但是没想到倒在了三面,一周后就挂了,伤心是有的,但是想到这才刚刚开始,还有很多机会,便继续准备下一次面试了,很快,被另外一个部门捞了,一进会议,面试官没开摄像头,看网上说没开摄像头很多都是kpi,但是自己给自己打气,认为面试官只是不方便开摄像头罢了,面完,感觉良好,没问什么很难得问题,基本都答出来了,算法两道也a了一道,感觉实习不会这么严格吧?还是过了一会挂了,因为这个?还是技术不太匹配?面试过程中说搞C++的,心想,搞c++的你面我干啥?唉,这时候有点气馁,然后就接下来半个月没有面试。这时已经是三月底了,看到牛客好多人都已经陆陆续续拿到了offer,看人家的面试准备也没那么早,有0实习的,有没刷算法的,有两个面的,,,唉,反正是一言难尽啊,感觉努力没有什么意义,面试多半是看面试官的感觉,主观性很大啊,只要你技术没有太大的问题。第三次面试腾讯,面试来的比较突然,期间已经有几天没看八股什么的了,临时看了一下之前自己做的面试笔记,但是面试却异常顺利,三天闯到了三面,自己也不敢相信,三面玩感觉也良好,脑子里不得不想着一些“offer结算画面”,但是过了一会查看流程显示“流程终止”,我?哎,当时真的有苦说不出啊,也是一晚没睡。后面就逐渐开始褪去大厂梦了,看着曾经跟自己交流的牛油,朋友,认识的人,觉得他们技术不太如你,算法刷的没你多,进了大厂,但是这又如何呢?能力强不强不是你了说了,面试官说了算。也逐渐知道,不是你能力好就可以了,还得有运气,运气,运气。这个过程太累了,和自己和解吧,不用非得大厂,找个合适一点的就好,放轻松一点。今天有点心事睡不着,闲着想写一些自己的面试过程,勿喷。附上一张面试的情况,公司就不方便透露了。
怒卷的斯科特:八分运气两分实力
点赞 评论 收藏
分享
评论
1
1
分享

创作者周榜

更多
牛客网
牛客企业服务