阿里雲國際 阿裡雲 ECS 實例記憶體溢出(OOM Killed)導致 MySQL/Java 進程崩潰治理
阿里雲國際 先搞清楚:OOM Killed 不是程式自己死掉
很多人第一次看到 MySQL 或 Java 進程突然退出,第一反應是程式有 bug、連線壞了,或者 JVM 出了奇怪問題。其實在阿里雲 ECS 上更常見的情況,是系統記憶體被吃滿後,Linux 核心啟動 OOM Killer,主動挑一個或多個進程殺掉,讓整台機器不要直接卡死。也就是說,真正的兇手不是某個單一應用,而是整體記憶體水位失控。
這類問題最麻煩的地方,不在於它會不會發生,而在於它常常反覆發生:MySQL 被殺後資料庫連不上,Java 服務跟著超時;Java 重啟後又把快取、堆、直譯記憶體一起拉高;最後整台 ECS 在高峰期不斷震盪。要治理這種問題,不能只靠重啟,必須從定位、止血、調參、監控四個層面一起做。
一、先定位:到底是誰吃光了記憶體
處理 OOM 的第一步,不是急著改參數,而是先確認記憶體到底是怎麼耗盡的。很多現場看起來像 MySQL 或 Java 被殺,實際上可能是備份任務、日誌分析、批次匯入、圖片處理服務,甚至是排程腳本臨時暴增把記憶體吃掉。只要根因沒找準,後面所有調整都只是補洞。
先看系統層面,重點是確認是否真的觸發了 OOM Killer,以及當時哪個進程被選中:
dmesg | tail -n 100
journalctl -k --since '1 hour ago'
如果日誌中出現 Out of memory、Killed process、oom_score_adj 等字樣,基本可以確定是核心層面的記憶體保護機制在工作。接著看當時的整體可用量與交換空間狀態:
free -h
vmstat 1 5
ps -eo pid,ppid,%mem,%cpu,rss,cmd --sort=-rss | head
這裡要注意一個常見誤區:很多人只看 Mem 可用量,卻忽略了 page cache、buffer、shared memory 與 swap 的影響。ECS 上如果沒留足夠的空間給系統與背景程序,MySQL 和 Java 再怎麼優化也會在高峰期被擠爆。
若機器上同時跑了多個服務,建議進一步看每個進程的實際常駐記憶體,而不是只看虛擬記憶體。虛擬記憶體很容易誤導人,真正決定 OOM 風險的是 RSS 與整體提交量。對於容器化部署,還要再看 cgroup 限制,因為有些進程不是吃爆了整台 ECS,而是先撞到容器內存上限。
二、MySQL 為什麼容易成為第一個被殺的對象
MySQL 的記憶體管理看起來保守,實際上在高併發或錯誤配置下也很容易膨脹。尤其是 InnoDB buffer pool、每連線私有記憶體、排序緩衝、臨時表、查詢快取相關配置,任何一項放大後,都可能把整個系統壓到臨界點。
最常見的問題是把 `innodb_buffer_pool_size` 設得太滿,幾乎等於機器總記憶體。這種做法在單一資料庫專機上看似可以提高命中率,但只要系統還要承擔日誌、監控代理、備份程序、SSH、cron、以及其他輔助服務,就沒有足夠餘裕。真正安全的做法,是給作業系統、檔案快取、守護程序和突發負載留下明顯空間,而不是把記憶體全塞給 InnoDB。
另一個容易被忽略的是連線數。MySQL 的部分記憶體是按連線分配的,像 `sort_buffer_size`、`join_buffer_size`、`read_buffer_size`、`read_rnd_buffer_size`、臨時表相關配置,單看每個數字不大,但乘上高峰連線數後就會非常可觀。很多資料庫在流量平時沒事,一到大促、批次匯入、報表查詢就突然爆掉,根本原因通常不是主快取,而是 per-thread memory 疊加過頭。
因此治理 MySQL 的思路,不是單純把某個 buffer 調大,而是把記憶體當成整體預算來分配。你需要先回答三個問題:第一,這台 MySQL 是否獨占機器;第二,高峰時的連線上限是多少;第三,是否存在大量大查詢、臨時表與排序操作。只有把場景說清楚,參數調整才有意義。
MySQL 調整原則
實務上可遵循幾個原則。第一,`innodb_buffer_pool_size` 不要追求極限值,先保留系統與波動空間。第二,連線池要控制在業務真正需要的範圍內,不要讓 `max_connections` 成為隱性炸彈。第三,凡是按連線放大的記憶體參數,都應該以峰值連線數反推總消耗,而不是看到單位值小就放心。第四,大查詢和報表任務盡量拆出去,避免和核心交易庫共用同一組記憶體預算。
如果資料庫已經多次因 OOM 被殺,先不要急著加大資源,應該先把慢查詢、臨時表、批次腳本和錯峰策略處理好。很多時候,真正的問題不是機器太小,而是應用沒有控制查詢形狀。
三、Java 進程為什麼也常常成為犧牲品
阿里雲國際 Java 的 OOM 事件經常讓人混淆。JVM 裡的 `OutOfMemoryError` 是應用層異常,但被 Linux OOM Killer 殺掉則是系統層事件,兩者不是一回事。前者通常還能留下堆疊與錯誤現場,後者往往是直接被系統 SIGKILL,來不及寫任何內容。這也是為什麼很多 Java 服務明明沒有報出 `OutOfMemoryError`,卻還是毫無預警地退出。
Java 的記憶體不只堆內存。除了 `-Xmx`、`-Xms` 這些大家熟悉的 JVM heap,還有 Metaspace、Thread Stack、Direct Memory、Code Cache、JNI 以及各種 Native 開銷。特別是在使用 Netty、Mina、NIO、RPC 框架、壓縮庫、影像處理庫時,直接記憶體與本地記憶體常常會在不知不覺中長高。很多人只把 `-Xmx` 設成機器記憶體的一大半,卻忘了 JVM 不是機器上唯一的吃記憶體者,結果一到流量高峰就被系統判定為超載。
Java 服務的治理,重點是讓 JVM 的配置與 ECS 的物理記憶體保持安全距離。`-Xmx` 不應該等於可用記憶體,也不應該讓 JVM 將空間吃到幾乎沒有緩衝。常見做法是先扣掉作業系統、頁面快取、代理程式、日誌緩衝與其他本機進程,再把剩餘部分分配給堆內存,並為 Direct Memory 和線程棧預留額度。若應用線程數很多,`-Xss` 也不能無腦設大,否則每個線程都會成為隱性記憶體成本。
如果 JVM 跑在容器內,更要注意容器與宿主機雙重限制。容器記憶體上限會先於 ECS 物理上限生效,這時候你看到的未必是 Linux OOM Killer,也可能是容器被內核限制後直接回收。治理上要同時看宿主機水位與容器內水位,否則常常只修了一半。
Java 調整重點
第一,把 `-Xmx`、`-Xms`、`MaxDirectMemorySize`、`-Xss` 當成一組來看,不要分開調。第二,確認 GC 日誌與啟動參數完整開啟,否則你只知道進程死了,卻不知道死前是堆膨脹、直接記憶體外洩,還是線程數暴增。第三,關注長時間運行後的元空間與本地記憶體增長趨勢,特別是動態載入類、頻繁熱部署、插件化架構的系統。
阿里雲國際 如果服務經常在流量高峰被殺,往往不是單次請求太大,而是併發控制失效。這時候除了 JVM 調參,還要回到應用設計,檢查快取大小、隊列堆積、批次任務、預載入資料,以及是否存在一次性拉取大集合的寫法。Java 的記憶體問題,很多時候是架構問題,不是單純的參數問題。
四、治理 OOM,不能只靠調參
如果把 OOM 當成單純的配置錯誤,很容易陷入一個循環:調大機器、調小參數、再被打爆、再重啟。真正有效的治理,是建立記憶體預算制度。簡單說,就是把整台 ECS 的記憶體當成有限資源,提前規劃每個模組能用多少,並且為峰值與異常預留空間。
首先要做的是資源隔離。資料庫、應用服務、監控代理、日誌收集、備份任務不要什麼都堆在同一台機器上。只要一台機器承擔的角色過多,記憶體風險就會彼此放大。其次要做容量設計,不只看平均流量,而要看尖峰、突發、重試、排隊、批次任務同時發生時的總需求。很多 OOM 都不是平時造成的,而是幾個看似獨立的事件剛好疊在一起。
再來是設告警門檻。不要等到 `free` 幾乎見底才報警,應該在可用記憶體持續下降、swap 開始增長、頁面回收加劇、Major Page Fault 增加時就先告警。告警的目的不是嚇人,而是讓團隊有時間在真正被殺之前做降級、限流、錯峰或擴容。
最後是壓測與演練。很多配置在測試環境看起來正常,一到真實流量就崩,原因通常是測試沒有覆蓋真實連線數、查詢結構、長尾請求和背景任務。你需要用接近生產的資料量與併發模型去驗證:當記憶體下降到某個臨界值時,系統是平穩退化,還是直接雪崩。
五、現場處置流程:先止血,再復盤
當 OOM 已經發生,處理順序很重要。第一步先確認是哪個進程被殺,並判斷是否需要立即重啟。若是核心資料庫或主服務被殺,先評估是否有副本、是否會引發更多故障,避免一口氣把所有實例一起拉起來,造成更嚴重的資源競爭。
第二步收集現場資料。至少保留當時的 `dmesg`、`journalctl`、應用日誌、MySQL 慢查詢片段、JVM GC 日誌、系統負載與進程 RSS 排行。很多團隊一重啟就把現場沖掉,後面只能靠猜。若是定時任務或批次作業導致的突發內存暴增,還要把任務時間、輸入大小和資料分佈記錄下來。
第三步做臨時止血。可以先減少並發、暫停非核心任務、限制大查詢、關閉不必要的批處理,必要時先將流量切走。這一步的目標不是完美修復,而是讓系統先恢復穩定,避免反覆被殺造成連鎖故障。
第四步才是復盤與修正。復盤時不要只寫一句「記憶體不足」,而要拆出具體原因:是 MySQL 參數過大、Java 堆配置不合理、某個排程洩漏、連線暴增、還是容量規劃失敗。只有把原因拆細,下一次才能真正避免重演。
六、最容易踩坑的幾個誤區
第一個誤區,是把 swap 當救命藥。swap 可以在短時間內緩解壓力,但如果指望它長期承擔核心服務的記憶體缺口,性能只會越拖越差。對 MySQL 和 Java 來說,進入高 swap 狀態通常意味著延遲暴增,甚至比直接被殺更難處理。
第二個誤區,是把 JVM 的 `-Xmx` 設得過滿。很多團隊會覺得堆越大越不容易 OOM,但如果本地記憶體沒有餘裕,最終還是會被 Linux 殺掉。第三個誤區,是認為 MySQL buffer pool 越大越好。對資料庫來說,快取確實重要,但它不是唯一目標;當記憶體緊張時,穩定性比快取命中率更重要。
第四個誤區,是只有故障發生後才看記憶體。真正成熟的治理,應該在還沒出事之前就能看到趨勢:是不是在每天固定時段緩慢上升,是否有週期性高峰,是否有版本升級後的基線變化,是否有某個新功能默默增加了記憶體壓力。只看事故,不看趨勢,永遠只能救火。
結語:把記憶體當成資產,不是當成緩存槽
阿里雲 ECS 上的 OOM Killed,表面看是某個 MySQL 或 Java 進程突然死亡,實際上反映的是一整套資源管理是否失控。要真正解決這類問題,不能只盯著單一參數,也不能指望一次重啟就風平浪靜。你需要先看清楚誰在吃記憶體,再根據業務形態拆分資源、設置緩衝、控制併發、建立監控,最後把應急流程和復盤機制固定下來。
當記憶體治理做到位,系統不只是少一次崩潰,而是整體穩定性、故障可控性、擴容效率都會一起提升。對線上服務來說,最值錢的不是把資源壓到極限,而是在高峰來臨時,仍然有足夠空間把服務撐住。

