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

本文介紹操做系統I/O工做原理,Java I/O設計,基本使用,開源項目中實現高性能I/O常見方法和實現,完全搞懂高性能I/O之道html

基礎概念

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

  • (1) 操做系統與內核

操做系統:管理計算機硬件與軟件資源的系統軟件 內核:操做系統的核心軟件,負責管理系統的進程、內存、設備驅動程序、文件和網絡系統等等,爲應用程序提供對計算機硬件的安全訪問服務git

  • 2 內核空間和用戶空間

爲了不用戶進程直接操做內核,保證內核安全,操做系統將內存尋址空間劃分爲兩部分: 內核空間(Kernel-space),供內核程序使用 用戶空間(User-space),供用戶進程使用 爲了安全,內核空間和用戶空間是隔離的,即便用戶的程序崩潰了,內核也不受影響github

  • 3 數據流

計算機中的數據是基於隨着時間變換高低電壓信號傳輸的,這些數據信號接二連三,有着固定的傳輸方向,相似水管中水的流動,所以抽象數據流(I/O流)的概念:指一組有順序的、有起點和終點的字節集合apache

抽象出數據流的做用:實現程序邏輯與底層硬件解耦,經過引入數據流做爲程序與硬件設備之間的抽象層,面向通用的數據流輸入輸出接口編程,而不是具體硬件特性,程序和底層硬件能夠獨立靈活替換和擴展編程

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堆內(HeapByteBuffer)或堆外內存(DirectByteBuffer)

堆外內存是指與堆內存相對應的,把內存對象分配在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(github.com/caison/cais…):

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

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

相關文章
相關標籤/搜索