上线门禁检查(Release Gate)
何时用
- 任何对外上线前:站点/页面、技能包、专家包、资产索引、API 端点、小程序版本、对外文档。
- 用户说"上线/发布/部署前检查一下"、"跑个门禁"、"有没有死链"、"压一下试试"。
- 事故复盘后:把新教训补成检查项 + 案例。
铁律(不可协商)
- 有任一 FAIL → 禁止上线。 不许"先上后修"。
- 失败项必须复测 3 次才可定 FAIL;偶发失败记 WARN(疑 CDN 抖动)。禁一次判死。
- 页码能打开 ≠ 功能能用。 CTA / 表单 / 第三方表单(kdocs、飞书、typeform、金数据…)必须逐个实测状态码。
- 上线后必须复验,报告落盘留痕,作为"已实跑"的证据。
- 测试工具自身也要被证伪。 结果出来先问:"这条是真的吗?会不会是我解析错了?"
- 🔴 部署前必查"基地是否真的更新":本地工作副本可能远落后于线上(实测差过 23 倍)。覆盖式上传会摧毁线上内容。
- 规则:一律以「线上当前版本」为基底做增量合并,绝不用本地旧副本整文件覆盖。
- 落地:部署前先
md5(线上) vs md5(本地)逐文件比对;不一致就拉线上当基底。 - 参考实现:
D:/Workbuddy/10-内容发布/uibc_entry_fix_20260914/deploy_safe.py(默认 dry-run,带 backup,可--upload)。
用法
# 全量六组
python D:/Workbuddy/05-工具/release_gate/release_gate_check.py --site https://medxpert.cn
# 只跑关键组(上线后复验 / 快速)
python ... --site <URL> --only gate,links,drift --file <本地待发文件>
# 只体检,不出报告
python ... --site <URL> --only net,seo
参数:--limit(爬页上限,默认 60)--concurrency(默认 8)--stress(压测次数,默认 20)--outdir(默认脚本同级 reports/)。
六组检查项
| 组 | 查什么 | 判定 |
|---|---|---|
| gate 功能链路(一票否决) | 全部 CTA / 表单 / 外呼表单逐个实测;#锚点须存在同页目标 id | 任一断链 = FAIL |
| net | 关键页可达 / 体积 / 耗时 | 非 200 = FAIL |
| links | sitemap+首页爬取,站内链接逐条实测,带来源页溯源 | 复测 3 次全败 = FAIL |
| seo | robots / sitemap / llms.txt / llms-full.txt / JSON-LD 合法性 / title / description / canonical / hreflang | 核心缺失 = FAIL |
| stress | 并发请求 p50 / p95 / 失败率 | 失败>0 或 p95>3000ms |
| drift | 本地文件 vs 线上文件逐字比对 | 不一致 = FAIL(改了没上线/被回退) |
坑(实测踩过,务必避开)
-
必须剥离
<script>/<style>再提取链接。页面 JS 里常见var src = "...",会被(?:href|src)\s*=误当成 HTML 属性 → 产生大量"幽灵死链"。 -
页内锚点
#xxx不是死链,只有当同页不存在对应id时才是。 -
别用
[^<\s]+提取<loc>:会永久漏检含空格的 URL(探测方法自带盲区)。用<loc>(.*?)</loc>+ strip。 -
.不跨行:.{70}关键词.{70}这类上下文正则,遇跨行会静默漏检。要探测就用属性级正则。 -
本机外呼必先清 proxy 环境变量(
HTTP_PROXY/HTTPS_PROXY/ALL_PROXY…),否则被代理劫持报错。 -
Windows 上 git 可能不在 PATH;
gh配置在%APPDATA%\GitHub CLI\hosts.yml(不是~/.config/gh)。 -
🔴 门禁默认不覆盖静态资源(图片/CSS/JS/字体):
links组只扫 HTML 的href链接。必须单独跑资源扫描: 提取(?:src|href)="...\.(png|jpe?g|svg|webp|gif|ico|css|js|woff2?|ttf)"→ 逐条实测 → 漏检过/assets/qrcode.png与/skills/assets/logo.png。 -
🔴 子目录页面必须用绝对路径:从根目录复制页面到子目录(如
/skills/index.html)时,相对路径(assets/logo.png、about.html、llms.txt)会解析到子目录下 → 整站导航全 404。- 实测:
/skills/页面 10 个导航链接 8 个 404,且页面本身 200,肉眼完全看不出来。 - 检查法:对每个子目录页,把其相对链接按
urljoin(页面URL, 链接)解析后实测;并对比urljoin(根URL, 链接)是否 200(是则说明"本该走根路径")。 - 修法:把该页
href/src中的相对路径改写为根绝对路径(/assets/...、/about.html),保留 fragment。 - 注意 canonical:复制页通常已
canonical指向主版本,修路径时不要动它。
- 实测:
-
🔴 部署前必查"源目录 ≠ stage 目录":新文件写在源目录,上传却走 stage 目录 → 漏同步 → 上传失败/404,而你以为已上线。
- 落地:上传前先出一份"stage vs 源目录差异清单"(文件名 + 大小 + md5),差异项要么补齐、要么确认是有意排除。
- 实测:
apply.html/agent/index.html写进源目录却未进 stage,上传连续失败两次。
-
本机 bash 可能缺 coreutils(实测
tail/dirname/sleep全部command not found):不要依赖 shell 管道与延时,改走 Python(subprocess.run([...], capture_output=True)+time.sleep())。- 症状:命令
exit 127、stdout 为空、stderr 只有 shim 报错 → 换 Python 立即恢复。
- 症状:命令
-
🔴 不要同时跑两个网络密集测试(如门禁 + 巡检并行):二者会互相抢连接,制造大量假"不可达"。
- 实测:门禁与巡检并行时,门禁报 10 条"页面不可达/握手超时";随后串行复测 10/10 全部 200。
- 规则:网络测试一次只跑一个;跑完再跑下一个。若必须并行,结果只能当参考,不能当判定。
-
🔴 并发爬取本身会产生假"不可达":
ThreadPoolExecutor并发拉 60 页时,握手超时会直接把页面判为不可达。- 正确做法:先并发收集失败项 → 再串行复测(
fetch_retry()),只有串行复测仍失败才判 FAIL。 - 已固化:
release_gate_check.py的check_net()与links组页面可达性均已内置复测;uibc_daily_check.py的check()同样内置。 - 自查提醒:立了"失败必复测"的规矩,就要检查自己的每个工具是否都遵守——实测曾发现巡检脚本一次判死,产生假阳性 FAIL(
robots.txt//uibc/prize/)。
- 正确做法:先并发收集失败项 → 再串行复测(
-
🔴 验证 DNS 切换必须避开本机缓存(2026-09-15 实测踩过)
- 现象:切
www到 CDN 后 5 小时,本机https://www.medxpert.cn/仍 403;而223.5.5.5/119.29.29.29/8.8.8.8三个公共解析器全都已是 CDN 边缘 IP。 - 误导性:403 恰是那条已被删除的旧记录(
cos.无-website端点)的典型返回码 → 极易误判成"切换失败/源站配错"。 - 根因:本机解析器缓存(TTL 600s)。执行
ipconfig /flushdns后立即恢复 200。 - 正确验证顺序(不要跳步):
- 查 CDN 侧状态:
tccli cdn DescribeDomains→Status应为online,且Origin指向正确端点 - 查公共 DNS(
nslookup <域名> 223.5.5.5)—— 这是"是否已生效"的权威依据,不要用本机解析判断 - 绕过 DNS 直连 CDN:连
Host头 + 不校验证书 → 用于区分"CDN 配置问题"与"DNS 问题" - 最后才测本机域名访问;若与第 2/3 步结论矛盾 → 先
ipconfig /flushdns再下结论
- 查 CDN 侧状态:
- 判据:响应含
X-NWS-LOG-UUID= 走腾讯云 CDN;没有它才是没走 CDN(Server: tencent-cos两者都有,不能单看)。
- 现象:切
-
🔴
coscmd upload会重置对象元数据(Cache-Control 丢失)(2026-09-15 实测)- 现象:上传
uibc/index.html、uibc/en/index.html、uibc/llms.txt、sitemap.xml后,Cache-Control全部变回「(无)」 —— 之前批量设过的值被静默清掉。 - 后果:CDN/浏览器失去缓存策略 → 回到"每次回源"或"按默认策略长期缓存"两种极端(后者会导致部分节点长期返回旧版)。
- 规则:凡用 coscmd 上传过的文件,必须重设 Cache-Control。
- 工具:
05-工具/cos_setmeta.py --set "/path=public, max-age=600"(按路径精确设;支持--get;幂等;内容字节不变,仅改元数据)。
- 现象:上传
-
🔴 CDN 路径刷新「done」≠ 生效(2026-09-15 实测,最反直觉的一条)
- 现象:
tccli cdn PurgePathCache提交 4 次(flush ×3 + delete ×1)全部返回status: done,但/sitemap.xml的缓存对象Age持续增长(1690→2077s),内容仍是 2 小时前的旧版。 - 已逐项排除:源站正确(加
?probe=1换新 key → 立刻返回新版);响应确实走 CDN(含X-NWS-LOG-UUID);任务日志逐条done。 - → 结论:路径刷新对个别对象存在不生效且不报错的情况。别把
done当生效凭证。 - 规避:更新型文件(sitemap / llms.txt / 各类索引)设**短 max-age(≤300s)**兜底,把刷新只当"加速手段"而非"唯一手段"。
- 该信的凭证:
Age是否归零、Last-Modified是否为新值。二者不变 = 没生效。
- 现象:
-
🔴 测量方法学:每次新建 TLS 连接会伪造出 14 倍的"慢"(2026-09-15 实测)
- 反例:
urllib.urlopen(每次新建连接)+ 并发 8 测本站 → p50 4080ms / 0.2rps。 - 正解:
http.client.HTTPSConnectionkeep-alive 复用 + 压测前预热 N 次并丢弃样本 → p50 291ms / 13.1rps。 - 差 14 倍,全部来自"握手成本被算进服务延迟"。
- 交叉验证法(必做):同刻对第三方站点(如
bing.com)跑同一方法 → 若第三方 87.9rps / p50 116ms,则本机出口没问题,瓶颈在测量方式而非被测站点。 - 落地:
05-工具/uibc_health_page.py的Conn类 +stress(warmup=3);顺序稳态与并发压测分成两个独立指标报告,不混为一个 p50。
- 反例:
-
🔴 测试 URL 带
?v=<时间戳>会绕过 CDN 缓存 → 缓存类问题永远测不出(2026-09-15 实测)- 背景:为防误报,巡检/门禁习惯给 URL 加时间戳参数(本身是好习惯,用于判断"服务是否活着")。
- 盲区:返回旧版、多节点内容不一致这类问题只有裸 URL 才能暴露。
- 实测:巡检全绿(带
?v=),但裸 URL/sitemap.xml仍是 2 小时前旧版。 - 规则:可用性用带参 URL;内容/缓存一致性必须用裸 URL。两套都要跑,缺一套就有盲区。
-
🔴 GraphQL 字符串不允许裸换行(2026-09-15 发 GitHub Discussions 踩到)
- 把多行 Markdown 直接塞进
mutation{createDiscussion(...body:"<多行>")}→ 报Expected string or block string, but it was malformed。 - 修:转义顺序 先
\→ 再"→ 最后\n/\t(\n→\\n、\t→\\t、去掉\r)。 - 附带:本机直连
github.com会TimeoutError WinError 10060,走 API(ghx.py api ...)正常 → 验证一律用 API,别用浏览器访问判断成败。 - 另:
tccli cdn的域名配置接口是DescribeDomainsConfig(复数),参数只有--Filters/--Limit/--Offset/--Sort(--Domain/--Domains都是 Unknown option)。 - 缓存层反直觉实测:源站
max-age=3600,CDN 上Age=23015s(≈6.4h)仍是Cache Hit→ 不能假设 CDN 会按源站 max-age 过期;判断真实生效只看Age是否归零 + 内容是否为新。
- 把多行 Markdown 直接塞进
性能根因诊断("三头一拆",5 分钟定位托管层问题)
页面全 200 但压测 p95 爆炸时,别急着改页面,先拆响应头:
| 看什么 | 怎么判 | 缺失意味着 |
|---|---|---|
| Server | 直出 tencent-cos / nginx 且无 X-Cache / Via / X-NWS-* | 请求直达源站、没有 CDN 边缘缓存(通常是主因) |
| Content-Encoding | 请求带 Accept-Encoding: gzip, deflate, br,响应是否回 gzip/br | 未开压缩,文本资产原样传输 |
| Cache-Control | 是否有 max-age / immutable | 无法缓存,每次回源 |
| TTFB vs 总耗时 | urlopen 返回时计时 vs read() 完成后计时 | TTFB ≈ 总耗时 → 瓶颈在"到达源站",不在传输 |
验收目标(静态站):p50 < 300ms · p95 < 800ms · 吞吐 > 100 rps · Content-Encoding 有值 · Cache-Control 有值。
归属提醒 —— 🔴 先枚举本机能力,再断言"做不到"(实测踩坑:曾误判"CDN 需登控制台",实际三项全可自动化):
| 层 | 本机可用手段 | 实测结果 |
|---|---|---|
| COS · Cache-Control | qcloud_cos SDK copy_object(CopyStatus='Replaced') 批量改元数据(CacheControl= / 保留原 ContentType) | ✅ 361 对象 76 秒完成,内容 md5 前后一致;工具 05-工具/cos_set_cache_control.py(幂等/可回滚 --clear) |
| COS · 压缩 | COS 不支持动态压缩 → 必须交给 CDN | — |
| CDN · 加速/缓存/压缩/安全头 | tccli cdn AddCdnDomain / UpdateDomainConfig(pip install tccli,入口在 venv Scripts/tccli.exe,不是 python -m tccli) | 命令与前提已备好,待点头 |
| DNS · 切 CNAME | tccli dnspod(先 DescribeDomainList 确认域名是否在 DNSPod) | medxpert.cn 实测在 DNSPod |
枚举清单:
shutil.which(<cli>)+importlib.util.find_spec(<sdk>)+ 凭据文件(~/.cos.conf、%APPDATA%\GitHub CLI\hosts.yml、~/.tccli)+ 环境变量。 另:区分"客观失效"与"我不试" —— 例:gh api user返回 401 Bad credentials 是 token 客观失效(需人工重新授权),而tccli未安装只是"没装",pip install即可。
⚠️ 加 CSP 前务必清点页面内联脚本,否则白屏;建议先加 HSTS / nosniff / Referrer-Policy / frame-ancestors,完整 CSP 另立一项并在测试域名先验。
参考:D:/Workbuddy/10-内容发布/UIBC_站点性能诊断与修复建议_20260914.md · D:/Workbuddy/10-内容发布/UIBC_性能修复_已完成的COS层与待决的CDN层_20260915.md
上线流程(每次照做)
- 改了东西 → 先跑
links+gate本地/预发。 - 部署 → 跑全量门禁 → 报告落盘。
BLOCKED→ 修 → 重跑,直到PASS/PASS_WITH_WARN。- 上线后复验:
--only gate,links,drift --file <本地文件>。 - 任何线上事故 → 回填一条 CASE(事件 / 教训 / 已固化的规则)。
事故案例库(每次事故回填一条)
| CASE | 事件 | 教训 | 已固化的规则 |
|---|---|---|---|
| CASE-006 | medxpert.cn/assets/qrcode.png 404,被 ≥4 页引用(cases/faq/articles×2),导航区"扫码关注"破图;门禁没报 | 门禁只扫 HTML href,不扫 src 静态资源 | 坑 #7:上线前必跑资源扫描 |
| CASE-007 | /skills/ 页面 10 个导航链接 8 个 404(logo/about/products/services/knowledge/llms/sitemap/skills.html),页面本身 200,肉眼无感 | 页面从根目录复制到子目录,相对路径未改 | 坑 #8:子目录页必须用根绝对路径 |
| CASE-008 | 拟部署的本地副本 llms-full.txt 仅 13KB,而线上 301KB(差 23 倍);若整文件覆盖将摧毁线上内容 | 本地工作副本可能远落后于线上 | 铁律 #6:以线上为基底增量合并 + deploy_safe.py |
| CASE-009 | 新写的 apply.html / agent/index.html 只在源目录,未进 stage → 上传失败两次;误以为已上线 | 源目录 ≠ stage 目录 | 坑 #9:上传前出 stage 差异清单 |
| CASE-010 | 页面全 200,但压测 p95=10065ms、吞吐 ~0.9rps;robots.txt 偶发 18.6s。响应头显示 Server: tencent-cos、无 Content-Encoding、无 Cache-Control、TTFB≈总耗时 → 站点直连对象存储源站,无 CDN 边缘缓存 | "能打开" ≠ "扛得住";性能根因常在托管层 | 新增「性能根因诊断(三头一拆)」节 |
| CASE-011 | verify.html 声称"指纹重算,改过的证书会被拦下",但实现只调了台账接口 | 文案声称了实现没做的事 — 比缺功能更危险 | 上线前对齐"页面声称 vs 代码实际",声称以能实证为限 |
配套
- SOP:
D:/Workbuddy/03-方案文档/测试专家能力建设_上线门禁SOP_v1.0_20260914.md - 修复工具(表单/CTA 死链):
D:/Workbuddy/05-工具/site_lead_fix/fix_lead_links.py - 首选外部交叉验证器:linkinator(
npx linkinator <URL> --recurse,零成本本地跑)
Scan to join WeChat group