版權(quán)說(shuō)明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡(jiǎn)介
Netty源碼剖析與應(yīng)用目錄TOC\h\h第1章Netty基礎(chǔ)篇\h1.1Netty概述\h1.2Netty服務(wù)端構(gòu)建\h1.3Netty客戶端的運(yùn)用\h1.3.1Java多線程交互\h1.3.2Netty客戶端與服務(wù)端短連接\h1.3.3Netty客戶端與服務(wù)端長(zhǎng)連接\h1.4小結(jié)\h第2章原理部分\h2.1多路復(fù)用器\h2.1.1NIO與BIO的區(qū)別\h2.1.2epoll模型與select模型的區(qū)別\h2.2Netty線程模型\h2.3編碼和解碼\h2.4序列化\h2.4.1Protobuf序列化\h2.4.2Kryo序列化\h2.5零拷貝\h2.6背壓\h2.6.1TCP窗口\h2.6.2Flink實(shí)時(shí)計(jì)算引擎的背壓原理\h2.7小結(jié)\h第3章分布式RPC\h3.1Netty整合Spring\h3.2采用Netty實(shí)現(xiàn)一套R(shí)PC框架\h3.3分布式RPC的構(gòu)建\h3.3.1服務(wù)注冊(cè)與發(fā)現(xiàn)\h3.3.2動(dòng)態(tài)代理\h第4章Netty核心組件源碼剖析\h4.1NioEventLoopGroup源碼剖析\h4.2NioEventLoop源碼剖析\h4.2.1NioEventLoop開(kāi)啟Selector\h4.2.2NioEventLoop的run()方法解讀\h4.2.3NioEventLoop重新構(gòu)建Selector和Channel的注冊(cè)\h4.3Channel源碼剖析\h4.3.1AbstractChannel源碼剖析\h4.3.2AbstractNioChannel源碼剖析\h4.3.3AbstractNioByteChannel源碼剖析\h4.3.4AbstractNioMessageChannel源碼剖析\h4.3.5NioSocketChannel源碼剖析\h4.3.6NioServerSocketChannel源碼剖析\h4.4Netty緩沖區(qū)ByteBuf源碼剖析\h4.4.1AbstractByteBuf源碼剖析\h4.4.2AbstractReferenceCountedByteBuf源碼剖析\h4.4.3ReferenceCountUpdater源碼剖析\h4.4.4CompositeByteBuf源碼剖析\h4.4.5PooledByteBuf源碼剖析\h4.5Netty內(nèi)存泄漏檢測(cè)機(jī)制源碼剖析\h4.5.1內(nèi)存泄漏檢測(cè)原理\h4.5.2內(nèi)存泄漏器ResourceLeakDetector源碼剖析\h4.6小結(jié)\h第5章Netty讀/寫請(qǐng)求源碼剖析\h5.1ServerBootstrap啟動(dòng)過(guò)程剖析\h5.2Netty對(duì)I/O就緒事件的處理\h5.2.1NioEventLoop就緒處理之OP_ACCEPT\h5.2.2NioEventLoop就緒處理之OP_READ(一)\h5.2.3NioEventLoop就緒處理之OP_READ(二)\h第6章Netty內(nèi)存管理\h6.1Netty內(nèi)存管理策略介紹\h6.2PoolChunk內(nèi)存分配\h6.2.1PoolChunk分配大于或等于8KB的內(nèi)存\h6.2.2PoolChunk分配小于8KB的內(nèi)存\h6.3PoolSubpage內(nèi)存分配與釋放\h6.4PoolArena內(nèi)存管理\h6.5RecvByteBufAllocator內(nèi)存分配計(jì)算\h6.6小結(jié)\h第7章Netty時(shí)間輪高級(jí)應(yīng)用\h7.1Netty時(shí)間輪的解讀\h7.1.1時(shí)間輪源碼剖析之初始化構(gòu)建\h7.1.2時(shí)間輪源碼剖析之Worker啟動(dòng)線程\h7.2Netty時(shí)間輪改造方案制訂\h7.3時(shí)間輪高級(jí)應(yīng)用之架構(gòu)設(shè)計(jì)\h7.4時(shí)間輪高級(jí)應(yīng)用之實(shí)戰(zhàn)10億級(jí)任務(wù)\h7.5小結(jié)\h第8章問(wèn)題分析與性能調(diào)優(yōu)\h8.1Netty服務(wù)在Linux服務(wù)器上的部署\h8.2Netty服務(wù)模擬秒殺壓測(cè)\h8.3常見(jiàn)生產(chǎn)問(wèn)題分析\h8.4性能調(diào)優(yōu)\h8.5小結(jié)第1章
Netty基礎(chǔ)篇本章采用一段簡(jiǎn)潔的文字描述了Netty的作用。同時(shí),通過(guò)一個(gè)長(zhǎng)連接通信實(shí)例讓讀者可以快速掌握如何運(yùn)用Netty。1.1Netty概述普通開(kāi)發(fā)人員在工作中一般很少接觸Netty,只有在閱讀一些分布式框架底層源碼時(shí),才會(huì)發(fā)現(xiàn)底層通信模塊大部分是Netty,如Dubbo、Flink、Spark、Elasticsearch、HBase等流行的分布式框架。HBase從2.0版本開(kāi)始默認(rèn)使用NettyRPCServer,用Netty替代HBase原生的RPCServer。至于微服務(wù)Dubbo和RPC框架(如gRPC),它們的底層核心部分也都是Netty。由此可見(jiàn),不管是開(kāi)發(fā)互聯(lián)網(wǎng)JavaWeb后臺(tái),還是研發(fā)大數(shù)據(jù),學(xué)好Netty都是很有必要的。Netty是一款流行的JavaNIO框架,那么它有哪些特性呢?為什么其他優(yōu)秀的Java框架的通信模塊會(huì)選擇Netty呢?使用過(guò)JavaNIO的讀者一定非常清楚,采用NIO編寫一套高效且穩(wěn)定的通信模塊很不容易,沒(méi)有一流的編程能力根本無(wú)法實(shí)現(xiàn),并且無(wú)法做到在高并發(fā)情況下的可靠和高效。然而,Netty這款優(yōu)秀的開(kāi)源框架卻可以快速地開(kāi)發(fā)高性能的面向協(xié)議的服務(wù)端和客戶端。Netty不僅易用、健壯、安全、高效,還可以輕松地自定義各種協(xié)議、采用各種序列化,并且它的可擴(kuò)展性極強(qiáng)。1.2Netty服務(wù)端構(gòu)建TCP通信是面向連接的、可靠的、基于字節(jié)流的通信協(xié)議,有嚴(yán)格的客戶端和服務(wù)端之分,本節(jié)運(yùn)用Netty構(gòu)建TCP服務(wù)端,同時(shí)為后面構(gòu)建分布式RPC服務(wù)器做好準(zhǔn)備。Netty服務(wù)端程序?qū)崿F(xiàn)步驟如下。(1)創(chuàng)建兩個(gè)線程組,分別為Boss線程組和Worker線程組。Boss線程專門用于接收來(lái)自客戶端的連接;Worker線程用于處理已經(jīng)被Boss線程接收的連接。(2)運(yùn)用服務(wù)啟動(dòng)輔助類ServerBootstrap創(chuàng)建一個(gè)對(duì)象,并配置一系列啟動(dòng)參數(shù),如參數(shù)ChannelOption.SO_RCVBUF和ChannelOption.SO_SNDBUF分別對(duì)應(yīng)接收緩沖區(qū)和發(fā)送緩沖區(qū)的大小。(3)當(dāng)Boss線程把接收到的連接注冊(cè)到Worker線程中后,需要交給連接初始化消息處理Handler鏈。由于不同的應(yīng)用需要用到不同的Handler鏈,所以Netty提供了ChannelInitializer接口,由用戶實(shí)現(xiàn)此接口,完成Handler鏈的初始化工作。(4)編寫業(yè)務(wù)處理Handler鏈,并實(shí)現(xiàn)對(duì)接收客戶端消息的處理邏輯。(5)綁定端口。由于端口綁定需要由Boss線程完成,所以主線程需要執(zhí)行同步阻塞方法,等待Boss線程完成綁定操作。在編碼前先構(gòu)建開(kāi)發(fā)環(huán)境。開(kāi)發(fā)工具選擇SpringToolSuite或IntelliJIDEA,其他工具選擇Mavenv3.2.5、JDKv1.8、穩(wěn)定版的Nettyv4.1.38.Final。首先打開(kāi)STS,構(gòu)建一個(gè)Maven工程,并將Netty的依賴放入pom.xml中。具體實(shí)現(xiàn)代碼如下:Netty的Maven工程構(gòu)建圖如圖1-1所示。圖1-1Netty的Maven工程構(gòu)建圖按照服務(wù)端程序?qū)崿F(xiàn)步驟新建一個(gè)Netty服務(wù)類,具體代碼如下:服務(wù)端還需要編寫一個(gè)業(yè)務(wù)邏輯處理Handler(名稱為ServerHandler),這個(gè)Handler需要讀取客戶端數(shù)據(jù),并對(duì)請(qǐng)求進(jìn)行業(yè)務(wù)邏輯處理,最終把響應(yīng)結(jié)果返回給客戶端。ServerHandler需要繼承ChannelInboundHandlerAdapter,它是ChannelInboundHandler的子類,這跟Netty的處理數(shù)據(jù)流向有關(guān)。當(dāng)NioEventLoop線程從Channel讀取數(shù)據(jù)時(shí),執(zhí)行綁定在Channel的ChannelInboundHandler對(duì)象上,并執(zhí)行其channelRead()方法。具體實(shí)現(xiàn)代碼如下:1.3Netty客戶端的運(yùn)用Netty除了可以編寫高性能服務(wù)端,還有配套的非阻塞I/O客戶端,關(guān)于客戶端與服務(wù)端的通信,涉及多線程數(shù)據(jù)交互,并運(yùn)用了JDK的鎖和多線程。1.3.1Java多線程交互本小節(jié)編寫了一個(gè)Java多線程實(shí)例,為后續(xù)介紹Netty客戶端做鋪墊。此實(shí)例模擬1條主線程循環(huán)寫數(shù)據(jù),另外99條子線程,每條子線程模擬睡眠1s,再給主線程發(fā)送結(jié)果,最終主線程阻塞獲取99條子線程響應(yīng)的結(jié)果數(shù)據(jù)。此實(shí)例有以下幾個(gè)類。第一個(gè)類FutureMain:主線程,擁有啟動(dòng)main()方法和實(shí)例的總體處理邏輯(請(qǐng)求對(duì)象的生成、子線程的構(gòu)建和啟動(dòng)、獲取子線程的響應(yīng)結(jié)果)。第二個(gè)類RequestFuture:模擬客戶端請(qǐng)求類,主要用于構(gòu)建請(qǐng)求對(duì)象(擁有每次的請(qǐng)求id,并對(duì)每次請(qǐng)求對(duì)象進(jìn)行了緩存),最核心的部分在于它的同步等待和結(jié)果通知方法。第三個(gè)類SubThread:子線程,用于模擬服務(wù)端處理,根據(jù)主線程傳送的請(qǐng)求對(duì)象RequestFuture構(gòu)建響應(yīng)結(jié)果,等待1s后,調(diào)用RequestFuture的響應(yīng)結(jié)果通知方法將結(jié)果交互給主線程。第四個(gè)類Response:響應(yīng)結(jié)果類,擁有響應(yīng)id和結(jié)果內(nèi)容。只有響應(yīng)id和請(qǐng)求id一致,才能從請(qǐng)求緩存中獲取當(dāng)前響應(yīng)結(jié)果的請(qǐng)求對(duì)象。如圖1-2為多線程交互數(shù)據(jù)UML圖。圖1-2多線程交互數(shù)據(jù)UML圖具體實(shí)現(xiàn)代碼如下。(1)FutureMain類:(2)RequestFuture類:(3)SubThread類:(4)Response類:運(yùn)行FutureMain的main()方法,控制臺(tái)會(huì)打印出子線程SubThread返回的Response消息。感興趣的讀者可通過(guò)以下兩個(gè)問(wèn)題對(duì)代碼進(jìn)行相應(yīng)的修改。(1)使用ReentrantLock與Condition更換同步關(guān)鍵詞,以及Object類的notify()和wait()方法。(2)將主線程for循環(huán)構(gòu)建請(qǐng)求而阻塞同步發(fā)送修改成線程池Executors.newFixedThreadPool生成100條線程異步請(qǐng)求。1.3.2Netty客戶端與服務(wù)端短連接Netty是一個(gè)異步網(wǎng)絡(luò)處理框架,使用了大量的Future機(jī)制,并在Java自帶Future的基礎(chǔ)上增加了Promise機(jī)制,從而使異步編程更加方便簡(jiǎn)單。本小節(jié)采用Netty客戶端與服務(wù)端實(shí)現(xiàn)短連接異步通信的方式,加深讀者對(duì)多線程的靈活運(yùn)用的認(rèn)識(shí),并幫助讀者初步了解Netty客戶端。Netty客戶端的線程模型比服務(wù)端的線程模型簡(jiǎn)單一些,它只需一個(gè)線程組,底層采用Java的NIO,通過(guò)IP和端口連接目標(biāo)服務(wù)器,請(qǐng)求發(fā)送和接收響應(yīng)結(jié)果數(shù)據(jù)與Netty服務(wù)端編程一樣,同樣需要經(jīng)過(guò)一系列Handler。請(qǐng)求發(fā)送從TailContext到編碼器Handler,再到HeadContext;接收響應(yīng)路徑從HeadContext到解碼器Handler,再到業(yè)務(wù)邏輯Handler(此處提到的數(shù)據(jù)流的編碼和解碼會(huì)在第5章進(jìn)行詳細(xì)講解)。TCP網(wǎng)絡(luò)傳輸?shù)氖嵌M(jìn)制數(shù)據(jù)流,且會(huì)源源不斷地流到目標(biāo)機(jī)器上,如果沒(méi)有對(duì)數(shù)據(jù)流進(jìn)行一些額外的加工處理,那么將無(wú)法區(qū)分每次請(qǐng)求的數(shù)據(jù)包。編碼是指在傳輸數(shù)據(jù)前,對(duì)數(shù)據(jù)包進(jìn)行加工處理,解碼發(fā)生在讀取數(shù)據(jù)包時(shí),根據(jù)加工好的數(shù)據(jù)的特點(diǎn),解析出正確的數(shù)據(jù)包。客戶端與服務(wù)端的交互流程如圖1-3所示。圖1-3中共有8個(gè)處理流程,分別如下。①Netty客戶端通過(guò)IP和端口連接服務(wù)端,并準(zhǔn)備好JSON數(shù)據(jù)包。②JSON數(shù)據(jù)包發(fā)送到網(wǎng)絡(luò)之前需要經(jīng)過(guò)一系列編碼器,并最終被寫入Socket中,發(fā)送給Netty服務(wù)端。③Netty服務(wù)端接收到Netty客戶端發(fā)送的數(shù)據(jù)流后,先經(jīng)過(guò)一系列解碼器,把客戶端發(fā)送的JSON數(shù)據(jù)包解碼出來(lái),然后傳遞給ServerHandler實(shí)例。④ServerHandler實(shí)例模擬業(yè)務(wù)邏輯處理。⑤Netty服務(wù)端把處理后的結(jié)果返回Netty客戶端。⑥Netty服務(wù)端同樣需要經(jīng)過(guò)一系列編碼器,最終將響應(yīng)結(jié)果發(fā)送到網(wǎng)絡(luò)中。⑦Netty客戶端解碼器接收響應(yīng)結(jié)果字節(jié)流并對(duì)其進(jìn)行解碼,然后把響應(yīng)的JSON結(jié)果數(shù)據(jù)返回給ClientHandler。⑧由于ClientHandler運(yùn)行在NioEventLoop線程上,所以結(jié)果數(shù)據(jù)在返回主線程時(shí)需要用到Netty的Promise機(jī)制,以實(shí)現(xiàn)多線程數(shù)據(jù)交互。圖1-3客戶端與服務(wù)端的交互流程N(yùn)etty服務(wù)端的Netty服務(wù)類在1.2節(jié)的基礎(chǔ)上新增了長(zhǎng)度編碼器和解碼器,具體代碼如下:服務(wù)端業(yè)務(wù)邏輯處理類ServerHandler的channelRead()方法需要返回Response對(duì)象,同時(shí)獲取請(qǐng)求id,具體變動(dòng)代碼如下:客戶端啟動(dòng)類NettyClient和Netty服務(wù)相似,輔助類用Bootstrap替代ServerBootstrap,同時(shí)引入了異步處理類DefaultPromise,用于異步獲取服務(wù)端的響應(yīng)結(jié)果,具體實(shí)現(xiàn)代碼如下:客戶端業(yè)務(wù)邏輯處理ClientHandler,接收服務(wù)端的響應(yīng)數(shù)據(jù),并運(yùn)用Promise喚醒主線程,實(shí)現(xiàn)代碼如下:通過(guò)以上的介紹,對(duì)Netty客戶端會(huì)有一個(gè)基本的了解,能正常發(fā)送與接收數(shù)據(jù)。上述用法看起來(lái)沒(méi)什么問(wèn)題,但性能偏弱。因?yàn)樵诿看伟l(fā)送請(qǐng)求時(shí)都需要?jiǎng)?chuàng)建連接,還不如直接使用普通Socket作為客戶端??梢赃\(yùn)用連接池優(yōu)化解決,把短連接先放入連接池,然后從連接池中獲取每次請(qǐng)求的連接,感興趣的讀者可以采用ApacheCommonsPool重構(gòu)上述代碼。1.3.3Netty客戶端與服務(wù)端長(zhǎng)連接本小節(jié)使用長(zhǎng)連接解決1.3.2小節(jié)中的性能問(wèn)題,通常各公司內(nèi)部的RPC通信一般會(huì)選擇長(zhǎng)連接通信模式。在改造代碼前,請(qǐng)先思考以下兩個(gè)問(wèn)題。(1)改造成長(zhǎng)連接后,ClientHandler不能每次都在main()方法中構(gòu)建,promise對(duì)象無(wú)法通過(guò)主線程傳送給ClientHandler,那么此時(shí)主線程如何獲取NioEventLoop線程的數(shù)據(jù)呢?(2)主線程每次獲取的響應(yīng)結(jié)果對(duì)應(yīng)的是哪次請(qǐng)求呢??通過(guò)多線程交互數(shù)據(jù)實(shí)例的學(xué)習(xí),很顯然第一個(gè)問(wèn)題可以通過(guò)多線程數(shù)據(jù)交互來(lái)解決。首先對(duì)Netty客戶端創(chuàng)建的連接進(jìn)行靜態(tài)化處理,以免每次調(diào)用時(shí)都需要重復(fù)創(chuàng)建;然后在給服務(wù)端發(fā)送請(qǐng)求后運(yùn)用RequestFuture的get()方法同步等待獲取響應(yīng)結(jié)果,以替代Netty的Promise同步等待;最后用RequestFuture.received替代ClientHandler的Promise異步通知。?第二個(gè)問(wèn)題的解決方案:每次請(qǐng)求帶上自增唯一的id,客戶端需要把每次請(qǐng)求先緩存起來(lái),同時(shí)服務(wù)端在接收到請(qǐng)求后,會(huì)把請(qǐng)求id放入響應(yīng)結(jié)果中一起返回客戶端。NettyClient的連接需要進(jìn)行靜態(tài)化處理,同時(shí),改成RequestFutrue的get()方法獲取異步響應(yīng)結(jié)果。具體的改造后的NettyClient代碼如下:下面對(duì)RequestFuture類也進(jìn)行一些改動(dòng),新增了一個(gè)id自增屬性AtomicLongaid,在構(gòu)造方法上,將自增aid賦給請(qǐng)求id,同時(shí)把當(dāng)前請(qǐng)求對(duì)象加入全局緩存futures中,代碼如下:將ClientHandler的channelRead()方法中的promise改為RequestFuture.received,代碼如下:服務(wù)端ServerHandler類將返回的結(jié)果加上對(duì)應(yīng)的請(qǐng)求id,客戶端與服務(wù)端長(zhǎng)連接響應(yīng)輸出如圖1-4所示。通過(guò)圖1-4發(fā)現(xiàn),服務(wù)器響應(yīng)的結(jié)果有序地輸出在客戶端控制臺(tái)上。不妨思考,要如何修改代碼,才能讓客戶端輸出的結(jié)果是無(wú)序的呢?圖1-4客戶端與服務(wù)端長(zhǎng)連接響應(yīng)輸出1.4小結(jié)本章雖然是Netty的基礎(chǔ)應(yīng)用部分,但是涉及Java多線程交互、Netty客戶端與服務(wù)端的長(zhǎng)連接通信,為編寫分布式RPC打好了基礎(chǔ)。在進(jìn)行后續(xù)的Netty多線程編程時(shí),會(huì)遇到各種問(wèn)題。例如,從表面上看,Netty客戶端只用一條線程就能完成與服務(wù)端的數(shù)據(jù)交互,為何要使用線程組?然而在實(shí)際應(yīng)用中,Netty服務(wù)會(huì)部署在多臺(tái)機(jī)器上,而客戶端與服務(wù)端的連接也會(huì)有多條,這些連接鏈路Channel可以注冊(cè)在同一個(gè)Worker線程組中。在學(xué)習(xí)Netty時(shí),要多發(fā)現(xiàn)問(wèn)題并多思考,以找到一個(gè)讓自己滿意的答案。第2章
原理部分2.1多路復(fù)用器通過(guò)第1章的介紹,我們對(duì)Netty有了初步的了解,但距離完全掌握Netty,并編寫高性能的RPC服務(wù)還有一定的差距。本章主要對(duì)Netty和NIO的一些特性進(jìn)行梳理,以了解它們的底層原理,為后面編寫RPC分布式服務(wù)打好理論基礎(chǔ)。NIO有一個(gè)非常重要的組件——多路復(fù)用器,其底層有3種經(jīng)典模型,分別是epoll、select和poll。與傳統(tǒng)I/O相比,一個(gè)多路復(fù)用器可以處理多個(gè)Socket連接,而傳統(tǒng)I/O對(duì)每個(gè)Socket連接都需要一條線程去同步阻塞處理。NIO有了多路復(fù)用器后,只需一條線程即可管理多個(gè)Socket連接的接入和讀寫事件。Netty的多路復(fù)用器默認(rèn)調(diào)用的模型是epoll模型。它除了JDK自帶的epoll模型的封裝,還額外封裝了一套,它們都是epoll模型的封裝,只是JDK的epoll模型是水平觸發(fā)的,而Netty采用JNI重寫的是邊緣觸發(fā)。2.1.1NIO與BIO的區(qū)別NIO為同步非阻塞I/O,BIO為同步阻塞I/O。阻塞I/O與非阻塞I/O的區(qū)別如下。阻塞I/O:例如,客戶端向服務(wù)器發(fā)送10B的數(shù)據(jù),若服務(wù)器一次只接收到8B的數(shù)據(jù),則必須一直等待后面2B大小的數(shù)據(jù)的到來(lái),在后面2B的數(shù)據(jù)未到達(dá)時(shí),當(dāng)前線程會(huì)阻塞在接收函數(shù)處。非阻塞I/O:從TCP緩沖區(qū)中讀取數(shù)據(jù),緩沖區(qū)中的數(shù)據(jù)可分多次讀取,不會(huì)阻塞線程。例如,已知后面將有10B的數(shù)據(jù)發(fā)送過(guò)來(lái),但是如果現(xiàn)在緩沖區(qū)只收到8B的數(shù)據(jù),那么當(dāng)前線程就會(huì)讀取這8B的數(shù)據(jù),讀完后立即返回,等另外2B的數(shù)據(jù)發(fā)來(lái)時(shí)再去讀取。NIO中的多路復(fù)用器管理成千上萬(wàn)條的Socket連接。當(dāng)多路復(fù)用器每次從TCP緩沖區(qū)讀數(shù)據(jù)時(shí),若有些客戶端數(shù)據(jù)包未能全部到達(dá),且讀取數(shù)據(jù)的線程是在阻塞的情況下,則只有全部數(shù)據(jù)到達(dá)時(shí)才能返回,這樣不僅性能弱,還不可控,無(wú)法預(yù)測(cè)等待的時(shí)間。圖2-1為BIO服務(wù)器流程圖,圖2-2為NIO服務(wù)器整體流程圖。圖2-1BIO服務(wù)器流程圖圖2-2NIO服務(wù)器整體流程圖從圖2-1和圖2-2中可以明顯地看出,NIO比BIO復(fù)雜得多,NIO主要是多了Selector。Selector能監(jiān)聽(tīng)多個(gè)Channel,當(dāng)運(yùn)行select()方法時(shí),會(huì)循環(huán)檢測(cè)是否有就緒事件的Channel。只需一條線程即可管理多個(gè)Channel,而且對(duì)Channel的讀/寫采用的都是非阻塞I/O。與BIO相比,NIO同時(shí)接入的Channel會(huì)更多、資源利用率會(huì)更高。因?yàn)锽IO的一條線程只能對(duì)單個(gè)Channel進(jìn)行阻塞讀/寫,處理完后才能繼續(xù)接入并處理其他Channel,并發(fā)處理能力太弱。2.1.2epoll模型與select模型的區(qū)別I/O多路復(fù)用器單個(gè)進(jìn)程可以同時(shí)處理多個(gè)描述符的I/O,Java應(yīng)用程序通過(guò)調(diào)用多路復(fù)用器來(lái)獲取有事件發(fā)生的文件描述符,以進(jìn)行I/O的讀/寫操作。多路復(fù)用器常見(jiàn)的底層實(shí)現(xiàn)模型有epoll模型和select模型,本節(jié)詳細(xì)介紹它們各自的特點(diǎn)。select模型有以下3個(gè)特點(diǎn)。(1)select模型只有一個(gè)select函數(shù),每次在調(diào)用select函數(shù)時(shí),都需要把整個(gè)文件描述符集合從用戶態(tài)拷貝到內(nèi)核態(tài),當(dāng)文件描述符很多時(shí),開(kāi)銷會(huì)比較大。(2)每次在調(diào)用select函數(shù)時(shí),內(nèi)核都需要遍歷所有的文件描述符,這個(gè)開(kāi)銷也很大,尤其是當(dāng)很多文件描述符根本就無(wú)狀態(tài)改變時(shí),也需要遍歷,浪費(fèi)性能。(3)select可支持的文件描述符有上限,可監(jiān)控的文件描述符個(gè)數(shù)取決于sizeOf(fd_set)的值。如果sizeOf(fd_set)=512,那么此服務(wù)器最多支持512×8=4096個(gè)文件描述符。epoll模型比select模型復(fù)雜,epoll模型有三個(gè)函數(shù)。第一個(gè)函數(shù)為intepoll_create(intsize),用于創(chuàng)建一個(gè)epoll句柄。第二個(gè)函數(shù)為intepoll_ctl(intepfd,intop,intfd,structepoll_event*event),其中,第一個(gè)參數(shù)為epoll_create函數(shù)調(diào)用返回的值;第二個(gè)參數(shù)表示操作動(dòng)作,由三個(gè)宏(EPOLL_CTL_ADD表示注冊(cè)新的文件描述符到此epfd上,EPOLL_CTL_MOD表示修改已經(jīng)注冊(cè)的文件描述符的監(jiān)聽(tīng)事件,EPOLL_CTL_DEL表示從epfd中刪除一個(gè)文件描述符)來(lái)表示;第三個(gè)參數(shù)為需要監(jiān)聽(tīng)的文件描述符;第四個(gè)參數(shù)表示要監(jiān)聽(tīng)的事件類型,事件類型也是幾個(gè)宏的集合,主要是文件描述符可讀、可寫、發(fā)生錯(cuò)誤、被掛斷和觸發(fā)模式設(shè)置等。epoll模型的第三個(gè)函數(shù)為epoll_wait,表示等待文件描述符就緒。epoll模型與select模型相比,在以下這些地方進(jìn)行了改善。?所有需要監(jiān)聽(tīng)的文件描述符只需在調(diào)用第二個(gè)函數(shù)intepoll_ctl時(shí)拷貝一次即可,當(dāng)文件描述符狀態(tài)發(fā)生改變時(shí),內(nèi)核會(huì)把文件描述符放入一個(gè)就緒隊(duì)列中,通過(guò)調(diào)用epoll_wait函數(shù)獲取就緒的文件描述符。?每次調(diào)用epoll_wait函數(shù)只會(huì)遍歷狀態(tài)發(fā)生改變的文件描述符,無(wú)須全部遍歷,降低了操作的時(shí)間復(fù)雜度。?沒(méi)有文件描述符個(gè)數(shù)的限制。?采用了內(nèi)存映射機(jī)制,內(nèi)核直接將就緒隊(duì)列通過(guò)MMAP的方式映射到用戶態(tài),避免了內(nèi)存拷貝帶來(lái)的額外性能開(kāi)銷。了解這兩種多路復(fù)用器模型的特點(diǎn)主要是為了加深對(duì)NIO底層原理的理解,同時(shí)對(duì)深入了解Netty源碼有很大的幫助。2.2Netty線程模型從本節(jié)開(kāi)始了解Netty的特性,首先從線程模型開(kāi)始。在第1章中,講到過(guò)兩個(gè)線程組,即Boss線程組和Worker線程組。其中,Boss線程組一般只開(kāi)啟一條線程,除非一個(gè)Netty服務(wù)同時(shí)監(jiān)聽(tīng)多個(gè)端口。Worker線程數(shù)默認(rèn)是CPU核數(shù)的兩倍,Boss線程主要監(jiān)聽(tīng)SocketChannel的OP_ACCEPT事件和客戶端的連接(主線程)。當(dāng)Boss線程監(jiān)聽(tīng)到有SocketChannel連接接入時(shí),會(huì)把SocketChannel包裝成NioSocketChannel,并注冊(cè)到Worker線程的Selector中,同時(shí)監(jiān)聽(tīng)其OP_WRITE和OP_READ事件。當(dāng)Worker線程監(jiān)聽(tīng)到某個(gè)SocketChannel有就緒的讀I/O事件時(shí),會(huì)進(jìn)行以下操作。(1)向內(nèi)存池中分配內(nèi)存,讀取I/O數(shù)據(jù)流。(2)將讀取后的ByteBuf傳遞給解碼器Handler進(jìn)行解碼,若能解碼出完整的請(qǐng)求數(shù)據(jù)包,就會(huì)把請(qǐng)求數(shù)據(jù)包交給業(yè)務(wù)邏輯處理Handler。(3)經(jīng)過(guò)業(yè)務(wù)邏輯處理Handler后,在返回響應(yīng)結(jié)果前,交給編碼器進(jìn)行數(shù)據(jù)加工。(4)最終寫到緩存區(qū),并由I/OWorker線程將緩存區(qū)的數(shù)據(jù)輸出到網(wǎng)絡(luò)中并傳輸給客戶端。Netty主從線程模型如圖2-3所示,圖中有個(gè)任務(wù)隊(duì)列,這個(gè)任務(wù)隊(duì)列主要是用來(lái)處理一些定時(shí)任務(wù)的,如連接的心跳檢測(cè)。同時(shí),當(dāng)開(kāi)啟了額外業(yè)務(wù)線程時(shí),寫回響應(yīng)結(jié)果也會(huì)被封裝成任務(wù),交給I/OWorker線程來(lái)完成。圖2-3Netty主從線程模型2.3編碼和解碼在第1章介紹Netty客戶端與服務(wù)端的通信原理時(shí),使用過(guò)編碼器和解碼器,但并未對(duì)其底層原理進(jìn)行詳細(xì)的介紹。如果使用JavaNIO來(lái)實(shí)現(xiàn)TCP網(wǎng)絡(luò)通信,則需要對(duì)TCP連接中的問(wèn)題進(jìn)行全面的考慮,如拆包和粘包導(dǎo)致的半包問(wèn)題和數(shù)據(jù)序列化等。對(duì)于這些問(wèn)題,Netty都做了很好的處理。本節(jié)通過(guò)Netty的編碼和解碼架構(gòu)及其源碼對(duì)上述問(wèn)題進(jìn)行詳細(xì)剖析。下面先看一幅簡(jiǎn)單的TCP通信圖,如圖2-4所示。圖2-4TCP通信圖在圖2-4中,客戶端給服務(wù)端發(fā)送消息并收到服務(wù)端返回的結(jié)果,共經(jīng)歷了以下6步。①TCP是面向字節(jié)流傳輸?shù)膮f(xié)議,它把客戶端提交的請(qǐng)求數(shù)據(jù)看作一連串的無(wú)結(jié)構(gòu)的字節(jié)流,并不知道所傳送的字節(jié)流的含義,也并不關(guān)心有多少數(shù)據(jù)流入TCP輸出緩沖區(qū)中。②每次發(fā)多少數(shù)據(jù)到網(wǎng)絡(luò)中與當(dāng)前的網(wǎng)絡(luò)擁塞情況和服務(wù)端返回的TCP窗口的大小有關(guān),涉及TCP的流量控制和阻塞控制,且與Netty的反壓有關(guān)。如果客戶端發(fā)送到TCP輸出緩沖區(qū)的數(shù)據(jù)塊太多,那么TCP會(huì)分割成多次將其傳送出去;如果太少,則會(huì)等待積累足夠多的字節(jié)后發(fā)送出去。很明顯,TCP這種傳輸機(jī)制會(huì)產(chǎn)生粘包問(wèn)題。③當(dāng)服務(wù)端讀取TCP輸入緩沖區(qū)中的數(shù)據(jù)時(shí),需要進(jìn)行拆包處理,并解決粘包和拆包問(wèn)題,比較常用的方案有以下3種。?將換行符號(hào)或特殊標(biāo)識(shí)符號(hào)加入數(shù)據(jù)包中,如HTTP和FTP等。?將消息分為head和body,head中包含body長(zhǎng)度的字段,一般前面4個(gè)字節(jié)是body的長(zhǎng)度值,用int類型表示,但也有像Dubbo協(xié)議那種,head中除body長(zhǎng)度外,還有版本號(hào)、請(qǐng)求類型、請(qǐng)求id等。?固定數(shù)據(jù)包的長(zhǎng)度,如固定100個(gè)字節(jié),不足補(bǔ)空格。步驟④~⑥與步驟①~③類似。TCP的這些機(jī)制與Netty的編碼和解碼有很大的關(guān)系。Netty采用模板設(shè)計(jì)模式實(shí)現(xiàn)了一套編碼和解碼架構(gòu),高度抽象,底層解決TCP的粘包和拆包問(wèn)題,對(duì)前面介紹的3種方案都做了具體實(shí)現(xiàn)。第1種方案,Netty有解碼器LineBasedFrameDecoder,可以判斷字節(jié)中是否出現(xiàn)了“\n”或“\r\n”。第2種方案,Netty有編解碼器LengthFieldPrepender和LengthFieldBasedFrameDecoder,可以在消息中加上消息體長(zhǎng)度值,這兩個(gè)編解碼器在之前的實(shí)戰(zhàn)中用到過(guò)。第3種方案,Netty有固定數(shù)據(jù)包長(zhǎng)度的解碼器FixedLengthFrameDecoder。此方案一般用得較少,比較常用的是前兩種方案。Netty對(duì)編碼和解碼進(jìn)行了抽象處理。編碼器和解碼器大部分都有共同的編碼和解碼父類,即MessageToMessageEncoder與ByteToMessageDecoder。ByteToMessageDecoder父類在讀取TCP緩沖區(qū)的數(shù)據(jù)并解碼后,將剩余的數(shù)據(jù)放入了讀半包字節(jié)容器中,具體解碼方案由子類負(fù)責(zé)。在解碼的過(guò)程中會(huì)遇到讀半包,無(wú)法解碼的數(shù)據(jù)會(huì)保存在讀半包字節(jié)容器中,等待下次讀取數(shù)據(jù)后繼續(xù)解碼。編碼邏輯比較簡(jiǎn)單,MessageToMessageEncoder父類定義了整個(gè)編碼的流程,并實(shí)現(xiàn)了對(duì)已讀內(nèi)存的釋放,具體編碼格式由子類負(fù)責(zé)。Netty的編碼和解碼除了解決TCP協(xié)議的粘包和拆包問(wèn)題,還有一些編解碼器做了很多額外的事情,如StringEncode(把字符串轉(zhuǎn)換成字節(jié)流)、ProtobufDecoder(對(duì)Protobuf序列化數(shù)據(jù)進(jìn)行解碼);還有各種常用的協(xié)議編解碼器,如HTTP2、Websocket等。本節(jié)只是介紹了Netty為什么要編/解碼、Netty編/解碼的實(shí)現(xiàn)思想,以及一些常用編/解碼的使用,后續(xù)章節(jié)會(huì)對(duì)常用的編碼器和解碼器進(jìn)行實(shí)戰(zhàn)應(yīng)用,并進(jìn)行詳細(xì)的源碼剖析。2.4序列化在圖2-4中,當(dāng)客戶端向服務(wù)端發(fā)送數(shù)據(jù)時(shí),如果發(fā)送的是一個(gè)Java對(duì)象,由于網(wǎng)絡(luò)只能傳輸二進(jìn)制數(shù)據(jù)流,所以Java對(duì)象無(wú)法直接在網(wǎng)絡(luò)中傳輸,則必須對(duì)Java對(duì)象的內(nèi)容進(jìn)行流化,因?yàn)橹挥辛骰蟮膶?duì)象才能在網(wǎng)絡(luò)中傳輸。序列化就是將Java對(duì)象轉(zhuǎn)換成二進(jìn)制流數(shù)據(jù)的過(guò)程,而這種轉(zhuǎn)化方式多種多樣,本節(jié)介紹幾種常用的序列化方式。(1)Java自帶序列化:使用非常簡(jiǎn)單,但在網(wǎng)絡(luò)傳輸時(shí)很少使用,這主要是因?yàn)槠湫阅芴?,序列化后的碼流太大。另外,它無(wú)法跨語(yǔ)言進(jìn)行反序列化。(2)為了解決Java自帶序列化的缺點(diǎn),會(huì)引入比較流行的序列化方式,如Protobuf、Kryo、JSON等。由于JSON格式化數(shù)據(jù)可讀性好,而且瀏覽器對(duì)JSON數(shù)據(jù)的支持性非常好,所以一般的Web應(yīng)用都會(huì)選擇它。另外,市場(chǎng)上有Fastjson、Jackson等工具包,使Java對(duì)象轉(zhuǎn)換成JSON也非常方便。但JSON序列化后的數(shù)據(jù)體積較大,不適合網(wǎng)絡(luò)傳輸和海量數(shù)據(jù)存儲(chǔ)。Protobuf和Kryo序列化后的體積與JSON相比要小很多,本節(jié)會(huì)對(duì)這兩種序列化各自的優(yōu)點(diǎn)和缺點(diǎn)及應(yīng)用場(chǎng)景進(jìn)行詳細(xì)的講解。2.4.1Protobuf序列化Protobuf是Google提供的一個(gè)具有高效協(xié)議數(shù)據(jù)交換格式的工具庫(kù)(類似JSON),但Protobuf有更高的轉(zhuǎn)化效率,且時(shí)間效率和空間效率都是JSON的3~5倍,為何采用Protobuf序列化后占用的存儲(chǔ)空間要比JSON占用的存儲(chǔ)空間小而反序列化要更快呢?例如,有一個(gè)Java對(duì)象,將其轉(zhuǎn)換為JSON格式{"userName":"zhangsan","age":20,"hobby":"swimming"},很顯然,當(dāng)采用這種方式進(jìn)行序列化時(shí),會(huì)寫進(jìn)去一些無(wú)用的信息,如{"userName",…},此實(shí)例對(duì)象字段少,就算再怎么浪費(fèi)也無(wú)妨。但當(dāng)類的屬性非常多并包含各種對(duì)象組合時(shí),開(kāi)銷會(huì)非常大,有時(shí)甚至?xí)^(guò)真正需要傳送的值。Protobuf對(duì)這些字段屬性進(jìn)行了額外處理,同類的每個(gè)屬性名采用Tag值來(lái)標(biāo)識(shí),這個(gè)Tag值在Protobuf中采用了varint編碼,當(dāng)類的屬性個(gè)數(shù)小于128時(shí),每個(gè)屬性名只需1B即可表示,同時(shí)屬性值的長(zhǎng)度也只占用1B。Protobuf對(duì)值也進(jìn)行了各種編碼,不同類型的數(shù)據(jù)值采用不同的編碼技術(shù),以盡量減小占用的存儲(chǔ)空間。可以將Protobuf序列化后的數(shù)據(jù)想象成下面這樣的格式:Protobuf序列化除了占用空間小,性能還非常好,主要是它帶有屬性值長(zhǎng)度,無(wú)須進(jìn)行字符串匹配,這個(gè)長(zhǎng)度值只占用1B的存儲(chǔ)空間。另外,JSON都是字符串解析,而Protobuf根據(jù)不同的數(shù)據(jù)類型有不同的大小,如bool類型只需讀取1B的數(shù)據(jù)。Protobuf的缺點(diǎn)如下。(1)從Protobuf序列化后的數(shù)據(jù)中發(fā)現(xiàn),Protobuf序列化不會(huì)把Java類序列化進(jìn)去。當(dāng)遇到對(duì)象的一個(gè)屬性是泛型且有繼承的情況時(shí),Protobuf序列化無(wú)法正確地對(duì)其進(jìn)行反序列化,還原子類信息。(2)Protobuf需要編寫.proto文件,比較麻煩,此時(shí)可以使用Protostuff來(lái)解決。Protostuff是Protobuf的升級(jí)版,無(wú)須編寫.proto文件,只需在對(duì)象屬性中加入@Tag注解即可。(3)可讀性差,只能通過(guò)程序反序列化解析查看具體內(nèi)容。本小節(jié)對(duì)Protobuf有了一個(gè)初步的了解。Protobuf一般用于公司內(nèi)部服務(wù)信息的交換。目前市場(chǎng)上序列化框架有很多,了解其優(yōu)點(diǎn)和缺點(diǎn)對(duì)技術(shù)選型有很大的幫助。對(duì)于數(shù)據(jù)量比較大、對(duì)象屬性是無(wú)泛型且有繼承的數(shù)據(jù),Protobuf是個(gè)很不錯(cuò)的工具,值得推薦。2.4.2Kryo序列化本小節(jié)介紹另外一個(gè)序列化框架——Kryo,它是一個(gè)快速高效的Java對(duì)象圖形序列化框架,主要特點(diǎn)是性能高、高效和易用。它的存儲(chǔ)與Protobuf的存儲(chǔ)一樣,采用了可變長(zhǎng)存儲(chǔ)機(jī)制。Kryo的序列化方式比較常用的有以下兩種。(1)默認(rèn)序列化方式——FieldSerializer。此方式非常高效,只寫字段數(shù)據(jù),沒(méi)有任何額外信息,但是它不支持添加、刪除或更改字段類型,并不適合業(yè)務(wù)系統(tǒng),因?yàn)闃I(yè)務(wù)數(shù)據(jù)模型會(huì)經(jīng)常改變。它只適用于序列化與反序列化的相關(guān)類一致的情況。(2)在長(zhǎng)期持久化存儲(chǔ)方面,可以考慮Kryo的另一種序列化方式——TaggedFieldSerializer。TaggedFieldSerializer與Protobuf非常相似,在需要每個(gè)字段上加上@Tag注解,不僅可以修改字段,還支持泛型的繼承,但會(huì)寫入對(duì)象中用到的具體類型。Kryo還支持對(duì)類進(jìn)行注冊(cè),為每個(gè)類分配一個(gè)id,用一個(gè)字節(jié)表示一長(zhǎng)串的類名。但這種方式很容易出現(xiàn)問(wèn)題,因此業(yè)務(wù)系統(tǒng)用Kryo基本上不考慮。在使用Kryo時(shí),需要仔細(xì)考慮以下幾點(diǎn)。?當(dāng)采用Tag方式兼容修改數(shù)據(jù)時(shí)要注意Kryo的版本。例如,選擇Kryo4.0.2版本,除了設(shè)置setSkipUnknownTags(true),還需要在@Tag注解里加上annexed=true,如@Tag(value=1,annexed=true),當(dāng)前字段在反序列化時(shí)才可以刪除;Kryo4.0.0版本則無(wú)須在@Tag注解里加上annexed=true。?Kryo的引用機(jī)制。若開(kāi)啟了引用,則字段值除了基本類型,其他的都有可能被引用上。例如,字符串的a字段值如果是""空字符串,b字段值和a字段值一樣,則在反序列化時(shí),a字段在類中被刪除,引用會(huì)報(bào)錯(cuò)(會(huì)報(bào)數(shù)組越界的異常)。如果系統(tǒng)無(wú)循環(huán)引用,則無(wú)須開(kāi)啟引用。?使用無(wú)須加注解的兼容模式——CompatibleFieldSerializer序列化方式。這種方式雖然能支持泛型的繼承,但序列化后的數(shù)據(jù)占用的空間可能會(huì)比JSON序列化占用的空間還大,因此一般不建議使用。?Kryo是非線程安全的,可以采用ThreadLocal將其緩存起來(lái)。上述Kryo的注意細(xì)節(jié)大部分是編者在使用過(guò)程中曾經(jīng)遇到的問(wèn)題,具體實(shí)戰(zhàn)還需進(jìn)入Kryo官網(wǎng)仔細(xì)閱讀。2.5零拷貝序列化主要與傳輸數(shù)據(jù)格式有關(guān),不管是Kryo還是Protobuf,它們都能對(duì)數(shù)據(jù)內(nèi)容進(jìn)行壓縮,并能完整地恢復(fù)。零拷貝是Netty的一個(gè)特性,主要發(fā)生在操作數(shù)據(jù)上,無(wú)須將數(shù)據(jù)Buffer從一個(gè)內(nèi)存區(qū)域拷貝到另一個(gè)內(nèi)存區(qū)域,少一次拷貝,CPU效率就會(huì)提升。Netty的零拷貝主要應(yīng)用在以下3種場(chǎng)景中。(1)Netty接收和發(fā)送ByteBuffer采用的都是堆外直接內(nèi)存,使用堆外直接內(nèi)存進(jìn)行Socket的讀/寫,無(wú)須進(jìn)行字節(jié)緩沖區(qū)的二次拷貝。如果使用傳統(tǒng)的堆內(nèi)存進(jìn)行Socket的讀/寫,則JVM會(huì)將堆內(nèi)存Buffer數(shù)據(jù)拷貝到堆外直接內(nèi)存中,然后才寫入Socket中。與堆外直接內(nèi)存相比,使用傳統(tǒng)的堆內(nèi)存,在消息的發(fā)送過(guò)程中多了一次緩沖區(qū)的內(nèi)存拷貝。(2)在網(wǎng)絡(luò)傳輸中,一條消息很可能會(huì)被分割成多個(gè)數(shù)據(jù)包進(jìn)行發(fā)送,只有當(dāng)收到一個(gè)完整的數(shù)據(jù)包后,才能完成解碼工作。Netty通過(guò)組合內(nèi)存的方式把這些內(nèi)存數(shù)據(jù)包邏輯組合到一塊,而不是對(duì)每個(gè)數(shù)據(jù)塊進(jìn)行一次拷貝,這類似于數(shù)據(jù)庫(kù)中的視圖。CompositeByteBuf是Netty在此零拷貝方案中的組合Buffer,在第4章節(jié)會(huì)對(duì)它進(jìn)行詳細(xì)剖析。(3)傳統(tǒng)拷貝文件的方法需要先把文件采用FileInputStream文件輸入流讀取到一個(gè)臨時(shí)的byte[]數(shù)組中,然后通過(guò)FileOutputStream文件輸出流,把臨時(shí)的byte[]數(shù)據(jù)內(nèi)容寫入目的文件中。當(dāng)拷貝大文件時(shí),頻繁的內(nèi)存拷貝操作會(huì)消耗大量的系統(tǒng)資源。Netty底層運(yùn)用JavaNIO的FileChannel.transfer()方法,該方法依賴操作系統(tǒng)實(shí)現(xiàn)零拷貝,可以直接將文件緩沖區(qū)的數(shù)據(jù)發(fā)送到目標(biāo)Channel中,避免了傳統(tǒng)的通過(guò)循環(huán)寫方式導(dǎo)致的內(nèi)存數(shù)據(jù)拷貝問(wèn)題。本節(jié)內(nèi)容偏少,且理論性較強(qiáng)。零拷貝機(jī)制在做上層應(yīng)用時(shí)幾乎不會(huì)接觸,但在面試時(shí),很有可能被問(wèn)到,因此一定要深入理解這3種重要的零拷貝場(chǎng)景。如果目前難以理解,則可暫時(shí)跳過(guò),在仔細(xì)看完源碼剖析部分內(nèi)容后,再來(lái)仔細(xì)分析。2.6背壓本節(jié)是Netty基本理論知識(shí)的最后一節(jié),主要介紹如何運(yùn)用Netty實(shí)現(xiàn)背壓,并在此基礎(chǔ)上分析它的具體應(yīng)用實(shí)例,不管是在面試時(shí),還是在實(shí)際工作中,都是非常有用的。下面先看一下TCP鏈路背壓場(chǎng)景圖,如圖2-5所示。圖2-5TCP鏈路背壓場(chǎng)景圖細(xì)看圖2-5可以發(fā)現(xiàn),當(dāng)消費(fèi)者的消費(fèi)速率低于生產(chǎn)者的發(fā)送速率時(shí),會(huì)造成背壓,此時(shí)消費(fèi)者無(wú)法從TCP緩存區(qū)中讀取數(shù)據(jù),因?yàn)樗鼰o(wú)法再?gòu)膬?nèi)存池中獲取內(nèi)存,從而造成TCP通道阻塞。生產(chǎn)者無(wú)法把數(shù)據(jù)發(fā)送出去,這就使生產(chǎn)者不再向緩存隊(duì)列中寫入數(shù)據(jù),從而降低了生產(chǎn)速率。當(dāng)消費(fèi)者的消費(fèi)速率提升且TCP通道不再阻塞時(shí),生產(chǎn)者的發(fā)送速率又會(huì)得到提升,整個(gè)鏈路運(yùn)行恢復(fù)正常。2.6.1TCP窗口Netty的背壓主要運(yùn)用TCP的流量控制來(lái)完成整個(gè)鏈路的背壓效果,而在TCP的流量控制中有個(gè)非常重要的概念——TCP窗口。TCP窗口的大小是可變的,因此也叫滑動(dòng)窗口。TCP窗口本質(zhì)上就是描述接收方的TCP緩存區(qū)能接收多少數(shù)據(jù)的,發(fā)送方可根據(jù)這個(gè)值來(lái)計(jì)算最多可以發(fā)送數(shù)據(jù)的長(zhǎng)度。接下來(lái)看圖2-6,以了解TCP窗口的工作過(guò)程,圖中涉及TCP窗口大小和ACK,這些參數(shù)都是TCP頭部比較重要的概念。?SequenceNumber:包的序號(hào),用來(lái)解決網(wǎng)絡(luò)包亂序問(wèn)題。?AcknowledgementNumber:簡(jiǎn)稱ACK(確認(rèn)序號(hào)),用來(lái)解決丟包問(wèn)題。?Window:TCP窗口,也叫滑動(dòng)窗口,用來(lái)解決流控。?TCPFlag:包的類型,主要用來(lái)操控TCP的11種狀態(tài)機(jī)。圖2-6TCP窗口的工作過(guò)程在圖2-6中,主機(jī)A與主機(jī)B通信,在第一次發(fā)送數(shù)據(jù)時(shí),主機(jī)A發(fā)送多少數(shù)據(jù)到網(wǎng)絡(luò)中由鏈路帶寬的大小來(lái)決定。同時(shí),主機(jī)B在收到數(shù)據(jù)段后,會(huì)把下一個(gè)要接收的數(shù)據(jù)序號(hào)返回給主機(jī)A,即圖中的ACK。(1)例如,主機(jī)A發(fā)送3個(gè)數(shù)據(jù)段長(zhǎng)度,然后等待主機(jī)B的確認(rèn),當(dāng)主機(jī)B收到1~3數(shù)據(jù)段時(shí),會(huì)給主機(jī)A返回ACK=4,以及當(dāng)前的TCP窗口大?。ㄟ@個(gè)窗口大小為接收端可用窗口),此時(shí)TCP窗口大小為2。(2)主機(jī)A在收到這些信息后,根據(jù)TCP窗口大小和主機(jī)B期待收到下一個(gè)字節(jié)的確認(rèn)序號(hào)滑動(dòng)其窗口,此時(shí)TCP窗口包含4~5數(shù)據(jù)段。同理繼續(xù)發(fā)送4~5和6~7數(shù)據(jù)段。(3)當(dāng)TCP窗口大小為0時(shí),主機(jī)A發(fā)送端將停止發(fā)送數(shù)據(jù),直到TCP窗口大小恢復(fù)為非零值。一些分布式計(jì)算引擎主要是利用了TCP窗口的特性,因此,無(wú)須加太多額外處理流程就能完成整套架構(gòu)的背壓,如Flink。2.6.2Flink實(shí)時(shí)計(jì)算引擎的背壓原理Flink采用Netty發(fā)送數(shù)據(jù)時(shí)的高低水位來(lái)控制整個(gè)鏈路的背壓,是非常好的Netty背壓實(shí)現(xiàn)實(shí)例。讀者可能會(huì)思考,為何像Dubbo這種RPC框架不采用Netty發(fā)送數(shù)據(jù)時(shí)的高低水位來(lái)控制整個(gè)鏈路的背壓呢?這是因?yàn)镽PC框架一般只需做請(qǐng)求TPS流量控制即可,而Flink有生產(chǎn)者和消費(fèi)者,若消費(fèi)者處理能力非常弱,則生產(chǎn)者需要得到感應(yīng),而且此時(shí)需要降低生產(chǎn)速率,以緩解消費(fèi)者的壓力。因此索引Flink無(wú)法通過(guò)限流來(lái)控制整個(gè)鏈路,需要用背壓機(jī)制來(lái)解決。在Flink中運(yùn)行的作業(yè)主要分以下三種。第一種:用于接收數(shù)據(jù),如接收Kafka的數(shù)據(jù)源。第二種:Task對(duì)這些數(shù)據(jù)進(jìn)行處理,如map、reduce、filter、join等操作,在這些操作過(guò)程中,可能會(huì)涉及HBase等數(shù)據(jù)庫(kù)的讀/寫操作。第三種:Task主要對(duì)第二種Task計(jì)算的結(jié)果數(shù)據(jù)進(jìn)行輸出,如輸出到Kafka、HBase中等。在高峰期,接收數(shù)據(jù)的速率遠(yuǎn)高于處理數(shù)據(jù)的速率。例如,當(dāng)程序更新版本時(shí),需要停止服務(wù),數(shù)據(jù)在Kafka上會(huì)積壓一段時(shí)間。當(dāng)服務(wù)啟動(dòng)時(shí),流入的數(shù)據(jù)會(huì)快速堆積。此時(shí),如果Flink沒(méi)有背壓,則可能會(huì)導(dǎo)致處理數(shù)據(jù)的TaskManager內(nèi)存耗盡,甚至整個(gè)系統(tǒng)直接崩潰。Flink究竟是如何處理背壓的呢?它是如何運(yùn)用Netty發(fā)送數(shù)據(jù)時(shí)的高低水位及TCP的流量控制來(lái)實(shí)現(xiàn)整條鏈路的背壓的呢?下面來(lái)看FlinkTask之間通信時(shí)的內(nèi)存分配與管理,如圖2-7所示。圖2-7FlinkTask之間通信時(shí)的內(nèi)存分配與管理考慮到很多Java程序員并未接觸過(guò)Flink、JStorm等大數(shù)據(jù)框架,因此不對(duì)其內(nèi)部架構(gòu)與原理進(jìn)行詳細(xì)的講解??梢园褕D2-7中的TaskManager看作一個(gè)JVM服務(wù),把Task看作一條線程,一個(gè)JVM服務(wù)中運(yùn)行多條Task線程,Task之間通過(guò)Netty長(zhǎng)連接進(jìn)行數(shù)據(jù)交互。當(dāng)然,一個(gè)TaskManager中只有一個(gè)Netty,當(dāng)Netty接收到數(shù)據(jù)后,它會(huì)把數(shù)據(jù)拷貝到Task中,拷貝數(shù)據(jù)需要內(nèi)存。圖2-7中的Task2作為消費(fèi)者,其內(nèi)存的申請(qǐng)及背壓處理步驟如下。(1)先到對(duì)應(yīng)Channel的LocalBufferPool緩沖池中進(jìn)行申請(qǐng),若緩沖池中沒(méi)有可用的內(nèi)存,且已申請(qǐng)的數(shù)量還未達(dá)到緩沖池的上限,則向NetworkBufferPool申請(qǐng)內(nèi)存塊,即圖中的①和②。(2)申請(qǐng)成功后,將其交給Channel填充數(shù)據(jù),即圖中的③和④。(3)若LocalBufferPool緩沖池申請(qǐng)的數(shù)量已經(jīng)達(dá)到上限或NetworkBufferPool中的內(nèi)存已經(jīng)被用盡,那么當(dāng)前Task的NettyChannel暫停讀取數(shù)據(jù)。此時(shí)數(shù)據(jù)積壓在TCP緩沖區(qū)中,導(dǎo)致其TCP窗口的大小變成零,其上游發(fā)送端會(huì)立刻暫停發(fā)送,整個(gè)鏈路進(jìn)入背壓狀態(tài)。(4)在Task1中,當(dāng)寫數(shù)據(jù)到達(dá)ResultPartition寫緩存中時(shí),也會(huì)向LocalBufferPool緩沖池請(qǐng)求內(nèi)存塊,如果沒(méi)有可用內(nèi)存塊,則線程狀態(tài)變成TIME_AWAIT,并且每隔一段時(shí)間就會(huì)去申請(qǐng)一次,整條線程也會(huì)阻塞在請(qǐng)求內(nèi)存塊的地方,達(dá)到暫停寫入的目的。那么,問(wèn)題來(lái)了,背壓形成后,整條鏈路什么時(shí)候才能恢復(fù)正常?想要恢復(fù)正常,只需Task線程在寫數(shù)據(jù)時(shí)能正常申請(qǐng)到內(nèi)存即可。當(dāng)Task2輸出端的處理能力增強(qiáng)后,會(huì)調(diào)用內(nèi)存回收方法,將內(nèi)存塊還給LocalBufferPool緩沖池。如果LocalBufferPool緩沖池中當(dāng)前申請(qǐng)的數(shù)量達(dá)到了上限,那么它會(huì)將該內(nèi)存塊回收給NetworkBufferPool,即圖2-7中的⑤和⑥,TCP窗口也會(huì)慢慢恢復(fù)正常。通過(guò)對(duì)圖2-7進(jìn)行的詳細(xì)分析可知,F(xiàn)link的背壓好像與Netty發(fā)送數(shù)據(jù)時(shí)的水位控制沒(méi)太大關(guān)系。但是,為了保證服務(wù)的穩(wěn)定,F(xiàn)link在數(shù)據(jù)生產(chǎn)端使用Netty發(fā)送數(shù)據(jù)時(shí)的高低水位機(jī)制來(lái)控制整個(gè)鏈路的背壓,不向緩存中寫太多數(shù)據(jù)。如果Netty輸出緩沖區(qū)的字節(jié)數(shù)超過(guò)了高水位值,則Channel.isWritable()為false,會(huì)觸發(fā)Handler的channelWritabilityChanged()方法。Flink在發(fā)送數(shù)據(jù)時(shí),若發(fā)現(xiàn)Channel.isWritable()為false,則不會(huì)從發(fā)送隊(duì)列中poll出需要發(fā)送的數(shù)據(jù),從而形成背壓。當(dāng)Netty輸出緩沖區(qū)的字節(jié)數(shù)降到低水位值以下時(shí),Channel.isWritable()返回true,同時(shí)channelWritabilityChanged()方法被觸發(fā),F(xiàn)link在channelWritabilityChanged事件中調(diào)用發(fā)送方法,這樣可以繼續(xù)發(fā)送數(shù)據(jù)。Netty的水位值設(shè)置如下:第5章會(huì)對(duì)Netty的高低水位進(jìn)行詳細(xì)的講解。本小節(jié)只是講了一些Flink的內(nèi)存管理及其主要背壓原理和思想,內(nèi)存管理對(duì)后續(xù)理解Netty的內(nèi)存管理有一定的幫助。例如,在圖2-7中,既然有了NetworkBufferPool,為何還要用到對(duì)應(yīng)Channel的LocalBufferPool緩沖池?這與Netty有了PoolArena,還需要用到PoolThreadLocalCache類似。目的是為每個(gè)Channel預(yù)先分配內(nèi)存,減少內(nèi)存在分配過(guò)程中的多線程競(jìng)爭(zhēng),提高性能。2.7小結(jié)本章大部分內(nèi)容都是TCP和NIO的理論知識(shí),主要涉及數(shù)據(jù)序列化、編/解碼器,以及數(shù)據(jù)的讀/寫和傳輸。本章內(nèi)容大部分在平時(shí)工作中很少接觸到,但對(duì)了解Netty底層原理有很大的幫助。想要寫出高性能的代碼,就必須對(duì)其底層原理有很深入的了解。第3章
分布式RPC本章運(yùn)用Netty實(shí)現(xiàn)了一套分布式RPC服務(wù)。結(jié)合第1章對(duì)客戶端和Netty服務(wù)端的長(zhǎng)連接通信,想要實(shí)現(xiàn)完整的分布式RPC,需要完成以下兩項(xiàng)工作。(1)在編寫業(yè)務(wù)代碼時(shí),只需像運(yùn)用SpringMVC或Dubbo一樣,引入對(duì)應(yīng)的jar包即可,無(wú)須關(guān)注太多的Netty底層實(shí)現(xiàn)。(2)服務(wù)端可動(dòng)態(tài)擴(kuò)展,不用指定具體的IP地址和端口進(jìn)行連接通信。當(dāng)服務(wù)端與客戶端通信時(shí),需要制定上層協(xié)議,運(yùn)用Java反射機(jī)制,把協(xié)議內(nèi)容與代碼進(jìn)行映射,讓業(yè)務(wù)代碼與Netty的Handler邏輯處理解耦。同時(shí)引入分布式協(xié)調(diào)器Zookeeper,實(shí)現(xiàn)服務(wù)的注冊(cè)與發(fā)現(xiàn),動(dòng)態(tài)擴(kuò)展服務(wù)。3.1Netty整合SpringWeb應(yīng)用程序開(kāi)發(fā)一般都會(huì)引入Spring框架,在RPC框架實(shí)現(xiàn)前需要先整合Spring容器。Spring的整合需要考慮如何調(diào)用Netty服務(wù)及啟動(dòng)Netty服務(wù)監(jiān)聽(tīng)端口。Netty服務(wù)啟動(dòng)后會(huì)阻塞線程,因此可以通過(guò)新建線程去啟動(dòng)它。由于Netty服務(wù)啟動(dòng)后會(huì)使用容器中的Bean,所以只有在Spring容器把所有的Bean初始化完成后才能去啟動(dòng)。Spring容器中有一種監(jiān)聽(tīng)器,可以在監(jiān)聽(tīng)到Bean初始化完成后得到觸發(fā),這種監(jiān)聽(tīng)器需要實(shí)現(xiàn)ApplicationListener<ContextRefreshedEvent>接口,在其onApplicationEvent()方法里啟動(dòng)Netty服務(wù):先在pom.xml文件中引入Spring5.1.9.RELEASE版本的依賴。具體實(shí)現(xiàn)代碼如下:編寫Spring容器啟動(dòng)類ApplicationMain。采用掃描注解方式啟動(dòng),并為Runtime添加關(guān)閉鉤子函數(shù),實(shí)現(xiàn)優(yōu)雅停機(jī)。具體代碼如下:在Spring監(jiān)聽(tīng)器類NettyApplicationListener的onApplicationEvent()方法中新建線程,啟動(dòng)Netty服務(wù),然后把Netty服務(wù)中main()方法的代碼放到start()方法中,即完成了Spring的整合。具體代碼如下:3.2采用Netty實(shí)現(xiàn)一套R(shí)PC框架雖然完成了Spring的整合,但只是采用Spring容器啟動(dòng)了Netty服務(wù)。本節(jié)采用Netty實(shí)現(xiàn)一套R(shí)PC框架,微服務(wù)統(tǒng)一使用這套框架來(lái)實(shí)現(xiàn)RPC通信。在改造代碼之前,先看一幅RPC內(nèi)部消息處理圖,如圖3-1所示。圖3-1RPC內(nèi)部消息處理圖在圖3-1中,除了請(qǐng)求轉(zhuǎn)交中介者和Controller處理請(qǐng)求并執(zhí)行業(yè)務(wù)代碼讀/寫數(shù)據(jù)庫(kù),大部分功能在第1章都已實(shí)現(xiàn)。在平時(shí)的工作中,當(dāng)運(yùn)用SpringMVC框架編寫業(yè)務(wù)代碼時(shí),只需新建Controller類和對(duì)應(yīng)的接口方法即可,無(wú)須知道SpringMVC的底層實(shí)現(xiàn)邏輯。接口實(shí)現(xiàn)完成后,通過(guò)HTTP請(qǐng)求指定URL來(lái)實(shí)現(xiàn)遠(yuǎn)程調(diào)用。運(yùn)用Netty實(shí)現(xiàn)的RPC與運(yùn)用SpringMVC實(shí)現(xiàn)的RPC類似。但對(duì)于通信協(xié)議,本書(shū)只支持TCP。SpringMVC底層主要通過(guò)解析URL獲取Controller對(duì)象和對(duì)應(yīng)的接口方法,然后運(yùn)用Java的反射運(yùn)行對(duì)應(yīng)的接口方法。獲取的方式依賴@Controller和@RequestMapping注解,由URL解析出接口方法上的注解@RequestMapping的值,再根據(jù)這個(gè)值映射對(duì)應(yīng)的接口方法。這種映射方式需要把注解@RequestMapping的值放入Map容器中緩存起來(lái),Map中的key為注解@RequestMapping的值、value為對(duì)應(yīng)的接口方法的Method對(duì)象。當(dāng)讀取URL時(shí),就相當(dāng)于有了key,此時(shí)就可以從容器中獲取接口方法的Method對(duì)象了。本書(shū)RPC框架的實(shí)現(xiàn)借用的也是上述這種方式,只是具體路徑需要客戶端和服務(wù)端制定上層協(xié)議。客戶端每次在發(fā)送請(qǐng)求時(shí)都需要把請(qǐng)求路徑傳送給服務(wù)端,服務(wù)端獲取路徑后,在本地緩存Map中得到對(duì)應(yīng)的調(diào)用方法。本地緩存Map在構(gòu)建之前需要先編寫注解類@Remote(與SpringMVC中的@RequestMapping注解類似),這個(gè)注解作用于接口方法,通過(guò)掃描這個(gè)注解類,可以獲取所有的接口方法。具體代碼如下:構(gòu)建本地緩存Map容器Mediator.methodBeans,用于緩存所有接口的對(duì)象和方法。把容器放在中介者M(jìn)ediator類中,這個(gè)類把Netty代碼與業(yè)務(wù)代碼解耦,后面還包含協(xié)議的解析,接口方法的調(diào)用。具體代碼如下:當(dāng)Spring容器啟動(dòng)并完成Bean的初始化后,可以運(yùn)用上下文刷新事件ContextRefreshedEvent,在事件中循環(huán)遍歷容器中的Bean,獲取帶有Controller的注解對(duì)象及其@Remote注解方法。并把它們放入緩存容器Mediator.methodBeans中。由于Netty服務(wù)的啟動(dòng)也是在ContextRefreshedEvent事件中完成的,所以兩個(gè)動(dòng)作的執(zhí)行有先后順序,為了保證在Netty服務(wù)啟動(dòng)前所有接口方法都已放入緩存容器中,Spring容器提供了Ordered接口,用來(lái)處理相同接口實(shí)現(xiàn)類的優(yōu)先級(jí)問(wèn)題。具體實(shí)現(xiàn)類InitLoadRemoteMethod的代碼如下:緩存容器Mediator.methodBeans初始化后,中介者M(jìn)ediator需要根據(jù)RequestFuture請(qǐng)求從緩存容器中獲取接口方法。RequestFuture類加上路徑Stringpath屬性,服務(wù)端Mediator根據(jù)path的值從緩存中獲取調(diào)用對(duì)象和方法,運(yùn)用Java反射運(yùn)行業(yè)務(wù)邏輯處理方法并獲取執(zhí)行結(jié)果。方法參數(shù)類型對(duì)List泛型集合需要用JSONArray反序列化。Mediator類的最終代碼如下:Mediator類作為中介者,銜接Netty服務(wù)的Handler類和業(yè)務(wù)邏輯處理類。如果需要對(duì)服務(wù)器的Handler進(jìn)行一些改動(dòng),就引入Mediator,并把請(qǐng)求RequestFuture交給它去處理。服務(wù)端的核心代碼基本上改造完了,最后編寫一個(gè)接口實(shí)例UserController類,這個(gè)類中有根據(jù)userId參數(shù)獲取用戶信息的方法。具體代碼如下:運(yùn)行服務(wù)端啟動(dòng)類ApplicationMain,同時(shí)修改NettyClient的main()方法,并在NettyClient類的sendRequest()方法中加上path參數(shù),把path參數(shù)getUserNameById賦給RequestFuture的path屬性。具體代碼如下:最終的RPCDemo測(cè)試輸出圖如圖3-2所示。圖3-2RPCDemo測(cè)試輸出圖3.3分布式RPC的構(gòu)建RPC框架整合了Spring,并實(shí)現(xiàn)了Netty核心通信代碼與業(yè)務(wù)邏輯代碼的解耦,但這個(gè)框架需要客戶端指定服務(wù)端具體的IP和端口才能調(diào)用,服務(wù)無(wú)法動(dòng)態(tài)擴(kuò)展。當(dāng)然,可以使用Nginx或LVS等反向代理服務(wù)實(shí)現(xiàn)橫向擴(kuò)展,但修改反向代理服務(wù)器配置并重啟無(wú)法動(dòng)態(tài)擴(kuò)展。本節(jié)引入一個(gè)分布式應(yīng)用程序協(xié)調(diào)服務(wù)——Zookeeper,以實(shí)現(xiàn)服務(wù)的注冊(cè)與發(fā)現(xiàn),完成一套分布式動(dòng)態(tài)擴(kuò)展RPC服務(wù)。3.3.1服務(wù)注冊(cè)與發(fā)現(xiàn)服務(wù)注冊(cè)與發(fā)現(xiàn)是分布式RPC必須具備的功能,目前市場(chǎng)上比較常用的服務(wù)注冊(cè)與發(fā)現(xiàn)的工具有Zookeeper、Consul、ETCD、Eureka。Consul在這幾款產(chǎn)品中功能比較全面,支持跨數(shù)據(jù)中心的同步、多語(yǔ)言接入、ACL(AccessControlLists,訪問(wèn)控制列表),并提供自身集群的監(jiān)控。在CAP原則的取舍上,Consul選擇了一致性和可用性;Zookeeper選擇了一致性和分區(qū)容錯(cuò)性,犧牲了可用性。從理論上來(lái)講,在服務(wù)發(fā)現(xiàn)的應(yīng)用場(chǎng)景下,Consul更合適。本書(shū)選擇Zookeeper來(lái)進(jìn)行分布式協(xié)調(diào),這主要是由于很多大數(shù)據(jù)組件基本上都會(huì)用Zookeeper,如Hadoop、HBase、JStorm、Kafka等。Zookeeper主要通過(guò)心跳來(lái)維護(hù)活動(dòng)會(huì)話的臨時(shí)節(jié)點(diǎn),從而維護(hù)集群中的服務(wù)器狀態(tài)。分布式RPC也是使用Zookeeper的臨時(shí)節(jié)點(diǎn)來(lái)維護(hù)Netty服務(wù)狀態(tài)的,RPC客戶端運(yùn)用Zookeeper的Watch機(jī)制監(jiān)聽(tīng)Netty服務(wù)在Zookeeper上注冊(cè)的目錄,從而及時(shí)感知服務(wù)列表的改變。分布式RPC架構(gòu)圖如圖3-3所示。圖3-3分布式RPC架構(gòu)圖圖3-3是在普通RPC架構(gòu)的基礎(chǔ)上進(jìn)行了些許改造,在服務(wù)端增加了服務(wù)注冊(cè)功能,同時(shí)客戶端新增了Watch機(jī)制,用來(lái)監(jiān)聽(tīng)Netty服務(wù)注冊(cè)在Zookeeper上的目錄??蛻舳伺cNetty服務(wù)連接的鏈路緩存在ChannelManager中,一旦發(fā)現(xiàn)有Netty服務(wù)宕機(jī)或新增的情況,緩存在ChannelManager中的鏈路就會(huì)發(fā)生相應(yīng)的改變??蛻舳嗣看卧诎l(fā)送請(qǐng)求之前都需要從ChannelManager中輪詢獲取一個(gè)連接。分布式服務(wù)發(fā)現(xiàn)的負(fù)載均衡算法采用的是輪詢加權(quán)重,每個(gè)服務(wù)的權(quán)重信息都放在配置文件中,當(dāng)Netty服務(wù)啟動(dòng)并向Zookeeper注冊(cè)時(shí),需要加上其權(quán)重信息。由于Netty服務(wù)與Zookeeper的會(huì)話也會(huì)出現(xiàn)被斷開(kāi)的情況,所以也需要在服務(wù)端加入監(jiān)聽(tīng)機(jī)制。具體實(shí)現(xiàn)步驟如下。(1)修改Netty服務(wù),加上服務(wù)注冊(cè)與服務(wù)監(jiān)聽(tīng)ServerWatcher類,只要發(fā)現(xiàn)其服務(wù)本身與Zookeeper的臨時(shí)會(huì)話丟失,就需要重新注冊(cè)。(2)客戶端新增與服務(wù)器列表連接的鏈路管理類ChannelManager,擁有鏈路緩存鏈表,以及對(duì)此鏈表提供增、刪、改、查的方法,且必須為原子性操作。(3)客戶端新增監(jiān)聽(tīng)器ServerChangeWatcher,監(jiān)聽(tīng)Netty服務(wù)注冊(cè)在Zookeeper上的目錄,且主要監(jiān)聽(tīng)其子目錄的變化。同時(shí)調(diào)用ChannelManager修改其鏈路緩存鏈表。(4)調(diào)整客戶端,不再通過(guò)服務(wù)端IP和端口直連,改成從ChannelManager中獲取連接。接下來(lái)完成這部分代碼的開(kāi)發(fā),先下載ZookeeperV3.4.6版本,并進(jìn)入其bin目錄,運(yùn)行zkServer.cmd,啟動(dòng)Zookeeper,同時(shí)將Zookeeper及其curator客戶端依賴引入pom.xml中。構(gòu)建Zookeeper連接的工廠類ZookeeperFactory,它擁有創(chuàng)建Zookeeper連接和重連的方法,其中main()方法只是調(diào)試使用。具體代碼如下:Netty服務(wù)類需要連接Zookeeper并注冊(cè)臨時(shí)節(jié)點(diǎn),還需要監(jiān)聽(tīng)類ServerWatcher監(jiān)聽(tīng)服務(wù)自身與Zookeeper的連接。當(dāng)臨時(shí)節(jié)點(diǎn)丟失時(shí),需要重新創(chuàng)建連接。具體代碼如下:分布式RPC服務(wù)端的修改基本上已完成,需要注意的是,一定要把服務(wù)端與Zookeeper的監(jiān)聽(tīng)加上,否則會(huì)出現(xiàn)服務(wù)端正常運(yùn)行,但Zookeeper中沒(méi)有此服務(wù)端注冊(cè)的臨時(shí)節(jié)點(diǎn)的情況。Zookeeper的地址,以及Netty服務(wù)啟動(dòng)監(jiān)聽(tīng)端口、權(quán)重、服務(wù)注冊(cè)到Zookeeper的路徑都需要寫入配置文件中,不能寫死在代碼里,配置信息在第8章會(huì)進(jìn)行修改??蛻舳说男薷南鄬?duì)服務(wù)端的修改稍微復(fù)雜些,客戶端需要新增兩個(gè)類,其中一個(gè)類主要用于管理與服務(wù)端創(chuàng)建的連接,另一個(gè)類主要用于監(jiān)聽(tīng)Zookeeper的SERVER_PATH路徑的變化、獲取最新的服務(wù)器列表信息、修改與服務(wù)器的連接列表。監(jiān)聽(tīng)類ServerChangeWatcher發(fā)現(xiàn)服務(wù)器列表,同時(shí)創(chuàng)建所有服務(wù)端的連接,當(dāng)獲取不到連接時(shí),還需提供初始化連接方法。具體實(shí)現(xiàn)代碼如下:需要為客戶端與服務(wù)端列表連接的鏈路管理類ChannelFutureManager提供一個(gè)線程安全的鏈路緩存鏈表,同時(shí)提供輪詢獲取連接鏈路的方法,以及其他修改緩存鏈表的輔助方法。具體代碼如下:Netty客戶端在發(fā)送數(shù)據(jù)前需要把創(chuàng)建與服務(wù)端連接的方式改成從ChannelFutureManager類中獲取,最后還需要一個(gè)常量,表示Netty服務(wù)已注冊(cè)到Zookeeper上的父目錄。在第8章還會(huì)增加幾個(gè)常量,整個(gè)RPC的服務(wù)注冊(cè)和服務(wù)發(fā)現(xiàn)已經(jīng)處理完了。還有部分參數(shù)需要根據(jù)壓測(cè)情況進(jìn)行相應(yīng)的調(diào)整。Netty客戶端改造后的代碼如下:3.3.2動(dòng)態(tài)代理分布式RPC框架在每次發(fā)送請(qǐng)求時(shí)都需要查看接口文檔,根據(jù)接口文檔封裝好請(qǐng)求參數(shù),指定具體的調(diào)用方法并設(shè)置請(qǐng)求路徑,只有這樣才能實(shí)現(xiàn)遠(yuǎn)程調(diào)用。顯然,這種方式開(kāi)發(fā)效率低、使用不夠便捷。想要把分布式RPC改造成類似Dubbo的框架,在只引入接口的jar包依賴、調(diào)用對(duì)應(yīng)的接口方法的情況下完成遠(yuǎn)程調(diào)用,就需要引入動(dòng)態(tài)代理,整體設(shè)計(jì)如圖3-4所示。圖3-4RPC動(dòng)態(tài)代理的調(diào)用示例圖3-4為用戶在登錄時(shí)使用動(dòng)態(tài)代理獲取用戶信息的遠(yuǎn)程調(diào)用過(guò)程,當(dāng)調(diào)用登錄接口時(shí),運(yùn)行UserService對(duì)象的getUserInfo()方法獲取用戶信息。然而,UserService是個(gè)接口,需要在服務(wù)啟動(dòng)時(shí)采用Java的反射機(jī)制把UserService屬性換成一個(gè)代理對(duì)象。常用的代理方式有以下兩種。(1)JDK動(dòng)態(tài)代理:通過(guò)java.lang.reflect.Proxy類的newProxyInstance()方法反射生成代理類。(2)Cglib動(dòng)態(tài)代理:采用Enhancer類的create()方法生成代理類,底層利用ASM字節(jié)碼生成框架,在內(nèi)存中生成一個(gè)需要被代理類的子類。上面這兩種代理方式都可以在RPC中應(yīng)用。在編碼前先了解Spring的一個(gè)擴(kuò)展接口——BeanPostProcessor。為了實(shí)現(xiàn)此接口及其兩個(gè)方法,可以在Bean初始化前后進(jìn)行一些額外的處理,這兩個(gè)方法分別是postProcessBeforeInitialization()和postProcessAfterInitialization()。當(dāng)bean在初始化并檢測(cè)到遠(yuǎn)程調(diào)用的Service屬性時(shí),把對(duì)應(yīng)的Service屬性通過(guò)Java反射和動(dòng)態(tài)代理修改成代理類,接下來(lái)分別采用兩套動(dòng)態(tài)代理機(jī)制完成整個(gè)代碼的改造。先使用JDK動(dòng)態(tài)代理編寫代理類JdkProxy,其核心功能有:動(dòng)態(tài)代理遠(yuǎn)程調(diào)用RPC服務(wù)端;在Bean初始化前獲取所有遠(yuǎn)程調(diào)用屬性并生成代理屬性,以替換遠(yuǎn)程調(diào)用屬性;使用注解RemoteInvoke標(biāo)識(shí)遠(yuǎn)程調(diào)用屬性。同時(shí),由于RequestFuture對(duì)象在動(dòng)態(tài)代理類中已構(gòu)建了,所以NettyClient類還需要重載一個(gè)sendRequest()方法。具體代碼如下:客戶端核心代碼還需要新增一個(gè)@RemoteInvoke注解:服務(wù)端可以去掉@Controller注解,@Remote注解可以同時(shí)作用于方法和類上,在Spring容器初始化后,不再通過(guò)@Remote注解值來(lái)作為緩存的Key,而是選擇@Remote注解上的類的所有方法,通過(guò)類名+方法名的方式組裝成Key,因此不再需要@Controller注解。具體代碼如下:編寫測(cè)試接口UserService、遠(yuǎn)程實(shí)現(xiàn)類UserServiceImpl、控制類LoginController、測(cè)試類TestProxyRpc。具體代碼如下:如果服務(wù)器和客戶端都是在本機(jī)IDEA啟動(dòng)調(diào)試的,那么一定要注意,當(dāng)Spring容器啟動(dòng)掃描包名時(shí),客戶端不能包含服務(wù)器涉及的包;服務(wù)器在啟動(dòng)時(shí)也要修改掃描包名。啟動(dòng)類ApplicationMain的具體代碼如下:最后按順序依次啟動(dòng)Zookeeper服務(wù)器、ApplicationMain類,運(yùn)行TestProxyRpc類的main()方法,運(yùn)行結(jié)果如圖3-5所示。圖3-5分布式RPC動(dòng)態(tài)代理測(cè)試運(yùn)行結(jié)果至此,JDK動(dòng)態(tài)代理已經(jīng)成功運(yùn)用在分布式RPC服務(wù)器上了。若使用Cglib動(dòng)態(tài)代理代替JDK動(dòng)態(tài)代理,就需要把JdkProxy的@Component注解注釋掉,同時(shí)新增一個(gè)類CglibProxy。CglibProxy與JdkProxy的區(qū)別在于動(dòng)態(tài)代理屬性的構(gòu)建方式不同,Cglib動(dòng)態(tài)代理使用Enhancer代替JDK動(dòng)態(tài)代理的Proxy。至此,整個(gè)分布式RPC服務(wù)的編碼全部結(jié)束了,可以對(duì)部分代碼進(jìn)行優(yōu)化調(diào)整。例如,序列化在使用了SPI(ServiceProviderInterface)技術(shù)后,可以不用寫死在代碼中,這樣可增強(qiáng)代理類的擴(kuò)展性。CglibProxy的具體代碼如下:第4章
Netty核心組件源碼剖析從本章開(kāi)始對(duì)Netty的核心類與方法進(jìn)行詳細(xì)的源碼剖析。反復(fù)閱讀Netty的源碼,不僅可以深入了解Netty的底層實(shí)現(xiàn)原理,對(duì)提升源碼的閱讀能力、自學(xué)能力也有很大的幫助。本章主要剖析與I/O線程模型相關(guān)的類、核心Channel組件、Netty緩沖區(qū)ByteBuf和Netty內(nèi)存泄漏檢測(cè)組件。它們大部分分布在ty.channel、ty.buffer、ty.util等包中。NioEventLoop、AbstractChannel、AbstractByteBuf是本章重點(diǎn)剖析的類。4.1NioEventLoopGroup源碼剖析第1章中為服務(wù)啟動(dòng)輔助類ServerBootstrap設(shè)置了兩個(gè)線程組,這兩個(gè)線程組都是NioEventLoopGroup線程組。NioEventLoopGroup線程組的功能、與NioEventLoop的關(guān)系及其整體設(shè)計(jì)是本節(jié)會(huì)詳細(xì)剖析的內(nèi)容。NioEventLoopGroup類主要完成以下3件事。?創(chuàng)建一定數(shù)量的NioEventLoop線程組并初始化。?創(chuàng)建線程選擇器chooser。當(dāng)獲取線程時(shí),通過(guò)選擇器來(lái)獲取。?創(chuàng)建線程工廠并構(gòu)建線程執(zhí)行器。NioEventLoopGroup的父類為MultithreadEventLoopGroup,父類繼承了抽象類MultithreadEventExecutorGroup。在初始化NioEventLoopGroup時(shí),會(huì)調(diào)用其父類的構(gòu)造方法。MultithreadEventLoopGroup中的DEFAULT_EVENT_LOOP_THREADS屬性決定生成多少NioEventLoop線程,默認(rèn)為CPU核數(shù)的兩倍,在構(gòu)造方法中會(huì)把此參數(shù)傳入,并最終調(diào)用MultithreadEventExecutorGroup類的構(gòu)造方法。此構(gòu)造方法運(yùn)用模板設(shè)計(jì)模式來(lái)構(gòu)建線程組生產(chǎn)模板。線程組的生產(chǎn)分兩步:第一步,創(chuàng)建一定數(shù)量的EventExecutor數(shù)組;第二步,通過(guò)調(diào)用子類的newChild()方法完成這些EventExecutor數(shù)組的初始化。為了提高可擴(kuò)展性,Netty的線程組除了NioEventLoopGroup,還有Netty通過(guò)JNI方式提供的一套由epoll模型實(shí)現(xiàn)的EpollEventLoopGroup線程組,以及其他I/O多路復(fù)用模型線程組,因此newChild()方法由具體的線程組子類來(lái)實(shí)現(xiàn)。MultithreadEventExecutorGroup的構(gòu)造方法和NioEventLoopGroup的newChild()方法的具體代碼解讀如下:在newChild()方法中,NioEventLoop的初始化參數(shù)有6個(gè):第1個(gè)參數(shù)為NioEventLoopGroup線程組本身;第2個(gè)參數(shù)為線程執(zhí)行器,用于啟動(dòng)線程,在SingleThreadEventExecutor的doStartThread()方法中被調(diào)用;第3個(gè)參數(shù)為NIO的Selector選擇器的提供者;第4個(gè)參數(shù)主要在NioEventLoop的run()方法中用于控制選擇循環(huán);第5個(gè)參數(shù)為非I/O任務(wù)提交被拒絕時(shí)的處理Handler;第6個(gè)參數(shù)為隊(duì)列工廠,在NioEventLoop中,隊(duì)列讀是單線程操作,而隊(duì)列寫則可能是多線程操作,使用支持多生產(chǎn)者、單消費(fèi)者的隊(duì)列比較合適,默認(rèn)為MpscChunkedArrayQueue隊(duì)列。NioEventLoopGroup通過(guò)next()方法獲取NioEventLoop線程,最終會(huì)調(diào)用其父類MultithreadEventExecutorGroup的next()方法,委托父類的選擇器EventExecutorChooser。具體使用哪種選擇器對(duì)象取決于MultithreadEventExecutorGroup的構(gòu)造方法中使用的策略模式。根據(jù)線程條數(shù)是否為2的冪次來(lái)選擇策略,若是,則選擇器為PowerOfTwoEventExecutorChooser,其選擇策略使用與運(yùn)算計(jì)算下一個(gè)選擇的線程組的下標(biāo)index,此計(jì)算方法在第7章中也有相似的應(yīng)用;若不是,則選擇器為GenericEventExecutorChooser,其選擇策略為使用求余的方法計(jì)算下一個(gè)線程在線程組中的下標(biāo)index。其中,PowerOfTwoEventExecutorChooser選擇器的與運(yùn)算性能會(huì)更好。由于Netty的NioEventLoop線程被包裝成了FastThreadLocalThread線程,同時(shí),NioEventLoop線程的狀態(tài)由它自身管理,因此每個(gè)NioEventLoop線程都需要有一個(gè)線程執(zhí)行器,并且在線程執(zhí)行前需要通過(guò)線程工廠ty.util.concurrent.DefaultThreadFactory將其包裝成FastThreadLocalThread線程。線程執(zhí)行器ThreadPerTaskExecutor與DefaultThreadFactory的newThread()方法的代碼解讀如下:4.2NioEventLoop源碼剖析NioEventLoop源碼比NioEventLoopGroup源碼復(fù)雜得多,每個(gè)NioEventLoop對(duì)象都與NIO中的多路復(fù)用器Selector一樣,要管理成千上萬(wàn)條鏈路,所有鏈路數(shù)據(jù)的讀/寫事件都由它來(lái)發(fā)起。本節(jié)通過(guò)NioEventLoo
溫馨提示
- 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒(méi)有圖紙預(yù)覽就沒(méi)有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 2024預(yù)制板購(gòu)銷合同
- 2025年度瓷磚研發(fā)中心實(shí)驗(yàn)室建設(shè)與運(yùn)營(yíng)合同3篇
- 2025年度危險(xiǎn)化學(xué)品儲(chǔ)存安全管理承包合同4篇
- 2025年度智能物流中心建設(shè)與運(yùn)營(yíng)管理合同4篇
- 2025年度商業(yè)地產(chǎn)租賃代理服務(wù)合同模板4篇
- 2024物業(yè)項(xiàng)目策劃2024委托代理合同
- 2025年度醫(yī)療器械代生產(chǎn)加工合同范本4篇
- 2025年度特殊用途車牌租賃與押金管理協(xié)議4篇
- 2025年度展會(huì)現(xiàn)場(chǎng)安保及應(yīng)急預(yù)案服務(wù)合同3篇
- 2024鐵路鋼軌鋪設(shè)及維護(hù)工程協(xié)議細(xì)則
- 勞動(dòng)合同續(xù)簽意見(jiàn)單
- 大學(xué)生國(guó)家安全教育意義
- 2024年保育員(初級(jí))培訓(xùn)計(jì)劃和教學(xué)大綱-(目錄版)
- 河北省石家莊市2023-2024學(xué)年高二上學(xué)期期末考試 語(yǔ)文 Word版含答案
- 企業(yè)正確認(rèn)識(shí)和運(yùn)用矩陣式管理
- 分布式光伏高處作業(yè)專項(xiàng)施工方案
- 陳閱增普通生物學(xué)全部課件
- 檢驗(yàn)科主任就職演講稿范文
- 人防工程主體監(jiān)理質(zhì)量評(píng)估報(bào)告
- 20225GRedCap通信技術(shù)白皮書(shū)
- 燃?xì)庥邢薰究蛻舴?wù)規(guī)范制度
評(píng)論
0/150
提交評(píng)論