ARTICLE DETAIL

资讯详情

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

cAdvisor 报错 too many open files:inotify 与文件描述符根因排查指南

cAdvisor 报错 too many open files:inotify 与文件描述符根因排查指南 先讲一段真实经历。有次凌晨被监控告警吵醒生产环境某个节点的 cAdvisor 容器反复 CrashLoopBackOffkubectl logs拉下来关键信息就那么一行inotify_init: too many open files。第一次碰到的人大概率会顺手把容器重启一下或者直接把 ulimit 调大结果过几天又一模一样地复发。这类问题的难点从来不在改配置而在搞清楚一件事cAdvisor 作为容器监控组件为什么会在 inotify 初始化这一步撞上文件描述符上限这个上限又是由哪一层决定的这篇文章我把完整的根因分析、排查链路、分场景的解决方案和后续监控治理都整理出来覆盖 Docker 部署、systemd 部署和 Kubernetes 环境。如果你在用 cAdvisor 做容器监控或者正在维护任何一启动就报 too many open files的服务这篇文章值得完整看一遍。1. inotify_init 报错背后文件描述符账本是怎么被塞满的1.1 cAdvisor 为什么会去用 inotify先厘清一个基础概念。inotify 是 Linux 内核从 2.6.13 开始提供的文件系统事件通知机制用户态程序可以告诉内核帮我盯着某个目录或文件有创建、删除、写入、属性变化就通知我。应用程序先调用inotify_init()创建一个 inotify 实例得到一个文件描述符再通过inotify_add_watch()把具体路径关联到这个实例上。cAdvisor 的定位是容器资源监控启动后要扫描容器文件系统、镜像层、Docker 的 overlay2 目录持续统计磁盘占用、文件系统容量、inode 用量这些指标。它需要在一些关键路径上注册 inotify 监听这样文件系统一有变化就能及时感知避免反复做全量扫描。问题恰恰出在这里。inotify_init()本质上是在分配一个新的文件描述符。如果进程当前的 fd 总数已经到达上限内核会直接返回EMFILE映射到用户态的错误信息就是 Too many open files。cAdvisor 启动早期要做大量初始化inotify 实例还没建出来就撞墙进程自然起不来。1.2 too many open files 的真实触发点这个报错的误导性很强。Too many open files字面意思是打开的文件太多了但在 Linux 里它实际指的是文件描述符分配失败。fd 在用户态只是一串整数内核通过它定位打开的文件、socket、管道等对象。EMFILE抛出的条件只有一个进程的 fd 数量达到了RLIMIT_NOFILE软上限。一个容易忽略的点是fd 不只是对应普通文件。socket、pipe、eventfd、timerfd、epoll fd还有这里的主角 inotify instance统统占用 fd 名额。一个 inotify 实例占一个 fd往里面添加 watch 不额外占 fd但会占内核的 inotify watch 配额。所以fd 耗尽和inotify watch 数量超限是两本不同的账报错信息却可能长得一样排查时必须区分清楚。2. 为什么 cAdvisor 天然容易撞上这个限制四层限制与 inotify 消耗模型2.1 进程、systemd、内核、容器运行时——限制到底卡在哪一层Linux 上跟 fd 相关的限制至少有四层每一层都可能成为那根压垮骆驼的稻草。第一层是内核级。fs.file-max控制整台机器能分配的最大 fd 数量一般很难触达除非节点上跑了几百上千个容器且都有 fd 泄漏。第二层是用户级 inotify 限制包括fs.inotify.max_user_instances单用户可创建的 inotify 实例数常见默认 128和fs.inotify.max_user_watches单用户可添加的 watch 数常见默认 8192 或 65536。第三层是进程级 rlimit也就是RLIMIT_NOFILE有 soft 和 hard 两个值进程实际可用的是 soft 值可以在不超过 hard 的前提下自行上调。第四层是容器运行时对进程默认值的设定docker daemon 可以配default-ulimitsdocker run 可以显式传--ulimitsystemd 托管服务则看LimitNOFILE。cAdvisor 最常见的部署方式就是容器。很多一键脚本用docker run把它拉起来压根不设置 ulimit。此时容器内进程的 nofile 继承自 docker daemon 的默认值而不少发行版和 Docker 版本的默认 nofile 只有 1024。一个负责监控整个节点容器状态的进程手上只有 1024 个 fd还要同时处理网络统计、文件系统扫描、inotify 监听在容器数量稍多的节点上启动阶段直接被卡死再正常不过。有朋友可能会觉得 1024 不算少。但 cAdvisor 启动时要对每个容器的挂载点、目录层级逐个处理叠加 Go runtime 的网络轮询、日志管道、事件循环fd 消耗速度非常快。我曾经在一台跑着 80 多个容器的节点上验证/proc/pid/fd下的条目数在启动初期就已经奔着 500 去了这还没进入稳定运行阶段。2.2 inotify 实例与文件描述符为什么这是两个容易混淆的限制这里有个高频误区看到inotify_init报错第一反应是去调fs.inotify.max_user_instances。这个参数确实管 inotify 实例数量上限但 cAdvisor 的场景里真正卡住的往往不是它而是进程的RLIMIT_NOFILE已经耗尽inotify_init()申请新 fd 时被拒绝。区分方法很简单看进程当前 fd 总数是否已经贴近 soft limit。贴得很近大概率是 rlimit 的问题如果 fd 总数还有大量富余才需要考虑 inotify instance 或 watch 限制。这个判断顺序非常关键我先在这埋个伏笔后面排查链路部分会完整演示。3. 完整排查三步走确认消耗类型、定位限制来源、验证当前用量我不喜欢一上来就丢解决方案因为那样你只是拿到了一个补丁下次换个形式照样懵。下面这套排查链路是我实际验证过的每一步都有明确目的照着走基本能实锤根因。3.1 第一步确认 cAdvisor 的运行方式与进程号动手之前先确认 cAdvisor 是容器方式还是 systemd 方式运行的这决定了后面要改哪一层的配置。# 容器运行 docker ps | grep cadvisor # 如果用的是 containerd/crictl crictl ps | grep cadvisor # 宿主机直接跑的进程 ps -ef | grep cadvisor拿到 PID 之后后续所有命令都以它为中心展开。3.2 第二步用 /proc 查清当前 fd 用量与上限这一步是整个排查的核心直接看数据说话。# 查看进程的 soft/hard 限制 cat /proc/PID/limits | grep -i open files # 统计当前已分配的 fd 数量 ls /proc/PID/fd | wc -l对比当前 fd 数量和Max open files soft limit这两个数字。如果前者已经等于或非常贴近后者基本可以实锤是RLIMIT_NOFILE不足导致的EMFILE。容器场景下还可以进容器再确认一遍docker exec 容器名 sh -c ulimit -n这里有个细节要注意容器内看到的 ulimit 不一定等于宿主机的 ulimit它来自 docker daemon 的default-ulimits配置或docker run --ulimit显式指定的值。所以容器内和宿主机两侧都要看才好判断限制到底从哪一层带进来的。3.3 第三步区分普通 fd、socket fd 与 inotify fd知道总量不够还得知道谁在消耗。这一步决定你该调 rlimit、调内核 inotify 参数还是去查连接泄漏。# 列出所有 fd 及其类型 ls -la /proc/PID/fd # 单独统计 inotify fd 数量 ls -la /proc/PID/fd | grep anon_inode:inotify | wc -l # 单独统计 socket fd 数量 ls -la /proc/PID/fd | grep socket | wc -l/proc/PID/fd下的符号链接会显示 fd 指向的对象。anon_inode:inotify就是 inotify 实例socket:[...]是网络连接普通文件则会显示具体的文件路径。如果普通文件 fd 占比很高可能跟日志文件、镜像层遍历有关如果 socket 占比很高重点排查网络连接是否异常增长如果 inotify fd 占比突然拉升才轮到 inotify 相关内核参数。最后看一眼宿主机的 inotify 总账sysctl fs.inotify.max_user_instances fs.inotify.max_user_watches再配合一段脚本统计当前 inotify 实例的实际用量for proc in /proc/[0-9]*/fd/*; do readlink $proc 2/dev/null | grep -q anon_inode:inotify echo ${proc%/fd/*} | cut -d/ -f3 done | sort | uniq -c | sort -rn | head -20这串命令会按用户统计所有进程持有的 inotify fd 数量和max_user_instances对比就能判断实例数是否真的触顶。3.4 一套完整判断逻辑分清三个 为什么把上面所有信息汇总后按下面的顺序做判断如果 fd 总量贴近 soft limit且 inotify fd 占了不少——先调进程的 nofile 限制。如果 fd 总量还有富余inotify_init仍然报错——检查fs.inotify.max_user_instances是否已满。如果 inotify watch 数量超限这个一般报的不是EMFILE而是ENOSPC错误信息通常是 inotify watch limit reached——调fs.inotify.max_user_watches。把这几个问题问清楚解决方案就是顺理成章的事而不是靠猜。4. 按部署方式对应的解法Docker、systemd、内核参数分层调整4.1 容器部署docker run、docker-compose 与 Kubernetes DaemonSet最直接的做法是给容器显式指定 nofile ulimit。docker run --ulimit nofile65536:65536 \ --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 \ --publish8080:8080 \ --detachtrue \ --namecadvisor \ gcr.io/cadvisor/cadvisor:latestdocker-compose 的写法services: cadvisor: image: gcr.io/cadvisor/cadvisor:latest ports: - 8080:8080 volumes: - /:/rootfs:ro - /var/run:/var/run:ro - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro - /dev/disk/:/dev/disk:ro ulimits: nofile: soft: 65536 hard: 65536这里有个非常容易踩的坑改完参数后旧容器不能docker restart必须docker stop再docker rm然后重新docker run。docker restart只是把同一个容器再启动一遍不会应用新的 ulimit 配置。我见过不止一个人在这个细节上卡了半天以为配置没生效实际是容器压根没换。Kubernetes 场景比较特殊。原生的 Pod spec 不直接支持设置 ulimit需要在容器运行时层面对所有容器设置默认值。如果 cAdvisor 是当成 DaemonSet 跑的一般会在容器的securityContext里加privileged: true然后依赖运行时默认的 nofile 值。这个默认值通常由 containerd 或 CRI-O 的配置决定也可以直接改 kubelet 的 systemd unit 里的LimitNOFILE让 kubelet 启动的所有 CRI 容器继承更高的上限。这个方法虽然不是零成本但在大规模集群里比一个个 Pod 去抠配置靠谱得多。4.2 systemd 托管LimitNOFILE 与默认值的影响如果 cAdvisor 以 systemd 服务方式运行解法是在 unit 文件里显式声明[Service] LimitNOFILE65536改完执行systemctl daemon-reload systemctl restart cadvisor等等这里还有个细节。systemd 本身也有自己的默认限制逻辑。不同发行版的DefaultLimitNOFILE值不一样有的默认是 1024有的更高。即使服务 unit 文件里不写LimitNOFILEsystemd 启动的子进程也会继承全局默认值。所以规范做法是先在 unit 里显式写清楚不要依赖发行版的默认行为。另外注意LimitNOFILE的写法在老版本 systemd 里还可以写成LimitNOFILE65536个别版本支持infinity关键字但不同版本对infinity的解释不完全一致。最稳妥的还是直接写死一个具体数字避免歧义。4.3 内核 inotify 参数什么时候才真正需要动它如果第三步的排查确认是 inotify instance 或 watch 数量确实吃紧再调内核参数sysctl -w fs.inotify.max_user_instances1024 sysctl -w fs.inotify.max_user_watches524288持久化写入/etc/sysctl.d/99-inotify.conffs.inotify.max_user_instances1024 fs.inotify.max_user_watches524288执行sysctl --system使其生效。这里要再次强调先确认根因再改动。我见过有人一上来就把max_user_watches调到 524288其实问题出在 nofile白调不说还掩盖了真正需要关注的现象。内核 inotify 限制不是不用管而是不能优先动。绝大多数 cAdvisor 启动失败的案例根因都在进程的RLIMIT_NOFILE而不是 inotify 的内核配额。如果实在拿不准安全路径是从下往上调先调进程 ulimit最直接、影响面最小再观察还不行再看 inotify 实例数最后才考虑系统级fs.file-max——这个参数在绝大多数情况下根本不需要动。5. 修复后的验证与长效监控别让问题在一周后卷土重来5.1 重启后如何确认参数真实生效很多人在这一步栽跟头配置改了服务重启了觉得应该好了。但参数有没有真实生效得用数据确认不能靠感觉。# 确认进程的 fd 上限已经更新 cat /proc/PID/limits | grep -i open files # 确认服务状态稳定没有持续重启 systemctl status cadvisor # 或 kubectl get pods | grep cadvisor然后观察日志里有没有再次出现inotify_init: too many open files。注意刚重启完的几分钟内没报错不代表没问题要持续观察 24 到 48 小时看 fd 用量的增长曲线是否平缓。我处理这类问题时有个习惯修复后把 fd 总量、inotify fd 数量、进程 uptime 三个数字记下来第二天同一时间再看一次。如果 fd 数量在稳定运行后不再持续攀升说明问题真正解决了如果数值还在缓慢爬升说明存在 fd 泄漏只是暂时没到临界点而已。5.2 用 Prometheus 盯住 cAdvisor 的 fd 与 inotify 使用率cAdvisor 自己崩溃时它的 metrics 接口也就断了。这时候不能指望它自己监控自己得靠 node_exporter 之类的独立采集器。node_exporter 的 textfile collector 很适合干这事。写一个简单脚本定时把 cAdvisor 进程的 fd 使用情况写到指定目录node_exporter 会自动暴露给 Prometheus。#!/bin/bash # /usr/local/bin/cadvisor_fd_metrics.sh PID$(pgrep -f cadvisor | head -1) if [ -z $PID ]; then exit 0 fi FD_COUNT$(ls /proc/$PID/fd 2/dev/null | wc -l) FD_LIMIT$(awk /Max open files/ {print $4} /proc/$PID/limits 2/dev/null) if [ -n $FD_COUNT ] [ -n $FD_LIMIT ]; then cat EOF # HELP cadvisor_process_open_fds Current open fd count of cadvisor process # TYPE cadvisor_process_open_fds gauge cadvisor_process_open_fds $FD_COUNT # HELP cadvisor_process_max_fds Max fd limit of cadvisor process # TYPE cadvisor_process_max_fds gauge cadvisor_process_max_fds $FD_LIMIT EOF fi配合 crontab 每分钟执行一次* * * * * /usr/local/bin/cadvisor_fd_metrics.sh /var/lib/node_exporter/textfile_collector/cadvisor_fd.promPrometheus 告警规则可以这样写groups: - name: cadvisor_alerts rules: - alert: CadvisorFDUtilizationHigh expr: cadvisor_process_open_fds / cadvisor_process_max_fds 0.8 for: 5m labels: severity: warning annotations: summary: cAdvisor fd usage high on {{ $labels.instance }} description: cAdvisor on {{ $labels.instance }} has used over 80% of its fd limit.这样可以提前几天收到预警而不是等 cAdvisor 彻底挂了才从告警风暴里发现问题。5.3 从根源降低 fd 压力cAdvisor 自身参数调优调完限制只是止血让 cAdvisor 对 fd 的消耗速度慢下来才是治本。比较有效的两个参数--housekeeping_interval30s --docker_onlytrue--housekeeping_interval控制 cAdvisor 执行周期性容器统计的频率默认是 10 秒有的版本是 1 秒改成 30 秒可以明显减少文件系统扫描和事件处理的频率。--docker_onlytrue让它只监控 Docker 容器跳过额外的存储驱动和运行时适配逻辑。有朋友担心间隔调长了监控数据不实时。我自己的生产经验是30 秒的 housekeeping 间隔对绝大多数资源监控场景完全够用Prometheus 本身抓取间隔通常也是 15 到 30 秒没必要在 cAdvisor 这一层追求秒级数据。如果磁盘统计不是刚需还可以配合--disable_metricsdisk,diskIO等参数裁剪不需要的指标进一步减少 fd 和内存压力。具体哪些指标可以关和你的监控需求强相关这里不展开但思路是明确的不需要的监控项果断关掉。6. 同一个 raw 错误、多个隐藏根因排查方法才是通用资产6.1 socket fd 耗尽、inotify watch 耗尽、RLIMIT_NOFILE 耗尽cAdvisor 这个案例其实是整个 too many open files 问题家族的一个缩影。同样的报错背后的根因可能截然不同。RLIMIT_NOFILE 耗尽最典型的场景进程 fd 总量触顶。症状是 fd 总数贴近 soft limit任何类型的 fd 分配都可能失败。socket fd 耗尽通常伴随连接泄漏比如 HTTP client 没设置超时、连接池没回收。症状是 socket fd 占比畸形偏高。inotify watch 耗尽报错一般不是 too many open files而是 No space left on device 或 inotify watch limit reached。症状是 inotify fd 数量正常但 watch 总数触到了max_user_watches上限。排查的思路是通用的先看总量再看类型最后对照对应层次的限制。这三个步骤不需要动任何配置只读/proc就能完成成本极低但能帮你避开 90% 的盲目调优。6.2 Kubernetes 环境下 kubelet 与 CRI 的 ulimit 传递链最后聊一下 Kubernetes 环境下的 ulimit 传递逻辑因为很多人在容器里改完配置重启后发现又变回去了。Pod 里进程的 ulimit来源链路大致是kubelet 进程的 systemd unit 配置LimitNOFILE→ kubelet 启动 CRI 运行时containerd 或 CRI-O→ 运行时为每个容器设置默认 rlimit → 容器内进程继承。所以如果你在 K8s 集群里发现所有容器默认 nofile 都很低最省事的做法是调整 kubelet 的 systemd unit[Service] LimitNOFILE1048576然后systemctl daemon-reload重启 kubelet。注意重启 kubelet 会影响节点上所有 Pod生产环境要按维护窗口来操作。containerd 也可以在某些版本里通过配置文件指定默认 ulimit但配置位置和格式随版本变化不如改 kubelet unit 来得直接。另外一个经验cAdvisor 本身不一定是唯一受害者。日志采集器filebeat、fluentd、边车代理、监控 agent在容器多、目录多的节点上同样容易踩 fd 相关的坑。把这些进程的 fd 用量统一纳入监控比等故障发生后再逐个排查要省心得多。最后分享一点个人心得。处理这个问题的过程中我最开始也走了弯路反复调大参数、反复重启以为上限够高就万事大吉结果过几天照样复发。后来静下心统计了 fd 类型分布才发现问题一直出在 inotify fd 的异常增长上。调大 nofile 只是把引爆点往后推真正要盯的是消耗趋势本身。所以每次看到 too many open files我第一反应已经不是把数值调大而是先去 /proc 里看清楚谁在吃 fd、吃到什么程度、趋势是平缓还是陡增。这套思路用在任何进程上都成立希望也能给你省掉几个加班的夜晚。
返回列表