KafkaProducer是线程安全的,可以多线程共用一个实例,而KafkaConsumer是非线程安全的,这点我对比了下KafkaProducer和KafkaConsumer中的成员变量。KafkaConsumer非线程安全的原因应该在于下面这个成员变量:
private boolean cachedSubscriptionHashAllFetchPositions;
cachedSubscriptionHashAllFetchPositions在每回poll方法调用时都会去判断订阅分区位移是否有效,有效为true,无效则在poll的指定时间内重置分区offset,这个参数多线程条件下修改是不安全的。
KafkaConsumer除了wakeUp()的其它公共方法调用时都会进行一次acquire()检测,类似于乐观锁的机制,判断是否被不同线程调用。acquire方法如下:
private void acquire() {
long threadId = Thread.currentThread().getId();
if (threadId != currentThread.get() && !currentThread.compareAndSet(NO_CURRENT_THREAD, threadId))
throw new ConcurrentModificationException("KafkaConsumer is not safe for multi-threaded access");
refcount.incrementAndGet();
}
通过本地维护一个原子变量currentThread来判断当前调用的线程id是否与之前存入的原子变量中的currentThread一致,若不一致则抛出异常。
消息消费多线程方式有以下几种:
1.线程封闭:每个线程实例化一个KafkaConsumer对象,每个KafkaConsumer实例可以消费一个或多个分区中的消息。由于之前介绍的默认情况下一个分区最多只能被一个消费者消费的关系,这种方式KafkaConsumer最多不能超过分区的数量,超过分区数量的消费者线程就会处于空闲状态。
2.assign、seek自定义消费流程。通过assign和seek方法可以打破消费线程不能超过分区数的限制,提高消费能力,但多个消费者消费同一分区时的位移提交和顺序处理会变得异常复杂,不推荐使用。
3.分离消息拉取与业务处理逻辑。采用A线程拉取消息,拉取到的消息丢入线程B或者线程池B进行处理,线程池B进行处理时需注意offsets读写需加锁,避免并发问题。同时考虑到会出现线程池消费数据0~99时异常未能提交offsets,但是消费数据100~199时正常提交offsets就会出现offsets覆盖的问题,所以滑动窗口的概念,就是说窗口中总共有0~99、100~199、200~299三组消息,三组消息同时在线程池B中进行业务逻辑处理,采用startOffset记录0~99所在位置数据,endOffsets记录200~299所在位置数据,只有当startOffset所在位置数据处理完之后,整个滑动窗口才能后移一格,如果100~199先处理完,那么需要等待0~199处理完之后整个窗口才能后移两格。为了避免startOffset所在窗口异常数据无法处理造成的停顿,还可以加上超时时间的设置。
上面几种多线程实现方式1是最简单的,3易于横向扩展消费能力的,并且减少了TCP连接,2逻辑处理麻烦,基本没啥用。
下表记录一些消费者端参数:
参数名称 | 默认值 | 参数释义 |
---|---|---|
fetch.min.bytes | 1(B) | 一次拉取请求拉取最小数据量 ,不足时会等待 |
fetch.max.bytes | 50(MB) | 一次拉取请求拉取最大数据量,单条消息若超过,则仍可以拉取(所以限制做啥用的?—保留疑问) |
fetch.max.wait.ms | 500(ms) | 一次拉取最长等待时间,与fetch.min.bytes对应,不足时的等待时间 |
max.partition.fetch.bytes | 1048576(B) | 每个分区返回的最大值,一次拉取中单个分区返回的消息大小限制,也是非强制,消息过大时仍可消费 |
max.poll.records | 500(条) | 一次拉取请求中拉取的最大消息数 |
connection.max.idle.ms | 540000(ms) | 多久之后关闭闲置的连接 |
exclude.internal.ms | true | 内部主题(__consumer_offsets和__transaction_state)是否可以向消费者公开,true代表不公开,false代表公开,公开则可以通过订阅正则表达式的方式订阅到内部主题 |
receive.buffer.bytes | 65536(B) | Socket接收消息缓冲区的大小,设为-1时代表使用操作系统默认值 |
send.buffer.bytes | 131072(B) | Socket发送消息缓冲区大小,设为-1时采用操作系统默认值 |
request.timeout.ms | 30000(ms) | Consumer等待请求响应的最长时间 |
metadata.max.age.ms | 300000(ms) | 元数据强制更新的间隔时间 |
reconnect.backoff.ms | 50(ms) | 连接失败重试的间隔时间 |
retry.backoff.ms | 100(ms) | 发送请求失败重试的间隔时间 |
isolation.level | read_uncommitted | 事务隔离级别 |
bootstrap.servers | 连接kafka集群所需的broker地址清单 | |
key.deserializer | key对应的反序列化类 | |
value.deserializer | value对应的反序列化类 | |
group.id | 消费组名称 | |
client.id | 消费者客户端id | |
heartbeat.interval.ms | 3000 | 消费者到消费者组协调器的心跳包时间 |
session.timeout.ms | 10000 | 组管理协议中检测消费者失效的超时时间 |
max.poll.interval.ms | 300000 | poll拉取消息的最长空闲时间,超时则认为消费者挂掉了,将其剔除出消费组 |
auto.offset.reset | latest | 消费位移自动重置的选项,可为earliest、latest或none |
enable.auto.commit | true | 是否开启自动提交消费位移功能 |
auto.commit.interval.ms | 5000 | 开启自动提交消费位移时,自动提交的时间间隔 |
partition.assignment.strategy | org.apache.kafka.clients.consumer.RangeAssignor | 消费者的分区策略 |
interceptor.class | 消费者客户端的拦截器 |