『宝藏代码胶囊开张啦!』—— 我的 CodeCapsule 来咯!✨写代码不再头疼!我的新站点 CodeCapsule 主打一个 “白菜价”+“量身定制”!无论是卡脖子的毕设/课设/文献复现,需要灵光一现的算法改进,还是想给项目加个“外挂”,这里都有便宜又好用的代码方案等你发现!低成本,高适配,助你轻松通关!速来围观 👉 CodeCapsule官网

应用部署与滚动更新:Kubernetes 零停机发布实战

一、引言:从传统部署到云原生发布的演进

在传统的应用部署中,发布新版本往往意味着停机维护——停止旧版本、部署新版本、重启服务,整个过程需要数分钟甚至更久。对于追求高可用的现代应用,停机就意味着用户体验下降、业务收入损失。而容器化与容器编排技术的出现,彻底改变了这一局面。

滚动更新(Rolling Update) 是 Kubernetes 提供的核心发布策略,它允许你逐步用新版本替换旧版本,在整个更新过程中始终保持应用在线,实现零停机发布。同时,Kubernetes 还提供了自动回滚、版本历史管理等机制,让发布变得安全可控。

然而,仅仅知道 kubectl set image 是不够的。如何配置更新策略以平衡速度和稳定性?如何监控更新过程?当更新失败时如何快速回滚?蓝绿部署、金丝雀发布与滚动更新的关系是什么?本文将从一个 Python Flask 应用 的实战出发,深入讲解 Kubernetes 中 Deployment 的滚动更新机制,并运用多阶段构建优化镜像体积,帮助你掌握生产级应用发布的完整流程。

二、核心概念:Deployment 与滚动更新机制

2.1 Deployment:声明式应用管理

Deployment 是 Kubernetes 中管理无状态应用的核心控制器。它通过 ReplicaSet 确保指定数量的 Pod 副本始终运行,并支持声明式的更新策略。当你想更新应用版本时,只需修改 Deployment 的 Pod 模板(如镜像标签),Deployment 会自动触发滚动更新。

Deployment

ReplicaSet v1

ReplicaSet v2

Pod 1 v1

Pod 2 v1

Pod 3 v2

Pod 4 v2

2.2 ReplicaSet:版本快照

每次 Deployment 的 Pod 模板发生变化,都会创建一个新的 ReplicaSet,并保留旧的 ReplicaSet。这不仅实现了版本追踪,还让回滚变得简单——只需将 Deployment 的 spec 指回旧的 ReplicaSet。

2.3 滚动更新策略参数

滚动更新的行为由 strategy.rollingUpdate 下的两个参数控制:

  • maxSurge:更新过程中,可以超出期望副本数的最大 Pod 数量。可以是绝对数(如 2)或百分比(如 25%)。默认 25%。
  • maxUnavailable:更新过程中,可以不可用的最大 Pod 数量。同样可以是绝对数或百分比。默认 25%。

这两个参数共同决定了更新速度和可用性之间的平衡。例如,对于 10 个副本的应用,maxSurge=25% 允许最多额外创建 2 个新 Pod,maxUnavailable=25% 允许最多 2 个旧 Pod 提前终止。

ReplicaSet v2 ReplicaSet v1 Kubernetes ReplicaSet v2 ReplicaSet v1 Kubernetes 开始滚动更新 等待新 Pod Ready 重复直到所有旧 Pod 被替换 创建 1 个新 Pod (maxSurge) 终止 1 个旧 Pod (maxUnavailable) 创建下一个新 Pod 终止下一个旧 Pod

2.4 更新状态与历史记录

kubectl rollout status 可以监控更新进度,kubectl rollout history 查看版本历史。每次更新都会记录 Revision,方便回滚。

三、实战演练:滚动更新 Python Flask 应用

本节将创建一个简单的 Flask 应用,通过两个版本(v1 和 v2)演示滚动更新的完整流程。

3.1 项目结构

rolling-update-demo/
├── app-v1/
│   ├── app.py              # v1 版本 Flask 应用
│   ├── requirements.txt
│   ├── Dockerfile          # 多阶段构建
│   └── .dockerignore
├── app-v2/                 # v2 版本(仅修改返回消息)
│   ├── app.py
│   ├── requirements.txt
│   ├── Dockerfile
│   └── .dockerignore
├── k8s/
│   ├── namespace.yaml      # 命名空间
│   ├── deployment.yaml     # Deployment 定义
│   └── service.yaml        # Service 暴露应用
└── README.md

3.2 Flask 应用代码

app-v1/app.py

import os
import socket
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def hello():
    hostname = socket.gethostname()
    return jsonify({
        'version': 'v1',
        'message': 'Hello from v1',
        'hostname': hostname
    })

@app.route('/health')
def health():
    return jsonify({'status': 'healthy'})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

app-v2/app.py(仅修改版本号):

import os
import socket
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def hello():
    hostname = socket.gethostname()
    return jsonify({
        'version': 'v2',
        'message': 'Hello from v2',
        'hostname': hostname
    })

@app.route('/health')
def health():
    return jsonify({'status': 'healthy'})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

app-v1/requirements.txtapp-v2/requirements.txt 相同:

flask==2.3.3
gunicorn==21.2.0

3.3 多阶段构建 Dockerfile

app-v1/Dockerfile(v2 相同):

# 构建阶段
FROM python:3.11-slim AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# 运行阶段
FROM python:3.11-slim

# 创建非root用户
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 --gid 1001 --no-create-home appuser

WORKDIR /app
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH

COPY . .
RUN chown -R appuser:appgroup /app

USER appuser
EXPOSE 5000
HEALTHCHECK CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')" || exit 1

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app:app"]

app-v1/.dockerignore

__pycache__
*.pyc
.env
.git
README.md
Dockerfile
.dockerignore

3.4 构建并推送镜像

# 构建 v1 和 v2 镜像
docker build -t your-registry/rolling-demo:v1 ./app-v1
docker build -t your-registry/rolling-demo:v2 ./app-v2

# 推送到镜像仓库
docker push your-registry/rolling-demo:v1
docker push your-registry/rolling-demo:v2

3.5 Kubernetes 资源配置

k8s/namespace.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: rolling-demo

k8s/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rolling-demo
  namespace: rolling-demo
spec:
  replicas: 4
  selector:
    matchLabels:
      app: rolling-demo
  # 滚动更新策略
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1          # 最多额外创建1个新Pod
      maxUnavailable: 0    # 不允许旧Pod不可用(保证零停机)
  template:
    metadata:
      labels:
        app: rolling-demo
    spec:
      containers:
      - name: app
        image: your-registry/rolling-demo:v1
        ports:
        - containerPort: 5000
        resources:
          requests:
            memory: "64Mi"
            cpu: "100m"
          limits:
            memory: "128Mi"
            cpu: "200m"
        readinessProbe:
          httpGet:
            path: /health
            port: 5000
          initialDelaySeconds: 5
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /health
            port: 5000
          initialDelaySeconds: 15
          periodSeconds: 10

k8s/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: rolling-demo-service
  namespace: rolling-demo
spec:
  selector:
    app: rolling-demo
  ports:
  - port: 80
    targetPort: 5000
  type: NodePort

3.6 部署 v1 版本

# 创建命名空间
kubectl apply -f k8s/namespace.yaml

# 部署 v1
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml

# 查看状态
kubectl get pods -n rolling-demo -w
kubectl get svc -n rolling-demo

# 获取 NodePort
NODE_PORT=$(kubectl get svc rolling-demo-service -n rolling-demo -o jsonpath='{.spec.ports[0].nodePort}')
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[0].address}')

# 测试访问
curl http://${NODE_IP}:${NODE_PORT}/

3.7 执行滚动更新到 v2

在一个终端持续请求,观察是否中断:

while true; do curl -s http://${NODE_IP}:${NODE_PORT}/ | grep version; sleep 0.5; done

在另一个终端触发更新:

# 修改镜像版本为 v2
kubectl set image deployment/rolling-demo app=your-registry/rolling-demo:v2 -n rolling-demo

# 监控更新状态
kubectl rollout status deployment/rolling-demo -n rolling-demo

# 查看 ReplicaSet 历史
kubectl rollout history deployment/rolling-demo -n rolling-demo

观察持续请求的终端,你会看到版本从 v1 逐渐变成 v2,但整个过程没有出现请求失败(除极短暂的重试外,通常零停机)。

3.8 回滚到 v1

如果新版本有问题,可以快速回滚:

# 回滚到上一个版本
kubectl rollout undo deployment/rolling-demo -n rolling-demo

# 回滚到指定版本(通过 history 查看 revision)
kubectl rollout undo deployment/rolling-demo --to-revision=1 -n rolling-demo

四、进阶优化:滚动更新最佳实践

4.1 合理设置 maxSurge 和 maxUnavailable

场景 maxSurge maxUnavailable 说明
零停机生产发布 1 (或 25%) 0 确保任何时候都有足够的 Pod 处理流量
快速更新(可容忍少量中断) 25% 25% 平衡速度与可用性
资源紧张环境 0 1 避免额外资源占用,但会有短暂容量下降

4.2 结合就绪探针(Readiness Probe)

滚动更新的核心是等待新 Pod 就绪后才终止旧 Pod。因此必须为容器配置正确的 readinessProbe,确保新 Pod 真正能够接收流量后才开始替换。

4.3 暂停与恢复更新

对于复杂更新(如需要手工验证的批次),可以暂停更新:

kubectl rollout pause deployment/rolling-demo -n rolling-demo
# 手工验证部分新 Pod
kubectl rollout resume deployment/rolling-demo -n rolling-demo

4.4 金丝雀发布(Canary Release)

滚动更新是自动的渐进式发布。但如果你想更精细地控制流量比例(如只让 10% 的流量流向新版本),可以结合 Service 和多个 Deployment 实现金丝雀发布:

90%

10%

Service

Deployment stable

Deployment canary

或者使用 Service Mesh(如 Istio)实现更精细的流量控制。

4.5 蓝绿部署(Blue-Green Deployment)

蓝绿部署是另一种发布策略:同时运行两套环境(蓝和绿),通过切换 Service 的 selector 实现瞬间切换。Kubernetes 中可以简单实现:

# 部署绿色版本
kubectl apply -f deployment-green.yaml
# 待绿色版本就绪后,修改 Service 的 selector 指向绿色
kubectl patch service rolling-demo-service -p '{"spec":{"selector":{"app":"rolling-demo","version":"v2"}}}'

4.6 多阶段构建收益

与之前系列一致,多阶段构建将镜像体积从 400MB+ 优化至 82MB,加速拉取和启动,从而缩短滚动更新总时长。

构建方式 基础镜像 最终镜像大小
单阶段 python:3.11 912 MB
单阶段 slim python:3.11-slim 412 MB
多阶段 python:3.11-slim 82 MB

五、效果验证

5.1 零停机验证

在滚动更新过程中持续请求,可以看到响应中的 version 字段从 v1 逐渐变为 v2,但从未出现连接错误(如 curl: (52) Empty reply from server)。

5.2 滚动速度控制

观察 Pod 的变化:

kubectl get pods -n rolling-demo -w

你会看到新 Pod 逐个创建并变为 Running,旧 Pod 逐个 Terminating,新旧 Pod 会短暂共存。

5.3 回滚验证

模拟新版本故障(例如故意让 v2 的 /health 返回 500),然后观察滚动更新是否会卡住,并测试回滚功能。

六、完整代码

6.1 app-v1/app.py

(见上文)

6.2 app-v1/requirements.txt

flask==2.3.3
gunicorn==21.2.0

6.3 app-v1/Dockerfile

(见上文)

6.4 app-v1/.dockerignore

(见上文)

6.5 app-v2/ 对应文件(略)

6.6 k8s/namespace.yaml

(见上文)

6.7 k8s/deployment.yaml

(见上文)

6.8 k8s/service.yaml

(见上文)

七、总结与最佳实践清单

通过本文的实战,我们掌握了 Kubernetes 中 Deployment 的滚动更新机制,并通过一个 Python Flask 应用演示了零停机发布的全过程。以下是 应用部署与滚动更新的最佳实践清单

  • 配置就绪探针:确保新 Pod 真正就绪后才接收流量。
  • 合理设置滚动更新参数:生产环境推荐 maxSurge=25%maxUnavailable=0maxSurge=1maxUnavailable=0
  • 监控更新过程:使用 kubectl rollout status 和事件监控。
  • 保留版本历史:不要手动删除旧的 ReplicaSet,以便快速回滚。
  • 回滚准备:确保回滚操作已经演练,关键应用可考虑蓝绿部署。
  • 结合金丝雀发布:对于重大变更,先让少量流量验证。
  • 镜像优化:多阶段构建减小体积,加速拉取和启动。
  • 资源限制:设置 requests 和 limits,保证更新过程集群稳定。

滚动更新是 Kubernetes 保障服务连续性的基石。掌握这一技能后,你可以自信地将应用的发布流程自动化,实现快速迭代与稳定运行的完美平衡。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐