本文阐述如何解决 Kubernetes 中与 CPU 限制相关的 Java 应用启动缓慢的问题。使用一个新的 Kubernetes 功能,称为“In-place Pod Vertical Scaling”。它允许调整分配给容器的资源(CPU 或内存)大小,而无需重新启动 Pod。 这个新功能从 Kubernetes 1.27 版本开始就可以使用。然而,由于是 alpha 功能,必须明确激活启用。
如果您在 Kubernetes 上运行 Java 应用程序,您可能已经遇到过设置过低 CPU 限制后启动缓慢的问题。 出现这种情况的原因是:Java 应用程序在初始化期间所需的 CPU 资源通常比标准工作期间多得多,解决办法两难:
从Kubernetes 1.27 版本由于有了这个新功能,这样 pod 可以在创建 pod 时请求更高的 CPU,并在应用程序完成初始化后将其调整到正常运行需要的大小。
我们还可以考虑如何在 pod 就绪后自动在集群上应用这些更改,为此,我们将使用 Kyverno。Kyverno 策略能够根据接纳回调来改变 Kubernetes 资源,这完全符合我们在本练习中的需求。
由于“就地 pod 垂直扩展”功能仍处于 alpha 状态,我们需要在 Kubernetes 上显式启用它。我正在 Minikube 上测试该功能。这是我的 minikube 启动命令(如果您愿意,可以尝试使用较低的内存):
$ minikube start --memory='8g' \ --feature-gates=InPlacePodVerticalScaling=true
第一步我们需要添加以下 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 命名空间中。
我们希望在 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)
我们需要添加一些允许 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
一旦我们部署了应用程序,一个新的 pod 就会启动。我们可以验证其当前的资源限制。正如你所看到的,它仍有 2 个 CPU。
我们的应用程序启动时间约为 10-15 秒。因此,准备就绪检查也会在开始调用执行器端点(initialDelaySeconds 参数)后等待 15 秒。之后,检查成功结束,我们的容器切换到就绪状态。
然后,Kyverno 检测到容器状态变化并触发策略。由于容器已准备就绪,因此策略前提条件已满足。现在,我们可以验证同一 pod 上当前的 CPU 限制。它是 500millicores。
现在,我们可以扩大应用程序的运行实例数量以继续测试。然后,您可以自行验证新的 pod 在启动后是否也会被 Kyverno 修改为 0.5 个核心。
最后一件事。如果我们一开始将 CPU 限制设置为 500 毫核,那么启动我们的应用程序需要多长时间?对于我的应用程序和这样的 CPU 限制,大约是 40 秒。所以差异是显着的。