3、消息生产者集成测试:同步发送、异步发送、回调函数、acks与retries配置验证
生产者是消息进入 Kafka 集群的入口,其发送行为的可靠性直接决定下游消费端的数据完整性。本章聚焦生产者集成测试的四个核心维度:同步发送、异步发送、回调函数以及 acks 与 retries 的配置验证。通过可重复的测试用例,验证生产者在正常与异常场景下的行为是否符合预期。
3.1 同步发送与异步发送的测试策略
Kafka 生产者提供两种发送模式,其测试关注点截然不同:
| 发送模式 | 阻塞行为 | 异常可见性 | 吞吐量特征 | 测试验证点 |
|---|---|---|---|---|
| 同步发送 | 调用 send().get() 阻塞直到 Broker 响应 |
立即抛出异常 | 低(等待每次确认) | 超时、Leader 切换、网络断开时的异常捕获 |
| 异步发送 | 调用 send() 立即返回 Future |
通过回调或 Future.get() 延迟获取 | 高(批量发送) | 回调执行顺序、异常传递、内存缓冲区溢出 |
测试原则:同步发送测试应覆盖超时阈值(
request.timeout.ms)和重试耗尽场景;异步发送测试必须验证回调函数在成功与失败分支均被正确调用,且不遗漏异常。
3.2 回调函数集成测试
回调函数是异步发送中捕获结果的标准机制。测试需覆盖以下场景:
- 成功回调:验证
onCompletion(RecordMetadata, null)中的元数据(分区、偏移量)非空且符合预期。 - 失败回调:验证
onCompletion(null, Exception)中的异常类型与重试策略一致。 - 顺序保证:同一分区内回调执行顺序与发送顺序一致。
- 线程模型:回调运行在生产者 I/O 线程,测试中不应阻塞回调线程。
// 异步发送 + 回调测试示例
@Test
void testAsyncSendWithCallback() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<RecordMetadata> metaRef = new AtomicReference<>();
AtomicReference<Exception> exRef = new AtomicReference<>();
producer.send(new ProducerRecord<>("test-topic", "key", "value"),
(metadata, exception) -> {
metaRef.set(metadata);
exRef.set(exception);
latch.countDown();
});
assertTrue(latch.await(10, TimeUnit.SECONDS));
assertNull(exRef.get(), "回调不应返回异常");
assertNotNull(metaRef.get(), "元数据不应为空");
assertTrue(metaRef.get().offset() >= 0);
}
3.3 acks 配置验证
acks 参数控制生产者等待 Broker 确认的持久化级别。集成测试需验证不同配置下的行为差异:
| acks 值 | 持久化保证 | 测试验证方法 |
|---|---|---|
acks=0 |
不等待确认,可能丢失 | 关闭 Broker 后发送,消息不报错但实际未写入 |
acks=1 |
等待 Leader 确认 | Leader 宕机但未同步,消息可能丢失 |
acks=all(或 -1) |
等待所有 ISR 副本确认 | 模拟 ISR 中一个副本宕机,仍能成功写入 |
测试陷阱:使用
acks=0 时,send() 方法永远不会抛出异常,即使 Broker 不可用。测试用例必须通过消费端或直接读取日志文件来验证消息是否实际落盘。
3.4 retries 与重试行为验证
retries 与 retry.backoff.ms 共同决定生产者的重试策略。集成测试应覆盖:
- 可重试异常:如
LeaderNotAvailableException、NotEnoughReplicasException,验证重试次数耗尽后抛出TimeoutException。 - 不可重试异常:如
RecordTooLargeException,验证直接抛出且不重试。 - 重试顺序:重试期间消息不会乱序(当
max.in.flight.requests.per.connection=1时)。
// 重试耗尽测试示例
@Test
void testRetriesExhausted() {
Properties props = new Properties();
props.put("retries", 2);
props.put("retry.backoff.ms", 100);
props.put("request.timeout.ms", 500);
// 配置一个不存在的 Broker 地址触发重试
try (KafkaProducer<String, String> producer = new KafkaProducer<>(props)) {
producer.send(new ProducerRecord<>("unavailable-topic", "data")).get();
fail("应抛出 TimeoutException");
} catch (ExecutionException e) {
assertTrue(e.getCause() instanceof TimeoutException);
} catch (Exception e) {
fail("异常类型不匹配");
}
}
3.5 生产者发送流程架构图
下图展示生产者从应用线程到 Broker 确认的完整路径,标注了同步/异步分支与 acks/retries 的介入点:
3.6 集成测试用例清单
以下清单覆盖生产者集成测试的核心场景,建议作为 CI 流水线的准入标准:
| 测试编号 | 测试场景 | 配置条件 | 预期结果 |
|---|---|---|---|
| PT-01 | 同步发送正常 | acks=all, retries=3 | 返回有效元数据,偏移量递增 |
| PT-02 | 同步发送 Broker 宕机 | acks=1, 关闭 Leader | 抛出 TimeoutException |
| PT-03 | 异步发送回调成功 | acks=1 | 回调中 metadata 非空 |
| PT-04 | 异步发送回调失败 | acks=all, ISR 不足 | 回调中 exception 非空 |
| PT-05 | acks=0 无确认 | acks=0, Broker 关闭 | send() 不抛异常,消息丢失 |
| PT-06 | 重试耗尽 | retries=2, 无效 Broker | 最终抛出 TimeoutException |
| PT-07 | 不可重试异常 | 消息大小超 max.request.size | 直接抛出 RecordTooLargeException |
| PT-08 | 回调顺序验证 | 同一分区连续发送 100 条 | 回调执行顺序与发送顺序一致 |
测试架构建议:使用 Testcontainers 启动嵌入式 Kafka 集群,通过动态调整 Broker 配置(如
min.insync.replicas)模拟 ISR 不足场景,避免依赖外部环境。每个测试用例独立创建生产者实例,防止状态污染。