1. 程式人生 > >Java I/O體系從原理到應用,這一篇全說清楚了

Java I/O體系從原理到應用,這一篇全說清楚了

本文介紹作業系統I/O工作原理,Java I/O設計,基本使用,開源專案中實現高效能I/O常見方法和實現,徹底搞懂高效能I/O之道

基礎概念

在介紹I/O原理之前,先重溫幾個基礎概念:

  • (1) 作業系統與核心

作業系統:管理計算機硬體與軟體資源的系統軟體
核心:作業系統的核心軟體,負責管理系統的程序、記憶體、裝置驅動程式、檔案和網路系統等等,為應用程式提供對計算機硬體的安全訪問服務

  • 2 核心空間和使用者空間

為了避免使用者程序直接操作核心,保證核心安全,作業系統將記憶體定址空間劃分為兩部分:
核心空間(Kernel-space),供核心程式使用
使用者空間(User-space),供使用者程序使用
為了安全,核心空間和使用者空間是隔離的,即使使用者的程式崩潰了,核心也不受影響

  • 3 資料流

計算機中的資料是基於隨著時間變換高低電壓訊號傳輸的,這些資料訊號連續不斷,有著固定的傳輸方向,類似水管中水的流動,因此抽象資料流(I/O流)的概念:指一組有順序的、有起點和終點的位元組集合,

抽象出資料流的作用:實現程式邏輯與底層硬體解耦,通過引入資料流作為程式與硬體裝置之間的抽象層,面向通用的資料流輸入輸出介面程式設計,而不是具體硬體特性,程式和底層硬體可以獨立靈活替換和擴充套件

I/O 工作原理

1 磁碟I/O

典型I/O讀寫磁碟工作原理如下:

tips: DMA:全稱叫直接記憶體存取(Direct Memory Access),是一種允許外圍裝置(硬體子系統)直接訪問系統主記憶體的機制。基於 DMA 訪問方式,系統主記憶體與硬體裝置的資料傳輸可以省去CPU 的全程排程

值得注意的是:

  • 讀寫操作基於系統呼叫實現
  • 讀寫操作經過使用者緩衝區,核心緩衝區,應用程序並不能直接操作磁碟
  • 應用程序讀操作時需阻塞直到讀取到資料

2 網路I/O

這裡先以最經典的阻塞式I/O模型介紹:

tips:recvfrom,經socket接收資料的函式

值得注意的是:

  • 網路I/O讀寫操作經過使用者緩衝區,Sokcet緩衝區
  • 服務端執行緒在從呼叫recvfrom開始到它返回有資料報準備好這段時間是阻塞的,recvfrom返回成功後,執行緒開始處理資料報

Java I/O設計

1 I/O分類

Java中對資料流進行具體化和實現,關於Java資料流一般關注以下幾個點:

  • (1) 流的方向
    從外部到程式,稱為輸入流;從程式到外部,稱為輸出流

  • (2) 流的資料單位
    程式以位元組作為最小讀寫資料單元,稱為位元組流,以字元作為最小讀寫資料單元,稱為字元流

  • (3) 流的功能角色

從/向一個特定的IO裝置(如磁碟,網路)或者儲存物件(如記憶體陣列)讀/寫資料的流,稱為節點流;
對一個已有流進行連線和封裝,通過封裝後的流來實現資料的讀/寫功能,稱為處理流(或稱為過濾流);

2 I/O操作介面

java.io包下有一堆I/O操作類,初學時看了容易搞不懂,其實仔細觀察其中還是有規律:
這些I/O操作類都是在繼承4個基本抽象流的基礎上,要麼是節點流,要麼是處理流

2.1 四個基本抽象流

java.io包中包含了流式I/O所需要的所有類,java.io包中有四個基本抽象流,分別處理位元組流和字元流:

  • InputStream
  • OutputStream
  • Reader
  • Writer

2.2 節點流

節點流I/O類名由節點流型別 + 抽象流型別組成,常見節點型別有:

  • File檔案
  • Piped 程序內執行緒通訊管道
  • ByteArray / CharArray (位元組陣列 / 字元陣列)
  • StringBuffer / String (字串緩衝區 / 字串)

節點流的建立通常是在建構函式傳入資料來源,例如:

FileReader reader = new FileReader(new File("file.txt"));
FileWriter writer = new FileWriter(new File("file.txt"));

2.3 處理流

處理流I/O類名由對已有流封裝的功能 + 抽象流型別組成,常見功能有:

  • 緩衝:對節點流讀寫的資料提供了緩衝的功能,資料可以基於緩衝批量讀寫,提高效率。常見有BufferedInputStream、BufferedOutputStream
  • 位元組流轉換為字元流:由InputStreamReader、OutputStreamWriter實現
  • 位元組流與基本型別資料相互轉換:這裡基本資料型別資料如int、long、short,由DataInputStream、DataOutputStream實現
  • 位元組流與物件例項相互轉換:用於實現物件序列化,由ObjectInputStream、ObjectOutputStream實現

處理流的應用了介面卡/裝飾模式,轉換/擴充套件已有流,處理流的建立通常是在建構函式傳入已有的節點流或處理流:

FileOutputStream fileOutputStream = new FileOutputStream("file.txt");
// 擴充套件提供緩衝寫
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
 // 擴充套件提供提供基本資料型別寫
DataOutputStream out = new DataOutputStream(bufferedOutputStream);

3 Java NIO

3.1 標準I/O存在問題

Java NIO(New I/O)是一個可以替代標準Java I/O API的IO API(從Java 1.4開始),Java NIO提供了與標準I/O不同的I/O工作方式,目的是為了解決標準 I/O存在的以下問題:

  • (1) 資料多次拷貝

標準I/O處理,完成一次完整的資料讀寫,至少需要從底層硬體讀到核心空間,再讀到使用者檔案,又從使用者空間寫入核心空間,再寫入底層硬體

此外,底層通過write、read等函式進行I/O系統呼叫時,需要傳入資料所在緩衝區起始地址和長度
由於JVM GC的存在,導致物件在堆中的位置往往會發生移動,移動後傳入系統函式的地址引數就不是真正的緩衝區地址了

可能導致讀寫出錯,為了解決上面的問題,使用標準I/O進行系統呼叫時,還會額外導致一次資料拷貝:把資料從JVM的堆內拷貝到堆外的連續空間記憶體(堆外記憶體)

所以總共經歷6次資料拷貝,執行效率較低

  • (2) 操作阻塞

傳統的網路I/O處理中,由於請求建立連線(connect),讀取網路I/O資料(read),傳送資料(send)等操作是執行緒阻塞的

// 等待連線
Socket socket = serverSocket.accept();

// 連線已建立,讀取請求訊息
StringBuilder req = new StringBuilder();
byte[] recvByteBuf = new byte[1024];
int len;
while ((len = socket.getInputStream().read(recvByteBuf)) != -1) {
    req.append(new String(recvByteBuf, 0, len, StandardCharsets.UTF_8));
}

// 寫入返回訊息
socket.getOutputStream().write(("server response msg".getBytes()));
socket.shutdownOutput();

以上面服務端程式為例,當請求連線已建立,讀取請求訊息,服務端呼叫read方法時,客戶端資料可能還沒就緒(例如客戶端資料還在寫入中或者傳輸中),執行緒需要在read方法阻塞等待直到資料就緒

為了實現服務端併發響應,每個連線需要獨立的執行緒單獨處理,當併發請求量大時為了維護連線,記憶體、執行緒切換開銷過大

3.2 Buffer

Java NIO核心三大核心元件是Buffer(緩衝區)、Channel(通道)、Selector

Buffer提供了常用於I/O操作的位元組緩衝區,常見的快取區有ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分別對應基本資料型別: byte, char, double, float, int, long, short,下面介紹主要以最常用的ByteBuffer為例,Buffer底層基於Java堆外記憶體

堆外記憶體是指與堆記憶體相對應的,把記憶體物件分配在JVM堆以外的記憶體,這些記憶體直接受作業系統管理(而不是虛擬機器,相比堆內記憶體,I/O操作中使用堆外記憶體的優勢在於:

  • 不用被JVM GC線回收,減少GC執行緒資源佔有
  • 在I/O系統呼叫時,直接操作堆外記憶體,可以節省一次堆外記憶體和堆內記憶體的複製

ByteBuffer底層的分配和釋放基於malloc和free函式,對外allocateDirect方法可以申請分配堆外記憶體,並返回繼承ByteBuffer類的DirectByteBuffer物件:

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

堆外記憶體的回收基於DirectByteBuffer的成員變數Cleaner類,提供clean方法可以用於主動回收,Netty中大部分堆外記憶體通過記錄定位Cleaner的存在,主動呼叫clean方法來回收;
另外,當DirectByteBuffer物件被GC時,關聯的堆外記憶體也會被回收

tips: JVM引數不建議設定-XX:+DisableExplicitGC,因為部分依賴Java NIO的框架(例如Netty)在記憶體異常耗盡時,會主動呼叫System.gc(),觸發Full GC,回收DirectByteBuffer物件,作為回收堆外記憶體的最後保障機制,設定該引數之後會導致在該情況下堆外記憶體得不到清理

堆外記憶體基於基礎ByteBuffer類的DirectByteBuffer類成員變數:Cleaner物件,這個Cleaner物件會在合適的時候執行unsafe.freeMemory(address),從而回收這塊堆外記憶體

Buffer可以見到理解為一組基本資料型別,儲存地址連續的的陣列,支援讀寫操作,對應讀模式和寫模式,通過幾個變數來儲存這個資料的當前位置狀態:capacity、 position、 limit:

  • capacity 緩衝區陣列的總長度
  • position 下一個要操作的資料元素的位置
  • limit 緩衝區陣列中不可操作的下一個元素的位置:limit <= capacity

3.3 Channel

Channel(通道)的概念可以類比I/O流物件,NIO中I/O操作主要基於Channel:
從Channel進行資料讀取 :建立一個緩衝區,然後請求Channel讀取資料
從Channel進行資料寫入 :建立一個緩衝區,填充資料,請求Channel寫入資料

Channel和流非常相似,主要有以下幾點區別:

  • Channel可以讀和寫,而標準I/O流是單向的
  • Channel可以非同步讀寫,標準I/O流需要執行緒阻塞等待直到讀寫操作完成
  • Channel總是基於緩衝區Buffer讀寫

Java NIO中最重要的幾個Channel的實現:

  • FileChannel: 用於檔案的資料讀寫,基於FileChannel提供的方法能減少讀寫檔案資料拷貝次數,後面會介紹
  • DatagramChannel: 用於UDP的資料讀寫
  • SocketChannel: 用於TCP的資料讀寫,代表客戶端連線
  • ServerSocketChannel: 監聽TCP連線請求,每個請求會建立會一個SocketChannel,一般用於服務端

基於標準I/O中,我們第一步可能要像下面這樣獲取輸入流,按位元組把磁碟上的資料讀取到程式中,再進行下一步操作,而在NIO程式設計中,需要先獲取Channel,再進行讀寫

FileInputStream fileInputStream = new FileInputStream("test.txt");
FileChannel channel = fileInputStream.channel();

tips: FileChannel僅能執行在阻塞模式下,檔案非同步處理的 I/O 是在JDK 1.7 才被加入的 java.nio.channels.AsynchronousFileChannel

// server socket channel:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), 9091));

while (true) {
    SocketChannel socketChannel = serverSocketChannel.accept();
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    int readBytes = socketChannel.read(buffer);
    if (readBytes > 0) {
        // 從寫資料到buffer翻轉為從buffer讀資料
        buffer.flip();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        String body = new String(bytes, StandardCharsets.UTF_8);
        System.out.println("server 收到:" + body);
    }
}

3.4 Selector

Selector(選擇器) ,它是Java NIO核心元件中的一個,用於檢查一個或多個NIO Channel(通道)的狀態是否處於可讀、可寫。實現單執行緒管理多個Channel,也就是可以管理多個網路連線

Selector核心在於基於作業系統提供的I/O複用功能,單個執行緒可以同時監視多個連線描述符,一旦某個連線就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作,常見有select、poll、epoll等不同實現

Java NIO Selector基本工作原理如下:

  • (1) 初始化Selector物件,服務端ServerSocketChannel物件
  • (2) 向Selector註冊ServerSocketChannel的socket-accept事件
  • (3) 執行緒阻塞於selector.select(),當有客戶端請求服務端,執行緒退出阻塞
  • (4) 基於selector獲取所有就緒事件,此時先獲取到socket-accept事件,向Selector註冊客戶端SocketChannel的資料就緒可讀事件事件
  • (5) 執行緒再次阻塞於selector.select(),當有客戶端連線資料就緒,可讀
  • (6) 基於ByteBuffer讀取客戶端請求資料,然後寫入響應資料,關閉channel

示例如下,完整可執行程式碼已經上傳github(https://github.com/caison/caison-blog-demo):

Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9091));
// 配置通道為非阻塞模式
serverSocketChannel.configureBlocking(false);
// 註冊服務端的socket-accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    // selector.select()會一直阻塞,直到有channel相關操作就緒
    selector.select();
    // SelectionKey關聯的channel都有就緒事件
    Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();

    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        // 服務端socket-accept
        if (key.isAcceptable()) {
            // 獲取客戶端連線的channel
            SocketChannel clientSocketChannel = serverSocketChannel.accept();
            // 設定為非阻塞模式
            clientSocketChannel.configureBlocking(false);
            // 註冊監聽該客戶端channel可讀事件,併為channel關聯新分配的buffer
            clientSocketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocateDirect(1024));
        }

        // channel可讀
        if (key.isReadable()) {
            SocketChannel socketChannel = (SocketChannel) key.channel();
            ByteBuffer buf = (ByteBuffer) key.attachment();

            int bytesRead;
            StringBuilder reqMsg = new StringBuilder();
            while ((bytesRead = socketChannel.read(buf)) > 0) {
                // 從buf寫模式切換為讀模式
                buf.flip();
                int bufRemain = buf.remaining();
                byte[] bytes = new byte[bufRemain];
                buf.get(bytes, 0, bytesRead);
                // 這裡當資料包大於byteBuffer長度,有可能有粘包/拆包問題
                reqMsg.append(new String(bytes, StandardCharsets.UTF_8));
                buf.clear();
            }
            System.out.println("服務端收到報文:" + reqMsg.toString());
            if (bytesRead == -1) {
                byte[] bytes = "[這是服務回的報文的報文]".getBytes(StandardCharsets.UTF_8);

                int length;
                for (int offset = 0; offset < bytes.length; offset += length) {
                    length = Math.min(buf.capacity(), bytes.length - offset);
                    buf.clear();
                    buf.put(bytes, offset, length);
                    buf.flip();
                    socketChannel.write(buf);
                }
                socketChannel.close();
            }
        }
        // Selector不會自己從已selectedKeys中移除SelectionKey例項
        // 必須在處理完通道時自己移除 下次該channel變成就緒時,Selector會再次將其放入selectedKeys中
        keyIterator.remove();
    }
}

tips: Java NIO基於Selector實現高效能網路I/O這塊使用起來比較繁瑣,使用不友好,一般業界使用基於Java NIO進行封裝優化,擴充套件豐富功能的Netty框架來優雅實現

高效能I/O優化

下面結合業界熱門開源專案介紹高效能I/O的優化

1 零拷貝

零拷貝(zero copy)技術,用於在資料讀寫中減少甚至完全避免不必要的CPU拷貝,減少記憶體頻寬的佔用,提高執行效率,零拷貝有幾種不同的實現原理,下面介紹常見開源專案中零拷貝實現

1.1 Kafka零拷貝

Kafka基於Linux 2.1核心提供,並在2.4 核心改進的的sendfile函式 + 硬體提供的DMA Gather Copy實現零拷貝,將檔案通過socket傳送

函式通過一次系統呼叫完成了檔案的傳送,減少了原來read/write方式的模式切換。同時減少了資料的copy, sendfile的詳細過程如下:

基本流程如下:

  • (1) 使用者程序發起sendfile系統呼叫
  • (2) 核心基於DMA Copy將檔案資料從磁碟拷貝到核心緩衝區
  • (3) 核心將核心緩衝區中的檔案描述資訊(檔案描述符,資料長度)拷貝到Socket緩衝區
  • (4) 核心基於Socket緩衝區中的檔案描述資訊和DMA硬體提供的Gather Copy功能將核心緩衝區資料複製到網絡卡
  • (5) 使用者程序sendfile系統呼叫完成並返回

相比傳統的I/O方式,sendfile + DMA Gather Copy方式實現的零拷貝,資料拷貝次數從4次降為2次,系統呼叫從2次降為1次,使用者程序上下文切換次數從4次變成2次DMA Copy,大大提高處理效率

Kafka底層基於java.nio包下的FileChannel的transferTo:

public abstract long transferTo(long position, long count, WritableByteChannel target)

transferTo將FileChannel關聯的檔案傳送到指定channel,當Comsumer消費資料,Kafka Server基於FileChannel將檔案中的訊息資料傳送到SocketChannel

1.2 RocketMQ零拷貝

RocketMQ基於mmap + write的方式實現零拷貝:
mmap() 可以將核心中緩衝區的地址與使用者空間的緩衝區進行對映,實現資料共享,省去了將資料從核心緩衝區拷貝到使用者緩衝區

tmp_buf = mmap(file, len); 
write(socket, tmp_buf, len);

mmap + write 實現零拷貝的基本流程如下:

  • (1) 使用者程序向核心發起系統mmap呼叫
  • (2) 將使用者程序的核心空間的讀緩衝區與使用者空間的快取區進行記憶體地址對映
  • (3) 核心基於DMA Copy將檔案資料從磁碟複製到核心緩衝區
  • (4) 使用者程序mmap系統呼叫完成並返回
  • (5) 使用者程序向核心發起write系統呼叫
  • (6) 核心基於CPU Copy將資料從核心緩衝區拷貝到Socket緩衝區
  • (7) 核心基於DMA Copy將資料從Socket緩衝區拷貝到網絡卡
  • (8) 使用者程序write系統呼叫完成並返回

RocketMQ中訊息基於mmap實現儲存和載入的邏輯寫在org.apache.rocketmq.store.MappedFile中,內部實現基於nio提供的java.nio.MappedByteBuffer,基於FileChannel的map方法得到mmap的緩衝區:

// 初始化
this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);

查詢CommitLog的訊息時,基於mappedByteBuffer偏移量pos,資料大小size查詢:

public SelectMappedBufferResult selectMappedBuffer(int pos, int size) {
    int readPosition = getReadPosition();
    // ...各種安全校驗
    
    // 返回mappedByteBuffer檢視
    ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
    byteBuffer.position(pos);
    ByteBuffer byteBufferNew = byteBuffer.slice();
    byteBufferNew.limit(size);
    return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this);
}

tips: transientStorePoolEnable機制
Java NIO mmap的部分記憶體並不是常駐記憶體,可以被置換到交換記憶體(虛擬記憶體),RocketMQ為了提高訊息傳送的效能,引入了記憶體鎖定機制,即將最近需要操作的CommitLog檔案對映到記憶體,並提供記憶體鎖定功能,確保這些檔案始終存在記憶體中,該機制的控制引數就是transientStorePoolEnable

因此,MappedFile資料儲存CommitLog刷盤有2種方式:

  • 1 開啟transientStorePoolEnable:寫入記憶體位元組緩衝區(writeBuffer) -> 從記憶體位元組緩衝區(writeBuffer)提交(commit)到檔案通道(fileChannel) -> 檔案通道(fileChannel) -> flush到磁碟
  • 2 未開啟transientStorePoolEnable:寫入對映檔案位元組緩衝區(mappedByteBuffer) -> 對映檔案位元組緩衝區(mappedByteBuffer) -> flush到磁碟

RocketMQ 基於 mmap+write 實現零拷貝,適用於業務級訊息這種小塊檔案的資料持久化和傳輸
Kafka 基於 sendfile 這種零拷貝方式,適用於系統日誌訊息這種高吞吐量的大塊檔案的資料持久化和傳輸

tips: Kafka 的索引檔案使用的是 mmap+write 方式,資料檔案傳送網路使用的是 sendfile 方式

1.3 Netty零拷貝

Netty 的零拷貝分為兩種:

  • 1 基於作業系統實現的零拷貝,底層基於FileChannel的transferTo方法
  • 2 基於Java 層操作優化,對陣列快取物件(ByteBuf )進行封裝優化,通過對ByteBuf資料建立資料檢視,支援ByteBuf 物件合併,切分,當底層僅保留一份資料儲存,減少不必要拷貝

2 多路複用

Netty中對Java NIO功能封裝優化之後,實現I/O多路複用程式碼優雅了很多:

// 建立mainReactor
NioEventLoopGroup boosGroup = new NioEventLoopGroup();
// 建立工作執行緒組
NioEventLoopGroup workerGroup = new NioEventLoopGroup();

final ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap 
     // 組裝NioEventLoopGroup 
    .group(boosGroup, workerGroup)
     // 設定channel型別為NIO型別
    .channel(NioServerSocketChannel.class)
    // 設定連線配置引數
    .option(ChannelOption.SO_BACKLOG, 1024)
    .childOption(ChannelOption.SO_KEEPALIVE, true)
    .childOption(ChannelOption.TCP_NODELAY, true)
    // 配置入站、出站事件handler
    .childHandler(new ChannelInitializer<NioSocketChannel>() {
        @Override
        protected void initChannel(NioSocketChannel ch) {
            // 配置入站、出站事件channel
            ch.pipeline().addLast(...);
            ch.pipeline().addLast(...);
        }
    });

// 繫結埠
int port = 8080;
serverBootstrap.bind(port).addListener(future -> {
    if (future.isSuccess()) {
        System.out.println(new Date() + ": 埠[" + port + "]繫結成功!");
    } else {
        System.err.println("埠[" + port + "]繫結失敗!");
    }
});

3 頁快取(PageCache)

頁快取(PageCache)是作業系統對檔案的快取,用來減少對磁碟的 I/O 操作,以頁為單位的,內容就是磁碟上的物理塊,頁快取能幫助程式對檔案進行順序讀寫的速度幾乎接近於記憶體的讀寫速度,主要原因就是由於OS使用PageCache機制對讀寫訪問操作進行了效能優化:

頁快取讀取策略:當程序發起一個讀操作 (比如,程序發起一個 read() 系統呼叫),它首先會檢查需要的資料是否在頁快取中:

  • 如果在,則放棄訪問磁碟,而直接從頁快取中讀取
  • 如果不在,則核心排程塊 I/O 操作從磁碟去讀取資料,並讀入緊隨其後的少數幾個頁面(不少於一個頁面,通常是三個頁面),然後將資料放入頁快取中

頁快取寫策略:當程序發起write系統呼叫寫資料到檔案中,先寫到頁快取,然後方法返回。此時資料還沒有真正的儲存到檔案中去,Linux 僅僅將頁快取中的這一頁資料標記為“髒”,並且被加入到髒頁連結串列中

然後,由flusher 回寫執行緒週期性將髒頁連結串列中的頁寫到磁碟,讓磁碟中的資料和記憶體中保持一致,最後清理“髒”標識。在以下三種情況下,髒頁會被寫回磁碟:

  • 空閒記憶體低於一個特定閾值
  • 髒頁在記憶體中駐留超過一個特定的閾值時
  • 當用戶程序呼叫 sync() 和 fsync() 系統呼叫時

RocketMQ中,ConsumeQueue邏輯消費佇列儲存的資料較少,並且是順序讀取,在page cache機制的預讀取作用下,Consume Queue檔案的讀效能幾乎接近讀記憶體,即使在有訊息堆積情況下也不會影響效能,提供了2種訊息刷盤策略:

  • 同步刷盤:在訊息真正持久化至磁碟後RocketMQ的Broker端才會真正返回給Producer端一個成功的ACK響應
  • 非同步刷盤,能充分利用作業系統的PageCache的優勢,只要訊息寫入PageCache即可將成功的ACK返回給Producer端。訊息刷盤採用後臺非同步執行緒提交的方式進行,降低了讀寫延遲,提高了MQ的效能和吞吐量

Kafka實現訊息高效能讀寫也利用了頁快取,這裡不再展開

參考

《深入理解Linux核心 —— Daniel P.Bovet》

Netty之Java堆外記憶體掃盲貼 ——江南白衣

Java NIO?看這一篇就夠了! ——朱小廝

RocketMQ 訊息儲存流程 —— Zhao Kun(趙坤)

一文理解Netty模型架構 ——caison

更多精彩,歡迎關注公眾號 分散式系統架構