前面十三章,你已經完整掌握「單機上的容器」。這一章跨出最後一步:當一台機器不夠用、或不能掛掉的時候。
Kubernetes 常被形容成「很複雜」,但複雜的是它的廣度(元件多、名詞多),核心思想其實只有一個——而且非常優雅:
你描述「期望的狀態」,系統不斷地比對現況並自動修正差異。
這叫做控制迴圈(control loop)。理解了它,K8s 的所有行為都變成同一件事的不同展現:Pod 掛了會自己重生、更新版本會逐步替換、節點壞掉工作負載會搬家—— 全都是「現況不等於期望,所以我來修正」。
這章會用你已經熟悉的 Compose 當對照,並且用 SRE 的角度切入:不只是「怎麼部署」,而是「它壞掉的時候會怎麼壞、你要怎麼查」。
先講清楚問題,才知道 K8s 那一大堆概念是在回應什麼。
up -d 更新服務時,舊容器停掉、新容器啟動,中間有幾十秒的空窗(Java 冷啟動更久)。--scale,而且只能在這一台機器上長,長到機器極限就沒了。如果這章你只記得一件事,請記這個。
while (true) {
期望狀態 = 讀取你提交的設定(etcd 裡的 YAML)
實際狀態 = 觀察叢集現在的樣子
if (實際 != 期望) {
採取行動縮小差距
}
}這叫做調和迴圈(reconciliation loop),執行它的元件叫 controller。up,它照做,然後就沒事了。之後容器掛掉,除非你設了 restart 策略(那也只是本機 daemon 的簡單重試),沒有人在「持續確保」狀態正確。kubectl delete pod xxx,結果它馬上又出現了(名字不同)。因為你刪的是「現況」,而「期望」還寫著要 3 個——controller 只是盡忠職守。要真的移除,得去改期望狀態(改副本數或刪掉 Deployment)。┌──────────── 控制平面(Control Plane,叢集的大腦)────────────┐
│ kube-apiserver 所有操作的唯一入口(kubectl 打的就是它) │
│ etcd 儲存所有狀態的資料庫(期望狀態存這裡) │
│ kube-scheduler 決定新 Pod 該放到哪個節點 │
│ controller-manager 跑各種控制迴圈的地方 ← 上一張卡的主角 │
└──────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
┌────┴──── Node 1 ────┐ ┌──┴─── Node 2 ────┐ ┌───┴── Node 3 ───┐
│ kubelet │ │ kubelet │ │ kubelet │
│ └ 負責在本機把 Pod │ │ │ │ │
│ 跑起來、回報狀態 │ │ │ │ │
│ kube-proxy │ │ kube-proxy │ │ kube-proxy │
│ └ 處理 Service 的 │ │ │ │ │
│ 網路轉發 │ │ │ │ │
│ 容器執行環境 │ │ containerd │ │ containerd │
│ (containerd) │ │ │ │ │
│ [Pod] [Pod] [Pod] │ │ [Pod] [Pod] │ │ [Pod] │
└─────────────────────┘ └──────────────────┘ └─────────────────┘localhost 互相連線、共用同一組 port 空間(所以不能撞埠)。你幾乎不會直接建立 Pod,而是宣告一個 Deployment:「我要 3 個這樣的 Pod,用這個映像,更新時請逐步替換。」
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3 # ← 期望狀態:我要 3 個
selector:
matchLabels: { app: myapp }
template: # ← 每個 Pod 長什麼樣
metadata:
labels: { app: myapp }
spec:
containers:
- name: app
image: myregistry.io/myapp:1.4.2 # 明確版本,不用 latest(ch04)
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef: { name: db-secret, key: password } # 秘密注入(ch10)
resources:
requests: { memory: 512Mi, cpu: 250m }
limits: { memory: 1Gi, cpu: 1000m }
securityContext: # ch10 的安全實務
runAsNonRoot: true
readOnlyRootFilesystem: true
capabilities: { drop: [ALL] }apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector: { app: myapp } # 找到所有帶這個標籤的 Pod
ports:
- port: 80
targetPort: 8080http://myapp 就連得到——跟 Compose 用服務名稱互連是同一個心智模型(ch07),只是背後從單機的 DNS 換成了叢集 DNS + kube-proxy 的轉發規則。ClusterIP(只在叢集內,預設)、NodePort(每個節點開一個埠)、LoadBalancer(雲端商配一顆負載平衡器)。對外的 HTTP 流量通常再上一層用 Ingress 統一處理路由與 TLS。ch05 講過 Docker 的 HEALTHCHECK。K8s 把它拆成三個用途不同的探測,設錯會導致服務詭異地反覆重啟或流量被丟到還沒準備好的實例上。
startupProbe: # 給它最多 5 分鐘啟動(30 次 × 10 秒)
httpGet: { path: /actuator/health/liveness, port: 8080 }
failureThreshold: 30
periodSeconds: 10
livenessProbe: # 只看 process 本身,不看相依服務
httpGet: { path: /actuator/health/liveness, port: 8080 }
periodSeconds: 10
failureThreshold: 3
readinessProbe: # 可以看相依服務(DB、Redis)
httpGet: { path: /actuator/health/readiness, port: 8080 }
periodSeconds: 5
failureThreshold: 3management.endpoint.health.probes.enabled=true,就會提供 /actuator/health/liveness 與 /actuator/health/readiness 兩個端點,語意剛好對應。而且它會在應用開始優雅關機時自動把 readiness 標記為 DOWN,流量提早停止導入——配合 ch05 的 SIGTERM 處理,才能做到真正的零停機更新。這組設定直接決定你的服務穩不穩、以及叢集的錢有沒有花在刀口上。
resources:
requests: # 「我至少需要這麼多」→ 用於「排程決策」
memory: 512Mi
cpu: 250m # 250 milli-core = 0.25 顆 CPU
limits: # 「我最多只能用這麼多」→ 用於「執行時限制」(就是 ch02 的 cgroup)
memory: 1Gi
cpu: 1000mrequests 和 limits 設成一樣(獲得 Guaranteed QoS 等級,最不容易在節點資源緊張時被驅逐)。搭配 -XX:MaxRAMPercentage=75(ch09),讓 JVM 自動適應這個上限。requests 一定要設(保證排程時拿得到);limits 則常見的建議是「設寬鬆一點,或延遲服務乾脆不設」——因為 CPU 限流造成的延遲尖峰,通常比讓它偶爾多用一點 CPU 更糟。ch13 教了 Docker 的除錯,這裡是 K8s 版本。好消息是:問題的成因幾乎完全一樣,只是名字不同。
kubectl logs <pod> --previous # ← 關鍵:看「上一次崩潰前」的日誌
kubectl describe pod <pod> # 看 Events 區段--previous 這個旗標非常重要,因為現在這個容器可能才剛啟動、日誌還是空的。imagePullSecrets)、或架構不符(ch04 的 M1 問題)。kubectl describe pod 的 Events 會寫出確切的失敗原因。requests、或有 taint/親和性規則擋住、或 PVC 還沒綁定。kubectl describe pod 會直接寫「0/5 nodes are available: insufficient memory」。limits。kubectl describe pod 的 Last State 會顯示 OOMKilled、exit code 137。MaxRAMPercentage?limits 是不是太小(Spring Boot 建議至少 512Mi)?是不是真的洩漏?selector 標籤跟 Pod 的 labels 對不上。kubectl get pods # READY 欄是 0/1 就是 readiness 沒過
kubectl get endpoints myapp # 後端清單是空的?那就是選不到 Poddocker ps → kubectl get pods
docker logs -f X → kubectl logs -f X
docker exec -it X bash → kubectl exec -it X -- bash
docker inspect X → kubectl describe pod X
docker stats → kubectl top pods
docker events → kubectl get events --sort-by=.lastTimestamp
(無對應) → kubectl port-forward pod/X 8080:8080 ← 本機直連 Pod,除錯神器你已經有整個容器基礎(前 13 章)+ K8s 的核心心智模型。這比「只會照著 YAML 抄」的人扎實得多——因為 K8s 的所有行為都建立在容器的原理上(Pod 共用 namespace、limits 就是 cgroup、映像就是 OCI image)。
kind(Docker 裡跑 K8s,最輕)、minikube、或 Docker Desktop 內建的 K8s。把 ch12 那套 Compose 環境翻成 Deployment + Service + ConfigMap + Secret 手動做一遍——這個練習的學習密度最高。| Compose 的東西 | K8s 的對應 | 差別在哪 |
|---|---|---|
| service | Deployment(+它管的 Pod) | K8s 多了副本數、滾動更新、自我修復 |
| 容器 | Pod(可含多個容器) | Pod 內共用 network namespace,可用 localhost 互連 |
| 服務名稱互連 | Service(ClusterIP) | 同樣用名稱,但多了負載平衡與跨節點 |
| ports 對外 | Service(NodePort/LoadBalancer) 或 Ingress | Ingress 可統一處理路由與 TLS |
| environment | env / ConfigMap | ConfigMap 可跨多個服務共用 |
| .env 秘密 | Secret | 可掛成檔案、有 RBAC 權限控管 |
| volumes | PersistentVolumeClaim | 多了「跨節點」的儲存問題要處理 |
| healthcheck | liveness / readiness / startup 三種 probe | 用途分離:殺掉重建 vs 停止導流 vs 等啟動 |
| deploy.resources | resources.requests / limits | requests 影響排程,limits 才是 cgroup 限制 |
| restart: unless-stopped | (內建)控制迴圈自動維持 | 不只重啟,還能換節點重建 |
| Probe | 問題 | 失敗的後果 | 可以檢查相依服務嗎 |
|---|---|---|---|
| Startup | 啟動完了嗎? | 超過次數才殺;成功前暫停其他兩種 | 不需要(Java 一定要設) |
| Liveness | 你還活著嗎? | 殺掉容器並重建 | ❌ 絕對不要!DB 抖動會害你無限重啟 |
| Readiness | 現在能接工作嗎? | 從 Service 後端移除,不再導流(不殺) | ✅ 這才是檢查 DB/Redis 的地方 |
| 狀態 | 意思 | 常見原因 | 第一個指令 |
|---|---|---|---|
| CrashLoopBackOff | 反覆啟動又崩潰 | 設定錯誤、相依連不上、liveness 設錯 | kubectl logs <pod> --previous |
| ImagePullBackOff | 拉不到映像 | 名稱/標籤錯、缺 imagePullSecrets、架構不符 | kubectl describe pod(看 Events) |
| Pending | 排不進任何節點 | 資源不足、taint/親和性擋住、PVC 未綁定 | kubectl describe pod |
| OOMKilled | 記憶體超過 limits | limits 太小、JVM 沒設 MaxRAMPercentage | describe 看 Last State(exit 137) |
| Running 但 READY 0/1 | 活著但不接流量 | readiness 沒過,或 Service selector 對不上 | kubectl get endpoints <service> |
點擊卡片翻面查看答案,共 12 張。