线程 Dump 分析
响应式系统的线程模型与阻塞系统不同。关键线程类型:
eventd-nio-1 # Reactor Netty EventLoop(处理 I/O)
eventd-nio-2
eventd-nio-3
eventd-nio-4
boundedElastic-1 # 弹性线程池(包裹阻塞操作)
boundedElastic-2
parallel-1 # CPU 计算线程池
parallel-2
检查 EventLoop 是否阻塞
jstack <pid> | grep -A 20 "eventd-nio-"
正常:EventLoop 线程要么 RUNNABLE(处理 I/O 或执行任务),要么在 selector 上等待事件(通常显示为 RUNNABLE 的 native 调用,不一定是 WAITING)。
异常:线程长时间停在同一处堆栈,且栈顶是 JDBC、文件 I/O、Thread.sleep、socketRead 或等锁,就说明 EventLoop 被阻塞。不要只找 BLOCKED:阻塞在 I/O 上时线程状态往往是 RUNNABLE。
同一份 dump 至少间隔几秒抓两三次,单次快照里短暂出现一次阻塞调用不能下结论。
检查 boundedElastic 饱和
jstack <pid> | grep "boundedElastic" | wc -l
# 如果接近 pool 上限(默认如 10×CPU),说明阻塞操作过多
连接泄漏定位
R2DBC Connection 泄漏
// 错误:忘记关闭连接
Mono.from(factory.create())
.flatMap(conn ->
conn.createStatement("SELECT * FROM users").execute()
.flatMap(result -> result.map(...)))
// conn.close() 在哪里?
.subscribe();
正确做法:
Mono.usingWhen(
factory.create(),
conn -> /* 使用连接 */,
Connection::close
);
检测泄漏:
# 查看 pool metrics
curl http://localhost:8081/actuator/prometheus | grep -E 'r2dbc_pool_(acquired|idle|pending|max_allocated)'
pending > 0 说明有请求在排队等连接,可能是池太小或下游慢,不一定是泄漏。更像泄漏的信号是:acquired 接近 max_allocated、idle 长期为 0、pending 持续上升,且流量下降后也不恢复。确认泄漏要看获取连接和归还连接的日志/堆栈对,而不是只看一个 gauge。
Netty ByteBuf 泄漏
这里的 DataBuffer 是 Spring WebFlux / Reactor Netty 的 org.springframework.core.io.buffer.DataBuffer(底层才是 Netty ByteBuf),不是 R2DBC SPI 的类型。手动读取请求体、文件上传或自定义编解码器时,池化缓冲区需要显式释放。
// 错误:读取 DataBuffer 后未释放
buffer.asInputStream().readAllBytes();
// ByteBuf 引用计数未减!
// 正确
DataBufferUtils.release(buffer);
Netty 泄漏检测:
java -Dio.netty.leakDetection.level=advanced -jar eventd.jar
旧属性名 io.netty.leakDetectionLevel 是 Netty 4.0 的写法;Netty 4.1+ 使用带点的 io.netty.leakDetection.level(simple/advanced/paranoid)。paranoid 开销很大,只适合在测试环境复现问题。
日志中出现:
LEAK: ByteBuf.release() was not called before it's garbage-collected.
Recent access records:
#1: io.netty.buffer.AdvancedLeakAwareByteBuf.readableBytes(...)
背压溢出诊断
症状
- 请求排队增长,延迟飙升
- 定时源(如
Flux.interval)在某个 tick 到达时下游没有未满足的请求,会取消订阅并抛出OverflowException,消息是Could not emit tick N due to lack of requests...,不会静默丢弃 - 有界
onBackpressureBuffer溢出时抛OverflowException Sinks.Many的tryEmitNext返回FAIL_OVERFLOW
诊断
// 在关键流上加监控
Flux.interval(Duration.ofMillis(10))
.doOnRequest(n -> System.out.println("Requested: " + n))
.doOnNext(i -> System.out.println("Emitted: " + i))
.doOnError(err -> System.out.println("Error: " + err))
.subscribe();
常见原因
| 原因 | 现象 | 修复 |
|---|---|---|
| 下游处理慢 | request(n) 很小 | 增加消费者,或明确丢弃/采样非关键数据 |
| 连接池满 | pending acquire > 0 | 增加 max-size 或减少并发 |
| 单线程消费者 | 只有一个 subscriber | 用 flatMap 并发订阅;阻塞操作加 subscribeOn(boundedElastic) |
| 缓冲无上限 | 内存持续增长 | 用 onBackpressureBuffer(maxSize, ...) 并定义溢出策略 |
OOM 诊断
响应式系统 OOM 的常见原因:
1. 无界缓冲
// 危险:缓存整个 Flux
List<Order> all = orderRepo.findAll().collectList().block();
// 如果表有 1000 万行,堆内存很可能被撑爆(取决于行宽和堆大小)
修复:流式处理,不 collect。
2. ByteBuf 泄漏
LEAK: ByteBuf.release() was not called
修复:所有 DataBuffer 读取后 DataBufferUtils.release()。
3. 线程风暴
// 错误:每个元素创建新线程
Flux.range(1, 10000)
.flatMap(i -> Mono.fromCallable(() -> heavyCompute(i))
.subscribeOn(Schedulers.newBoundedElastic(10, 100, "storm")))
.subscribe();
修复:复用同一个 Scheduler,并限制并发度:
private final Scheduler blocking = Schedulers.newBoundedElastic(10, 100, "blocking");
Flux.range(1, 10000)
.flatMap(i -> Mono.fromCallable(() -> heavyCompute(i))
.subscribeOn(blocking), 8) // 同时在途 8 个
.subscribe();
newBoundedElastic 每次都会创建线程池,必须在应用生命周期内复用并在关闭时 dispose()。
常见错误速查
| 错误 | 原因 | 修复 |
|---|---|---|
io.r2dbc.spi.R2dbcTimeoutException | maxAcquireTime 内拿不到连接,或 pending 队列已满 | 增加 pool size、缩短持有时间或减少并发;同时检查慢查询是否长期占用连接 |
ConnectionPoolException | 连接池的通用包装异常(初始化、配置、释放路径等),不是「池耗尽」的专用信号 | 先看 cause;池等待超时表现为 R2dbcTimeoutException,r2dbc-pool 没有 PoolExhaustedException |
DataBufferLimitException | 请求体超过 maxInMemorySize | 增加配置或用 FilePart |
CancellationException | 订阅被取消 | 检查 timeout、take 或客户端断开 |
OverflowException | 上游生产快于下游消费,且策略为报错 | 添加 onBackpressureBuffer/Drop/Latest,或降低生产速率 |
IllegalReferenceCountException | ByteBuf 已释放后访问 | 检查释放时机 |
WebClientResponseException: 429 | 限流触发 | 降低请求频率 |
CallNotPermittedException | 断路器打开 | 检查下游服务健康 |
排查流程图
系统异常
↓
CPU 高? → 线程 dump → 找 RUNNABLE 热点
↓ 否
内存高? → heap dump → 找 ByteBuf/数组堆积
↓ 否
延迟高? → metrics → 检查 pool pending / circuit breaker / rate limit
↓ 否
错误率高? → 日志 → 找异常类型 → 对照速查表
小结
| 问题 | 工具 | 关键指标 |
|---|---|---|
| EventLoop 阻塞 | jstack(间隔多次) | 栈顶停在阻塞调用,状态可能是 RUNNABLE |
| 连接泄漏 | pool metrics | acquired 接近上限且流量下降后不恢复 |
| ByteBuf 泄漏 | -Dio.netty.leakDetection.level=advanced | LEAK 日志 |
| 背压溢出 | doOnRequest + doOnNext | request < produce |
| OOM | heap dump | 大对象 / ByteBuf |
下一节 14.3 v2 验收 :全链路可运行验证、压测指标、文档与交付。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。