目录
- 应用部署与滚动更新:Kubernetes 零停机发布实战
-
- 一、引言:从传统部署到云原生发布的演进
- 二、核心概念:Deployment 与滚动更新机制
-
- 2.1 Deployment:声明式应用管理
- 2.2 ReplicaSet:版本快照
- 2.3 滚动更新策略参数
- 2.4 更新状态与历史记录
- 三、实战演练:滚动更新 Python Flask 应用
-
- 3.1 项目结构
- 3.2 Flask 应用代码
- 3.3 多阶段构建 Dockerfile
- 3.4 构建并推送镜像
- 3.5 Kubernetes 资源配置
- 3.6 部署 v1 版本
- 3.7 执行滚动更新到 v2
- 3.8 回滚到 v1
- 四、进阶优化:滚动更新最佳实践
-
- 4.1 合理设置 maxSurge 和 maxUnavailable
- 4.2 结合就绪探针(Readiness Probe)
- 4.3 暂停与恢复更新
- 4.4 金丝雀发布(Canary Release)
- 4.5 蓝绿部署(Blue-Green Deployment)
- 4.6 多阶段构建收益
- 五、效果验证
-
- 5.1 零停机验证
- 5.2 滚动速度控制
- 5.3 回滚验证
- 六、完整代码
-
- 6.1 app-v1/app.py
- 6.2 app-v1/requirements.txt
- 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
- 七、总结与最佳实践清单
『宝藏代码胶囊开张啦!』—— 我的 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 会自动触发滚动更新。
#mermaid-svg-04xWqOvXsAyQ26DG{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-04xWqOvXsAyQ26DG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-04xWqOvXsAyQ26DG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-04xWqOvXsAyQ26DG .error-icon{fill:#552222;}#mermaid-svg-04xWqOvXsAyQ26DG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-04xWqOvXsAyQ26DG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-04xWqOvXsAyQ26DG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-04xWqOvXsAyQ26DG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-04xWqOvXsAyQ26DG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-04xWqOvXsAyQ26DG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-04xWqOvXsAyQ26DG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-04xWqOvXsAyQ26DG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-04xWqOvXsAyQ26DG .marker.cross{stroke:#333333;}#mermaid-svg-04xWqOvXsAyQ26DG svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-04xWqOvXsAyQ26DG p{margin:0;}#mermaid-svg-04xWqOvXsAyQ26DG .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-04xWqOvXsAyQ26DG .cluster-label text{fill:#333;}#mermaid-svg-04xWqOvXsAyQ26DG .cluster-label span{color:#333;}#mermaid-svg-04xWqOvXsAyQ26DG .cluster-label span p{background-color:transparent;}#mermaid-svg-04xWqOvXsAyQ26DG .label text,#mermaid-svg-04xWqOvXsAyQ26DG span{fill:#333;color:#333;}#mermaid-svg-04xWqOvXsAyQ26DG .node rect,#mermaid-svg-04xWqOvXsAyQ26DG .node circle,#mermaid-svg-04xWqOvXsAyQ26DG .node ellipse,#mermaid-svg-04xWqOvXsAyQ26DG .node polygon,#mermaid-svg-04xWqOvXsAyQ26DG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-04xWqOvXsAyQ26DG .rough-node .label text,#mermaid-svg-04xWqOvXsAyQ26DG .node .label text,#mermaid-svg-04xWqOvXsAyQ26DG .image-shape .label,#mermaid-svg-04xWqOvXsAyQ26DG .icon-shape .label{text-anchor:middle;}#mermaid-svg-04xWqOvXsAyQ26DG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-04xWqOvXsAyQ26DG .rough-node .label,#mermaid-svg-04xWqOvXsAyQ26DG .node .label,#mermaid-svg-04xWqOvXsAyQ26DG .image-shape .label,#mermaid-svg-04xWqOvXsAyQ26DG .icon-shape .label{text-align:center;}#mermaid-svg-04xWqOvXsAyQ26DG .node.clickable{cursor:pointer;}#mermaid-svg-04xWqOvXsAyQ26DG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-04xWqOvXsAyQ26DG .arrowheadPath{fill:#333333;}#mermaid-svg-04xWqOvXsAyQ26DG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-04xWqOvXsAyQ26DG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-04xWqOvXsAyQ26DG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-04xWqOvXsAyQ26DG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-04xWqOvXsAyQ26DG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-04xWqOvXsAyQ26DG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-04xWqOvXsAyQ26DG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-04xWqOvXsAyQ26DG .cluster text{fill:#333;}#mermaid-svg-04xWqOvXsAyQ26DG .cluster span{color:#333;}#mermaid-svg-04xWqOvXsAyQ26DG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-04xWqOvXsAyQ26DG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-04xWqOvXsAyQ26DG rect.text{fill:none;stroke-width:0;}#mermaid-svg-04xWqOvXsAyQ26DG .icon-shape,#mermaid-svg-04xWqOvXsAyQ26DG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-04xWqOvXsAyQ26DG .icon-shape p,#mermaid-svg-04xWqOvXsAyQ26DG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-04xWqOvXsAyQ26DG .icon-shape rect,#mermaid-svg-04xWqOvXsAyQ26DG .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-04xWqOvXsAyQ26DG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-04xWqOvXsAyQ26DG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-04xWqOvXsAyQ26DG :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
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
#mermaid-svg-flIAJviTfbhJo9bt{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-flIAJviTfbhJo9bt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-flIAJviTfbhJo9bt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-flIAJviTfbhJo9bt .error-icon{fill:#552222;}#mermaid-svg-flIAJviTfbhJo9bt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-flIAJviTfbhJo9bt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-flIAJviTfbhJo9bt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-flIAJviTfbhJo9bt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-flIAJviTfbhJo9bt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-flIAJviTfbhJo9bt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-flIAJviTfbhJo9bt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-flIAJviTfbhJo9bt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-flIAJviTfbhJo9bt .marker.cross{stroke:#333333;}#mermaid-svg-flIAJviTfbhJo9bt svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-flIAJviTfbhJo9bt p{margin:0;}#mermaid-svg-flIAJviTfbhJo9bt .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-flIAJviTfbhJo9bt text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-flIAJviTfbhJo9bt .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-flIAJviTfbhJo9bt .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-flIAJviTfbhJo9bt .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-flIAJviTfbhJo9bt .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-flIAJviTfbhJo9bt #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-flIAJviTfbhJo9bt .sequenceNumber{fill:white;}#mermaid-svg-flIAJviTfbhJo9bt #sequencenumber{fill:#333;}#mermaid-svg-flIAJviTfbhJo9bt #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-flIAJviTfbhJo9bt .messageText{fill:#333;stroke:none;}#mermaid-svg-flIAJviTfbhJo9bt .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-flIAJviTfbhJo9bt .labelText,#mermaid-svg-flIAJviTfbhJo9bt .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-flIAJviTfbhJo9bt .loopText,#mermaid-svg-flIAJviTfbhJo9bt .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-flIAJviTfbhJo9bt .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-flIAJviTfbhJo9bt .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-flIAJviTfbhJo9bt .noteText,#mermaid-svg-flIAJviTfbhJo9bt .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-flIAJviTfbhJo9bt .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-flIAJviTfbhJo9bt .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-flIAJviTfbhJo9bt .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-flIAJviTfbhJo9bt .actorPopupMenu{position:absolute;}#mermaid-svg-flIAJviTfbhJo9bt .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-flIAJviTfbhJo9bt .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-flIAJviTfbhJo9bt .actor-man circle,#mermaid-svg-flIAJviTfbhJo9bt line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-flIAJviTfbhJo9bt :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
开始滚动更新
等待新 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.txt 和 app-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
| 零停机生产发布 | 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 实现金丝雀发布:
#mermaid-svg-hEcn8W5Ah26v61Vn{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hEcn8W5Ah26v61Vn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hEcn8W5Ah26v61Vn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hEcn8W5Ah26v61Vn .error-icon{fill:#552222;}#mermaid-svg-hEcn8W5Ah26v61Vn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hEcn8W5Ah26v61Vn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hEcn8W5Ah26v61Vn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hEcn8W5Ah26v61Vn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hEcn8W5Ah26v61Vn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hEcn8W5Ah26v61Vn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hEcn8W5Ah26v61Vn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hEcn8W5Ah26v61Vn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hEcn8W5Ah26v61Vn .marker.cross{stroke:#333333;}#mermaid-svg-hEcn8W5Ah26v61Vn svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hEcn8W5Ah26v61Vn p{margin:0;}#mermaid-svg-hEcn8W5Ah26v61Vn .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-hEcn8W5Ah26v61Vn .cluster-label text{fill:#333;}#mermaid-svg-hEcn8W5Ah26v61Vn .cluster-label span{color:#333;}#mermaid-svg-hEcn8W5Ah26v61Vn .cluster-label span p{background-color:transparent;}#mermaid-svg-hEcn8W5Ah26v61Vn .label text,#mermaid-svg-hEcn8W5Ah26v61Vn span{fill:#333;color:#333;}#mermaid-svg-hEcn8W5Ah26v61Vn .node rect,#mermaid-svg-hEcn8W5Ah26v61Vn .node circle,#mermaid-svg-hEcn8W5Ah26v61Vn .node ellipse,#mermaid-svg-hEcn8W5Ah26v61Vn .node polygon,#mermaid-svg-hEcn8W5Ah26v61Vn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hEcn8W5Ah26v61Vn .rough-node .label text,#mermaid-svg-hEcn8W5Ah26v61Vn .node .label text,#mermaid-svg-hEcn8W5Ah26v61Vn .image-shape .label,#mermaid-svg-hEcn8W5Ah26v61Vn .icon-shape .label{text-anchor:middle;}#mermaid-svg-hEcn8W5Ah26v61Vn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hEcn8W5Ah26v61Vn .rough-node .label,#mermaid-svg-hEcn8W5Ah26v61Vn .node .label,#mermaid-svg-hEcn8W5Ah26v61Vn .image-shape .label,#mermaid-svg-hEcn8W5Ah26v61Vn .icon-shape .label{text-align:center;}#mermaid-svg-hEcn8W5Ah26v61Vn .node.clickable{cursor:pointer;}#mermaid-svg-hEcn8W5Ah26v61Vn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hEcn8W5Ah26v61Vn .arrowheadPath{fill:#333333;}#mermaid-svg-hEcn8W5Ah26v61Vn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hEcn8W5Ah26v61Vn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hEcn8W5Ah26v61Vn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hEcn8W5Ah26v61Vn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hEcn8W5Ah26v61Vn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hEcn8W5Ah26v61Vn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hEcn8W5Ah26v61Vn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hEcn8W5Ah26v61Vn .cluster text{fill:#333;}#mermaid-svg-hEcn8W5Ah26v61Vn .cluster span{color:#333;}#mermaid-svg-hEcn8W5Ah26v61Vn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hEcn8W5Ah26v61Vn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hEcn8W5Ah26v61Vn rect.text{fill:none;stroke-width:0;}#mermaid-svg-hEcn8W5Ah26v61Vn .icon-shape,#mermaid-svg-hEcn8W5Ah26v61Vn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hEcn8W5Ah26v61Vn .icon-shape p,#mermaid-svg-hEcn8W5Ah26v61Vn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hEcn8W5Ah26v61Vn .icon-shape rect,#mermaid-svg-hEcn8W5Ah26v61Vn .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hEcn8W5Ah26v61Vn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hEcn8W5Ah26v61Vn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hEcn8W5Ah26v61Vn :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
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=0 或 maxSurge=1、maxUnavailable=0。
- ✅ 监控更新过程:使用 kubectl rollout status 和事件监控。
- ✅ 保留版本历史:不要手动删除旧的 ReplicaSet,以便快速回滚。
- ✅ 回滚准备:确保回滚操作已经演练,关键应用可考虑蓝绿部署。
- ✅ 结合金丝雀发布:对于重大变更,先让少量流量验证。
- ✅ 镜像优化:多阶段构建减小体积,加速拉取和启动。
- ✅ 资源限制:设置 requests 和 limits,保证更新过程集群稳定。
滚动更新是 Kubernetes 保障服务连续性的基石。掌握这一技能后,你可以自信地将应用的发布流程自动化,实现快速迭代与稳定运行的完美平衡。

