執行時指標
Docker 統計資訊
您可以使用 `docker stats` 命令即時流式傳輸容器的執行時指標。該命令支援 CPU、記憶體使用量、記憶體限制和網路 IO 指標。
以下是 `docker stats` 命令的示例輸出
$ docker stats redis1 redis2
CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
redis1 0.07% 796 KB / 64 MB 1.21% 788 B / 648 B 3.568 MB / 512 KB
redis2 0.07% 2.746 MB / 64 MB 4.29% 1.266 KB / 648 B 12.4 MB / 0 B
`docker stats` 參考頁面有關於 `docker stats` 命令的更多詳細資訊。
控制組
Linux 容器依賴於 控制組(control groups),它不僅跟蹤程序組,還公開有關 CPU、記憶體和塊 I/O 使用情況的指標。您可以訪問這些指標並獲取網路使用情況指標。這對於“純”LXC 容器和 Docker 容器都很有意義。
控制組透過一個偽檔案系統公開。在現代發行版中,您應該可以在 `/sys/fs/cgroup` 下找到這個檔案系統。在該目錄下,您會看到多個子目錄,名為 `devices`、`freezer`、`blkio` 等。每個子目錄實際上對應一個不同的 cgroup 層次結構。
在較舊的系統上,控制組可能掛載在 `/cgroup` 上,沒有明顯的層次結構。在這種情況下,您不會看到子目錄,而是在該目錄中看到一堆檔案,以及可能一些對應於現有容器的目錄。
要確定您的控制組掛載在哪裡,可以執行
$ grep cgroup /proc/mounts
列舉 cgroup
cgroup v1 和 v2 的檔案佈局有顯著不同。
如果您的系統上存在 `/sys/fs/cgroup/cgroup.controllers`,則您使用的是 v2,否則您使用的是 v1。請參閱與您的 cgroup 版本對應的部分。
cgroup v2 在以下發行版中預設使用:
- Fedora(從 31 開始)
- Debian GNU/Linux(從 11 開始)
- Ubuntu(從 21.10 開始)
cgroup v1
您可以檢視 `/proc/cgroups` 來了解系統已知的不同控制組子系統、它們所屬的層次結構以及它們包含的組數。
您還可以檢視 `/proc/
cgroup v2
在 cgroup v2 主機上,`/proc/cgroups` 的內容沒有意義。請參閱 `/sys/fs/cgroup/cgroup.controllers` 以瞭解可用的控制器。
更改 cgroup 版本
更改 cgroup 版本需要重啟整個系統。
在基於 systemd 的系統上,可以透過向核心命令列新增 `systemd.unified_cgroup_hierarchy=1` 來啟用 cgroup v2。要將 cgroup 版本恢復到 v1,您需要改為設定 `systemd.unified_cgroup_hierarchy=0`。
如果您的系統上有 `grubby` 命令(例如在 Fedora 上),可以如下修改命令列:
$ sudo grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=1"
如果 `grubby` 命令不可用,請編輯 `/etc/default/grub` 中的 `GRUB_CMDLINE_LINUX` 行,並執行 `sudo update-grub`。
在 cgroup v2 上執行 Docker
Docker 從 20.10 版本開始支援 cgroup v2。在 cgroup v2 上執行 Docker 還需要滿足以下條件:
- containerd: v1.4 或更高版本
- runc: v1.0.0-rc91 或更高版本
- 核心:v4.15 或更高版本(推薦 v5.2 或更高版本)
請注意,cgroup v2 模式的行為與 cgroup v1 模式略有不同:
- 預設的 cgroup 驅動程式 (`dockerd --exec-opt native.cgroupdriver`) 在 v2 上是 `systemd`,在 v1 上是 `cgroupfs`。
- 預設的 cgroup 名稱空間模式(`docker run --cgroupns`)在 v2 上是 `private`,在 v1 上是 `host`。
- 在 v2 上,`docker run` 的標誌 `--oom-kill-disable` 和 `--kernel-memory` 將被丟棄。
查詢給定容器的 cgroup
對於每個容器,在每個層次結構中都會建立一個 cgroup。在使用較舊版本的 LXC 使用者空間工具的舊系統上,cgroup 的名稱就是容器的名稱。對於較新版本的 LXC 工具,cgroup 是 `lxc/
對於使用 cgroups 的 Docker 容器,cgroup 名稱是容器的完整 ID 或長 ID。如果一個容器在 `docker ps` 中顯示為 ae836c95b4c3,其長 ID 可能類似於 `ae836c95b4c3c9e9179e0e91015512da89fdec91612f63cebae57df9a5444c79`。您可以使用 `docker inspect` 或 `docker ps --no-trunc` 來查詢它。
綜合起來,要檢視 Docker 容器的記憶體指標,請檢視以下路徑:
- `/sys/fs/cgroup/memory/docker/
/` 在 cgroup v1 上,使用 `cgroupfs` 驅動 - `/sys/fs/cgroup/memory/system.slice/docker-
.scope/` 在 cgroup v1 上,使用 `systemd` 驅動 - `/sys/fs/cgroup/docker/
/` 在 cgroup v2 上,使用 `cgroupfs` 驅動 - `/sys/fs/cgroup/system.slice/docker-
.scope/` 在 cgroup v2 上,使用 `systemd` 驅動
來自 cgroup 的指標:記憶體、CPU、塊 I/O
注意本節尚未針對 cgroup v2 進行更新。有關 cgroup v2 的更多資訊,請參閱核心文件。
對於每個子系統(記憶體、CPU 和塊 I/O),都存在一個或多個包含統計資訊的偽檔案。
記憶體指標:`memory.stat`
記憶體指標位於 `memory` cgroup 中。記憶體控制組會增加一點開銷,因為它對主機上的記憶體使用情況進行了非常精細的核算。因此,許多發行版選擇預設不啟用它。通常,要啟用它,您只需新增一些核心命令列引數:`cgroup_enable=memory swapaccount=1`。
指標位於偽檔案 `memory.stat` 中。它看起來像這樣:
cache 11492564992
rss 1930993664
mapped_file 306728960
pgpgin 406632648
pgpgout 403355412
swap 0
pgfault 728281223
pgmajfault 1724
inactive_anon 46608384
active_anon 1884520448
inactive_file 7003344896
active_file 4489052160
unevictable 32768
hierarchical_memory_limit 9223372036854775807
hierarchical_memsw_limit 9223372036854775807
total_cache 11492564992
total_rss 1930993664
total_mapped_file 306728960
total_pgpgin 406632648
total_pgpgout 403355412
total_swap 0
total_pgfault 728281223
total_pgmajfault 1724
total_inactive_anon 46608384
total_active_anon 1884520448
total_inactive_file 7003344896
total_active_file 4489052160
total_unevictable 32768
前半部分(沒有 `total_` 字首)包含與 cgroup 內程序相關的統計資訊,不包括子 cgroup。後半部分(帶有 `total_` 字首)則包括子 cgroup。
一些指標是“儀表值”,即可以增加或減少的值。例如,`swap` 是 cgroup 成員使用的交換空間量。其他一些是“計數器”,即只能增加的值,因為它們代表特定事件的發生次數。例如,`pgfault` 表示自 cgroup 建立以來的頁面錯誤次數。
cache
- 此控制組的程序使用的記憶體量,可以精確地與塊裝置上的一個塊相關聯。當您從磁碟上的檔案讀取和寫入時,這個數量會增加。如果您使用“傳統”I/O(`open`、`read`、`write` 系統呼叫)以及對映檔案(使用 `mmap`),情況就是如此。它還計入了 `tmpfs` 掛載所使用的記憶體,儘管原因尚不清楚。
rss
- 與磁碟上任何內容都不對應的記憶體量:堆疊、堆和匿名記憶體對映。
mapped_file
- 表示控制組中程序對映的記憶體量。它不提供有關使用了多少記憶體的資訊;而是告訴您記憶體是如何被使用的。
- `pgfault`, `pgmajfault`
- 分別表示 cgroup 中的一個程序觸發“頁面錯誤”和“主要錯誤”的次數。當一個程序訪問其虛擬記憶體空間中不存在或受保護的部分時,會發生頁面錯誤。前者可能發生在該程序有 bug 並嘗試訪問無效地址時(它會收到一個 `SIGSEGV` 訊號,通常會以著名的 `Segmentation fault` 訊息殺死它)。後者可能發生在程序從一個已被換出的記憶體區域或對應於一個對映檔案的記憶體區域讀取時:在這種情況下,核心會從磁碟載入頁面,並讓 CPU 完成記憶體訪問。當程序寫入一個寫時複製(copy-on-write)記憶體區域時也可能發生:同樣,核心會搶佔該程序,複製記憶體頁面,並在程序自己的頁面副本上恢復寫操作。“主要”錯誤發生在核心實際需要從磁碟讀取資料時。當它只是複製一個現有頁面,或分配一個空頁面時,這是一個常規(或“次要”)錯誤。
swap
- 此 cgroup 中程序當前使用的交換空間量。
- `active_anon`、`inactive_anon`
- 被核心分別識別為*活動*和*非活動*的匿名記憶體量。“匿名”記憶體是指*不*與磁碟頁面連結的記憶體。換句話說,這相當於上面描述的 rss 計數器。實際上,rss 計數器的確切定義是 `active_anon` + `inactive_anon` - `tmpfs`(其中 tmpfs 是此控制組掛載的 `tmpfs` 檔案系統使用的記憶體量)。現在,“活動”和“非活動”有什麼區別?頁面最初是“活動”的;核心會定期掃描記憶體,並將一些頁面標記為“非活動”。每當它們再次被訪問時,它們會立即被重新標記為“活動”。當核心幾乎耗盡記憶體,需要換出到磁碟時,核心會換出“非活動”頁面。
- `active_file`、`inactive_file`
- 快取記憶體,其中*活動*和*非活動*與上面的*匿名*記憶體類似。確切的公式是 `cache` = `active_file` + `inactive_file` + `tmpfs`。核心在活動集和非活動集之間移動記憶體頁面的確切規則與用於匿名記憶體的規則不同,但基本原理是相同的。當核心需要回收記憶體時,從這個池中回收一個乾淨的(=未修改的)頁面更便宜,因為它可以立即被回收(而匿名頁面和髒/修改過的頁面需要先寫入磁碟)。
unevictable
- 無法回收的記憶體量;通常,它計算的是用 `mlock` “鎖定”的記憶體。它經常被加密框架用來確保金鑰和其他敏感材料永遠不會被換出到磁碟。
- `memory_limit`, `memsw_limit`
- 這些並非真正的指標,而是對此 cgroup 應用的限制的提醒。第一個表示此控制組的程序可以使用的最大物理記憶體量;第二個表示 RAM+swap 的最大總量。
在頁面快取中核算記憶體非常複雜。如果不同控制組中的兩個程序都讀取同一個檔案(最終依賴於磁碟上的相同塊),相應的記憶體費用將在這些控制組之間分攤。這很好,但這也意味著當一個 cgroup 終止時,它可能會增加另一個 cgroup 的記憶體使用量,因為它們不再為那些記憶體頁面分攤成本了。
CPU 指標:`cpuacct.stat`
現在我們已經討論了記憶體指標,相比之下,其他一切都簡單多了。CPU 指標位於 `cpuacct` 控制器中。
對於每個容器,一個名為 `cpuacct.stat` 的偽檔案包含了容器內程序累積的 CPU 使用量,並細分為 `user` 和 `system` 時間。區別在於:
- `user` 時間是程序直接控制 CPU、執行程序程式碼的時間量。
- `system` 時間是核心代表程序執行系統呼叫的時間。
這些時間以 1/100 秒的滴答聲表示,也稱為“使用者 jiffies”。每秒有 `USER_HZ` 個*“jiffies”*,在 x86 系統上,`USER_HZ` 是 100。歷史上,這精確地對映到每秒排程器“滴答”的數量,但更高頻率的排程和無滴答核心已經使滴答數量變得無關緊要。
塊 I/O 指標
塊 I/O 在 `blkio` 控制器中進行核算。不同的指標分散在不同的檔案中。雖然您可以在核心文件的 blkio-controller 檔案中找到深入的細節,但這裡列出了最相關的一些:
blkio.sectors
- 包含 cgroup 成員程序按裝置讀取和寫入的 512 位元組扇區數。讀取和寫入合併在一個計數器中。
blkio.io_service_bytes
- 表示 cgroup 讀取和寫入的位元組數。它每個裝置有 4 個計數器,因為對於每個裝置,它區分同步 I/O 與非同步 I/O,以及讀取與寫入。
blkio.io_serviced
- 執行的 I/O 運算元,不考慮其大小。它每個裝置也有 4 個計數器。
blkio.io_queued
- 指示當前為此 cgroup 排隊的 I/O 運算元。換句話說,如果 cgroup 沒有進行任何 I/O,這個值是零。反之則不成立。也就是說,如果沒有 I/O 排隊,並不意味著 cgroup 處於空閒狀態(I/O 方面)。它可能正在一個本來靜止的裝置上進行純粹的同步讀取,因此可以立即處理這些讀取而無需排隊。此外,雖然這有助於確定哪個 cgroup 給 I/O 子系統帶來了壓力,但請記住,這是一個相對量。即使一個程序組沒有執行更多的 I/O,它的佇列大小也可能僅僅因為其他裝置的裝置負載增加而增加。
網路指標
網路指標並非由控制組直接公開。對此有一個很好的解釋:網路介面存在於*網路名稱空間*的上下文中。核心或許可以累積有關一組程序傳送和接收的資料包和位元組的指標,但這些指標不會非常有用。您需要的是每個介面的指標(因為在本地 `lo` 介面上發生的流量實際上不算數)。但是,由於單個 cgroup 中的程序可以屬於多個網路名稱空間,這些指標將更難解釋:多個網路名稱空間意味著多個 `lo` 介面,可能還有多個 `eth0` 介面等;這就是為什麼沒有簡單的方法透過控制組收集網路指標的原因。
相反,您可以從其他來源收集網路指標。
iptables
iptables(或者更確切地說,iptables 只是其介面的 netfilter 框架)可以進行一些嚴肅的核算。
例如,您可以設定一條規則來統計 Web 伺服器的出站 HTTP 流量:
$ iptables -I OUTPUT -p tcp --sport 80
沒有 `-j` 或 `-g` 標誌,所以這條規則只計算匹配的資料包,然後轉到下一條規則。
稍後,您可以使用以下命令檢查計數器的值:
$ iptables -nxvL OUTPUT
技術上講,`-n` 不是必需的,但它可以防止 iptables 進行 DNS 反向查詢,這在這種情況下可能沒有用。
計數器包括資料包和位元組。如果您想為容器流量設定這樣的指標,您可以執行一個 `for` 迴圈,為每個容器 IP 地址新增兩條 `iptables` 規則(每個方向一條),在 `FORWARD` 鏈中。這隻計量透過 NAT 層的流量;您還需要新增透過使用者空間代理的流量。
然後,您需要定期檢查這些計數器。如果您碰巧使用 `collectd`,有一個不錯的外掛可以自動收集 iptables 計數器。
介面級計數器
由於每個容器都有一個虛擬乙太網介面,您可能想直接檢查該介面的 TX 和 RX 計數器。每個容器都與主機中的一個虛擬乙太網介面相關聯,其名稱類似於 `vethKk8Zqi`。不幸的是,要確定哪個介面對應哪個容器是困難的。
但就目前而言,最好的方法是*從容器內部*檢查指標。為了實現這一點,您可以使用**ip-netns 魔法**在容器的網路名稱空間內執行來自主機環境的可執行檔案。
`ip-netns exec` 命令允許您在當前程序可見的任何網路名稱空間內執行任何程式(存在於主機系統中)。這意味著您的主機可以進入容器的網路名稱空間,但您的容器無法訪問主機或其他對等容器。不過,容器可以與其子容器互動。
該命令的確切格式是:
$ ip netns exec <nsname> <command...>
例如:
$ ip netns exec mycontainer netstat -i
`ip netns` 透過使用名稱空間偽檔案來找到 `mycontainer` 容器。每個程序都屬於一個網路名稱空間、一個 PID 名稱空間、一個 `mnt` 名稱空間等,這些名稱空間在 `/proc/
當您執行 `ip netns exec mycontainer ...` 時,它期望 `/var/run/netns/mycontainer` 是這些偽檔案之一。(接受符號連結。)
換句話說,要在一個容器的網路名稱空間內執行一個命令,我們需要:
- 找出我們想要調查的容器內任何程序的 PID;
- 建立一個從 `/var/run/netns/
` 到 `/proc/ /ns/net` 的符號連結 - 執行 `ip netns exec
....`
請回顧列舉 Cgroups部分,瞭解如何找到您想要測量網路使用情況的容器內程序的 cgroup。從那裡,您可以檢查名為 `tasks` 的偽檔案,其中包含 cgroup 中(因此也在容器中)的所有 PID。選擇其中任何一個 PID。
綜合來看,如果容器的“短 ID”儲存在環境變數 `$CID` 中,那麼您可以這樣做:
$ TASKS=/sys/fs/cgroup/devices/docker/$CID*/tasks
$ PID=$(head -n 1 $TASKS)
$ mkdir -p /var/run/netns
$ ln -sf /proc/$PID/ns/net /var/run/netns/$CID
$ ip netns exec $CID netstat -i
高效能指標收集技巧
每次想要更新指標時都執行一個新程序是(相對)昂貴的。如果您想以高解析度和/或在大量容器(比如單個主機上有 1000 個容器)上收集指標,您不希望每次都 fork 一個新程序。
以下是如何從單個程序收集指標的方法。您需要用 C 語言(或任何允許您進行低階系統呼叫的語言)編寫您的指標收集器。您需要使用一個特殊的系統呼叫 `setns()`,它允許當前程序進入任何任意的名稱空間。然而,它需要一個開啟的檔案描述符指向名稱空間偽檔案(記住:那是 `/proc/
然而,有一個陷阱:您不能一直保持這個檔案描述符開啟。如果您這樣做,當控制組的最後一個程序退出時,名稱空間不會被銷燬,其網路資源(如容器的虛擬介面)會一直存在(直到您關閉那個檔案描述符)。
正確的方法是跟蹤每個容器的第一個 PID,並在每次需要時重新開啟名稱空間偽檔案。
在容器退出時收集指標
有時,您不關心即時指標收集,但是當一個容器退出時,您想知道它使用了多少 CPU、記憶體等。
Docker 使這變得困難,因為它依賴於 `lxc-start`,它會仔細地清理自己。通常更容易以固定的時間間隔收集指標,這也是 `collectd` LXC 外掛的工作方式。
但是,如果您仍然想在容器停止時收集統計資訊,方法如下:
為每個容器啟動一個收集程序,並透過將其 PID 寫入 cgroup 的 tasks 檔案,將其移動到您想要監控的控制組中。收集程序應定期重新讀取 tasks 檔案,以檢查它是否是控制組中的最後一個程序。(如果您還想如前一節所述收集網路統計資訊,您還應該將該程序移動到相應的網路名稱空間。)
當容器退出時,`lxc-start` 會嘗試刪除控制組。這會失敗,因為控制組仍在使用中;但這沒關係。您的程序現在應該檢測到它是該組中唯一剩下的程序。現在是收集您需要的所有指標的正確時機!
最後,您的程序應該將自己移回根控制組,並移除容器控制組。要移除一個控制組,只需 `rmdir` 其目錄即可。在一個仍然包含檔案的目錄上 `rmdir` 是違反直覺的;但請記住這是一個偽檔案系統,所以通常的規則不適用。清理完成後,收集程序可以安全退出。