Kubernetes 中如何保证优雅地中止 Pod

做者:吴叶磊git

一直以来我对优雅地中止 Pod 这件事理解得很单纯:不就利用是 PreStop hook 作优雅退出吗?但最近发现不少场景下 PreStop Hook 并不能很好地完成需求,这篇文章就简单分析一下“优雅地中止 Pod”这回事儿。github

何谓优雅中止?

优雅中止(Graceful shutdown)这个说法来自于操做系统,咱们执行关机以后都得 OS 先完成一些清理操做,而与之相对的就是硬停止(Hard shutdown),好比拔电源。web

到了分布式系统中,优雅中止就不只仅是单机上进程本身的事了,每每还要与系统中的其它组件打交道。好比说咱们起一个微服务,网关把一部分流量分给咱们,这时:数据库

  • 假如咱们一声不吭直接把进程杀了,那这部分流量就没法获得正确处理,部分用户受到影响。不过还好,一般来讲网关或者服务注册中心会和咱们的服务保持一个心跳,过了心跳超时以后系统会自动摘除咱们的服务,问题也就解决了;这是硬停止,虽然咱们整个系统写得不错可以自愈,但仍是会产生一些抖动甚至错误。
  • 假如咱们先告诉网关或服务注册中心咱们要下线,等对方完成服务摘除操做再停止进程,那不会有任何流量受到影响;这是优雅中止,将单个组件的启停对整个系统影响最小化。

按照惯例,SIGKILL 是硬终止的信号,而 SIGTERM 是通知进程优雅退出的信号,所以不少微服务框架会监听 SIGTERM 信号,收到以后去作反注册等清理操做,实现优雅退出。api

PreStop Hook

回到 Kubernetes(下称 K8s),当咱们想干掉一个 Pod 的时候,理想情况固然是 K8s 从对应的 Service(假若有的话)把这个 Pod 摘掉,同时给 Pod 发 SIGTERM 信号让 Pod 中的各个容器优雅退出就好了。但实际上 Pod 有可能犯各类幺蛾子:安全

  • 已经卡死了,处理不了优雅退出的代码逻辑或须要好久才能处理完成。
  • 优雅退出的逻辑有 BUG,本身死循环了。
  • 代码写得野,根本不理会 SIGTERM。

所以,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 退出的流程(官方文档里更严谨哦):运维

  1. 用户删除 Pod。
    • 2.1. Pod 进入 Terminating 状态。
    • 2.2. 与此同时,K8s 会将 Pod 从对应的 service 上摘除。
    • 2.3. 与此同时,针对有 PreStop Hook 的容器,kubelet 会调用每一个容器的 PreStop Hook,假如 PreStop Hook 的运行时间超出了 grace period,kubelet 会发送 SIGTERM 并再等 2 秒。
    • 2.4. 与此同时,针对没有 PreStop Hook 的容器,kubelet 发送 SIGTERM。
  2. grace period 超出以后,kubelet 发送 SIGKILL 干掉还没有退出的容器。

这个过程很不错,但它存在一个问题就是咱们没法预测 Pod 会在多久以内完成优雅退出,也没法优雅地应对“优雅退出”失败的状况。而在咱们的产品 TiDB Operator 中,这就是一个没法接受的事情。

有状态分布式应用的挑战

为何说没法接受这个流程呢?其实这个流程对无状态应用来讲一般是 OK 的,但下面这个场景就稍微复杂一点:

TiDB 中有一个核心的分布式 KV 存储层 TiKV。TiKV 内部基于 Multi-Raft 作一致性存储,这个架构比较复杂,这里咱们能够简化描述为一主多从的架构,Leader 写入,Follower 同步。而咱们的场景是要对 TiKV 作计划性的运维操做,好比滚动升级,迁移节点。

在这个场景下,尽管系统能够接受小于半数的节点宕机,但对于预期性的停机,咱们要尽可能作到优雅中止。这是由于数据库场景自己就是很是严苛的,基本上都处于整个架构的核心部分,所以咱们要把抖动作到越小越好。要作到这点,就得作很多清理工做,好比说咱们要在停机前将当前节点上的 Leader 所有迁移到其它节点上。

得益于系统的良好设计,大多数时候这类操做都很快,然而分布式系统中异常是屡见不鲜,优雅退出耗时过长甚至失败的场景是咱们必需要考虑的。假如相似的事情发生了,为了业务稳定和数据安全,咱们就不能强制关闭 Pod,而应该中止操做过程,通知工程师介入。 这时,上面所说的 Pod 退出流程就再也不适用了。

当心翼翼:手动控制全部流程

这个问题其实 K8s 自己没有开箱即用的解决方案,因而咱们在本身的 Controller 中(TiDB 对象自己就是一个 CRD)与很是细致地控制了各类操做场景下的服务启停逻辑。

抛开细节不谈,最后的大体逻辑是在每次停服务前,由 Controller 通知集群进行节点下线前的各类迁移操做,操做完成后,才真正下线节点,并进行下一个节点的操做。

而假如集群没法正常完成迁移等操做或耗时太久,咱们也能“守住底线”,不会强行把节点干掉,这就保证了诸如滚动升级,节点迁移之类操做的安全性。

但这种办法存在一个问题就是实现起来比较复杂,咱们须要本身实现一个控制器,在其中实现细粒度的控制逻辑而且在 Controller 的控制循环中不断去检查可否安全中止 Pod。

另辟蹊径:解耦 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 则会不断拒绝,除非集群已经完成了全部的清理和准备工做

下面是这个流程的分步描述:

  1. 用户更新资源对象。
  2. controller-manager watch 到对象变动。
  3. controller-manager 开始同步对象状态,尝试删除第一个 Pod。
  4. apiserver 调用外部 webhook。
  5. webhook server 请求集群作 tikv-1 节点下线前的准备工做(这个请求是幂等的),并查询准备工做是否完成,假如准备完成,容许删除,假如没有完成,则拒绝,整个流程会由于 controller manager 的控制循环回到第 2 步。

好像一会儿全部东西都清晰了,这个 webhook 的逻辑很清晰,就是要保证全部相关的 Pod 删除操做都要先完成优雅退出前的准备,彻底不用关心外部的控制循环是怎么跑的,也所以它很是容易编写和测试,很是优雅地知足了咱们“保证优雅关闭(不然不关闭)”的需求,目前咱们正在考虑用这种方式替换线上的旧方案。

后记

其实 Dynamic Admission Control 的应用很广,好比 Istio 就是用 MutatingAdmissionWebhook 来实现 envoy 容器的注入的。从上面的例子中咱们也能够看到它的扩展能力很强,并且经常能站在一个正交的视角上,很是干净地解决问题,与其它逻辑作到很好的解耦。

固然了,Kubernetes 中还有 很是多的扩展点,从 kubectl 到 apiserver,scheduler,kubelet(device plugin,flexvolume),自定义 Controller 再到集群层面的网络(CNI),存储(CSI)能够说是到处能够作事情。之前作一些常规的微服务部署对这些并不熟悉也没用过,而如今面对 TiDB 这样复杂的分布式系统,尤为在 Kubernetes 对有状态应用和本地存储的支持还不够好的状况下,得在每个扩展点上去悉心考量,作起来很是有意思,所以后续可能还有一些 TiDB Operator 中思考过的解决方案分享。

相关文章
相关标签/搜索