第一章:Go Context超时传递失效的本质与现象定位
Go 中 context.Context 的超时传递看似简单,实则极易因上下文树断裂或错误的上下文继承方式而悄然失效。最典型的现象是:父 context 设置了 WithTimeout,子 goroutine 却未在预期时间内被取消,导致资源泄漏或响应延迟。
常见失效场景
- 错误地使用
context.Background()或context.TODO()作为子 context 的父节点:切断了与原始 timeout context 的继承链; - 跨 goroutine 传递 context 时发生值拷贝而非引用传递(如通过结构体字段未显式赋值 context);
- 调用
context.WithCancel/context.WithTimeout后未将返回的新 context 用于后续操作,仍沿用旧 context。
失效验证方法
可通过以下代码快速复现并定位问题:
func demoTimeoutFailure() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// ❌ 错误:此处应使用 ctx,却误用 background
go func() {
select {
case <-time.After(500 * time.Millisecond):
fmt.Println("goroutine still running — timeout failed!")
case <-context.Background().Done(): // ← 使用了 background,非 ctx!
fmt.Println("cancelled (never reached)")
}
}()
time.Sleep(200 * time.Millisecond)
}
执行后输出 "goroutine still running — timeout failed!",表明子 goroutine 完全未感知父级超时。
关键诊断步骤
- 检查所有
go func()启动处是否显式接收并使用上游传入的 context; - 使用
ctx.Err()在关键路径打印日志,确认是否为context.DeadlineExceeded; - 利用
runtime/pprof抓取 goroutine stack,搜索阻塞在<-ctx.Done()的协程,观察其 context 是否已Deadline()过期但未触发。
| 检查项 | 合规示例 | 违规示例 |
|---|---|---|
| context 传递 | go worker(ctx, req) |
go worker(context.Background(), req) |
| 超时检查位置 | select { case <-ctx.Done(): return ctx.Err() } |
if time.Now().After(deadline) { return errors.New("timeout") } |
真正的超时控制必须依赖 ctx.Done() 通道的关闭信号,而非手动时间比对——后者无法触发关联 cancel 链,也无法释放 context.Value 等附属资源。
第二章:Context超时机制的底层原理与调用链穿透分析
2.1 context.WithTimeout 的 goroutine 安全性与 cancel 传播路径
context.WithTimeout 创建的 Context 实例天然具备 goroutine 安全性——其 Done()、Err() 等方法可被任意数量 goroutine 并发调用,底层通过 atomic 操作与 channel 关闭保障线程安全。
cancel 传播机制
- 超时触发时,父 context 自动关闭其
donechannel - 所有监听该 channel 的 goroutine 收到信号并退出
- cancel 不会“递归广播”,仅沿显式继承链单向传递(子 → 父不反向)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel() // 必须调用,否则泄漏 timer 和 goroutine
go func() {
select {
case <-ctx.Done():
fmt.Println("timeout or cancelled:", ctx.Err()) // context.DeadlineExceeded
}
}()
cancel() 函数是幂等的;ctx.Done() 返回只读 channel,关闭后所有接收者立即唤醒;ctx.Err() 在 channel 关闭后返回具体错误类型。
| 场景 | ctx.Err() 返回值 |
是否可重用 Context |
|---|---|---|
| 正常超时 | context.DeadlineExceeded |
❌ 已失效 |
| 主动 cancel | context.Canceled |
❌ |
| 未触发 | nil |
✅ |
graph TD
A[context.Background] --> B[WithTimeout]
B --> C[goroutine #1]
B --> D[goroutine #2]
B --> E[timer goroutine]
E -- 100ms到期 --> B
B -- close done --> C & D
2.2 timerCtx 的生命周期管理与 channel 关闭时机验证
timerCtx 是 context.WithTimeout 或 context.WithDeadline 创建的上下文,其核心依赖 timer 和内部 done channel 的协同关闭机制。
channel 关闭的唯一性保障
timerCtx.done 是只读 channel,由 cancel 函数一次性关闭,且关闭前会原子检查 closed 标志位,避免重复 close panic。
func (c *timerCtx) cancel(removeFromParent bool, err error) {
if c.err != nil {
return // 已取消,直接返回
}
c.err = err
close(c.done) // ✅ 唯一关闭点
// ... 后续清理逻辑
}
close(c.done)仅在首次调用cancel时执行;c.err非 nil 表示已终止,确保 channel 关闭的幂等性。
生命周期关键状态转移
| 状态 | 触发条件 | done 状态 |
|---|---|---|
| active | ctx 创建后未超时 | open |
| canceled | 定时器触发或手动 cancel | closed |
| expired | 超时时间到达 | closed |
graph TD
A[active] -->|Timer fires| B[canceled]
A -->|cancel() called| B
B --> C[done closed]
2.3 父 Context 超时未触发子 Context cancel 的竞态复现与 pprof 定位
复现场景构造
以下代码模拟父 Context 超时但子 Context 未及时取消的竞态:
func reproduceRace() {
parent, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
child, childCancel := context.WithCancel(parent)
go func() {
time.Sleep(200 * time.Millisecond) // 模拟延迟调用 cancel
childCancel()
}()
select {
case <-child.Done():
fmt.Println("child cancelled")
case <-time.After(300 * time.Millisecond):
fmt.Println("child still alive — race detected!")
}
}
逻辑分析:
parent超时后应自动触发child.Done(),但因childCancel()延迟执行且未监听parent.Done()传播链,导致子 Context 泄漏。关键参数:100ms父超时 vs200ms手动 cancel 延迟,暴露 cancel 传播非原子性。
pprof 定位路径
使用 go tool pprof -http=:8080 cpu.prof 可定位阻塞点:
| 栈帧深度 | 函数名 | 累计耗时 | 关键线索 |
|---|---|---|---|
| 1 | runtime.gopark | 98% | goroutine 长期阻塞 |
| 2 | context.(*cancelCtx).Done | 95% | child.Done() 未唤醒 |
取消传播机制
graph TD
A[Parent timeout] --> B[notify parent's children]
B --> C{Child ctx registered?}
C -->|Yes| D[call child.cancel()]
C -->|No| E[leak: child remains active]
该流程揭示:若子 Context 创建后未及时被父节点注册(如并发创建+延迟注册),则超时信号无法传递。
2.4 defer cancel() 遗漏与 context.Value 污染对超时链路的隐式破坏
defer cancel() 遗漏:无声的资源泄漏
当 context.WithTimeout 创建的 cancel 函数未被 defer 调用,子 context 不会及时终止,导致 goroutine 和 timer 泄漏:
func badHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 100*time.Millisecond)
// ❌ 忘记 defer cancel() → timer 持续运行,ctx.Done() 永不关闭
select {
case <-time.After(200 * time.Millisecond):
w.Write([]byte("timeout"))
case <-ctx.Done():
w.Write([]byte("done"))
}
}
逻辑分析:cancel() 释放内部 timer 和 channel;遗漏后,ctx.Done() 保持阻塞,下游依赖该 channel 的 goroutine(如数据库查询)无法感知取消信号,超时链路失效。
context.Value 污染:跨层级隐式污染
在中间件中滥用 context.WithValue 注入非请求元数据,易引发 key 冲突与生命周期错位:
| 场景 | 后果 |
|---|---|
多中间件重复 WithValue(ctx, key, val) |
后写覆盖前写,语义丢失 |
使用 &struct{} 作 key |
GC 延迟导致 value 持有长生命周期引用 |
graph TD
A[HTTP Handler] --> B[Auth Middleware]
B --> C[DB Middleware]
C --> D[Query]
B -.->|ctx.WithValue\|userKey| C
C -.->|ctx.WithValue\|traceID| D
D -.->|ctx.Value\|userKey| 读取已过期 user
根本原因:context.Value 本应仅承载请求级只读元数据;污染后,超时 context 的 cancel 信号虽发出,但业务逻辑因错误 value 继续执行——形成“链路断裂但行为未终止”的隐式破坏。
2.5 Go runtime trace 中 context.cancelCtx.cancel 调用栈的逆向追踪实践
当 context.WithCancel 创建的 cancelCtx 被显式取消时,cancelCtx.cancel 方法被触发,其调用路径常隐匿于 goroutine 阻塞点之后。需借助 go tool trace 逆向定位源头。
追踪关键步骤
- 启动 trace:
go run -gcflags="-l" -trace=trace.out main.go(禁用内联以保留符号) - 在
traceUI 中筛选runtime.traceGoStart事件,定位cancelCtx.cancel执行的 P 和 goroutine ID - 关联
GC或Syscall事件,回溯至前序阻塞点
典型 cancel 调用栈片段(简化)
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if c.err != nil { // 已取消,直接返回
return
}
c.err = err
if removeFromParent {
removeChild(c.context, c) // 从父 context 解绑
}
for child := range c.children { // 逐个通知子节点
child.cancel(false, err) // 递归取消,不移除父引用
}
close(c.done) // 触发 <-ctx.Done()
}
removeFromParent=false在递归调用中避免重复解绑;c.done是chan struct{},关闭后所有监听者立即唤醒。
trace 中关键字段对照表
| trace 事件字段 | 对应 runtime 操作 | 说明 |
|---|---|---|
goroutine id |
runtime.goid() |
标识执行 cancel 的 goroutine |
stack |
runtime/debug.Stack() |
包含完整调用链,含 http.(*conn).serve 等上游入口 |
graph TD
A[HTTP Handler] --> B[select{<-ctx.Done()}]
B --> C[context.WithCancel]
C --> D[cancelCtx.cancel]
D --> E[close c.done]
E --> F[goparkunlock → 唤醒所有 select]
第三章:HTTP Client 层超时传递断点排查与修复
3.1 http.Client.Timeout 与 request.Context 超时叠加导致的优先级冲突实测
当 http.Client.Timeout 与 req.Context().WithTimeout() 同时设置时,Go 的 HTTP 客户端会以更早触发的超时为准,但底层行为存在隐式优先级差异。
关键机制验证
client := &http.Client{
Timeout: 5 * time.Second, // Client 级硬限
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
req, _ := http.NewRequestWithContext(ctx, "GET", "https://httpbin.org/delay/4", nil)
// 实际请求在 3s 后因 ctx 取消而终止
此处
context超时(3s)先于Client.Timeout(5s)触发,net/http会主动关闭连接并返回context.DeadlineExceeded。Client.Timeout仅在无显式 context 时兜底生效。
超时优先级对比表
| 来源 | 触发时机 | 是否可取消 | 错误类型 |
|---|---|---|---|
request.Context |
任意嵌套深度 | ✅ | context.DeadlineExceeded |
http.Client.Timeout |
请求发起后计时 | ❌ | net/http: request canceled |
行为流程图
graph TD
A[发起请求] --> B{Context 是否已设 Deadline?}
B -->|是| C[启动 context timer]
B -->|否| D[启动 Client.Timeout timer]
C --> E[任一 timer 到期 → 取消请求]
D --> E
3.2 Transport.RoundTrip 中 context.Deadline() 被忽略的边界条件注入验证
当 http.Transport 处理带 deadline 的 context.Context 时,若底层连接池复用已建立的空闲连接(persistConn),RoundTrip 可能跳过 deadline 检查——因连接复用路径绕过了 cancelTimer 初始化逻辑。
触发条件分析
- 请求前 context 已超时(
ctx.Err() == context.DeadlineExceeded) - 连接池中存在可用
persistConn(未关闭、未过期) transport.roundTrip()直接复用连接,未调用cancelTimer绑定 deadline
关键代码片段
// src/net/http/transport.go:roundTrip
if pc != nil && pc.alt == nil {
// 复用连接:此处未检查 ctx.Deadline() 或启动 cancelTimer
return pconn.roundTrip(ctx, req)
}
此处
ctx传入pconn.roundTrip,但persistConn.roundTrip内部仅监听ctx.Done(),不主动校验ctx.Deadline()是否已过期,导致阻塞读写可能无限期等待。
| 条件组合 | 是否触发 deadline 忽略 | 原因 |
|---|---|---|
| 空闲连接 + 过期 context | ✅ | 复用路径跳过 timer 初始化 |
| 新建连接 + 过期 context | ❌ | dialConn 会立即检查 ctx.Err() |
graph TD
A[RoundTrip 开始] --> B{连接池有空闲 persistConn?}
B -->|是| C[直接调用 pconn.roundTrip]
B -->|否| D[新建连接,检查 ctx.Deadline]
C --> E[仅监听 ctx.Done,忽略 Deadline 已过期]
3.3 自定义 RoundTripper 中 context 携带与 timeout 继承的正确模式重构
在自定义 RoundTripper 时,直接复用原始 req.Context() 而忽略 timeout 继承,会导致子请求脱离父级超时控制。
正确上下文传递原则
- 必须基于
req.Context()衍生新 context(如context.WithTimeout) - 禁止覆盖
req.Cancel或直接替换req.Context()
func (rt *timeoutRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
// ✅ 正确:继承原始 context 并叠加 timeout
ctx, cancel := context.WithTimeout(req.Context(), rt.timeout)
defer cancel()
// 构造新请求,保留原始 headers、body,仅更新 context
newReq := req.Clone(ctx) // ← 关键:Clone 保证 body 可重用且 context 隔离
return rt.base.RoundTrip(newReq)
}
逻辑分析:
req.Clone(ctx)安全继承req.URL,Header,Body和TLSClientConfig;context.WithTimeout确保父 context 的取消信号可穿透,同时新增超时约束。若直接req = req.WithContext(ctx),则丢失Body的可读性保障(因*http.Request不是深拷贝)。
常见反模式对比
| 方式 | 是否继承 cancel | 是否保留 Body 可读性 | 是否支持 deadline 传递 |
|---|---|---|---|
req.WithContext(ctx) |
✅ | ❌(可能 panic) | ✅ |
req.Clone(ctx) |
✅ | ✅(推荐) | ✅ |
&http.Request{...} 手动构造 |
❌ | ⚠️(易遗漏字段) | ❌ |
graph TD
A[原始 Request] --> B[req.Clone ctx]
B --> C[base.RoundTrip]
C --> D[响应/错误]
B --> E[ctx.Done 触发 cancel]
E --> F[底层 Transport 中断连接]
第四章:gRPC 与 Middleware 场景下的 Context 超时链路治理
4.1 gRPC UnaryInterceptor 中 context.WithTimeout 覆盖原 ctx.Deadline() 的静默失效
当在 UnaryInterceptor 中调用 context.WithTimeout(parent, timeout),新生成的 ctx 会完全覆盖上游传入的 ctx.Deadline() —— 即使原 context 已设 deadline,新 timeout 也会重置计时起点,导致服务端超时策略被意外覆盖。
关键行为对比
| 场景 | 原 context deadline | WithTimeout 参数 | 实际生效 deadline |
|---|---|---|---|
| 客户端设 5s | 2024-05-20T10:00:05Z |
3s |
now()+3s(无视原 deadline) |
| 客户端未设 deadline | ok=false |
3s |
now()+3s(正常生效) |
典型误用代码
func timeoutInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// ⚠️ 静默覆盖:无论原 ctx 是否含 deadline,此处均重置为固定偏移
newCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
return handler(newCtx, req)
}
逻辑分析:
context.WithTimeout创建新 context 时,不继承原 deadline 时间点,而是基于当前时间重新计算。参数2*time.Second是相对时长,与原ctx.Deadline()无关联;cancel()仅释放新 ctx 资源,不影响上游 deadline 状态。
正确做法建议
- 优先使用
context.WithDeadline并显式取min(原deadline, 新deadline) - 或通过
ctx.Deadline()+time.Until()动态计算剩余时间
4.2 grpc.DialContext 的 deadline 与 server 端 stream.Context().Done() 响应延迟对比实验
实验设计要点
- 客户端设置
grpc.WithTimeout(500 * time.Millisecond)并调用grpc.DialContext - Server 端在
StreamServerInterceptor中监听stream.Context().Done()触发时机 - 使用
time.Now()打点记录客户端发起连接、server 收到首请求、ctx.Done()触发三阶段时间戳
关键观测现象
// 客户端 dial 代码片段
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
conn, err := grpc.DialContext(ctx, "localhost:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
// ⚠️ 注意:DialContext 的 deadline 控制的是连接建立(TCP + TLS + HTTP/2 handshake)全过程,不包含后续 RPC 调用
该 deadline 仅约束连接初始化阶段;一旦连接成功,stream.Context() 的生命周期由每次 RPC 的 rpcCtx 决定,与 dial 时的 ctx 无关。
延迟对比数据(单位:ms)
| 场景 | DialContext 生效延迟 | stream.Context().Done() 响应延迟 |
|---|---|---|
| 网络高丢包(15%) | 498 ± 12 | 512 ± 28 |
| DNS 解析失败(mock) | 499 | —(未进入 stream) |
核心结论
DialContext的 deadline 是连接层守门员,失败即终止;stream.Context().Done()是RPC 生命周期信号灯,其触发依赖于本次调用绑定的 context(如client.CallOption.WithDeadline)。
二者作用域正交,不可混用。
4.3 Gin/Echo 中间件中 context.WithValue + context.WithTimeout 混用引发的 cancel 泄漏
当在 Gin 或 Echo 的中间件中先调用 context.WithValue,再基于该 value-context 调用 context.WithTimeout,会导致父 context 的 cancel 函数被新 context 隐式持有,却未被显式调用。
问题复现代码
func riskyMiddleware(c echo.Context) error {
// ❌ 错误:WithValue 后再 WithTimeout,cancel 被泄漏
ctx := c.Request().Context()
valCtx := context.WithValue(ctx, "trace-id", "abc")
timeoutCtx, cancel := context.WithTimeout(valCtx, 5*time.Second)
defer cancel() // ⚠️ 表面安全,但若下游 panic 或提前 return,cancel 可能不被执行
c.SetRequest(c.Request().WithContext(timeoutCtx))
return next(c)
}
cancel()仅在当前函数作用域 defer 执行,但若中间件链中后续 handler panic 或未执行到 defer 行(如return c.JSON(500, ...)提前退出),cancel永不触发,导致 goroutine 泄漏与 timer 持续运行。
正确模式对比
| 方式 | cancel 是否可控 | 是否推荐 | 原因 |
|---|---|---|---|
WithTimeout(parent) → WithValue(child) |
✅ 显式管理 | ✅ | cancel 独立于 value 传播 |
WithValue(parent) → WithTimeout(valCtx) |
❌ 隐式绑定 | ❌ | cancel 依赖 valCtx 生命周期,易遗漏 |
安全重构建议
func safeMiddleware(c echo.Context) error {
// ✅ 正确:先 WithTimeout,再 WithValue
parent := c.Request().Context()
timeoutCtx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel()
enrichedCtx := context.WithValue(timeoutCtx, "trace-id", "abc")
c.SetRequest(c.Request().WithContext(enrichedCtx))
return next(c)
}
4.4 基于 context.WithCancelCause(Go 1.21+)重构超时错误溯源链的生产级实践
传统 context.WithTimeout 在超时取消后仅返回 context.DeadlineExceeded,丢失原始触发原因,导致下游无法区分是服务延迟、网络抖动还是上游主动中止。
超时与因果分离的痛点
- 超时错误被统一归为
context.DeadlineExceeded - 中间件/重试逻辑无法判断是否应重试(如:因 DB 连接池耗尽导致超时 vs 网络瞬断)
- 日志与指标中缺失可操作的根因标签
使用 WithCancelCause 构建可追溯链
ctx, cancel := context.WithCancelCause(parent)
go func() {
select {
case <-time.After(3 * time.Second):
cancel(fmt.Errorf("db: connection pool exhausted at %s", time.Now().UTC()))
case <-done:
cancel(nil) // 正常完成
}
}()
逻辑分析:
WithCancelCause允许显式注入带语义的取消原因。cancel(err)将err存入 context 内部,后续调用errors.Unwrap(ctx.Err())可逐层提取真实根因。参数err必须非 nil 才生效,nil表示正常终止。
错误溯源能力对比
| 场景 | WithTimeout |
WithCancelCause |
|---|---|---|
| 超时发生 | context.DeadlineExceeded |
context.Canceled + Unwrap() → "db: connection pool exhausted..." |
| 日志可读性 | ❌ 无上下文 | ✅ 直接携带业务语义 |
graph TD
A[HTTP Handler] --> B[Service Call]
B --> C[DB Query]
C -- timeout after 3s --> D[cancel(fmt.Errorf(...))]
D --> E[ctx.Err() → Canceled]
E --> F[errors.Unwrap → db: connection pool exhausted]
第五章:全栈超时一致性设计原则与演进路线
在高并发电商大促场景中,某平台曾因支付链路超时策略割裂导致严重资损:前端按钮3秒无响应即置灰,但网关层设置15秒超时,下游库存服务却配置了30秒重试+兜底延迟释放锁。结果用户重复点击触发多次扣减,而最终仅一笔支付成功,造成库存超卖2700+件。这一事故成为推动全栈超时治理的直接动因。
超时必须具备可追溯性
所有超时阈值需通过统一配置中心(如Apollo)注入,并携带元数据标签:source=order-service, scope=payment, owner=@finance-team。每次请求进入时,框架自动注入X-Timeout-Trace头,记录各环节生效的超时值及决策依据。以下为真实日志片段:
{
"request_id": "req-8a9f3c1e",
"stage": "inventory-check",
"configured_timeout_ms": 800,
"effective_timeout_ms": 650,
"reason": "global_slo_reduced_for_peak_hour"
}
分层超时必须满足单调递减约束
任何下游调用的超时值不得超过上游剩余可用时间。我们强制实施以下校验规则:
| 层级 | 推荐基准值 | 强制上限公式 | 违规示例 |
|---|---|---|---|
| 前端 | 3000ms | — | 未设超时 |
| API网关 | 5000ms | ≤ 前端超时 × 0.9 | 设为4800ms(合规) |
| 核心服务 | 2000ms | ≤ 网关超时 − 1200ms | 设为3500ms(拒绝发布) |
| 数据库连接池 | 800ms | ≤ 核心服务超时 × 0.35 | 设为1000ms(自动截断) |
演进路线采用三阶段灰度验证
从单点治理走向全链路协同,分阶段推进:
- 熔断器对齐期(2023.Q3):统一Hystrix/Sentinel降级超时阈值,禁用
timeoutInMilliseconds=0配置 - SLA契约期(2024.Q1):服务间gRPC接口定义
.proto中强制声明option (timeout_ms) = 1500;,CI阶段静态扫描 - 动态自适应期(2024.Q3起):基于Prometheus指标实时计算P999响应延迟,通过OpenTelemetry SDK自动调整下游超时值,流程如下:
graph LR
A[每分钟采集P999延迟] --> B{是否突增>40%?}
B -- 是 --> C[查询依赖服务历史水位]
C --> D[按衰减系数0.75重算新超时]
D --> E[推送至Envoy xDS配置]
B -- 否 --> F[维持当前值]
客户端需主动参与超时协商
移动端SDK不再硬编码超时,而是首次启动时向/v1/timeout/negotiate发起POST请求,携带设备类型、网络类型、地理位置等上下文:
curl -X POST https://api.example.com/v1/timeout/negotiate \
-H "Content-Type: application/json" \
-d '{"device":"android-14","network":"wifi","region":"shenzhen"}'
# 返回:{"read_timeout_ms":2800,"connect_timeout_ms":1200}
压测必须覆盖超时边界组合
使用ChaosMesh注入网络延迟故障时,同步验证三组超时组合:
- 正常路径:前端3s → 网关4.5s → 订单服务1.8s → 库存服务600ms
- 高负载路径:前端2.2s → 网关3.3s → 订单服务1.1s → 库存服务400ms
- 故障恢复路径:前端3s → 网关5s(含2s重试)→ 订单服务2.2s → 库存服务800ms
所有压测报告需包含超时中断率热力图,横轴为请求路径深度,纵轴为超时触发节点,颜色深浅表示中断占比。某次压测发现深度为4的路径中断率达63%,定位到缓存层未配置超时导致线程池耗尽。
