DAY 02 —— 部署 Pod 的几种方式(全流程实操)
【学习目标】
1. 分清"裸 Pod / Deployment / DaemonSet / StatefulSet / Job / 静态 Pod"
2. 每种方式都能独立写出 YAML 并跑起来、验证、清理
3. 掌握探针、资源限制、调度约束、滚动更新与回滚
4. 建立"生产不裸跑 Pod"的正确直觉
----------------------------------------------------------------
零、先记住这张选型表
----------------------------------------------------------------
需求场景 用哪个
-------------------------------------- ----------------------
一次性调试、验证镜像 kubectl run(裸 Pod)
无状态 Web/API,要滚动更新、要扩缩容 Deployment ★最常用
每个节点必须跑一个(日志/监控/CNI agent) DaemonSet
有状态、需稳定域名和独立存储(DB/MQ) StatefulSet
跑完就结束的批处理 Job
定时任务(每天备份、每小时对账) CronJob
kubelet 直接管理的系统组件 静态 Pod(/etc/kubernetes/manifests)
需要固定 Pod IP、单实例且要保留 IP 裸 Pod / StatefulSet + Headless
----------------------------------------------------------------
一、方式 1:kubectl run(命令式,最快,仅调试)
----------------------------------------------------------------
# 最简
kubectl run web --image=nginx:1.27
# 带端口、副本(注意:1.18+ 的 run 不再支持 --replicas,副本用 scale 或 deploy)
kubectl run web --image=nginx:1.27 --port=80
# 交互式临时 Pod,退出即删(排障神器)
kubectl run tmp --rm -it --image=busybox:1.36 --restart=Never -- sh
# 容器内测试集群 DNS:
# nslookup kubernetes.default.svc.cluster.local
# wget -qO- http://web
# 生成 YAML 而不真正创建(--dry-run=client -o yaml)
kubectl run web --image=nginx:1.27 --port=80 \
--dry-run=client -o yaml > web-pod.yaml
查看与清理:
kubectl get pod web -o wide
kubectl describe pod web
kubectl delete pod web
要点:
- 裸 Pod 没有控制器,删了不重建、节点故障不迁移
- --dry-run=client -o yaml 是写 YAML 的官方"脚手架"技巧,后面每种资源都能这么干
----------------------------------------------------------------
二、方式 2:直接 apply 一个 Pod YAML(声明式基础)
----------------------------------------------------------------
cat > pod-nginx.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
tier: frontend
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- name: http
containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
env:
- name: TZ
value: Asia/Shanghai
EOF
kubectl apply -f pod-nginx.yaml
kubectl get pod nginx -o yaml # 看 apiserver 补全的字段(status 等)
kubectl get pod nginx -o jsonpath='{.status.podIP}{"\n"}'
kubectl delete -f pod-nginx.yaml
YAML 四段结构(所有 K8s 对象通用):
apiVersion / kind / metadata(name、namespace、labels、annotations)/ spec(期望状态)
你只写 spec,status 由系统写。
----------------------------------------------------------------
三、方式 3:Deployment(生产标准,必须掌握)
----------------------------------------------------------------
3.1 最小可用 Deployment
----------------------------------------------------------------
cat > deploy-web.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 最多超出期望副本 1 个
maxUnavailable: 0 # 升级期间不允许不可用(滚动不中断)
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
resources:
requests: {cpu: 100m, memory: 128Mi}
limits: {cpu: 500m, memory: 256Mi}
EOF
kubectl apply -f deploy-web.yaml
kubectl rollout status deploy/web
kubectl get rs,po -l app=web -o wide
层级关系(务必理解):
Deployment ──管理──> ReplicaSet ──管理──> Pod
每次改镜像模板 → 新建一个 RS → 新 RS 扩、老 RS 缩 → 老 RS 保留(用于回滚)
3.2 暴露服务:Service
----------------------------------------------------------------
cat > svc-web.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web # 关键:靠 label 选中 Pod
ports:
- port: 80 # Service 端口
targetPort: 80 # 容器端口
type: ClusterIP
EOF
kubectl apply -f svc-web.yaml
kubectl get svc web
kubectl get endpointslices -l kubernetes.io/service-name=web # 看后端 Pod IP
三种类型:
- ClusterIP(默认):仅集群内可访问,集群内用 web.default.svc.cluster.local
- NodePort:每节点开 30000-32767 端口,curl <节点IP>:<nodePort>
- LoadBalancer:云上自动建 SLB;裸机可配 MetalLB
集群内验证(用临时 Pod 当客户端):
kubectl run curl --rm -it --image=curlimages/curl --restart=Never -- \
curl -s -o /dev/null -w '%{http_code}\n' http://web
3.3 扩缩容
----------------------------------------------------------------
kubectl scale deploy web --replicas=5
kubectl get po -l app=web -w # 观察实时变化,Ctrl+C 退出
kubectl autoscale deploy web --min=2 --max=10 --cpu-percent=70 # HPA(需 metrics-server)
kubectl get hpa
3.4 滚动更新与回滚(重点实操)
----------------------------------------------------------------
# 改镜像(触发滚动更新)
kubectl set image deploy/web web=nginx:1.28
kubectl rollout status deploy/web
kubectl rollout history deploy/web
kubectl get rs -l app=web # 能看到新旧两个 RS
# 模拟坏版本,观察 maxUnavailable=0 的效果
kubectl set image deploy/web web=nginx:9.9.9
kubectl get po -l app=web -w # 新 Pod ImagePullBackOff,旧 Pod 还在服务
kubectl rollout status deploy/web # 会卡住并提示
kubectl rollout undo deploy/web # 回滚
kubectl rollout status deploy/web
# 回到指定版本
kubectl rollout history deploy/web --revision=2
kubectl rollout undo deploy/web --to-revision=2
# 暂停/恢复(批量改多个字段时避免多次滚动)
kubectl rollout pause deploy/web ... ; kubectl rollout resume deploy/web
# 修改资源限制(会重建 Pod,注意 requests 变化会重新调度)
kubectl set resources deploy/web -c web --limits=cpu=1,memory=512Mi
3.5 探针:让 K8s 知道你的应用到底活没活
----------------------------------------------------------------
cat > deploy-probe.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 2
selector:
matchLabels: {app: api}
template:
metadata:
labels: {app: api}
spec:
containers:
- name: api
image: nginx:1.27
ports: [{containerPort: 80}]
readinessProbe: # 就绪:失败则从 Service 后端摘除,不重启
httpGet: {path: /, port: 80}
initialDelaySeconds: 3
periodSeconds: 5
failureThreshold: 3
livenessProbe: # 存活:失败则重启容器
httpGet: {path: /, port: 80}
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
startupProbe: # 启动:启动慢的应用用,成功后 liveness 才开始计时
tcpSocket: {port: 80}
failureThreshold: 30
periodSeconds: 2
lifecycle:
preStop: # 优雅下线:先从 Endpoints 摘除,再等连接排空
exec:
command: ["sh","-c","sleep 5"]
terminationGracePeriodSeconds: 30
EOF
三种探针取舍:
- 只配 readiness:优雅发布、不过早接流量
- liveness:绝不能配成"依赖数据库挂了就重启我",会导致雪崩
- startupProbe:给 JVM/大模型服务留足启动时间,避免被 liveness 误杀
验证:
kubectl describe pod <pod> | grep -A3 -E 'Readiness|Liveness'
kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[*].ready}'
3.6 配置注入:ConfigMap / Secret
----------------------------------------------------------------
kubectl create configmap app-cfg --from-literal=APP_ENV=prod \
--from-file=nginx.conf
kubectl create secret generic db-secret \
--from-literal=MYSQL_ROOT_PASSWORD='S3cr3t!'
# 在 Deployment 里使用
# envFrom:
# - configMapRef: {name: app-cfg}
# - secretRef: {name: db-secret}
# 或整目录挂载:
# volumeMounts: [{name: cfg, mountPath: /etc/nginx/conf.d}]
# volumes: [{name: cfg, configMap: {name: app-cfg}}]
注意:Secret 默认只是 base64,不是加密。生产要开 etcd 静态加密(EncryptionConfiguration)
+ RBAC 限制 + 外接 KMS。不要把 Secret 提交到 Git。
3.7 生产必备:Pod 反亲和 + 中断预算
----------------------------------------------------------------
# 让 3 个副本尽量分散到不同节点(节点故障不至于全挂)
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels: {app: web}
# PDB:节点维护时至少保留 2 个可用
cat > pdb-web.yaml <<'EOF'
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: {name: web-pdb}
spec:
minAvailable: 2
selector:
matchLabels: {app: web}
EOF
kubectl apply -f pdb-web.yaml
----------------------------------------------------------------
四、方式 4:DaemonSet(每节点一个)
----------------------------------------------------------------
典型用途:日志采集(filebeat)、监控(node-exporter)、CNI、安全 agent。
cat > ds-agent.yaml <<'EOF'
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-agent
namespace: kube-system
spec:
selector:
matchLabels: {app: node-agent}
updateStrategy:
type: RollingUpdate
rollingUpdate: {maxUnavailable: 1}
template:
metadata:
labels: {app: node-agent}
spec:
tolerations: # 允许跑到有污点的 master 上
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers:
- name: agent
image: busybox:1.36
command: ["sh","-c","while true; do date; sleep 60; done"]
resources:
requests: {cpu: 20m, memory: 32Mi}
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
volumes:
- name: varlog
hostPath: {path: /var/log}
EOF
kubectl apply -f ds-agent.yaml
kubectl get ds -n kube-system node-agent
kubectl get po -n kube-system -l app=node-agent -o wide # 数量 = 节点数
要点:不给 replicas(跟随节点数自动增减);常用 hostPath / hostNetwork;
需要容忍 master 污点才能覆盖全节点。
----------------------------------------------------------------
五、方式 5:StatefulSet(有状态)
----------------------------------------------------------------
适用:MySQL 主从、Redis Cluster、Kafka、ZooKeeper、Elasticsearch。
三个"稳定":
1) 稳定网络标识:pod-0.svc-name.ns.svc.cluster.local(需 Headless Service)
2) 稳定存储:每个副本一个 PVC(volumeClaimTemplates),删 Pod 不删盘
3) 稳定顺序:0→N-1 依次创建,N-1→0 逆序删除
cat > sts-demo.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
clusterIP: None # Headless
selector: {app: redis}
ports: [{port: 6379, name: redis}]
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: redis
replicas: 3
selector:
matchLabels: {app: redis}
template:
metadata:
labels: {app: redis}
spec:
containers:
- name: redis
image: redis:7.2
ports: [{containerPort: 6379, name: redis}]
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests: {storage: 1Gi}
# storageClassName: standard # 按集群实际 SC 填写
EOF
kubectl apply -f sts-demo.yaml
kubectl get po -l app=redis -w # 观察 0→1→2 顺序创建
kubectl get pvc # data-redis-0/1/2
kubectl delete po redis-0
kubectl get pvc # 盘还在,重新挂回
集群内解析验证:
kubectl run dns --rm -it --image=busybox:1.36 --restart=Never -- \
nslookup redis-0.redis.default.svc.cluster.local
----------------------------------------------------------------
六、方式 6:Job 与 CronJob(批处理)
----------------------------------------------------------------
6.1 Job:跑完即止,成功即 Completed
----------------------------------------------------------------
cat > job-pi.yaml <<'EOF'
apiVersion: batch/v1
kind: Job
metadata:
name: pi
spec:
completions: 3 # 总共要成功 3 次
parallelism: 2 # 同时跑 2 个
backoffLimit: 4 # 失败重试上限
activeDeadlineSeconds: 300
ttlSecondsAfterFinished: 600 # 完成后 10 分钟自动清理
template:
spec:
restartPolicy: Never # Job 必须 Never 或 OnFailure
containers:
- name: pi
image: perl:5.38
command: ["perl","-Mbignum=bpi","-wle","print bpi(2000)"]
EOF
kubectl apply -f job-pi.yaml
kubectl get job,pod -l job-name=pi -w
kubectl logs job/pi
【重要】Job 的 Pod 模板里 restartPolicy 只能是 Never / OnFailure,
写成 Always 会被 apiserver 直接拒绝。
6.2 CronJob:定时
----------------------------------------------------------------
cat > cj-backup.yaml <<'EOF'
apiVersion: batch/v1
kind: CronJob
metadata:
name: backup
spec:
schedule: "0 2 * * *" # 每天 02:00(集群内是 UTC,注意时区)
timeZone: "Asia/Shanghai" # 1.27+ 支持,低版本需给容器设 TZ
concurrencyPolicy: Forbid # 上一次没跑完就跳过本次
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: busybox:1.36
command: ["sh","-c","echo backup at $(date); sleep 10"]
EOF
kubectl apply -f cj-backup.yaml
kubectl get cj
# 立刻手动触发一次(不用等到 2 点)
kubectl create job --from=cronjob/backup backup-manual
kubectl get po -l job-name=backup-manual
kubectl logs -f job/backup-manual
坑:CronJob 的 schedule 按 controller-manager 时区(一般 UTC),
表达式别写错;concurrencyPolicy 默认 Allow,会叠加跑。
----------------------------------------------------------------
七、方式 7:静态 Pod(kubelet 直管)
----------------------------------------------------------------
定义:YAML 放在节点的 /etc/kubernetes/manifests/,kubelet 直接读取并启动,
不经过 apiserver 调度(但 apiserver 里能看到一个 mirror pod)。
# 在节点上(需要 root)
mkdir -p /etc/kubernetes/manifests
cat > /etc/kubernetes/manifests/static-nginx.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: static-nginx
namespace: kube-system
spec:
containers:
- name: nginx
image: nginx:1.27
ports: [{containerPort: 80, hostPort: 8080}]
hostNetwork: true
EOF
# 删除文件即停止该 Pod
mv /etc/kubernetes/manifests/static-nginx.yaml /tmp/
典型用户:kube-apiserver、etcd、kube-controller-manager、kube-scheduler
(kubeadm 装的集群,这四个就在 master 的 manifests 目录里)。
注意:改了静态 Pod 的 YAML,kubelet 会检测到并重建;改坏了 apiserver 起不来,
只能登机器修 —— 生产改之前先备份,并想好回滚。
----------------------------------------------------------------
八、调度约束:把 Pod 放到你想放的地方
----------------------------------------------------------------
1) nodeSelector(最简单)
nodeSelector:
disktype: ssd
2) 节点亲和(软硬兼备)
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # 硬性
nodeSelectorTerms:
- matchExpressions:
- {key: zone, operator: In, values: [az-a, az-b]}
preferredDuringSchedulingIgnoredDuringExecution: # 软性加权
- weight: 80
preference:
matchExpressions:
- {key: gpu, operator: In, values: ["true"]}
3) Pod 亲和/反亲和(把同类打散或聚拢)
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels: {app: web}
topologyKey: kubernetes.io/hostname
4) 污点与容忍
# 给节点打污点
kubectl taint nodes node1 dedicated=gpu:NoSchedule
# Pod 加容忍才能上去
tolerations:
- key: dedicated
operator: Equal
value: gpu
effect: NoSchedule
# 取消污点
kubectl taint nodes node1 dedicated=gpu:NoSchedule-
5) 常用排错
kubectl get pod <p> -o wide # 落在哪个节点
kubectl describe pod <p> | tail -20 # FailedScheduling 会写明原因
kubectl describe node <n> | grep -E 'Taints|Allocated'
----------------------------------------------------------------
九、一个"像样"的完整部署(把上面串起来)
----------------------------------------------------------------
cat > web-full.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels: {app: web}
spec:
replicas: 3
revisionHistoryLimit: 5
selector:
matchLabels: {app: web}
strategy:
type: RollingUpdate
rollingUpdate: {maxSurge: 1, maxUnavailable: 0}
template:
metadata:
labels: {app: web}
spec:
securityContext:
runAsNonRoot: true
runAsUser: 101
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels: {app: web}
containers:
- name: web
image: nginx:1.27
ports: [{containerPort: 80, name: http}]
envFrom:
- configMapRef: {name: app-cfg}
resources:
requests: {cpu: 100m, memory: 128Mi}
limits: {cpu: 500m, memory: 256Mi}
readinessProbe:
httpGet: {path: /, port: http}
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
httpGet: {path: /, port: http}
initialDelaySeconds: 15
periodSeconds: 10
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop: ["ALL"]
---
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector: {app: web}
ports:
- {name: http, port: 80, targetPort: http}
type: ClusterIP
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web
spec:
minAvailable: 2
selector:
matchLabels: {app: web}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
spec:
ingressClassName: nginx
rules:
- host: web.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port: {number: 80}
EOF
kubectl create configmap app-cfg --from-literal=TZ=Asia/Shanghai
kubectl apply -f web-full.yaml
kubectl get deploy,rs,po,svc,ingress,pdb -l app=web
kubectl rollout status deploy/web
kubectl port-forward svc/web 8080:80
# 另一终端:curl -I localhost:8080
kubectl delete -f web-full.yaml
----------------------------------------------------------------
十、排障速查(部署阶段的 90% 问题)
----------------------------------------------------------------
症状 先看什么 / 常见原因
--------------------------- -------------------------------------------
Pending describe → FailedScheduling:资源不足、污点、
亲和性不满足、PVC 未绑定、镜像太大
ContainerCreating 久 拉镜像中(大镜像/网络慢)、挂载卷失败、
CNI 异常 → describe 看 Events
ImagePullBackOff/ErrImagePull 镜像名/标签错、私有仓库需 imagePullSecrets、
containerd 未配 registry mirror
CrashLoopBackOff 容器自身启动即退出 → logs --previous、
检查 command/args、配置、权限
Running 但 not Ready readinessProbe 失败 → exec 进容器 curl 探针路径
describe 看 "Readiness probe failed"
OOMKilled limits.memory 太小,或内存泄漏 → kubectl top /
describe 看 Last State
Evicted 节点磁盘/内存压力 → describe node conditions
Service 访问不通 selector 与 Pod labels 不匹配、targetPort 错、
EndpointSlice 为空、NetworkPolicy 拦了
滚动更新卡住 rollout status 看卡在哪个 Pod;必要时
rollout undo 快速止血
三条黄金命令,按顺序打:
kubectl get pod -o wide
kubectl describe pod <name> # 只看最后 Events 段
kubectl logs <name> [--previous] [-c 容器名]
----------------------------------------------------------------
十一、Day 02 自检清单
----------------------------------------------------------------
[ ] 能用 kubectl run --dry-run=client -o yaml 生成脚手架
[ ] 手写一个带 requests/limits + 三种探针的 Deployment
[ ] 完成一次 set image → 观察滚动 → undo 回滚
[ ] 用 NodePort 或 port-forward 真访问到服务
[ ] 起一个 DaemonSet,确认 Pod 数 = 节点数
[ ] 起一个 StatefulSet,验证删除 Pod 后 PVC 仍在、名字仍是 -0/-1/-2
[ ] 起一个 CronJob 并手动触发一次
[ ] 能解释 Pending / ImagePullBackOff / CrashLoopBackOff 的区别与查法
下一篇预告:Day 03 —— Service 与 Ingress 网络模型、CoreDNS 解析链路、
NetworkPolicy 隔离;Day 04 —— 存储 PV/PVC/StorageClass;Day 05 ——
配置与密钥、RBAC 与安全基线;Day 06 —— 监控日志与故障演练。
- 点赞
- 收藏
- 关注作者
评论(0)