做者:吴叶磊git
一直以来我对优雅地中止 Pod 这件事理解得很单纯:不就利用是 PreStop hook 作优雅退出吗?但最近发现不少场景下 PreStop Hook 并不能很好地完成需求,这篇文章就简单分析一下“优雅地中止 Pod”这回事儿。github
优雅中止(Graceful shutdown)这个说法来自于操做系统,咱们执行关机以后都得 OS 先完成一些清理操做,而与之相对的就是硬停止(Hard shutdown),好比拔电源。web
到了分布式系统中,优雅中止就不只仅是单机上进程本身的事了,每每还要与系统中的其它组件打交道。好比说咱们起一个微服务,网关把一部分流量分给咱们,这时:数据库
按照惯例,SIGKILL 是硬终止的信号,而 SIGTERM 是通知进程优雅退出的信号,所以不少微服务框架会监听 SIGTERM 信号,收到以后去作反注册等清理操做,实现优雅退出。api
回到 Kubernetes(下称 K8s),当咱们想干掉一个 Pod 的时候,理想情况固然是 K8s 从对应的 Service(假若有的话)把这个 Pod 摘掉,同时给 Pod 发 SIGTERM 信号让 Pod 中的各个容器优雅退出就好了。但实际上 Pod 有可能犯各类幺蛾子:安全
所以,K8s 的 Pod 终止流程中还有一个“最多能够容忍的时间”,即 grace period(在 Pod 的 .spec.terminationGracePeriodSeconds
字段中定义),这个值默认是 30 秒,咱们在执行 kubectl delete
的时候也可经过 --grace-period
参数显式指定一个优雅退出时间来覆盖 Pod 中的配置。而当 grace period 超出以后,K8s 就只能选择 SIGKILL 强制干掉 Pod 了。网络
不少场景下,除了把 Pod 从 K8s 的 Service 上摘下来以及进程内部的优雅退出以外,咱们还必须作一些额外的事情,好比说从 K8s 外部的服务注册中心上反注册。这时就要用到 PreStop Hook 了,K8s 目前提供了 Exec
和 HTTP
两种 PreStop Hook,实际用的时候,须要经过 Pod 的 .spec.containers[].lifecycle.preStop
字段为 Pod 中的每一个容器单独配置,好比:架构
spec: contaienrs: - name: my-awesome-container lifecycle: preStop: exec: command: ["/bin/sh","-c","/pre-stop.sh"]
/pre-stop.sh
脚本里就能够写咱们本身的清理逻辑。框架
最后咱们串起来再整个表述一下 Pod 退出的流程(官方文档里更严谨哦):运维
这个过程很不错,但它存在一个问题就是咱们没法预测 Pod 会在多久以内完成优雅退出,也没法优雅地应对“优雅退出”失败的状况。而在咱们的产品 TiDB Operator 中,这就是一个没法接受的事情。
为何说没法接受这个流程呢?其实这个流程对无状态应用来讲一般是 OK 的,但下面这个场景就稍微复杂一点:
TiDB 中有一个核心的分布式 KV 存储层 TiKV。TiKV 内部基于 Multi-Raft 作一致性存储,这个架构比较复杂,这里咱们能够简化描述为一主多从的架构,Leader 写入,Follower 同步。而咱们的场景是要对 TiKV 作计划性的运维操做,好比滚动升级,迁移节点。
在这个场景下,尽管系统能够接受小于半数的节点宕机,但对于预期性的停机,咱们要尽可能作到优雅中止。这是由于数据库场景自己就是很是严苛的,基本上都处于整个架构的核心部分,所以咱们要把抖动作到越小越好。要作到这点,就得作很多清理工做,好比说咱们要在停机前将当前节点上的 Leader 所有迁移到其它节点上。
得益于系统的良好设计,大多数时候这类操做都很快,然而分布式系统中异常是屡见不鲜,优雅退出耗时过长甚至失败的场景是咱们必需要考虑的。假如相似的事情发生了,为了业务稳定和数据安全,咱们就不能强制关闭 Pod,而应该中止操做过程,通知工程师介入。 这时,上面所说的 Pod 退出流程就再也不适用了。
这个问题其实 K8s 自己没有开箱即用的解决方案,因而咱们在本身的 Controller 中(TiDB 对象自己就是一个 CRD)与很是细致地控制了各类操做场景下的服务启停逻辑。
抛开细节不谈,最后的大体逻辑是在每次停服务前,由 Controller 通知集群进行节点下线前的各类迁移操做,操做完成后,才真正下线节点,并进行下一个节点的操做。
而假如集群没法正常完成迁移等操做或耗时太久,咱们也能“守住底线”,不会强行把节点干掉,这就保证了诸如滚动升级,节点迁移之类操做的安全性。
但这种办法存在一个问题就是实现起来比较复杂,咱们须要本身实现一个控制器,在其中实现细粒度的控制逻辑而且在 Controller 的控制循环中不断去检查可否安全中止 Pod。
复杂的逻辑老是没有简单的逻辑好维护,同时写 CRD 和 Controller 的开发量也不小,能不能有一种更简洁,更通用的逻辑,能实现“保证优雅关闭(不然不关闭)”的需求呢?
有,办法就是 ValidatingAdmissionWebhook。
这里先介绍一点点背景知识,Kubernetes 的 apiserver 一开始就有 AdmissionController 的设计,这个设计和各种 Web 框架中的 Filter 或 Middleware 很像,就是一个插件化的责任链,责任链中的每一个插件针对 apiserver 收到的请求作一些操做或校验。举两个插件的例子:
DefaultStorageClass
,为没有声明 storageClass 的 PVC 自动设置 storageClass。ResourceQuota
,校验 Pod 的资源使用是否超出了对应 Namespace 的 Quota。虽说这是插件化的,但在 1.7 以前,全部的 plugin 都须要写到 apiserver 的代码中一块儿编译,很不灵活。而在 1.7 中 K8s 就引入了 Dynamic Admission Control 机制,容许用户向 apiserver 注册 webhook,而 apiserver 则经过 webhook 调用外部 server 来实现 filter 逻辑。1.9 中,这个特性进一步作了优化,把 webhook 分红了两类: MutatingAdmissionWebhook
和 ValidatingAdmissionWebhook
,顾名思义,前者就是操做 api 对象的,好比上文例子中的 DefaultStroageClass
,然后者是校验 api 对象的,好比 ResourceQuota
。拆分以后,apiserver 就能保证在校验(Validating)以前先作完全部的修改(Mutating),下面这个示意图很是清晰:
而咱们的办法就是,利用 ValidatingAdmissionWebhook
,在重要的 Pod 收到删除请求时,先在 webhook server 上请求集群进行下线前的清理和准备工做,并直接返回拒绝。这时候重点来了,Control Loop 为了达到目标状态(好比说升级到新版本),会不断地进行 reconcile,尝试删除 Pod,而咱们的 webhook 则会不断拒绝,除非集群已经完成了全部的清理和准备工做。
下面是这个流程的分步描述:
好像一会儿全部东西都清晰了,这个 webhook 的逻辑很清晰,就是要保证全部相关的 Pod 删除操做都要先完成优雅退出前的准备,彻底不用关心外部的控制循环是怎么跑的,也所以它很是容易编写和测试,很是优雅地知足了咱们“保证优雅关闭(不然不关闭)”的需求,目前咱们正在考虑用这种方式替换线上的旧方案。
其实 Dynamic Admission Control 的应用很广,好比 Istio 就是用 MutatingAdmissionWebhook
来实现 envoy 容器的注入的。从上面的例子中咱们也能够看到它的扩展能力很强,并且经常能站在一个正交的视角上,很是干净地解决问题,与其它逻辑作到很好的解耦。
固然了,Kubernetes 中还有 很是多的扩展点,从 kubectl 到 apiserver,scheduler,kubelet(device plugin,flexvolume),自定义 Controller 再到集群层面的网络(CNI),存储(CSI)能够说是到处能够作事情。之前作一些常规的微服务部署对这些并不熟悉也没用过,而如今面对 TiDB 这样复杂的分布式系统,尤为在 Kubernetes 对有状态应用和本地存储的支持还不够好的状况下,得在每个扩展点上去悉心考量,作起来很是有意思,所以后续可能还有一些 TiDB Operator 中思考过的解决方案分享。