ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

cAdvisor启动失败排查:inotify实例耗尽导致too many open files

cAdvisor启动失败排查:inotify实例耗尽导致too many open files 最近在维护容器监控平台的时候碰到一台节点上的 cAdvisor 一直启动失败日志里反复出现inotify_init: too many open files。一开始我以为只是文件描述符不够简单调大 ulimit 就完事结果发现事情没那么简单。这个报错在容器监控、Prometheus 采集节点、甚至一些日志采集 Agent 里都很常见但很多人一看到too many open files就去改 nofile忽略了 inotify 相关参数最后折腾半天还是没解决。这篇就把我这次的排查过程和最终处理方案完整写出来遇到同样问题的同行可以直接照做。1. 先搞清楚报错到底在说什么1.1 inotify 和 too many open files 的字面含义cAdvisor 是 Google 开源的容器资源监控工具用来采集容器和宿主机的 CPU、内存、网络、磁盘等指标很多 Kubernetes 监控方案里都会把它作为节点级监控的底层依赖。它启动时会遍历宿主机上的 cgroup 目录同时用 inotify 去监听 cgroup 文件系统的变化以便及时感知容器的创建和删除。这里的关键是 inotify。它是 Linux 内核提供的一种文件系统事件通知机制应用程序可以通过它监控指定目录或文件的打开、创建、删除、写入等动作。cAdvisor 依赖 inotify 监听 cgroup 路径所以启动阶段会调用inotify_init来创建新的 inotify 实例。too many open files对应的系统错误码是 EMFILE。单看这行日志很多人会立刻以为是进程打开的文件描述符太多突破了nofile限制。但实际上inotify_init失败还有一种更隐蔽的可能系统里 inotify 实例总数已经超过了内核参数fs.inotify.max_user_instances的上限。当这个参数被占满时即使进程自己的 fd 配额完全够用内核也会拒绝创建新的 inotify 实例并且抛出和普通too many open files一模一样的错误。所以这个报错至少有两个嫌疑方向一个是进程级 fd 配额不够一个是系统级 inotify 实例数超限。不区分清楚改配置很容易改偏。1.2 cAdvisor 为什么会触发这个错误我们在生产节点上跑了很多容器每个容器都有对应的 cgroup 目录cAdvisor 启动时会扫描/sys/fs/cgroup下的所有子目录同时会针对每个 watch 目录注册 inotify 监听。默认情况下它会尝试监听/sys/fs/cgroup以及所有容器的 cgroup 路径这个数量在节点容器较多时会非常大。另外如果宿主机上还同时跑着 Prometheus Node Exporter、日志采集器、你常用的文件同步工具或者其他基于 inotify 的进程系统的max_user_instances会被快速消耗。我这次出问题的那台机器上光日志采集 Agent 就占了几百个 inotify 实例Kubernetes 里的各种组件也会注册一些再叠加其他历史遗留进程inotify 实例数早就逼近默认上限了。cAdvisor 一旦无法创建新的 inotify 实例就会直接退出反映到 systemd 或容器编排平台里就是 CrashLoopBackOff。如果你是在容器里跑的 cAdvisor还会受到容器运行时、容器编排平台对 ulimit 的默认限制影响进一步加剧这个问题。1.3 这个错误和普通 fd 耗尽有什么区别进程普通 fd 耗尽通常表现为分配 socket、打开文件时报错日志里可能是Cannot assign requested address、Too many open files但位置不一定是inotify_init。你可以通过lsof -p PID | wc -l或ls /proc/PID/fd | wc -l来确认进程当前 fd 数量。如果是 inotify 实例超限即使进程 fd 数量很少只要创建 inotify 实例也会报 EMFILE。这时候用lsof统计 fd数字可能只有几百远低于限制但系统里所有进程创建的 inotify 实例总数已经达到fs.inotify.max_user_instances的限制。我见过不少人被这个细节坑了ulimit -n已经调到 65535cAdvisor 还是启动失败。因为问题根本不在 nofile而在 sysctl 的内核参数。所以定位时必须两条腿走路不能只看其中一个。2. 五分钟定位到底是谁把上限占满了2.1 先看进程自己的文件描述符用量遇到这种报错第一步先确认 cAdvisor 是不是自己把 fd 用光了。确认方式很简单假设 cAdvisor 的 PID 是 12345ls /proc/12345/fd | wc -l或者用 lsof 统计lsof -p 12345 | wc -l然后再看一下进程的实际限制cat /proc/12345/limits | grep -i open files如果你是在 systemd 服务里运行的 cAdvisor还要看对应的 service 配置systemctl show cadvisor.service | grep -i LimitNOFILE如果 fd 数量接近限制值那确实需要调 nofile。如果差得远就要继续往 inotify 方向查。2.2 再看系统 inotify 实例和 watch 数量Linux 系统里和 inotify 相关的内核参数主要有两个fs.inotify.max_user_instances每个真实用户 ID 可以创建的 inotify 实例数量上限。fs.inotify.max_user_watches每个用户 ID 可以注册的 inotify watch 项总数上限。查询当前使用量和上限的常见方式如下。查当前 inotify 实例总数可以通过 proc 接口统计find /proc/*/fd -lname anon_inode:inotify 2/dev/null | wc -l这条命令会去遍历所有进程的 fd 软链接找出指向 inotify 的文件描述符数量。如果嫌 find 慢可以换一种写法for pid in /proc/[0-9]*; do ls -l $pid/fd 2/dev/null; done | grep -c anon_inode:inotify注意max_user_instances的限制是按用户维度统计的不是按进程维度。所以更严谨的排查方式是统计每个用户下 inotify fd 的归属不过日常排障先看总数也能判断个大概。watch 总数可以通过下面这个文件查看cat /proc/sys/fs/inotify/max_user_watches比较麻烦的是系统没有直接提供当前 inotify watch 总数只能通过inotifywait这类工具大概估算或者依赖监控平台。如果你们有 node_exporter并且开了 inotify metrics可以直接在 Grafana 里看比较省事。2.3 顺手排查是不是有程序泄漏了 inotify 对象还有一种情况需要特别警惕某个进程不断创建 inotify 实例但忘记关闭导致系统实例数被慢慢占满。你可以逐进程统计 inotify fd 数量定位到大户for pid in /proc/[0-9]*; do n$(ls -l $pid/fd 2/dev/null | grep -c anon_inode:inotify) if [ $n -gt 0 ]; then echo $pid $n $(cat $pid/comm 2/dev/null) fi done | sort -k2 -n -r | head -20这条脚本会把所有打开 inotify fd 的进程按数量排序前面几位就是嫌疑对象。我那次排查发现除了 cAdvisor 本身还有好几个跑了好几百天的旧进程各自占着几十上百个 inotify 实例没释放。再加上系统用户跑的监控采集器总数一下子就上来了。如果你发现某个进程的 inotify fd 数一直在增长基本可以判定存在句柄泄漏这种情况单纯调大max_user_instances只是拖延问题必须从业务侧修掉泄漏点。3. 针对 cAdvisor 启动失败的处理方案3.1 方案一提高 cAdvisor 进程的 nofile 上限先处理最简单的可能。cAdvisor 本身会打开大量文件、socket、inotify fd所以把 nofile 调到 65535 甚至 1048576 是生产环境的常规做法。如果 cAdvisor 是通过 systemd 管理的在 service 文件里追加[Service] LimitNOFILE65535 LimitNPROC65535然后重载并重启systemctl daemon-reload systemctl restart cadvisor如果 cAdvisor 是通过 docker run 方式启动的要加上--ulimit参数因为容器默认的 nofile 一般是 1024 或者由 docker daemon 默认配置决定docker run -d \ --name cadvisor \ --ulimit nofile65535:65535 \ --volume /:/rootfs:ro \ --volume /var/run:/var/run:ro \ --volume /sys:/sys:ro \ --volume /var/lib/docker/:/var/lib/docker:ro \ --volume /dev/disk/:/dev/disk:ro \ --privileged \ --device /dev/kmsg \ gcr.io/cadvisor/cadvisor:latest如果你用 Kubernetes DaemonSet 部署需要在容器定义里加 resources 和 securityContext但 ulimit 这种比较难直接在 Pod spec 里配置。一般做法是给节点的 containerd/docker 配置默认 ulimit或者直接用特权模式加 initContainer 设置宿主机 sysctl。这点后面细说。3.2 方案二调整内核 inotify 参数如果确认 nofile 没问题接下来就把fs.inotify.max_user_instances调大。先看当前值sysctl fs.inotify.max_user_instances fs.inotify.max_user_watches我这次机器上max_user_instances默认只有 128但系统里所有进程累计需要的 inotify 实例已经超过 300cAdvisor 启动必然失败。临时调大可以这样sysctl -w fs.inotify.max_user_instances8192 sysctl -w fs.inotify.max_user_watches524288想持久化写到/etc/sysctl.d/90-inotify.conffs.inotify.max_user_instances8192 fs.inotify.max_user_watches524288然后执行sysctl --systemmax_user_watches为什么也要调大因为 inotify 实例数量上去之后cAdvisor 还会为每个监听的目录注册 watch。如果 watch 总数也超了日志里同样可能出现inotify_add_watch: too many open files或者No space left on device。这两个参数建议一起调整。数值给多少合适如果单个节点容器数量在 100 到 300 之间8192的实例数和524288的 watch 数通常够用。如果节点是大型 Docker 主机容器上千那就需要更高。经验上先设置实例数 8192watch 数 524288然后观察监控曲线再微调。3.3 方案三优化 cAdvisor 启动参数降低监控压力调大系统上限是治标减少 cAdvisor 对 inotify 的依赖才是治本方向之一。cAdvisor 的很多启动参数都能降低它对 cgroup 路径的 watch 数量。最直接的两个参数--docker_onlytrue只监控 Docker 容器忽略机器上其他 cgroup 路径。如果节点上不需要关注系统 cgroup 和自定义容器运行时建议开启。--housekeeping_interval30s调整 cAdvisor 内部指标采集周期默认好像是 1s。周期越短对文件系统事件和文件 fd 的占用越频繁。生产环境一般不需要秒级采集30s 甚至 1m 都是常态。还有其他参数比如--store_container_labelsfalse可以减少 label 处理开销但对 inotify 帮助不大。关键是别把不必要的路径全纳入监控范围。完整的启动命令示例docker run -d \ --name cadvisor \ --ulimit nofile65535:65535 \ --volume /:/rootfs:ro \ --volume /var/run:/var/run:ro \ --volume /sys:/sys:ro \ --volume /var/lib/docker/:/var/lib/docker:ro \ --volume /dev/disk/:/dev/disk:ro \ --privileged \ --device /dev/kmsg \ gcr.io/cadvisor/cadvisor:latest \ --docker_onlytrue \ --housekeeping_interval30s \ --disable_metricsdisk,tcp,udp,process--disable_metrics可以按需关掉一些不用的指标也能减少不必要的系统调用和路径访问。但注意metrics 列表在不同版本里可能不一样最好先看下当前版本支持哪些再决定。3.4 配置完成后如何验证配置都改好后别急着认为万事大吉。先用简单的启动方式验证一下/usr/bin/cadvisor --docker_onlytrue --housekeeping_interval30s前台启动能看到完整日志确认不再有inotify_init报错。然后再用 systemd 或 docker 方式正式拉起。启动成功后检查进程状态和端口ss -lntp | grep 8080 curl -s http://localhost:8080/metrics | head -n 20cAdvisor 默认端口是 8080看到 Prometheus metrics 正常输出就基本没问题。同时可以再确认一下当前 inotify 实例的占用情况对比调参前后变化find /proc/*/fd -lname anon_inode:inotify 2/dev/null | wc -l如果实例数和 watch 数还在持续快速上涨说明有进程泄漏或者 cAdvisor 监听的目录数还在异常膨胀需要继续查。4. 放生产环境前必须做的长期治理4.1 把配置固化到启动文件而不是临时生效临时sysctl -w或ulimit在重启后会失效所以要把配置固化下来。sysctl 写入/etc/sysctl.d/目录下命名要清晰比如90-inotify.conf避免和其他配置文件冲突。systemd 服务方面如果你用包管理或者自己的 systemd unit确保LimitNOFILE和LimitNPROC写在[Service]段。如果 cAdvisor 是通过 Docker 运行的注意 Docker daemon 的默认 ulimit 配置。可以在/etc/docker/daemon.json里加{ default-ulimits: { nofile: { Name: nofile, Hard: 65535, Soft: 65535 } } }这样所有容器默认就有较高的 nofile不用每个容器都单独指定。不过这个配置会影响所有容器改之前要和业务方确认避免某些老应用对 fd 数有特殊依赖。如果用的是 Kubernetes DaemonSet可以在 Pod 的securityContext里配置sysctl但 inotify 参数属于非命名空间级 sysctl需要 kubelet 开启允许。4.2 给监控告警留好空间不要等 inotify 实例耗尽再去处理而是要提前告警。Node Exporter 新版本会暴露 inotify 相关的指标比如node_inotify_max_instances和某个指标表示当前 inotify 实例数。建议在 Prometheus 里加一条规则- alert: InotifyInstancesNearLimit expr: node_inotify_instances / node_inotify_max_instances 0.8 for: 10m labels: severity: warning annotations: summary: inotify instances near limit on {{ $labels.instance }}同样可以给 watch 数加类似告警。告警阈值设在 80% 比较合理这样还有时间排查泄漏或规划扩容而不是等到节点级的监控组件全部 Crash 才被叫醒。4.3 业务容器也要规范 inotify 使用cAdvisor 只是 inotify 的消费者之一如果整个宿主机上到处都在创建 inotify 实例光调 cAdvisor 的配置解决不了根本问题。Linux 下很多常见工具都会使用 inotify比如rsync、inotifywait、一些日志同步组件、配置热加载框架。生产环境里尤其要注意那些配置了目录实时监控且长时间运行的程序这类程序如果写得不严谨很容易泄漏 inotify fd。治理时可以做两件事建立 inotify 实例数基线和告警机制让泄漏在早期暴露。在容器镜像层面规范 uid 和进程管理避免一个用户下所有进程共享 inotify 实例配额。如果节点上跑着很多第三方 Agent可以在不通知业务方之前先按 uid 统计一下占用看是不是有某个用户占用了绝大多数配额。方式就是前面那段脚本按 pid 和 user 汇总。5. 排障实录与避坑清单5.1 排查过一次真实故障过程供参考还原一下我这次的故障现场。当时监控页面发现某台节点的 cAdvisor 出现 CrashLoopBackOff日志只有一行E0503 10:22:31.123456 12345 cadvisor.go:123] inotify_init: too many open files我第一反应是把 cAdvisor 的 nofile 调大改完重启依然报错。然后我看了下/proc/pid/limits发现 nofile 已经 1048576但 fd 实际占用才 2000 多明显不是 fd 耗尽。接着我统计系统 inotify 实例数find /proc/*/fd -lname anon_inode:inotify 2/dev/null | wc -l结果 300但fs.inotify.max_user_instances只有 128根因找到了。再用脚本逐进程看for pid in /proc/[0-9]*; do n$(ls -l $pid/fd 2/dev/null | grep -c anon_inode:inotify) if [ $n -gt 0 ]; then echo $pid $n $(cat $pid/comm 2/dev/null) fi done | sort -k2 -n -r | head -10排在最前面的是两个日志采集 Agent 和一套配置同步服务基本都是长驻进程。它们各自占了几十个 inotify 实例加起来直接把配额吃光了。我当时没直接重启业务进程而是先把fs.inotify.max_user_instances临时调到 8192让 cAdvisor 先起来。随后和各个 Agent 的负责人确认那几个长驻进程为什么占那么多 inotify最后发现其中有一个版本确实存在句柄泄漏升级后实例数下降很多。cAdvisor 的启动参数也做了优化加上--docker_onlytrue后续几个月没有再出现类似告警。5.2 常见误区第一个误区是只看进程 fd 数量不看系统 inotify 实例数。too many open files太容易让人联想到 ulimit但如果报错来自inotify_init一定要同时查 inotify 相关的内核参数。第二个误区是调高max_user_instances之后不关注max_user_watches。cAdvisor 这种监控工具inotify 实例数是一方面它注册的 watch 数量可能更多。如果 watch 数过大同样会有资源耗尽的问题。第三个误区是只在命令行临时调参没有持久化。很多人在测试环境用sysctl -w解决了问题一旦重启节点老问题复现还以为配置没生效。第四个误区是盲目重启所有相关进程。如果 inotify 实例占用异常直接重启业务进程确实能暂时释放资源但根因泄漏不修过段时间还会复发。应该先统计出哪些进程占用最多再有针对性地处理。5.3 速查表这里把我这次处理过程中用到的主要命令和配置整理成一张速查表方便你直接复制。排查项命令或配置说明查看进程 fd 数量ls /proc/PID/fd | wc -l观察 fd 是否接近 nofile查看进程 limitscat /proc/PID/limits确认 nofile 软硬限制查看 inotify 实例总数find /proc/*/fd -lname anon_inode:inotify 2/dev/null | wc -l系统内 inotify fd 总量查看 inotify 限额sysctl fs.inotify.max_user_instances fs.inotify.max_user_watches内核参数当前值定位 inotify 占用进程遍历/proc/*/fd并统计找出大户进程systemd 调 nofile[Service] LimitNOFILE65535重启服务生效docker 调 nofile--ulimit nofile65535:65535容器启动参数系统调 inotify/etc/sysctl.d/90-inotify.conf持久化配置验证 cAdvisor 指标curl localhost:8080/metrics确认采集正常参数大小要根据实际环境设置。我见过一些帖子里直接让人把max_user_instances调到 102400其实没必要实例数太多并不会带来性能提升反而会占用内核内存。根据节点容器规模按需调整通常8192起步足够最多65536一般也顶天了。5.4 最后再分享一个小技巧如果 cAdvisor 经常因为各种资源限制启动失败可以在 systemd unit 里加一条Restarton-failure和RestartSec10让它失败后自动重试。这不是根因修复但能帮你在值班时争取一点排查时间。不过别依赖自动重启还是要从 inotify 配额和进程泄漏两个方向把问题真正解决。另外cAdvisor 版本差异也很大老版本对 inotify 的消耗明显更高。如果你在升级后发现 inotify 实例占用下降不用惊讶这是新版本优化了 cgroup 监听策略。定期升级 cAdvisor 到稳定版本身也是一种降低此类问题发生概率的手段。
返回列表