14.2 故障排查

响应式系统故障排查完整手册:jstack 线程 dump 识别 EventLoop 阻塞与 boundedElastic 饱和、R2DBC Connection 与 Netty ByteBuf 连接泄漏定位、背压溢出 request(n) 诊断、io.netty.leakDetection.level=advanced 内存泄漏检测、错误日志速查表。

线程 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.R2dbcTimeoutExceptionmaxAcquireTime 内拿不到连接,或 pending 队列已满增加 pool size、缩短持有时间或减少并发;同时检查慢查询是否长期占用连接
ConnectionPoolException连接池的通用包装异常(初始化、配置、释放路径等),不是「池耗尽」的专用信号先看 cause;池等待超时表现为 R2dbcTimeoutException,r2dbc-pool 没有 PoolExhaustedException
DataBufferLimitException请求体超过 maxInMemorySize增加配置或用 FilePart
CancellationException订阅被取消检查 timeout、take 或客户端断开
OverflowException上游生产快于下游消费,且策略为报错添加 onBackpressureBuffer/Drop/Latest,或降低生产速率
IllegalReferenceCountExceptionByteBuf 已释放后访问检查释放时机
WebClientResponseException: 429限流触发降低请求频率
CallNotPermittedException断路器打开检查下游服务健康

排查流程图

系统异常
    ↓
CPU 高? → 线程 dump → 找 RUNNABLE 热点
    ↓ 否
内存高? → heap dump → 找 ByteBuf/数组堆积
    ↓ 否
延迟高? → metrics → 检查 pool pending / circuit breaker / rate limit
    ↓ 否
错误率高? → 日志 → 找异常类型 → 对照速查表

小结

问题工具关键指标
EventLoop 阻塞jstack(间隔多次)栈顶停在阻塞调用,状态可能是 RUNNABLE
连接泄漏pool metricsacquired 接近上限且流量下降后不恢复
ByteBuf 泄漏-Dio.netty.leakDetection.level=advancedLEAK 日志
背压溢出doOnRequest + doOnNextrequest < produce
OOMheap dump大对象 / ByteBuf

下一节 14.3 v2 验收 :全链路可运行验证、压测指标、文档与交付。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南