Skip to content

[Bug]: proxy connCache performs backend I/O under global mutex and can block all tenants and shutdown #25827

Description

@Ariznawlll

问题概述

最新 main 上,Proxy 的连接缓存使用一个全局 mutex 保护所有租户和所有 CN 的缓存状态,但 connCache.Pop 在持有该 mutex 时同步执行后端网络 I/O:

  • SET CONNECTION IDsc.ExecStmt(...)
  • 错误/过期连接清理:sc.Quit()

如果任意一个缓存 CN 连接不响应,当前 Pop 会一直持有全局 mutex,导致其他租户的 Push/Pop/Count 以及 Proxy 关闭时的 connCache.Close 全部一起阻塞。

connCache.Push 同样在持锁状态下执行 resetSessionFunc RPC,也存在最长数秒的全局串行阻塞。

验证基线

  • 仓库:matrixorigin/matrixone
  • 分支:最新 main
  • Commit:824090c2e61351972cc16f866a57d52c7c2cab6e
  • Commit 时间:2026-07-17T01:10:07+00:00
  • Commit 标题:perf(hashmap): allocate int iterator buffers lazily (#25796)
  • 环境:macOS arm64,Go go1.26.2

测试前再次执行 git fetch origin main,确认 origin/main 未前进。

白盒测试过程

临时新增 TestConnCacheCloseDoesNotWaitForBlockedPopNetworkIO

  1. 创建真实 connCache,注入无网络行为的 reset/auth 配置。
  2. Push 一个 fake ServerConntenant-a
  3. fake ServerConn.ExecStmt 在 channel 上阻塞,模拟缓存 CN 已失去响应。
  4. 并发调用 cache.Pop("tenant-a", ...),确认已经进入 ExecStmt
  5. 再并发调用 cache.Close()
  6. 期望:Close 不应被一个后端连接的网络 I/O 永久阻塞,应能标记 cache closed 并主动清理/取消连接。
  7. 实际:100ms 后 Close 仍未返回;只有测试解除 ExecStmt 阻塞、让 Pop 释放全局 mutex 后,Close 才能完成。
  8. 断言前完成所有 channel/goroutine/连接清理,测试失败不会遗留后台任务。

核心场景:

go func() {
    popDone <- cache.Pop("tenant-a", 1, nil, nil)
}()
<-sc.entered // Pop 正持有 cache.mu 并进入后端 ExecStmt

go func() {
    closeDone <- cache.Close()
}()

select {
case <-closeDone:
    closedPromptly = true
case <-time.After(100 * time.Millisecond):
}

实际执行与结果

普通模式

go test ./pkg/proxy \
  -run '^TestConnCacheCloseDoesNotWaitForBlockedPopNetworkIO$' \
  -count=20 -v -timeout=60s

结果:FAIL 20/20

Race 模式

go test -race ./pkg/proxy \
  -run '^TestConnCacheCloseDoesNotWaitForBlockedPopNetworkIO$' \
  -count=5 -v -timeout=60s

结果:FAIL 5/5;没有 race detector 告警,失败原因始终是 Close 等待 cache mutex。

代表性输出:

--- FAIL: TestConnCacheCloseDoesNotWaitForBlockedPopNetworkIO (0.10s)
Error:       Should be true
Messages:    connCache.Close is blocked by network I/O performed under the cache mutex

根因分析

pkg/proxy/conn_cache.go

  1. Pop 在第 376 行获取 c.mu,第 377 行 defer unlock。
  2. 第 434-437 行在锁内调用 sc.ExecStmt(SET CONNECTION ID)serverConn.ExecStmt 的读取路径没有 caller context。
  3. 第 444、472 行错误/过期路径也在锁内调用 sc.Quit()
  4. Close 需要获取同一个 c.mu 才能设置 closed 并取出连接,因此被网络 I/O 阻塞。
  5. 代码在 unhealthy-CN 分支已经明确先从缓存移除连接、临时 unlock、再执行 sc.Quit(),说明网络清理应在临界区外完成;正常/错误/过期分支尚未采用同样模式。
  6. Push 第 330-357 行也在持有全局 mutex 时调用 reset-session RPC。

影响

  • 单个失去响应的缓存 CN 连接可以阻塞所有租户的连接缓存操作。
  • 新登录连接可能在 Proxy cache mutex 上排队,与故障租户/故障 CN 相互影响。
  • Proxy 关闭可能永久等待,影响重启、滚动升级和故障恢复。
  • reset-session RPC 在 Push 锁内执行,即使最终超时也会造成全局串行停顿。
  • 该问题属于 Proxy 普通连接缓存路径,与 2PC 无关。

建议修复

  • 在锁内只完成连接的选取、占用/移除和状态校验。
  • resetSessionFuncExecStmtQuit 等网络调用移动到临界区外。
  • 网络调用结束后重新加锁,并检查 cache 是否已关闭、候选连接是否仍属于本次操作。
  • 给网络操作贯穿可取消 context/timeout,确保 Proxy Close 可以主动中止。
  • 增加跨租户并发与 Close 场景回归测试,并覆盖 race 模式。

查重

已搜索 connCache Pop mutex ExecStmtproxy connection cache network I/O lockconnCache Close blockedcached connection hang all tenants,未发现相同 issue。

Metadata

Metadata

Assignees

Labels

kind/bugSomething isn't workingseverity/s0Active / top priority for current sprint. Owner has committed to working on it now.

Type

Fields

No fields configured for Bug.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions