深入理解消息隊列:如何實現高性能的異步網絡傳輸?

本文已收錄GitHub,更有互聯網大廠面試真題,面試攻略,高效學習資料等

異步與同步模型最大的區別是,同步模型會阻塞線程等待資源,而異步模型不會阻塞線程,它是等資源準備好後,再通知業務代碼來完成後續的資源處理邏輯。這種異步設計的方法,可以很好地解決 IO 等待的問題。

我們開發的絕大多數業務系統,它都是 IO 密集型系統。跟 IO 密集型系統相對的另一種系統叫計算密集型系統。通過這兩種系統的名字,估計你也能大概猜出來 IO 密集型系統是什麼意思。

IO 密集型系統大部分時間都在執行 IO 操作,這個 IO 操作主要包括網絡 IO 和磁盤 IO,以及與計算機連接的一些外圍設備的訪問。與之相對的計算密集型系統,大部分時間都是在使用 CPU 執行計算操作。我們開發的業務系統,很少有非常耗時的計算,更多的是網絡收發數據,讀寫磁盤和數據庫這些 IO 操作。這樣的系統基本上都是 IO 密集型系統,特別適合使用異步的設計來提升系統性能。

應用程序最常使用的 IO 資源,主要包括磁盤 IO 和網絡 IO。由於現在的 SSD 的速度越來越快,對於本地磁盤的讀寫,異步的意義越來越小。所以,使用異步設計的方法來提升 IO性能,我們更加需要關注的問題是,如何來實現高性能的異步網絡傳輸。

今天,咱們就來聊一聊這個話題。

理想的異步網絡框架應該是什麼樣的?

在我們開發的程序中,如果要實現通過網絡來傳輸數據,需要用到開發語言提供的網絡通信類庫。大部分語言提供的網絡通信基礎類庫都是同步的。一個 TCP 連接建立後,用戶代碼會獲得一個用於收發數據的通道。每個通道會在內存中開闢兩片區域用於收發數據的緩存。

發送數據的過程比較簡單,我們直接往這個通道里面來寫入數據就可以了。用戶代碼在發送時寫入的數據會暫存在緩存中,然後操作系統會通過網卡,把發送緩存中的數據傳輸到對端的服務器上。

只要這個緩存不滿,或者說,我們發送數據的速度沒有超過網卡傳輸速度的上限,那這個發送數據的操作耗時,只不過是一次內存寫入的時間,這個時間是非常快的。所以,發送數據的時候同步發送就可以了,沒有必要異步

比較麻煩的是接收數據。對於數據的接收方來說,它並不知道什麼時候會收到數據。那我們能直接想到的方法就是,用一個線程阻塞在那兒等着數據,當有數據到來的時候,操作系統會先把數據寫入接收緩存,然後給接收數據的線程發一個通知,線程收到通知後結束等待,開始讀取數據。處理完這一批數據後,繼續阻塞等待下一批數據到來,這樣週而復始地處理收到的數據。

這就是同步網絡 IO 的模型。同步網絡 IO 模型在處理少量連接的時候,是沒有問題的。但是如果要同時處理非常多的連接,同步的網絡 IO 模型就有點兒力不從心了。

因爲,每個連接都需要阻塞一個線程來等待數據,大量的連接數就會需要相同數量的數據接收線程。當這些 TCP 連接都在進行數據收發的時候,會導致什麼情況呢?對,會有大量的線程來搶佔 CPU 時間,造成頻繁的 CPU 上下文切換,導致 CPU 的負載升高,整個系統的性能就會比較慢。

所以,我們需要使用異步的模型來解決網絡 IO 問題。怎麼解決呢?

你可以先拋開你知道的各種語言的異步類庫和各種異步的網絡 IO 框架,想一想,對於業務開發者來說,一個好的異步網絡框架,它的 API 應該是什麼樣的呢

我們希望達到的效果,無非就是,只用少量的線程就能處理大量的連接,有數據到來的時候能第一時間處理就可以了。

對於開發者來說,最簡單的方式就是,事先定義好收到數據後的處理邏輯,把這個處理邏輯作爲一個回調方法,在連接建立前就通過框架提供的 API 設置好。當收到數據的時候,由框架自動來執行這個回調方法就好了。

實際上,有沒有這麼簡單的框架呢?

使用Netty來實現異步網絡通信

在 Java 中,大名鼎鼎的 Netty 框架的 API 設計就是這樣的。接下來我們看一下如何使用Netty 實現異步接收數據。

//創建一組線性
EventLoopGroup group = new NioEventLoopGroup();
try{
	//初始化Server
	ServerBootstrap serverBootstrap = new ServerBootstrap();
	serverBootstrap.group(group);
	serverBootstrap.channel(NioServerSocketChannel.class);
	serverBootstrap.localAddress(new InetSocketAddress("localhost", 9999));
	//設置收到數據後的處理的Handler
	serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
		protected void initChannel(SocketChannel socketChannel) throws Exception {
			socketChannel.pipeline().addLast(new MyHandler());
		}
	}
	);
	//綁定端口,開始提供服務
	ChannelFuture channelFuture = serverBootstrap.bind().sync();
	channelFuture.channel().closeFuture().sync();
}
catch(Exception e){
	e.printStackTrace();
}
finally {
	group.shutdownGracefully().sync();
}

這段代碼它的功能非常簡單,就是在本地 9999 端口,啓動了一個 Socket Server 來接收數據。我帶你一起來看一下這段代碼:

  1. 首先我們創建了一個 EventLoopGroup 對象,命名爲 group,這個 group 對象你可以簡單把它理解爲一組線程。這組線程的作用就是來執行收發數據的業務邏輯。
  2. 然後,使用 Netty 提供的 ServerBootstrap 來初始化一個 Socket Server,綁定到本地 9999 端口上。
  3. 在真正啓動服務之前,我們給 serverBootstrap 傳入了一個 MyHandler 對象,這個MyHandler 是我們自己來實現的一個類,它需要繼承 Netty 提供的一個抽象類:ChannelInboundHandlerAdapter,在這個 MyHandler 裏面,我們可以定義收到數據後的處理邏輯。這個設置 Handler 的過程,就是我剛剛講的,預先來定義回調方法的過程。
  4. 最後就可以真正綁定本地端口,啓動 Socket 服務了。

服務啓動後,如果有客戶端來請求連接,Netty 會自動接受並創建一個 Socket 連接。你可以看到,我們的代碼中,並沒有像一些同步網絡框架中那樣,需要用戶調用 Accept() 方法來接受創建連接的情況,在 Netty 中,這個過程是自動的。

當收到來自客戶端的數據後,Netty 就會在我們第一行提供的 EventLoopGroup 對象中,獲取一個 IO 線程,在這個 IO 線程中調用接收數據的回調方法,來執行接收數據的業務邏輯,在這個例子中,就是我們傳入的 MyHandler 中的方法。

Netty 本身它是一個全異步的設計,我們上節課剛剛講過,異步設計會帶來額外的複雜度,所以這個例子的代碼看起來會比較多,比較複雜。但是你看,其實它提供了一組非常友好API。

真正需要業務代碼來實現的就兩個部分:一個是把服務初始化並啓動起來,還有就是,實現收發消息的業務邏輯 MyHandler。而像線程控制、緩存管理、連接管理這些異步網絡 IO中通用的、比較複雜的問題,Netty 已經自動幫你處理好了,有沒有感覺很貼心?所以,非常多的開源項目使用 Netty 作爲其底層的網絡 IO 框架,並不是沒有原因的。

在這種設計中,Netty 自己維護一組線程來執行數據收發的業務邏輯。如果說,你的業務需要更靈活的實現,自己來維護收發數據的線程,可以選擇更加底層的 Java NIO。其實,Netty 也是基於 NIO 來實現的。

使用NIO來實現異步網絡通信

在 Java 的 NIO 中,它提供了一個 Selector 對象,來解決一個線程在多個網絡連接上的多路複用問題。什麼意思呢?在 NIO 中,每個已經建立好的連接用一個 Channel 對象來表示。我們希望能實現,在一個線程裏,接收來自多個 Channel 的數據。也就是說,這些Channel 中,任何一個 Channel 收到數據後,第一時間能在同一個線程裏面來處理。

我們可以想一下,一個線程對應多個 Channel,有可能會出現這兩種情況:

  1. 線程在忙着處理收到的數據,這時候 Channel 中又收到了新數據;
  2. 線程閒着沒事兒幹,所有的 Channel 中都沒收到數據,也不能確定哪個 Channel 會在什麼時候收到數據。

Selecor 通過一種類似於事件的機制來解決這個問題。首先你需要把你的連接,也就是Channel 綁定到 Selector 上,然後你可以在接收數據的線程來調用 Selector.select() 方法來等待數據到來。這個 select 方法是一個阻塞方法,這個線程會一直卡在這兒,直到這些Channel 中的任意一個有數據到來,就會結束等待返回數據。它的返回值是一個迭代器,你可以從這個迭代器裏面獲取所有 Channel 收到的數據,然後來執行你的數據接收的業務邏輯。

你可以選擇直接在這個線程裏面來執行接收數據的業務邏輯,也可以將任務分發給其他的線
程來執行,如何選擇完全可以由你的代碼來控制。

總結

傳統的同步網絡 IO,一般採用的都是一個線程對應一個 Channel 接收數據,很難支持高併發和高吞吐量。這個時候,我們需要使用異步的網絡 IO 框架來解決問題。

然後我們講了 Netty 和 NIO 這兩種異步網絡框架的 API 和他們的使用方法。這裏面,你需要體會一下這兩種框架在 API 設計方面的差異。Netty 自動地解決了線程控制、緩存管理、連接管理這些問題,用戶只需要實現對應的 Handler 來處理收到的數據即可。而 NIO是更加底層的 API,它提供了 Selector 機制,用單個線程同時管理多個連接,解決了多路複用這個異步網絡通信的核心問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章