作者:罗健文 李来 云服务开发工程师
在生产场景中部署的服务提供者常因业务升级或其他场景需要下线和上线的部署操作,本文总结了应用在上下线过程中会遇到的典型问题,并思考在虚机和容器场景该如何处理这些问题,规避该过程中可能出现的服务消费者的请求失败,实现应用的优雅上下线。
一、微服务应用上/下线发布过程中存在的问题
在应用上下线发布过程中,如何做到流量的无损上/下线,是一个系统能保证SLA的关键。如果应用上下线不平滑,就会出现短时间的服务调用报错,比如连接被拒绝、请求超时、没有实例和请求异常等问题。
1.1上线过程中的问题
在应用上线发布过程中,由于过早暴露服务,实例可能仍处在JVM JIT编译或者使用的中间件还在加载,若此时大量流量进入,可能会瞬间压垮新起的服务实例。我们在实际场景中,曾经遇到provider服务启动后,但是数据库连接出现异常,未做好启动前的资源准备,导致该provider服务在注册中心暴露后DB异常还未修复,无法正常提供被consumer调用的能力,导致大量请求异常返回。如下图日志所示,应用初始化时,DB连接失败(该服务对DB是弱依赖)。
1.2下线过程中的问题
在应用下线过程中,服务消费者感知服务提供者下线有延迟,在一段时间内,被路由到已下线服务提供者实例的请求都抛连接被拒绝异常。其次服务实例在接收到SIGKILL信号时,会立即关闭,但是这时候可能在请求队列中存在一部分请求还在处理,如果立即关闭这些请求都会损失掉。实际应用中,我们在环境上部署了provider的唯一一个实例,该服务被consumer调用,然后再执行kill -9 强杀应用provider的唯一实例后,服务进程实际上已经被终止,但是服务的注册信息还会在注册中心(该场景使用的是ServiceComb)保留一段时间,未及时清除,如下图所示。若此时消费者服务consumer调到该实例会报连接拒绝错误。因为消费者consumer服务还能发现该实例,获取其IP和端口尝试去调用,但是该provider服务实例其实已经被销毁了。
二、如何处理应用上/下线问题
那么有哪些优化措施,可以减少应用上/下线中流量的损失?
2.1 处理应用上线问题
应用上线发布主要问题是:其中一个原因是注册太早,过早的暴露了服务;另一个原因是一些应用初始化缓慢,若遇到大量流量,应用容易宕机。可以采取以下优化措施:
-
延迟注册:微服务应用可以采用延迟注册的方式,即在应用启动之后一定时间再进行注册。这样可以确保应用完全就绪后再注册,避免了服务未就绪就被外部访问的情况。
-
健康检查:微服务应用可以实现健康检查接口,通过该接口可以检查服务是否就绪。注册中心可以通过定期调用该接口来判断服务是否可以对外提供服务,从而避免了服务未就绪就被外部访问的情况。
-
预热:对新实例进行预热,而不是突然将所有流量转移到新实例上,从而避免新实例遇到大量流量,应用容易宕机的情况。
-
启动优化:对于整个服务启动的过程,可以进行一些优化措施,比如减少不必要的依赖、调整启动顺序等,从而加快服务启动速度。
2.2 应用合理的上线过程
合理的应用上线大致分为这样一个过程:当应用启动后,通过设置延迟注册时间(服务对外暴露的时间)确保应用多久后可提供服务,其次可依赖平台检查服务的就绪状态(比如K8S的就绪探针)确保服务对外提供服务为就绪状态,然后通过预热对刚启动应用进行保护,确保流量慢慢进入刚启动的应用,最后流量逐渐增到正常情况。
2.3 处理应用下线问题
应用下线过程最主要问题是:消费者应用无法及时感知到注册中心列表的刷新,导致可能还有新流量访问下线应用。可以采取以下优化措施:
-
减少注册中心缓存时间:将注册中心中服务列表的缓存时间缩短,可以使消费者应用更快地获取到服务列表的最新信息。这样可以减少因服务列表缓存而导致的访问下线应用的流量。
-
实时性优化:在服务消费者和注册中心之间使用长连接、实时通知等机制,从而能够实时获取注册中心中服务列表的变化。
-
实现熔断机制:在消费者应用中实现熔断机制,当某个服务实例出现故障或不可用时,可以快速切换到其他可用的服务实例。这样可以避免将流量发送到已下线的应用程序上,并确保消费者应用的可用性。
2.4 应用合理的下线过程
合理的应用下线大致分为这样一个过程:当应用接受到外部的关闭(停止服务)请求后,不能在接收新的业务请求,但是会存在一些正在处理的业务请求,需等这些请求处理完后再销毁应用使用的资源,最后就可以通知主进程退出。
三、应用下线注意点
针对应用下线在虚机场景和容器场景需要关注一些注意点。
3.1 虚机场景
当我们要关闭虚拟机应用时,我们一般会使用ps -ef | grep xxx查找到进程ID,然后再执行kill -9 PID操作。
kill 命令使用科普:
-
kill -9, 系统会发出SIGKILL(9)信号,由操作系统内核完成杀进程操作,该信号不允许忽略和阻塞,应用程序会立即终止(强制杀死)。
-
kill -15,默认使用信号,系统向应用发送SIGTERM(15)信号,给目标进程一个清理善后工作的机会是一种优雅终止进程的方式,告诉进程需要停止运行并开始清理资源。
因为kill -9 PID会强制杀死应用,以合理的应用下线流程看,应需处理完相关旧业务请求,清理相关资源后再退出进程,所以当要关闭虚拟机应用时,请执行kill PID——以优雅的方式停止运行。
3.2 容器场景
Kubernetes目前是业界容器编排领域的事实标准,业界一般默认都是用K8S来管理容器。K8S提供了Pod优雅退出机制,允许Pod在退出前完成一些清理工作。preStop会先执行完,然后K8S才会给Pod发送TERM信号。在容器场景利用K8S提供的preStop机制,配合延迟下线API使用,这样就能保证流量的无损下线。
...
spec:
containers:
- name: lifecycle-demo-container
image: nginx
lifecycle:
preStop:
exec:
command: ["/bin/sh","-c","to do xxx; do sleep 30; done"]
...
(1)为什么容器应用(K8S环境)要配置preStop?首先要介绍一下Pod的终止过程。
参考:4.5.5 Pod 的终止过程 · Kubernetes Documentation
-
用户发送删除 Pod 对象的命令。
-
API 服务器中的 Pod 对象会随着时间的推移而更新,在宽限期内(默认为30秒),Pod被视为“dead”。
-
将 Pod 标记为 “Terminating” 状态。
-
(与第3步同时运行)kubelet 在监控到 Pod 对象转为 “Terminating” 状态的同时启动 Pod 关闭程序。
-
(与第3步同时运行)端点控制器监控到 Pod 对象的关闭行为时将其从所有匹配到此端点的 Service 资源的端点列表中移除。
-
Pod 对象中的容器进程收到 TERM 信号。
-
如果当前当前 Pod 对象定义了 preStop 钩子处理器,则在其标记为 “Terminating” 后即会以同步的方式启动执行;如若宽限期结束后,preStop 仍未执行结束,则第2步会被重新执行并额外获取一个时长为2秒的小宽限期。
-
宽限期结束后,若存在任何一个仍在运行的进程,那么 Pod 对象即会收到 SIGKILL 信号。
-
kubelet 请求 API Server 将此 Pod 资源的宽限期设置为0从而完成删除操作,它变得对用户不在可见。
默认情况下,所有删除操作的宽限期都是30秒,不过,kubectl delete 命令可以使用“--grace-period=”选项自定义其时长,若使用0值则表示直接强制删除指定的资源,不过,此时需要同时为命令使用 “--force” 选项。
从上述Pod终止过程的时序图可知,关闭Pod流程(关注红色框),给Pod内的进程发送TERM信号(即kill, kill -15),如果配置了preStop钩子也会同时处理,最后宽限期结束后,若存在任何一个仍在运行的进程,那么Pod对象即会收到SIGKILL(kill-9)信号。
(2) 存在这样一种情况Pod中的业务进程接受不到SIGTERM信号
存在这样一种情况Pod中的业务进程接受不到SIGTERM信号(而且没有配置preStop钩子),等待一段时间业务进程直接被SIGKILL强制杀死了。
为什么业务进程接受不到SIGTERM信号?
通常都是因为容器启动入口使用了 shell,比如使用了类似 /bin/sh -c my-app 或 /docker-entrypoint.sh 这样的 ENTRYPOINT 或 CMD,这就可能就会导致容器内的业务进程收不到SIGTERM信号,原因是:
-
容器主进程是shell,业务进程是在shell中启动的,成为了shell进程的子进程。
-
shell 进程默认不会处理 SIGTERM 信号,自己不会退出,也不会将信号传递给子进程,导致业务进程不会触发停止逻辑。
-
当等到 K8S 优雅停止超时时间 (terminationGracePeriodSeconds,默认30s),发送SIGKILL强制杀死shell及其子进程。
(3) 如何解决上述Pod中的业务进程接收不到SIGTERM信号问题
-
配置preStop钩子(K8S场景),处理退出前完成一些清理工作,比如使用无损上下线插件的应用服务需在停止前通知实例进行下线。
-
如果可以的话,尽量不使用 shell 启动业务进程。
-
如果一定要通过 shell 启动,比如在启动前需要用 shell 进程一些判断和处理,或者需要启动多个进程,那么就需要在 shell 中传递下 SIGTERM 信号了。
参考:为什么我的容器收不到 SIGTERM 信号 ? | kubernetes 学习笔记
所以容器应用(K8S环境)要配置preStop,在停止前通知实例进行下线,加了一层防护,保证Pod中的业务能优雅的结束。
四、Sermant如何解决应用上/下线问题
针对应用上下线发布过程中的问题,Sermant插件提供预热和延迟下线机制,为应用提供无损上下线的能力。预热是无损上线的核心机制,延迟下线是无损下线的核心机制,而且为了无损上线,还做了延迟注册机制。
4.1上线问题的解决方式
-
延迟注册: 若服务还未完全初始化就已经注册到注册中心提供给消费者调用,很有可能因资源为加载完成导致请求报错。可以通过设置延迟注册,让服务充分初始化后再注册到注册中心对外提供服务。
-
预热:是基于客户端实现的,当流量进入时,Sermant会动态调整流量,根据服务的预热配置,对流量进行动态分配。对于开启服务预热的实例,在刚启动时,相对于其他已启动的实例,分配的流量会更少,流量将以曲线方式随时间推移增加直至与其他实例近乎持平。目的是采用少流量对服务实例进行初始化,防止服务崩溃。 4.2下线问题的解决方式
上图描述了Sermant是如何解决服务下线问题的: 0. 微服务应用consumerA、providerA、consumerB、providerB携带Sermant启动,并将相关ip:port等信息注册到注册中心;
-
微服务应用consumerA可以正常调用providerA和providerB;
-
若要重启providerA,providerA会标记自身将下线(通知注册中心将下线),并开始统计请求确保当前请求已全部处理完成;
-
providerA会通知其上游应用其自身的下线信息;
-
consumerA接受到providerA下线信息后,将其从缓存实例列表移除;
-
providerA在处理完当前的所有请求后,即可重启。
总的来说,Sermant对于服务下线的机制概括为:
-
延迟下线: 即对下线的实例提供保护,插件基于下线实时通知+刷新缓存的机制快速更新上游的实例缓存,同时基于流量统计的方式,确保即将下线的实例尽可能的将流量处理完成,最大程度避免流量丢失。提供了延迟下线API,方便在K8S环境中配置preStop。 http://127.0.0.1:16688/\$\$sermant\$\$/shutdown
-
流量统计: 为确保当前请求已全部处理完成,在服务下线时,Sermant会尝试等待30s(可配置),定时统计和判断当前实例请求是否均处理完成,处理完成后最终下线。
Sermant优雅上下线能力的详细介绍和使用见:无损上下线 | Sermant
五、总结
Sermant插件为微服务应用提供无损上下线的能力,若要下线应用,针对虚拟场景,请使用kill PID;针对容器场景(K8S环境),请配置preStop钩子。
结束语
Sermant 作为专注于服务治理领域的字节码增强框架,致力于提供高性能、可扩展、易接入、功能丰富的服务治理体验,并会在每个版本中做好性能、功能、体验的看护,广泛欢迎大家的加入。
-
Sermant 官网:https://sermant.io
-
GitHub 仓库地址:https://github.com/huaweicloud/Sermant
-
扫码加入 Sermant 社区交流群