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.
91 lines
4.5 KiB
Markdown
91 lines
4.5 KiB
Markdown
# 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 劣化
|