补丁打完了,问题才刚开始
那篇 502 复盘写完之后,我做了一件当时没写进文里的事:去另一台服务器看了一眼。
那边的站跑的是同一份 emlog
核心代码,毛病一模一样——getCurrentTemplate() 照样无条件
setcookie(),带 ?theme=
的请求里,几百条重复响应头照发不误。区别只在于,那台前面没有一层会被撑穿的网关缓冲,于是它不会
502,只是安安静静地,把几十 KB 的重复响应头发给每一个访客。
这件事让我有点后背发凉。不是因为这个 bug 有多严重,而是因为:我是在「已经知道病灶长什么样」之后,才想起去查第二个站的。
如果没有那篇复盘,我大概永远不知道第二台也在漏。而这暴露了一个更一般的问题——一份核心、两台机器、多个站点的时候,补丁到底该怎么管。
<h1
id="为什么会变成一份核心多站多机">为什么会变成「一份核心、多站多机」
先把家底说清楚。
- 主站跑在 1Panel 上,前面是 OpenResty。
- 另一台服务器走宝塔,上面是同一份 emlog 核心代码。
- 核心代码不是我写的,改它,等于在别人的地基上贴了一张补丁纸。
emlog 的核心补丁有三个讨厌的性质:
- 它不在版本库里。 我改的是服务器上的文件,不是在某个仓库里提交。谁改的、改了什么、什么时候改的,只有我自己记得——而我显然记不住。
- 升级即失效。
include/service/template.php是核心文件,后台点一次升级就被覆盖回去,老毛病原地复活。 - 它不会自己同步。 两台机器之间没有部署管道,改完一台,另一台不会知道。
三条叠起来的结论很朴素:补丁不能靠记忆管,得靠台账。
台账:先记下「我补了什么」
我现在的做法很简单——每打一个核心补丁,就往一张表里加一行。这行只有六个字段:
| 字段 | 记什么(以 502 那条补丁为例) |
|---|---|
| 补丁编号 | P-001 |
| 目标文件 | include/service/template.php |
| 依据 | 带 ?theme= 的请求一次渲染累积数百条 Set-Cookie,撑穿网关响应头缓冲导致 502 |
| 改动摘要 | 「先比对再发」+ 手写回写本请求的 COOKIE 超全局;命门是那行手写回写,漏掉它补丁等于没打 |
| 复测命令 | curl 数 Set-Cookie 条数(下面给出) |
| 打在哪些站 | 1Panel + OpenResty 站;宝塔站 |
这张表的价值不在「记录」本身,而在三个地方:
- 它定义了「打完」的标准。 一条补丁打完,不是文件替换成功,而是复测命令输出符合预期。台账里那行复测命令,就是验收标准。
- 它让升级后的回归变成照单执行。 升级完拿着表逐条跑,不用回忆「我到底改过哪些核心文件」。
- 它让第二台机器不再是盲区。 行里明确写着「打在哪些站」——有空格的,就是漏的。
还有一条经验:「依据」这一列不要写「修了个
bug」,要写「能复现的现象或编号」。 半年后回头看「修了个
bug」,你完全不知道自己当时在防什么;而写下「带 ?theme=
的请求累积数百条 Set-Cookie」,一眼就能想起为什么要打它。
写这张表花不了十分钟,省下的是事后怀疑人生的时间。
多站同步的最小流程
有了台账,同步就变成了执行问题。我用的流程四步,两台机器动作一样,只是面板入口不同:
- 先在本机仿真,别直接上线。 补丁只该改行为、不改功能,上线前先确认它到底改掉了什么、留下了什么。
- 备份。 替换前,在同目录复制一份
template.php.bak。核心文件被改坏,最坏情况是整站白屏,一个.bak是唯一的退路。 - 替换。 只替掉那一段,别顺手格式化整个文件——你没法保证对方的缩进和编码跟你一致。
- 重启进程,把 OPcache 清掉。 这一步最容易省,也最容易踩坑。
第 4 步值得单独说一句:PHP 会把编译好的字节码留在 OPcache 里,只改文件不重启,curl 出来的还是老行为。 你很可能会把它误判成「补丁无效」,然后回头去怀疑那几行代码本身。1Panel 上是在容器列表里重启 emlog 容器;宝塔那边对应的动作是重启 PHP-FPM。
另外,两台机器我不会同时动手:先在一台打完、复测通过,再去改第二台。这样万一补丁本身有问题,坏掉的也只有一台,另一台还能留着当对照。
第 1 步的「仿真」,具体就是先算账再上线。拿 502 那条补丁说,三种场景的期望是这样:
| 场景 | 旧逻辑 | 打补丁后 |
|---|---|---|
| 首次访问,首页一次渲染 | 200 条 | 1 条 |
| 第二次访问(Cookie 已是该主题) | 200 条 | 0 条 |
| 从 stationery 切到 memo | 200 条 | 1 条 |
三行对应三种语义:该发的时候恰好发一条,不该发的时候一条不发,切换记忆还在。这就是验收标准。
一条命令,跑遍所有站
同步能不能落地,关键看「复测」这一步能不能一台一台地重复。我用的脚本只有几行:
for u in "/?theme=lcbk-stationery" "/essay/1.html?theme=lcbk-stationery"; do
printf "%-42s " "$u"
curl -s -m 25 -D - -o /dev/null "https://你的域名$u" \
| awk 'BEGIN{c=0} /^HTTP/{s=$2} /^[Ss]et-[Cc]ookie: emlog_theme/{c++} END{printf "status=%s cookies=%s\n",s,c}'
done期望两行都是 status=200 cookies=1,第二次访问起
cookies=0。把域名换掉,逐站跑一遍,谁没打补丁立刻现形。
顺手学到的另一个经验:复测不要只看目标那一条
URL,老链接也要扫一遍。 我打完补丁后回归过
/、/essay/22.html、/page/2、/essay、/guestbook.html、/rss.php,全部
200,正文里没有 Warning: 或 Fatal error:
泄漏——这才说明那几行改得干净。
升级后的回归清单
emlog 后台升级会覆盖核心文件,所以每次升级之后,固定做这四件事:
- 跑自查命令。 数一次 Set-Cookie 条数:结果是 0 或 1,说明补丁还在;几十上百,说明被冲掉了。命令和上面复测那条一样。
- 扫关键页面。 首页、文章页、分页、归档、留言页、订阅源,确认都是 200。
- 查
.bak是否还在。 升级有时会连备份一起清掉,没有.bak的时候再动手风险更高。 - 更新台账。 重打之后,把这一行的日期和结果补上。
四件事加起来不到五分钟,但它把「补丁是否还在」从一道记忆题,变成了一道执行题。清单要放在随手能翻到的地方——升级常常是深夜顺手点的,那个时间点没人想得起还有四件事要做。
让补丁不再需要手动重打
手动重打终归是权宜。往远看有三个方向,按成本从低到高:
- 先报上游。 这不是主题的问题,是 emlog
核心的问题:
getCurrentTemplate()是一次「读取」,而读取不该带写 Cookie 的副作用。复现成本是一条链接,影响面是所有用预览功能的站——这种 bug 进下个版本的机会不小。 - 再考虑外置。 如果上游不动,能不能把改动做成配置或插件,而不是改核心文件?至少升级时不会被覆盖。
- 最后是流程化。 把「打补丁 → 复测 → 记账」这套固定下来。哪怕补丁仍要手动打,至少不会再出现「两台机只有一台打了」。
那条为 502 加在 OpenResty 上的
proxy_buffer_size,我留着当保险不动。它治的是症状,补丁治的是病因,两层各留各的。
带走几条
- 改核心文件之前先建台账。 六个字段就够,重点是有「复测命令」和「打在哪些站」这两列。
- 打完不是成功,复测通过才算。 一条命令数 Set-Cookie,比分十次「感觉好了」都可信。
- 改完记得重启清 OPcache。 否则你会把「没生效」误判成「补丁错了」。
- 多站同步的敌人是沉默。 第二台不会报错,它只会安安静静地把重复头发给每一个访客——只有主动去数,才能发现它。
- 给核心打的补丁都有保质期,配一行自查命令,同时往上游推。
一份核心、两台机器本身不是问题;问题是你以为只有一台。