速读#
我以前把一次请求画成 DNS、Cloudflare、Tunnel、Nginx、后端、数据库再返回的固定七站,既容易让人误以为是 OSI 七层,也不符合当前站点。迁移到 Astro 后,普通文章是构建期生成的静态 HTML,正常请求不会经过业务后端和数据库;如果 Cloudflare 已经缓存了资源,甚至不会到达源站。只有 OAuth、部署 webhook 等少数路径会由 Nginx 转给本机 Node 服务。
把真实链路画清楚后,排障不再是把所有技术都检查一遍,而是先确认这次请求走了哪个分支:边缘缓存、静态源站、OAuth,还是部署接口。请求链解决“用户怎样读到内容”,发布链解决“新内容怎样进入当前目录”,两条链有关联,却不能混成一条。
当前公开页面的真实路径#
用户访问 https://s1oopx.bond/posts/... 时,正常路径可以写成:
浏览器 -> DNS 返回 Cloudflare 边缘地址 -> Cloudflare 边缘终止 HTTPS,并执行已配置的缓存/WAF 规则 -> 命中缓存:直接返回,不访问源站 -> 未命中或不可缓存:沿 Tunnel 转发 -> cloudflared 的出站连接 -> 127.0.0.1:80 上的 Nginx -> /var/www/astro-narrow/current 中的静态 HTML、CSS、JS 或图片 -> 响应沿各段连接返回浏览器这里没有一个贯穿浏览器到 Nginx 的单一 TCP 连接。浏览器连接 Cloudflare,Cloudflare 再通过 Tunnel 把请求送到源站,各段分别建立连接;“沿原路返回”只是逻辑方向相反,不代表端到端连接没有被终止。TLS 对访客在 Cloudflare 边缘终止,Tunnel 自身再保护边缘到 cloudflared 的传输,Nginx 因此可以只监听本机回环 HTTP,而不公开 Web 入站端口。
各组件的职责也应准确分开:
| 位置 | 负责什么 | 不负责什么 |
|---|---|---|
| DNS | 把主机名解析到 Cloudflare 边缘 | 不证明源站 IP 永远无法被其他记录或历史发现 |
| Cloudflare 边缘 | 访客 TLS、缓存、WAF 与边缘路由 | 不保证源站应用和静态文件一定正常 |
| Tunnel | 让源站主动建立出站通道 | 不替代访问授权、进程守护和应用健康检查 |
| Nginx | 按主机名与路径提供文件或转发到本机服务 | 不生成 Astro 页面,也不保存文章数据 |
| 发布目录 | 保存某个已构建版本的静态产物 | 不负责自动构建、切换和回滚 |
当前 Nginx 的 root 指向 /var/www/astro-narrow/current,这个路径是发布脚本切换的当前版本。/_astro/ 下带内容哈希的资源返回长期不可变缓存头,普通页面则按实际响应策略由 Cloudflare 判断。因为站点是静态生成,文章内容来自构建时的 Markdown,而不是请求时查询 MySQL 或 SQLite。
同一个域名下还有几条动态分支#
“静态站”不等于域名下没有服务端代码。管理后台页面本身是静态文件,但登录入口 /oauth 会由 Nginx 转发到 127.0.0.1:4180 的 OAuth 服务;它生成 GitHub 授权请求、校验一次性 state、换取用户信息并限制允许账号。/deploy/github 则转发到 127.0.0.1:4181 的 webhook 服务,由它对 GitHub 原始请求体验签、检查事件和目标提交,再触发受控部署。
因此不同路径的依赖不同:
| 请求 | 源站分支 | 额外依赖 |
|---|---|---|
/posts/...、/_astro/... | Nginx 读取静态文件 | 当前发布目录和磁盘 |
/admin/index.html | Nginx 读取静态 CMS 页面 | Cloudflare Access 策略;登录后还依赖 GitHub |
/oauth、/oauth/callback | Nginx → OAuth Node 服务 | GitHub OAuth、环境变量和允许名单 |
/deploy/github | Nginx → webhook Node 服务 | GitHub 签名、部署脚本、Git 与构建环境 |
/healthz、/health.json | Nginx 读取最小健康文件 | 当前发布目录;health.json 还记录构建 revision |
看到主页正常,不能推出 OAuth 服务正常;反过来,OAuth 挂掉也不应该影响读文章。路径拆分的价值就在这里:一个故障只影响它实际依赖的分支,排查也应从具体 URL 开始。
缓存命中会让源站完全看不到请求#
排障时最容易漏掉的分支是边缘缓存。如果某个静态资源返回 CF-Cache-Status: HIT,Cloudflare 可以直接回答,Nginx 访问日志和源站进程都不会出现这次请求。反过来,MISS、DYNAMIC 或绕过缓存时,请求才可能继续走 Tunnel;具体值要结合 Cache Rules、响应头和资源类型解释,不能把所有非 HIT 都当成故障。
从外部先检查响应链:
curl -I https://s1oopx.bond/curl -I https://s1oopx.bond/_astro/<实际资源文件>重点看状态码、重定向、Cache-Control、Age、CF-Cache-Status 和 Cloudflare Ray ID。若怀疑缓存掩盖源站问题,可以请求一个确定不缓存的健康地址,或按受控方式清理单个 URL;不应在没有证据时全站 purge,因为那会让所有资源同时回源,反而制造新的负载。
我现在怎样按层收缩故障#
“网站打不开”先从外部和源站两端取证,而不是按固定顺序重启所有服务:
| 检查目标 | 命令或动作 | 能说明什么 |
|---|---|---|
| DNS | Resolve-DnsName s1oopx.bond 或 dig s1oopx.bond | 域名是否返回预期边缘地址 |
| 公网 HTTP | curl -I https://s1oopx.bond/ | 边缘是否能返回状态码、是否发生重定向或缓存 |
| Tunnel | systemctl status cloudflared、查看对应 journal | 源站到 Cloudflare 的出站通道是否在线 |
| Nginx 与静态文件 | curl -I -H 'Host: s1oopx.bond' http://127.0.0.1/ | 绕过边缘后,本机入口与当前发布目录是否正常 |
| 当前版本 | readlink -f /var/www/astro-narrow/current | Nginx 实际服务的是哪个 release |
| OAuth | curl -fsS http://127.0.0.1:4180/healthz | OAuth 进程是否在本机响应 |
| Webhook | curl -fsS http://127.0.0.1:4181/healthz | 部署 webhook 进程是否在本机响应 |
如果公网失败而本机 Nginx 正常,范围在 DNS、Cloudflare 规则、Tunnel 或主机名路由;如果本机 Nginx 也失败,再看 Nginx 配置、current 链接、文件权限和发布目录。某个动态路径失败时,只检查对应 Node 服务和外部依赖,不因为 OAuth 失败就怀疑静态文章数据库——当前架构根本没有这一步。
仓库还保留了一条公开侧烟雾检查,可以在部署完成后从外部验证主页、后台边界、OAuth、无签名 webhook 和健康接口:
SMOKE_BASE_URL=https://s1oopx.bond pnpm production:smoke自动检查不能替代日志,但能证明几个关键分支至少表现出预期状态,比只在服务器本机执行 curl localhost 更接近用户视角。
发布链与请求链不要混在一起#
用户请求静态页面时不会运行 Astro 构建。新提交先经过 GitHub 工作流验证,符合条件的 workflow_run webhook 到达部署服务,部署脚本拉取指定提交、安装依赖、构建到新的 release 目录、执行检查,再原子切换 current 链接;失败时保持旧版本或执行回滚。Nginx 只是继续读取 current 指向的文件,不知道背后发生过多少构建步骤。
Git push -> GitHub Actions 验证 -> 已签名 webhook -> 本机部署服务 -> 新 release 构建与检查 -> current 链接切换 -> 后续请求读到新文件把两条链分开后,问题也更容易命名。页面返回旧内容,可能是边缘缓存或 current 没切换;构建成功但公网 404,可能是产物路径、Nginx try_files 或 base 配置;部署 webhook 不工作,不代表当前线上静态文件会立即消失。每个症状都对应有限的观察点。
这张图还暴露了什么缺口#
当前站点仍是单源站、单 VPS。Cloudflare 边缘有分布式能力,不能等同于源站高可用;主机损坏后仍需要靠代码仓库、环境配置、备份和部署文档重建。这个规模下我不急着增加多区域源站和数据库集群,更优先验证发布回滚、服务代码复制、环境文件权限、异地备份和恢复步骤。
系统理解也因此从“我学过 DNS、Nginx、Docker 和数据库”变成了“我知道当前一个具体请求实际经过哪些组件,以及哪些组件根本不会参与”。链路图的价值不是层数多,而是每条箭头都能用一次请求、一个日志或一条命令验证;架构变化后,图也必须跟着改,不能让旧经验继续冒充现状。
