指标定义
面板统计五个值,每个策略一份,不能跨策略共享:
| 指标 | 计数位置 | 含义 |
|---|---|---|
emitted | 源之后的 doOnNext | 上游实际产生了多少元素 |
delivered | 消费者 onNext | 下游真正收到多少元素 |
dropped | 各策略的丢弃回调或 doOnDiscard | 被策略丢弃多少元素 |
requests | 消费者侧的 doOnRequest | 下游累计请求了多少次 |
latency | 消费者 onNext 内计时 | 处理一个元素的耗时 |
注意 emitted 不等于 delivered,两者之差也不一定等于 dropped:BUFFER 策略在取消时可能还有元素留在队列里,这部分既不叫「已交付」,也不叫「已丢弃」。
代码
import org.reactivestreams.Subscription;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.BufferOverflowStrategy;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction;
public class BackpressureDashboard {
static final class Stats {
final AtomicInteger emitted = new AtomicInteger();
final AtomicInteger delivered = new AtomicInteger();
final AtomicInteger dropped = new AtomicInteger();
final AtomicInteger requests = new AtomicInteger();
final AtomicLong latencyNanos = new AtomicLong();
}
public static void main(String[] args) throws Exception {
run("BUFFER(10)", (flux, stats) -> flux.onBackpressureBuffer(
10,
v -> stats.dropped.incrementAndGet(),
BufferOverflowStrategy.DROP_LATEST));
run("DROP", (flux, stats) ->
flux.onBackpressureDrop(v -> stats.dropped.incrementAndGet()));
run("LATEST", (flux, stats) ->
flux.onBackpressureLatest()
.doOnDiscard(Long.class, v -> stats.dropped.incrementAndGet()));
}
static void run(String name, BiFunction<Flux<Long>, Stats, Flux<Long>> strategy)
throws Exception {
Stats stats = new Stats();
CountDownLatch done = new CountDownLatch(1);
Flux<Long> producer = Flux.interval(Duration.ofMillis(10))
.doOnNext(v -> stats.emitted.incrementAndGet());
Flux<Long> flux = strategy.apply(producer, stats)
.publishOn(Schedulers.boundedElastic(), 1)
.doOnRequest(n -> stats.requests.addAndGet((int) Math.min(n, Integer.MAX_VALUE)));
flux.subscribe(new BaseSubscriber<Long>() {
long start;
@Override
protected void hookOnSubscribe(Subscription s) {
start = System.nanoTime();
request(1);
}
@Override
protected void hookOnNext(Long value) {
stats.delivered.incrementAndGet();
long t0 = System.nanoTime();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
cancel();
done.countDown();
return;
}
stats.latencyNanos.addAndGet(System.nanoTime() - t0);
if (System.nanoTime() - start < Duration.ofSeconds(1).toNanos()) {
request(1);
} else {
cancel();
done.countDown();
}
}
@Override
protected void hookOnError(Throwable t) {
t.printStackTrace();
done.countDown();
}
});
done.await(5, TimeUnit.SECONDS);
System.out.println("=== " + name + " ===");
System.out.println("Requests: " + stats.requests.get());
System.out.println("Emitted: " + stats.emitted.get());
System.out.println("Delivered: " + stats.delivered.get());
System.out.println("Dropped: " + stats.dropped.get());
System.out.println("Avg Latency: "
+ (stats.latencyNanos.get() / Math.max(stats.delivered.get(), 1) / 1_000_000) + "ms");
System.out.println("Produced - Delivered: "
+ (stats.emitted.get() - stats.delivered.get()));
System.out.println();
}
}
publishOn(..., 1) 的预取量设为 1,是为了让背压信号快速传回上游;默认预取 256 会把下游的慢消费掩盖在队列里,1 秒的演示根本触发不了丢弃。
一次实测
本机 JDK 25、Reactor 3.8.8 的一次运行(数字随时序和调度抖动变化,不是基准):
=== BUFFER(10) ===
Requests: 18
Emitted: 104
Delivered: 18
Dropped: 76
Avg Latency: 57ms
Produced - Delivered: 86
=== DROP ===
Requests: 16
Emitted: 101
Delivered: 16
Dropped: 85
Avg Latency: 57ms
Produced - Delivered: 85
=== LATEST ===
Requests: 18
Emitted: 102
Delivered: 18
Dropped: 84
Avg Latency: 56ms
Produced - Delivered: 84
逐项解释:
- BUFFER(10):
Emitted 104,Delivered 18,差值是 86。其中 76 个被DROP_LATEST丢弃,另外 10 个在取消时仍留在缓冲区。Dropped与Produced - Delivered不相等是正常的,差额就是队列里没来得及消费的元素。 - DROP:
Dropped 85,Produced - Delivered 85,两者相等。onBackpressureDrop不保留队列,没有「滞留元素」。 - LATEST:
Dropped 84来自doOnDiscard。onBackpressureLatest只保留一个最新值,被新值替换的旧值走 discard 钩子,所以能精确计数。 - Requests 是消费者发出的请求次数,不是请求的元素总数。消费者每次
request(1),因此它和Delivered很接近;多出来的一两次是取消前已经发出的请求。 - Avg Latency 用
Delivered做分母,不是Emitted。拿「处理耗时」除以「上游产量」会得到明显偏小的假数字。
关键陷阱:两参数的 onBackpressureBuffer 不是「丢弃」
很多示例写成:
flux.onBackpressureBuffer(50, dropped::incrementAndGet)
看起来像「溢出时调用 dropped 并丢弃」,实际语义是:溢出时调用这个消费者,然后以 OverflowException 终止整条流。用容量 50 实测时,缓冲区刚满、Dropped 变成 1,流就结束了;后面的元素不会再产生。
要让流继续,必须显式指定溢出策略:
flux.onBackpressureBuffer(
50,
dropped::incrementAndGet,
BufferOverflowStrategy.DROP_LATEST);
三个可选策略:
| 策略 | 行为 |
|---|---|
DROP_LATEST | 丢弃新来的元素,保留队列里的旧元素 |
DROP_OLDEST | 丢弃队首,给新元素腾位置 |
ERROR | 默认行为,发 OverflowException |
onBackpressureBuffer(50) 单参数重载同样使用 ERROR。它的「缓冲」只是延迟溢出,不是消除溢出。
为什么 Flux.interval 需要策略
Flux.interval 是定时源:时钟不会为下游变慢,到点就有一个 tick 要交付。它确实遵守 request(n),但只在 tick 到来时检查额度;如果这一刻没有未满足的需求,它不会静默丢弃,而是直接以 OverflowException 终止整条流:
reactor.core.Exceptions$OverflowException: Could not emit tick 0 due to lack of requests
(interval doesn't support small downstream requests that replenish slower than the ticks)
本机用 .subscribe(..., s -> s.request(0)) 实测即可复现。也就是说,interval 的默认背压策略等价于 ERROR。要让它接慢消费者,必须在中间显式选择一种策略:
onBackpressureBuffer:加有界队列,队列满后按BufferOverflowStrategy丢弃或报错;onBackpressureDrop/onBackpressureLatest:向上游请求无界,由策略在交付前丢弃。
onBackpressureBuffer、onBackpressureDrop、onBackpressureLatest 就是三种显式选择。不要用无界缓冲掩盖问题:无界队列只是把 OOM 推迟到更晚。
如果源本身能降速(例如 Flux.range、数据库游标),优先让背压一路传回源,而不是在中途加策略。
面板的读数方法
| 现象 | 说明 |
|---|---|
Delivered 远小于 Emitted,Dropped 接近差值 | 有损策略在起作用 |
Delivered 远小于 Emitted,Dropped 很小 | 元素滞留在缓冲区,取消时被丢弃 |
Requests 接近 Delivered | 下游逐个请求,没有预取 |
Requests 远大于 Delivered | 下游预取较多,或请求了但上游没产出 |
Avg Latency 接近消费者的 sleep 时间 | 处理耗时是主要成本,不是调度 |
这些读数只能说明「这一秒发生了什么」,不能直接外推成吞吐基准。要比较策略优劣,至少固定源速率、消费者耗时、运行时长和调度器,并重复多次取分位数。
小结
| 策略 | 保留什么 | 丢弃计数方式 |
|---|---|---|
onBackpressureBuffer(n, consumer, DROP_LATEST) | 最多 n 个旧元素 | 溢出消费者 |
onBackpressureDrop(consumer) | 不保留 | 丢弃消费者 |
onBackpressureLatest() | 最新 1 个 | doOnDiscard |
onBackpressureBuffer(n) | 最多 n 个,溢出即错误 | 无(流终止) |
阅读导航
下一篇:14.1 用 Project Reactor 重写 eventd
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。