扩缩应用并实现负载均衡

KubernetesBeginner
立即练习

简介

Service 为一组 Pod 提供一个稳定的访问地址。当请求量发生变化时,这一点尤其重要:Deployment 可以增加或减少副本,而 Service 的身份保持不变。

在本实验中,每个后端都会返回自身所在 Pod 的主机名。你将把副本数从 2 个扩展到 4 个,通过一个 Service 发送彼此独立的请求,并观察来自多个 Pod 的响应。随后,你会将副本数缩减回 2 个,并观察 Deployment 与 EndpointSlice 如何逐步收敛到新的期望状态。

这是手动水平扩缩容。基于 HPA 的自动扩缩容依赖资源请求、指标和控制策略,因此应在你充分理解手动调整副本行为之后再学习。

构建可观测的副本应用

环境启动: 本实验会为你启动一个完整的 Kubernetes 集群。配置控制平面、节点和网络组件通常需要 2~3 分钟。请耐心等待环境加载完成后再开始操作。

在本步骤中,你将创建两个能够显示具体由哪个 Pod 处理请求的后端。这样一来,Service 的流量分配就不再抽象,而是可以直接观察。

第一条命令使用 cd 进入已准备好的工作目录。下一条命令使用 Here Documentcat <<'EOF' > hostname-web.yaml 会将后续每一行内容写入 YAML 文件,直到遇到结束标记 EOF,并覆盖文件原有内容。

cd /home/labex/project/scale-lab
cat <<'EOF' > hostname-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hostname-web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hostname-web
  template:
    metadata:
      labels:
        app: hostname-web
    spec:
      containers:
        - name: web
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c"]
          args:
            - mkdir -p /www; hostname > /www/index.html; exec httpd -f -p 8080 -h /www
          ports:
            - name: http
              containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: hostname-web
spec:
  selector:
    app: hostname-web
  ports:
    - name: http
      port: 80
      targetPort: http
EOF
kubectl apply -f hostname-web.yaml
kubectl rollout status deployment/hostname-web --timeout=60s
kubectl get pods -l app=hostname-web

结束标记 EOF 之后,kubectl apply -f 会将文件中的所有对象提交给 API 服务器。rollout status 最多等待 60 秒,直到两个期望的 Pod 都准备就绪;-l app=hostname-web 则只列出带有该标签的 Pod。

容器命令中的分号会按顺序执行多个操作:创建 /www,将主机名重定向写入 index.html,然后使用 exec 让 Web 服务器成为容器的主进程。两个 Pod 的名称不同,因此每个后端返回的页面内容也不同。

建立基线后端

在本步骤中,你将把 Deployment 的副本数量与 Service 的就绪后端数量对应起来。

查看三个相互关联的视图。get deployment 显示期望副本数和就绪副本数;-l 用于选择应用 Pod,-o wide 会额外显示 IP 和节点列;最后的标签选择器会找到为该 Service 创建的 EndpointSlice:

kubectl get deployment hostname-web
kubectl get pods -l app=hostname-web -o wide
kubectl get endpointslices -l kubernetes.io/service-name=hostname-web

Deployment 应显示 2/2,EndpointSlice 应显示两个地址。接下来,从一个持续运行的客户端 Pod 多次请求该 Service。

由于设置了 --restart=Neverkubectl run 会创建一个独立 Pod。分隔符 -- 表示 kubectl 选项到此结束;sleep 3600 是让容器保持运行的命令。for 循环使用 seq 1 6 生成 6 次迭代,而 kubectl exec POD -- COMMAND 则在客户端内部每次执行 wget

kubectl run load-client --image=busybox:1.36 --image-pull-policy=IfNotPresent --restart=Never -- sleep 3600
kubectl wait --for=condition=Ready pod/load-client --timeout=30s
for i in $(seq 1 6); do kubectl exec load-client -- wget -qO- http://hostname-web; done

每次响应都是一个 Pod 名称。在这么短的样本中,你可能只看到其中一个名称,也可能看到两个名称;Service 的流量分配并不保证严格按照轮询顺序进行。

以声明方式扩容

在本步骤中,你将把保存的期望状态从 2 个副本改为 4 个副本。编辑清单文件可以确保文件内容与线上对象保持一致。

sed -i 's/old/new/' file 会直接在文件中替换匹配的文本。随后,grep -n 搜索 replicas: 并显示其行号,让你在应用修改前快速确认结果:

cd /home/labex/project/scale-lab
sed -i 's/replicas: 2/replicas: 4/' hostname-web.yaml
grep -n 'replicas:' hostname-web.yaml
kubectl apply -f hostname-web.yaml
kubectl rollout status deployment/hostname-web --timeout=60s
kubectl get deployment hostname-web

Deployment 应显示 4/4。由于实际状态低于新的期望状态,控制器会额外创建两个 Pod。

观察 Service 后端集合扩展

在本步骤中,你将验证未发生变化的 Service 是否会自动发现新创建的 Pod。

EndpointSlice 命令使用 JSONPath,因为普通表格输出可能会省略部分细节。range 会为每个端点重复执行模板;每次重复都会打印第一个地址、字面量 ready、就绪状态值以及换行符。反斜杠用于将一条 Shell 命令延续到显示上的下一行:

kubectl get pods -l app=hostname-web -o wide
kubectl get endpointslices -l kubernetes.io/service-name=hostname-web \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'

现在应该有 4 个就绪地址。你没有修改 Service,因为它的选择器仍然能够匹配所有带有 app=hostname-web 标签且处于就绪状态的 Pod。

对比稳定的 Service IP 与扩展后的后端集合:

kubectl get service hostname-web -o wide

ClusterIP 保持稳定,而 EndpointSlice 中的成员会发生变化。

观察请求到达多个 Pod

在本步骤中,你将通过一个 Service 发送彼此独立的 HTTP 请求,并统计返回响应的 Pod 主机名。

首先,rm -f 会删除已有的结果文件;如果文件不存在,-f 也不会报错。循环会发送 20 个请求。>> 将每个主机名追加到文件中,而不会覆盖之前的结果。最后,管道将排序后的内容交给 uniq -c,它会合并相邻的重复行,并在每个主机名前显示出现次数:

rm -f /tmp/hostname-responses.txt
for i in $(seq 1 20); do
  kubectl exec load-client -- wget -qO- http://hostname-web >> /tmp/hostname-responses.txt
done
sort /tmp/hostname-responses.txt | uniq -c

你应该能看到多个主机名。计数可能不均衡,而且一次较短的请求序列不一定会命中全部 4 个后端。Kubernetes Service 会分配连接流量,但不会保证序列完全均匀或严格有序。

确认本次样本命中了多少个不同的后端。sort -u 为每个主机名保留一份副本,管道将这些行传给 wc -l,而 wc -l 会统计行数:

sort -u /tmp/hostname-responses.txt | wc -l

如果结果大于 1,就能直接证明:稳定的 Service 地址将请求路由到了多个 Pod。

以命令方式缩容

在本步骤中,你将使用 kubectl scale,把副本数从 4 个快速调整回 2 个。

kubectl scale 会立即修改线上 Deployment 的期望副本数。--replicas=2 指定新的副本数量,但不会编辑 YAML 文件:

kubectl scale deployment/hostname-web --replicas=2
kubectl rollout status deployment/hostname-web --timeout=60s
kubectl get pods -l app=hostname-web

Kubernetes 会终止两个 Pod,并保留两个 Pod。请等待 Service 的后端集合也完成收敛。

下面这个有上限的轮询循环最多尝试 30 次。每次循环都会将就绪端点数量保存到 count 中;jq 会筛选 EndpointSlice JSON 中处于就绪状态的端点,并返回数组长度。[ "$count" -eq 2 ] 是 Shell 中的数值测试,&& break 会在测试成功时退出循环,sleep 1 则会在重试前暂停 1 秒:

for i in $(seq 1 30); do
  count=$(kubectl get endpointslices -l kubernetes.io/service-name=hostname-web -o json | jq '[.items[].endpoints[] | select(.conditions.ready == true)] | length')
  [ "$count" -eq 2 ] && break
  sleep 1
done
echo "Ready backends: $count"

此时,线上 Deployment 已经请求 2 个副本,但文件中仍然写着 4 个副本。这种差异是下一步骤的有意安排。

使清单文件重新一致,并查看控制器记录

在本步骤中,你将让保存的清单文件与线上 2 副本状态保持一致,并把扩缩容操作与控制器记录联系起来。

首先比较两种期望状态。grep -n 显示文件中的对应行;JSONPath 只提取线上对象的 .spec.replicas 字段,并添加换行符:

grep -n 'replicas:' /home/labex/project/scale-lab/hostname-web.yaml
kubectl get deployment hostname-web -o jsonpath='Live replicas: {.spec.replicas}{"\n"}'

将文件中的副本数从 4 改回 2,然后应用修改:

sed -i 's/replicas: 4/replicas: 2/' /home/labex/project/scale-lab/hostname-web.yaml
kubectl apply -f /home/labex/project/scale-lab/hostname-web.yaml

由于线上状态已经是 2 个副本,此次应用修改不应再创建 Pod。接下来查看 Deployment 事件。管道会将完整的 describe 输出传给 sed -n/Events:/,$p 表示「从包含 Events: 的行开始,一直打印到末尾」:

kubectl describe deployment hostname-web | sed -n '/Events:/,$p'

查找显示扩容与缩容决策的 ScalingReplicaSet 消息。最后删除临时客户端。--ignore-not-found 可以确保即使该 Pod 已经消失,清理操作也能成功:

kubectl delete pod load-client --ignore-not-found

手动扩缩容会修改期望副本数;Deployment 控制器负责创建或终止 Pod,而 Service 会自动跟踪处于就绪状态的成员。让清单文件与线上状态保持同步,可以避免之后执行 kubectl apply 时意外恢复旧的副本数量。

总结

你手动将 Deployment 从 2 个副本扩展到 4 个,然后又缩减回 2 个。你观察了控制器如何协调 Pod,看到 EndpointSlice 如何在不修改 Service 的情况下跟随就绪后端变化,并直接验证了一个 Service 地址如何将独立请求路由到多个 Pod。

需要牢牢记住的模型是:副本数代表期望状态,Deployment 控制器负责协调实际的 Pod,而 Service 会跟踪带有匹配标签且处于就绪状态的后端。对于声明式文件,应当在有意修改线上状态后及时同步清单内容,这样未来执行应用操作时才能保持可预测性。