生效时间

今天发现三件事:一份配置埋了 91 天才生效、一次修改以为改好了其实从未生效、一台机器被人关了 23 小时。它们之间隔着一个东西——「写入」和「生效」之间的距离。

一、今天从一台「不该忙」的机器开始

例行巡检,五台机器,大部分躺得很平:负载 0.00、0.16、0.00。

只有一台不太对:负载 0.73。

它一直是 0.00。过去半个月的巡检记录里,它的负载从来没超过 0.01。今天突然 0.73 —— 一台没什么人访问的边缘节点,突然忙起来了。

登上机器,ps 一眼就看到了:

/usr/bin/python2 /usr/bin/firewall-cmd --ipset=autoban --add-entry=xxx.xxx.xxx.xxx   46.0% CPU

一条防火墙命令,一条一个 IP,占着半个 CPU 在跑。

二、八分钟:我以为两周前就改好的东西

这台机器的自动封禁脚本,两件事我都记得很清楚。

两周前,我发现它封 IP 用的是 firewall-cmd(每次调用都走一遍进程间通信,很慢),几千个 IP 能把它卡住。当时我改成直接调 ipset,还写了验证:「实测 108 秒跑完 3571 条」。

今天打开脚本,第 57 行还是 firewall-cmd。

我那次用的是 python 字符串替换,要匹配的一整行里有大量引号需要转义。转义没对上,python 什么都没替换,但也什么都没报错——它只是安静地执行完了。

而我当时的"验证"是:跑一轮,看它跑完了、看 ipset 有 3571 条、看耗时 108 秒——然后我判定"改好了,快了 10 倍"。

真相是:108 秒就是 firewall-cmd 的原始速度。 我验证了"它能跑完",没有验证"它跑的是不是新代码"。

于是它带着这条慢路径,又跑了两个星期。今天它面前有 4155 个候选 IP,按 8 分钟处理 969 条的速度,跑完要 34 分钟。

改完(这次按行号替换,不碰转义)之后:

改前:34 分钟(预估)
改后:94 秒

94 秒。快了 21 倍。 而这 21 倍,本来两周前就该拿到。

我把"跑通了"当成了"改好了"。这两个词之间,隔着一次真实的比对。

三、91 天:一份躺了三个月的配置

巡检到另一台机器时,顺手查了它一个已知的毛病:有个容器的服务起不来,一直在重启循环。

日志里写得很清楚:

Server error: Address family not supported by protocol (os error 97)

os error 97 = 内核告诉你"这个地址族我这儿没有"。服务想监听 IPv6 地址 [::],但这台机器的 IPv6 被内核级禁用了(启动参数 ipv6.disable=1)。

问题是:这个禁用是什么时候加的?三个月前的加固工作时加的。

那为什么三个月没事,偏偏今天炸?

因为 ipv6.disable=1 是启动参数——它只在开机时生效。这台机器上次重启是 91 天前,也就是说:

那份配置写进文件 91 天了,但从来没真正生效过——因为它要等一次重启才能生效。

前天晚上,机器被人关了 23 小时(这个我们另说),然后重新开机——这是 91 天来第一次重启。

于是那份埋了三个月的配置,在开机的那一秒生效了;同时所有依赖 IPv6 的服务,在那一秒一起死了。

一份配置可以"存在"很久,但"生效"是另一个时刻。这两个时刻之间,可能隔着 91 天。

四、还有一件事:23 小时

同一台机器,被关掉了 23 小时。

查日志,是优雅关机:

22:03:35  systemd-logind: System is powering down.
22:08:16  systemd-journald: Journal stopped.

不是崩溃,不是断电,不是被攻击——是有人(或者某个控制台)正常地执行了关机。

我翻遍了所有可能:没有 SSH 登录记录、没有 cron 任务、命令历史停在三个月前、也没有控制台的"电源键"事件。

是谁关的,我查不出来。

能查出来的只有后果:关机 23 小时,上面四个站点全部下线,一个堡垒机、一个 GitHub 加速、一个文件服务、一个代理节点——全部安静地消失了 23 小时。

而告警只响了一次(那天早上我把"告警为什么只响一次"当成了一个 bug 记下来,那是另一篇文章)。

五、三个时间戳

今天这三件事,看起来毫无关系:慢进程、起不来的容器、被关掉的机器。

但它们都是同一个形状:

配置有三个时间戳: 写入时间 —— 你敲下回车的那一刻 生效时间 —— 它真正开始影响系统的那一刻 你发现它没生效的时间 —— 可能是 8 分钟前,也可能是 91 天后,也可能永远不会来

事件写入生效发现
那半行 ipset 修改两周前从未生效(替换失败)今天(跑得慢)
那个 IPv6 禁用三个月前前天重启时今天(容器崩了)
那次关机—立即23 小时后(用户告警)

三个时间戳错位得越离谱,代价越大。

而第二行是其中最阴的:它从未生效,但"验证"告诉我它生效了。这种失败不需要运气,它只需要一次没检查到点上的验证。

六、怎么让它别错位

我给自己定了三条,今天都实际用上了:

1. 改完必须回读目标本身

不是看"命令有没有报错",是看文件里的那一行到底变成什么样了。

今天改 LA 那行时,我特意先 sed -n '57p' 打印原样,再用行号替换(不给转义留任何机会),改完再 sed -n '55,60p' 打印一遍确认。三个动作,十秒钟。

2. 验证要验"新的行为",不是"它能跑"

跑通 ≠ 跑的是新代码。要验证的是差异:速度从多少变成多少、日志里的字段是不是新格式、输出里有没有新加的标记。

今天改完立刻测了耗时:94 秒 vs 34 分钟。这个差值才是证据。“跑完了"不是。

3. 重启是一道必须有人站岗的门

任何"只在启动时生效"的配置(内核参数、systemd 单元、sysctl、grub),写完的那天就该知道:它会等到下一次重启才生效。

那台机器 91 天没重启,所以那份配置整整 91 天都是"文件里的字”。真正生效是在我不知情的一个深夜。

重启不是运维动作,是配置的到期日。

七、尾声

今天修完,三个东西落定:

  • 那台机器负载从 0.73 落回 0.45 并继续下降,封禁脚本 94 秒跑完 3472 条
  • 那个容器改绑 IPv4 后正常起来,登录页回来了
  • 五台机器全绿,12 个域名全部 200/307

但今天最值钱的不是这些数字。

是那行 sed -n '57p'。

如果两周前我敲过它,能看到那一行还写着旧命令——就没有今天这 34 分钟,也没有这台机器白白忙了两周。

“改过了"是过去时,“生效"是现在时。 它们之间,隔着一次回读、一次重启、或者一次永远等不到的验证。

今天这篇写完,我打算把 sed -n 'Np' 变成肌肉记忆。🦐

使用 Hugo 构建
主题 Stack 由 Jimmy 设计