综合实战:微服务部署
概念引入
恭喜你走到了最后一篇!前面文章覆盖了从 Pod 到集群运维的全部知识。但知识散落在各处------就像你有一箱乐高积木,每块都认识,但从来没拼过一个完整作品。
这篇就是拼乐高的时刻。
我们要从零部署一个完整的微服务应用:有前端、有后端、有数据库、有缓存,还有自动扩缩容、网络隔离、Ingress 路由......面试时你不需要讲 29 个零散概念,只需要讲 一个项目------然后面试官会从项目里追问出所有知识点。
目标架构
#mermaid-svg-hNKz1omOdTFq5ugE{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-hNKz1omOdTFq5ugE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hNKz1omOdTFq5ugE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hNKz1omOdTFq5ugE .error-icon{fill:#552222;}#mermaid-svg-hNKz1omOdTFq5ugE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hNKz1omOdTFq5ugE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hNKz1omOdTFq5ugE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hNKz1omOdTFq5ugE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hNKz1omOdTFq5ugE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hNKz1omOdTFq5ugE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hNKz1omOdTFq5ugE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hNKz1omOdTFq5ugE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hNKz1omOdTFq5ugE .marker.cross{stroke:#333333;}#mermaid-svg-hNKz1omOdTFq5ugE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hNKz1omOdTFq5ugE p{margin:0;}#mermaid-svg-hNKz1omOdTFq5ugE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hNKz1omOdTFq5ugE .cluster-label text{fill:#333;}#mermaid-svg-hNKz1omOdTFq5ugE .cluster-label span{color:#333;}#mermaid-svg-hNKz1omOdTFq5ugE .cluster-label span p{background-color:transparent;}#mermaid-svg-hNKz1omOdTFq5ugE .label text,#mermaid-svg-hNKz1omOdTFq5ugE span{fill:#333;color:#333;}#mermaid-svg-hNKz1omOdTFq5ugE .node rect,#mermaid-svg-hNKz1omOdTFq5ugE .node circle,#mermaid-svg-hNKz1omOdTFq5ugE .node ellipse,#mermaid-svg-hNKz1omOdTFq5ugE .node polygon,#mermaid-svg-hNKz1omOdTFq5ugE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hNKz1omOdTFq5ugE .rough-node .label text,#mermaid-svg-hNKz1omOdTFq5ugE .node .label text,#mermaid-svg-hNKz1omOdTFq5ugE .image-shape .label,#mermaid-svg-hNKz1omOdTFq5ugE .icon-shape .label{text-anchor:middle;}#mermaid-svg-hNKz1omOdTFq5ugE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hNKz1omOdTFq5ugE .rough-node .label,#mermaid-svg-hNKz1omOdTFq5ugE .node .label,#mermaid-svg-hNKz1omOdTFq5ugE .image-shape .label,#mermaid-svg-hNKz1omOdTFq5ugE .icon-shape .label{text-align:center;}#mermaid-svg-hNKz1omOdTFq5ugE .node.clickable{cursor:pointer;}#mermaid-svg-hNKz1omOdTFq5ugE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hNKz1omOdTFq5ugE .arrowheadPath{fill:#333333;}#mermaid-svg-hNKz1omOdTFq5ugE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hNKz1omOdTFq5ugE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hNKz1omOdTFq5ugE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hNKz1omOdTFq5ugE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hNKz1omOdTFq5ugE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hNKz1omOdTFq5ugE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hNKz1omOdTFq5ugE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hNKz1omOdTFq5ugE .cluster text{fill:#333;}#mermaid-svg-hNKz1omOdTFq5ugE .cluster span{color:#333;}#mermaid-svg-hNKz1omOdTFq5ugE 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-hNKz1omOdTFq5ugE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hNKz1omOdTFq5ugE rect.text{fill:none;stroke-width:0;}#mermaid-svg-hNKz1omOdTFq5ugE .icon-shape,#mermaid-svg-hNKz1omOdTFq5ugE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hNKz1omOdTFq5ugE .icon-shape p,#mermaid-svg-hNKz1omOdTFq5ugE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hNKz1omOdTFq5ugE .icon-shape .label rect,#mermaid-svg-hNKz1omOdTFq5ugE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hNKz1omOdTFq5ugE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hNKz1omOdTFq5ugE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hNKz1omOdTFq5ugE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 安全层
数据层
后端层
前端层
入口层 --- Ingress
/
/api/*
限制访问
🌐 用户请求
nginx-ingress-controller
路由规则:/ → frontend
/api/* → backend
frontend Deployment
Nginx 静态页面
replicas: 2
frontend-svc
ClusterIP:80
backend Deployment
http-echo API 模拟
replicas: 2
backend-svc
ClusterIP:8080
HPA
min:2 max:10
cpu > 70% → 扩容
PDB
minAvailable: 1
保证滚动更新不中断
PostgreSQL StatefulSet
PVC 持久化
replicas: 1
pg-svc
Headless (clusterIP: None)
Redis Deployment
缓存
replicas: 1
redis-svc
ClusterIP:6379
NetworkPolicy
deny-all → 白名单放行
DB 只允许 backend 访问
学完本篇你将能够:
- 从零部署一个包含前端、后端、数据库、缓存的完整微服务应用
- 理解每个 YAML 字段的含义,能独立排查部署问题
- 在面试中用这个项目串联 Deployment、Service、Ingress、HPA、NetworkPolicy、PDB 等 15+ 个知识点
原理讲解:分层架构
整个系统分四层,每层解决不同的问题。面试时画这个图,然后逐层讲解:
┌─────────────────────────────────────────────────────────────────┐
│ 入口层:Ingress │
│ 解决问题:外部流量怎么进来?怎么路由到不同的服务? │
│ • nginx-ingress-controller 监听 Ingress 资源 │
│ • / → frontend-svc:80 (用户访问网页) │
│ • /api/* → backend-svc:8080 (前端调用后端 API) │
├─────────────────────────────────────────────────────────────────┤
│ 应用层:frontend + backend │
│ 解决问题:怎么处理用户请求? │
│ • frontend: Nginx 提供静态页面,/api/ 请求反向代理到 backend │
│ • backend: API 服务(本实验用 http-echo 模拟真实后端) │
│ • initContainer: backend 启动前等 PostgreSQL DNS 就绪 │
│ • 探针: liveness + readiness 确保流量只到健康的 Pod │
├─────────────────────────────────────────────────────────────────┤
│ 数据层:PostgreSQL + Redis │
│ 解决问题:数据存在哪?怎么保证不丢? │
│ • PostgreSQL: StatefulSet + PVC(数据持久化,Pod 重启数据不丢) │
│ • Redis: Deployment(缓存,丢了可以重建) │
├─────────────────────────────────────────────────────────────────┤
│ 安全 + 弹性层 │
│ 解决问题:怎么保证安全?怎么应对流量波动? │
│ • NetworkPolicy: deny-all 基础上白名单放行(最小权限原则) │
│ • HPA: 根据 CPU 使用率自动扩缩 backend Pod 数量 │
│ • PDB: 保证滚动更新/节点维护时至少有 1 个 backend Pod 可用 │
└─────────────────────────────────────────────────────────────────┘
请求的完整生命周期
面试高频问题:"一个请求从用户到数据库,经过了 K8s 的哪些组件?"
PostgreSQL backend Pod (http-echo) frontend Pod (nginx) Ingress Controller (nginx Pod) 用户浏览器 PostgreSQL backend Pod (http-echo) frontend Pod (nginx) Ingress Controller (nginx Pod) 用户浏览器 #mermaid-svg-8oMb0di3AKtYNhXk{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-8oMb0di3AKtYNhXk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8oMb0di3AKtYNhXk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8oMb0di3AKtYNhXk .error-icon{fill:#552222;}#mermaid-svg-8oMb0di3AKtYNhXk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8oMb0di3AKtYNhXk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8oMb0di3AKtYNhXk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8oMb0di3AKtYNhXk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8oMb0di3AKtYNhXk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8oMb0di3AKtYNhXk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8oMb0di3AKtYNhXk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8oMb0di3AKtYNhXk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8oMb0di3AKtYNhXk .marker.cross{stroke:#333333;}#mermaid-svg-8oMb0di3AKtYNhXk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8oMb0di3AKtYNhXk p{margin:0;}#mermaid-svg-8oMb0di3AKtYNhXk .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8oMb0di3AKtYNhXk text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-8oMb0di3AKtYNhXk .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-8oMb0di3AKtYNhXk .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-8oMb0di3AKtYNhXk .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-8oMb0di3AKtYNhXk .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-8oMb0di3AKtYNhXk #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-8oMb0di3AKtYNhXk .sequenceNumber{fill:white;}#mermaid-svg-8oMb0di3AKtYNhXk #sequencenumber{fill:#333;}#mermaid-svg-8oMb0di3AKtYNhXk #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-8oMb0di3AKtYNhXk .messageText{fill:#333;stroke:none;}#mermaid-svg-8oMb0di3AKtYNhXk .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8oMb0di3AKtYNhXk .labelText,#mermaid-svg-8oMb0di3AKtYNhXk .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-8oMb0di3AKtYNhXk .loopText,#mermaid-svg-8oMb0di3AKtYNhXk .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-8oMb0di3AKtYNhXk .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-8oMb0di3AKtYNhXk .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-8oMb0di3AKtYNhXk .noteText,#mermaid-svg-8oMb0di3AKtYNhXk .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-8oMb0di3AKtYNhXk .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8oMb0di3AKtYNhXk .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8oMb0di3AKtYNhXk .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8oMb0di3AKtYNhXk .actorPopupMenu{position:absolute;}#mermaid-svg-8oMb0di3AKtYNhXk .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-8oMb0di3AKtYNhXk .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8oMb0di3AKtYNhXk .actor-man circle,#mermaid-svg-8oMb0di3AKtYNhXk line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-8oMb0di3AKtYNhXk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 场景 1:用户访问首页 场景 2:前端调用后端 API 场景 3:完整数据流(真实后端) GET http://localhost/ 匹配 / → frontend-svc 返回 Nginx 默认页面 GET http://localhost/api/health 匹配 /api/* → backend-svc {"status":"ok","service":"backend"} GET http://localhost/api/users 转发请求 SELECT * FROM users 返回数据 JSON 响应
面试加分点: 提到 Ingress Controller 本身也是一个 Pod(运行在
ingress-nginxnamespace),外部流量先到它,再由它根据 Ingress 规则转发到对应的 Service → Pod。
动手实验
配套实验位于
docs/labs/beginner/microservice-deploy/
本实验用 9 个 YAML 文件 部署完整的微服务栈。下面逐个文件讲解,然后一键部署。
步骤 1:部署整个微服务栈
bash
cd docs/labs/beginner/microservice-deploy
bash setup.sh
setup.sh 做了这些事:
bash
# 1. 创建 Kind 集群(1 控制面 + 2 工作节点)
# 2. 安装 Nginx Ingress Controller(处理外部流量入口)
# 3. 安装 metrics-server(给 HPA 提供 CPU 指标)
# └─ Kind 需要加 --kubelet-insecure-tls 参数
# 4. 创建 prod namespace
# 5. 按顺序部署:Secret → PostgreSQL → Redis → Backend → Frontend → Ingress → NetworkPolicy → HPA/PDB
# 6. 等待所有组件就绪
⏳ 首次运行需要拉取多个镜像(nginx、postgres、redis、http-echo、busybox),可能需要 5-10 分钟。
部署完成后,检查全局状态:
bash
kubectl get all -n prod
预期输出(关键部分):
NAME READY STATUS RESTARTS AGE
pod/backend-xxx-abc 1/1 Running 0 2m
pod/backend-xxx-def 1/1 Running 0 2m
pod/frontend-xxx-ghi 1/1 Running 0 2m
pod/frontend-xxx-jkl 1/1 Running 0 2m
pod/postgres-0 1/1 Running 0 2m
pod/redis-xxx-mno 1/1 Running 0 2m
NAME TYPE CLUSTER-IP PORT(S)
service/backend-svc ClusterIP 10.96.x.x 8080/TCP
service/frontend-svc ClusterIP 10.96.x.x 80/TCP
service/pg-svc ClusterIP None 5432/TCP ← Headless!
service/redis-svc ClusterIP 10.96.x.x 6379/TCP
NAME READY UP-TO-DATE AVAILABLE
deployment.apps/backend 2/2 2 2
deployment.apps/frontend 2/2 2 2
deployment.apps/redis 1/1 1 1
NAME REFERENCE TARGETS MINPODS MAXPODS
horizontalpodautoscaler/backend-hpa Deployment/backend 0%/70% 2 10
NAME MIN AVAILABLE ALLOWED DISRUPTIONS
poddisruptionbudget/backend-pdb 1 1
💡 注意
pg-svc的 CLUSTER-IP 是 None ------ 这是 Headless Service(#15 StatefulSet 讲的),DNS 直接解析到 Pod IP,不经过负载均衡。PostgreSQL 只有一个实例,不需要负载均衡。
步骤 2:逐层理解每个组件
下面按部署顺序讲解每个 YAML 文件的关键配置。
2.1 Namespace:环境隔离(#11)
yaml
# manifests/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: prod # 所有组件都部署在这个 namespace 下
为什么用 Namespace? 生产环境通常有 dev、staging、prod 三个 namespace。资源限额、RBAC、NetworkPolicy 都可以按 namespace 隔离。
2.2 Secret:敏感信息(#07)
yaml
# manifests/secrets.yaml(关键部分)
apiVersion: v1
kind: Secret
metadata:
name: pg-secret # ← backend 和 postgres 都会引用这个名字
namespace: prod
type: Opaque
stringData: # stringData 自动 Base64 编码(比 data 方便)
POSTGRES_USER: app
POSTGRES_PASSWORD: demo123
POSTGRES_DB: myapp
面试要点: stringData vs data------stringData 写明文,K8s 自动编码;data 要你自己写 Base64。生产环境用 Sealed Secrets 或 External Secrets Operator,不要把明文 Secret 提交到 Git。
2.3 PostgreSQL:StatefulSet + Headless Service + PVC(#15, #10, #06)
这是整个系统中最有状态的组件,用了三个 K8s 概念:
yaml
# manifests/postgres.yaml
apiVersion: v1
kind: Service
metadata:
name: pg-svc
namespace: prod
spec:
clusterIP: None # ← Headless Service:没有虚拟 IP
selector: # DNS 直接解析到 Pod IP
app: postgres # → pg-0.pg-svc.prod.svc.cluster.local
ports:
- port: 5432
---
apiVersion: apps/v1
kind: StatefulSet # ← 不是 Deployment!
metadata:
name: postgres
namespace: prod
spec:
serviceName: pg-svc # ← StatefulSet 必须关联一个 Headless Service
replicas: 1 # 单实例就够了(演示用)
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
envFrom:
- secretRef:
name: pg-secret # ← 把 Secret 注入为环境变量
ports:
- containerPort: 5432
volumeMounts:
- name: pg-data
mountPath: /var/lib/postgresql/data # ← 数据存到 PVC
resources:
requests:
cpu: 100m # ← 资源请求(#13)
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe: # ← 存活探针(#12)
exec:
command: ["pg_isready", "-U", "app"] # pg 自带的健康检查命令
initialDelaySeconds: 10
periodSeconds: 5
readinessProbe: # ← 就绪探针
exec:
command: ["pg_isready", "-U", "app"]
initialDelaySeconds: 5
periodSeconds: 3
volumeClaimTemplates: # ← PVC 模板(#10)
- metadata:
name: pg-data # StatefulSet 自动为每个 Pod 创建 PVC
spec:
accessModes: ["ReadWriteOnce"] # 只能被一个节点挂载读写
resources:
requests:
storage: 1Gi # 即使 Pod 被删除重建,PVC 和数据还在
关键问题:为什么用 StatefulSet 而不是 Deployment?
| 特性 | Deployment | StatefulSet |
|---|---|---|
| Pod 名 | 随机(backend-abc123) | 有序(postgres-0) |
| 存储 | 所有 Pod 共享 PVC | 每个 Pod 独立 PVC |
| 启动顺序 | 并行 | 有序(0→1→2) |
| 网络标识 | 无 | pod-name.service.ns.svc |
| 适用场景 | 无状态(Web 服务) | 有状态(数据库、消息队列) |
2.4 Redis:Deployment(#04)
yaml
# manifests/redis.yaml
apiVersion: apps/v1
kind: Deployment # ← Redis 做缓存,用 Deployment 就够了
metadata:
name: redis
namespace: prod
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
livenessProbe:
tcpSocket: # ← TCP 探针:端口通了就算活着
port: 6379
initialDelaySeconds: 5
periodSeconds: 5
面试要点: Redis 在这里做缓存(非持久化),数据丢了可以从数据库重建,所以用 Deployment 更简单。如果 Redis 做持久化存储(RDB/AOF),那也应该用 StatefulSet。
2.5 Backend:Deployment + ConfigMap + InitContainer + 探针(#04, #07, #21, #12)
Backend 是整个系统中配置最复杂的组件,串联了 4 个知识点:
yaml
# manifests/backend.yaml
# ① ConfigMap:非敏感配置
apiVersion: v1
kind: ConfigMap
metadata:
name: backend-config
namespace: prod
data:
DB_HOST: "pg-svc.prod.svc.cluster.local" # ← 用 FQDN 访问 PostgreSQL
DB_PORT: "5432"
REDIS_HOST: "redis-svc.prod.svc.cluster.local" # ← 用 FQDN 访问 Redis
REDIS_PORT: "6379"
---
# ② Service:暴露 backend 给 frontend 和 Ingress
apiVersion: v1
kind: Service
metadata:
name: backend-svc
namespace: prod
spec:
selector:
app: backend # ← 通过标签选择 backend Pod
ports:
- port: 8080
targetPort: 8080
---
# ③ Deployment:管理 backend Pod
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: prod
spec:
replicas: 2 # ← 2 个副本(和 HPA minReplicas 一致)
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
# ④ InitContainer:在 backend 启动前等 PostgreSQL 就绪
initContainers:
- name: wait-for-db
image: busybox:1.36
command:
- sh
- -c
- |
echo "Waiting for PostgreSQL..."
# nslookup 查 Headless Service 的 DNS
# pg-svc 是 Headless Service,只有 Pod 就绪后 DNS 才能解析
until nslookup pg-svc.prod.svc.cluster.local; do sleep 2; done
echo "PostgreSQL is up!"
containers:
- name: backend
# 本实验用 http-echo 模拟真实后端
# 真实项目这里会是你的 Python/Go/Java 应用镜像
image: hashicorp/http-echo
args:
- "-text={\"status\":\"ok\",\"service\":\"backend\"}" # 所有请求返回这个 JSON
- "-listen=:8080"
ports:
- containerPort: 8080
env:
- name: DB_HOST
valueFrom:
configMapKeyRef: # ← 从 ConfigMap 读配置
name: backend-config
key: DB_HOST
- name: DB_PASSWORD
valueFrom:
secretKeyRef: # ← 从 Secret 读密码
name: pg-secret
key: POSTGRES_PASSWORD
resources:
requests:
cpu: 50m # ← HPA 根据这个值计算 CPU 利用率
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
livenessProbe: # ← 存活探针:/health 返回 200 就活着
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
readinessProbe: # ← 就绪探针:200 才接收流量
httpGet:
path: /health
port: 8080
initialDelaySeconds: 3
periodSeconds: 3
InitContainer 的工作原理(#21):
Pod 启动流程:
initContainer: wait-for-db
├─ nslookup pg-svc.prod.svc.cluster.local
├─ 失败 → sleep 2 → 重试
├─ ...(循环等待)
├─ 成功 → initContainer 退出(exit 0)
└─ 主容器 backend 才开始启动
为什么需要等?
→ backend 启动时会尝试连接数据库
→ 如果数据库还没准备好,backend 直接崩溃退出
→ K8s 会反复重启 backend(CrashLoopBackOff)
→ InitContainer 避免了这种"启动竞争"
⚠️ 实验说明: 本实验的 backend 用
hashicorp/http-echo模拟------它对所有路径都返回固定的 JSON,不会真正连接数据库。真实项目中,你需要用自己的应用镜像(Flask/Spring Boot/Go)替换这个 image,并在代码里连接DB_HOST和REDIS_HOST。
2.6 Frontend:Deployment + Nginx ConfigMap(#04, #07)
yaml
# manifests/frontend.yaml
# ① ConfigMap:Nginx 反向代理配置
apiVersion: v1
kind: ConfigMap
metadata:
name: frontend-config
namespace: prod
data:
default.conf: |
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri /index.html; # SPA 路由 fallback
}
location /api/ {
# /api/xxx 请求转发到 backend-svc
proxy_pass http://backend-svc.prod.svc.cluster.local:8080/;
}
}
---
# ② Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: prod
spec:
replicas: 2
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: nginx:1.27
ports:
- containerPort: 80
volumeMounts:
- name: config
mountPath: /etc/nginx/conf.d/default.conf # ← 覆盖 Nginx 默认配置
subPath: default.conf # 只替换一个文件
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: config
configMap:
name: frontend-config # ← 把 ConfigMap 挂载为文件
Nginx 配置的巧妙之处: 前端既能直接服务静态页面(/),又能把 API 请求代理给 backend(/api/)。这样从 Ingress 的角度,/ 和 /api/* 都先到各自的 Service,但 frontend 的 Nginx 也会二次代理 /api/ 请求------提供了双保险。
2.7 Ingress:外部流量入口(#23)
yaml
# manifests/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
namespace: prod
annotations:
# 重写 URL:/api/users → backend 收到 /users
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx # ← 由 nginx-ingress-controller 处理
rules:
- host: localhost # ← Kind 环境用 localhost
http:
paths:
- path: /api(/|$)(.*) # ← 正则匹配:/api、/api/、/api/users 等
pathType: ImplementationSpecific
backend:
service:
name: backend-svc
port:
number: 8080
- path: / # ← 其他所有请求
pathType: Prefix
backend:
service:
name: frontend-svc
port:
number: 80
URL 重写规则解读:
用户请求 Ingress 匹配 Backend 收到
─────────────────────────────────────────────────────────────────
GET /api/users → /api(/|$)(.*) 匹配 → /users
$2 = "users"
rewrite-target: /$2
GET /api/health → /api(/|$)(.*) 匹配 → /health
$2 = "health"
GET / → / Prefix 匹配 → /
转发到 frontend-svc
💡
rewrite-target: /$2的意思是:把正则捕获组$2(即/api/后面的部分)作为新的路径发给 backend。所以 backend 不需要知道/api前缀的存在。
2.8 NetworkPolicy:网络隔离(#22)
这是面试最爱问的部分------"你的微服务之间怎么保证安全?"
yaml
# manifests/networkpolicy.yaml
# ① 默认拒绝所有入站流量(deny-all 基线)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: prod
spec:
podSelector: {} # ← 选中 prod namespace 下的所有 Pod
policyTypes:
- Ingress # ← 没有 ingress 规则 → 全部拒绝
# ② 白名单:允许 frontend 接收外部流量
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-ingress
namespace: prod
spec:
podSelector:
matchLabels:
app: frontend
policyTypes:
- Ingress
ingress:
- {} # ← 空规则 = 允许所有来源(frontend 需要接收 Ingress 流量)
# ③ 白名单:backend 只允许 frontend 和 Ingress Controller 访问
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-from-frontend
namespace: prod
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend # ← 允许 frontend Pod
ports:
- protocol: TCP
port: 8080
- from:
- namespaceSelector: {} # ← 允许任何 namespace 中的
podSelector: # ingress-nginx Pod
matchLabels: # (因为 Ingress Controller 在 ingress-nginx namespace)
app.kubernetes.io/name: ingress-nginx
ports:
- protocol: TCP
port: 8080
# ④ 白名单:PostgreSQL 只允许 backend 访问
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-db-from-backend
namespace: prod
spec:
podSelector:
matchLabels:
app: postgres
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: backend # ← 只有 backend 能连数据库
ports:
- protocol: TCP
port: 5432
# ⑤ 白名单:Redis 只允许 backend 访问(同上)
NetworkPolicy 的安全模型:
默认 deny-all → 逐条白名单放行
frontend: 允许所有入站(Ingress Controller 需要访问它)
backend: 只允许 frontend + Ingress Controller 入站
postgres: 只允许 backend 入站(frontend 直连 DB → 被拒绝 ✅)
redis: 只允许 backend 入站
2.9 HPA + PDB:弹性 + 可用性(#08, #25)
yaml
# manifests/hpa-pdb.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: backend-hpa
namespace: prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: backend # ← 控制 backend 的副本数
minReplicas: 2 # ← 最少 2 个 Pod
maxReplicas: 10 # ← 最多 10 个 Pod
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # ← 平均 CPU 利用率超过 70% → 扩容
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: backend-pdb
namespace: prod
spec:
minAvailable: 1 # ← 任何时刻至少 1 个 backend Pod 可用
selector:
matchLabels:
app: backend
HPA 和 PDB 的关系(面试高频陷阱):
| 场景 | HPA 想做什么 | PDB 允许吗 | 结果 |
|---|---|---|---|
| CPU 高 → 扩到 5 个 | 扩容 | ✅ 不管 | 正常扩容 |
| CPU 低 → 缩到 1 个 | 缩容 | ❌ minAvailable: 1 但缩到 1 后做滚动更新时 0 个可用 |
缩容被阻止 |
| 节点维护 → drain | 驱逐 Pod | ✅ 只要还剩 ≥ 1 个 | 正常驱逐 |
步骤 3:端到端测试
部署完成后,逐层验证系统是否正常工作:
bash
# ① 通过 Ingress 访问前端(HTTP,Kind 端口映射到 localhost:80)
curl -s http://localhost/ | head -5
# 预期:返回 Nginx 欢迎页面的 HTML
# ② 通过 Ingress 访问后端 API(健康检查端点)
curl -s http://localhost/api/health
# 预期:{"status":"ok"}
# 说明:http-echo 内置了 /health 端点,返回简化的状态
# ③ 测试 backend 的其他路径
curl -s http://localhost/api/users
# 预期:{"status":"ok","service":"backend"}
# 说明:http-echo 对非 /health 路径返回 -text 参数指定的完整 JSON
⚠️ 注意: 本实验用
http-echo模拟后端,所有路径返回固定 JSON(/health除外)。真实项目中替换为你的应用镜像后,/api/users会返回实际的用户数据。
验证 Service 间通信(frontend 的 nginx 镜像自带 curl):
bash
# frontend 能访问 backend 吗?(应该可以 --- NetworkPolicy 已放行)
kubectl exec -n prod deploy/frontend -- \
curl -s --max-time 3 http://backend-svc:8080/api/test
# 预期:{"status":"ok","service":"backend"}
# backend 能访问 PostgreSQL 吗?(TCP 端口应该可达)
kubectl exec -n prod deploy/backend -c backend -- \
sh -c 'echo quit | timeout 3 nc pg-svc 5432' 2>&1 && \
echo "✅ PostgreSQL 端口可达" || \
echo "检查 NetworkPolicy 或 PostgreSQL 状态"
步骤 4:验证 NetworkPolicy 隔离
这是最有意思的验证------证明网络隔离真的生效了:
bash
# 测试 1:frontend 直接访问 PostgreSQL(应该被拒绝!)
# NetworkPolicy allow-db-from-backend 只允许 app: backend 的 Pod 访问 pg
kubectl exec -n prod deploy/frontend -- \
curl -s --max-time 3 http://pg-svc:5432 && \
echo "⚠️ 可以访问(不应该)" || \
echo "✅ 连接超时 --- NetworkPolicy 生效,frontend 无法直连数据库"
# 测试 2:Redis 同理,frontend 也不应该能访问
kubectl exec -n prod deploy/frontend -- \
curl -s --max-time 3 http://redis-svc:6379 && \
echo "⚠️ 可以访问(不应该)" || \
echo "✅ 连接超时 --- NetworkPolicy 生效,frontend 无法访问 Redis"
# 测试 3:backend 访问 PostgreSQL(应该成功)
kubectl exec -n prod deploy/backend -c backend -- \
sh -c 'echo quit | timeout 3 nc pg-svc 5432' 2>&1 && \
echo "✅ backend 可以访问 PostgreSQL" || \
echo "TCP 可达(非 HTTP 协议,nc 报错但端口是通的)"
面试加分点: 解释 NetworkPolicy 是 "deny by default + allow by exception" 的安全模型,类似防火墙。先
deny-all,再逐条白名单放行------这比逐个写 "deny xxx" 更安全,因为新增的 Pod 默认就被隔离了。
步骤 5:验证 HPA 自动扩缩
bash
# 查看当前 HPA 状态
kubectl get hpa -n prod
# 预期:TARGETS 列显示当前 CPU 利用率,如 "5%/70%"
# 用循环并发请求制造负载(http-echo CPU 很低,可能不会触发扩容)
for i in $(seq 1 200); do
curl -s http://localhost/api/health > /dev/null &
done
wait
# 持续观察 HPA 是否检测到 CPU 变化
kubectl get hpa -n prod --watch
# 如果 CPU 利用率超过 70%,会看到 replicas 从 2 增加到更多
# Ctrl+C 退出 watch
💡 为什么 HPA 可能不触发?
http-echo是非常轻量的 Go 程序,CPU 使用极低。真实后端(Python/Java)处理请求会消耗明显 CPU,HPA 就容易触发了。这个实验主要验证 HPA 配置正确且 metrics-server 正常采集指标。
验证 metrics-server 工作正常:
bash
kubectl top pods -n prod
# 预期:显示每个 Pod 的 CPU 和内存使用量
# 如果显示 "no metrics available",等 30 秒再试(metrics-server 每 15 秒采集一次)
步骤 6:验证 PDB
bash
kubectl get pdb -n prod
# 预期:
# NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS
# backend-pdb 1 N/A 1
kubectl describe pdb backend-pdb -n prod
# 关注字段:
# - minAvailable: 1 → 任何时候至少 1 个 Pod 可用
# - currentHealthy: 2 → 当前 2 个 Pod 健康
# - allowedDisruptions: 1 → 允许同时中断 1 个 Pod(2 - 1 = 1)
PDB 在什么场景下生效?
✅ 会触发 PDB 的操作:
• kubectl drain node(节点维护)
• 集群自动缩容(Cluster Autoscaler)
• 滚动更新时删除旧 Pod
❌ 不会触发 PDB 的操作:
• kubectl delete pod(手动删除,PDB 不管)
• 节点意外宕机(不是主动驱逐)
步骤 7:一键查看所有资源
用这几个命令全面检查系统状态:
bash
# 所有资源概览
kubectl get all -n prod
# NetworkPolicy
kubectl get networkpolicy -n prod
# 预期:5 条策略(deny-all + 4 条白名单)
# 查看 Ingress 路由规则
kubectl describe ingress app-ingress -n prod
# 查看 backend 的环境变量配置(验证 ConfigMap + Secret 注入)
# http-echo 容器极精简无 shell,用 describe 查看配置
kubectl describe pod -n prod -l app=backend | grep -A3 "Environment:"
# 预期看到 backend 容器部分:
# Environment:
# DB_HOST: <set to the key 'DB_HOST' of config map 'backend-config'>
# DB_PASSWORD: <set to the key 'POSTGRES_PASSWORD' in secret 'pg-secret'>
步骤 8:清理
bash
bash teardown.sh
# 删除 prod namespace → 删除 Kind 集群
自检问题
-
基础 这个项目中,为什么 PostgreSQL 用 StatefulSet 而 Redis 用 Deployment?
-
理解 如果 backend 的 HPA
minReplicas: 2,PDB 的minAvailable: 2,当 CPU 降低 HPA 想缩容到 2 个 Pod 时,PDB 会不会阻止?如果 HPA 想缩到 1 个呢? -
应用 面试中面试官问:"你的微服务项目里,一个用户请求从浏览器到数据库,经过了 K8s 的哪些组件?" 请用本项目的架构回答。
-
分析 如果 backend Pod 一直卡在
Init:0/1状态,你怎么排查?写出排查步骤和对应的 kubectl 命令。
查看答案
-
PostgreSQL 需要:① 稳定的网络标识(
pg-0.pg-svc.prod.svc.cluster.local,Pod 重启后 DNS 不变)② 独立的持久化存储(PVC,Pod 删了数据不丢)③ 有序启停------这些都是 StatefulSet 的能力。Redis 做缓存 (非持久化),数据丢了重建就行,Deployment 更简单。但如果 Redis 开启 RDB/AOF 持久化,也应该用 StatefulSet。 -
HPA 缩到 2 个 Pod 时,PDB
minAvailable: 2不会阻止 ------因为缩容后还有 2 个 Pod,满足 minAvailable。但如果 HPA 想缩到 1 个 ,PDB 会阻止 ------因为缩到 1 个后,如果恰好有节点维护要驱逐这 1 个 Pod,就没有 Pod 可用了。经验法则:PDB 的 minAvailable 应该 ≤ HPA 的 minReplicas,否则缩容后无法承受任何中断。 -
完整路径:
用户浏览器
→ DNS 解析(本地用 localhost)
→ Ingress Controller Pod(ingress-nginx namespace,nginx 反向代理)
→ 根据 Ingress 规则匹配路径
→ /api/* → backend-svc(ClusterIP)→ kube-proxy 负载均衡 → backend Pod
→ backend Pod 内 initContainer 已确认 PostgreSQL 就绪
→ backend 代码连接 pg-svc.prod.svc.cluster.local:5432
→ NetworkPolicy allow-db-from-backend 放行(app: backend → app: postgres:5432)
→ PostgreSQL Pod(StatefulSet,数据写到 PVC)
→ / → frontend-svc → frontend Pod(Nginx 返回静态页面)
额外加分:提到 Service 的 kube-proxy 做负载均衡、NetworkPolicy 的 deny-all 白名单模型、探针保证只把流量发给健康 Pod。
- 排查步骤:
bash
# 1. 看 initContainer 日志(最常见:数据库没起来)
kubectl logs <backend-pod> -n prod -c wait-for-db
# 2. 看 PostgreSQL 是否在运行
kubectl get pods -n prod -l app=postgres
kubectl logs postgres-0 -n prod
# 3. 看 DNS 是否能解析
kubectl exec -n prod <backend-pod> -c wait-for-db -- nslookup pg-svc.prod.svc.cluster.local
# 4. 看 NetworkPolicy 是否阻止了 DNS(CoreDNS 在 kube-system)
# deny-all 只影响 Ingress 类型,不影响 Egress,所以 DNS 不会被阻止
# 但如果有人加了 Egress deny-all,就需要放行 UDP 53
# 5. 看事件
kubectl describe pod <backend-pod> -n prod | tail -20
最常见原因:PostgreSQL 镜像拉取失败、PostgreSQL Pod 启动失败(PVC 问题)、或 Secret 密码错误导致 pg 拒绝启动。
面试怎么讲这个项目
面试时用 STAR 法则(Situation → Task → Action → Result)讲 2-3 分钟,然后面试官会从你的描述中追问细节:
"我做了一个 K8s 微服务项目,前端 Nginx + 后端 API + PostgreSQL + Redis,全部部署在 K8s 上。
架构上 ,我用 Ingress 做入口路由,
/到前端、/api/*到后端并做 URL rewrite。后端用 Deployment 2 副本 + HPA 自动扩缩容,PostgreSQL 用 StatefulSet + PVC 持久化。安全上,我用 NetworkPolicy 做 deny-all + 白名单------数据库只允许后端 Pod 访问,前端直连数据库会被拒绝。敏感信息用 Secret,非敏感用 ConfigMap。
可用性上,后端有 PDB 保证滚动更新不中断,有 liveness/readiness 探针保证只把流量发给健康 Pod,还有 initContainer 等数据库就绪后再启动主容器。"
面试官可能追问的方向:
| 你的话 | 面试官追问 | 对应文章 |
|---|---|---|
| "HPA 自动扩缩容" | "HPA 怎么采集 CPU 指标的?" | #08 扩缩容 |
| "StatefulSet + PVC" | "PVC 的 StorageClass 是什么?" | #10 存储 |
| "NetworkPolicy deny-all" | "怎么调试 NetworkPolicy 不通的问题?" | #22 网络策略 |
| "initContainer 等数据库" | "initContainer 失败了怎么办?" | #21 多容器 |
| "PDB 保证不中断" | "PDB 和 HPA 冲突了怎么办?" | #25 高可用 |
| "Ingress URL rewrite" | "Ingress Controller 和 Ingress 的区别?" | #23 Ingress |
毕业总结
🎉 恭喜你完成了 K8s Guide 初学者轨道的全部 30 篇课程!
你学到的技能树
📦 基础入门 (01-10) 🔧 生产化 (11-20) 🚀 进阶实战 (21-30)
───────────────── ──────────────── ──────────────────
• K8s 是什么 • 多租户隔离 • 多容器模式
• 本地 Kind 集群 • 健康检查 • 网络策略隔离
• Pod 管理 • 资源管控 • Ingress 生产实战
• Deployment 部署 • RBAC 权限 • Pod 安全加固
• ReplicaSet 控制器 • 有状态 + 守护进程 • 高可用保障
• Service 服务发现 • 批处理 + 定时任务 • Pod 身份认证
• 配置管理 • Helm 包管理 • K8s 扩展模型
• 扩缩容 + 滚动发布 • 日志 + 监控 • Kustomize 配置
• 网络基础 • 排障方法论 • 集群升级运维
• 存储基础 • Gateway API • 完整微服务部署
下一步建议
- 🔍 求职者轨道:深入系统全景图 → 领域深潜 → 面试题库
- 📝 CKA/CKAD 认证:初学者轨道覆盖了考试 ~70% 的知识点
- 🛠️ 实战练手:用本项目的架构,换成你熟悉的语言栈(Flask/Spring Boot/Go)实现一个真正的 CRUD API
→ 🎓 毕业啦
📚 本文来自 K8s Guide ------ 开源免费的 Kubernetes 中文学习指南
- 🗺️ 初学者轨道 + 面试轨道,从零基础到拿 Offer 一站式覆盖
- 🧪 每篇文章配套 Kind 实验脚本,本地一键运行
- 🔗 本文源码:docs/beginner/20-gateway-api.md
- ⭐ 如果对你有帮助,欢迎 Star! github.com/callmebg/k8s-guide