运行时指标

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`上,没有明显的层次结构。在这种情况下,您不会看到子目录,而是会看到该目录中的一堆文件,以及可能对应于现有容器的一些目录。

要确定控制组的安装位置,您可以运行

$ grep cgroup /proc/mounts

枚举控制组

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`以查看进程属于哪些控制组。控制组显示为相对于层次结构挂载点的根的路径。`/`表示该进程尚未分配给组,而`/lxc/pumpkin`表示该进程是名为`pumpkin`的容器的成员。

cgroup v2

在cgroup v2主机上,`/proc/cgroups`的内容没有意义。请参阅`/sys/fs/cgroup/cgroup.controllers`以了解可用的控制器。

更改控制组版本

更改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还需要满足以下条件

  • 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`。
  • `docker run`标志`--oom-kill-disable`和`--kernel-memory`在v2上被丢弃。

查找给定容器的控制组

对于每个容器,都会在每个层次结构中创建一个cgroup。在使用旧版LXC用户空间工具的旧系统上,cgroup的名称是容器的名称。使用较新版本的LXC工具,cgroup为`lxc/.`。

对于使用cgroup的Docker容器,容器名称是容器的完整ID或长ID。如果容器在`docker ps`中显示为ae836c95b4c3,则其长ID可能是类似`ae836c95b4c3c9e9179e0e91015512da89fdec91612f63cebae57df9a5444c79`的内容。您可以使用`docker inspect`或`docker ps --no-trunc`查找它。

将所有内容放在一起以查看Docker容器的内存指标,请查看以下路径

  • 在 cgroup v1 上,cgroupfs 驱动程序使用路径/sys/fs/cgroup/memory/docker/<longid>/
  • 在 cgroup v1 上,systemd 驱动程序使用路径/sys/fs/cgroup/memory/system.slice/docker-<longid>.scope/
  • 在 cgroup v2 上,cgroupfs 驱动程序使用路径/sys/fs/cgroup/docker/<longid>/
  • 在 cgroup v2 上,systemd 驱动程序使用路径/sys/fs/cgroup/system.slice/docker-<longid>.scope/

来自控制组的指标:内存、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时间。区别在于:

  • user时间是进程直接控制 CPU 执行进程代码的时间量。
  • system时间是内核代表进程执行系统调用所花费的时间。

这些时间以 1/100 秒的滴答数表示,也称为“用户节拍”。每秒有USER_HZ“节拍”,在 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 循环,为每个容器 IP 地址(每个方向一个)在 FORWARD 链中添加两条 iptables 规则。这仅测量通过 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/<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> ....

查看 枚举 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 个容器)收集指标,则不希望每次都派生一个新进程。

以下是如何从单个进程收集指标的方法。您需要使用 C 语言(或任何允许您进行低级系统调用的语言)编写您的指标收集器。您需要使用一个特殊的系统调用 setns(),它允许当前进程进入任何任意的命名空间。但是,它需要一个指向命名空间伪文件的打开文件描述符(记住:这是 /proc/<pid>/ns/net 中的伪文件)。

但是,有一个问题:您不能保持此文件描述符打开。如果您这样做,当控制组的最后一个进程退出时,命名空间不会被销毁,其网络资源(如容器的虚拟接口)将永远保留(或直到您关闭该文件描述符)。

正确的方法是跟踪每个容器的第一个 PID,并每次重新打开命名空间伪文件。

容器退出时收集指标

有时,您不关心实时指标收集,而是在容器退出时,您想知道它使用了多少 CPU、内存等。

Docker 使此操作变得困难,因为它依赖于 lxc-start,后者会仔细清理自身。通常,定期收集指标更容易,这就是 collectd LXC 插件的工作方式。

但是,如果您仍然想在容器停止时收集统计信息,方法如下:

对于每个容器,启动一个收集进程,并将其移动到您要监视的控制组,方法是将其 PID 写入 cgroup 的 tasks 文件。收集进程应定期重新读取 tasks 文件以检查它是否是控制组的最后一个进程。(如果您还想收集上一节中解释的网络统计信息,您还应将进程移动到相应的网络命名空间。)

容器退出时,lxc-start 尝试删除控制组。它失败了,因为控制组仍在使用中;但这没关系。您的进程现在应该检测到它是组中唯一剩下的进程。现在是收集所有所需指标的最佳时机!

最后,您的进程应将自身移回根控制组,并删除容器控制组。要删除控制组,只需 rmdir 其目录即可。rmdir 一个仍然包含文件的目录似乎违反直觉;但请记住这是一个伪文件系统,因此通常的规则不适用。清理完成后,收集进程可以安全退出。