
做Netty源碼分析繞不開(kāi)兩個(gè)最基礎(chǔ)也最容易被混為一談的操作客戶(hù)端的Connect服務(wù)端的Bind。我最初啃源碼時(shí)也有個(gè)錯(cuò)覺(jué)這兩個(gè)操作不都是“把channel往某個(gè)地址上一掛”么一個(gè)連遠(yuǎn)程一個(gè)綁本地好像只是參數(shù)不同。真把調(diào)用鏈一路追下去才發(fā)現(xiàn)從入口到NIO底層的興趣事件處理它們完完全全是兩套邏輯只是中段共用了一段注冊(cè)代碼導(dǎo)致表面看著很像。這篇Connect與Bind對(duì)比總結(jié)就把這兩條鏈路從入口到NIO事件循環(huán)徹底拆開(kāi)講順便把backlog、connectTimeoutMillis、OP_ACCEPT和OP_CONNECT這幾個(gè)大家經(jīng)常面試被問(wèn)、排查時(shí)又搞不清的細(xì)節(jié)一并說(shuō)透。無(wú)論你是在準(zhǔn)備N(xiāo)etty相關(guān)面試還是排查線(xiàn)上連接不上、端口占用這類(lèi)問(wèn)題這篇都能當(dāng)一份源碼級(jí)的速查手冊(cè)用。1. 先說(shuō)結(jié)論Connect與Bind招式像內(nèi)功完全兩套1.1 從一段最常見(jiàn)的使用代碼說(shuō)起先用最典型的兩段代碼把場(chǎng)景立住。服務(wù)端是這么寫(xiě)的ServerBootstrap serverBootstrap new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 1024) .childHandler(new ChannelInitializerSocketChannel() { Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new ServerHandler()); } }); serverBootstrap.bind(8080).sync();客戶(hù)端是這么寫(xiě)的Bootstrap bootstrap new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializerSocketChannel() { Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new ClientHandler()); } }); bootstrap.connect(127.0.0.1, 8080).sync();從使用者的角度兩個(gè)操作都返回ChannelFuture都可以.sync()阻塞等待失敗都能拿到.cause()寫(xiě)起來(lái)體感高度一致。但它們背后的channel類(lèi)型都不同一個(gè)是NioServerSocketChannel一個(gè)是NioSocketChannel這已經(jīng)暗示了后續(xù)走向完全不同。接下來(lái)從入口開(kāi)始一追到底。1.2 兩條鏈路的“形似”與“神離”我把兩條調(diào)用鏈從入口到最終落到底層NIO的路徑壓縮成一句話(huà)bind鏈路ServerBootstrap.bind()→AbstractBootstrap.doBind()→initAndRegister()→doBind0()→channel.bind()→pipeline.fireChannelBind()→NioServerSocketChannel.doBind()→ServerSocketChannel.bind(port, backlog)connect鏈路Bootstrap.connect()→doResolveAndConnect()→initAndRegister()→channel.connect()→pipeline.fireChannelConnect()→NioSocketChannel.doConnect()→SocketChannel.connect(remoteAddress)兩條鏈都在initAndRegister()這里匯合完成channel創(chuàng)建和注冊(cè)到NioEventLoop的selector之后立刻分道揚(yáng)鑣。后面會(huì)看到真正的格局差異在NioEventLoop處理就緒事件時(shí)才暴露出來(lái)bind之后注冊(cè)O(shè)P_ACCEPT等新連接connect之后先注冊(cè)O(shè)P_CONNECT等連接完成完成之后再轉(zhuǎn)向OP_READ讀寫(xiě)數(shù)據(jù)。1.3 這篇總結(jié)適合誰(shuí)讀完能帶走什么這篇總結(jié)適合這么幾類(lèi)人正準(zhǔn)備N(xiāo)etty源碼面試需要把啟動(dòng)流程講清楚的人線(xiàn)上遇到“偶爾連不上”、“大量TIME_WAIT”、“端口明明沒(méi)占用卻bind失敗”這類(lèi)問(wèn)題想從原理層面找切入點(diǎn)的開(kāi)發(fā)以及剛開(kāi)始看Netty源碼被doBind0、initAndRegister這些方法名繞暈的自學(xué)者。讀完你至少能帶走四樣?xùn)|西第一Connect和Bind在傳播路徑上的真正分水嶺第二OP_ACCEPT與OP_CONNECT在NioEventLoop里是怎么被區(qū)別處理的第三backlog、connectTimeoutMillis這些參數(shù)到底作用在哪一層第四遇到bind失敗、connect超時(shí)這些常見(jiàn)錯(cuò)誤時(shí)一套可復(fù)用的排查思路。2. 入口分叉Bootstrap.connect與ServerBootstrap.bind在AbstractBootstrap里怎么走2.1 bind()的調(diào)用鏈從validate到doBind0ServerBootstrap本身沒(méi)有重寫(xiě)bind方法它直接繼承了AbstractBootstrap的bind(SocketAddress localAddress)。我們跟進(jìn)去看核心的doBindprivate ChannelFuture doBind(final SocketAddress localAddress) { final ChannelFuture regFuture initAndRegister(); final Channel channel regFuture.channel(); if (regFuture.cause() ! null) { return regFuture; } if (regFuture.isDone()) { ChannelPromise promise channel.newPromise(); doBind0(regFuture, channel, localAddress, promise); return promise; } else { final PendingRegistrationPromise promise new PendingRegistrationPromise(channel); regFuture.addListener(new ChannelFutureListener() { Override public void operationComplete(ChannelFuture future) throws Exception { Throwable cause future.cause(); if (cause ! null) { promise.setFailure(cause); } else { promise.registered(); doBind0(regFuture, channel, localAddress, promise); } } }); return promise; } }這段代碼的關(guān)鍵邏輯在于initAndRegister()是異步的channel注冊(cè)到EventLoop上不一定已經(jīng)完成所以這里分兩種情況處理——注冊(cè)已完成就直接doBind0未完成就掛一個(gè)監(jiān)聽(tīng)器等注冊(cè)完成后再執(zhí)行doBind0。這是Netty異步編程的一個(gè)典型設(shè)計(jì)后續(xù)動(dòng)作不一定立刻執(zhí)行但它一定會(huì)在正確的時(shí)間點(diǎn)被觸發(fā)。繼續(xù)看doBind0這里有個(gè)值得一提的小細(xì)節(jié)private static void doBind0( final ChannelFuture regFuture, final Channel channel, final SocketAddress localAddress, final ChannelPromise promise) { channel.eventLoop().execute(new Runnable() { Override public void run() { if (regFuture.isSuccess()) { channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); } else { promise.setFailure(regFuture.cause()); } } }); }注意兩個(gè)點(diǎn)。第一channel.bind()被放進(jìn)了eventLoop().execute()里執(zhí)行保證了channel操作一定在它所屬的IO線(xiàn)程上串行發(fā)生從根源上避免了多線(xiàn)程并發(fā)寫(xiě)channel的問(wèn)題。第二這里加了一個(gè)CLOSE_ON_FAILURE監(jiān)聽(tīng)器bind一旦失敗channel會(huì)被自動(dòng)關(guān)閉這是個(gè)很容易被忽略的兜底機(jī)制——所以線(xiàn)上bind失敗后即使你不手動(dòng)close這個(gè)channel也不會(huì)泄漏。2.2 connect()的調(diào)用鏈doResolveAndConnect先把地址解析交給線(xiàn)程池Bootstrap重寫(xiě)了connect但真正的實(shí)現(xiàn)不在connect里而是轉(zhuǎn)到了doResolveAndConnectprivate ChannelFuture doResolveAndConnect( final SocketAddress remoteAddress, final SocketAddress localAddress) { final ChannelFuture regFuture initAndRegister(); final Channel channel regFuture.channel(); if (regFuture.isDone()) { if (!regFuture.isSuccess()) { return regFuture; } return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise()); } else { final PendingRegistrationPromise promise new PendingRegistrationPromise(channel); regFuture.addListener(new ChannelFutureListener() { Override public void operationComplete(ChannelFuture future) throws Exception { Throwable cause future.cause(); if (cause ! null) { promise.setFailure(cause); } else { promise.registered(); doResolveAndConnect0(channel, remoteAddress, localAddress, promise); } } }); return promise; } }結(jié)構(gòu)與doBind幾乎一致也都是等注冊(cè)完成后再發(fā)起真正的連接動(dòng)作。接下來(lái)看doResolveAndConnect0這里就出現(xiàn)了和bind的第一個(gè)顯著差異private ChannelFuture doResolveAndConnect0( final Channel channel, SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) { try { EventLoop eventLoop channel.eventLoop(); AddressResolverSocketAddress resolver this.resolver.getResolver(eventLoop); if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) { channel.connect(remoteAddress, localAddress, promise); return promise; } FutureSocketAddress resolveFuture resolver.resolve(remoteAddress); ... } }如果遠(yuǎn)程目標(biāo)是域名而不是IPNetty會(huì)先用AddressResolver異步解析DNS解析完成后再真正發(fā)起channel.connect()。DNS解析默認(rèn)走的是DefaultAddressResolverGroup底層用Netty自己封裝的一套異步DNS客戶(hù)端實(shí)現(xiàn)。2.3 為什么connect要異步解析地址bind卻不用這個(gè)問(wèn)題我當(dāng)初也想過(guò)bind拿到的是本地地址本機(jī)網(wǎng)卡信息是確定性的不存在“需要查DNS”的場(chǎng)景所以bind鏈路不需要resolver這一層。connect則完全相反你寫(xiě)connect(some-service.example.com, 8080)時(shí)底層需要先把域名解析成IP而DNS查詢(xún)?cè)诰W(wǎng)絡(luò)環(huán)境里可能耗時(shí)幾十毫秒甚至數(shù)秒如果阻塞在IO線(xiàn)程上整個(gè)EventLoop上的其他channel都會(huì)被拖累。Netty把這一步也異步化了讓DNS解析發(fā)生在獨(dú)立的Resolver線(xiàn)程組里解析完成后由回調(diào)把結(jié)果帶回EventLoop再執(zhí)行真正的connect。這一點(diǎn)對(duì)比能幫我們?cè)谠创a層面理解一個(gè)宏觀(guān)設(shè)計(jì)原則任何可能阻塞IO線(xiàn)程的操作Netty都會(huì)想方設(shè)法移出去或者以異步回調(diào)的方式回來(lái)。bind是純本地系統(tǒng)調(diào)用最快路徑就是直接在EventLoop里同步執(zhí)行connect涉及域名解析就要額外走一層。3. 源碼逐行看bind與connect在Channel層真正做了什么3.1 bind從pipeline一路摸到NioServerSocketChannel.doBindchannel.bind(localAddress, promise)會(huì)從DefaultChannelPipeline進(jìn)入。Netty的pipeline傳播規(guī)則是出站事件從Tail開(kāi)始往前找直到某個(gè)ChannelOutboundHandler處理它入站事件從Head開(kāi)始往后傳播。bind是典型的出站事件所以它會(huì)倒著走pipelineTailContext - 用戶(hù)OutboundHandler - ... - HeadContextHeadContext本身就實(shí)現(xiàn)了ChannelOutboundHandler它的bind方法最終調(diào)用了unsafe.bind。unsafe是Netty對(duì)JDK底層channel操作的封裝層全名叫Channel.Unsafe名字聽(tīng)著危險(xiǎn)其實(shí)是完成真正臟活累活的地方。再往下就到了AbstractChannel.bindOverride public final void bind(final SocketAddress localAddress, final ChannelPromise promise) { ... boolean wasActive isActive(); doBind(localAddress); if (!wasActive isActive()) { pipeline.fireChannelActive(); } promise.setSuccess(); }這里有兩個(gè)關(guān)鍵判斷bind之前channel處于inactive狀態(tài)bind成功之后channel變成active于是觸發(fā)fireChannelActive()。這個(gè)事件很重要——服務(wù)端會(huì)在doBeginRead里根據(jù)isActive狀態(tài)注冊(cè)O(shè)P_ACCEPT。真正落到NIO層的是NioServerSocketChannel.doBindOverride protected void doBind(SocketAddress localAddress) throws Exception { if (PlatformDependent.javaVersion() 7) { javaChannel().bind(localAddress, config.getBacklog()); } else { javaChannel().socket().bind(localAddress, config.getBacklog()); } active true; }這里直接調(diào)用了JDK NIO的ServerSocketChannel.bind(localAddress, backlog)。注意第二個(gè)參數(shù)config.getBacklog()這個(gè)值就是從ChannelOption.SO_BACKLOG讀出來(lái)的傳給操作系統(tǒng)控制連接隊(duì)列深度的關(guān)鍵參數(shù)。如果這個(gè)端口已經(jīng)被別的進(jìn)程占用這一行會(huì)拋出SocketException(Address already in use)一路向上傳播最終體現(xiàn)在你bind().sync()拿到的異常里。3.2 connect從pipeline一路摸到NioSocketChannel.doConnectconnect同樣是出站事件同樣從Tail倒著走到HeadContext.connect然后進(jìn)入unsafe.connect最終到達(dá)NioSocketChannel.doConnectOverride protected boolean doConnect( SocketAddress remoteAddress, SocketAddress localAddress) throws Exception { if (localAddress ! null) { javaChannel().socket().bind(localAddress); } boolean success false; try { boolean connected javaChannel().connect(remoteAddress); if (!connected) { selectionKey().interestOps(SelectionKey.OP_CONNECT); } success true; return connected; } finally { if (!success) { doClose(); } } }這段代碼是理解非阻塞connect的關(guān)鍵。JDK NIO的SocketChannel.connect在非阻塞模式下并不會(huì)阻塞到連接建立完成而是立即返回一個(gè)布爾值如果返回true說(shuō)明連接已經(jīng)立刻建立比如連本機(jī)回環(huán)地址如果返回false說(shuō)明連接還在進(jìn)行中。此時(shí)Netty立刻把興趣事件設(shè)置為OP_CONNECT把“等待連接完成”這件事交給了Selector——相當(dāng)于告訴操作系統(tǒng)這個(gè)socket正在連接中等它連好了你通知我。finally塊里的doClose()是一個(gè)很?chē)?yán)謹(jǐn)?shù)亩档兹绻谠O(shè)置興趣事件之前出了任何異常說(shuō)明連接沒(méi)正常發(fā)起必須關(guān)閉channel避免半初始化狀態(tài)的socket泄漏。當(dāng)操作系統(tǒng)通知連接完成時(shí)NioSocketChannel.doFinishConnect會(huì)被調(diào)用Override protected void doFinishConnect() throws Exception { if (!javaChannel().finishConnect()) { throw new ConnectException(finishConnect() returned false); } }finishConnect()會(huì)確認(rèn)連接狀態(tài)如果連接已經(jīng)建立返回true這條鏈路就通了如果連接失敗比如對(duì)端拒絕這里會(huì)直接拋出ConnectException——這就是你在客戶(hù)端sync()時(shí)拿到Connection refused異常的最底層來(lái)源。3.3 NioEventLoop里OP_ACCEPT與OP_CONNECT的分叉處理服務(wù)端和客戶(hù)端的命運(yùn)在NioEventLoop的processSelectedKey里真正分道揚(yáng)鑣??催@段核心代碼if ((readyOps (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) ! 0 || readyOps 0) { unsafe.read(); } if ((readyOps SelectionKey.OP_WRITE) ! 0) { ch.unsafe().forceFlush(); } if ((readyOps SelectionKey.OP_CONNECT) ! 0) { int ops k.interestOps(); ops ~SelectionKey.OP_CONNECT; k.interestOps(ops); unsafe.finishConnect(); }OP_ACCEPT被歸到和OP_READ同一個(gè)分支走的是unsafe.read()。但這時(shí)的“讀”不是讀數(shù)據(jù)而是讀新連接。NioMessageUnsafe.read()內(nèi)部會(huì)調(diào)用doReadMessages把已經(jīng)完成三次握手的連接逐個(gè)accept()出來(lái)封裝成NioSocketChannel然后觸發(fā)pipeline.fireChannelRead。服務(wù)端的pipeline里有一個(gè)關(guān)鍵handler叫ServerBootstrapAcceptor它會(huì)在channelRead里把新accept出來(lái)的channel注冊(cè)到workerGroup上完成從boss線(xiàn)程到worker線(xiàn)程的交接。OP_CONNECT則是獨(dú)立的第三個(gè)分支處理邏輯更簡(jiǎn)單先清理掉OP_CONNECT興趣位然后調(diào)用finishConnect確認(rèn)連接結(jié)果。確認(rèn)成功之后客戶(hù)端channel進(jìn)入active狀態(tài)觸發(fā)fireChannelActive隨后開(kāi)始注冊(cè)O(shè)P_READ進(jìn)入正常的讀寫(xiě)工作狀態(tài)。這里可以梳理成一張對(duì)比表對(duì)比維度Bind鏈路Connect鏈路入口方法ServerBootstrap.bindBootstrap.connect監(jiān)聽(tīng)事件OP_ACCEPT等待新連接OP_CONNECT先等連接完成就緒事件分支與OP_READ同分支走unsafe.read獨(dú)立分支走unsafe.finishConnect完成后動(dòng)作accept出NioSocketChannel交給workerchannelActive后轉(zhuǎn)OP_READ底層NIO操作ServerSocketChannel.bindSocketChannel.connect/finishConnect失敗典型異常Address already in useConnection refused / connect timed out4. 參數(shù)、超時(shí)與狀態(tài)機(jī)三個(gè)容易忽略但決定成敗的細(xì)節(jié)4.1 backlog如何影響bind本地地址與臨時(shí)端口問(wèn)題backlog這個(gè)參數(shù)在面試?yán)锍霈F(xiàn)頻率很高但很多人只知道“設(shè)大一點(diǎn)可以提高并發(fā)”說(shuō)不清它到底管什么。在TCP協(xié)議里服務(wù)端socket的listen隊(duì)列由兩部分組成未完成三次握手的半連接隊(duì)列SYN Queue和已完成握手的全連接隊(duì)列Accept Queue。backlog主要控制全連接隊(duì)列的長(zhǎng)度。當(dāng)并發(fā)連接瞬間涌入accept處理速度跟不上連接建立速度時(shí)隊(duì)列就是緩沖區(qū)。隊(duì)列滿(mǎn)了新的連接請(qǐng)求會(huì)被內(nèi)核直接丟棄或拒絕。Netty的NioServerSocketChannelConfig在設(shè)置默認(rèn)backlog時(shí)有個(gè)細(xì)節(jié)優(yōu)先讀操作系統(tǒng)的net.core.somaxconn配置讀不到才用默認(rèn)值1024。也就是說(shuō)你ServerBootstrap里不顯式設(shè)置SO_BACKLOG時(shí)實(shí)際生效值不一定是你以為的默認(rèn)值。我在實(shí)測(cè)中遇到過(guò)sysctl net.core.somaxconn為128的服務(wù)器不設(shè)置SO_BACKLOG時(shí)并發(fā)一大就出現(xiàn)大量連接被重置最后顯式設(shè)置SO_BACKLOG為1024才解決——這種問(wèn)題從現(xiàn)象上很難想到根因在這里。再提一個(gè)connect鏈路上的端口細(xì)節(jié)connect方法還有一個(gè)重載可以傳localAddress但絕大多數(shù)場(chǎng)景不需要。如果你不指定操作系統(tǒng)會(huì)從臨時(shí)端口范圍內(nèi)自動(dòng)選擇一個(gè)作為客戶(hù)端端口如果指定了底層NioSocketChannel.doConnect里會(huì)先對(duì)本地地址做一次bind。指定本地端口時(shí)要注意端口沖突概率很高尤其是當(dāng)你部署多個(gè)客戶(hù)端實(shí)例在同一臺(tái)機(jī)器上時(shí)指定固定本地端口容易遇到Address already in use。4.2 connectTimeoutMillis背后的定時(shí)任務(wù)邏輯ChannelOption.CONNECT_TIMEOUT_MILLIS是客戶(hù)端連接超時(shí)配置默認(rèn)值是30秒。這個(gè)超時(shí)時(shí)間的實(shí)現(xiàn)也值得講一下它不是在doConnect里同步等待而是通過(guò)EventLoop的schedule注冊(cè)了一個(gè)延遲任務(wù)在指定時(shí)間后檢查連接是否還在進(jìn)行中如果是則關(guān)閉channel并讓promise失敗。有一個(gè)容易被誤解的點(diǎn)這個(gè)超時(shí)是從調(diào)用connect到連接事件就緒的總超時(shí)。如果系統(tǒng)內(nèi)核在更早的時(shí)候返回了ECONNREFUSED那你拿到的會(huì)是ConnectException而不是超時(shí)異常兩者產(chǎn)生原因完全不同排查方向也不同。我在實(shí)際調(diào)線(xiàn)上接口時(shí)發(fā)現(xiàn)很多“莫名其妙連接超時(shí)”其實(shí)是因?yàn)槟繕?biāo)端口被防火墻默默丟棄了請(qǐng)求包內(nèi)核收不到任何響應(yīng)才硬生生等到超時(shí)。這種情況從源碼角度很好理解connect發(fā)出了但沒(méi)有任何ACK或RST回來(lái)NIO事件一直不觸發(fā)只能靠定時(shí)任務(wù)兜底。4.3 從狀態(tài)機(jī)再看兩件事的區(qū)別Netty的channel狀態(tài)遷移是理解兩個(gè)操作底層差異的一個(gè)很好的視角。在bind鏈路上NioServerSocketChannel創(chuàng)建時(shí)是inactivebind成功后變active于是注冊(cè)O(shè)P_ACCEPT之后一直穩(wěn)定在“接受新連接”的狀態(tài)不再有大的狀態(tài)遷移。在connect鏈路上NioSocketChannel創(chuàng)建時(shí)同樣是inactiveconnect發(fā)起后進(jìn)入“連接中”的中間態(tài)finishConnect成功后才active然后注冊(cè)O(shè)P_READ開(kāi)始讀寫(xiě)。這里藏著一個(gè)經(jīng)常被忽視的點(diǎn)客戶(hù)端channel在register階段并不會(huì)注冊(cè)任何IO興趣事件。它得等connect完成之后通過(guò)fireChannelActive事件觸發(fā)doBeginRead才注冊(cè)O(shè)P_READ。如果你在handlerAdded里就嘗試讀數(shù)據(jù)會(huì)發(fā)現(xiàn)讀不到任何東西——不是沒(méi)數(shù)據(jù)而是事件還沒(méi)注冊(cè)上。這個(gè)時(shí)序問(wèn)題導(dǎo)致很多新手在寫(xiě)客戶(hù)端ChannelInitializer時(shí)把“連接建立后主動(dòng)發(fā)請(qǐng)求”的邏輯放錯(cuò)了地方正確做法是放到channelActive回調(diào)里。5. 實(shí)戰(zhàn)排查遇到連接類(lèi)問(wèn)題怎么用源碼思維定位5.1 先分清是bind還是connect階段出錯(cuò)排查連接問(wèn)題第一步永遠(yuǎn)是定位錯(cuò)誤發(fā)生在bind階段還是connect階段這兩個(gè)階段的異常類(lèi)完全不一樣但現(xiàn)象可能很接近。bind階段最常見(jiàn)的錯(cuò)誤是Address already in use也就是端口占用。但要注意這個(gè)異常不一定只出現(xiàn)在bind那一刻有些時(shí)候出現(xiàn)在ServerBootstrapAcceptor處理新連接時(shí)——如果你把childGroup的線(xiàn)程數(shù)配得太小新連接accept出來(lái)之后沒(méi)法及時(shí)注冊(cè)到worker也可能出現(xiàn)資源相關(guān)的異常但這就不是bind本身的問(wèn)題了。判斷方法很簡(jiǎn)單服務(wù)端啟動(dòng)時(shí)bind().sync()拋出的異?;揪褪莃ind階段問(wèn)題啟動(dòng)成功后運(yùn)行期間報(bào)的異?;径家鵤ccept和worker線(xiàn)程方向去查。connect階段的錯(cuò)誤類(lèi)型更豐富異常或現(xiàn)象可能原因排查方向Connection refused目標(biāo)端口未監(jiān)聽(tīng)或連接被RSTss -lnt看端口狀態(tài)Connection timed out防火墻丟包、目標(biāo)IP不可達(dá)檢查路由、防火墻策略、安全組Address already in use客戶(hù)端本地端口被占用檢查是否顯式指定了localAddress大量TIME_WAIT導(dǎo)致端口耗盡短連接過(guò)多連接未復(fù)用考慮連接池、調(diào)整端口范圍No buffer space available本地端口或文件句柄耗盡sysctl查看端口范圍、ulimit5.2 常見(jiàn)連接失敗問(wèn)題速查表結(jié)合我自己踩過(guò)的坑整理一份更貼近現(xiàn)場(chǎng)的速查表服務(wù)端端口看著沒(méi)被占用但bind一直失敗先查netstat -tlnp確認(rèn)是TCP端口而不是UDP端口占用沖突再查是否設(shè)置了SO_REUSEADDR。Linux下端口處于TIME_WAIT狀態(tài)時(shí)如果沒(méi)開(kāi)SO_REUSEADDRbind同樣會(huì)失敗Netty默認(rèn)SO_REUSEADDR是false所以高并發(fā)短連接場(chǎng)景下服務(wù)端重啟很容易遇到這個(gè)問(wèn)題建議在option里顯式打開(kāi)??蛻?hù)端連接拒絕但服務(wù)端確實(shí)在監(jiān)聽(tīng)確認(rèn)你連的不是回環(huán)地址。如果服務(wù)端監(jiān)聽(tīng)在127.0.0.1你拿內(nèi)網(wǎng)IP去連是連不上的反過(guò)來(lái)監(jiān)聽(tīng)在0.0.0.0或具體內(nèi)網(wǎng)IP用127.0.0.1連也可能失敗取決于防火墻規(guī)則。連接超時(shí)但三五秒后又恢復(fù)先懷疑半連接隊(duì)列溢出。看ss -s里SynCookies相關(guān)的統(tǒng)計(jì)或netstat -s里的listen queue overflow計(jì)數(shù)如果一直在增長(zhǎng)說(shuō)明backlog不夠或者accept處理不過(guò)來(lái)。connect失敗但服務(wù)端沒(méi)收到任何信息基本可以確定流量根本沒(méi)到服務(wù)端從防火墻、安全組、網(wǎng)絡(luò)策略方向排查不要繼續(xù)在應(yīng)用代碼里浪費(fèi)時(shí)間。5.3 我常用的斷點(diǎn)調(diào)試與日志技巧源碼分析不能只看不調(diào)我自己的經(jīng)驗(yàn)是準(zhǔn)備一個(gè)最小復(fù)現(xiàn)工程一個(gè)只打印channelActive的客戶(hù)端ChannelInitializer一個(gè)帶LoggingHandler的服務(wù)端ChannelInitializer。然后打兩組斷點(diǎn)——第一組在NioSocketChannel.doConnect和NioServerSocketChannel.doBind確認(rèn)底層NIO調(diào)用參數(shù)第二組在NioEventLoop.processSelectedKey確認(rèn)事件就緒后走的是哪個(gè)分支。日志方面Netty自帶的LoggingHandler能打印出pipeline上每個(gè)事件的傳播過(guò)程包括REGISTERED、ACTIVE、READ等這些日志可以把時(shí)序問(wèn)題可視化比你在業(yè)務(wù)handler里打日志全面得多。我排查一個(gè)客戶(hù)端偶發(fā)連接失敗問(wèn)題時(shí)就是靠LoggingHandler發(fā)現(xiàn)channelActive和channelRead之間出現(xiàn)了接近兩秒的間隔才定位到DNS解析耗時(shí)的根因——不是連接慢是域名解析慢。connectTimeoutMillis這個(gè)參數(shù)也要在測(cè)試環(huán)境里刻意壓出超時(shí)場(chǎng)景來(lái)驗(yàn)證行為把超時(shí)設(shè)為1毫秒連一個(gè)不存在且被防火墻靜默丟棄的IP觀(guān)察channel的關(guān)閉時(shí)機(jī)和異常類(lèi)型。這樣你能準(zhǔn)確區(qū)分“內(nèi)核立即告訴我們連不上”和“等超時(shí)任務(wù)把我們踢下線(xiàn)”兩種模式線(xiàn)上遇到問(wèn)題時(shí)的第一反應(yīng)就會(huì)完全不一樣。6. 一點(diǎn)個(gè)人體會(huì)源碼拆到這一步我自己最大的收獲不是記住了哪幾個(gè)方法名而是形成了一個(gè)判斷網(wǎng)絡(luò)問(wèn)題的“分層反射”先看錯(cuò)誤發(fā)生在bind還是connect再看是內(nèi)核直接反饋還是靠超時(shí)兜底最后才鉆進(jìn)業(yè)務(wù)代碼找原因。Netty把Connect和Bind設(shè)計(jì)成這樣兩張各有節(jié)奏的流程圖本質(zhì)上是為了在同一套非阻塞模型下同時(shí)安頓好“等待連接建立”和“等待新連接到來(lái)”這兩種完全不同性質(zhì)的等待。理解了這一層再看Netty源碼分析里其他和連接生命線(xiàn)相關(guān)的代碼比如重連、斷線(xiàn)檢測(cè)、優(yōu)雅停機(jī)都會(huì)順暢很多。最后再分享一個(gè)小技巧把這兩條鏈路的源碼各讀三遍之后試著關(guān)掉IDE自己在紙上畫(huà)出從Bootstrap.connect到內(nèi)核connect()的每一跳再畫(huà)出從ServerBootstrap.bind到OP_ACCEPT就緒的每一跳。畫(huà)得出來(lái)的這部分源碼你就真的吃透了。