Ubuntu 26.04 LTS「Resolute Raccoon」在 2026 年 4 月 23 日釋出,隨附 Linux kernel 7.0 跟 systemd 259。這兩包東西合起來做了一件在 LTS 分支上少見的粗暴事:cgroup v1 的掛載路徑跟相容層被整個拔掉。這不是 deprecation warning,也不是 opt-out 的 boot 參數,而是任何一台還在 v1 上跑的機器連 do-release-upgrade 都會直接被擋下來——升級器在 pre-check 階段就退回錯誤,理由是「這台機器啟動時走的是 legacy hierarchy,升級後你的容器全會壞」。
臺灣不少 VPS 使用者手上的 Ubuntu 22.04 主機還沒動過 cgroup 設定,預設就是 hybrid 模式(v1 跟 v2 混掛)。這批機器直接跳 26.04 會踩到這道牆。就算硬把 systemd.unified_cgroup_hierarchy=1 加進 GRUB 撐過升級,容器實際跑起來還會遇到第二層問題:v2 對記憶體的計算方式跟 v1 差很多,同一份 limits.memory 在 v2 上很容易提早 OOMKill。這篇拆解為什麼這波遷移比多數人預期的痛,還有實務上該怎麼收拾。
拔得徹底:systemd 259 連 hybrid 模式都不留
cgroup v2 從 kernel 4.5 就進了主線,systemd 從 226 版起就支援 unified hierarchy。過去十年 Linux 社群談的都是「準備好切 v2」,但實際上多數發行版採取的是雙軌——同一台機器上 v1 跟 v2 兩套 hierarchy 同時掛載,讓還沒切 driver 的舊軟體繼續在 v1 上跑,新的容器 runtime 走 v2。這個 hybrid 模式的代價是核心裡兩套 cgroup 控制器要同時維護,效能損耗跟語意衝突一直都在。
systemd 259 做的事很直接:mount code 裡跟 v1 有關的分支整段刪掉,開機時只會嘗試掛 v2。連 cgroup_no_v1=all 這種 kernel command line 都變成沒意義的裝飾,因為 userspace 根本沒有東西會去掛 v1 給你用。發行版打包的時候可以選擇要不要 backport 這個變動,Debian 分支的維護者選擇跟上——所以 Ubuntu 26.04 就是這個決定的第一個 LTS 落地版本。
實際影響是什麼?任何直接讀 /sys/fs/cgroup/memory/ 這類 v1 路徑的工具會直接找不到目錄。舊版 Docker(20.10 以前)、cAdvisor 舊版本、部分 APM agent(New Relic、Datadog 都有踩過)、以及自己寫過的 shell script 抓 memory.usage_in_bytes 之類 v1 專屬檔案的監控腳本,全部要更新。Docker 官方從 20.10 就支援 v2,containerd 從 1.4、runc 從 1.0.0-rc93 之後也都跟上,這部分照理說沒事——但 VPS 上長期沒動過的 Docker Engine 版本有多老,只有機器主自己知道。
升級 pre-check 為什麼會擋你
do-release-upgrade 22.04 → 24.04 → 26.04 這條路上,26.04 的 upgrade meta 加了一個 v1 detection 步驟。判斷的邏輯是讀 /proc/1/cgroup:如果第一個欄位裡看到任何非 0:: 開頭的行,代表這台機器目前是 hybrid 或 v1-only 模式在跑。看到的話升級器就會退出,訊息大致是「你的系統目前使用 legacy cgroup hierarchy,升級後 systemd 259 將無法啟動你現有的容器工作負載,請先切到 unified hierarchy 並確認容器正常運作再重試」。
要通過這道 check,做法是編輯 /etc/default/grub,把 GRUB_CMDLINE_LINUX_DEFAULT 加上 systemd.unified_cgroup_hierarchy=1,跑 update-grub 之後重開機。重開之後 stat -fc %T /sys/fs/cgroup/ 應該回 cgroup2fs,cat /proc/1/cgroup 應該只剩一行 0::/...。這個時候再跑一次 do-release-upgrade 才會過。
問題是這一步的用意本來是保護,但實務上很多人跳過去只是把警告消掉、沒去驗證容器真的還跑得動。切到純 v2 環境之後,20.10 以前版本的 Docker Engine 因為沒有 v2 driver,實際跑容器時會在 cgroup 建立階段失敗——具體錯誤字串在不同版本、不同 storage driver 下不太一樣,比較可靠的做法是切完 v2、重開機之後跑一次 docker run --rm hello-world,如果連這個都起不來,就代表 Docker 版本已經跟不上新的 host。升級 Docker 到 24.x 或 25.x 是必要動作,這在升 OS 之前就得做完。
記憶體計算方式改變:v2 上的 OOMKill 會提早
真正吃虧的是即使 Docker 版本對、容器起得來,記憶體 limit 的行為在 v1 跟 v2 底下不一樣。這件事在遷移文件裡通常只用一句話帶過「memory accounting has changed」,但生產環境上會直接反映在 OOMKill 事件的頻率上。
差異在哪?v1 的 memory.usage_in_bytes 只算 anonymous memory 加上 page cache,kernel 使用的記憶體(socket buffer、fd 表、thread stack 等)預設不進去。v2 的 memory.current 把 kernel memory 也算進來——sock、kernel_stack、slab 這些欄位在 memory.stat 裡都會被納入 usage 統計。
實務上這代表什麼?一個開了大量 socket 的 gRPC service,或者用了幾百條 thread 的 JVM 應用,遷到 v2 之後 memory.current 會比在 v1 上多出幾十到幾百 MB。原本設 limits.memory: 1Gi 剛好夠用的 pod,在 v2 上會突然被 OOMKill。這不是應用程式有 bug,是同樣的容量在新的計算基準下算出來就是超過限制。
修正的方向不是把 limit 拉高一個定值。比較穩的做法是把預期會用到的 kernel memory 量測出來——cat /sys/fs/cgroup/<slice>/memory.stat | grep -E 'sock|kernel_stack|slab_reclaimable'——然後在 v2 上把 limit 上調到能容納這一段。如果是 Kubernetes 環境,Vertical Pod Autoscaler 在 recommendation 模式下跑一段時間可以自動吐出新的建議值,比拍腦袋加 20% 準得多。
另外一個容易踩雷的地方是 --memory-swap 跟 --oom-kill-disable。v2 底下 --oom-kill-disable 會被靜默忽略——container OOM 保護只能靠 memory.oom.group 這類 v2 專屬 knob。--memory-swap 在 v2 上的行為也變了,unlimited swap 需要主機先在 kernel 上啟用 swap accounting,否則設了等於沒設。這批 flag 相容性問題在小型 VPS 上特別容易被忽略,因為 swap 用量小到平常沒感覺。
Kubernetes 節點池的過渡策略
自架 Kubernetes 的 VPS 使用者這波要處理的事情更多。Kubernetes 從 1.25 起 cgroup v2 支援才算 GA,1.22 到 1.24 的 kubelet 只能勉強跑但 memory QoS 這類新功能拿不到。手上還在 1.24 以下的叢集要先把控制平面升到 1.28 以上再處理節點 OS。
節點池的做法建議 rolling 升級——不要整批機器一起切。先把一個 worker node cordon 起來、drain 掉工作負載、升級到 26.04、重開後把它拉回 schedulable 狀態,觀察一週。這段期間監控 kubelet 的 container_oom_events_total metric 有沒有暴衝、每個 namespace 的 memory usage 有沒有明顯拉高。有問題的 workload 挑出來調整 request/limit 之後再處理下一批。
比較實際的檢核清單:kubelet 的 --cgroup-driver flag 或 config 檔要設 systemd(不是 cgroupfs),跟 containerd 或 CRI-O 的 driver 一致。Prometheus node-exporter 用 1.6 以上版本、cAdvisor 直接用 kubelet 內建的(獨立部署 cAdvisor 的舊做法在 v2 上會漏欄位)。監控告警的規則裡如果有 hard-code 讀 v1 metric 路徑的(例如 container_memory_usage_bytes 在某些 exporter 版本會回不同數值),要重寫。
對 VPS 使用者的具體建議
如果手上是單台 VPS 跑 Docker Compose,準備 26.04 升級的順序建議倒過來走:先把 Docker Engine 更新到最新的 stable release,然後手動切 v2(改 GRUB 參數重開)、確認所有 container 正常啟動、觀察一週的記憶體用量、有問題的 service 調 mem_limit,等這一輪都收乾淨才動 OS 升級。這樣的節奏比較保險,因為一旦 OS 升上去發現 container 起不來,回滾成本很高(Ubuntu LTS 之間不支援官方 downgrade)。
如果手上是還在 20.04 或更早版本的 VPS,跳 26.04 需要中間停留在 22.04 或 24.04 至少完成一次 do-release-upgrade,Ubuntu 不支援跨兩個 LTS 直接升。這個時候比較實際的做法可能是開一台新的 26.04 VPS,把服務用 Docker Compose 或 Ansible 重新部署過去,資料用 restic 或 rsync 搬。這條路長期看比原地升級穩很多,畢竟 20.04 上累積下來的 dpkg 殘留跟手動改過的 config 檔在跨兩個 LTS 之後不見得都能乾淨遷移。
還有一件事值得先確認——雲端服務商或 VPS 商提供的 26.04 image 有沒有預先處理過 v2 切換。有些 provider 的 minimal image 開機參數還沒帶 systemd.unified_cgroup_hierarchy=1,就算裝的是 26.04 也可能因為某些原因跑在 hybrid 模式殘留設定上;新開機器記得先 stat -fc %T /sys/fs/cgroup/ 確認一次。
真的想穩定運行容器化服務、又不想每次 Ubuntu 大版本更新都要重新驗證整套 workload 的話,選對硬體與網路基礎的 VPS 才是根本。NCSE Network 位於臺灣是方電訊機房,採用 Intel Gold CPU 與 NVMe SSD,提供 10M 到 100G 的彈性頻寬選擇,對於需要跑容器化生產環境、又希望有本地技術支援的團隊會是穩定的選擇。想了解更多 VPS 方案的規格與網路架構,可以到 NCSE Network 看更多細節。