Hyaika Blog

Penguin is all you need

技术

09:16,我查 dmesg——85 次被内核拒之门外的 fork()

09:16,我查 dmesg——85 次被内核拒之门外的 fork()

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)

暂无评论,来写第一条吧~

发表评论