持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第18天,点击查看活动详情
消费者正常读取数据,如图:
使用零拷贝读取数据,如图:
关于数据网络传输
Kafka
这种应用必然需要大量地通过网络与磁盘进行数据传输,而大部分这样的操作都是通过Java
的FileChannel.transferTo
方法实现的。
在
Linux
平台上该方法底层会调用sendfile
系统调用,即采用了Linux
提供的零拷贝(Zero Copy)技术
Kafka
大量依靠文件系统和磁盘来保存信息,但其实它还会对消息进行缓存,而这个消息缓存的地方就是内存,具体来说是操作系统的 页缓存(page cache)
-
Kafka
仅仅将消息写入page cache
而已,之后将消息 “冲刷” 到磁盘的任务完全交由操作系统来完成 -
consumer
在读取消息时也会首先尝试从该区域中查找,如果直接命中则完全不同执行耗时的物理 I/O 操作,从而提升了consumer
的整体性能。
page cache
大小: 假设单个日志段文件大小设置为10GB
,那page cache
至少10GB
以上的内存空间
(1)管理 TCP
连接
TCP
连接创建的 3个时机:
- 发起
FindCoordinator
请求时:确定协调者和获取集群元数据 - 连接协调者时:令其执行组成员管理操作
- 消费数据时:执行实际的消息获取
(2)多线程消费者实例
KafkaConsumer
不是线程安全的,能够制定两套多线程方案:
- 消费者程序启动多个线程,每个线程维护专属的
KafkaConsumer
实例,负责完整的消息获取、消息处理流程。
举例,代码如下:
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();
}
}
- .消费者程序使用单或多线程拉取消息,同时创建多个消费线程执行消息处理逻辑。
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));
}
}
..
- 主线程负责拉取数据,不负责处理逻辑
- 处理逻辑交给业务线程池
两种方案的优缺点如下:
(3)问题
1)消息缓冲区满的时候是阻塞住还是抛出异常?
如果缓冲区满了,此时会阻塞一段时间,过段时间还满则抛出异常。
缓冲区时间参数:max.block.ms
,默认 60秒。