Files
tamabox/outputs/proxy-image-timeout-2026-09-02.md
T
tamakyi e3e59e7c69 chore: baseline snapshot before moderation-strictness feature
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.
2026-09-10 10:56:27 +08:00

91 lines
4.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 劣化