踩坑①
现象:新版生产者发不出去消息,broker端也没收到消息
原因:旧版生产者有个配置"producer.type",async为异步发送,sync为同步发送,默认为同步发送;而新版本废弃了该配置,于是每次调用send方法时候会将消息缓存在本地的buffer中而不是立即发送,只有等到消息总大小或到达批处理发送的间隔时间才会把消息发出去,而发送代码如下:
ZzKafkaProducer producer = KafkaManager.registerKafkaProducer("lxr", "xxx.xxx.xxx.1:9092,xxx.xxx.xxx.2:9092,xxx.xxx.xxx.3:9092"); for (int i = 0; i <= 100; i++) {
producer.send("k1", "lxrlxr" + i + " 0");
}
System.out.println("finish!");
使用旧版不会有问题,而使用新版本的生产者就会有问题,消息没来得及发送出去就因程序执行完了而清理掉,可以通过设置batch.size来自定义批量发送的大小
ps:这里新版默认是大于1.x.x的,旧版默认为小于1.x.x的
踩坑②
现象:消费者端阻塞,消费不到数据,不抛异常,服务器上的zookeeper会不断打印如下日志
原因:使用新版消费者的连接配置为bootstrap.servers,对应的是broker的ip与端口,误使用了zookeeper的ip与端口,便会导致一直打开zk连接而zk会自动断开连接
踩坑③
现象:多线程下消费者一运行就抛ConcurrentModificationException
原因:新版消费者不是线程安全的,一个消费者只能对应一个线程去消费,KafkaConsumer很多绝大多数方法执行前都会调用如下方法:
private final AtomicInteger refcount = new AtomicInteger(0);
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();
}
方法执行后会调用如下方法:
private void release() {
if (refcount.decrementAndGet() == 0)
currentThread.set(NO_CURRENT_THREAD);
}
相当于jdk的Lock接口的lock()与unlock()之间的关系
踩坑④
现象:生产者发送数据都成功,消费者也能消费,但必定会丢一部分数据,而且客户端也没报任何异常,到服务器查看 broker与zookeeper的日志均没任何异常信息
原因:kafka客户端新旧版本2个版本的生产者与消费者之间不兼容,生产者使用老版本(kafka.javaapi.producer.Producer),而消费者使用的是新版本(org.apache.kafka.clients.consumer.Consumer)
踩坑⑤
现象:在某个时间段内,消费者消费数据速度越来越慢,最后不消费,到zookeeper上看发现消费者全部死亡
原因:消费者端为保证每个分区的消费者消费数据的进度都差不多,于是当有分区消费速度比其他分区的快的时候,在消费者线程执行了Thread.sleep()方法,导致该分区消费者一直在sleep,而kafka消费者端有个delay task是做客户端与服务端的心跳任务的,该delay task是每次消费者到broker拉取数据时候触发的,消费者一直在sleep,因此没去拉取数据,delay task没触发,导致心跳任务没执行,broker端认为该消费者死亡,从zookeeper中移除了
消费速度远低于生产速度或各个分区消费能力差别较大时候的解决方案
上面之所以会有踩坑⑤,归根到底就是生产者生产消息速度远大于消费者的消费能力,且各个分区消费者的消费能力差别大,因此需要解决消费者端的问题
1.第一种方式,最简单粗暴,直接给topic增加分区,增加消费者数量,这个方案的缺点就是,当生产者的生产速度不是恒定的时候,比如早上是消息生产的高峰期,而中午是低峰期,高峰与低峰消息数量差距很大,这种方案会导致资源闲置,浪费资源
2.第二种方式,比较适合实时性要求比较高且容许丢失部分数据的场景,可以要求生产者端发送消息时候带上create_time字段,对于消费能力差的,直接丢弃一批过时or不满足要求的数据,直接跳过去拉取较新的数据
3.第三种方式,kafka的消费者api提供了pause方法,可以暂停消费某个topic指定的分区数据,但是delay task依旧会触发,消费者不会被认为是死亡,但是这种方式,需要消费者线程之间进行通信,如:发现分区1消费进度比其他分区快很多时,分区1的消费者线程调用pause方法暂停消费,当其他分区消费进度赶上来时候,分区1的消费者线程再调用resume方法继续消费,实现起来较为复杂
4.第四种方式,压轴的放最后,使用spring-kafka吧,spring-kafka会为消费者线程创建一个阻塞队列,从broker拉取数据后就丢到阻塞队列中,然后再从阻塞队列取数据进行处理,并且如果阻塞队列满了导致取到的数据塞不进去的话,spring-kafka会调用kafka的pause方法,则consumer会停止从kafka里面继续再拿数据,接着spring-kafka还会处理一些异常的情况,比如失败之后是不是需要commit offset这样的逻辑等等
这部分完~