什么是内存映射(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的日志段为什么不用内存映射?的更多相关文章

  1. kafka学习笔记(四)kafka的日志模块

    概述 日志段及其相关代码是 Kafka 服务器源码中最为重要的组件代码之一.你可能会非常关心,在 Kafka 中,消息是如何被保存和组织在一起的.毕竟,不管是学习任何消息引擎,弄明白消息建模方式都是首 ...

  2. Kafka日志段读写分析

    引子 之所以写这篇文章是因为之前面试时候被面试官问到(倒)了,面试官说:"你说你对Kafka比较熟?看过源码? 那说说kafka日志段如何读写的吧?" 我心里默默的说了句 &quo ...

  3. Openresty+Lua+Kafka实现日志实时采集

    简介 在很多数据采集场景下,Flume作为一个高性能采集日志的工具,相信大家都知道它.许多人想起Flume这个组件能联想到的大多数都是Flume跟Kafka相结合进行日志的采集,这种方案有很多他的优点 ...

  4. ELK+kafka构建日志收集系统

    ELK+kafka构建日志收集系统   原文  http://lx.wxqrcode.com/index.php/post/101.html   背景: 最近线上上了ELK,但是只用了一台Redis在 ...

  5. ELK+Kafka 企业日志收集平台(一)

    背景: 最近线上上了ELK,但是只用了一台Redis在中间作为消息队列,以减轻前端es集群的压力,Redis的集群解决方案暂时没有接触过,并且Redis作为消息队列并不是它的强项:所以最近将Redis ...

  6. 基于Flume+LOG4J+Kafka的日志采集架构方案

    本文将会介绍如何使用 Flume.log4j.Kafka进行规范的日志采集. Flume 基本概念 Flume是一个完善.强大的日志采集工具,关于它的配置,在网上有很多现成的例子和资料,这里仅做简单说 ...

  7. 【Linux】浅谈段页式内存管理

    让我们来回顾一下历史,在早期的计算机中,程序是直接运行在物理内存上的.换句话说,就是程序在运行的过程中访问的都是物理地址.如果这个系统只运行一个程序,那么只要这个程序所需的内存不要超过该机器的物理内存 ...

  8. JavaWeb项目架构之Kafka分布式日志队列

    架构.分布式.日志队列,标题自己都看着唬人,其实就是一个日志收集的功能,只不过中间加了一个Kafka做消息队列罢了. kafka介绍 Kafka是由Apache软件基金会开发的一个开源流处理平台,由S ...

  9. ELK + kafka 分布式日志解决方案

    概述 本文介绍使用ELK(elasticsearch.logstash.kibana) + kafka来搭建一个日志系统.主要演示使用spring aop进行日志收集,然后通过kafka将日志发送给l ...

  10. 【转】flume+kafka+zookeeper 日志收集平台的搭建

    from:https://my.oschina.net/jastme/blog/600573 flume+kafka+zookeeper 日志收集平台的搭建 收藏 jastme 发表于 10个月前 阅 ...

随机推荐

  1. 手撸二叉树——AVL平衡二叉树

    还记得上一篇中我们遗留的问题吗?我们再简要回顾一下,现在有一颗空的二叉查找树,我们分别插入1,2,3,4,5,五个节点,那么得到的树是什么样子呢?这个不难想象,二叉树如下: 树的高度是4,并且数据结构 ...

  2. 一次生产 KubeSphere 日志无法正常采集事件解决记录

    作者:宇轩辞白,运维研发工程师,目前专注于云原生.Kubernetes.容器.Linux.运维自动化等领域. 前言 2023 年 11 月 7 号下午,研发同事反馈,项目线上日志平台某个服务无法查看近 ...

  3. 欢迎来到IoT解忧杂货铺

    这是一间特殊的杂货铺 门面不大,却包罗万物 如果你也遇到一些烦恼 欢迎来到,IoT解忧杂货铺 解忧秘方·工业 厂里的几十台设备真让人头疼 协议种类太多太复杂 设备没法全联网 产线故障了也不知道 自己出 ...

  4. ChatGPT:编程的 “蜜糖” 还是 “砒霜”?告别依赖,拥抱自主编程的秘籍在此!

    在当今编程界,ChatGPT 就像一颗耀眼却又颇具争议的新星,它对编程有着不可忽视的影响.但这影响就像一把双刃剑,使用不当,就可能让我们在编程之路上"受伤". 一.过度依赖 Cha ...

  5. pytest框架之fixture

    1.在进行接口关联时,一般很多个接口共用一个上行接口(例如)登录,可以使用fixture定义一个测试夹具,将登录的接口写在框架的conftest.py文件中: @pytest.fixture(scop ...

  6. delphi Image32 图片转换成SVG

    image32中有2种算法转换图像为svg,一种是按透明度计算找边缘,另一种是分析像素梯度找边缘,demo代码整理后如下: unit uFrmImageToSVG; interface uses Wi ...

  7. http: server gave HTTP response to HTTPS client

    出现这问题的原因是:Docker自从1.3.X之后docker registry交互默认使用的是HTTPS,但是搭建私有镜像默认使用的是HTTP服务,所以与私有镜像交时出现以上错误. 这个报错是在本地 ...

  8. 前端截图取色工具Snipaste

    在Web前端开发中,在写页面CSS样式时经常要用工具去取色来设置字体颜色.背景颜色.边框颜色等等,以还原设计图的最佳效果.今天给大家推荐的取色工具是Snipaste.Snipaste 是一个简单但强大 ...

  9. (Python基础教程之三)Python代码中添加注释

    Python基础教程 在SublimeEditor中配置Python环境 Python代码中添加注释 Python中的变量的使用 Python中的数据类型 Python中的关键字 Python字符串操 ...

  10. 优秀的 Java 程序员所应该知道的 Java 知识

    JDK 相关知识 JDK 的使用 JDK 源代码 JDK 相应技术背后的原理 JVM 相关知识 服务器端开发需要重点熟悉的 Java 技术 Java 并发 Java IO 开源框架 Java 之外的知 ...