Netty與RPC

1、Netty原理java

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

2、Netty的高性能react

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

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

1.多路複用通信方式spring

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

  

 

  客戶端通訊序列圖以下:後端

  

  Netty的IO線程NIOEventLoop因爲聚合了多路複用器Selector,能夠同時併發處理成敗上千個客戶端Channel,因爲讀寫操做都是非阻塞的,這就能夠充分提高IO線程的運行效率,避免因爲頻繁IO阻塞致使的線程掛起。安全

2.異步通信NIOruby

  因爲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上,提高網絡並行處理性能。

3、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 }

 

4、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"));

 

5、Protocol Buffer

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

1.特色

  

  Protocol Buffer的序列化 & 反序列化簡單 & 速度快的緣由是:

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

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

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

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

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

6、Thrift

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

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

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

  

相關文章
相關標籤/搜索