# 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 的行为差异): 1. `route/proxy.go` 旧 `proxyImageDialContext` 解析后**固定拨第一个 IP**——覆盖了标准库的多 IP 容错。anycast 恰好抽中被劣化的边缘 → 稳定等满超时;curl 默认多 IP 尝试所以能成功。 2. 超时预算过紧:`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 - 超时放宽:`Timeout` 10→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 | ## 五、部署验证清单(服务器侧) 1. 重新编译部署(Linux 补跑 `go build ./...`,webp/CGO 惯例确认) 2. 观察日志:`ProxyImage fetch failed` 应消失 3. 刷新提问箱页:头像/背景图正常出图 4. 触发一次分享卡生成(任意提问箱分享链接),确认头像/背景/社交图标完整 5. **判断标准**:链路本身 ~6s 属正常(curl 实测),重点是**不再 10s 稳定超时** ## 六、补充诊断(可选) 服务器 curl 成功也要 6.46s,建议部署后定位慢在哪个阶段: ```bash 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 劣化