今天主要都著重在觀念介紹!
就沒有那麼多的實際操作啦
所以今天可以快速看過!
廢話不多說,進入今天的主題!
目前!!
我們的 Application 已經不像一開始只有一個單純的 FastAPI,而是開始依賴其他服務:
API
├── Redis
└── PostgreSQL
這時候就會出現一個很重要的問題。
假設 FastAPI 本身沒有掛掉,Uvicorn Process 也還正常執行,但是 Redis 突然無法連線:
FastAPI:正常
Redis:掛掉
Kubernetes 可能還是會看到:
Pod = Running
但這個 API 真的算「健康」嗎?
答案是不一定。
因為如果你的 API 處理 Request 時一定要使用 Redis,那 Redis 掛掉之後,即使 Python Process 還活著,使用者的 Request 仍然可能全部失敗。
所以今天要理解的核心,就是:
Pod 活著
跟:
Pod 可以正常提供服務
其實是兩件不同的事情。
先理解 Kubernetes 裡面一個非常重要的觀念:
STATUS = Running
主要是在告訴你:
Container 裡面的主要 Process 還在執行。
例如我們的 Container 執行:
uvicorn app.main:app
只要 Uvicorn 還沒有 Crash,Container 就可能維持:
Running
但是 Application 背後可能已經發生:
Redis 無法連線
PostgreSQL 無法連線
外部 API 無法使用
也就是說:
Container Process 還活著
≠
Application 可以正常提供服務
這也是 Kubernetes 為什麼除了 Container 是否正在 Running 之外,還需要額外判斷:
Liveness
Readiness
可以先把它們記成:
Liveness
→ 你還活著嗎?
Readiness
→ 你現在可以工作嗎?
這兩個問題看起來很像,但用途完全不同。
Liveness Probe 處理的是:
這個 Application 是否已經壞到需要重新啟動?
我們之前 FastAPI 已經準備過:
@app.get("/health/live")
def live():
return {
"status": "alive"
}
接著可以在 Deployment 裡加入:
livenessProbe:
httpGet:
path: /health/live
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
意思是 Container 啟動之後,Kubernetes 先等:
10 秒
然後每:
10 秒
向:
/health/live
送一次 HTTP Request。
正常情況下:
GET /health/live
↓
200 OK
↓
Kubernetes 認為 Application 還活著
但如果 Application 發生 Deadlock、程式完全卡住,或 HTTP Server 已經無法正常回應:
GET /health/live
↓
失敗
↓
再次檢查
↓
持續失敗
達到失敗條件之後,kubelet 就可能重新啟動這個 Container。
所以 Liveness Probe 的主要目的不是決定「要不要把流量送給它」,而是決定:
這個 Container
需不需要被重新啟動?
這一點非常重要。
因此一般來說,不要把太多外部 Dependency 塞進 Liveness Probe。
例如:
Redis 掛掉
↓
Liveness 失敗
↓
API 被 Restart
↓
Redis 還是掛掉
↓
API 又 Restart
這樣重新啟動 API 根本沒有解決問題。
因此比較常見的設計是:
Liveness
→ 確認 Application 自己有沒有活著
Readiness
→ 確認 Application 現在能不能正常工作
接著就是今天最重要的 Readiness Probe。
Readiness 問的問題不是:
你有沒有活著?
而是:
你現在準備好接收使用者 Request 了嗎?
例如我們的 API 需要 Redis 才能正常工作,那就可以增加:
@app.get("/health/ready")
def ready():
redis_client.ping()
return {
"status": "ready"
}
這段程式非常簡單。
Kubernetes 呼叫:
GET /health/ready
FastAPI 就會先執行:
redis_client.ping()
如果 Redis 正常:
Redis PING 成功
↓
回傳 200
↓
Pod Ready
如果 Redis 掛掉:
Redis PING 失敗
↓
Endpoint 回傳錯誤
↓
Readiness Probe 失敗
↓
Pod Not Ready
Deployment 裡加入:
readinessProbe:
httpGet:
path: /health/ready
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
代表 Container 啟動五秒之後,Kubernetes 每五秒檢查一次:
/health/ready
這是 Liveness 和 Readiness 最大的差異之一。
假設:
Readiness Probe 失敗
通常 Kubernetes 不會因為這件事情直接 Restart Container。
而是把 Pod 標記成:
Not Ready
例如原本:
READY STATUS
1/1 Running
可能變成:
READY STATUS
0/1 Running
注意這個畫面非常重要:
0/1 Running
乍看好像很奇怪。
既然:
STATUS = Running
為什麼:
READY = 0/1
原因就是:
Running
→ Process 還活著
Ready
→ Kubernetes 認為它可以接流量
這個 Pod 的 Uvicorn Process 還活著,所以它仍然是:
Running
但是 Readiness Probe 沒有通過,所以 Kubernetes 認為:
你現在不能提供服務。
Readiness 真正重要的地方,會出現在 Service。
假設我們現在有三個 API Pod:
Service
│
├── Pod A Ready
├── Pod B Ready
└── Pod C Not Ready
正常情況下,Service 應該只把流量送給:
Pod A
Pod B
而不是:
Pod C
所以 Readiness Probe 可以看成 Kubernetes 幫我們建立的一道保護機制:
Application 暫時出問題
↓
Readiness Failed
↓
Pod 仍然活著
↓
暫時不要接收正常流量
等 Redis 恢復之後:
Redis 恢復
↓
/health/ready 又回傳成功
↓
Pod 重新 Ready
↓
Service 又可以把流量送過來
不一定需要重新建立 Pod。
學 Kubernetes 很重要的一個方法就是:
故意把東西弄壞。
例如我們原本:
readinessProbe:
httpGet:
path: /health/ready
port: 8000
故意改成不存在的:
readinessProbe:
httpGet:
path: /health/wrong
port: 8000
重新 Apply:
kubectl apply -f k8s/
接著查看:
kubectl get pods -n cka-lab
可能看到:
NAME READY STATUS RESTARTS
api-xxxxxxxxxx-xxxxx 0/1 Running 0
這個結果非常值得理解。
STATUS = Running
代表:
Uvicorn 還活著。
但是:
READY = 0/1
代表:
Readiness Probe 沒有通過。
也就是:
Process 活著
但是
Kubernetes 不認為它現在可以服務。
這就是 Running 與 Ready 最直接的差異。
看到:
0/1 Running
之後,不應該第一個反應就是:
重開 Docker
或:
刪掉 Pod 看看。
更好的做法是先找原因。
例如:
kubectl describe pod API_POD -n cka-lab
往下看 Events,很可能看到類似:
Readiness probe failed
或者:
HTTP probe failed with statuscode: 404
這時候就可以推理:
Pod Running
↓
但是 Ready 0/1
↓
查看 describe
↓
Readiness Probe Failed
↓
檢查 path / port / Application
接著才發現:
/health/wrong
根本不存在。
這才是真正在 Kubernetes 裡面的 Troubleshooting 流程。
而不是:
不知道發生什麼事
↓
重開
↓
好了
↓
但不知道為什麼
除了 Health Check,今天還有另一個非常重要的 Kubernetes 概念:
Resources
一個 Kubernetes Cluster 裡面可能同時跑很多 Pod。
例如:
Node
├── API Pod
├── Redis Pod
├── PostgreSQL Pod
├── Worker Pod
└── 其他 Pod
Scheduler 在決定:
新的 Pod 要放在哪一台 Node?
就必須知道:
這個 Pod 大概需要多少資源?
因此可以在 Deployment 裡設定:
resources:
requests:
cpu: 100m
memory: 128Mi
Request 可以先理解成:
這個 Container 希望 Kubernetes 至少替它保留多少資源。
Kubernetes CPU 很常看到:
100m
250m
500m
這裡的:
m
代表:
millicpu
可以這樣理解:
1000m = 1 CPU Core
500m = 0.5 Core
100m = 0.1 Core
因此:
cpu: 100m
表示這個 Container Request:
0.1 CPU Core
例如:
memory: 128Mi
可以先簡單理解成:
大約 128 MiB Memory
所以:
requests:
cpu: 100m
memory: 128Mi
可以想成這個 Pod 在跟 Kubernetes 說:
我要執行的話,
請至少幫我準備:
CPU:0.1 Core
Memory:128Mi
這裡有一個非常容易誤解的地方。
Requests 並不是單純:
寫給人看的建議值。
Scheduler 真的會使用它。
假設現在有:
Node A
剩餘可分配 Memory:500Mi
你建立一個 Pod:
resources:
requests:
memory: 1Gi
Scheduler 看到的是:
這個 Pod 宣告需要 1Gi
但是:
Node A 只剩 500Mi
所以它可能直接判斷:
放不下。
結果 Pod 就可能停留在:
Pending
即使你打開電腦監控工具,發現:
實際 RAM 明明還有很多。
還是有可能排程失敗。
原因是 Scheduler 不會單純根據:
現在 Application 實際用了多少 RAM
來做 Scheduling。
它會根據 Pod 宣告的:
Resource Requests
判斷 Node 是否還有足夠的可分配資源。
因此可以把 Request 想成:
Scheduling 時的資源需求。
Request 處理的是:
我至少需要多少。
而 Limit 處理的是:
我最多可以使用多少。
例如:
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
這代表:
CPU Request:100m
CPU Limit:500m
Memory Request:128Mi
Memory Limit:256Mi
可以簡化理解成:
Request
→ Scheduler 排程時的重要依據
Limit
→ Container 執行時不能無限制使用資源
例如:
limits:
cpu: 500m
代表 CPU 使用量受到:
0.5 Core
的限制。
如果 Application 很忙,想使用更多 CPU,Linux / Kubernetes 可能會進行:
CPU Throttling
簡單來說就是:
你想跑更快
↓
但是你的 CPU Limit 只有 500m
↓
系統限制 CPU 使用時間
↓
Application 變慢
但通常不會只是因為超過 CPU Limit 就直接把 Container 殺掉。
Memory 的行為就不一樣。
假設:
limits:
memory: 256Mi
但是你的 Application 持續增加 Memory:
150Mi
↓
200Mi
↓
250Mi
↓
300Mi
超過 Memory Limit 之後,Container 有可能因為記憶體不足被終止。
這時候你可能看到:
OOMKilled
OOM 就是:
Out Of Memory
所以以後看到:
OOMKilled
第一個應該想到的方向就是:
Application 實際 Memory Usage
VS
Container Memory Limit
可能是:
Memory Limit 設太低
也可能是:
Application Memory Leak
而不是看到 Pod Restart 就直接認為:
Kubernetes 壞掉了。
假設某個 API 設定:
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
可以把整件事情理解成:
Scheduling 階段
Pod:
「我要至少 100m CPU、128Mi Memory。」
Scheduler:
「哪一台 Node 有能力容納你?」
Pod 被排到 Node 之後開始執行:
Runtime 階段
CPU 最多:
500m
Memory 最多:
256Mi
如果 CPU 嘗試使用超過限制:
可能被 Throttling (限制、節流)
如果 Memory 超過限制:
可能 OOMKilled
因此:
Requests
→ 主要影響 Scheduling
Limits
→ 主要限制 Runtime 資源使用
這是目前最值得先記住的差異。
目前我們的架構已經變成:
Client
│
▼
Service
│
▼
API Pod
│
├── Redis
│
└── PostgreSQL
Kubernetes 不只是要知道:
API Container 有沒有在跑。
還需要知道:
Application 是否健康?
現在是否可以接 Request?
需要多少 CPU?
需要多少 Memory?
最多允許使用多少資源?
所以今天加入兩組非常重要的機制。
第一組是:
Liveness Probe
Readiness Probe
負責 Application Health。
可以記成:
Liveness
→ 我還活著嗎?
→ 長期失敗可能 Restart Container
Readiness
→ 我現在可以工作嗎?
→ 失敗就先不要送流量給我
第二組則是:
Resource Requests
Resource Limits
負責資源管理。
可以記成:
Requests
→ 排程時,我需要多少資源
Limits
→ 執行時,我最多可以用多少資源
因此以後看到:
0/1 Running
不要只覺得「Pod 明明 Running 為什麼不能用」,而應該想到:
Readiness Probe
看到:
Pod Pending
除了 Image、PVC 等問題之外,也應該想到:
Resource Request 是否超過 Node 能提供的資源?
看到:
OOMKilled
則應該想到:
Memory Usage
vs
Memory Limit
到了這裡,我們開始不只是會:
把 Pod 跑起來
而是開始學真正 Kubernetes 在 Production 環境很重要的事情:
讓 Kubernetes 知道:
什麼時候 Application 是健康的,
什麼時候可以接流量,
什麼時候需要重新啟動,
以及這個 Application 到底需要多少資源。
明天開始!
要來做一個第一次 Kubernetes 綜合實戰演練
假裝來把系統弄壞,再來對應的 troubleshooting 如何做~!