網絡傳輸方式問題:傳統的RPC 框架或者基於RMI 等方式的遠程服務(過程)調用採用了同步阻塞IO,當客戶端的併發壓力或者網絡時延增大以後,同步阻塞IO 會因爲頻繁的wait 致使IO 線程常常性的阻塞,因爲線程沒法高效的工做,IO 處理能力天然降低。下面,咱們經過BIO 通訊模型圖看下BIO 通訊的弊端:react
採用BIO 通訊模型的服務端,一般由一個獨立的Acceptor 線程負責監聽客戶端的鏈接,接收到客戶端鏈接以後爲客戶端鏈接建立一個新的線程處理請求消息,處理完成以後,返回應答消息給客戶端,線程銷燬,這就是典型的一請求一應答模型。該架構最大的問題就是不具有彈性伸縮能力,當併發訪問量增長後,服務端的線程個數和併發訪問數成線性正比,因爲線程是JAVA 虛擬機很是寶貴的系統資源,當線程數膨脹以後,系統的性能急劇降低,隨着併發量的繼續增長,可能會發生句柄溢出、線程堆棧溢出等問題,並致使服務器最終宕機。算法
序列化方式問題:Java 序列化存在以下幾個典型問題:編程
線程模型問題:因爲採用同步阻塞IO,這會致使每一個TCP 鏈接都佔用1 個線程,因爲線程資源是JVM 虛擬機很是寶貴的資源,當IO 讀寫阻塞致使線程沒法及時釋放時,會致使系統性能急劇降低,嚴重的甚至會致使虛擬機沒法建立新的線程。後端
經過使用Netty(NIO 框架)相比於傳統基於Java 序列化+BIO(同步阻塞IO)的通訊框架,性能提高了8 倍多。經過選擇合適的NIO 框架,精心的設計Reactor 線程模型,達到上述性能指標是徹底有可能的。數組
在IO 編程過程當中,當須要同時處理多個客戶端接入請求時,能夠利用多線程或者IO 多路複用技術進行處理。IO 多路複用技術經過把多個IO 的阻塞複用到同一個select 的阻塞上,從而使得系統在單線程的狀況下能夠同時處理多個客戶端請求。與傳統的多線程/多進程模型比,I/O 多路複用的最大優點是系統開銷小,系統不須要建立新的額外進程或者線程,也不須要維護這些進程和線程的運行,下降了系統的維護工做量,節省了系統資源。JDK1.4 提供了對非阻塞IO(NIO)的支持,JDK1.5_update10 版本使用epoll 替代了傳統的select/poll,極大的提高了NIO 通訊的性能。JDK NIO 通訊模型以下所示:安全
與Socket 類和ServerSocket 類相對應,NIO 也提供了SocketChannel 和ServerSocketChannel 兩種不一樣的套接字通道實現。這兩種新增的通道都支持阻塞和非阻塞兩種模式。阻塞模式使用很是簡單,可是性能和可靠性都很差,非阻塞模式正好相反。開發人員通常能夠根據本身的須要來選擇合適的模式,通常來講,低負載、低併發的應用程序能夠選擇同步阻塞IO 以下降編程複雜度。可是對於高負載、高併發的網絡應用,須要使用NIO 的非阻塞模式進行開發。Netty 架構按照Reactor 模式設計和實現。服務器
它的服務端通訊序列圖以下:網絡
客戶端通訊序列圖以下:多線程
Netty 的IO 線程NioEventLoop 聚合了多路複用器Selector,能夠同時併發處理成百上千個客戶端Channel,因爲讀寫操做都是非阻塞的,這就能夠充分提高IO 線程的運行效率,避免因爲頻繁IO 阻塞致使的線程掛起。另外,因爲Netty採用了異步通訊模式,一個IO 線程能夠併發處理N 個客戶端鏈接和讀寫操做,這從根本上解決了傳統同步阻塞IO 一鏈接一線程模型,架構的性能、彈性伸縮能力和可靠性都獲得了極大的提高。架構
Netty 的「零拷貝」主要體如今以下三個方面:
三個維度:
隨着JVM 虛擬機和JIT 即時編譯技術的發展,對象的分配和回收是個很是輕量級的工做。可是對於緩衝區Buffer,狀況卻稍有不一樣,特別是對於堆外直接內存的分配和回收,是一件耗時的操做。爲了儘可能重用緩衝區,Netty 提供了基於內存池的緩衝區重用機制。下面咱們一塊兒看下Netty ByteBuf 的實現:
Netty 提供了多種內存管理策略,經過在啓動輔助類中配置相關參數,能夠實現差別化的定製。下面經過性能測試,咱們看下基於內存池循環利用的ByteBuf 和普通ByteBuf 的性能差別。
用例一,使用內存池分配器建立直接內存緩衝區:
final byte[] CONTENT = new byte[1024]; int loop = 1800000; long startTime = System.currentTimeMillis(); ByteBuf poolBuffer = null; for (int i = 0; i < loop; i++) { poolBuffer = PooledByteBufAllocator.DEFAULT.directBuffer(1024); poolBuffer.writeBytes(CONTENT); poolBuffer.release(); }
long endTime = System.currentTimeMillis(); System.out.println("內存池分配緩衝區耗時" + (endTime - startTime) + "ms.");
用例二,使用非堆內存分配器建立的直接內存緩衝區:
long startTime2 = System.currentTimeMillis(); ByteBuf buffer = null; for (int i = 0; i < loop; i++) { buffer = Unpooled.directBuffer(1024);
buffer.writeBytes(CONTENT);
buffer.release(); }
endTime = System.currentTimeMillis(); System.out.println("非內存池分配緩衝區耗時" + (endTime - startTime2) + "ms.");
性能測試經驗代表,採用內存池的ByteBuf 相比於朝生夕滅的ByteBuf,性能高了很多(性能數據與使用場景強相關)。下面咱們一塊兒簡單分析下Netty 內存池的內存分配:
public ByteBuf directBuffer(int initialCapacity, int maxCapacity) { if (initialCapacity == 0 && maxCapacity == 0) { return this.emptyBuf; } else { validate(initialCapacity, maxCapacity); return this.newDirectBuffer(initialCapacity, maxCapacity); } }
繼續看newDirectBuffer 方法,咱們發現它是一個抽象方法,由AbstractByteBufAllocator 的子類負責具體實現,代碼以下:
代碼跳轉到PooledByteBufAllocator 的newDirectBuffer 方法,從Cache 中獲取內存區域PoolArena,調用它的allocate方法進行內存分配:
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) { PoolThreadCache cache = (PoolThreadCache)this.threadCache.get(); PoolArena<ByteBuffer> directArena = cache.directArena; Object buf; if (directArena != null) { buf = directArena.allocate(cache, initialCapacity, maxCapacity); } else { buf = PlatformDependent.hasUnsafe() ? UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) : new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity); } return toLeakAwareBuffer((ByteBuf)buf); }
PoolArena 的allocate 方法以下:
PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) { PooledByteBuf<T> buf = this.newByteBuf(maxCapacity); this.allocate(cache, buf, reqCapacity); return buf; }
咱們重點看newByteBuf 的實現,它一樣是個抽象方法:
由子類DirectArena 和HeapArena 來實現不一樣類型的緩衝區分配,因爲測試用例使用的是堆外內存,所以重點分析DirectArena 的實現:若是沒有開啓使用sun 的unsafe:
protected PooledByteBuf<ByteBuffer> newByteBuf(int maxCapacity) { return (PooledByteBuf)(HAS_UNSAFE ? PooledUnsafeDirectByteBuf.newInstance(maxCapacity) : PooledDirectByteBuf.newInstance(maxCapacity)); }
則執行PooledDirectByteBuf 的newInstance 方法,代碼以下:
static PooledDirectByteBuf newInstance(int maxCapacity) { PooledDirectByteBuf buf = (PooledDirectByteBuf)RECYCLER.get(); buf.reuse(maxCapacity); return buf; }
經過RECYCLER 的get 方法循環使用ByteBuf 對象,若是是非內存池實現,則直接建立一個新的ByteBuf 對象。從緩衝池中獲取ByteBuf 以後,調用AbstractReferenceCountedByteBuf 的setRefCnt 方法設置引用計數器,用於對象的引用計數和內存回收(相似JVM 垃圾回收機制)。而 Unpooled.directBuffer(1024) 則是每次都要new
public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); }
經常使用的Reactor 線程模型有三種,分別以下:
Reactor 單線程模型;
Reactor 多線程模型;
主從Reactor 多線程模型
Reactor 單線程模型,指的是全部的IO 操做都在同一個NIO 線程上面完成,NIO 線程的職責以下:
Reactor 單線程模型示意圖以下所示:
因爲Reactor 模式使用的是異步非阻塞IO,全部的IO 操做都不會致使阻塞,理論上一個線程能夠獨立處理全部IO 相關的操做。從架構層面看,一個NIO 線程確實能夠完成其承擔的職責。例如,經過Acceptor 接收客戶端的TCP 鏈接請求消息,鏈路創建成功以後,經過Dispatch 將對應的ByteBuffer 派發到指定的Handler 上進行消息解碼。用戶Handler能夠經過NIO 線程將消息發送給客戶端。對於一些小容量應用場景,可使用單線程模型。可是對於高負載、大併發的應用卻不合適,主要緣由以下:
爲了解決這些問題,演進出了Reactor 多線程模型,下面咱們一塊兒學習下Reactor 多線程模型。Rector 多線程模型與單線程模型最大的區別就是有一組NIO 線程處理IO 操做,它的原理圖以下:
Reactor 多線程模型的特色:
在絕大多數場景下,Reactor 多線程模型均可以知足性能需求;可是,在極特殊應用場景中,一個NIO 線程負責監聽和處理全部的客戶端鏈接可能會存在性能問題。例如百萬客戶端併發鏈接,或者服務端須要對客戶端的握手消息進行安全認證,認證自己很是損耗性能。在這類場景下,單獨一個Acceptor 線程可能會存在性能不足問題,爲了解決性能問題,產生了第三種Reactor 線程模型-主從Reactor 多線程模型。
主從Reactor 線程模型的特色是:
服務端用於接收客戶端鏈接的再也不是個1 個單獨的NIO 線程,而是一個獨立的NIO線程池。Acceptor 接收到客戶端TCP 鏈接請求處理完成後(可能包含接入認證等),將新建立的SocketChannel 註冊到IO 線程池(sub reactor 線程池)的某個IO 線程上,由它負責SocketChannel 的讀寫和編解碼工做。Acceptor線程池僅僅只用於客戶端的登錄、握手和安全認證,一旦鏈路創建成功,就將鏈路註冊到後端subReactor 線程池的IO線程上,由IO 線程負責後續的IO 操做。它的線程模型以下圖所示:
利用主從NIO 線程模型,能夠解決1 個服務端監聽線程沒法有效處理全部客戶端鏈接的性能不足問題。所以,在Netty的官方demo 中,推薦使用該線程模型。事實上,Netty 的線程模型並不是固定不變,經過在啓動輔助類中建立不一樣的EventLoopGroup 實例並經過適當的參數配置,就能夠支持上述三種Reactor 線程模型。正是由於Netty 對Reactor 線程模型的支持提供了靈活的定製能力,因此能夠知足不一樣業務場景的性能訴求。
在大多數場景下,並行多線程處理能夠提高系統的併發性能。可是,若是對於共享資源的併發訪問處理不當,會帶來嚴重的鎖競爭,這最終會致使性能的降低。爲了儘量的避免鎖競爭帶來的性能損耗,能夠經過串行化設計,即消息的處理儘量在同一個線程內完成,期間不進行線程切換,這樣就避免了多線程競爭和同步鎖。
爲了儘量提高性能,Netty 採用了串行無鎖化設計,在IO 線程內部進行串行操做,避免多線程競爭致使的性能降低。表面上看,串行化設計彷佛CPU 利用率不高,併發程度不夠。可是,經過調整NIO 線程池的線程參數,能夠同時啓動多個串行化的線程並行運行,這種局部無鎖化的串行線程設計相比一個隊列-多個工做線程模型性能更優。Netty 的串行化設計工做原理圖以下:
Netty 的NioEventLoop 讀取到消息以後,直接調用ChannelPipeline 的fireChannelRead(Object msg),只要用戶不主動切換線程,一直會由NioEventLoop 調用到用戶的Handler,期間不進行線程切換,這種串行化處理方式避免了多線程操做致使的鎖的競爭,從性能角度看是最優的。
Netty 的高效併發編程主要體如今以下幾點:
影響序列化性能的關鍵因素總結以下:
Netty 默認提供了對Google Protobuf 的支持,經過擴展Netty 的編解碼接口,用戶能夠實現其它的高性能序列化框架,例如Thrift 的壓縮二進制編解碼框架。下面咱們一塊兒看下不一樣序列化&反序列化框架序列化後的字節數組對比:
從上圖能夠看出,Protobuf 序列化後的碼流只有Java 序列化的1/4 左右。正是因爲Java 原生序列化性能表現太差,才催生出了各類高性能的開源序列化技術和框架(性能差只是其中的一個緣由,還有跨語言、IDL 定義等其它因素)。
合理設置TCP 參數在某些場景下對於性能的提高能夠起到顯著的效果,例如SO_RCVBUF 和SO_SNDBUF。若是設置不當,對性能的影響是很是大的。下面咱們總結下對性能影響比較大的幾個配置項:
Netty 在啓動輔助類中能夠靈活的配置TCP 參數,知足不一樣的用戶場景。相關配置接口定義以下:
基本上對於Netty的高性能是由以上主要的八點所共同支撐的。