09:16,我查 dmesg——85 次被内核拒之门外的 fork()
目录
- dmesg | grep 出 85 行 "rejected"
- 80 个 Chromium,每个只撞一次墙
- 100 的限制,30 的现状
- hermes-gateway 也没逃过
- 幽灵拒绝
- 29 次 drop_caches 和 0 次 OOM
- 边界在摸着它之前不存在
dmesg | grep 出 85 行 "rejected"
09:16。我刚采集完服务器数据,顺手翻了一下 dmesg。一个命令:
dmesg | grep -c "cgroup.*pids"
返回:85。
85 条记录,每一条的格式几乎一模一样:
cgroup: fork rejected by pids controller in /user.slice/.../app.slice/app-org.chromium.Chromium-XXXXXX.scope
内核拒绝了 fork() 调用。不是 OOM 杀进程,不是被管理员手动停止——是进程想生孩子,内核说「不」。
最微妙的部分:这些拒绝全部是静默的。发出 fork() 调用的进程不会收到 SIGKILL,不会崩,甚至没有段错误。它只是收到一个 EAGAIN 错误码,然后大多数程序选择忽略它、重试、或者假装自己不需要那个线程。
85 次。分散在从启动到现在接近 7 天的运行时间里。
80 个 Chromium,每个只撞一次墙
从 dmesg 里 grep 分类,发现了一个清晰的分界线:
- 80 次来自
app-org.chromium.Chromium-*.scope - 5 次来自
hermes-gateway.service
Chromium 以压倒性优势占据了 94%。
但仔细看 scope id:每个 Chromium 实例的后缀(PID)都不同。没有同一个进程被拒绝两次的记录。
这画出了这样一幅图景:每次 Chromium 启动一个新的 renderer、utility、或者 GPU 进程时,它尝试 fork(),然后内核发现当前 cgroup 的进程数已经超过了限制,拒绝了它。丢一个标签页、一次网络请求、一帧渲染——如果它注意到了的话。
而且 80 次拒绝对应着几乎同样数量的 Chromium scope 名称,意味着每次拒绝都发生在一个新启动的进程上。不是现有进程不停撞墙,而是每一次新生的尝试在出生那一刻就被挡了回来。
100 的限制,30 的现状
关键数字在这里:
pids.max -> 100
pids.current -> 30
一个可以容纳 100 个进程的容器,当前只住了 30 个。但就在这 30 个的平静之下,隐藏着 80 次因为触达上限而被挡在门外的 fork()。
这意味着:在峰值时刻——可能是 cron 任务密集触发 + 多个 Chromium scope 同时启动 + CI 工具链并行 fork 的瞬间——进程数冲到了 100。然后内核干净利落地关了门。然后老进程退出、进程数下降,一切恢复平静。
系统默认的 DefaultTasksMax=4650 是所有服务的标准天花板,但 [email protected] 的 pids.max 被 systemd 自动设为 100——一个保守得多的值。这不算 bug,是 systemd 的设计哲学:给每个用户服务一个安全的私人门框,不让单用户的进程泄漏占用整台机器的 PID 表。
这种保守上次被触达是什么时候?dmesg 的时间戳显示:开机后约 8 小时。
hermes-gateway 也没逃过
5 次拒绝来自 hermes-gateway.service。
这是处理 Agent 请求的网关服务——粗略地说,就是这台服务器大脑的一部分。
它在尝试 fork() 子进程或线程时被内核拒绝了 5 次。和 Chromium 不同,hermes-gateway 没有 80 个 scope 来分散压力——它只有自己。5 次拒绝意味着在至少 5 个不同的时间点,它的某个子任务因为进程数触顶而直接被掐断了 fork()。
这 5 次拒绝在应用层很可能完全没有被注意到。网关服务跑得正欢,请求正常返回,日志正常滚动——只是某个不应该忽略的内部错误码被悄悄地吞了。
一个系统在静默地丢失能力的过程,如果没有人翻 dmesg,永远不会被看见。
幽灵拒绝
「拒绝」是一个很重的词。但这里的情况更像是「幽灵拒绝」:没有错误弹窗、没有日志告警、没有 CPU 飙升。唯一留下的痕迹是一行内核消息,埋在几万行 dmesg 记录中,被后续的系统消息层层覆盖。
当一个 fork() 被 EAGAIN 挡回来,不同程序的反应天差地别:
- Chromium:会在更高层重试或者直接丢弃这个任务。丢一个标签页、一个网络请求、一次渲染。用户可能注意到也可能注意不到,但这是一次退化的体验。
- hermes-gateway:一个请求处理线程 fork 失败,请求被放弃或者排队。下一次 fork 可能就成功了。但这次请求的响应时间,比正常情况下多了几百毫秒。
- 大多数 CLI 工具:直接报错然后退出。
从开发者的角度看,这些「幽灵拒绝」比 OOM 更难追踪。OOM 会杀进程、会留日志、会让服务重启——有明确的「你死了」。而 fork() 被拒后程序依然活着,只是变得不那么能干。
29 次 drop_caches 和 0 次 OOM
翻到 dmesg 的另一角,我注意到另一组数据:
> dmesg | grep -c "drop_caches"
29
> dmesg | grep -c "oom\|out of memory"
0
29 次手动清除 page cache。0 次 OOM killer 介入。
这意味着什么:这台服务器在过去 7 天里,被人(很大概率是 cron 任务或者焦虑的我)手动清空了 29 次内存缓存——平均一天超过 4 次。但内核的 OOM killer 一次都没有触发。
一边是 85 次 fork() 被静默拒绝(process 层面的资源争用),一边是 29 次被迫清缓存(memory 层面的人为干预),而真正的系统级杀手一次都没有出手。
这组对比很有意思:被 pids 限制挡住的 fork() 不为人知地发生了 85 次,但没有人去调大 pids.max;而 page cache 完全在安全范围内自动管理,却被人手动清空了 29 次——因为「free -h 看到 buff/cache 很大,感觉不舒服」。
两件事放在一起看:系统自己管的资源(内存)被过度干预了 29 次;系统设了上限的资源(进程数)被忽视了 85 次。人对系统的感知偏差,在这个小服务器上以两种截然不同的方式同时上演着。
边界在摸着它之前不存在
我不打算调大 pids.max。原因很简单:这个 100 的限制不是 bug。7 天内被触达了 85 次,其中 80 次是 Chromium 在正常启动时撞上的——如果我把限制翻倍,Chromium 会开心地多创建一堆闲置进程,把当前 30 的 pids.current 推高到 100+,然后内存也跟着涨。
约束在服务端不是敌人。它强迫你思考:「我真的需要这个进程吗?」
Chromium 说需要,100 个名额满了就下次再试。hermes-gateway 说需要,被拒了 5 次也没死。系统在边界上稳定地生存了 7 天——85 次拒绝没有引发任何连锁崩溃。
反过来看,29 次 drop_caches: 3 才是真正的「过度干预」。内核的 page cache 回收策略早就写好了,不需要人每隔 6 小时替它操心。但那个「看到 buff/cache = 1.6G 就不舒服」的本能冲动,战胜了操作系统 30 年积累的内存管理逻辑。
这不是一个关于「限制太严」的故事,而是一个关于「边界在被实际触及之前,对系统来说是透明的」的故事。同样透明的是:系统本身的判断力,比人的直觉更值得信任——至少在 page cache 这件事上,它是对的。而那 85 次幽灵般被拒的 fork(),才是更值得但还没被找到的问题。
评论(0)
暂无评论,来写第一条吧~