运行时指标

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下找到这个文件系统。在该目录下,你会看到多个子目录,称为devicesfreezerblkio等。每个子目录实际上对应一个不同的控制组层次结构。

在较旧的系统上,控制组可能挂载在/cgroup上,没有独立的层次结构。在这种情况下,您不会看到子目录,而是看到该目录中的一堆文件,以及可能对应现有容器的一些目录。

要找出您的控制组挂载的位置,您可以运行:

$ grep cgroup /proc/mounts

枚举cgroups

cgroups 的文件布局在 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版本

更改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容器,容器名称是容器的完整ID或长ID。如果容器在docker ps中显示为ae836c95b4c3,其长ID可能类似于ae836c95b4c3c9e9179e0e91015512da89fdec91612f63cebae57df9a5444c79。您可以使用docker inspectdocker 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 驱动

来自cgroups的指标:内存、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
表示控制组中进程映射的内存量。它不会告诉您使用了多少内存;而是告诉您如何使用内存。
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
缓存内存,具有与上述anon内存类似的活动非活动状态。确切的公式是cache = active_file + inactive_file + tmpfs。内核用于在活动和非活动集之间移动内存页面的确切规则与用于匿名内存的规则不同,但一般原则是相同的。当内核需要回收内存时,从这个池中回收干净(=未修改)的页面更便宜,因为它可以立即回收(而匿名页面和脏/修改页面需要先写入磁盘)。
unevictable
无法回收的内存量;通常,它指的是使用mlock“锁定”的内存。加密框架经常使用它来确保密钥和其他敏感材料永远不会被交换到磁盘。
memory_limit, memsw_limit
这些并不是真正的指标,而是对此cgroup应用的限制的提醒。第一个表示此控制组进程可以使用的最大物理内存量;第二个表示RAM+交换的最大量。

在页面缓存中计算内存是非常复杂的。如果两个不同控制组中的进程都读取同一个文件(最终依赖于磁盘上的相同块),相应的内存费用会在控制组之间分摊。这很好,但也意味着当一个控制组终止时,它可能会增加另一个控制组的内存使用量,因为它们不再分摊这些内存页面的成本。

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(或者更准确地说,netfilter框架,iptables只是它的一个接口)可以进行一些重要的统计。

例如,您可以设置一个规则来考虑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//ns/ 下具体化。例如,PID 42 的网络命名空间由伪文件 /proc/42/ns/net 具体化。

当你运行 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个容器),您不希望每次都分叉一个新进程。

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

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

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

在容器退出时收集指标

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

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

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

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

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

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