針對異構語言特性,調整 K8S 資源分配以達到最佳效能。
K8S 的 resources.requests 是排程依據(告訴 Scheduler 這個 Pod 大概需要多少資源),resources.limits 是硬上限,超過會被 OOMKilled(記憶體)或被 CPU 節流(CPU)。這兩個值該怎麼設,Java 與 Python 因為記憶體管理模型完全不同,考量點也不一樣。
JVM 在容器裡跑,預設會嘗試感知自己實際被分配到多少資源(-XX:+UseContainerSupport,Java 10+ 預設開啟),並用 -XX:MaxRAMPercentage 決定 Heap 上限佔可用記憶體的比例。services/user-service/Dockerfile 的啟動參數:
CMD ["java", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"-jar", "app.jar"]
這串設定實際運作起來是什麼樣子,直接進容器驗證:

▲ JVM 資源調優實測:cgroup 記憶體上限、MaxRAMPercentage 換算出的實際 Heap 大小、以及 kubectl top 顯示的真實記憶體佔用
有以下三個關鍵數字對得上:
limits.memory: 512Mi
/sys/fs/cgroup/memory.max 顯示 536870912(=512Mi)——這是 JVM 透過 cgroup 介面實際讀到的記憶體邊界MaxHeapSize 算出 402653184(=384Mi),正好是 512Mi 的 75%這證明 MaxRAMPercentage 不是一個「建議值」,是 JVM 真的會拿容器的 cgroup 限制去乘上這個百分比,算出實際的 Heap 上限。
kubectl top pods 顯示 user-service 閒置時就用了 327Mi,逼近 512Mi 的上限——但 Heap 上限明明只設了 384Mi,為什麼閒置佔用就已經這麼高?因為 JVM 的實際記憶體佔用(RSS)包含:
MaxRAMPercentage 限制)只調 MaxRAMPercentage 而不管其他部分,容器實際佔用還是可能逼近甚至超過 limits.memory。如果要更精細地控制,-XX:MaxMetaspaceSize、-Xss(每執行緒 stack 大小)都可以個別限制。
Python 的 CPython 直譯器不像 JVM 有獨立的 Heap 上限設定——記憶體怎麼用,完全取決於程式邏輯本身在做什麼。wafer-backend 用 Pandas 處理 Delta Lake 的資料,一次 groupby 或統計運算會把整份資料載進記憶體操作,併發請求一多,記憶體用量會隨併發數線性甚至更快地增加。
這正是 Day 25 實際發生過的事:wafer-backend 在 30 個併發請求同時做 pandas 運算時被 OOMKilled。對照這次的 kubectl top 結果,wafer-backend 在閒置狀態只用 117Mi,跟當時壓力測試下衝破 1Gi 限制的情形反差很大——這正是 Python 記憶體調優最大的陷阱:閒置時的數字完全不能反映尖峰負載下的實際需求。
對於這類工作負載,比起單純調高 limits.memory(治標),更根本的做法是控制併發度(用 Gateway 層的 rate limit、或 Uvicorn 的 worker/併發數限制)或改用分批 (chunking) 處理避免一次性把整份資料攤開在記憶體裡。
| Java (JVM) | Python (Pandas/FastAPI) | |
|---|---|---|
| 記憶體上限機制 | MaxRAMPercentage 可設定 Heap 佔比 |
無內建機制,取決於應用邏輯 |
| 閒置基線 | 偏高(Metaspace、Thread Stack 常駐) | 偏低 |
| 尖峰風險 | GC 壓力上升、但通常不會突然暴衝 | 併發運算可能瞬間吃光記憶體 |
| 調優重點 | 拆解 RSS 各部分分別限制 | 控制併發度、批次處理 |
limits.memory 這個數字,Java 與 Python 該怎麼定,考量的東西完全不同——Java 有明確的 Heap 換算公式可以驗證,Python 更依賴實際壓測找出尖峰需求。調優不是設一次就結束,需要搭配 Day 22 建立的監控持續觀察。明天回到現場排查的主題:Pod 進入 CrashLoopBackOff 時,實際的除錯步驟。