制剖析:責(zé)任鏈與事件流轉(zhuǎn)實(shí)戰(zhàn))
排查過(guò)一次線上偶發(fā)斷連問(wèn)題后我對(duì)Netty里的ChannelHandler算是真正服氣了。當(dāng)時(shí)問(wèn)題表象是客戶端偶爾收不到響應(yīng)服務(wù)端日志一片正常最后在Pipeline上加了日志Handler才定位到某個(gè)中間Handler拋了異常后續(xù)Handler根本沒(méi)機(jī)會(huì)執(zhí)行。從那時(shí)起我就意識(shí)到如果只停留在會(huì)用addLast、不會(huì)追問(wèn)事件到底怎么流轉(zhuǎn)遇到詭異問(wèn)題基本只能靠猜。ChannelHandler是Netty最核心的抽象之一它決定了一條連接上所有數(shù)據(jù)幀的進(jìn)出路徑、業(yè)務(wù)編排方式和異常兜底策略。這篇文章我會(huì)從責(zé)任鏈設(shè)計(jì)、Handler類型與生命周期、事件雙向流轉(zhuǎn)的底層邏輯再到粘包拆包這一最高頻實(shí)戰(zhàn)場(chǎng)景把ChannelHandler的機(jī)制和工作原理講透。適合剛接觸Netty想系統(tǒng)理解Pipeline的開(kāi)發(fā)者也適合工作中被Handler順序、ByteBuf釋放、異常傳播折磨過(guò)的老手。1. ChannelHandler在Netty整體架構(gòu)中的定位從一條連接說(shuō)起1.1 一次連接的生命周期視角一條TCP連接在Netty里對(duì)應(yīng)一個(gè)Channel準(zhǔn)確說(shuō)是NioSocketChannel或EpollSocketChannel這類實(shí)現(xiàn)。這個(gè)Channel從accept到read再到write中間要經(jīng)過(guò)非常多的處理解幀、反序列化、鑒權(quán)、業(yè)務(wù)邏輯、序列化、組幀、寫(xiě)回。如果把這些邏輯全塞進(jìn)一個(gè)回調(diào)里代碼會(huì)迅速腐化而且完全沒(méi)辦法組合復(fù)用。Netty給出的答案是ChannelPipeline一條雙向鏈表。鏈表上的每個(gè)節(jié)點(diǎn)就是一個(gè)ChannelHandler。數(shù)據(jù)從網(wǎng)絡(luò)進(jìn)來(lái)按鏈表順序流經(jīng)注冊(cè)的Handler數(shù)據(jù)要發(fā)出去也按順序反向流經(jīng)Handler。核心設(shè)計(jì)就是責(zé)任鏈模式每個(gè)Handler只關(guān)心自己該處理的那部分處理完用fire方法把事件交給下一個(gè)節(jié)點(diǎn)不處理就透明通過(guò)。打個(gè)比方這很像機(jī)場(chǎng)安檢通道。旅客數(shù)據(jù)從入口進(jìn)入依次經(jīng)過(guò)證件查驗(yàn)、行李掃描、人身檢查幾個(gè)環(huán)節(jié)。每個(gè)環(huán)節(jié)只干自己那件事完后把人交給下一個(gè)環(huán)節(jié)。如果某個(gè)環(huán)節(jié)發(fā)現(xiàn)異常整條通道就要攔截。Netty的Pipeline就是這個(gè)安檢通道ChannelHandler就是那些安檢柜位。1.2 為什么選責(zé)任鏈而不是強(qiáng)綁定回調(diào)很多新框架更愿意用dispatcher或者回調(diào)注冊(cè)的方式來(lái)處理請(qǐng)求比如一條連接上綁一個(gè)onData回調(diào)、onClose回調(diào)。這種模型在簡(jiǎn)單場(chǎng)景下很清爽一旦鏈路變長(zhǎng)比如要同時(shí)支持協(xié)議升級(jí)、流量統(tǒng)計(jì)、日志采集、多版本編解碼回調(diào)之間就變成一團(tuán)亂麻誰(shuí)先執(zhí)行、誰(shuí)的數(shù)據(jù)要透?jìng)鹘o誰(shuí)、出錯(cuò)誰(shuí)兜底全得靠約定。責(zé)任鏈的好處在于順序就是規(guī)則Pipeline里Handler的排列順序直接決定了處理順序。這一點(diǎn)對(duì)協(xié)議處理至關(guān)重要。比如粘包拆包的Decode必須排在業(yè)務(wù)Handler前因?yàn)闃I(yè)務(wù)Handler拿到的必須已經(jīng)是一個(gè)完整的消息鑒權(quán)Handler又必須排在使用身份信息的Handler前。順序可插拔行為可組合這是大型服務(wù)器程序非常需要的彈性。還有一個(gè)容易被忽略的點(diǎn)責(zé)任鏈天然支持動(dòng)態(tài)修改。調(diào)用Pipeline的addBefore、addAfter、remove方法可以在線調(diào)整處理鏈路不用重啟服務(wù)就能改協(xié)議行為。這在灰度發(fā)布、動(dòng)態(tài)協(xié)議適配場(chǎng)景里價(jià)值巨大。從實(shí)現(xiàn)角度看Pipeline內(nèi)部維護(hù)著DefaultChannelHandlerContext組成的雙向鏈表每個(gè)Context包裹一個(gè)Handler實(shí)例同時(shí)保存著Channel、Executor等信息。鏈表的頭部是HeadContext尾部是TailContext這兩個(gè)是Netty內(nèi)置的不對(duì)外暴露也不能刪除。所有用戶Handler都夾在它們之間。2. Handler類型與核心方法不只是讀和寫(xiě)2.1 三個(gè)族譜Inbound、Outbound、DuplexHandlerChannelHandler接口本身是個(gè)標(biāo)記接口只有兩個(gè)生命周期方法handlerAdded和handlerRemoved。真正干活的是它的子接口。ChannelInboundHandler處理入站事件也就是數(shù)據(jù)從網(wǎng)絡(luò)進(jìn)來(lái)后觸發(fā)的事件包括channelRegistered、channelActive、channelRead、channelReadComplete、exceptionCaught、channelInactive等。這些方法命名基本都是“事件被動(dòng)發(fā)生”由Netty的事件循環(huán)調(diào)用。ChannelOutboundHandler處理出站事件包括bind、connect、write、flush、close、read等。注意這里有個(gè)非常重要的語(yǔ)義區(qū)別入站Handler里你被動(dòng)接收事件出站Handler里你主動(dòng)發(fā)起操作。比如調(diào)用ctx.write(data)并不是直接把數(shù)據(jù)塞進(jìn)Socket而是發(fā)起一個(gè)出站事件讓出站鏈路沿途的Handler都能處理。ChannelDuplexHandler同時(shí)繼承兩者既能處理入站也能攔截出站適合做日志、統(tǒng)計(jì)、鑒權(quán)、編解碼這類橫切邏輯。協(xié)議編解碼器其實(shí)最適合用DuplexHandler實(shí)現(xiàn)因?yàn)榫幋a管出站、解碼管入站兩者本來(lái)就是對(duì)同一協(xié)議的正反兩面。2.2 生命周期回調(diào)從注冊(cè)到斷開(kāi)Handler掛在Pipeline上之后會(huì)隨著Channel的狀態(tài)變化觸發(fā)一系列生命周期回調(diào)。順序大致是handlerAddedHandler被加入Pipeline時(shí)觸發(fā)??梢杂脕?lái)做資源初始化。channelRegisteredChannel綁定到EventLoop后觸發(fā)。channelActive連接建立完成后觸發(fā)TCP層面可以開(kāi)始讀寫(xiě)。channelRead收到數(shù)據(jù)幀時(shí)觸發(fā)。注意這里是已經(jīng)經(jīng)過(guò)解碼的數(shù)據(jù)。channelReadComplete一次讀循環(huán)讀取完所有數(shù)據(jù)后觸發(fā)。適合批量刷新、發(fā)送心跳等。channelInactive連接斷開(kāi)或失效時(shí)觸發(fā)。handlerRemovedHandler從Pipeline移除時(shí)觸發(fā)。適合釋放資源。理解這個(gè)順序很重要。channelActive是發(fā)送歡迎消息的理想時(shí)機(jī)因?yàn)榇丝踢B接真正可用channelInactive是清理連接級(jí)狀態(tài)的位置channelReadComplete則是你處理完一批數(shù)據(jù)的收尾點(diǎn)。很多人會(huì)把channelRead里做太多事但把flush、批量提交這類操作放在channelReadComplete會(huì)更合適。2.3 ChannelHandlerContext每個(gè)Handler的隱形傳話人每個(gè)Handler在Pipeline里都有一個(gè)對(duì)應(yīng)的ChannelHandlerContext。這個(gè)Context暴露了幾乎全部交互入口讀寫(xiě)數(shù)據(jù)、觸發(fā)下一個(gè)Handler、獲取Channel和EventLoop。一定要養(yǎng)成通過(guò)ctx去調(diào)用傳播方法fireChannelRead、write等的習(xí)慣而不是用Channel的write方法。原因很微妙ctx.fireChannelRead是從當(dāng)前節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)開(kāi)始傳播channel.write則是從Pipeline的Tail開(kāi)始反向走完整條鏈路。這會(huì)導(dǎo)致行為完全不同后面我會(huì)專門展開(kāi)。另外ChannelHandlerContext里還有一個(gè)executor()方法返回Handler執(zhí)行的EventLoop。如果你了解Netty的線程模型就會(huì)知道每個(gè)Channel綁定一個(gè)EventLoop線程Handler基本上都在這個(gè)線程里被調(diào)用。所以單個(gè)Channel內(nèi)Handler狀態(tài)天然不用加鎖這是Netty高性能的基石之一。但如果你在Handler里把數(shù)據(jù)提交到別的線程池處理那就要注意跨線程同步了。3. 事件在流水線上流轉(zhuǎn)的底層邏輯入站與出站的兩個(gè)方向3.1 fireXxx方法是如何觸發(fā)下一個(gè)節(jié)點(diǎn)的當(dāng)Socket讀到了字節(jié)流Netty這個(gè)內(nèi)部會(huì)封裝成ByteBuf并觸發(fā)一次channelRead入站事件。這個(gè)事件的起點(diǎn)其實(shí)是HeadContext它調(diào)用下一個(gè)Handler的channelRead方法然后用戶代碼手動(dòng)調(diào)用ctx.fireChannelRead(msg)再沿著鏈表向下傳遞最終到達(dá)TailContext被丟棄或釋放??吹?jīng)]有這里的關(guān)鍵是“手動(dòng)”兩個(gè)字。Handler要主動(dòng)調(diào)用fire方法事件才能繼續(xù)往下走。你不調(diào)鏈路就斷在這里。這既是責(zé)任鏈的靈活性也是最大的坑很多新手在channelRead里處理完消息后忘了調(diào)fireChannelRead導(dǎo)致后面Handler永遠(yuǎn)等不到數(shù)據(jù)。入站傳播方法包括fireChannelRegistered、fireChannelActive、fireChannelRead、fireChannelReadComplete、fireExceptionCaught、fireUserEventTriggered等它們都遵循同一個(gè)規(guī)則從當(dāng)前Context的下一個(gè)節(jié)點(diǎn)開(kāi)始沿正向鏈表傳播。3.2 出站方向的write事件為什么需要自己調(diào)ctx.write出站方向麻煩一點(diǎn)。當(dāng)你要向客戶端寫(xiě)數(shù)據(jù)時(shí)調(diào)用的是ctx.writeAndFlush(data)。這不是直接把數(shù)據(jù)推到Socket而是發(fā)起一個(gè)出站事件讓數(shù)據(jù)從當(dāng)前Context開(kāi)始沿鏈表反向傳播。也就是說(shuō)出站事件是“從后往前”走的最靠近Tail的Handler反而先執(zhí)行。這帶來(lái)一個(gè)非常容易踩坑的設(shè)計(jì)Encoder編碼器通常放在Pipeline靠前的位置也就是越靠近ChannelInboundHandler報(bào)讀之后的位置越靠前但出站時(shí)編碼器卻是靠后執(zhí)行的。為什么呢因?yàn)槌稣臼录膶?xiě)入點(diǎn)開(kāi)始逆向往Head方向傳播Encoder放在前面意味著它比后面的Handler更靠近Head會(huì)在鏈路靠后階段執(zhí)行此時(shí)數(shù)據(jù)已經(jīng)被靠前執(zhí)行的Handler包裝過(guò)最終交給Head寫(xiě)入Socket。我舉個(gè)例子。Pipeline順序是StringEncoderFrameLengthDecoder業(yè)務(wù)Handler當(dāng)業(yè)務(wù)Handler調(diào)用ctx.writeAndFlush({json字符串})出站事件從業(yè)務(wù)Handler所在Context開(kāi)始往前找先到FrameLengthDecoder然后再到StringEncoder。StringEncoder把字符串編碼成ByteBuf后再繼續(xù)傳向HeadContext最后寫(xiě)入Socket。如果你在Pipeline里把Encoder放在業(yè)務(wù)Handler后面那出站事件從業(yè)務(wù)Handler開(kāi)始往前找永遠(yuǎn)找不到Encoder字符串就原樣寫(xiě)出去了。很多人Handler順序排得奇怪就是因?yàn)樗麄儧](méi)有理解出站傳播方向。3.3 HeadContext與TailContext管道兩端發(fā)生了什么Pipeline的兩端是內(nèi)置節(jié)點(diǎn)。HeadContext既實(shí)現(xiàn)ChannelInboundHandler也實(shí)現(xiàn)ChannelOutboundHandler它的一頭連接著事件循環(huán)和底層Socket另一頭對(duì)接Handler鏈TailContext大多數(shù)情況下是個(gè)“兜底”節(jié)點(diǎn)入站事件到了它這里如果沒(méi)有被消費(fèi)默認(rèn)是釋放消息防止內(nèi)存泄漏。有意思的是當(dāng)你調(diào)用channel.write(data)時(shí)事件其實(shí)是從TailContext開(kāi)始反向傳播的所以鏈條上所有出站Handler都會(huì)看到這條數(shù)據(jù)。從這個(gè)角度來(lái)看調(diào)用channel.write和ctx.write有本質(zhì)區(qū)別channel.write一定會(huì)被整條出站鏈路處理ctx.write只會(huì)被當(dāng)前節(jié)點(diǎn)之前的出站節(jié)點(diǎn)處理。如果你在業(yè)務(wù)Handler里不小心用了channel.writeAndFlush那下行數(shù)據(jù)會(huì)無(wú)視你之前的Handler順序直接沖到Head該有的加密、編碼全部被跳過(guò)。我在項(xiàng)目里看到過(guò)這種事故加密Handler放在Pipeline比較靠后業(yè)務(wù)Handler調(diào)用channel.writeAndFlush結(jié)果所有數(shù)據(jù)都是明文發(fā)出去的正是這個(gè)原因。4. 實(shí)戰(zhàn)一個(gè)粘包拆包的Handler鏈設(shè)計(jì)4.1 粘包拆包問(wèn)題的根源TCP是流式協(xié)議沒(méi)有消息邊界。上層發(fā)的三條消息可能在底層被合并成一個(gè)包發(fā)出去粘包也可能一條消息被拆成多個(gè)小包分批到達(dá)拆包。比如客戶端連續(xù)調(diào)了三次write操作系統(tǒng)可能為了效率把三次數(shù)據(jù)一次flush出去接收方一次性就會(huì)讀到一個(gè)拼接后的ByteBuf反過(guò)來(lái)如果一條消息有4KB而TCP窗口只允許讀2KB接收方就要分兩次才能收完。這就是Netty用戶最常提到的“粘包處理”場(chǎng)景。解決粘包問(wèn)題的本質(zhì)是確定消息邊界。常見(jiàn)方案有四種固定長(zhǎng)度、分隔符、長(zhǎng)度字段、自定義協(xié)議頭。Netty里對(duì)應(yīng)的解碼器分別是FixedLengthFrameDecoder、LineBasedFrameDecoder、LengthFieldBasedFrameDecoder和自定義Decoder。4.2 用解碼器解決ByteToMessageDecoder與常見(jiàn)FrameDecoder核心類是ByteToMessageDecoder它是一個(gè)ChannelInboundHandlerAdapter的抽象子類專門用來(lái)把入站ByteBuf解碼成一個(gè)個(gè)業(yè)務(wù)消息對(duì)象。使用它時(shí)你只需要重寫(xiě)decode方法每調(diào)用一次傳入一個(gè)ByteBuf和一個(gè)Listout你把解析出的完整消息add到out里Netty會(huì)替你把out里的每個(gè)對(duì)象逐個(gè)在Pipeline上傳播出去。LengthFieldBasedFrameDecoder是最常用的通用解碼器。它根據(jù)消息頭里的長(zhǎng)度字段來(lái)確定完整幀長(zhǎng)度。假設(shè)協(xié)議格式是“2字節(jié)魔數(shù) 2字節(jié)長(zhǎng)度 N字節(jié)內(nèi)容”配置如下new LengthFieldBasedFrameDecoder( // 最大幀長(zhǎng)超過(guò)拋異常防止惡意包 1024, // 長(zhǎng)度字段偏移量前面有2字節(jié)魔數(shù) 2, // 長(zhǎng)度字段占用的字節(jié)數(shù) 2, // 長(zhǎng)度校正值比如長(zhǎng)度字段只統(tǒng)計(jì)內(nèi)容長(zhǎng)度則不需要調(diào)整 0, // 跳過(guò)前多少字節(jié)通常是剝掉頭部 0 )解碼器放在Pipeline最前面業(yè)務(wù)Handler放在后面粘包拆包問(wèn)題就基本解決了。需要注意解碼器是有狀態(tài)的它內(nèi)部要緩存上一次decode剩下的半包數(shù)據(jù)所以千萬(wàn)不要用Sharable注解標(biāo)注這個(gè)類更不要多個(gè)Channel復(fù)用同一個(gè)實(shí)例否則跨連接的狀態(tài)會(huì)互相污染。4.3 一個(gè)完整示例自定義Decoder 業(yè)務(wù)Handler如何連接下面是一個(gè)最簡(jiǎn)可跑的Demo級(jí)鏈路ServerBootstrap b new ServerBootstrap(); b.group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializerSocketChannel() { Override protected void initChannel(SocketChannel ch) { ChannelPipeline p ch.pipeline(); // 先拆包按自定義協(xié)議長(zhǎng)度字段拆幀 p.addLast(frameDecoder, new LengthFieldBasedFrameDecoder(4096, 2, 2, 0, 0)); // 再反序列化ByteBuf - 一個(gè)Request對(duì)象 p.addLast(msgDecoder, new ByteToMessageDecoder() { Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, ListObject out) { // 走到這里已經(jīng)是一個(gè)完整幀 byte[] bytes new byte[in.readableBytes()]; in.readBytes(bytes); out.add(new Request(bytes)); } }); // 業(yè)務(wù)處理 p.addLast(bizHandler, new SimpleChannelInboundHandlerRequest() { Override protected void channelRead0(ChannelHandlerContext ctx, Request msg) { // 這里收到的一定是完整的Request對(duì)象 ctx.writeAndFlush(handleBiz(msg)); } }); } });這里有個(gè)細(xì)節(jié)值得多說(shuō)一句第二個(gè)Decoder本質(zhì)上還是入站Handler它把ByteBuf變成Request對(duì)象后繼續(xù)調(diào)fireChannelRead最終進(jìn)入SimpleChannelInboundHandler。SimpleChannelInboundHandler好用的地方在于它自動(dòng)釋放非引用計(jì)數(shù)的消息資源如果你用手寫(xiě)ChannelInboundHandler一定記得主動(dòng)釋放ByteBuf或做引用計(jì)數(shù)管理否則時(shí)間一長(zhǎng)老是內(nèi)存泄漏。有一種情況更需要警惕就是自定義Decoder的循環(huán)粘包問(wèn)題。ByteToMessageDecoder的decode方法可能被調(diào)用多次如果一整批數(shù)據(jù)里包含兩個(gè)完整報(bào)文你需要在一次decode里把兩個(gè)報(bào)文都解析出來(lái)或者配合decodeLast。如果你的decode方法里new了一個(gè)對(duì)象就return第二幀就永遠(yuǎn)處理不到表現(xiàn)就是偶發(fā)丟消息但沒(méi)異常。我踩過(guò)這個(gè)坑排查時(shí)把Decode調(diào)用次數(shù)打了日志才發(fā)現(xiàn)。5. 使用ChannelHandler的若干血淚經(jīng)驗(yàn)5.1 Handler能不能被共享Sharable到底該怎么用ChannelHandler里有個(gè)Sharable注解加了它表示這個(gè)Handler實(shí)例可以被多個(gè)Channel共享。不加注解的Handler每個(gè)Channel都應(yīng)該有自己獨(dú)立的實(shí)例或者說(shuō)至少每個(gè)Channel要new一個(gè)否則狀態(tài)會(huì)串。最常見(jiàn)的反例有人為了省內(nèi)存把統(tǒng)計(jì)用的Handler直接add到所有Channel的Pipeline上標(biāo)簽類也沒(méi)有狀態(tài)用Sharable標(biāo)注后安全無(wú)害。但一旦Handler里有個(gè)計(jì)數(shù)器字段或者緩存了某個(gè)Connection的上下文共享實(shí)例就會(huì)讓不同連接互相污染這種Bug非常難查。我的建議是除非確認(rèn)Handler完全無(wú)狀態(tài)或者狀態(tài)本身就是全局共享的比如全局計(jì)數(shù)器、全局RateLimiter否則一律每個(gè)Channel新建實(shí)例。ChannelInitializer里每條連接都會(huì)執(zhí)行initChannel在它內(nèi)部new Handler最安全。不要圖省事在ServerBootstrap上加共享Handler除非你真的很清楚你在做什么。5.2 阻塞調(diào)用、ByteBuf釋放與引用計(jì)數(shù)Netty的EventLoop是單線程串行執(zhí)行事件一個(gè)Channel的Handler幾乎都在同一個(gè)線程里跑。如果你在Handler里做了阻塞操作比如調(diào)用數(shù)據(jù)庫(kù)同步查詢、RPC同步調(diào)用、Thread.sleep那么整個(gè)EventLoop都會(huì)被卡住。這個(gè)EventLoop上注冊(cè)的其他Channel全部停止處理相當(dāng)于一臺(tái)服務(wù)器因?yàn)橐粭l慢請(qǐng)求癱瘓了。正確的做法是把耗時(shí)操作提交到獨(dú)立的業(yè)務(wù)線程池處理完再通過(guò)Channel的EventLoop切回IO線程去寫(xiě)回。Netty官方推薦在Handler里用ctx.executor()或者提交給單獨(dú)的ExecutorService后用ctx.channel().eventLoop().execute()再切回來(lái)。ByteBuf釋放的問(wèn)題同樣隱蔽。ByteBuf是引用計(jì)數(shù)對(duì)象假如你在handler里new了一個(gè)ByteBuf或者接收了一個(gè)ByteBuf必須確認(rèn)它被release否則計(jì)數(shù)器歸不了零內(nèi)存池就泄漏。SimpleChannelInboundHandler會(huì)在channelRead0返回后自動(dòng)release msg而普通ChannelInboundHandler里的msg不會(huì)自動(dòng)釋放除非你調(diào)用ReferenceCountUtil.release(msg)或調(diào)ctx.fireChannelRead把釋放責(zé)任繼續(xù)傳遞下去。這里的核心原則是誰(shuí)最后消費(fèi)消息誰(shuí)負(fù)責(zé)釋放每new一個(gè)ByteBuf就要匹配一次release。5.3 異常傳播機(jī)制以及為什么不能亂catchexceptionCaught是入站異常事件它的傳播方向也是從頭到尾。如果在某個(gè)Handler里業(yè)務(wù)代碼拋異常且沒(méi)有被catchNetty會(huì)捕獲它并調(diào)用fireExceptionCaught異常事件開(kāi)始沿著Pipeline傳播。如果沒(méi)有Handler處理最終會(huì)到達(dá)TailContext被日志輸出。這里的關(guān)鍵是一旦你catch住某個(gè)異常卻沒(méi)有調(diào)用ctx.fireExceptionCaught這個(gè)異常就被吞掉了后續(xù)Handler完全不知情。有些場(chǎng)景你覺(jué)得“我已經(jīng)處理好了”但下游Handler可能需要感知這條消息處理失敗來(lái)做補(bǔ)償或統(tǒng)計(jì)。所以我建議自己無(wú)法覆蓋的異常一律繼續(xù)fire不要默默catch。異常Handler的擺放位置也有講究。如果想做全局兜底通常把異常兜底Handler加在Pipeline最前面最靠近Head解碼器的位置或者最后面最靠近Tail的位置。個(gè)人經(jīng)驗(yàn)是放在尾部做兜底日志和連接關(guān)閉在業(yè)務(wù)層只處理自己關(guān)心的局部異常。放在最前面能捕獲后面所有Handler的異常但因?yàn)楫惓挠|發(fā)點(diǎn)開(kāi)始傳播如果這個(gè)Handler放在業(yè)務(wù)Handler前面后續(xù)Handler都能收到異常這點(diǎn)很多新手容易搞反。5.4 調(diào)試技巧完整的日志鏈路與線程狀態(tài)輔助排查Netty問(wèn)題有一個(gè)性價(jià)比極高的做法在Pipeline首尾各掛一個(gè)日志Handler把所有入站出站事件的event類型、channelId、線程名打出來(lái)。這樣你一眼就能看出事件是否斷層、順序是否顛倒、write是否沒(méi)走到Encoder。我在調(diào)試某次協(xié)議兼容問(wèn)題時(shí)專門寫(xiě)了個(gè)DebugHandlerpublic class DebugHandler extends ChannelDuplexHandler { Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { logger.info([IN] {} has bytes {}, ctx.channel().id(), msg instanceof ByteBuf ? ((ByteBuf) msg).readableBytes() : msg.getClass()); super.channelRead(ctx, msg); } Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { logger.info([OUT] {} has bytes {}, ctx.channel().id(), msg instanceof ByteBuf ? ((ByteBuf) msg).readableBytes() : msg.getClass()); super.write(ctx, msg, promise); } }把這樣的Handler加在Pipeline最前面和業(yè)務(wù)Handler前面各一份基本能還原完整的事件鏈條。打日志的時(shí)候順手把當(dāng)前線程名打出來(lái)借助Netty內(nèi)部線程名如nioEventLoopGroup-x-y還能輔助確認(rèn)阻塞問(wèn)題是否導(dǎo)致同一個(gè)EventLoop線程被長(zhǎng)時(shí)間占用判斷是哪條連接拉低了整個(gè)線程池。另外一個(gè)建議是不要只靠斷點(diǎn)調(diào)試Netty。因?yàn)镋ventLoop線程里的斷點(diǎn)會(huì)阻塞整個(gè)IO線程影響并發(fā)連接的行為容易掩蓋問(wèn)題。依賴詳細(xì)日志分析效果通常好得多。Netty的ChannelHandler機(jī)制說(shuō)穿了其實(shí)不復(fù)雜一個(gè)雙向鏈表、兩個(gè)方向的事件流、每個(gè)節(jié)點(diǎn)自己決定怎么處理以及是否接力。但正是這個(gè)簡(jiǎn)單的模型撐起了大量高并發(fā)服務(wù)器的協(xié)議層邏輯。我自己折騰完那臺(tái)線上問(wèn)題機(jī)器之后最大的體會(huì)是凡是Handler行為不如預(yù)期先打印事件鏈條而不是先懷疑框架凡是內(nèi)存異常增長(zhǎng)先排查ByteBuf有沒(méi)有被正確釋放而不是先加堆內(nèi)存。把這些基本功練扎實(shí)了Netty項(xiàng)目里一大半的疑難雜癥都能被你用日志和順序推理直接揪出來(lái)。