如何加快Kubernetes中Java启动速度?


本文阐述如何解决 Kubernetes 中与 CPU 限制相关的 Java 应用启动缓慢的问题。使用一个新的 Kubernetes 功能,称为“In-place Pod Vertical Scaling”。它允许调整分配给容器的资源(CPU 或内存)大小,而无需重新启动 Pod。
这个新功能从 Kubernetes 1.27 版本开始就可以使用。然而,由于是 alpha 功能,必须明确激活启用。

场景
如果您在 Kubernetes 上运行 Java 应用程序,您可能已经遇到过设置过低 CPU 限制后启动缓慢的问题。
出现这种情况的原因是:Java 应用程序在初始化期间所需的 CPU 资源通常比标准工作期间多得多,解决办法两难:

  • 如果Java应用指定了只适合常规操作的请求和限制,则可能会导致启动时间过长。
  • 另一方面,如果只是为了快速启动而指定较高的 CPU 限制,这可能不是管理 Kubernetes 资源限制的最佳方法。

从Kubernetes 1.27 版本由于有了这个新功能,这样 pod 可以在创建 pod 时请求更高的 CPU,并在应用程序完成初始化后将其调整到正常运行需要的大小。

我们还可以考虑如何在 pod 就绪后自动在集群上应用这些更改,为此,我们将使用 Kyverno。Kyverno 策略能够根据接纳回调来改变 Kubernetes 资源,这完全符合我们在本练习中的需求。

启用就地 Pod 垂直扩展
由于“就地 pod 垂直扩展”功能仍处于 alpha 状态,我们需要在 Kubernetes 上显式启用它。我正在 Minikube 上测试该功能。这是我的 minikube 启动命令(如果您愿意,可以尝试使用较低的内存):

$ minikube start --memory='8g' \ --feature-gates=InPlacePodVerticalScaling=true

在 Kubernetes 上安装 Kyverno
第一步我们需要添加以下 Helm 存储库:

$ helm repo add kyverno https://kyverno.github.io/kyverno/

在安装过程中,我们需要自定义一个属性。

默认情况下,Kyverno 会过滤掉 system:nodes 组中成员在 Kubernetes 上进行的更新。
其中一个成员是kubelet,它负责更新节点上运行的容器的状态。

因此,如果我们想从 kubelet 捕捉容器就绪事件,就需要覆盖该行为。这就是我们将 config.excludeGroups 属性设置为空数组的原因。下面是我们的 values.yaml 文件:

config:
  excludeGroups: []

最后,我们可以使用以下 Helm 命令在 Kubernetes 上安装 Kyverno:

$ helm install kyverno kyverno/kyverno -n kyverno \
  --create-namespace -f values.yaml

Kyverno 已安装在 kyverno 命名空间中。

创建调整 CPU 限制的策略
我们希望在 pod 启动及其状态更新时触发 Kyverno 策略,如下代码 (1)标记:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: resize-pod-policy
spec:
  mutateExistingOnPolicyUpdate: false
  rules:
    - name: resize-pod-policy
      match:
        any:
          - resources: # (1)
              kinds:
                - Pod/status
                - Pod
      preconditions: 
        all: # (2)
          - key: "{{request.object.status.containerStatuses[0].ready}}"
            operator: Equals
            value: true
      mutate:
        targets:
          - apiVersion: v1
            kind: Pod
            name: "{{request.object.metadata.name}}"
        patchStrategicMerge:
          spec:
            containers:
              - (name): sample-kotlin-spring # (3)
                resources:
                  limits:
                    cpu: 0.5 # (4)

  • 只有在当前准备状态为 true 时,我们才会对资源进行更改 (2)。
  • 我们可以使用名为 "锚"(anchor)的特殊元素来选择目标容器(3)。
  • 最后,我们可以使用 patchStrategicMerge 部分为目标 pod 内的容器定义新的 CPU 限制 (4)。

我们需要添加一些允许 Kyverno 后台控制器更新 pod 的额外权限。我们不需要创建 ClusterRoleBinding,而只需要创建一个带有正确聚合标签的 ClusterRole,以便让这些权限生效。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: kyverno:update-pods
  labels:
    app.kubernetes.io/component: background-controller
    app.kubernetes.io/instance: kyverno
    app.kubernetes.io/part-of: kyverno
rules:
  - verbs:
      - patch
      - update
    apiGroups:
      - ''
    resources:
      - pods

之后,我们可能会再次尝试制定一项策略。

部署 Java 应用程序并在启动后调整 CPU 限制
让我们来看看 Java 应用的部署清单:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-kotlin-spring
  namespace: demo
  labels:
    app: sample-kotlin-spring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-kotlin-spring
  template:
    metadata:
      labels:
        app: sample-kotlin-spring
    spec:
      containers:
      - name: sample-kotlin-spring # (1)
        image: quay.io/pminkows/sample-kotlin-spring:1.5.1.1
        ports:
        - containerPort: 8080
        resources:
          limits:
            cpu: 2 # (2)
            memory: "1Gi"
          requests:
            cpu: 0.1
            memory: "256Mi"
        resizePolicy: # (3)
        - resourceName: "cpu"
          restartPolicy: "NotRequired"
        readinessProbe: # (4)
          httpGet:
            path: /actuator/health/readiness
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 15
          periodSeconds: 5
          successThreshold: 1
          failureThreshold: 3

  • 应用程序容器的名称是 sample-kotlin-spring,与 Kyverno 策略中的条件 "anchor"(锚)相匹配(1)。
  • 如你所见,我将 CPU 限制设置为 2 核 (2)。
  • 这里还使用了一个新字段 resizePolicy (3)。
  • 由于默认值是 NotRequired,所以我不必设置它。这意味着更改资源限制或请求不会导致 pod 重启。部署对象还包含一个就绪探针,用于调用 Spring Boot Actuator (4) 暴露的 GET/actuator/health/readiness。

一旦我们部署了应用程序,一个新的 pod 就会启动。我们可以验证其当前的资源限制。正如你所看到的,它仍有 2 个 CPU。

我们的应用程序启动时间约为 10-15 秒。因此,准备就绪检查也会在开始调用执行器端点(initialDelaySeconds 参数)后等待 15 秒。之后,检查成功结束,我们的容器切换到就绪状态。

然后,Kyverno 检测到容器状态变化并触发策略。由于容器已准备就绪,因此策略前提条件已满足。现在,我们可以验证同一 pod 上当前的 CPU 限制。它是 500millicores。

现在,我们可以扩大应用程序的运行实例数量以继续测试。然后,您可以自行验证新的 pod 在启动后是否也会被 Kyverno 修改为 0.5 个核心。

最后一件事。如果我们一开始将 CPU 限制设置为 500 毫核,那么启动我们的应用程序需要多长时间?对于我的应用程序和这样的 CPU 限制,大约是 40 秒。所以差异是显着的。