零停机给Kubernetes集群节点打系统补丁

2021-06-08 19:03:37 浏览数 (1)

作者 | Vaishnavi Galgali

译者 | 王者

策划 | 万佳

1背景简介

Salesforce 的 Einstein Vision 和语言服务部署在 AWS Elastic Kubernetes Service(EKS) 集群上。其中有一个最主要的安全和合规性需求,就是给集群节点的操作系统打补丁。部署服务的集群节点需要通过打补丁的方式进行系统的定期更新。这些补丁减少了可能让虚拟机暴露于攻击之下的漏洞。

打补丁的过程

爱因斯坦服务以 Kubernetes Pod 的形式部署在不可变的 EC2 节点组 (也称为 AWS 自动伸缩组,缩写为 ASG) 中。打补丁的过程包括构建新的 Amazon Machine Image (AMI),镜像中包含了所有更新的安全补丁。新的 AMI 用于更新节点组,每一次需要启动一个新的 EC2 实例。当新实例通过运行健康状况检查后,旧实例将被终止。这个过程将会持续下去,直到节点组中的所有 EC2 实例都被新实例替换,这个过程也称为滚动更新。

然而,这个打补丁的过程给我们带来了一个挑战。当旧的 EC2 实例被终止时,在这些 EC2 实例上运行的服务 Pod 也会被终止。如果 Pod 的终止过程没有得到妥善处理,可能会导致用户请求处理失败。要优雅地终止 Pod,需要基础设施组件 (Kubernetes API 和 AWS ASG) 和应用程序组件 (服务 / 应用程序容器) 的支持。

2优雅终止应用程序

在这个过程中,首先要优雅地终止应用程序。终止一个 Pod 可能会导致 Pod 中的 Docker 容器突然终止,在 Docker 容器中运行的进程也会突然终止。这可能会导致正在处理中的请求被终止,最终导致当时正在调用应用程序的上游服务调用失败。

当一个 EC2 实例在打补丁过程中被终止,该实例上的 Pod 也将被驱逐。Pod 被标志为终止,在 EC2 实例上运行的 kubelet 就开始了关闭 Pod 的过程。kubelet 将发出 SIGTERM 信号。如果在 Pod 中运行的应用程序没有处理 SIGTERM 信号的逻辑,正在执行的任务可能会被突然终止。因此,你需要更新应用程序来处理这个信号,并实现优雅的终止。

例如,对于 Java 应用程序,有一种方法可以实现优雅的终止(不同的框架处理方式有所不同):

代码语言:javascript复制
public static final int gracefulShutdownTimeoutSeconds = 30;@Override
public void onApplicationEvent(@NotNull ContextClosedEvent contextClosedEvent) {
    this.connector.pause();
    Executor executor = this.connector.getProtocolHandler().getExecutor();
    if (executor instanceof ThreadPoolExecutor) {
    try {
        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
        threadPoolExecutor.shutdown();
        logger.warn("Gracefully shutdown the service.");
        if (!threadPoolExecutor.awaitTermination(gracefulShutdownTimeoutSeconds, TimeUnit.SECONDS)) {
        logger.warn("Forcefully shutdown the service after {} seconds.", gracefulShutdownTimeoutSeconds);
        threadPoolExecutor.shutdownNow();
        }
    } catch (InterruptedException ex) {
        Thread.currentThread().interrupt();
    }
    }
}

在上面的代码片段中,关闭信号被触发,并在 30 秒后强制终止应用程序,这给了应用程序 30 秒的时间来处理正在执行中的任务。

如果 Pod 由多个容器组成,并且容器终止的顺序很重要,那么最好要定义一个容器 preStop 钩子,以确保容器能以正确顺序终止(例如,在终止日志边车容器前先终止应用程序容器)。

https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/

在关闭 Pod 的过程中,kubelet 会执行容器生命周期钩子 (如果定义了的话)。在我们的例子中,一个 Pod 中有多个容器,因此,对我们来说,终止顺序很重要。我们为应用程序容器定义了一个 preStop 钩子,如下所示:

代码语言:javascript复制
lifecycle:
    preStop:
        exec:
          command:
          - /bin/sh
          - -c
          - kill -SIGTERM 1 && while ps -p 1 > /dev/null; do sleep 1; done;

preStop 钩子中定义的动作将向 Docker 容器中的进程 (PID 1) 发送一个 SIGTERM 信号,并以 1 秒为等待时间间隔,直到进程成功终止。进程可以完成任何一个挂起的任务,并正常终止。

preStop 钩子的默认超时时间是 30 秒。在我们的例子中,这提供了足够多的时间让进程优雅地终止。如果默认的时间不够,可以在 preStop 钩子中使用terminationGracePeriodSeconds字段来指定其他值。

3优雅地终止 EC2 实例

如上所述,我们的服务运行在 EC2 实例的节点组上。优雅地终止 EC2 实例可以通过使用 AWS ASG 生命周期钩子和 AWS Lambda 服务来实现。

AWS EC2 自动伸缩生命周期钩子

有了生命周期钩子,我们就可以实现在启动新实例或终止旧实例前暂停实例状态,并执行自定义操作。一旦实例被暂停,你就可以通过触发 Lambda 函数或在实例上运行命令来完成生命周期操作。实例会一直保持等待状态,直到生命周期操作完成。

我们使用 Terminating:Wait 生命周期钩子将要终止的实例置于等待状态。有关 ASG 生命周期钩子的更多细节,请参阅 AWS 文档。

http://ttps//docs.aws.amazon.com/autoscaling/ec2/userguide/lifecycle-hooks.html

AWS Lambda

我们使用 SAM 框架来部署 Lambda 函数(这个 Lambda 函数是内部开发的,我们把它叫作 node-drainer),当发生特定的 ASG 生命周期钩子事件时被触发。下图显示了优雅地终止节点组中的 EC2 实例所涉及的事件序列。

  • 当 Patching Automation 请求终止实例时,生命周期钩子将启动,并将实例置于 Terminating:Wait 状态。
  • 当实例处于 terminate:Wait 状态,生命周期钩子就会触发 AWS Lambda 函数。
  • Lambda 函数调用 Kubernetes API 并隔离被终止的实例。隔离实例可防止在被终止的实例上启动新的 Pod。
  • 隔离实例后,该实例所有的 Pod 都将被驱逐,并放在一个正常的节点上。
  • Kubernetes 负责为健康实例提供新的 Pod。
  • 生命周期钩子等待,直到所有 Pod 被驱逐出实例,并且新 Pod 出现在一个正常的实例中。
  • 一旦节点被完全清空,生命周期钩子将移除 WAIT 状态,并继续执行终止操作。
  • 这确保了全部现有的请求都已处理完成,然后将 Pod 从节点中移除。
  • 在这样做的同时,我们要确保新 Pod 能处理新的请求。
  • 这种优雅的关闭过程确保没有 Pod 是被突然关闭的,也不会出现服务中断。

4RBAC(基于角色的访问控制)

为了能从 AWS Lambda 函数访问 Kubernetes 资源,我们创建了一个 IAM 角色、一个clusterrole和一个clusterrolebinding。IAM 角色用于授予访问 ASG 的权限,clusterroleclusterrolebindingnode-drainer Lambda 函数授予驱逐 Kubernetes Pod 的权限。

IAM 角色策略

代码语言:javascript复制
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "autoscaling:CompleteLifecycleAction",
                "ec2:DescribeInstances",
                "eks:DescribeCluster",
                "sts:GetCallerIdentity"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

Clusterrole

代码语言:javascript复制
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: lambda-cluster-access
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/eviction", "nodes"]
    verbs: ["create", "list", "patch"]

Clusterrolebinding

代码语言:javascript复制
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: lambda-user-cluster-role-binding
subjects:
  - kind: User
    name: lambda
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: lambda-cluster-access
  apiGroup: rbac.authorization.k8s.io

5结论

通过结合使用 AWS Lambda、AWS EC2 自动伸缩生命周期钩子和优雅的应用程序进程终止,我们确保了在打补丁期间实现零停机频繁滚动更新 EC2 实例。

原文链接:

https://engineering.salesforce.com/zero-downtime-node-patching-in-a-kubernetes-cluster-cdceb21c8c8c

0 人点赞