运行时指标
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
枚举 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/<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 还需要满足以下条件:
- 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。 - The
docker run标志--oom-kill-disable和--kernel-memory在 v2 上被丢弃。
查找给定容器的 cgroup
对于每个容器,在每个层级中都会创建一个 cgroup。在使用旧版本 LXC 用户态工具的旧系统上,cgroup 的名称就是容器的名称。在较新版本的 LXC 工具中,cgroup 的名称是 lxc/<container_name>.
对于使用 cgroups 的 Docker 容器,容器名称是容器的完整 ID 或长 ID。如果一个容器在 docker ps 中显示为 ae836c95b4c3,其长 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驱动
来自 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(
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 的最大量。
页面缓存中的内存计算非常复杂。如果不同控制组中的两个进程都读取同一个文件(最终依赖于磁盘上的相同块),相应的内存费用将在控制组之间分摊。这很好,但也意味着当一个控制组终止时,可能会增加另一个控制组的内存使用量,因为它们不再为那些内存页面分摊成本。
CPU 指标: cpuacct.stat
既然我们已经介绍了内存指标,相比之下,其他一切都变得简单了。CPU指标位于
cpuacct 控制器中。
对于每个容器,伪文件 cpuacct.stat 包含该容器进程累积的 CPU 使用情况,细分为 user 和
system 时间。区别在于:
user时间是指进程直接控制CPU并执行进程代码的时间量。systemtime 是内核代表进程执行系统调用的时间。
这些时间以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
循环,为每个容器 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/<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它似乎违反直觉;但请记住,这是一个伪文件系统,因此常规规则不适用。清理完成后,收集进程可以安全退出。