Kubernetes 自动扩容和自愈

tags: 实践

Kubernetes 实现自动扩容和自愈应用实践-LMLPHP

1. 背景

在生产非 kubernetes 集群中,负载均衡器往往是集群的唯一入口,它在接受访问流量后,一般会将流量通过加权轮训的方式转发到后端集群。负载均衡器一般是直接使用云厂商的产品,有一些团队也会自建高可用的 Nginx 作为集群入口。为了保证伸缩组节点的业务一致性,弹性伸缩组的所有 VM 都使用同一个虚拟机镜像。其次,要在 VM 粒度实现业务自愈,常见的方案是使用 Crontab 定时检查业务进程或者通过守护进程的方式来运行。

传统扩容和自愈的缺点但是,这种架构有一些显而易见的缺陷。最大的问题有两个:

  • 扩容慢;
  • 负载均衡无法感知业务健康情况。

扩容慢主要体现在两方面。首先是 VM 指标会有一定的延迟;其次,扩容的 VM 冷启动时间比较慢,弹性伸缩组需要执行购买 VM、配置镜像、加入伸缩组、启动 VM 等操作。这会让我们失去扩容的最佳时机,并最终影响用户体验。负载均衡无法感知业务健康情况的意思是,VM 是否加入到弹性伸缩组接收外部流量,一般取决于 VM 的健康状态,但 VM 健康并不等于业务健康,这导致在扩缩容的过程中,请求仍然有可能会转发至业务不健康的节点,造成业务短暂中断的问题。

Kubernets 可以自动自愈和自动扩容。接下来我们一起验证。

2. 准备

我的 kind 安装方式

curl -Lo ./kind https://github.com/kubernetes-sigs/kind/releases/download/v0.17.0/kind-linux-amd64
chmod +x ./kind
mv ./kind /usr/local/bin/kind

3. kind 部署 kubernetes

  • config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  kubeadmConfigPatches:
  - |
    kind: InitConfiguration
    nodeRegistration:
      kubeletExtraArgs:
        node-labels: "ingress-ready=true"
  extraPortMappings:
  - containerPort: 80
    hostPort: 80
    protocol: TCP
  - containerPort: 443
    hostPort: 443
    protocol: TCP

创建 K8s 集群:

$ kind create cluster --config config.yaml
enabling experimental podman provider
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.25.3) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-kind"
You can now use your cluster with:

kubectl cluster-info --context kind-kind

Have a nice day! 👋

4.实践

  • Pod 会被 Deployment 工作负载管理起来,例如创建和销毁等;
  • Service 相当于弹性伸缩组的负载均衡器,它能以加权轮训的方式将流量转发到多个 Pod 副本上;
  • Ingress 相当于集群的外网访问入口。

4.1 部署 deployment

kubectl create deployment hello-world-flask --image=lyzhang1999/hello-world-flask:latest --replicas=2 

单纯输出 Manifest 内容:

$ kubectl create deployment hello-world-flask --image lyzhang1999/hello-world-flask:latest --replicas=2 --dry-run=client -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: hello-world-flask
  name: hello-world-flask
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello-world-flask
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: hello-world-flask
    spec:
      containers:
      - image: lyzhang1999/hello-world-flask:latest
        name: hello-world-flask
        resources: {}
status: {}

4.2 创建 Service

kubectl create service clusterip hello-world-flask --tcp=5000:5000

4.3 创建 Ingress

kubectl create ingress hello-world-flask --rule="/=hello-world-flask:5000"

4.4 部署 Ingress-nginx

kubectl create -f https://ghproxy.com/https://raw.githubusercontent.com/lyzhang1999/resource/main/ingress-nginx/ingress-nginx.yaml

4.5 K8s 实现自愈

$ kubectl get pods
NAME                                 READY   STATUS    RESTARTS   AGE
hello-world-flask-56fbff68c8-2xz7w   1/1     Running   0          3m38s
hello-world-flask-56fbff68c8-4f9qz   1/1     Running   0          3m38s

由于之前我们已经通过 Kind 在本地创建了集群,也暴露监听了本地的 80 端口,所以集群的 Ingress 访问入口是 127.0.0.1。有了 Ingress,我们访问 Pod 就不再需要进行端口转发了,我们可以直接访问 127.0.0.1。下面的命令会每隔 1 秒钟发送一次请求,并打印出时间和返回内容:

$ while true; do sleep 1; curl http://127.0.0.1; echo -e '\n'$(date);done
Hello, my first docker images! hello-world-flask-56fbff68c8-4f9qz
202297日 星期三 19时21分03秒 CST
Hello, my first docker images! hello-world-flask-56fbff68c8-2xz7w
202297日 星期三 19时21分04秒 CST

在这里,“Hello, my first docker images” 后面紧接的内容是 Pod 名称。通过返回内容我们会发现,请求被平均分配到了两个 Pod 上,Pod 名称是交替出现的。我们要保留这个命令行窗口,以便继续观察。

接下来,我们模拟其中的一个 Pod 宕机,观察返回内容。打开一个新的命令行窗口,执行下面的命令终止容器内的 Python 进程,这个操作是在模拟进程意外中止导致宕机的情况。

$ kubectl exec -it hello-world-flask-56fbff68c8-2xz7w -- bash -c "killall python3"

另一个终端:

$ while true; do sleep 1; curl http://127.0.0.1; echo -e '\n'$(date);done
Hello, my first docker images! hello-world-flask-56fbff68c8-4f9qz
202297日 星期三 19时27分44秒 CST
Hello, my first docker images! hello-world-flask-56fbff68c8-4f9qz
202297日 星期三 19时27分45秒 CST
Hello, my first docker images! hello-world-flask-56fbff68c8-4f9qz

所有的请求流量都被转发到了没有故障的 Pod,也就是说,故障成功地被转移了!等待几秒钟,继续观察,我们会重新发现 hello-world-flask-56fbff68c8-2xz7w Pod 的返回内容,这说明 Pod 被重启恢复后,重新加入到了负载均衡接收外部流量:

Hello, my first docker images! hello-world-flask-56fbff68c8-2xz7w
202297日 星期三 19时27分52秒 CST
Hello, my first docker images! hello-world-flask-56fbff68c8-4f9qz
202297日 星期三 19时27分53秒 CST
Hello, my first docker images! hello-world-flask-56fbff68c8-2xz7w

然后,我们再次使用 kubectl get pods 查看 Pod:

$ kubectl get pods
NAME                                 READY   STATUS    RESTARTS   AGE
hello-world-flask-56fbff68c8-2xz7w   1/1     Running   1(1m ago)  3m38s
hello-world-flask-56fbff68c8-4f9qz   1/1     Running   0          3m38s

这里要注意看, hello-world-flask-56fbff68c8-2xz7w Pod 的 RESTARTS 值为 1 ,也就是说 K8s 自动帮我们重启了这个 Pod。

我们重新来梳理一下全过程。首先, K8s 感知到了业务 Pod 故障,立刻进行了故障转移并隔离了有故障的 Pod,并将请求转发到了其他健康的 Pod 中。随后重启了有故障的 Pod,最后将重启后的 Pod 加入到了负载均衡并开始接收外部请求。这些过程都是自动化完成的。

4.6 k8s 实现自动扩容

安装 K8s Metric Server

kubectl apply -f https://ghproxy.com/https://raw.githubusercontent.com/lyzhang1999/resource/main/metrics/metrics.yaml

等待 Metric 工作负载就绪

kubectl wait deployment -n kube-system metrics-server --for condition=Available=True --timeout=90s

Metric Server 就绪后,通过 kubectl autoscale 命令来为 Deployment 创建自动扩容策略:

kubectl autoscale deployment hello-world-flask --cpu-percent=50 --min=2 --max=10

其中,–cpu-percent 表示 CPU 使用率阈值,当 CPU 超过 50% 时将进行自动扩容,–min 代表最小的 Pod 副本数,–max 代表最大扩容的副本数。也就是说,自动扩容会根据 CPU 的使用率在 2 个副本和 10 个副本之间进行扩缩容。

最后,要使自动扩容生效,还需要为我们刚才部署的 hello-world-flask Deployment 设置资源配额。你可以通过下面的命令来配置:

kubectl patch deployment hello-world-flask --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/0/resources", "value": {"requests": {"memory": "100Mi", "cpu": "100m"}}}]'

现在,Deployment 将会重新创建两个新的 Pod,你可以使用下面的命令筛选出新的 Pod:

$ kubectl get pod --field-selector=status.phase==Running
NAME                                 READY   STATUS    RESTARTS   AGE
hello-world-flask-64dd645c57-4clbp   1/1     Running   0          117s
hello-world-flask-64dd645c57-cc6g6   1/1     Running   0          117s

选择一个 Pod 并使用 kubectl exec 进入到容器内:

$ kubectl exec -it hello-world-flask-64dd645c57-4clbp -- bash
root@hello-world-flask-64dd645c57-4clbp:/app#

接下来,我们模拟业务高峰期场景,使用 ab 命令来创建并发请求:

root@hello-world-flask-64dd645c57-4clbp:/app# ab -c 50 -n 10000 http://127.0.0.1:5000/

在这条压力测试的命令中,-c 代表 50 个并发数,-n 代表一共请求 10000 次,整个过程大概会持续十几秒。接下来,我们打开一个新的命令行窗口,使用下面的命令来持续监控 Pod 的状态:

$ kubectl get pods --watch
NAME                                 READY   STATUS    RESTARTS   AGE
hello-world-flask-64dd645c57-9x869   1/1     Running   0          4m6s
hello-world-flask-64dd645c57-vw8nc   0/1     Pending   0          0s
hello-world-flask-64dd645c57-46b6s   0/1     ContainerCreating   0          0s
hello-world-flask-64dd645c57-vw8nc   1/1     Running             0          18s

这里参数 --watch 表示持续监听 Pod 状态变化。在 ab 压力测试的过程中,会不断创建新的 Pod 副本,这说明 K8s 已经感知到了 Pod 的业务压力,并且正在自动进行横向扩容。

5. 其他

  1. Kubernetes HPA:Vertical Pod Autoscaler 根据 CPU 利用率增加或减少复制控制器、部署、副本集或有状态集中的 pod 数量——缩放是水平的
  2. Kubernetes VPA:Horizo​​ntal Pod Autoscaler 增加和减少容器 CPU 和内存资源配置,以使集群资源分配与实际使用情况保持一致。
  3. Kubernetes CA :Cluster Autoscaler 根据 pod 的资源请求自动添加或删除集群中的节点。
12-23 15:42