运行时指标
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 容器依赖于 控制组 ,控制组不仅跟踪进程组,还公开有关 CPU、内存和块 I/O 使用情况的指标。您可以访问这些指标并获取网络使用情况指标。这与“纯”LXC 容器以及 Docker 容器相关。
控制组通过伪文件系统公开。在现代发行版中,您应该在/sys/fs/cgroup
.在该目录下,您可以看到多个子目录,称为devices
、freezer
、blkio
等。每个子目录实际上对应着不同的cgroup层次结构。
在较旧的系统上,控制组可能安装在 上/cgroup
,没有明显的层次结构。在这种情况下,您不会看到子目录,而是会看到该目录中的一堆文件,可能还看到一些与现有容器对应的目录。
要确定控制组的安装位置,您可以运行:
$ grep cgroup /proc/mounts
枚举 cgroup
v1 和 v2 之间的 cgroup 文件布局存在显着差异。
如果/sys/fs/cgroup/cgroup.controllers
您的系统上存在 ,则您使用的是 v2,否则您使用的是 v1。请参阅与您的 cgroup 版本对应的小节。
以下发行版默认使用 cgroup v2:
- 软呢帽(自 31 以来)
- Debian GNU/Linux(自 11 起)
- Ubuntu(自 21.10 起)
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
命令不可用,请编辑GRUB_CMDLINE_LINUX
in 中的行/etc/default/grub
并运行sudo update-grub
。
在 cgroup v2 上运行 Docker
Docker 从 Docker 20.10 开始支持 cgroup v2。在 cgroup v2 上运行 Docker 还需要满足以下条件:
- 容器:v1.4 或更高版本
- runc:v1.0.0-rc91 或更高版本
- 内核:v4.15或更高版本(推荐v5.2或更高版本)
请注意,cgroup v2 模式的行为与 cgroup v1 模式略有不同:
- 默认 cgroup 驱动程序 (
dockerd --exec-opt native.cgroupdriver
) 位于systemd
v2 和cgroupfs
v1 上。 - 默认 cgroup 命名空间模式 (
docker run --cgroupns
)private
在 v2 上,host
在 v1 上。 - 标志
docker run
和在 v2 上被丢弃--oom-kill-disable
。--kernel-memory
查找给定容器的 cgroup
对于每个容器,每个层次结构中都会创建一个 cgroup。在具有旧版本 LXC 用户空间工具的旧系统上,cgroup 的名称就是容器的名称。对于最新版本的 LXC 工具,cgroup 是lxc/<container_name>.
对于使用 cgroup 的 Docker 容器,容器名称是容器的完整 ID 或长 ID。如果容器在 中显示为 ae836c95b4c3 docker ps
,则其长 ID 可能类似于
ae836c95b4c3c9e9179e0e91015512da89fdec91612f63cebae57df9a5444c79
。您可以使用docker inspect
或来查找docker ps --no-trunc
。
将所有内容放在一起查看 Docker 容器的内存指标,请查看以下路径:
/sys/fs/cgroup/memory/docker/<longid>/
在 cgroup v1 上,cgroupfs
驱动程序/sys/fs/cgroup/memory/system.slice/docker-<longid>.scope/
在 cgroup v1 上,systemd
驱动程序/sys/fs/cgroup/docker/<longid>/
在 cgroup v2 上,cgroupfs
驱动程序/sys/fs/cgroup/system.slice/docker-<longid>.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的进程触发“页面错误”和“严重错误”的次数。当进程访问不存在或受保护的虚拟内存空间的一部分时,就会发生页面错误。如果进程有错误并尝试访问无效地址(会发送一个
SIGSEGV
信号,通常会用著名的Segmentation fault
消息杀死它),则可能会发生前者。当进程从已换出的内存区域或对应于映射文件的内存区域读取数据时,可能会发生后者:在这种情况下,内核从磁盘加载页面,并让 CPU 完成内存访问。当进程写入写时复制内存区域时也可能发生这种情况:同样,内核抢占进程,复制内存页面,并在进程自己的页面副本上恢复写操作。当内核实际需要从磁盘读取数据时,就会发生“重大”错误。当它只是复制现有页面或分配空页面时,这是常规(或“次要”)错误。 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
time 是进程直接控制 CPU、执行进程代码的时间量。system
time 是内核代表进程执行系统调用的时间。
这些时间以 1/100 秒的刻度表示,也称为“用户 jiffies”。每秒
有USER_HZ
“jiffies”USER_HZ
,在 x86 系统上为 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
or-g
标志,因此该规则仅对匹配的数据包进行计数并转到以下规则。
稍后,您可以使用以下命令检查计数器的值:
$ iptables -nxvL OUTPUT
从技术上讲,-n
这不是必需的,但它会阻止 iptables 进行 DNS 反向查找,这在这种情况下可能毫无用处。
计数器包括数据包和字节。如果您想为这样的容器流量设置指标,您可以执行一个for
循环,在链中为每个容器 IP 地址添加两条iptables
规则(每个方向各一条)FORWARD
。这仅计量通过 NAT 层的流量;您还需要添加通过用户层代理的流量。
然后,您需要定期检查这些计数器。如果您碰巧使用collectd
,有一个
很好的插件
可以自动收集 iptables 计数器。
接口级计数器
由于每个容器都有一个虚拟以太网接口,您可能需要直接检查该接口的 TX 和 RX 计数器。每个容器都与主机中的虚拟以太网接口相关联,其名称类似于vethKk8Zqi
.不幸的是,弄清楚哪个接口对应于哪个容器是很困难的。
但目前,最好的方法是检查容器内的指标。为此,您可以使用ip-netns magic从容器的网络命名空间内的主机环境运行可执行文件。
该ip-netns exec
命令允许您执行当前进程可见的任何网络命名空间内的任何程序(存在于主机系统中)。这意味着您的主机可以进入容器的网络命名空间,但您的容器无法访问主机或其他对等容器。不过,容器可以与其子容器交互。
该命令的具体格式为:
$ ip netns exec <nsname> <command...>
例如:
$ ip netns exec mycontainer netstat -i
ip netns
mycontainer
使用命名空间伪文件查找容器。每个进程都属于一个网络命名空间、一个PID命名空间、一个mnt
命名空间等,并且这些命名空间在
/proc/<pid>/ns/
.例如,PID 42 的网络命名空间由伪文件具体化
/proc/42/ns/net
。
当您运行 时ip netns exec mycontainer ...
,它期望/var/run/netns/mycontainer
成为这些伪文件之一。 (接受符号链接。)
换句话说,要在容器的网络命名空间内执行命令,我们需要:
- 找出我们想要调查的容器内任何进程的PID;
/var/run/netns/<somename>
创建从到 的符号链接/proc/<thepid>/ns/net
- 执行
ip netns exec <somename> ....
查看
枚举 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 个容器)收集指标,您不希望每次都派生一个新进程。
以下是如何从单个进程收集指标。您需要用 C(或任何允许您执行低级系统调用的语言)编写指标收集器。您需要使用特殊的系统调用,
setns()
它可以让当前进程进入任意名称空间。但是,它需要命名空间伪文件的打开文件描述符(记住:这是 中的伪文件
/proc/<pid>/ns/net
)。
但是,有一个问题:您不能保持此文件描述符打开。如果这样做,当控制组的最后一个进程退出时,命名空间不会被破坏,并且其网络资源(如容器的虚拟接口)将永远保留(或直到您关闭该文件描述符)。
正确的方法是跟踪每个容器的第一个 PID,并每次重新打开名称空间伪文件。
容器退出时收集指标
有时,您不关心实时指标收集,但当容器退出时,您想知道它使用了多少CPU、内存等。
Docker 让这变得很困难,因为它依赖于lxc-start
,而后者会在完成后仔细清理。定期收集指标通常更容易,这就是collectd
LXC 插件的工作方式。
但是,如果您仍然想在容器停止时收集统计信息,请按以下方法操作:
对于每个容器,启动一个收集进程,并将其 PID 写入 cgroup 的任务文件,将其移动到要监视的控制组。收集进程应定期重新读取任务文件以检查它是否是控制组的最后一个进程。 (如果您还想按照上一节中的说明收集网络统计信息,则还应该将进程移动到适当的网络命名空间。)
当容器退出时,lxc-start
尝试删除控制组。它失败了,因为对照组仍在使用;但没关系。您的进程现在应该检测到它是该组中唯一剩余的进程。现在是收集您需要的所有指标的最佳时机!
最后,您的进程应将自身移回根控制组,并删除容器控制组。要删除控制组,只需删除
rmdir
其目录即可。它与rmdir
目录相反,
因为它仍然包含文件;但请记住,这是一个伪文件系统,因此通常的规则不适用。清理完成后,收集进程就可以安全退出。