【杂谈】Kafka的日志段为什么不用内存映射?
什么是内存映射(Memory-Mapped File)?
内存映射(mmap)是一种将文件内容映射到内存中的技术,应用程序可以像操作内存一样对文件内容进行读写,而不需要显式地进行磁盘 I/O 操作。修改的内容会自动由操作系统同步到磁盘。
内存映射需要读取磁盘文件吗?
需要。毕竟,内存中的数据来源于磁盘文件。操作系统会将文件的部分或全部内容加载到内存,供程序访问。
为什么不直接读取文件?
直接读取文件,缓存到用户进程内,这样不也可以随意访问吗?相比这种方式,mmap有何优势?
1. 数据拷贝次数少
内存映射相比直接读取文件的一个主要优势是减少了数据拷贝的次数
- 内存映射:磁盘 => 内核空间
- 直接读取:磁盘 => 内核空间 => 用户空间
正常情况下,应用程序不能直接访问内核空间中的数据。要访问这些数据,通常需要触发系统调用将数据从内核空间拷贝到用户空间。
而内存映射通过将文件内容直接映射到进程的虚拟地址空间,消除了这种额外的拷贝开销,从而提高了效率。
2. 加载范围与按需加载
直接读取文件时,通常需要将整个文件加载到进程的内存缓存中,这对于大文件来说非常低效。而内存映射则更加高效,操作系统会根据需要按需加载文件的部分内容。
对于用户来说,内存映射的效果是可以像操作内存一样访问文件内容,而无需担心数据加载的问题。
3. 自动写回磁盘
- 内存映射:修改的内容会自动同步到磁盘,操作系统会处理文件内容的写回。
- 直接读取:如果是直接读取,文件内容的修改要么全部写回磁盘,要么应用程序需要识别哪些区域发生了变化并单独写回磁盘,这样的管理工作相对繁琐。
Kafka在哪里使用了内存映射?
从源码中可以看到,Kafka 只在索引文件中使用了内存映射(mmap)。内存映射的优势在于它允许随机访问,这与索引文件的应用场景非常匹配。
Kafka的索引文件通过二分法查找消息的存储位置,而内存映射的随机访问特性使得这个过程更加高效。

但是看源码可以发现,日志段则没有使用文件映射,而是直接使用FileChannel.write(buffer)写出数据。
//kafka 3.9.0部分源码
LogSegment.java
package org.apache.kafka.storage.internals.log
...
public class LogSegment implements Closeable {
...
private final FileRecords log;
...
/**
* Append the given messages starting with the given offset. Add
* an entry to the index if needed.
*
* It is assumed this method is being called from within a lock, it is not thread-safe otherwise.
*
* @param largestOffset The last offset in the message set
* @param largestTimestampMs The largest timestamp in the message set.
* @param shallowOffsetOfMaxTimestamp The last offset of earliest batch with max timestamp in the messages to append.
* @param records The log entries to append.
* @throws LogSegmentOffsetOverflowException if the largest offset causes index offset overflow
*/
public void append(long largestOffset,
long largestTimestampMs,
long shallowOffsetOfMaxTimestamp,
MemoryRecords records) throws IOException {
if (records.sizeInBytes() > 0) {
LOGGER.trace("Inserting {} bytes at end offset {} at position {} with largest timestamp {} at offset {}",
records.sizeInBytes(), largestOffset, log.sizeInBytes(), largestTimestampMs, shallowOffsetOfMaxTimestamp);
int physicalPosition = log.sizeInBytes();
if (physicalPosition == 0)
rollingBasedTimestamp = OptionalLong.of(largestTimestampMs);
ensureOffsetInRange(largestOffset);
// append the messages
long appendedBytes = log.append(records);
LOGGER.trace("Appended {} to {} at end offset {}", appendedBytes, log.file(), largestOffset);
// Update the in memory max timestamp and corresponding offset.
if (largestTimestampMs > maxTimestampSoFar()) {
maxTimestampAndOffsetSoFar = new TimestampOffset(largestTimestampMs, shallowOffsetOfMaxTimestamp);
}
// append an entry to the index (if needed)
// 稀疏索引,有一定的间隔。可以减少索引量
if (bytesSinceLastIndexEntry > indexIntervalBytes) {
offsetIndex().append(largestOffset, physicalPosition);
timeIndex().maybeAppend(maxTimestampSoFar(), shallowOffsetOfMaxTimestampSoFar());
bytesSinceLastIndexEntry = 0;
}
bytesSinceLastIndexEntry += records.sizeInBytes();
}
}
...
}
FileRecords.java
package org.apache.kafka.common.record;
...
public class FileRecords extends AbstractRecords implements Closeable {
...
private final FileChannel channel;
....
public int append(MemoryRecords records) throws IOException {
if (records.sizeInBytes() > Integer.MAX_VALUE - size.get())
throw new IllegalArgumentException("Append of size " + records.sizeInBytes() +
" bytes is too large for segment with current file position at " + size.get());
int written = records.writeFullyTo(channel);
size.getAndAdd(written);
return written;
}
...
}
MemoryRecords.java
package org.apache.kafka.common.record;
.... public class MemoryRecords extends AbstractRecords {
...
private final ByteBuffer buffer;
... /**
* Write all records to the given channel (including partial records).
* @param channel The channel to write to
* @return The number of bytes written
* @throws IOException For any IO errors writing to the channel
*/
public int writeFullyTo(GatheringByteChannel channel) throws IOException {
buffer.mark();
int written = 0;
while (written < sizeInBytes())
written += channel.write(buffer);
buffer.reset();
return written;
} ....
}
为什么日志段不使用内存映射?
按理说,直接读写内存不是更快吗?日志段为什么不使用内存映射。
1. 内存消耗过大
Kafka 每个主题和分区都有多个日志段文件。如果将所有日志段文件都映射到内存中,将消耗大量的内存资源。尤其是在日志数据量非常大的情况下,这种做法会极大增加内存的负担,可能会在内存受限的环境中不可行。
2. 顺序读写已足够高效
连续区域:Kafka 的写入和读取操作通常涉及批量消息,这些消息在磁盘上是按顺序存储的。由于数据在物理存储上是连续的,操作系统可以通过一次磁盘寻道就定位到所需的区域,从而减少寻道时间和开销。
页缓存(Page Cache):操系统的页缓存机制(Page Cache)能够将频繁访问的文件内容缓存到内存中。操作系统也会预读取一部分文件后续内容到缓存中,提高缓存命中的概率,避免频繁从磁盘加载数据。
零拷贝(sendfile):Kafka 的日志文件主要由远端消费者触发读取。由于日志在写入文件的时候都已经处理好了,而且读取也是顺序进行的,故Kafka Broker无需进行额外处理,数据可以直接从磁盘通过 sendfile() 系统调用发送到客户端,从内核直接拷贝到 socket 缓冲区,而不需要先载入到用户空间内存中。

总结
内存映射技术通过将文件内容映射到内存,有效避免了多次拷贝和高昂的 I/O 成本,非常适合需要随机访问的场景。然而,对于 Kafka 的日志段文件,顺序写入和读取已经足够高效,因此 Kafka 选择不使用内存映射,而是依赖操作系统的页缓存来提高性能。通过这种设计,Kafka 在内存消耗和 I/O 性能之间实现了良好的平衡。
参考内容
https://stackoverflow.com/questions/2100584/difference-between-sequential-write-and-random-write
https://storedbits.com/sequential-vs-random-data/
https://www.mail-archive.com/users@kafka.apache.org/msg30260.html
https://lists.freebsd.org/pipermail/freebsd-questions/2004-June/050371.html
【杂谈】Kafka的日志段为什么不用内存映射?的更多相关文章
- kafka学习笔记(四)kafka的日志模块
概述 日志段及其相关代码是 Kafka 服务器源码中最为重要的组件代码之一.你可能会非常关心,在 Kafka 中,消息是如何被保存和组织在一起的.毕竟,不管是学习任何消息引擎,弄明白消息建模方式都是首 ...
- Kafka日志段读写分析
引子 之所以写这篇文章是因为之前面试时候被面试官问到(倒)了,面试官说:"你说你对Kafka比较熟?看过源码? 那说说kafka日志段如何读写的吧?" 我心里默默的说了句 &quo ...
- Openresty+Lua+Kafka实现日志实时采集
简介 在很多数据采集场景下,Flume作为一个高性能采集日志的工具,相信大家都知道它.许多人想起Flume这个组件能联想到的大多数都是Flume跟Kafka相结合进行日志的采集,这种方案有很多他的优点 ...
- ELK+kafka构建日志收集系统
ELK+kafka构建日志收集系统 原文 http://lx.wxqrcode.com/index.php/post/101.html 背景: 最近线上上了ELK,但是只用了一台Redis在 ...
- ELK+Kafka 企业日志收集平台(一)
背景: 最近线上上了ELK,但是只用了一台Redis在中间作为消息队列,以减轻前端es集群的压力,Redis的集群解决方案暂时没有接触过,并且Redis作为消息队列并不是它的强项:所以最近将Redis ...
- 基于Flume+LOG4J+Kafka的日志采集架构方案
本文将会介绍如何使用 Flume.log4j.Kafka进行规范的日志采集. Flume 基本概念 Flume是一个完善.强大的日志采集工具,关于它的配置,在网上有很多现成的例子和资料,这里仅做简单说 ...
- 【Linux】浅谈段页式内存管理
让我们来回顾一下历史,在早期的计算机中,程序是直接运行在物理内存上的.换句话说,就是程序在运行的过程中访问的都是物理地址.如果这个系统只运行一个程序,那么只要这个程序所需的内存不要超过该机器的物理内存 ...
- JavaWeb项目架构之Kafka分布式日志队列
架构.分布式.日志队列,标题自己都看着唬人,其实就是一个日志收集的功能,只不过中间加了一个Kafka做消息队列罢了. kafka介绍 Kafka是由Apache软件基金会开发的一个开源流处理平台,由S ...
- ELK + kafka 分布式日志解决方案
概述 本文介绍使用ELK(elasticsearch.logstash.kibana) + kafka来搭建一个日志系统.主要演示使用spring aop进行日志收集,然后通过kafka将日志发送给l ...
- 【转】flume+kafka+zookeeper 日志收集平台的搭建
from:https://my.oschina.net/jastme/blog/600573 flume+kafka+zookeeper 日志收集平台的搭建 收藏 jastme 发表于 10个月前 阅 ...
随机推荐
- 云原生爱好者周刊:目前 WebAssembly 的最佳应用场景有哪些?
云原生一周动态要闻: Istio 1.11 发布 Facebook.Google.Isovalent.微软和 Netflix 宣布成立 eBPF 基金会 GitHub 工程团队将开发环境迁移到 Cod ...
- go: 在proto中使用oneof类型
在proto中,可以使用OneOf类型,使用一个字段存储不同类型的数据.类似go中的interface. 假设有proto如下,Val是一个OneOf数据类型,它可以为double/int/str.. ...
- HEU KMS:一款超稳的MS Windows激活工具
HEU KMS ACTIVATOR是一款功能齐全的免费Windows和Office系列激活工具. 打开程序,一步到位.直接点击开始就可以准备激活,Windows11/10乃至更早的版本都可以通用,顺带 ...
- Linux利用scp命令上传下载文件
scp是secure copy的简写,用于在 Linux 下进行远程拷贝文件的命令,和它类似的命令有cp,不过cp只是在本机进行拷贝不能跨服务器. scp传输是加密的,可能会稍微影响一下速度.当你服务 ...
- Protues中51单片机按键无法复位(已解决)
前言 昨晚用 Protues 搭建了 51 的最小系统电路,在实物中好用的复位电路,到仿真里不能正常复位了. 51 单片机是高电平复位,所以在运行时 RST 引脚应该是低电平,但在仿真中 RST 引脚 ...
- 买了个mini主机当服务器
虽然有苹果的电脑,但是在装一些软件的时候,从想着能不能有一个小型的服务器,免得各种设置什么帮我强各种别的导致Mac出现各种的异常,整体上的话去看了一些小的主机,看过苹果的MV迷你Mac,但是发现是太贵 ...
- spring生态体系
spring boot使用默认开发配置来实现快速开发spring xd用来简化大数据应用开发spring cloud为分布式系统开发提供工具集spring data对主流的关系型和nosql数据库的支 ...
- SpringBoot进阶教程(八十三)Kaptcha
Kaptcha是谷歌开源的一个可高度配置的比较老旧的实用验证码生成工具.它可以实现:(1)验证码的字体/大小颜色:(2)验证码内容的范围(数字,字母,中文汉字):(3)验证码图片的大小,边框,边框粗细 ...
- CF2027D The Endspeaker (Hard Version) 题解
题面 给你一个长度为 \(n\) 的数组 \(a\) 和一个长度为 \(m\) 的数组 \(b\) (所有 \(1 \le i < m\) 满足 \(b_i > b_{i+1}\) ).最 ...
- 痞子衡嵌入式:利用i.MXRT10xx系列内部DCP引擎计算CRC32值时需注意数据对齐
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是利用i.MXRT10xx系列内部DCP引擎计算CRC32值时需注意数据对齐. MCU 开发里常常需要 CRC 校验来检查数据完整性,CR ...