【Kafka】消费者 - 读取消息

118 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第18天,点击查看活动详情

消费者正常读取数据,如图:

数据读取.png

使用零拷贝读取数据,如图:

数据读取2.png

关于数据网络传输

Kafka这种应用必然需要大量地通过网络与磁盘进行数据传输,而大部分这样的操作都是通过JavaFileChannel.transferTo方法实现的。

Linux平台上该方法底层会调用 sendfile 系统调用,即采用了 Linux 提供的零拷贝(Zero Copy)技术

Kafka 大量依靠文件系统和磁盘来保存信息,但其实它还会对消息进行缓存,而这个消息缓存的地方就是内存,具体来说是操作系统的 页缓存(page cache)

  1. Kafka 仅仅将消息写入 page cache 而已,之后将消息 “冲刷” 到磁盘的任务完全交由操作系统来完成

  2. consumer 在读取消息时也会首先尝试从该区域中查找,如果直接命中则完全不同执行耗时的物理 I/O 操作,从而提升了 consumer 的整体性能。

page cache 大小: 假设单个日志段文件大小设置为 10GB,那 page cache 至少 10GB 以上的内存空间

(1)管理 TCP 连接

TCP 连接创建的 3个时机:

  • 发起 FindCoordinator 请求时:确定协调者和获取集群元数据
  • 连接协调者时:令其执行组成员管理操作
  • 消费数据时:执行实际的消息获取

(2)多线程消费者实例

KafkaConsumer 不是线程安全的,能够制定两套多线程方案:

  1. 消费者程序启动多个线程,每个线程维护专属的 KafkaConsumer 实例,负责完整的消息获取、消息处理流程。

2022-06-1217-20-55.png

举例,代码如下:

public class KafkaConsumerRunner implements Runnable {
     private final AtomicBoolean closed = new AtomicBoolean(false);
     private final KafkaConsumer consumer;

     public void run() {
         try {
             consumer.subscribe(Arrays.asList("topic"));
             while (!closed.get()) {
                 ConsumerRecords records = 
                     consumer.poll(Duration.ofMillis(10000));
                 //  执行消息处理逻辑
             }
         } catch (WakeupException e) {
             // Ignore exception if closing
             if (!closed.get()) throw e;
         } finally {
             consumer.close();
         }
     }

     // Shutdown hook which can be called from a separate thread
     public void shutdown() {
         closed.set(true);
         consumer.wakeup();
     }
}
  1. .消费者程序使用单或多线程拉取消息,同时创建多个消费线程执行消息处理逻辑。

2022-06-1217-25-32.png

private final KafkaConsumer<String, String> consumer;
private ExecutorService executors;
...


private int workerNum = ...;
executors = new ThreadPoolExecutor(
  workerNum, workerNum, 0L, TimeUnit.MILLISECONDS,
  new ArrayBlockingQueue<>(1000), 
  new ThreadPoolExecutor.CallerRunsPolicy());


...
while (true)  {
  ConsumerRecords<String, String> records = 
    consumer.poll(Duration.ofSeconds(1));
  for (final ConsumerRecord record : records) {
    executors.submit(new Worker(record));
  }
}
..
  1. 主线程负责拉取数据,不负责处理逻辑
  2. 处理逻辑交给业务线程池

两种方案的优缺点如下:

2022-06-1217-26-17.png

(3)问题

1)消息缓冲区满的时候是阻塞住还是抛出异常?

如果缓冲区满了,此时会阻塞一段时间,过段时间还满则抛出异常。

缓冲区时间参数:max.block.ms,默认 60秒。