在Kubernetes中优雅下线微服务应用

在去年写过一篇关于微服务优雅上下线的文章,比较笼统的将了一下微服务保证优雅上下线的一些方式。但随着应用的逐渐k8s化,原有的微服务下线会存在一些问题。

下线信号钩子

之前针对优雅下线使用的还是通过信号响应的方式。一个是docker以及k8s中下线信号SIGTERM,由于这个信号会被JVM处理,所以我们写了一份下线逻辑在其shutdownHook里。另一个是我们自定义的信号SIGUSR2,这个本身就是保留给用户自定义的,通过SignalHandler,也写了一份下线逻辑注册到了应用内。

这里就出现了一些问题,一方面是代码上的问题:

  1. 不同系统对信号的支持不同,代码上没有考虑对跑在windows上机器的兼容
  2. 在钩子函数里的下线逻辑中存在抛出Exception的可能性,导致一些意想不到的问题

还有一部分是钩子函数本身的局限性:

  1. 信号可能被其他二方包或者业务代码占用了
  2. 应用可能卡死了,处理不了钩子函数或者需要很久时间
  3. 不适合做一些更精细的操作,例如循环验证确认等。这部分逻辑天生就应该放在外部而不是应用内部。

下线函数

这里我们打算使用下线函数以替换下线信号钩子,从而避免其存在的一些缺点。

那么使用下线函数的最主要的问题是,这个函数应该写在哪,是应该放在应用上?还是一个第三方的adminController上?还是应用的各自的sidecar proxy上?考虑到目前的实际现状,我们目前暂时把下线接口定在应用内sdk上:

  1. 应用下线接口在很早的sdk版本就提供了,普及度很高。这是不重新优化原来shutdownHook逻辑最主要的原因。
  2. 目前k8s化大概不到一半,还有大部分是kvm机器,不是k8s在没有pod的加持下搞sidecar container,运维上就会有较高的成本。
  3. 不做在第三方adminController的原因,一个是下线接口已经存在了,另一个是在应用内会更容易进行bean的销毁,而不仅仅是从注册中心反注册。

函数提供方确定了,函数调用方其实不用过多思考。在目前kvm机器部署方式下,调用方由cicd系统担任。而在我们重点关注的k8s方式下,k8s本身提供了preStop的扩展点,目前提供了'HTTPGET'与'EXEC'两种模式来进行pod退出逻辑的扩展。使用方式如下:

spec:
  contaienrs:
  - name: soa-test
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh","-c","/preStop.sh"]

这样我们将下线逻辑都写在preStop.sh中即可,preStop.sh中直接调用应用的下线接口即可。

除此之外我们还做了一个二次确认的操作,考虑到调用应用下线接口也不能保证实例就从注册中心反注册了(例如与信号钩子同样的问题,接口也卡死了),我们通过调用第三方adminController的接口,去注册中心验证一下实例是否真正下线,如果实例还在,那么表示应用的下线接口可能出现了些幺蛾子,直接通过adminController去从注册中心摘除实例。

k8s的动态pod ip

在目前k8s与微服务框架结合的情况来看,我们微服务应用的服务注册还是基于ip的,也就是说在注册中心上我们看到的是服务的pod ip。关于服务间通信的问题,我们使用了nodeport打通了同k8s集群下pod间的网络,使用node配置路由的方式打通了外部节点与pod间的网络。

那么这种基于pod ip的方式对于微服务上下线又有什么影响。我们知道pod ip都是动态的,每次重启后ip都会变化。这对于基础的服务注册与发现并没有什么影响,通过注册中心的通知机制可以做到动态变更。但是对于微服务的服务配置来说,就会出现问题了。我们这里讲的服务配置指对单个实例的配置,例如实例的权重等等。目前我们对于实例配置的生命周期是完全独立于实例自身的。也就是说,你在微服务ops平台上对实例所做的配置重启之后仍然保留。那么我们对于实例的区分是基于ip的,而pod实例的ip确是动态的。于是,可以预想的是,随着应用pod的不断重启,导致残留的无用实例配置越来越多,注册中心做消息通知时推送的消息变得很大,应用内对于这些消息的存储对象也变得很大,最坏甚至可能导致应用crush或者大范围的网络问题。

解决这个问题最简单的方式是在微服务下线的时候,判断一下如果是k8s网段的ip,则清除对pod所做的实例配置,这段逻辑同样通过调用接口完成。至于重启后恢复之前的实例配置,在目前我们无状态的应用部署模式,以及基于pod ip的微服务&k8s情况下暂时无法做到。

那么问题又来了,清除实例配置的这个接口应该放在哪呢?仍然考虑到不对业务应用做入侵(升级微服务sdk),我们把清除实例的配置还是放在了adminController,与实例下线的二次确认一块儿。

目前的下线方案

目前在k8s中微服务的整体下线逻辑如上图所示,一眼看去就有很多不合理的地方, shutDown和preStop接口放在两个地方,需要调用两次实在有点蠢,并且放接口在集中式的adminController上可能会引起性能问题等。虽然我为这个找了许多借口,例如实例上的下线接口方便应用内bean销毁,preStop接口放在adminController上符合了二次确认的语义,并能避免应用内本身接口的卡死。但其实更多是因为考虑到不需要业务应用做改动(升级微服务sdk),这是我们目前首要考虑的。整体来说,综合考虑目前的k8s化进度、k8s中服务注册的方式、目前业务应用微服务sdk的离散程度以及对业务应用的侵入性,这可能是比较现实的一套方案了。

一些细节

在第10步额外进行了一定时间的sleep,是为了处理那些正卡在网络传输途中的请求,这里我们默认sleep的时间就是微服务默认的接口超时时间。

理想的下线方案

目前下线方案的很多不合理以及妥协之处,主要还是囿于k8s化的进度。当应用全部都用k8s部署之后,配合sidecar container,我们理想的下线方案会变成下面这样:

可以看到基本上微服务下线已经和adminController没什么关系了,所有的下线逻辑通过k8s的preStop通知到与应用app containter同pod的sidecar container进行执行,同时完成配置清除与二次确认等操作。这种方案避免了之后可能出现的性能隐患。并且。所有关于下线的逻辑都写在了一个接口里面,更加简洁。最重要的是,这种方案仍然可以避免对业务应用产生入侵,毕竟大家都不太愿意升级sdk。

Comments
Write a Comment