Kubernetes 故障模擬: OOMKilled、Probe、ConfigMap 與 Namespace
DevOps 學習筆記
承接前兩篇 (K8s 核心架構、Service/HPA/Debug), 這篇深入 K8s 故障場景的實際行為, 加上 ConfigMap、Secret、Namespace 的用法 目標是能預測和診斷問題, 而不只是會 apply YAML
環境: k3s v1.34 single node cluster + go-api (Go HTTP server, scratch image)
OOMKilled — Container 被 Kernel 殺掉
什麼是 OOMKilled
Container 的記憶體用量超過 limits.memory 設定的上限, Linux kernel 的 OOM killer 直接把 process 殺掉
不是 K8s 殺的, 是 kernel 殺的 K8s 只是觀察到 container 死了, 紀錄原因
實驗
在 go-api 加一個 /oom endpoint, 瘋狂配置記憶體:
|
|
Deployment 設定 memory limit 為 64Mi:
|
|
用 curl 打 /oom, 連線直接斷掉 — container 被殺了, 所以 connection 中斷
觀察結果
|
|
|
|
| 欄位 | 值 | 意義 |
|---|---|---|
| Reason | OOMKilled | 超過 memory limit, 被 OOM killer 終止 |
| Exit Code | 137 | 128 + 9 = SIGKILL, 不是程式自己退出, 是被外力殺掉的 |
| RESTARTS | 增加 | K8s 自動重啟了 container |
重啟後因為 /oom 沒有再被打, Pod 穩定下來 但如果 container 啟動就 OOM (比如 init 時就吃太多記憶體), 就會不斷重啟, 最終進入 CrashLoopBackOff
OOMKilled vs CrashLoopBackOff
這兩個常常搞混, 但它們不是同一個層級:
- OOMKilled 是原因 — 記憶體超標被殺
- CrashLoopBackOff 是狀態 — container 反覆 crash, K8s 拉長重啟間隔 (指數退避: 10s → 20s → 40s → … 最多 5 分鐘)
一個 container 可以因為 OOMKilled 而進入 CrashLoopBackOff, 也可以因為其他原因 (程式碼 bug、設定錯誤) 進入 CrashLoopBackOff
requests vs limits — 兩個完全不同的東西
|
|
requests — 「我至少需要這麼多」
Scheduler 用 requests 決定 Pod 放到哪個 Node 如果 Node 剩餘可分配資源 < Pod 的 requests, Scheduler 就不會把 Pod 排到那個 Node, Pod 狀態會是 Pending
limits — 「最多只能用這麼多」
Container 實際執行時的硬上限 超過就被 OOM kill
關鍵差異
| requests | limits | |
|---|---|---|
| 誰看 | Scheduler | Kernel (cgroup) |
| 什麼時候看 | 排程時 | 執行時 |
| 超過會怎樣 | Pod Pending (排不上去) | OOMKilled (被殺掉) |
實驗: ResourceQuota 模擬資源不足
因為本機 k3s node 有 39GB 記憶體, 用小小的 requests 根本排不滿 改用 ResourceQuota 在 namespace 層級限制:
|
|
Deploy 4 個 Pod, 各 requests 32Mi (4 × 32Mi = 128Mi > quota 100Mi):
|
|
3 個 Pod 成功, 第 4 個被擋 注意這裡和真正的 Node 資源不足不同:
| 情境 | 錯誤來源 | Pod 狀態 | Event |
|---|---|---|---|
| ResourceQuota 超過 | API server 直接拒絕 | Pod 不存在 | FailedCreate on ReplicaSet |
| Node 資源不足 | Scheduler 找不到合適 Node | Pending | FailedScheduling on Pod |
Probe — K8s 怎麼知道你的 App 是不是正常的
Container 在跑不代表 app 正常 — 可能 deadlock 了, 可能 database 斷了 K8s 需要一個方法去確認
Probe 就是 K8s 定期打你指定的 endpoint, 看 HTTP status code:
- 200 → 正常
- 非 200 → 有問題
兩種 Probe 的差別
| Readiness Probe | Liveness Probe | |
|---|---|---|
| 問的問題 | 「你準備好接流量了嗎?」 | 「你還活著嗎?」 |
| 失敗後果 | 從 Service 移除, 不送流量 | 砍掉 container, 重啟 |
| Pod 還在嗎 | 在, 只是不接客 | container 被殺, 重新啟動 |
| 用途 | 啟動中、暫時忙碌、依賴斷了 | 真的卡死了, 需要重啟才能救 |
用餐廳比喻:
- Readiness = 「廚房準備好了嗎?」→ 沒好就先不帶客人進來, 但廚房還在
- Liveness = 「廚師還有呼吸嗎?」→ 沒有就換一個廚師 (重啟)
設定方式
在 container spec 裡用不同的 key 名稱:
|
|
可以同時設兩個, 也可以指向同一個 endpoint Production 裡通常分開: readiness 檢查比較多 (database、cache), liveness 只檢查 app 本身有沒有卡死
Readiness Probe 實驗
設定 readiness probe 指向不存在的 /ready endpoint, probe 每 5 秒打一次都拿到 404:
|
|
|
|
READY 是 0/1 — container 在跑, 但 K8s 認定沒準備好
|
|
|
|
檢查 EndpointSlice:
|
|
EndpointSlice 會列出所有 address, 但每個 address 標記 ready: false Service 不會送流量到 ready: false 的 Pod
注意: K8s v1.33+ 開始
kubectl get endpoints會出 deprecation warning, 改用kubectl get endpointslice -l kubernetes.io/service-name=<svc>查
Liveness Probe 實驗
設定 liveness probe 指向不存在的 /alive endpoint, 每 3 秒失敗一次, 連續 3 次後 K8s 殺掉 container:
|
|
|
|
和 readiness 完全不同 — RESTARTS 不斷增加, container 被殺了又重啟, 最終進入 CrashLoopBackOff
什麼時候用哪個 — 常見錯誤
一個常見的錯誤: 用 liveness probe 檢查 database 連線 如果 database 掛了, liveness 失敗 → K8s 重啟你的 app → app 起來後 database 還是掛著 → 又 liveness 失敗 → 無限重啟
正確做法: database 連線放 readiness probe database 掛了, readiness 失敗, Pod 只是暫時不接流量, 等 database 恢復就自動回來, 不需要重啟
ConfigMap 與 Secret
解決什麼問題
App 需要設定值 (環境、log level、DB 密碼) 如果寫死在 code 裡, 改設定就要重新 build image, 不同環境 (dev/staging/prod) 要不同 image
ConfigMap 和 Secret 把設定從 image 抽出來, 同一個 image 在不同環境只換設定
ConfigMap — 非敏感設定
一個 key-value 的設定檔, 存在 K8s 裡:
|
|
Secret — 敏感資料
和 ConfigMap 幾乎一樣, 但用來放密碼、API key:
|
|
type: Opaque 是 Secret 的類型, 表示「一般用途的 secret」 90% 的情況用 Opaque
其他內建類型:
| type | 用途 |
|---|---|
| Opaque | 通用, 自己定義 key/value (最常用) |
| kubernetes.io/tls | TLS 憑證 (必須有 tls.crt 和 tls.key) |
| kubernetes.io/dockerconfigjson | Docker registry 登入帳密 |
注入 — 把值傳進 Container
Container 本身只認兩種東西: 環境變數和檔案 需要把 ConfigMap / Secret 的值變成 container 讀得到的格式
方式一 — 環境變數:
|
|
envFrom 把整個 ConfigMap 攤開, valueFrom 從 Secret 挑單一個 key
方式二 — 掛成檔案 (volume mount):
|
|
Container 裡會有 /etc/config/APP_ENV 這個檔案, 內容是 staging 適合設定檔比較大的情況 (例如 nginx.conf)
實驗結果
Go code 用 os.Getenv 讀取:
|
|
|
|
三個值都成功注入
K8s Secret 安全嗎
K8s Secret 預設只是 base64 編碼, 不是加密 任何有 kubectl 權限的人都能:
|
|
Production 裡不會直接把密碼寫在 Secret YAML 然後 commit 到 git 實際做法:
|
|
中間同步的「工具」通常是:
- External Secrets Operator — 裝在 K8s 裡, 定期從外部 secret manager 同步到 K8s Secret
- CI/CD pipeline — deploy 時用
kubectl create secret建出來 - Terraform — 用
kubernetes_secretresource 建出來
K8s Secret 是外部 secret manager 和 container 之間的橋樑 — container 不會直接呼叫 GCP Secret Manager API, 它只從 K8s Secret 讀環境變數
如果用 GCP Cloud Run (不是 K8s), 則不需要這層橋樑 — Cloud Run 可以直接從 GCP Secret Manager 注入環境變數
Namespace — Cluster 裡的隔離區域
什麼是 Namespace
想像一個 cluster 是一棟辦公大樓:
- Namespace = 樓層 — 每個樓層有自己的房間 (Pod)、服務 (Service)、設定 (ConfigMap)
- 不同樓層可以有一樣名字的房間, 互不衝突
- 但大樓的電梯 (Node)、停車場 (Storage) 是共用的
- 預設樓層之間的走廊是通的 (網路互通)
不是 .NET 那種 code 組織用的 namespace, 也不是 Linux kernel 的 namespace (process 隔離) K8s Namespace 是資源的邏輯分群
用途
|
|
什麼隔離, 什麼不隔離
Namespace 各自獨立的:
- Pod、Deployment、Service、ConfigMap、Secret — 名字可以重複, 互不干擾
- ResourceQuota — 可以對不同 namespace 設不同資源配額
- RBAC 權限 — 可以限制某人只能操作某個 namespace (RBAC = Role-Based Access Control, 用角色控制誰能做什麼)
整個 Cluster 共用的:
- Node — 所有 namespace 的 Pod 都跑在同一批機器上
- Storage (PersistentVolume)
- 網路 — 預設跨 namespace 可以互通, 要擋要另外設 NetworkPolicy
跨 Namespace 網路互通實驗
建立 dev 和 staging namespace, 各自部署 go-api 和 Service (名字都叫 go-api, 不衝突)
從 dev 的 Pod 打 staging 的 Service:
|
|
|
|
成功拿到回應, 證明跨 namespace 網路預設是通的
K8s 內部的 DNS 規則: <service-name>.<namespace>.svc.cluster.local 同 namespace 內可以省略, 直接用 service name
注意: scratch image 裡面沒有任何工具 (連 wget、curl、sh 都沒有), 所以不能
kubectl exec進去 debug 需要另外跑一個有工具的 Pod 來測
必須記住的東西
故障診斷對照表
| 現象 | 原因 | 怎麼查 |
|---|---|---|
| OOMKilled (exit code 137) | 超過 memory limit | kubectl describe pod 看 Last State |
| CrashLoopBackOff | Container 反覆 crash | kubectl logs --previous 看上次的 log |
| Pending | Node 資源不足或 Scheduler 排不上 | kubectl describe pod 看 Events |
| FailedCreate | ResourceQuota 超標 | kubectl get events 看 ReplicaSet event |
| READY 0/1 但 Running | Readiness probe 失敗 | kubectl describe pod 看 Conditions |
| RESTARTS 不斷增加 | Liveness probe 失敗或 app crash | kubectl describe pod 看 probe 設定 |
Probe 選擇原則
- 檢查 app 本身卡死 → liveness (重啟能救)
- 檢查外部依賴 (DB、cache) → readiness (重啟沒用, 等依賴恢復就好)
- 不確定的時候 → 先用 readiness, 比 liveness 安全
requests vs limits 一句話
- requests = Scheduler 排程用, 「我至少需要這麼多才能被排到 Node 上」
- limits = Runtime 上限, 「超過這麼多就被殺」
References
- Kubernetes Documentation — Container Resources — requests 與 limits 的完整說明
- Kubernetes Documentation — Pod Lifecycle — Pod 狀態與 restart policy
- Kubernetes Documentation — Liveness, Readiness and Startup Probes — 三種 probe 的設定與差異
- Kubernetes Documentation — ConfigMaps — ConfigMap 的建立與使用方式
- Kubernetes Documentation — Secrets — Secret 類型、建立、注入方式
- Kubernetes Documentation — Namespaces — Namespace 的用途與隔離邊界
- Kubernetes Documentation — Resource Quotas — 用 ResourceQuota 限制 namespace 資源用量
- Kubernetes Documentation — DNS for Services and Pods — 跨 namespace 的 DNS 規則
- Kubernetes Documentation — EndpointSlice — EndpointSlice 取代舊版 Endpoints