CloneSet Lifecycle:在 Pod 生命周期管理中插入定制化逻辑

因为各种各样的历史原因和客观因素,有些用户可能无法将自己公司的整套体系架构 Kubernetes 化,比如有些用户暂时无法使用 Kubernetes 本身提供的服务发现机制 Kubernetes Service,而是使用了独立于 Kubernetes 之外的另外一套服务注册和发现体系。在这种架构下,如果用户对服务进行 Kubernetes 化改造,可能会遇到诸多问题。 例如,每当 Kubernetes 成功创建出一个 Pod,都需要自行将该 Pod 注册到服务发现中心,以便能够对内对外提供服务;相应的,想要下线一个 Pod,也通常先要将其在服务发现中心删除,才能将 Pod 优雅下线,否则就可能导致流量损失。但是在原生的 Kubernetes 体系中, Pod 的生命周期由 Workload 管理(例如 Deployment), 当这些 Workload 的 Replicas 字段发生变化后,相应的 Controller 会立即添加或删除掉 Pod,用户很难定制化地去管理 Pod 的生命周期。

面对这类问题,摆在用户面前的往往无非就这两种解决思路:一是约束 Kubernetes 的弹性能力, 例如规定只能由特定的链路对 Workload 进行扩缩容,以保证在删除 Pod 前先把 Pod IP 在服务注册中心摘除, 但这样一来会制约 Kubernetes 本身的弹性能力, 并且也增加了链路管控的难度和风险。 二是在根本上改造现有的服务发现体系,显然这是一个更加漫长和高风险的事情。

why

那么有没有一种既能够充分利用 Kubernetes 弹性能力,又避免对既有服务发现体系进行改造,快速弥补两个系统之间 Gap 的方法呢?答案是肯定的。

OpenKruise CloneSet 就提供了这样一组高度可定制化的扩展能力来专门应对此类场景,让用户能够对 Pod 生命周期做更精细化、定制化的管理。 CloneSet 在 Pod 生命周期中几个重要的时间节点预留了 Hook,使得用户可以在这些时间节点插入一些定制化的扩展动作。 比如,在 Pod 升级前,将 Pod IP 在服务发现中心删除,升级完成后再将 Pod IP 注册到服务发现中心,或者做一些特殊的嗅探和监控动作。 在下文中,我们将会在一个具体的场景对这项能力进行展开讲解,帮你你进一步深入理解这一机制。

场景假设

我们假设场景如下:

  • 用户不使用 Kubernetes Service 作为基本的服务发现机制,服务发现体系完全独立于 Kubernetes;
  • 使用 CloneSet 作为 Kubernetes 工作负载。

并且对具体的需求做如下合理假设:

  • 当 Kubernetes Pod 被创建时:
    • 在创建成功,且 Pod Ready 之后,将 Pod IP 注册到服务发现中心;
  • 当 Kubernetes Pod 原地升级时:
    • 在升级之前,需要将 Pod IP 从服务发现中心删除(或主动 FailOver);
    • 在升级完成,且 Pod Ready 之后,将 Pod IP 再次注册到服务发现中心;
  • 当 Kubernetes Pod 被删除时:
    • 在删除之前,需要先将 Pod IP 从服务发现中心删除;

基于以上假设,本文将详细讲述如何利用 CloneSet LifeCycle 编写一个简单的 Operator 来实现用户定义的 Pod 生命周期管理机制。

原理说明

CloneSet LifeCycle 将 Pod 的生命周期定义为以下 5 种状态:

  • Normal:正常状态;
  • PreparingUpdate:准备原地升级,通过 Lifecycle 机制阻拦 CloneSet 对 Pod 的升级操作,以便等待用户执行 Hook,完成升级前的一些预处理操作;
  • Updating:正在原地升级;
  • Updated:原地升级完成,通知用户 Pod 升级完成,可以做一些收尾工作;
  • PreparingDelete:准备删除,通过 Lifecycle 机制阻拦 CloneSet 对 Pod 的删除操作,以便等待用户执行 Hook,完成删除前的一些预处理操作;

以上 5 种状态之间的转换逻辑由一个状态机所控制,CloneSet 官方文档 中对此进行了详细解释。 用户可以只选择自己所关心的一种或多种,编写一个独立的 Operator 来实现这些状态的转换,控制 Pod 的生命周期,并在所关心的时间节点插入自己的定制化逻辑。

CloneSet LifeCycle 配置

  1. apiVersion: apps.kruise.io/v1alpha1
  2. kind: CloneSet
  3. metadata:
  4. namespace: demo
  5. name: cloneset-lifecycle-demo
  6. spec:
  7. replicas: 2
  8. ##########################################################################
  9. ## Lifecycle configuration
  10. lifecycle:
  11. inPlaceUpdate:
  12. labelsHandler:
  13. ## define the label that:
  14. ## 1. block inPlace update pod operation for cloneset controller
  15. ## 2. inform your operator to execute inPlace update hook
  16. example.com/unready-blocker-inplace: "true"
  17. preDelete:
  18. labelsHandler:
  19. ## define the label that:
  20. ## 1. block deletion pod operation for cloneset controller
  21. ## 2. inform your operator to execute preDelete hook
  22. example.com/unready-blocker-delete: "true"
  23. ##########################################################################
  24. selector:
  25. matchLabels:
  26. app: nginx
  27. template:
  28. metadata:
  29. labels:
  30. app: nginx
  31. ## this label is useful to judge whether this pod is newly-created.
  32. example.com/newly-create: "true"
  33. ## corresponding to the spec.lifecycle.inPlaceUpdate.labelsHandler.example.com/unready-blocker-inplace
  34. example.com/unready-blocker-inplace: "true"
  35. ## corresponding to the spec.lifecycle.preDelete.labelsHandler.example.com/unready-blocker-inplace
  36. example.com/unready-blocker-delete: "true"
  37. spec:
  38. containers:
  39. - name: main
  40. image: nginx:latest
  41. imagePullPolicy: Always
  42. updateStrategy:
  43. maxUnavailable: 20%
  44. type: InPlaceIfPossible

LifeCycle Operator 构建

在 OpenKruise Github 仓库中,我们给出了一个完整的 CloneSet LifeCycle Operator 代码示例。 因此在本文中,我们不再赘述一些代码细节,而是主要结合场景对一些关键的代码逻辑进行讲解,如果需要参考完整的代码,可以直接到该仓库中去找。

Operator 初始化

推荐使用 Kubebuilder 构建 Operator,具体构建步骤请参考 Kubebuilder 官方文档。 初始化完成的 Operator 目录结构类似如下:

  1. $ tree
  2. .
  3. ├── Dockerfile
  4. ├── LICENSE
  5. ├── Makefile
  6. ├── PROJECT
  7. ├── README.md
  8. ├── config
  9. └── ....
  10. ├── controllers
  11. └── lifecyclehook
  12. └── lifecyclehook_controller.go
  13. ├── go.mod
  14. ├── go.sum
  15. ├── hack
  16. └── ....
  17. └── main.go

Controller 逻辑编写

Pod 生命周期状态管理逻辑都会在 lifecyclehook_controller.go 文件的 Reconcile(req ctrl.Request) (ctrl.Result, error) 方法中编写。

例如在本文的假设的场景中,我们可以将 Pod 的生命周期管理逻辑通过以下代码来实现:

  1. const (
  2. deleteHookLabel = "example.com/unready-blocker-delete"
  3. inPlaceHookLabel = "example.com/unready-blocker-inplace"
  4. newlyCreateLabel = "example.com/newly-create"
  5. )
  6. func (r *SampleReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
  7. ... ...
  8. switchLabel := func(pod *v1.Pod, key, value string) error {
  9. body := fmt.Sprintf(`{"metadata":{"labels":{"%s":"%s"}}}`, key, value)
  10. if err := r.Patch(context.TODO(), pod, client.RawPatch(types.StrategicMergePatchType, []byte(body))); err != nil {
  11. return err
  12. }
  13. return nil
  14. }
  15. /*
  16. Pod LifeCycle Hook Logic
  17. */
  18. switch {
  19. // handle newly-created pod
  20. case IsNewlyCreateHooked(pod):
  21. // register this pod to your service discovery center
  22. if err := postRegistry(pod); err != nil {
  23. return reconcile.Result{}, err
  24. }
  25. if err := switchLabel(pod, newlyCreateLabel, "false"); err != nil {
  26. return reconcile.Result{}, err
  27. }
  28. // handle the pod which is preparing to inplace update
  29. case IsPreUpdateHooked(pod):
  30. // let the service discover center fail over this pod
  31. if err := postFailOver(pod); err != nil {
  32. return reconcile.Result{}, err
  33. }
  34. if err := switchLabel(pod, inPlaceHookLabel, "false"); err != nil {
  35. return reconcile.Result{}, err
  36. }
  37. // handle the pod which is updated completely
  38. case IsUpdatedHooked(pod):
  39. // register this pod again to your service discovery center
  40. if err := postRegistry(pod); err != nil {
  41. return reconcile.Result{}, err
  42. }
  43. if err := switchLabel(pod, inPlaceHookLabel, "true"); err != nil {
  44. return reconcile.Result{}, err
  45. }
  46. // handle the pod which is preparing to delete
  47. case IsPreDeleteHooked(pod):
  48. // just unregister this pod from your service discovery center
  49. if err := postUnregister(pod); err != nil {
  50. return reconcile.Result{}, err
  51. }
  52. if err := switchLabel(pod, deleteHookLabel, "false"); err != nil {
  53. return reconcile.Result{}, err
  54. }
  55. }
  56. return ctrl.Result{}, nil
  57. }
  58. func IsNewlyCreateHooked(pod *v1.Pod) bool {
  59. return kruiseappspub.LifecycleStateType(pod.Labels[kruiseappspub.LifecycleStateKey]) == kruiseappspub.LifecycleStateNormal && pod.Labels[newlyCreateLabel] == "true" && IsPodReady(pod)
  60. }
  61. func IsPreUpdateHooked(pod *v1.Pod) bool {
  62. return kruiseappspub.LifecycleStateType(pod.Labels[kruiseappspub.LifecycleStateKey]) == kruiseappspub.LifecycleStatePreparingUpdate && pod.Labels[inPlaceHookLabel] == "true"
  63. }
  64. func IsUpdatedHooked(pod *v1.Pod) bool {
  65. return kruiseappspub.LifecycleStateType(pod.Labels[kruiseappspub.LifecycleStateKey]) == kruiseappspub.LifecycleStateUpdated && pod.Labels[inPlaceHookLabel] == "false" && IsPodReady(pod)
  66. }
  67. func IsPreDeleteHooked(pod *v1.Pod) bool {
  68. return kruiseappspub.LifecycleStateType(pod.Labels[kruiseappspub.LifecycleStateKey]) == kruiseappspub.LifecycleStatePreparingDelete && pod.Labels[DeleteHookLabel] == "true"
  69. }

上述代码中四个 case 分别从上到下对应 Pod 的创建后、升级前、升级后、删除前等四个重要声明周期节点,用户需要根据自己的需要来完善相应的 Hook。在本文的场景中,上述几个 Hook 的行为具体为:

  • postRegistry(pod *v1.Pod) : 发送请求通知服务发现中心注册该 Pod 服务;
  • postFailOver(pod *v1.Pod) : 发送请求通知服务发现中心 Fail Over 该 Pod 服务;
  • postUnregiste(pod *v1.Pod): 发送请求通知服务发现中心将该 Pod 服务注销。

Operator 部署

当该 Operator 代码逻辑完善后,需要将该 Operator 部署到目标集群,部署方式可参考 Kubebuilder 官方文档,此处不再赘述。

Operator 部署完成后,该 Operator 将持续监听集群中 Pod 的状态,并在每个 Pod 的关键生命周期节点自动执行上述 Hook,从而将 Kubernetes 与你的服务发现中心进行衔接,弥合两者的 Gap。