netty socket超时设置_Netty 学习和进阶策略
背景
Netty 框架的特點(diǎn)
Netty 的一個(gè)特點(diǎn)就是入門(mén)相對(duì)比較容易,但是真正掌握并精通是非常困難的,原因有如下幾個(gè):
涉及的知識(shí)面比較廣:Netty 作為一個(gè)高性能的 NIO 通信框架,涉及到的知識(shí)點(diǎn)包括網(wǎng)絡(luò)通信、多線(xiàn)程編程、序列化和反序列化、異步和同步編程模型、SSL/TLS 安全、內(nèi)存池、HTTP、MQTT 等各種協(xié)議棧,這些知識(shí)點(diǎn)在 Java 語(yǔ)言中本身就是難點(diǎn)和重點(diǎn),如果對(duì)這些基礎(chǔ)知識(shí)掌握不扎實(shí),是很難真正掌握好 Netty 的。
調(diào)試比較困難:因?yàn)榇罅渴褂卯惒骄幊探涌?#xff0c;以及消息處理過(guò)程中的各種線(xiàn)程切換,相比于傳統(tǒng)同步代碼,調(diào)試難度比較大。
類(lèi)繼承層次比較深,有些代碼很晦澀(例如內(nèi)存池、Reactor 線(xiàn)程模型等),對(duì)于初學(xué)者而言,通過(guò)閱讀代碼來(lái)掌握 Netty 難度還是比較大的。
代碼規(guī)模龐大:目前,Netty 的代碼規(guī)模已經(jīng)非常龐大,特別是協(xié)議棧部分,提供了對(duì) HTTP/2、MQTT、WebSocket、SMTP 等多種協(xié)議的支持,相關(guān)代碼非常多。如果學(xué)習(xí)方式不當(dāng),抓不住重點(diǎn),全量閱讀 Netty 源碼,既耗時(shí)又很難吃透,很容易半途而廢。
資料比較零散,缺乏實(shí)踐相關(guān)的案例:網(wǎng)上各種 Netty 的資料非常多,但是以理論講解為主,Netty 在各行業(yè)中的應(yīng)用、問(wèn)題定位技巧以及案例實(shí)踐方面的資料很少,缺乏系統(tǒng)性的實(shí)踐總結(jié),也是 Netty 學(xué)習(xí)的一大痛點(diǎn)。
初學(xué)者常見(jiàn)問(wèn)題
對(duì)于很多初學(xué)者,在學(xué)習(xí)過(guò)程中經(jīng)常會(huì)遇到如下幾個(gè)問(wèn)題:
相關(guān)領(lǐng)域知識(shí)的儲(chǔ)備不足:想了解學(xué)習(xí) Netty 需要儲(chǔ)備哪些技能,掌握哪些知識(shí)點(diǎn),有什么學(xué)習(xí)技巧可以更快的掌握 Netty。由于對(duì) Java 多線(xiàn)程編程、Socket 通信、TCP/IP 協(xié)議棧等知識(shí)掌握不扎實(shí),后續(xù)在學(xué)習(xí) Netty 的過(guò)程中會(huì)遇到很多困難。
理論學(xué)習(xí)完,實(shí)踐遇到難題:學(xué)習(xí)完理論知識(shí)之后,想在實(shí)際項(xiàng)目中使用,但是真正跟具體項(xiàng)目結(jié)合在一起解決實(shí)際問(wèn)題時(shí),又感覺(jué)比較棘手,不知道自己使用的方式是否最優(yōu),希望能夠多學(xué)一些案例實(shí)踐方面的知識(shí),以便更好的在業(yè)務(wù)中使用 Netty。
出了問(wèn)題不會(huì)定位:在項(xiàng)目中遇到了問(wèn)題,但是由于對(duì) Netty 底層細(xì)節(jié)掌握不扎實(shí),無(wú)法有效的定位并解決問(wèn)題,只能靠網(wǎng)上搜索相關(guān)案例來(lái)參考,問(wèn)題解決效率比較低,甚至束手無(wú)策。
Netty 學(xué)習(xí)策略
Netty 入門(mén)相對(duì)簡(jiǎn)單,但是要在實(shí)際項(xiàng)目中用好它,出了問(wèn)題能夠快速定位和解決,卻并非易事。只有在入門(mén)階段扎實(shí)的學(xué)好 Netty,后面使用才能夠得心應(yīng)手。
入門(mén)知識(shí)準(zhǔn)備
Java NIO 類(lèi)庫(kù)
需要熟悉和掌握的類(lèi)庫(kù)主要包括:
緩沖區(qū) Buffer。
通道 Channel。
多路復(fù)用器 Selector。
首先介紹緩沖區(qū)(Buffer)的概念,Buffer 是一個(gè)對(duì)象,它包含一些要寫(xiě)入或者要讀出的數(shù)據(jù)。在 NIO 類(lèi)庫(kù)中加入 Buffer 對(duì)象,體現(xiàn)了新庫(kù)與原 I/O 的一個(gè)重要區(qū)別。在面向流的 I/O 中,可以將數(shù)據(jù)直接寫(xiě)入或者將數(shù)據(jù)直接讀到 Stream 對(duì)象中。在 NIO 庫(kù)中,所有數(shù)據(jù)都是用緩沖區(qū)處理的。在讀取數(shù)據(jù)時(shí),它是直接讀到緩沖區(qū)中的;在寫(xiě)入數(shù)據(jù)時(shí),寫(xiě)入到緩沖區(qū)中。任何時(shí)候訪(fǎng)問(wèn) NIO 中的數(shù)據(jù),都是通過(guò)緩沖區(qū)進(jìn)行操作。
緩沖區(qū)實(shí)質(zhì)上是一個(gè)數(shù)組。通常它是一個(gè)字節(jié)數(shù)組(ByteBuffer),也可以使用其他種類(lèi)的數(shù)組。但是一個(gè)緩沖區(qū)不僅僅是一個(gè)數(shù)組,緩沖區(qū)提供了對(duì)數(shù)據(jù)的結(jié)構(gòu)化訪(fǎng)問(wèn)以及維護(hù)讀寫(xiě)位置(limit)等信息。
最常用的緩沖區(qū)是 ByteBuffer,一個(gè) ByteBuffer 提供了一組功能用于操作 byte 數(shù)組。比較常用的就是 get 和 put 系列方法,如下所示:
圖 1 ByteBuffer 常用接口定義Channel 是一個(gè)通道,可以通過(guò)它讀取和寫(xiě)入數(shù)據(jù),它就像自來(lái)水管一樣,網(wǎng)絡(luò)數(shù)據(jù)通過(guò) Channel 讀取和寫(xiě)入。通道與流的不同之處在于通道是雙向的,流只是在一個(gè)方向上移動(dòng)(一個(gè)流必須是 InputStream 或者 OutputStream 的子類(lèi)),而且通道可以用于讀、寫(xiě)或者同時(shí)用于讀寫(xiě)。因?yàn)?Channel 是全雙工的,所以它可以比流更好地映射底層操作系統(tǒng)的 API。特別是在 UNIX 網(wǎng)絡(luò)編程模型中,底層操作系統(tǒng)的通道都是全雙工的,同時(shí)支持讀寫(xiě)操作。
比較常用的 Channel 是 SocketChannel 和 ServerSocketChannel,其中 SocketChannel 的繼承關(guān)系如下圖所示:
圖 2 SocketChannel 繼承關(guān)系Selector 是 Java NIO 編程的基礎(chǔ),熟練地掌握 Selector 對(duì)于掌握 NIO 編程至關(guān)重要。多路復(fù)用器提供選擇已經(jīng)就緒的任務(wù)的能力。簡(jiǎn)單來(lái)講,Selector 會(huì)不斷地輪詢(xún)注冊(cè)在其上的 Channel,如果某個(gè) Channel 上面有新的 TCP 連接接入、讀和寫(xiě)事件,這個(gè) Channel 就處于就緒狀態(tài),會(huì)被 Selector 輪詢(xún)出來(lái),然后通過(guò) SelectionKey 可以獲取就緒 Channel 的集合,進(jìn)行后續(xù)的 I/O 操作。
Java 多線(xiàn)程編程
作為異步事件驅(qū)動(dòng)、高性能的 NIO 框架,Netty 代碼中大量運(yùn)用了 Java 多線(xiàn)程編程技巧,熟練掌握多線(xiàn)程編程是掌握 Netty 的必備條件。
需要掌握的多線(xiàn)程編程相關(guān)知識(shí)包括:
Java 內(nèi)存模型。
關(guān)鍵字 synchronized。
讀寫(xiě)鎖。
volatile 的正確使用。
CAS 指令和原子類(lèi)。
JDK 線(xiàn)程池以及各種默認(rèn)實(shí)現(xiàn)。
以關(guān)鍵字 synchronized 為例,它可以保證在同一時(shí)刻,只有一個(gè)線(xiàn)程可以執(zhí)行某一個(gè)方法或者代碼塊。同步的作用不僅僅是互斥,它的另一個(gè)作用就是共享可變性,當(dāng)某個(gè)線(xiàn)程修改了可變數(shù)據(jù)并釋放鎖后,其它的線(xiàn)程可以獲取被修改變量的最新值。如果沒(méi)有正確的同步,這種修改對(duì)其它線(xiàn)程是不可見(jiàn)的。
下面我們就通過(guò)對(duì) Netty 的源碼進(jìn)行分析,看看 Netty 是如何對(duì)并發(fā)可變數(shù)據(jù)進(jìn)行正確同步的。以 AbstractBootstrap 為例進(jìn)行分析,首先看它的 option 方法:
這個(gè)方法的作用是設(shè)置 ServerBootstrap 或 Bootstrap 的 Socket 屬性,它的屬性集定義如下:
復(fù)制代碼
由于是非線(xiàn)程安全的 LinkedHashMap, 所以如果多線(xiàn)程創(chuàng)建、訪(fǎng)問(wèn)和修改 LinkedHashMap 時(shí),必須在外部進(jìn)行必要的同步。由于 ServerBootstrap 和 Bootstrap 被調(diào)用方線(xiàn)程創(chuàng)建和使用,無(wú)法保證它的方法和成員變量不被并發(fā)訪(fǎng)問(wèn)。因此,作為成員變量的 options 必須進(jìn)行正確的同步。由于考慮到鎖的范圍需要盡可能的小,所以對(duì)傳參的 option 和 value 的合法性判斷不需要加鎖,保證鎖的范圍盡可能的細(xì)粒度。
Netty 加鎖的地方非常多,大家在閱讀代碼的時(shí)候可以仔細(xì)體會(huì)下,為什么有的地方要加鎖,有的地方有不需要?如果不需要,為什么?當(dāng)你對(duì)鎖的原理理解以后,對(duì)于這些鎖的使用時(shí)機(jī)和技巧理解起來(lái)就相對(duì)容易了。
Netty 源碼學(xué)習(xí)
關(guān)鍵類(lèi)庫(kù)學(xué)習(xí)
Netty 的核心類(lèi)庫(kù)可以分為 5 大類(lèi),需要熟練掌握:
1、ByteBuf 和相關(guān)輔助類(lèi):ByteBuf 是個(gè) Byte 數(shù)組的緩沖區(qū),它的基本功能應(yīng)該與 JDK 的 ByteBuffer 一致,提供以下幾類(lèi)基本功能:
7 種 Java 基礎(chǔ)類(lèi)型、byte 數(shù)組、ByteBuffer(ByteBuf)等的讀寫(xiě)。
緩沖區(qū)自身的 copy 和 slice 等。
設(shè)置網(wǎng)絡(luò)字節(jié)序。
構(gòu)造緩沖區(qū)實(shí)例。
操作位置指針等方法。
動(dòng)態(tài)的擴(kuò)展和收縮。
從內(nèi)存分配的角度看,ByteBuf 可以分為兩類(lèi):堆內(nèi)存(HeapByteBuf)字節(jié)緩沖區(qū):特點(diǎn)是內(nèi)存的分配和回收速度快,可以被 JVM 自動(dòng)回收;缺點(diǎn)就是如果進(jìn)行 Socket 的 I/O 讀寫(xiě),需要額外做一次內(nèi)存復(fù)制,將堆內(nèi)存對(duì)應(yīng)的緩沖區(qū)復(fù)制到內(nèi)核 Channel 中,性能會(huì)有一定程度的下降。直接內(nèi)存(DirectByteBuf)字節(jié)緩沖區(qū):非堆內(nèi)存,它在堆外進(jìn)行內(nèi)存分配,相比于堆內(nèi)存,它的分配和回收速度會(huì)慢一些,但是將它寫(xiě)入或者從 Socket Channel 中讀取時(shí),由于少了一次內(nèi)存復(fù)制,速度比堆內(nèi)存快。
2、Channel 和 Unsafe:io.netty.channel.Channel 是 Netty 網(wǎng)絡(luò)操作抽象類(lèi),它聚合了一組功能,包括但不限于網(wǎng)路的讀、寫(xiě),客戶(hù)端發(fā)起連接、主動(dòng)關(guān)閉連接,鏈路關(guān)閉,獲取通信雙方的網(wǎng)絡(luò)地址等。它也包含了 Netty 框架相關(guān)的一些功能,包括獲取該 Chanel 的 EventLoop,獲取緩沖分配器 ByteBufAllocator 和 pipeline 等。Unsafe 是個(gè)內(nèi)部接口,聚合在 Channel 中協(xié)助進(jìn)行網(wǎng)絡(luò)讀寫(xiě)相關(guān)的操作,它提供的主要功能如下表所示:
Unsafe API 功能列表3、ChannelPipeline 和 ChannelHandler: Netty 的 ChannelPipeline 和 ChannelHandler 機(jī)制類(lèi)似于 Servlet 和 Filter 過(guò)濾器,這類(lèi)攔截器實(shí)際上是職責(zé)鏈模式的一種變形,主要是為了方便事件的攔截和用戶(hù)業(yè)務(wù)邏輯的定制。Servlet Filter 是 JEE Web 應(yīng)用程序級(jí)的 Java 代碼組件,它能夠以聲明的方式插入到 HTTP 請(qǐng)求響應(yīng)的處理過(guò)程中,用于攔截請(qǐng)求和響應(yīng),以便能夠查看、提取或以某種方式操作正在客戶(hù)端和服務(wù)器之間交換的數(shù)據(jù)。攔截器封裝了業(yè)務(wù)定制邏輯,能夠?qū)崿F(xiàn)對(duì) Web 應(yīng)用程序的預(yù)處理和事后處理。過(guò)濾器提供了一種面向?qū)ο蟮哪K化機(jī)制,用來(lái)將公共任務(wù)封裝到可插入的組件中。這些組件通過(guò) Web 部署配置文件(web.xml)進(jìn)行聲明,可以方便地添加和刪除過(guò)濾器,無(wú)須改動(dòng)任何應(yīng)用程序代碼或 JSP 頁(yè)面,由 Servlet 進(jìn)行動(dòng)態(tài)調(diào)用。通過(guò)在請(qǐng)求 / 響應(yīng)鏈中使用過(guò)濾器,可以對(duì)應(yīng)用程序(而不是以任何方式替代)的 Servlet 或 JSP 頁(yè)面提供的核心處理進(jìn)行補(bǔ)充,而不破壞 Servlet 或 JSP 頁(yè)面的功能。由于是純 Java 實(shí)現(xiàn),所以 Servlet 過(guò)濾器具有跨平臺(tái)的可重用性,使得它們很容易地被部署到任何符合 Servlet 規(guī)范的 JEE 環(huán)境中。
Netty 的 Channel 過(guò)濾器實(shí)現(xiàn)原理與 Servlet Filter 機(jī)制一致,它將 Channel 的數(shù)據(jù)管道抽象為 ChannelPipeline,消息在 ChannelPipeline 中流動(dòng)和傳遞。ChannelPipeline 持有 I/O 事件攔截器 ChannelHandler 的鏈表,由 ChannelHandler 對(duì) I/O 事件進(jìn)行攔截和處理,可以方便地通過(guò)新增和刪除 ChannelHandler 來(lái)實(shí)現(xiàn)不同的業(yè)務(wù)邏輯定制,不需要對(duì)已有的 ChannelHandler 進(jìn)行修改,能夠?qū)崿F(xiàn)對(duì)修改封閉和對(duì)擴(kuò)展的支持。ChannelPipeline 是 ChannelHandler 的容器,它負(fù)責(zé) ChannelHandler 的管理和事件攔截與調(diào)度:
圖 3 ChannelPipeline 對(duì)事件流的攔截和處理流Netty 中的事件分為 inbound 事件和 outbound 事件。inbound 事件通常由 I/O 線(xiàn)程觸發(fā),例如 TCP 鏈路建立事件、鏈路關(guān)閉事件、讀事件、異常通知事件等。
Outbound 事件通常是由用戶(hù)主動(dòng)發(fā)起的網(wǎng)絡(luò) I/O 操作,例如用戶(hù)發(fā)起的連接操作、綁定操作、消息發(fā)送等操作。ChannelHandler 類(lèi)似于 Servlet 的 Filter 過(guò)濾器,負(fù)責(zé)對(duì) I/O 事件或者 I/O 操作進(jìn)行攔截和處理,它可以選擇性地?cái)r截和處理自己感興趣的事件,也可以透?jìng)骱徒K止事件的傳遞。基于 ChannelHandler 接口,用戶(hù)可以方便地進(jìn)行業(yè)務(wù)邏輯定制,例如打印日志、統(tǒng)一封裝異常信息、性能統(tǒng)計(jì)和消息編解碼等。
4、EventLoop:Netty 的 NioEventLoop 并不是一個(gè)純粹的 I/O 線(xiàn)程,它除了負(fù)責(zé) I/O 的讀寫(xiě)之外,還兼顧處理以下兩類(lèi)任務(wù):
普通 Task:通過(guò)調(diào)用 NioEventLoop 的 execute(Runnable task) 方法實(shí)現(xiàn),Netty 有很多系統(tǒng) Task,創(chuàng)建它們的主要原因是:當(dāng) I/O 線(xiàn)程和用戶(hù)線(xiàn)程同時(shí)操作網(wǎng)絡(luò)資源時(shí),為了防止并發(fā)操作導(dǎo)致的鎖競(jìng)爭(zhēng),將用戶(hù)線(xiàn)程的操作封裝成 Task 放入消息隊(duì)列中,由 I/O 線(xiàn)程負(fù)責(zé)執(zhí)行,這樣就實(shí)現(xiàn)了局部無(wú)鎖化。
定時(shí)任務(wù):通過(guò)調(diào)用 NioEventLoop 的 schedule(Runnable command, long delay, TimeUnit unit) 方法實(shí)現(xiàn)。
Netty 的線(xiàn)程模型并不是一成不變的,它實(shí)際取決于用戶(hù)的啟動(dòng)參數(shù)配置。通過(guò)設(shè)置不同的啟動(dòng)參數(shù),Netty 可以同時(shí)支持 Reactor 單線(xiàn)程模型、多線(xiàn)程模型和主從 Reactor 多線(xiàn)層模型。它的工作原理如下所示:
圖 4 Netty 的線(xiàn)程模型通過(guò)調(diào)整線(xiàn)程池的線(xiàn)程個(gè)數(shù)、是否共享線(xiàn)程池等方式,Netty 的 Reactor 線(xiàn)程模型可以在單線(xiàn)程、多線(xiàn)程和主從多線(xiàn)程間切換,這種靈活的配置方式可以最大程度地滿(mǎn)足不同用戶(hù)的個(gè)性化定制。
為了盡可能地提升性能,Netty 在很多地方進(jìn)行了無(wú)鎖化的設(shè)計(jì),例如在 I/O 線(xiàn)程內(nèi)部進(jìn)行串行操作,避免多線(xiàn)程競(jìng)爭(zhēng)導(dǎo)致的性能下降問(wèn)題。表面上看,串行化設(shè)計(jì)似乎 CPU 利用率不高,并發(fā)程度不夠。但是,通過(guò)調(diào)整 NIO 線(xiàn)程池的線(xiàn)程參數(shù),可以同時(shí)啟動(dòng)多個(gè)串行化的線(xiàn)程并行運(yùn)行,這種局部無(wú)鎖化的串行線(xiàn)程設(shè)計(jì)相比一個(gè)隊(duì)列—多個(gè)工作線(xiàn)程的模型性能更優(yōu)。它的設(shè)計(jì)原理如下圖所示:
圖 5 NioEventLoop 串行執(zhí)行 ChannelHandler5、Future 和 Promise:在 Netty 中,所有的 I/O 操作都是異步的,這意味著任何 I/O 調(diào)用都會(huì)立即返回,而不是像傳統(tǒng) BIO 那樣同步等待操作完成。異步操作會(huì)帶來(lái)一個(gè)問(wèn)題:調(diào)用者如何獲取異步操作的結(jié)果?ChannelFuture 就是為了解決這個(gè)問(wèn)題而專(zhuān)門(mén)設(shè)計(jì)的。下面我們一起看它的原理。ChannelFuture 有兩種狀態(tài):uncompleted 和 completed。當(dāng)開(kāi)始一個(gè) I/O 操作時(shí),一個(gè)新的 ChannelFuture 被創(chuàng)建,此時(shí)它處于 uncompleted 狀態(tài)——非失敗、非成功、非取消,因?yàn)?I/O 操作此時(shí)還沒(méi)有完成。一旦 I/O 操作完成,ChannelFuture 將會(huì)被設(shè)置成 completed,它的結(jié)果有如下三種可能:
操作成功。
操作失敗。
操作被取消。
ChannelFuture 的狀態(tài)遷移圖如下所示:
圖 6 ChannelFuture 狀態(tài)遷移圖Promise 是可寫(xiě)的 Future,Future 自身并沒(méi)有寫(xiě)操作相關(guān)的接口,Netty 通過(guò) Promise 對(duì) Future 進(jìn)行擴(kuò)展,用于設(shè)置 I/O 操作的結(jié)果,它的接口定義如下:
圖 7 Netty 的 Promise 接口定義關(guān)鍵流程學(xué)習(xí)
需要重點(diǎn)掌握 Netty 服務(wù)端和客戶(hù)端的創(chuàng)建,以及創(chuàng)建過(guò)程中使用到的核心類(lèi)庫(kù)和 API、以及消息的發(fā)送和接收、消息的編解碼。
Netty 服務(wù)端創(chuàng)建流程如下:
圖 8 Netty 服務(wù)端創(chuàng)建流程Netty 客戶(hù)端創(chuàng)建流程如下:
圖 9 Netty 客戶(hù)端創(chuàng)建流程Netty 項(xiàng)目實(shí)踐
實(shí)踐主要分為兩類(lèi),如果項(xiàng)目中需要用到 Netty,則直接在項(xiàng)目中應(yīng)用,通過(guò)實(shí)踐來(lái)不斷提升對(duì) Netty 的理解和掌握。如果暫時(shí)使用不到,則可以通過(guò)學(xué)習(xí)一些開(kāi)源的 RPC 或者服務(wù)框架,看這些框架是怎么集成并使用 Netty 的。以 gRPC Java 版為例,我們一起看下 gRPC 是如何使用 Netty 的。
gRPC 服務(wù)端
gRPC 通過(guò)對(duì) Netty HTTP/2 的封裝,向用戶(hù)屏蔽底層 RPC 通信的協(xié)議細(xì)節(jié),Netty HTTP/2 服務(wù)端的創(chuàng)建流程如下:
圖 10 Netty HTTP/2 服務(wù)端創(chuàng)建流程服務(wù)端 HTTP/2 消息的讀寫(xiě)主要通過(guò) gRPC 的 NettyServerHandler 實(shí)現(xiàn),它的類(lèi)繼承關(guān)系如下所示:
圖 11 gRPC NettyServerHandler 類(lèi)繼承關(guān)系從類(lèi)繼承關(guān)系可以看出,NettyServerHandler 主要負(fù)責(zé) HTTP/2 協(xié)議消息相關(guān)的處理,例如 HTTP/2 請(qǐng)求消息體和消息頭的讀取、Frame 消息的發(fā)送、Stream 狀態(tài)消息的處理等,相關(guān)接口定義如下:
圖 12 NettyServerHandler 處理 HTTP/2 協(xié)議消息相關(guān)接口gRPC 客戶(hù)端
gRPC 的客戶(hù)端調(diào)用主要包括基于 Netty 的 HTTP/2 客戶(hù)端創(chuàng)建、客戶(hù)端負(fù)載均衡、請(qǐng)求消息的發(fā)送和響應(yīng)接收處理四個(gè)流程,gRPC 的客戶(hù)端調(diào)用總體流程如下圖所示:
gRPC 的客戶(hù)端調(diào)用總體流程如下圖所示:
圖 13 gRPC 客戶(hù)端總體調(diào)用流程gRPC 的客戶(hù)端調(diào)用流程如下:
客戶(hù)端 Stub(GreeterBlockingStub) 調(diào)用 sayHello(request),發(fā)起 RPC 調(diào)用。
通過(guò) DnsNameResolver 進(jìn)行域名解析,獲取服務(wù)端的地址信息(列表),隨后使用默認(rèn)的 LoadBalancer 策略,選擇一個(gè)具體的 gRPC 服務(wù)端實(shí)例。
如果與路由選中的服務(wù)端之間沒(méi)有可用的連接,則創(chuàng)建 NettyClientTransport 和 NettyClientHandler,發(fā)起 HTTP/2 連接。
對(duì)請(qǐng)求消息使用 PB(Protobuf)做序列化,通過(guò) HTTP/2 Stream 發(fā)送給 gRPC 服務(wù)端。
接收到服務(wù)端響應(yīng)之后,使用 PB(Protobuf)做反序列化。
回調(diào) GrpcFuture 的 set(Response) 方法,喚醒阻塞的客戶(hù)端調(diào)用線(xiàn)程,獲取 RPC 響應(yīng)。
需要指出的是,客戶(hù)端同步阻塞 RPC 調(diào)用阻塞的是調(diào)用方線(xiàn)程(通常是業(yè)務(wù)線(xiàn)程),底層 Transport 的 I/O 線(xiàn)程(Netty 的 NioEventLoop)仍然是非阻塞的。
線(xiàn)程模型
gRPC 服務(wù)端線(xiàn)程模型整體上可以分為兩大類(lèi):
網(wǎng)絡(luò)通信相關(guān)的線(xiàn)程模型,基于 Netty4.1 的線(xiàn)程模型實(shí)現(xiàn)。
服務(wù)接口調(diào)用線(xiàn)程模型,基于 JDK 線(xiàn)程池實(shí)現(xiàn)。
gRPC 服務(wù)端線(xiàn)程模型和交互圖如下所示:
圖 14 gRPC 服務(wù)端線(xiàn)程模型其中,HTTP/2 服務(wù)端創(chuàng)建、HTTP/2 請(qǐng)求消息的接入和響應(yīng)發(fā)送都由 Netty 負(fù)責(zé),gRPC 消息的序列化和反序列化、以及應(yīng)用服務(wù)接口的調(diào)用由 gRPC 的 SerializingExecutor 線(xiàn)程池負(fù)責(zé)。
gRPC 客戶(hù)端的線(xiàn)程主要分為三類(lèi):
業(yè)務(wù)調(diào)用線(xiàn)程
客戶(hù)端連接和 I/O 讀寫(xiě)線(xiàn)程
請(qǐng)求消息業(yè)務(wù)處理和響應(yīng)回調(diào)線(xiàn)程
gRPC 客戶(hù)端線(xiàn)程模型工作原理如下圖所示(同步阻塞調(diào)用為例):
圖 15 客戶(hù)端調(diào)用線(xiàn)程模型客戶(hù)端調(diào)用主要涉及的線(xiàn)程包括:
應(yīng)用線(xiàn)程,負(fù)責(zé)調(diào)用 gRPC 服務(wù)端并獲取響應(yīng),其中請(qǐng)求消息的序列化由該線(xiàn)程負(fù)責(zé)。
客戶(hù)端負(fù)載均衡以及 Netty Client 創(chuàng)建,由 grpc-default-executor 線(xiàn)程池負(fù)責(zé)。
HTTP/2 客戶(hù)端鏈路創(chuàng)建、網(wǎng)絡(luò) I/O 數(shù)據(jù)的讀寫(xiě),由 Netty NioEventLoop 線(xiàn)程負(fù)責(zé)。
響應(yīng)消息的反序列化由 SerializingExecutor 負(fù)責(zé),與服務(wù)端不同的是,客戶(hù)端使用的是 ThreadlessExecutor,并非 JDK 線(xiàn)程池。
SerializingExecutor 通過(guò)調(diào)用 responseFuture 的 set(value),喚醒阻塞的應(yīng)用線(xiàn)程,完成一次 RPC 調(diào)用。
gRPC 采用的是網(wǎng)絡(luò) I/O 線(xiàn)程和業(yè)務(wù)調(diào)用線(xiàn)程分離的策略,大部分場(chǎng)景下該策略是最優(yōu)的。但是,對(duì)于那些接口邏輯非常簡(jiǎn)單,執(zhí)行時(shí)間很短,不需要與外部網(wǎng)元交互、訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)和磁盤(pán),也不需要等待其它資源的,則建議接口調(diào)用直接在 Netty /O 線(xiàn)程中執(zhí)行,不需要再投遞到后端的服務(wù)線(xiàn)程池。避免線(xiàn)程上下文切換,同時(shí)也消除了線(xiàn)程并發(fā)問(wèn)題。
例如提供配置項(xiàng)或者接口,系統(tǒng)默認(rèn)將消息投遞到后端服務(wù)調(diào)度線(xiàn)程,但是也支持短路策略,直接在 Netty 的 NioEventLoop 中執(zhí)行消息的序列化和反序列化、以及服務(wù)接口調(diào)用。
減少鎖競(jìng)爭(zhēng)優(yōu)化:當(dāng)前 gRPC 的線(xiàn)程切換策略如下:
圖 16 gRPC 線(xiàn)程鎖競(jìng)爭(zhēng)優(yōu)化之后的 gRPC 線(xiàn)程切換策略:
圖 17 gRPC 線(xiàn)程鎖競(jìng)爭(zhēng)優(yōu)化通過(guò)線(xiàn)程綁定技術(shù)(例如采用一致性 hash 做映射), 將 Netty 的 I/O 線(xiàn)程與后端的服務(wù)調(diào)度線(xiàn)程做綁定,1 個(gè) I/O 線(xiàn)程綁定一個(gè)或者多個(gè)服務(wù)調(diào)用線(xiàn)程,降低鎖競(jìng)爭(zhēng),提升性能。
Netty 故障定位技巧
盡管 Netty 應(yīng)用廣泛,非常成熟,但是由于對(duì) Netty 底層機(jī)制不太了解,用戶(hù)在實(shí)際使用中還是會(huì)經(jīng)常遇到各種問(wèn)題,大部分問(wèn)題都是業(yè)務(wù)使用不當(dāng)導(dǎo)致的。Netty 使用者需要學(xué)習(xí) Netty 的故障定位技巧,以便出了問(wèn)題能夠獨(dú)立、快速的解決。‘’
接收不到消息
如果業(yè)務(wù)的 ChannelHandler 接收不到消息,可能的原因如下:
業(yè)務(wù)的解碼 ChannelHandler 存在 BUG,導(dǎo)致消息解碼失敗,沒(méi)有投遞到后端。
業(yè)務(wù)發(fā)送的是畸形或者錯(cuò)誤碼流(例如長(zhǎng)度錯(cuò)誤),導(dǎo)致業(yè)務(wù)解碼 ChannelHandler 無(wú)法正確解碼出業(yè)務(wù)消息。
業(yè)務(wù) ChannelHandler 執(zhí)行了一些耗時(shí)或者阻塞操作,導(dǎo)致 Netty 的 NioEventLoop 被掛住,無(wú)法讀取消息。
執(zhí)行業(yè)務(wù) ChannelHandler 的線(xiàn)程池隊(duì)列積壓,導(dǎo)致新接收的消息在排隊(duì),沒(méi)有得到及時(shí)處理。
對(duì)方確實(shí)沒(méi)有發(fā)送消息。
定位策略如下:
在業(yè)務(wù)的首個(gè) ChannelHandler 的 channelRead 方法中打斷點(diǎn)調(diào)試,看是否讀取到消息。
在 ChannelHandler 中添加 LoggingHandler,打印接口日志。
查看 NioEventLoop 線(xiàn)程狀態(tài),看是否發(fā)生了阻塞。
通過(guò) tcpdump 抓包看消息是否發(fā)送成功。
內(nèi)存泄漏
通過(guò) jmap -dump:format=b,file=xx pid 命令 Dump 內(nèi)存堆棧,然后使用 MemoryAnalyzer 工具對(duì)內(nèi)存占用進(jìn)行分析,查找內(nèi)存泄漏點(diǎn),然后結(jié)合代碼進(jìn)行分析,定位內(nèi)存泄漏的具體原因,示例如下所示:
圖 18 通過(guò) MemoryAnalyzer 工具分析內(nèi)存堆棧性能問(wèn)題
如果出現(xiàn)性能問(wèn)題,首先需要確認(rèn)是 Netty 問(wèn)題還是業(yè)務(wù)問(wèn)題,通過(guò) jstack 命令或者 jvisualvm 工具打印線(xiàn)程堆棧,按照線(xiàn)程 CPU 使用率進(jìn)行排序(top -Hp 命令采集),看線(xiàn)程在忙什么。通常如果采集幾次都發(fā)現(xiàn) Netty 的 NIO 線(xiàn)程堆棧停留在 select 操作上,說(shuō)明 I/O 比較空閑,性能瓶頸不在 Netty,需要繼續(xù)分析看是否是后端的業(yè)務(wù)處理線(xiàn)程存在性能瓶頸:
圖 19 Netty NIO 線(xiàn)程運(yùn)行堆棧如果發(fā)現(xiàn)性能瓶頸在網(wǎng)絡(luò) I/O 讀寫(xiě)上,可以適當(dāng)調(diào)大 NioEventLoopGroup 中的 work I/O 線(xiàn)程數(shù),直到 I/O 處理性能能夠滿(mǎn)足業(yè)務(wù)需求。
總結(jié)
以上是生活随笔為你收集整理的netty socket超时设置_Netty 学习和进阶策略的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: arrays中copyof_为什么阿里巴
- 下一篇: java8 lambda python_