執行時期指標

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 容器依賴 控制群組

列舉 cgroup

cgroup 的檔案佈局在 v1 和 v2 之間存在顯著差異。

如果您的系統上存在 /sys/fs/cgroup/cgroup.controllers,則您使用的是 v2,否則您使用的是 v1。請參閱與您的 cgroup 版本對應的小節。

下列發行版本預設使用 cgroup v2

cgroup v1

您可以查看 /proc/cgroups 以查看系統已知的不同控制群組子系統、它們所屬的階層,以及它們包含的群組數量。

您也可以查看 /proc/<pid>/cgroup 以查看程序屬於哪些控制群組。控制群組顯示為相對於階層掛載點根目錄的路徑。/ 表示程序尚未分配到群組,而 /lxc/pumpkin 表示程序是名為 pumpkin 的容器的成員。

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 自 Docker 20.10 起支援 cgroup v2。在 cgroup v2 上執行 Docker 還需要滿足以下條件

請注意,cgroup v2 模式的行為與 cgroup v1 模式略有不同。

尋找指定容器的 cgroup

對於每個容器,在每個階層中都會建立一個 cgroup。在使用較舊版本 LXC 使用者空間工具的舊系統上,cgroup 的名稱是容器的名稱。使用較新版本的 LXC 工具時,cgroup 為 lxc/<容器名稱>

對於使用 cgroup 的 Docker 容器,容器名稱是容器的完整 ID 或長 ID。如果容器在 docker ps 中顯示為 ae836c95b4c3,則其長 ID 可能是 ae836c95b4c3c9e9179e0e91015512da89fdec91612f63cebae57df9a5444c79 之類的。您可以使用 docker inspectdocker ps --no-trunc 查詢它。

綜合以上所有資訊來查看 Docker 容器的記憶體指標,請查看以下路徑

來自 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 (openreadwrite 系統呼叫) 以及映射檔案 (使用 mmap),情況就是如此。它也說明了 tmpfs 掛載點使用的記憶體,儘管原因尚不清楚。
rss
不對應於磁碟上任何內容的記憶體量:堆疊、堆和匿名記憶體映射。
mapped_file(映射檔案)
表示控制群組中行程映射的記憶體量。它沒有提供有關使用了多少記憶體的資訊;它反而告訴您記憶體是如何使用的。
pgfaultpgmajfault
分別表示 cgroup 的行程觸發「頁面錯誤」和「主要錯誤」的次數。當行程存取其虛擬記憶體空間中不存在或受保護的部分時,就會發生頁面錯誤。如果行程有錯誤並嘗試存取無效位址,則可能會發生前者(它會收到 SIGSEGV 訊號,通常會使用著名的 Segmentation fault 訊息將其終止)。後者可能在行程從已交換出的記憶體區域讀取,或對應於映射檔案時發生:在這種情況下,核心會從磁碟載入頁面,並讓 CPU 完成記憶體存取。當行程寫入寫入時複製的記憶體區域時,也可能發生這種情況:同樣,核心會搶佔行程,複製記憶體頁面,並在行程自己的頁面副本上繼續寫入操作。「主要」錯誤發生在核心實際需要從磁碟讀取資料時。當它只是複製現有頁面或配置空頁面時,這是一個常規(或「次要」)錯誤。
swap(交換)
此 cgroup 中的行程目前使用的交換空間量。
active_anoninactive_anon
已被核心識別為分別處於 *作用中* 和 *非作用中* 狀態的匿名記憶體量。「匿名」記憶體是 *未* 連結到磁碟頁面的記憶體。換句話說,這相當於上述的 rss 計數器。事實上,rss 計數器的確切定義是 active_anon + inactive_anon - tmpfs(其中 tmpfs 是此控制群組掛載的 tmpfs 檔案系統所使用的記憶體量)。現在,「作用中」和「非作用中」之間有什麼區別?頁面最初是「作用中」的;核心會定期掃描記憶體,並將一些頁面標記為「非作用中」。每當再次存取它們時,它們會立即重新標記為「作用中」。當核心幾乎耗盡記憶體,並且需要交換到磁碟時,核心會交換「非作用中」的頁面。
active_fileinactive_file
快取記憶體,其 *作用中* 和 *非作用中* 類似於上述的 *anon* 記憶體。確切公式為 cache = active_file + inactive_file + tmpfs。核心用於在作用中和非作用中集合之間移動記憶體頁面的確切規則與用於匿名記憶體的規則不同,但一般原則是相同的。當核心需要回收記憶體時,從此池中回收乾淨(=未修改)的頁面成本較低,因為它可以立即回收(而匿名頁面和髒/修改的頁面需要先寫入磁碟)。
unevictable(不可逐出)
無法回收的記憶體量;通常,它說明了已使用 mlock「鎖定」的記憶體。密碼框架經常使用它來確保金鑰和其他敏感資料永遠不會交換到磁碟。
memory_limitmemsw_limit
這些並非真正的指標,而是用來提醒此 cgroup 所套用的限制。第一個表示此控制群組的處理程序可使用的最大實體記憶體量;第二個表示 RAM+swap 的最大量。

計算頁面快取中的記憶體非常複雜。如果不同控制群組中的兩個處理程序都讀取相同的檔案(最終依賴磁碟上的相同區塊),則相應的記憶體費用會在控制群組之間分攤。這很好,但也表示當一個 cgroup 終止時,它可能會增加另一個 cgroup 的記憶體使用量,因為它們不再分攤這些記憶體頁面的成本。

CPU 指標:cpuacct.stat

既然我們已經涵蓋了記憶體指標,相比之下,其他一切都簡單了。CPU 指標位於 cpuacct 控制器中。

對於每個容器,一個名為 cpuacct.stat 的偽檔案包含容器處理程序累積的 CPU 使用量,細分為 usersystem 時間。區別是

這些時間以 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,以及讀取與寫入。
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 迴圈,在 FORWARD 鏈中為每個容器 IP 位址新增兩個 iptables 規則(每個方向一個)。這只會測量通過 NAT 層的流量;您還需要新增通過使用者空間代理的流量。

然後,您需要定期檢查這些計數器。如果您碰巧使用 collectd,則有一個很棒的插件介面級計數器

由於每個容器都有一個虛擬乙太網路介面,您可能想要直接檢查此介面的 TX 和 RX 計數器。每個容器都與主機中的一個虛擬乙太網路介面關聯,名稱類似 vethKk8Zqi。遺憾的是,要找出哪個介面對應於哪個容器是很困難的。

但目前,最好的方法是從容器內部檢查指標。要做到這一點,您可以使用 **ip-netns 魔法** 在容器的網路命名空間內從主機環境運行一個可執行檔。

ip-netns exec 命令允許您在目前行程可見的任何網路命名空間內執行任何程式(存在於主機系統中)。這表示您的主機可以進入容器的網路命名空間,但您的容器無法訪問主機或其他對等容器。不過,容器可以與其子容器互動。

該命令的確切格式為

例如

ip netns 通過使用命名空間虛擬檔案來找到 mycontainer 容器。每個行程都屬於一個網路命名空間、一個 PID 命名空間、一個 mnt 命名空間等等,而這些命名空間會具體化在 /proc/<pid>/ns/ 下。例如,PID 42 的網路命名空間由虛擬檔案 /proc/42/ns/net 具體化。

當您運行 ip netns exec mycontainer ... 時,它預期 /var/run/netns/mycontainer 是這些虛擬檔案之一。(接受符號連結。)

換句話說,要在容器的網路命名空間內執行命令,我們需要

查看 列舉 Cgroup 了解如何找到您想要測量其網路使用情況的容器內行程的 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>/ns/net 中的虛擬檔案)。

但是,有一個陷阱:您不能讓這個檔案描述符保持打開狀態。如果您這樣做,當控制組的最後一個行程退出時,命名空間不會被銷毀,並且其網路資源(例如容器的虛擬介面)將永遠存在(或者直到您關閉該檔案描述符)。

正確的方法是追蹤每個容器的第一個 PID,並在每次都重新打開命名空間虛擬檔案。

容器結束時收集指標

有時,您不關心即時指標收集,但是當容器退出時,您想知道它使用了多少 CPU、記憶體等。

Docker 使這變得困難,因為它依賴於 lxc-start,它會在自身之後仔細清理。通常更容易定期收集指標,這就是 collectd LXC 插件的工作方式。

但是,如果您仍然想在容器停止時收集統計資訊,方法如下

對於每個容器,啟動一個收集行程,並通過將其 PID 寫入 cgroup 的 tasks 檔案將其移動到您想要監控的控制組。收集行程應定期重新讀取 tasks 檔案,以檢查它是否是控制組的最後一個行程。(如果您還想按照上一節中的說明收集網路統計資訊,您還應該將行程移動到適當的網路命名空間。)

當容器退出時,lxc-start 會嘗試刪除控制組。它會失敗,因為控制組仍在使用中;但這沒關係。您的行程現在應該檢測到它是組中唯一剩下的行程。現在是收集您所需的所有指標的正確時機!

最後,您的行程應該將自身移回根控制組,並移除容器控制組。要移除控制組,只需 rmdir 其目錄即可。在目錄仍然包含檔案時 rmdir 它似乎違反直覺;但請記住,這是一個虛擬檔案系統,因此通常的規則不適用。清理完成後,收集行程可以安全退出。