做多集群 Kafka 接入这件事,最开始我以为只是多配几个地址的事。真上手才发现,客户端实例怎么管理、消费线程怎么划分、位移怎么提交、集群间消息怎么追溯,每一项都有坑。这篇文章就把我在不同 Kafka 集群下同时跑多个消费者监听和多个生产者的完整思路、实现细节和排查经验整理出来,给同样要做多集群接入的同学做个参考。
1. 多Kafka集群架构的整体思路
1.1 多集群场景是怎么来的
先说清楚什么叫“不同 Kafka 集群下的多个消费者监听及多个生产者”。打个比方,你所在的公司有一套核心业务集群,专门承接订单、支付这类实时性要求高的消息;另一套是数据中台集群,用于日志采集、行为上报这类吞吐量大、允许一定延迟的消息。业务系统需要同时往这两套集群里发数据,也要从两套集群里消费数据,这就构成了“多个消费者监听”和“多个生产者”并存的基本形态。
除此之外还有几种典型场景:
- 公司收购或合并后,多个业务系统的 Kafka 集群需要临时打通,新系统要同时消费旧系统集群的消息;
- 跨机房容灾架构中,双活集群同时提供服务,应用需要双写双读;
- 测试环境、预发环境、生产环境分别部署独立 Kafka,一套代码需要切换或兼顾多环境;
- 外部合作方提供独立 Kafka 集群,数据通过专线或公网接入,应用侧需要额外对接。
不管是哪种场景,本质都一样:一套应用进程面对多个 Kafka 地址段,既要向不同集群发消息,又要从不同集群拉消息,并且这些动作要稳定、可控、好排查。
1.2 两种常见的错误设计
多集群接入最容易踩的第一个坑,是用一个 KafkaProducer 实例同时往多个集群发数据。Kafka 客户端的设计里,bootstrap.servers 只是用来发现集群元数据的入口地址,一个 Producer 实例一旦通过这个入口获取了某个集群的 broker 列表与分区信息,它后续所有请求都会发往那个集群的 broker。你把它当成“万能投递器”,往不同集群的 topic 里 send,结果大概率是:batch 数据堆积、元数据刷新异常、消息神秘丢失,日志里全是 timeout。
第二个坑是用一个 KafkaConsumer 实例订阅多个集群的 topic。同样地,一个 Consumer 实例只能归属于一个集群的消费者组,它从单个集群的 Coordinator 处获取分区分配结果。你以为自己订阅了一堆来自不同集群的 topic,实际上 Consumer 可能只在一个集群里注册成功,另一边的分区永远没有消费者接管。
这两个坑的根源,是混淆了“客户端实例”和“逻辑配置”的边界。正确做法是:一个集群对应一套独立的客户端实例,实例之间互不共享,但可以通过统一的配置管理器和工厂类来控制它们的生命周期。
1.3 接入层设计原则
我的多集群接入方案遵循三条基本原则:
集群维度隔离:每个 Kafka 集群有一组独立的 Producer 和 Consumer 实例,实例之间不共享任何 Kafka 客户端内部对象(包括 metrics、metadata、sender 线程等)。
统一配置入口:所有集群的地址、认证信息、topic 映射关系都收敛到一个配置文件中,代码里不硬编码任何 broker 地址。
监听逻辑与集群解耦:业务消费逻辑抽成统一的处理器接口,每个集群的消费者线程只负责把消息取出来交给处理器,处理器内部再根据消息头里的来源标识决定后续动作。
这三个原则解决的是“怎么组织多集群客户端”的核心问题。下面两个章节分别展开生产者和消费者的具体实现,配置管理放在后面单独讲。
2. 多生产者端的实现
2.1 为什么每个集群必须单独创建 Producer 实例
上一节说了不能共用一个 Producer,这里从原理层面再深入一下。KafkaProducer 内部有几个核心组件:Metadata(元数据缓存)、RecordAccumulator(消息累加器)、Sender(发送线程)、NetworkClient(网络客户端)。Metadata 会定期从 bootstrap 地址连接到的集群拉取 topic 的分区元数据,然后缓存在本地;Sender 线程从 RecordAccumulator 里取批次数据,按目标 broker 发送。
如果你尝试用同一个 Producer 给不同集群的 topic 发消息,会发生什么?Producer 通过 Metadata 获取的 broker 节点信息全部来自第一个集群,当它尝试发送另一个集群的 topic 时,要么发现元数据里没有这个 topic 的 leader,要么因为 broker 地址对不上而连接失败。即使两个集群的网络是通的,发送方也无法确认消息到底落到了哪个分区,更别提指定 key 的路由了。
所以生产端多集群的落地模型非常朴素:N 个集群,就建 N 个 KafkaProducer 实例。每个实例只服务于一个集群的所有 topic,实例之间通过工厂类统一管理,按业务名或集群名寻址。
2.2 生产端封装与路由选择
实际开发中,我不会在业务代码里直接 new KafkaProducer,而是封装一个 ProducerRouter。它内部维护一个 Map,key 是集群逻辑名(比如 core-kafka、data-kafka),value 是对应集群的 KafkaProducer 实例。发送逻辑根据目标 topic 所在的集群名,取出对应的 Producer 实例完成 send。
public class ProducerRouter implements Closeable { private final Map<String, KafkaProducer<String, String>> producers = new ConcurrentHashMap<>(); public void register(String clusterName, Properties props) { KafkaProducer<String, String> producer = new KafkaProducer<>(props); producers.put(clusterName, producer); } public SendResult send(String clusterName, String topic, String key, String value) { KafkaProducer<String, String> producer = producers.get(clusterName); if (producer == null) { throw new IllegalStateException("unknown cluster: " + clusterName); } // 实际项目中一般用异步发送+回调,这里为了演示同步获取结果 try { RecordMetadata metadata = producer.send( new ProducerRecord<>(topic, key, value) ).get(5, TimeUnit.SECONDS); return new SendResult(metadata.topic(), metadata.partition(), metadata.offset()); } catch (Exception e) { // 记录日志、重试或者走降级逻辑 throw new KafkaSendException("send message to " + clusterName + " failed", e); } } @Override public void close() { producers.forEach((name, producer) -> { producer.flush(); producer.close(Duration.ofSeconds(10)); }); } }一个最容易忽略的设计点是路由表的来源。生产端路由信息纯靠代码传参容易出错,一般推荐把它放进配置中心或本地配置文件里,让“集群名 -> 集群地址 -> 可用 topic 列表”成为一份可运维的清单,而不是散落在代码里。
发送方式还要结合消息的重要程度选择。同步 get 适合对可靠性要求极高的场景,但会阻塞业务线程;异步发送加回调适合大多数在线业务,失败时可以记录后重试或转储本地文件;如果要追求高吞吐,还可以考虑批量异步发送。我实践中用得最多的是异步发送+回调,回调里只做失败统计和日志记录,不做影响主流程的重试,重试交给独立的重试队列去处理。
2.3 多生产者核心配置参数
无论多少个集群,每个集群的 KafkaProducer 参数都是同一个套路,但有几个参数在多集群场景下需要尤其注意。
retries 与会话超时:retries 默认是 2147483647,看起来很多,但结合 retry.backoff.ms 才能理解它的实际效果。多集群环境下某一个集群抖动时,会产生大量消息重试,如果同一批消息在本地积压过多,容易触发内存问题。我把 retries 设置成 3,配合 retry.backoff.ms=300,保证至少能容忍短时间抖动,又不至于无限重试下去。
linger.ms 和 batch.size:多集群生产者实例数变多了,单个实例的吞吐可以适当降低来换稳定性。我的经验是 linger.ms 设 5~10ms,batch.size 设 16KB~32KB。这样既不会因为等待太久增加延迟,也能让批处理效果保持在合理区间。
max.block.ms:这是容易被忽略的参数。默认 60 秒,意思是当缓冲区满了或元数据不可用时,producer.send() 方法最多阻塞这个时间,超时就抛异常。多集群里某个集群的 broker 故障可能导致它对应的 Producer 缓冲区迅速堆积,其他集群的生产者不会受影响,但那一瞬间业务线程可能大规模阻塞。把 max.block.ms 调到 2000~5000ms 可以在故障时更快暴露问题,而不是让请求全部卡死。
compression.type:建议设置成 lz4 或 zstd。多集群环境下网络带宽往往比本机房单集群更紧张,压缩能显著降低跨集群流量。
下面给一份我常用的生产者配置模板,新增集群时只需要改 bootstrap.servers 和部分特定参数。
Properties producerProps(String clusterName) { Properties props = new Properties(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, getBootstrapServers(clusterName)); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); props.put(ProducerConfig.ACKS_CONFIG, "all"); props.put(ProducerConfig.RETRIES_CONFIG, 3); props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 300); props.put(ProducerConfig.LINGER_MS_CONFIG, 5); props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); props.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 3000); props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4"); return props; }多集群实践里我还建议给每台生产者客户端设置 client.id,格式建议为:应用名-集群名-实例序号。这样在 Kafka 服务端看到连接来源时,可以立刻判断是哪套应用、哪个集群的哪个实例在发消息,排查问题效率会高很多。
3. 多消费者端的实现
3.1 消费者监听设计:一个集群一套监听线程
生产端解决了“往哪里发”,消费端要解决的是“从哪里收”。多消费者的核心问题不是“一个应用能启动多少消费者线程”,而是“消费者线程和集群、消费者组之间如何正确映射”。
KafkaConsumer 实例和 KafkaProducer 有一个相似点:一个实例只能连接一个集群。但消费端还有一个额外的限制——每个消费者线程必须持有独立的 KafkaConsumer 实例,因为 KafkaConsumer 不是线程安全的。你不能在一个线程里 poll,在另一个线程里调用 commitSync,更不能多个线程共享同一个 Consumer 实例做并行消费。
所以消费者的组织结构是这样的:
- 每个集群对应一个消费协调器;
- 每个消费协调器内部启动一个或多个消费线程;
- 每个消费线程持有一个独立的 KafkaConsumer 实例;
- 同一集群内同一消费组的多个线程各自订阅 topic 的不同分区,由 Kafka 的消费组协议自动分配;
- 不同集群的消费线程互不干扰,各自提交位移。
下面是一个消费协调器的简化实现。
public class ClusterConsumerCoordinator implements Closeable { private final String clusterName; private final List<ConsumerThread> threads = new ArrayList<>(); public ClusterConsumerCoordinator(String clusterName, Properties consumerProps, List<String> topics, MessageHandler handler, int threadCount) { this.clusterName = clusterName; for (int i = 0; i < threadCount; i++) { ConsumerThread thread = new ConsumerThread( clusterName, consumerProps, topics, handler, i ); threads.add(thread); } } public void start() { threads.forEach(ConsumerThread::start); } @Override public void close() { threads.forEach(ConsumerThread::shutdown); } }ConsumerThread 内部循环的逻辑是标准的 poll-处理-提交位移三段式。
public class ConsumerThread extends Thread { private final KafkaConsumer<String, String> consumer; private final MessageHandler handler; private final AtomicBoolean running = new AtomicBoolean(true); public ConsumerThread(String clusterName, Properties props, List<String> topics, MessageHandler handler, int threadIndex) { // 每个线程都 new 一个独立 Consumer 实例 this.consumer = new KafkaConsumer<>(props); this.handler = handler; // 每个线程订阅相同的 topic 列表,分区由消费组自动分配 consumer.subscribe(topics); } @Override public void run() { while (running.get()) { ConsumerRecords<String, String> records; try { records = consumer.poll(Duration.ofMillis(1000)); } catch (WakeupException e) { break; } for (ConsumerRecord<String, String> record : records) { try { handler.handle(record); } catch (Exception e) { // 单条消息异常不应阻断整个 poll 循环,取决于业务设计 } } // 批处理完成后手动提交位移 consumer.commitSync(); } } public void shutdown() { running.set(false); consumer.wakeup(); } }3.2 偏移量提交策略详解
Kafka 消费里有自动提交和手动提交两种模式。多集群环境下,我强烈建议关闭自动提交,全部改为手动提交。
为什么?自动提交是每隔 auto.commit.interval.ms(默认5秒)提交一次当前 poll 到的最大位移。如果处理一批消息需要 10 秒,自动提交可能在你还在处理时就提交了位移,一旦后面消费者崩溃重启,这 10 秒内的消息就永久丢失了。手动提交可以精确控制“处理完一批再提交上一批位移”,两者各有利弊,但对于多集群架构这种排查链路更复杂的场景,手动提交至少保证消息处理的“至少一次”语义清晰可解释。
手动提交我最常用的是 commitSync。它的缺点是 batch 处理完会阻塞 poll 循环(因为这个线程的 Consumer 正在被当前线程使用),但这个代价换来的是提交结果的确定性——要么提交成功,要么抛异常,不会出现回调里不知道到底有没有提交成功的情况。高吞吐场景下如果想降低提交阻塞的影响,可以每处理 N 条或每间隔几秒再提交一次,但必须保证同一 Consumer 实例只有提交线程在使用。
还有一个细节:commitSync 提交的是最后一次 poll 获取的所有分区的最新位移,不是单个消息的位移。如果你在循环里对每条消息处理完后都调一次 commitSync,会在性能上付出极大代价,而且也没必要。正确姿势是 batch 级别提交。
3.3 消费者组管理与多线程注意事项
消费者组是 Kafka 消费端最核心的协调机制。同一消费者组内的所有消费者线程共享订阅 topic 的全部分区,每条消息只会被组内一个消费者处理。不同消费者组之间完全隔离,都能收到全量消息。
多集群场景下,消费者组的命名一定要带上集群和应用标识。比如 core-cluster 的订单应用消费组可以叫 core-order-service,data-cluster 的日志应用消费组叫>kafka: clusters: core: bootstrap.servers: 10.10.20.11:9092,10.10.20.12:9092 security.protocol: SASL_PLAINTEXT sasl.mechanism: PLAIN sasl.jaas.username: core-app sasl.jaas.password: ****** consumer: group.id: core-order-service enable.auto.commit: false auto.offset.reset: latest thread.count: 4 producers: default: acks: all retries: 3 data: bootstrap.servers: 10.10.30.21:9092,10.10.30.22:9092,10.10.30.23:9092 security.protocol: SASL_PLAINTEXT sasl.mechanism: SCRAM-SHA-256 sasl.jaas.username:>public class KafkaClientManager implements Closeable { private final Map<String, ProducerRouter> producerRouters = new ConcurrentHashMap<>(); private final Map<String, ClusterConsumerCoordinator> consumerCoordinators = new ConcurrentHashMap<>(); public void init(GlobalConfig config) { // 生产端初始化 config.getClusters().forEach((name, clusterConfig) -> { ProducerRouter router = new ProducerRouter(); router.register(name, buildProducerProps(clusterConfig)); producerRouters.put(name, router); }); // 消费端初始化 config.getClusters().forEach((name, clusterConfig) -> { ClusterConsumerCoordinator coordinator = new ClusterConsumerCoordinator( name, buildConsumerProps(clusterConfig), clusterConfig.getTopics(), routeMessageHandler(clusterConfig), clusterConfig.getConsumerThreadCount()); coordinator.start(); consumerCoordinators.put(name, coordinator); }); } public ProducerRouter producer(String clusterName) { return producerRouters.get(clusterName); } @Override public void close() { consumerCoordinators.forEach((name, coordinator) -> coordinator.close()); producerRouters.forEach((name, router) -> router.close()); } }
这个管理器的关键点在于它做到了启动期全量初始化。如果你的应用是用 Spring,可以在 @PostConstruct 里调用 init,在 @PreDestroy 里调用 close。如果不用 Spring,也可以在 main 方法里手动调用 init,然后注册 shutdown hook。
有人会问,如果某个集群暂时不可用,启动期初始化失败,整个应用是不是就起不来了?看业务需要。我是这么处理的:生产端允许失败重试,但最多等 3 秒,超过就抛异常,避免应用带着不完整的生产链路半启动,结果业务调用时才发现发不出去。消费端则更宽松一点,如果集群不可用,可以先启动其他集群的消费监听,故障集群的消费线程标记为 pending,等集群恢复后再自动重建并加入消费组。这种“宽松消费 + 严格生产”的策略,在多集群实践中对可用性的帮助很明显。
4.3 优雅关闭的细节
多集群应用的发布重启最怕两件事:消费位移提交丢失、生产消息积压未刷出。优雅关闭就是解决这两个问题的。
关闭顺序应该是先关闭消费端、再关闭生产端。理由很简单:消费端关闭后,不再有新的下游请求进来,此时再 flush 生产端,把内存里积压的消息全部发出去,消息链路是完整的。如果先关生产端,消费端还在接收消息并且试图发往下游,这时生产者已经关了,消息就会在本地堆积甚至丢失。
生产端的 close(Duration) 这个带超时时间的重载方法一定要用。close() 默认行为是等到所有消息发送完成,但对于积压很严重的集群,可能等很久。设置一个合理的超时时间(比如 10 秒),超时后 KafkaProducer 会放弃未发送的消息并抛异常。这个策略不算完美,但比无限期阻塞强得多——至少应用不会因为一个故障集群的堆积而迟迟无法下线。
消费端关闭时要先停止 poll 循环,再调用 consumer.wakeup() 中断阻塞中的 poll,最后在退出前 commitSync 一次,把最后处理完的位移提交掉。注意 wakeup() 只能由其他线程调用,调用后当前 poll 会抛出 WakeupException,需要在 catch 里优雅退出。
5. 多集群实践中踩过的坑与排查思路
5.1 高频问题与诊断方法
下面是多集群接入过程中出现频率最高的几个问题,每条都配了排查思路。
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 生产端消息发送超时,错误日志频繁刷 metadata 请求 | bootstrap.servers 指向了错误的集群,Producer 无法获取 topic 元数据 | 检查配置中该集群的地址;用命令行客户端连接验证地址;查看服务端 broker 日志确认连接来源 |
| 消费端一条消息都不消费,group 状态显示一直在 rebalance | 消费线程数量(Consumer 实例数)频繁变化,或 group.id 配置不一致 | 检查所有消费线程的 group.id 是否一致;检查是否有其他实例重复注册同名 group;查看 rebalance 日志频率 |
| 两个集群的消息都到同一套处理器,但无法区分来源 | 路由逻辑按 topic 判断,但不同集群存在同名 topic | 在 MessageHandler 里显式传入 clusterName;消费消息时记录 record 来源而不是只靠 topic |
| 某个集群故障后,其他集群消费也变慢 | 线程池被故障集群的处理逻辑阻塞(比如同步发送阻塞) | 检查处理器中是否有阻塞调用;将不同集群的处理器线程池隔离 |
| 消费位移出现跳跃式前进,一小部分消息没被处理 | enable.auto.commit=true 且处理耗时超过提交间隔 | 关闭自动提交,改为 batch 处理后 commitSync;检查是否有多个消费线程误共享了同一个 Consumer 实例 |
| 生产者偶发 “Invalid metadata” 但客户端没重启 | 某个 topic 在新集群不存在,或集群名路由表没有正确映射该 topic | 检查 topic 是否在目标集群已创建;检查路由表是否配了新增 topic 的映射 |
5.2 集群故障隔离的具体做法
多集群最忌连带影响。一个集群的抖动不能拖垮所有集群的消息收发。我把故障隔离拆成几个层面:
第一层是线程隔离。每个消费协调器里的消费线程是独立的,A 集群消费线程 poll 超时或处理卡顿,最多影响 A 集群的消费速率,B 集群的消费线程可以继续跑。前提是处理器内部不能有跨集群的同步调用。比如 A 集群的消息处理函数里如果同步向 B 集群的某个接口发消息,B 集群慢了,A 的消费线程也会被拖慢。
第二层是缓冲隔离。生产端每个集群一个 Producer 实例,RecordAccumulator 缓冲是独立分配的,A 集群 Producer 因为 broker 故障导致缓冲区堆积,不会挤占 B 集群生产者的可用内存。
第三层是重试隔离。多集群接入时最好给每个集群单独配置重试次数和退避时间。例如核心集群因为重要,重试次数可以多些;日志集群对延迟不敏感,重试次数少些。统一配置管理里这一项是允许按集群覆盖的。
5.3 一个典型的故障复盘
说一个我印象比较深的故障。某次凌晨,数据中台集群的一个 broker 节点因为磁盘写满被自动踢出,那段时间正好有个大促活动,业务量较大。由于该集群消费线程数量较多,broker 掉线触发了连续多次 rebalance,消费者组一直处于亚健康状态,消费延迟从几秒飙升到接近一小时。
排查过程大概是这样:先看监控,数据集群消费延迟指标拉满;再看消费组日志,发现 rebalance 次数异常增多了几十次;最后定位到有一台应用实例因为网络抖动导致心跳超时,触发了组内成员变化。
这次的教训有三条:
消费线程数并不是越多越好。当时数据集群 topic 分区数是 14,消费线程数却开了 12 个,意味着任何一个消费线程异常退出都会导致大规模 rebalance 重新分配分区。如果只开 6~8 个线程,单一线程异常影响半径会小很多,但前提是单线程处理能力足够。
需要给消费线程设置合理的 max.poll.interval.ms。默认值是 5 分钟,如果单批消息处理时间经常超过 5 分钟,例如某次批量写数据库很慢,Consumer 会被判定为“死亡”并触发 rebalance。我把这个参数调整到 10 分钟,同时优化批量处理逻辑避免单批积压太多,大幅减少了误 rebalance。
监控一定要细到集群级别。全局的消费延迟监控图在多集群场景下几乎没有意义,必须把延迟拆到每个集群、每个消费组、每个 topic。否则像上面这种单集群故障,业务侧只看到“消息整体慢了”,很难定位是哪一个集群出了问题。
6. 从接入到运维:多集群方案的进一步扩展
6.1 跨集群消息溯源与消息幂等
多集群并存后,最让人头疼的是消息链路追踪。同一业务消息可能先进入核心集群,经过处理后转发到数据集群,再被下游系统消费。中途一旦出现消息丢失或重复处理,定位链路非常困难。
我的经验是在消息生产侧统一追加一个全局唯一的 messageId。这个 ID 贯穿整个消息生命周期,所有集群的消费处理器在打印日志或写入存储时必须带上它。这样即使消息跨了多个 Kafka 集群,也能通过 messageId 把所有日志串联起来。
ProducerRecord<String, String> record = new ProducerRecord<>(topic, key, value); record.headers().add("messageId", UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8));消费端读取 header,取出 messageId,在业务逻辑里作为幂等键使用。多集群环境下,消息可能因为重试、消费组切换等原因被重复消费,幂等键能拦住大部分重复问题。
6.2 消费线程数与分区数的动态调整
生产环境刚上线多集群接入时,每个集群的消费线程数是拍脑袋定的。我用了一段时间后发现,单纯靠静态配置很难适配所有流量场景。
后来做了一次优化:提供一个动态调整消费线程数的脚本接口。运维同学可以在控制台上针对某个集群修改 thread.count 参数,应用收到指令后先停止旧消费者线程、等待消费者组 rebalance 收敛、再启动新的消费者线程。整个过程对业务无感,但前提是每台机器上的消费者实例数不能超过分区总数太多,否则多余的线程只会空转,rebalance 成本却一点不少。
不过这个操作我建议还是少用。频繁调线程数等于频繁触发 rebalance,对线上稳定性有潜在风险。我更推荐根据长期监控数据确定一个合理值,然后通过增加分区数来提升并发度,而不是只靠调线程数。
6.3 监控告警与容量评估的量化建议
多集群接入方案上线后,运维侧至少要盯这几个指标:
- 每个集群的生产者发送成功率与失败率;
- 每个集群的消费延迟(当前时间与最新消费消息时间戳的差值);
- 每个集群的消费组 rebalance 次数与耗时;
- 每个集群的消费者线程空闲率与处理耗时;
- broker 端连接数,特别是 client.id 维度的连接数分布。
容量评估也要按集群算。假如核心集群每天消息量 2000 万条,单条 1KB,约每天 20GB;数据集群每天 1 亿条,约每天 100GB。不同集群的存储需求、带宽需求、分区数规划都不一样,不能用一个公式套所有集群。做多集群规划的时候,我建议每个新接入的集群都独立写一份容量评估表,至少包括吞吐预估、topic 分区数规划、消费端线程数上限和存储保留期。
有一点要特别注意:多集群方案上线后,broker 端看到的连接数会显著增长。假设你有两个集群,每个集群 4 个消费者线程,另外还有 4 个生产者实例,那么一台应用服务器至少会建立 2*4 + 2 = 10 个 Kafka 客户端长连接。如果部署了 20 台应用服务器,就是 200 个连接。这还只是两个集群的规模,集群数增多后连接数是线性增长的,broker 端的文件描述符和线程开销都要提前评估。
最后再分享一个很小的经验
我在做多集群接入的时候,有段时间总觉得代码没问题,可测试环境里消息就是不到达。查了很久才发现是配置文件里的 bootstrap.servers 写错了,把数据集群填成了生产集群的地址,导致所有消费者都连到了同一个集群上,而另一个集群的消费线程一直在空转。
从那以后,我养成一个习惯:每接入一个新集群,先在这个集群上用命令行工具手动生产一条消息、消费一条消息,确认连通性后,再启动应用里的 Producer 和 Consumer。这个过程只需要几分钟,但能避免在应用代码层面排查半天。多集群接入听着复杂,真正落地时把配置文件梳理透,把客户端实例管好,把监控指标拆细,稳定性就不会差。