前一篇文章刚解决 A Tour of Go 多语言项目的 Production 公网验收稳定性问题:把大量公网 HTTP 检查从本地跨境 SOCKS 链路迁到了 zgocloud direct。
原以为网络问题解决以后,意大利语站点的首次 Production 应该可以顺利收尾。
结果 machine acceptance 全部通过后,又卡在了下一关:
[production-browser] FAILED: page did not render而且第一次失败的是首页:
https://it-go-dev.shuijingwanwq.com/再次执行以后,首页通过了,却又失败在:
https://it-go-dev.shuijingwanwq.com/tour/更奇怪的是,同一台电脑直接使用 curl 连续访问这些页面,20 次请求全部成功,而且都不到 1 秒。
最终定位下来,这次和前面的跨境公网网络问题其实是两个完全独立的问题。
真正需要调整的是 Headless Chrome 的:
render readiness 判定方式。
一、Machine Acceptance 已经 PASS,Browser Acceptance 却失败
正式 Production 流程里,machine acceptance 和 browser acceptance 是两个独立阶段。
前者主要验证:
public routes
HTML identity
sitemap
socket boundary
CDN cache后者则会真正启动 Headless Chrome,通过 Chrome DevTools Protocol 检查页面渲染、桌面端和移动端、编辑器、Run / Format / Reset、SPA、广告等浏览器行为。
第一次进入 browser acceptance 时,公网 machine gate 已经成功:
PRODUCTION MACHINE ACCEPTANCE: PASS
[首次生产] 公网验收:PASS(71.1s)但是紧接着:
[production-browser] FAILED: page did not render:
https://it-go-dev.shuijingwanwq.com/整个首次 Production 因此停止在:
stage: browser我随后正式 resume。
第二次 machine acceptance 依然通过:
PRODUCTION MACHINE ACCEPTANCE: PASS
[首次生产] 公网验收:PASS(49.1s)这次首页似乎正常了,但错误移动到了 /tour/:
[production-browser] FAILED: page did not render:
https://it-go-dev.shuijingwanwq.com/tour/
/ 和 /tour/ 判定页面没有完成渲染这个现象让我开始怀疑:
真的是页面访问失败了吗?
如果 Production 本身有持续故障,失败位置通常应该更加稳定。
但现在却是:
第一次:/
第二次:/tour/看起来更像是 browser acceptance 自己的某个瞬态判断出了问题。
二、先确认是不是普通公网访问真的不稳定
前一篇刚刚处理完跨境网络问题,所以这里最容易犯的错误,就是把新的 browser failure 继续归因到:
zgocloud
Cloudflare
跨境网络但这次必须先把实际链路分清楚。
Machine acceptance 的公网部分现在是:
本地维护机
↓ SSH
zgocloud
↓
Cloudflare
↓
Production而 browser acceptance 并不是在 zgocloud 上运行。
Headless Chrome 实际运行在我的本地 ThinkPad:
ThinkPad
↓
Cloudflare
↓
Production所以:
browser acceptance 失败,不能直接说明 zgocloud 有问题。
为了判断是不是本地公网链路本身不稳定,我直接从同一台 ThinkPad 连续测试失败过的两个 URL。
首页:
https://it-go-dev.shuijingwanwq.com/连续请求 10 次。
/tour/:
https://it-go-dev.shuijingwanwq.com/tour/同样连续请求 10 次。
结果:
20 / 20 HTTP 200而且总耗时大多只有:
约 0.56 ~ 0.84 秒
/ 和 /tour/,连续 20 次全部返回 HTTP 200这不能证明网络永远不会出现瞬态问题。
但它至少提供了一个非常重要的判断依据:
当前普通 HTTP 公网访问没有表现出能够解释连续 browser failure 的异常。
因此没有必要再次修改 zgocloud、Cloudflare 或 Production 网络架构。
问题应该继续往 Headless Chrome 自身的渲染判定里查。
三、旧的 render readiness 判断过于严格
继续检查 browser acceptance 的实现以后,发现旧逻辑在导航到页面以后,会等待页面满足类似这样的条件:
document.readyState == "complete"
+
body 已经出现足够的文本内容等待时间最长大约 30 秒。
乍一看,这个判断似乎很合理:
页面只有进入
complete,才算加载完成。
但对于真实网页来说,complete 并不一定是最适合判断“页面已经可以验收”的条件。
浏览器通常会经历:
loading
↓
interactive
↓
complete当状态进入:
interactive时,DOM 已经完成解析,页面主体通常已经可以正常交互。
但:
complete还需要等待更多外部资源完成。
而真实 Production 页面并不只有自己的 HTML、CSS 和 JavaScript。
还可能包含:
统计脚本
广告脚本
第三方资源
网络请求这些资源的加载状态,并不一定影响课程页面本身是否已经正确渲染。
于是就可能出现一种情况:
页面主体已经出现
用户实际上已经可以使用
↓
某个外围资源还没有结束
↓
readyState 迟迟没有满足旧条件
↓
browser acceptance 判定:
page did not render这就产生了 false failure。
四、为什么失败页面还会变化
这也解释了一个此前看起来比较奇怪的现象。
第一次:
/失败。
第二次:
/通过了,但是:
/tour/又失败。
如果页面本身存在确定性的 HTML 或 JavaScript 错误,那么通常应该可以稳定复现到同一个 route。
但是,如果问题来自:
页面已经基本渲染
+
外围资源完成时间存在波动
+
readiness 条件要求 complete那么某一次 navigation 超时发生在哪个页面,就可能具有偶然性。
这与实际观察到的现象更加吻合。
五、不能简单把 30 秒 timeout 调成 60 秒
发现这个问题以后,最简单的修复办法似乎是:
30 秒
→
60 秒甚至:
120 秒但这样实际上没有解决判断标准本身的问题。
如果一个页面在:
interactive状态下:
- DOM 已经存在
- 页面正文已经出现
- 后续语义检查也能够执行
那么为了等待某个非关键资源,把整个 browser acceptance 再挂几十秒并没有太大价值。
反过来,如果页面真的没有渲染出来,只是一味延长 timeout,同样只是让失败变得更慢。
所以这次没有简单增加总等待时间。
六、新的策略:interactive 或 complete + 实际页面内容
最终调整后的 readiness 判断,不再只接受:
readyState == complete而是允许:
interactive
或
complete同时仍然要求:
页面 body 已经出现有效文本也就是说:
不是只看浏览器状态字符串,而是把浏览器生命周期状态和实际页面内容结合起来。
逻辑大致变成:
readyState ∈ {interactive, complete}
+
bodyTextLength > 最低阈值这样可以避免一种错误:
DOM 已经正常
页面也已经显示
只是外围资源尚未全部结束
↓
却被判定为 page did not render与此同时,也不能只检查:
interactive否则一个几乎空白的页面也可能被误认为正常。
所以 body 内容检查仍然保留。
七、一次长等待,改成 3 次独立 navigation
这次还有另一个调整。
旧逻辑更接近:
一次 navigation
↓
最长等待约 30 秒
↓
成功 / 失败现在改成:
navigation 1
最多等待约 10 秒
↓
如果 render readiness 未满足
navigation 2
最多等待约 10 秒
↓
仍未满足
navigation 3
最多等待约 10 秒
↓
仍然失败才 fail closed也就是最多 3 次独立 navigation。
这里很重要的一点是:
不是单纯把一次 navigation 无限重试。
总体 render readiness 时间预算仍然大致保持在原来的量级。
区别在于,如果某一次 navigation 恰好遇到浏览器或资源加载的瞬态状态,可以重新建立一次干净的页面导航。
相对于:
同一次异常 navigation 挂满 30 秒这种方式对 transient render failure 更友好。
八、但真正的语义错误不能重试到通过
增加 retry 很容易带来另一个风险:
如果一个真正的浏览器验收错误多试几次,偶然绕过去怎么办?
所以这次对重试范围进行了严格限制。
允许 retry 的只有:
render readiness也就是:
页面到底有没有进入可以继续验收的状态一旦页面 readiness 已经满足,后续正式的语义检查仍然是:
单次
+
fail closed例如:
canonical 是否正确
html lang 是否正确
课程内容是否正确
Run 是否工作
Format 是否工作
Reset 是否工作
SPA 是否正常
广告 mount 是否正常这些检查如果失败,就是真的 Production browser acceptance failure。
不能通过刷新 3 次、5 次把它“刷成 PASS”。
这也是这次修改里我比较看重的边界:
只给真正可能瞬态的部分重试,不给语义错误重试。
九、修改以后,同一份 Production release 正式通过
完成调整后,我没有重新 publish,也没有重新 deploy。
继续使用之前已经部署完成的正式 it-IT release,再次 resume first-production。
Machine acceptance:
PRODUCTION MACHINE ACCEPTANCE: PASS
[首次生产] 公网验收:PASS(31.1s)随后 Browser Acceptance:
[production-browser] desktop routes: PASS
[production-browser] mobile /tour/moretypes/1: PASS
[production-browser] Run / Format / Reset / SPA / ads: PASS
PRODUCTION BROWSER ACCEPTANCE: PASS
[首次生产] 浏览器验收:PASS(41.4s)最后流程正式推进到:
[首次生产] READY FOR HUMAN VISUAL GATE
这张结果也证明:
这次修改并不是为了跳过 browser acceptance。
后面的:
desktop routes
mobile
Run
Format
Reset
SPA
ads依然全部实际执行并通过。
只是修复了最前面的:
“页面是否已经可以开始验收”这个判断。
十、这次和前一篇网络问题其实完全不同
这两次问题刚好连续发生,很容易把它们混在一起。
但实际链路完全不同。
前一篇的问题是:
本地
↓
跨境 SOCKS
↓
zgocloud
↓
Cloudflare大量公网请求经过一条没必要存在的跨境链路,出现:
curl 28
curl 97最终解决方式是:
zgocloud direct而这次:
Machine Acceptance: PASS
↓
本地 Headless Chrome
↓
page did not render普通 curl 又:
20 / 20 PASS真正需要调整的是:
Headless Chrome render readiness所以同样是:
Production FAILED背后的正确回流路径却完全不同。
前一个应该处理:
network execution point后一个应该处理:
browser readiness semantics这也是为什么我越来越不希望看到 FAILED 就直接:
重新部署
重新执行
增加 timeout
增加 retry首先还是应该看:
stage
+
evidence十一、自动化验收最难的不是“严格”,而是“严格得合理”
自动化 Production gate 很容易走向两个极端。
一种是太宽松:
有问题也 PASS这显然没有意义。
另一种则是:
检查条件越严格越安全但实际上也未必。
例如:
必须 readyState == complete看起来比:
interactive 或 complete更加严格。
但如果这个额外限制和真正需要验证的 Production 语义没有直接关系,那么它带来的可能不是更高的安全性,而是:
更多 false failure真正合理的标准应该是:
页面是否已经达到执行后续正式语义验收所需要的状态。
而不是:
所有可能的外围资源是不是已经全部完成。
这两个目标并不完全相同。
十二、总结
这次问题最开始看起来很奇怪:
Machine Acceptance: PASS但是:
Browser Acceptance: FAILED
page did not render再次执行以后,失败页面甚至从:
/移动到了:
/tour/而同一台电脑直接执行普通 HTTP 请求:
20 / 20 HTTP 200最后发现,真正的问题不在 Production 页面本身,也不在前一篇刚刚调整过的 zgocloud 网络路径。
而在于:
Headless Chrome render readiness原来的判定过于依赖:
document.readyState == complete现在则调整成:
interactive 或 complete
+
有效 body 内容
+
最多 3 次独立 navigation同时继续坚持:
正式语义检查
=
单次
+
fail closed这样既降低了瞬态浏览器状态造成的 false failure,也没有降低 Production Browser Acceptance 本身的标准。
这次给我留下最深的印象其实是:
一个好的自动化 gate,不只是要能够发现错误,也要尽量准确地区分真正的错误和验收工具自己的瞬态失败。
否则验收工具本身,也会成为 Production 流程里新的不稳定因素。
A Tour of Go 多语言翻译项目
本系列完整记录 A Tour of Go 多语言翻译项目从架构设计、整页翻译、结构保护、自动校验,到生产发布与后续维护的实际开发过程。
项目入口:
✅ 简体中文:A Tour of Go 简体中文版
✅ 日语:A Tour of Go 日语版
✅ 德语:A Tour of Go 德语版
✅ 法语:A Tour of Go 法语版
✅ 项目源码:GitHub:shuijingwan/go-tour-i18n
当前已上线简体中文、日语、德语和法语版本。项目属于非官方社区多语言翻译项目,与 Go 官方无隶属或授权关系。
