Repo .git was damaged (refs/ missing, index truncated, object store near-empty). Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak, then full worktree committed as a rollback baseline.
4.5 KiB
4.5 KiB
proxy-image 回源超时事故处置报告
- 日期:2026-09-02
- 影响:
/proxy-image拉取 R2 图片稳定超时 502(后台ProxyImage fetch failed);分享卡生成存在同根因隐患 - 状态:代码侧已修复并全量验证;待服务器部署验证
一、症状
ERRO[0013] ProxyImage fetch failed error="Get \"https://r2.337845818.best/tamabox/picture/...\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)"
2026-09-02 23:51:00 Logger: Completed method=GET path="/proxy-image?url=..." status=502 duration=10.57s
- 同一 URL 每次请求稳定 10.5s 超时(旧 client
Timeout: 10s+ 开销) - 用户在浏览器直接访问该 R2 链接正常
二、证据链(根因判定过程)
| 证据 | 结论 |
|---|---|
| 报错文本 "awaiting headers" | TCP+TLS 已完成、HTTP 已发出,等不到响应头(非 DNS/dial/TLS 失败) |
r2.337845818.best 解析结果 |
Cloudflare anycast:104.21.47.52 / 172.67.144.141 + 2 IPv6 → 多边缘 IP |
| 本地对照:走 https_proxy 代理 | ✅ 正常响应 |
| 本地对照:强制直连两个 CF IPv4 | ❌ code=000,TLS 未建立即断 → 裸连 CF 存在劣化边缘 |
| 服务器 curl 裸连 | ✅ 200 但耗时 6.46s → 链路"慢而通",非完全不通 |
服务器 env | grep -i proxy |
无代理 → 排除"程序绕过代理"假设 |
代码侧根因(与 curl 的行为差异):
route/proxy.go旧proxyImageDialContext解析后固定拨第一个 IP——覆盖了标准库的多 IP 容错。anycast 恰好抽中被劣化的边缘 → 稳定等满超时;curl 默认多 IP 尝试所以能成功。- 超时预算过紧:
Timeout: 10s(curl 成功都要 6.46s)、TLS 5s、读头阶段无独立兜底。
三、修复内容
1. 共享多 IP 并发拨号(新增)
internal/shared/multidial.go → DialFirstReachable(ctx, dialer, network, ips, port, maxCandidates)
- 对候选 IP 并发拨号取最先成功(happy-eyeballs 风格)
- IP 字面量直连、无二次解析 → DNS rebinding 免疫(SSRF 防护不降级)
- 成功路径取消其余拨号、关闭落选连接(防泄漏);空候选/ctx 取消/截断均有防御
2. /proxy-image(route/proxy.go)
| 项 | 旧 | 新 |
|---|---|---|
| DialContext | 固定单 IP | shared.DialFirstReachable(上限 6 并发) |
Client.Timeout |
10s | 25s |
TLSHandshakeTimeout |
5s | 8s |
ResponseHeaderTimeout |
无 | 8s(兜底"响应头迟到"黑洞) |
| 超时类错误 | 不重试 | 自动重试一次(fetchProxyImageWithRetry) |
3. 分享卡生成(route/question/share.go)——排查发现的同根因复发点
shareDialContext同款固定单 IP → 改调共享 helper- 超时放宽:
Timeout10→20s、TLS 5→8s、读头 10→8s(图源同为 R2,宁等勿缺) - 全仓
DialContext扫查:仅上述两处,无第三处同类
4. 测试
- 新增
internal/shared/multidial_test.go(5 项):坏 IP 在前仍选通 / 全败报错 / ctx 取消即返回 / 空候选 / maxCandidates 截断 - 原
route/proxy_test.go随实现迁移删除
四、验证矩阵
| 项 | 结果 |
|---|---|
| gofmt | 无差异 |
overlay go build ./...(全模块) |
exit 0 |
go vet(route + internal/shared) |
干净 |
| 全量安全相关测试(12 包:templates/cache/conf/form/i18n/imagecheck/internal-route/autoban/censor/shared/internal-template/route-vote) | 全 PASS |
| 新增回归测试 | 5/5 PASS |
五、部署验证清单(服务器侧)
- 重新编译部署(Linux 补跑
go build ./...,webp/CGO 惯例确认) - 观察日志:
ProxyImage fetch failed应消失 - 刷新提问箱页:头像/背景图正常出图
- 触发一次分享卡生成(任意提问箱分享链接),确认头像/背景/社交图标完整
- 判断标准:链路本身 ~6s 属正常(curl 实测),重点是不再 10s 稳定超时
六、补充诊断(可选)
服务器 curl 成功也要 6.46s,建议部署后定位慢在哪个阶段:
curl -sI --max-time 15 -o /dev/null -w \
"dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} total=%{time_total}\n" \
"https://r2.337845818.best/tamabox/picture/2026/05/13/f95ba0476f90331e190baa65052a03"
- 若
dns占大头 → 服务器 DNS 解析慢(可换公共 DNS / 加 hosts) - 若
tls占大头 → Cloudflare 边缘 TLS 劣化(多 IP 并发已缓解,可考虑图源域名走非代理直连评估) - 若
connect占大头 → 链路 TCP 劣化