Netty與RPC


一、Netty原理

  Netty是一個高性能、異步事件驅動的NIO框架,基於Java NIO提供的API實現。它提供了對TCP、UDP和文件傳輸的支持,作為一個異步NIO框架,Netty的所有IO操作都是異步非阻塞的,通過Future-Listener機制,用戶可以方便的主動獲取或通過通知機制獲得IO操作結果。

二、Netty的高性能

  在IO編程過程中,當需要同時處理多個客戶端接入請求時,可以利用多線程或IO多路復用技術進行處理。IO多路復用技術通過多個IO阻塞復用到同一個select的阻塞上,從而使得系統在單線程的情況下可以同時處理多個客戶端請求。與傳統的多線程/多進程模型相比,IO多路復用的最大優勢是系統開銷小,系統不需要創建新的額外進程或線程,也不需要維護這些進程和線程的運行,降低了系統的維護工作量,節省了系統資源。

  與socket類和serversocket類相對應,NIO也提供了socketchannel和serversocketchannel兩種不同的套接字通道實現。

1.多路復用通訊方式

  Netty架構按照Reactor模式設計和實現,它的服務端通信序列圖如下:

  

 

  客戶端通信序列圖如下:

  

  Netty的IO線程NIOEventLoop由於聚合了多路復用器Selector,可以同時並發處理成敗上千個客戶端Channel,由於讀寫操作都是非阻塞的,這就可以充分提升IO線程的運行效率,避免由於頻繁IO阻塞導致的線程掛起。

2.異步通訊NIO

  由於Netty采用了異步通信模式,一個IO線程可以並發處理N個客戶端連接和讀寫操作,這從根本上解決了傳統同步阻塞IO一連接一線程模型,架構的性能、彈性伸縮能力和可靠性都得到了極大的提升。

3.零拷貝(direct buffers 使用堆外直接內存)

  1)Netty的接受和發送ByteBuffer采用direct buffers,使用堆外直接內存進行socket讀寫,不需要進行字節緩沖區的二次拷貝。如果使用傳統的堆內存(heap buffers)進行socket讀寫,JVM會將堆內存buffer拷貝一份到直接內存中,然后才寫入socket中。相比於堆外直接內存,消息在發送過程中多了一次緩沖區的內存拷貝。

  2)Netty提供了組合buffer對象,可以聚合多個ByteBuffer對象,用戶可以像操作一個buffer那樣方便地對組合buffer進行操作,避免了傳統通過內存拷貝的方式將幾個小buffer合並成一個大的buffer。

  3)Netty的文件傳輸采用了transferTo方法,它可以直接將文件緩沖區的數據發送到目標channel,避免了傳統通過循環write方法導致的內存拷貝問題。

4.內存池(基於內存池的緩沖區重用機制)

  隨着JVM虛擬機和JIT即時編譯技術的發展,對象的分配和回收是個非常輕量級的工作。但是對於緩沖區buffer,情況卻稍有不同,特別是對於堆外直接內存的分配和回收,是一件耗時的操作。為了盡量重用緩沖區,Netty提供了基於內存池的緩沖區重用機制。

5.高效的Reactor線程模型

  常用的reactor線程模型有三種,reactor單線程模型,reactor多線程模型,主從reactor多線程模型。

 1)reactor單線程模型,指的是所有的IO操作都在同一個NIO線程上面完成,NIO線程的職責如下:

  •   作為NIO服務端,接收客戶端的TCP連接;
  •   作為NIO客戶端,向服務端發起TCP連接;
  •   讀取通信對端的請求或應答消息;
  •   向通信對端發送消息請求或應答消息;

  

  由於reactor模式使用的是異步非阻塞IO,所有的IO操作都不會導致阻塞,理論上一個線程可以獨立處理所有IO相關的操作。從架構層面看,一個NIO線程確實可以完成其承擔的職責。例如,通過acceptor接收客戶端的TCP連接請求消息,鏈路建立成功后,通過dispatch將對應的ByteBuffer派發到指定的handler上進行消息解碼。用戶handler可以通過NIO線程將消息發送給客戶端。

 2)reactor多線程模型

  reactor多線程模型與單線程模型最大的區別就是有一組NIO線程處理IO操作。有專門一個NIO線程-Acceptor線程用於監聽服務端,接收客戶端的TCP連接請求;網絡IO操作-讀、寫等由一個NIO線程池負責,線程池可以采用標准的JDK線程池實現,它包含一個任務隊列和N個可用的線程,由這些NIO負責消息的讀取、解碼、編碼和發送;

  

 3)主從reactor多線程模型

  服務端用於接收客戶端連接的不再是一個單獨的NIO線程,而是一個獨立的NIO線程池。acceptor接收客戶端TCP連接請求處理完成后(可能包含接入認證等),將新創建的socketchannel注冊到IO線程池(subReactor 線程池)的某個IO線程上,由它負責socketchannel的毒血和編解碼工作。acceptor線程池僅僅只用於客戶端的登陸、握手和安全認證,一旦鏈路建立成功,就將鏈路注冊到后端subReactor線程池的IO線程上,由IO線程負責后續的IO操作。

  

6.無鎖設計、線程鎖定

  Netty采用了串行無鎖化設計,在IO線程內部進行串行操作,避免多線程競爭導致的性能下降。表面上看,串行化設計似乎CPU利用率不高,並發程度不夠。但是,通過調整NIO線程池的線程參數,可以同時啟動多個串行化的線程並行運行,這種局部無鎖化的串行線程設計相比一個隊列-多個工作線程模型性能更優。

  

  Netty的NioEventLoop讀取到消息后,直接調用ChannelPipeline的fireChannelRead(Object msg),只要用戶不主動切換線程,一直會由NioEventLoop調用到用戶的handler,期間不進行線程切換,這種串行化處理方式避免了多線程操作導致的鎖競爭,從性能角度看是最優的。

7.高性能的序列化框架

  Netty默認提供了對Google Protobuf的支持,通過擴展Netty的編解碼接口,用戶可以實現其他的高性能序列化框架,例如Thrift的壓縮二進制編解碼框架。

  1)SO_RCVBUF和SO_SNDBUF:通常建議值為128K或256K。

 小包封大包,防止網絡阻塞

  2)SO_TCPNODELAY:NAGLE算法通過將緩沖區內的小封包自動相連,組成較大的封包,阻止大量小封包的發送阻塞網絡,從而提高網絡應用效率。但是對於時延敏感的應用場景需要關閉該優化算法。

 軟中斷Hash值和CPU綁定

  3)軟中斷:開啟RPS后可以實現軟中斷,提升網絡吞吐量。RPS根據數據包的源地址,目的地址以及目的和源端口,計算出一個hash值,然后根據這個hash值來選擇軟中斷運行的CPU,從上層來看,也就是說將每個連接和CPU綁定,並通過這個hash值,來均衡軟中斷在多個CPU上,提升網絡並行處理性能。

三、Netty RPC實現

  RPC,即Remote Procedure Call(遠程過程調用),調用遠程計算機上的服務,就像調用本地服務一樣。RPC可以很好的解耦系統,如webservice就是一種基於HTTP協議的RPC。

  這個RPC整體框架如下:

  

1.關鍵技術

  1)服務發布與訂閱:服務端使用zookeeper注冊服務地址,客戶端從zookeeper獲取可用的服務地址。

  2)通信:使用Netty作為通信框架。

  3)Spring:使用spring配置服務,加載bean,掃描注解。

  4)動態代理:客戶端使用代理模式透明化服務調用。

  5)消息編解碼:使用Protostuff序列化和反序列化消息。

2.核心流程

  1)服務消費方(client)調用以本地調用方式調用服務。

  2)client stub 接收到調用后負責將方法、參數等組裝成能夠進行網絡傳輸的消息體。

  3)client stub找到服務地址,並將消息發送到服務端。

  4)server stub 收到消息后進行解碼。

  5)server stub 根據解碼結果調用本地的服務。

  6)本地服務執行並將結果返回給server stub。

  7)server stub 將返回結果打包成消息並發送至消費方。

  8)client stub 接受到消息,並進行解碼。

  9)服務消費方得到最終結果。

  RPC的目標就是要2~8這些步驟都封裝起來,讓用戶對這些細節透明。Java一般使用動態代理方式實現遠程調用。

  

3.消息編解碼

  消息數據結構(接口名稱+方法名+參數類型和參數值+超時時間+requestID)

  客戶端的請求消息結構一般需要包括以下內容:

    1)接口名稱:在我們的例子里接口名是“HelloWorldService”,如果不傳,服務端就不知道調用哪個接口了;

    2)方法名:一個接口內可能有很多方法,如果不傳方法名,服務端就不知道調用的哪個方法;

    3)參數類型和參數值:參數類型有很多,例如有boolean、int、long、double、string、map、list,甚至如struct(class);以及相應的參數值;

    4)超時時間

    5)requestID:標識唯一請求id;

    6)服務端返回的消息:一般包括:返回值+狀態code+requestID

  序列化

    目前互聯網公司廣泛使用Protobuf、Thrift、Avro等成熟的序列化解決方案來搭建RPC框架,這些都是久經考驗的解決方案。

4.通訊過程

 核心問題(線程暫停、消息亂序)

  如果使用netty的話,一般會用channel.writeAndFlush()方法來發送消息二進制串,這個方法調用后對於整個遠程調用(從發送請求到接收到結果)來說是一個異步的,即對於當前線程來說,將請求發送出來后,線程就可以往后執行了。至於服務端的結果,是服務端處理完成后,再以消息的形式發送給客戶端的。於是這里出現以下兩個問題:

  1)怎么讓當前線程“暫停”,等結果回來后,再向后執行?

  2)如果有多個線程同時進行遠程方法調用,這是建立在client server 之間的socket連接上會有很多雙方發送的消息傳遞,前后順序也可能是隨機的,server處理完結果后,將結果消息發送給client,client收到很多消息,怎么知道哪個消息是原先哪個線程調用的?

  如下圖所示,線程A和線程B同時向client socket發送請求requestA和requestB,socket先后將requestB和requestA發送至server,而server可能將responseB先返回,盡管requestB請求到達的時間更晚。我們需要一種機制保證responseA丟給ThreadA,responseB丟給ThreadB。

  

  通訊流程

    requestID生成-AtomicLong

    1)client 線程每次通過socket調用一次遠程接口前,生成一個唯一的ID,即requestID(requestID必須保證在一個socket連接里面是唯一的),一般常常使用AtomicLong從0開始累計數字生成唯一ID。

    存放回調對象callback到全局ConcurrentHashMap

    2)將處理結果的回調對象callback,存放到全局ConcurrentHashMap里面put(requestID,callback);

    synchronized獲取回調對象callback的鎖並自旋wait

    3)當線程調用channel.writeAndFlush()發送消息后,緊接着執行callback的get()方法試圖獲取遠程返回的結果。在get()內部,則使用synchronized獲取回調對象callback的鎖,再先檢測是否已經獲取到結果,如果沒有,然后調用callback的wait()方法,釋放callback上的鎖,讓當前線程處於等待狀態。

    監聽消息的線程收到消息,找到callback上的鎖並喚醒

    4)服務端接收到請求並處理后,將response結果(此結果中包含了前面的requestID)發送給客戶端,客戶端socket連接上專門監聽消息的線程收到消息,分析結果,取到requestID,再從前面的ConcurrnetHashMap里面get(requestID),從而找到callback對象,再用synchronized獲取callback上的鎖,將方法調用結果設置到callback對象里,再調用callback.notifyAll()喚醒前面處於等待狀態的線程。

 1 public Object get() {  2     synchronized (this) { // 旋鎖
 3         while (true) { // 是否有結果了
 4             If (!isDone){  5                 wait(); //沒結果釋放鎖,讓當前線程處於等待狀態
 6             }else{//獲取數據並處理
 7  }  8  }  9  } 10 } 11 private void setDone(Response res) { 12     this.res = res; 13     isDone = true; 14     synchronized (this) { //獲取鎖,因為前面 wait()已經釋放了 callback 的鎖了
15         notifyAll(); // 喚醒處於等待的線程
16  } 17 }

 

四、RMI實現方式

  Java遠程方法調用,即Java RMI(Java remote method invocation)是Java編程語言里,一種用於實現遠程調用的應用程序編程接口。它使客戶機上運行的程序可以調用遠程服務器上的對象。遠程方法調用特性使Java編程人員能夠在網絡環境中分布操作。RMI全部的宗旨就是盡可能簡化遠程接口對象的使用。

1.實現步驟

  1)編寫遠程服務接口,該接口必須繼承java.rmi.Remote接口,方法必須拋出java.rmi.RemoteException異常。

  2)編寫遠程接口實現類,該實現類必須繼承java.rmi.server.UnicastRemoteObject類;

  3)運行RMI編譯器(rmic),創建客戶端stub類和服務端skeleton類;

  4)啟動一個RMI注冊表,以便駐留這些服務;

  5)在RMI注冊表中注冊服務;

  6)客戶端查找遠程對象,並調用遠程方法;

 1 1:創建遠程接口,繼承 java.rmi.Remote 接口  2 public interface GreetService extends java.rmi.Remote {  3     String sayHello(String name) throws RemoteException;  4 }  5 2:實現遠程接口,繼承 java.rmi.server.UnicastRemoteObject 類  6 public class GreetServiceImpl extends java.rmi.server.UnicastRemoteObject implements GreetService {  7     private static final long serialVersionUID = 3434060152387200042L;  8     public GreetServiceImpl() throws RemoteException {  9         super(); 10  } 11  @Override 12     public String sayHello(String name) throws RemoteException { 13         return "Hello " + name; 14  } 15 } 16 3:生成 Stub 和 Skeleton; 17 4:執行 rmiregistry 命令注冊服務 18 5:啟動服務 19   LocateRegistry.createRegistry(1098); 20   Naming.bind("rmi://10.108.1.138:1098/GreetService", new GreetServiceImpl()); 21 6.客戶端調用 22   GreetService greetService = (GreetService) Naming.lookup("rmi://10.108.1.138:1098/GreetService"); 23   System.out.println(greetService.sayHello("Jobs"));

 

五、Protocol Buffer

  Protocol buffer是Google的一個開源項目,它是用於結構化數據串行化的靈活、高效、自動的方法,例如XML,不過它比XML更小、更快、更簡單。你可以定義自己的數據結構,然后使用代碼生成器的代碼來讀寫這個數據結構。你甚至可以在無需重新部署程序的情況下更新數據結構。

1.特點

  

  Protocol Buffer的序列化 & 反序列化簡單 & 速度快的原因是:

    1)編碼/解碼方式簡單(只需要簡單的數字運算=位移等)

    2)采用protocol buffer 自身的框架代碼和編譯器共同完成;

  Protocol Buffer的數據壓縮效果好(即序列化的數據量體積小)的原因是:

    1)采用了獨特的編碼方式,如Varint、Zigzag編碼方式等;

    2)采用T-L-V的數據存儲方式,減少了分隔符的使用 & 數據存儲的緊湊

六、Thrift

  Apache Thrift是Facebook實現的一種高效的、支持多中編程語言的遠程服務調用的框架。

  目前流行的服務調用方式有很多種,例如基於SOAP消息格式的web service,基於JSON消息格式的RESTful服務等。其中所用到的數據傳輸方式包括XML、JSON等,然而XML相對體積太大,傳輸效率低,JSON體積較小,新穎,但不夠完善。

  本文將介紹由facebook開發的遠程服務調用框架Apache Thrift,它采用接口描述語言定義並創建服務,支持可擴展的跨語言服務開發,所包含的代碼生成引擎可以在多種語言中,如C++、Java、python、PHP、ruby等創建高效的、無縫的服務,其傳輸數據采用二進制格式,相對XML和JSON體積更小,對於高並發、大數據量和多語言的環境更有優勢。

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM