Netty案例集錦之多線程篇(續)
原文 http://www.infoq.com/cn/articles/the-multithreading-of-netty-cases-part02
1. Netty構建推送服務問題
1.1. 問題描述
最近在使用Netty構建推送服務的過程中,遇到了一個問題,想再次請教您:如何正確的處理業務邏輯?問題主要來源于閱讀您發表在InfoQ上的文章《Netty系列之Netty線程模型》,文中提到 “2.4Netty線程開發最佳實踐中 2.4.2復雜和時間不可控業務建議投遞到后端業務線程池統一處理。對于此類業務,不建議直接在業務ChannelHandler中啟動線程或者線程池處理,建議將不同的業務統一封裝成Task,統一投遞到后端的業務線程池中進行處理。”
我不太理解“統一投遞到后端的業務線程池中進行處理”具體如何操作?像下面這樣做是否可行:
private ExecutorService executorService = Executors.newFixedThreadPool(4); @Override public void channelRead (final ChannelHandlerContext ctx, final Object msg) throws Exception { executorService.execute(new Runnable() {@Override public void run() { doSomething();
其實我想了解的是真實生產環境中如何將業務邏輯與Netty網絡處理部分很好的作隔離,有沒有通用的做法?
1.2. 答疑解惑
Netty的ChannelHandler鏈由I/O線程執行,如果在I/O線程做復雜的業務邏輯操作,可能會導致I/O線程無法及時進行read()或者write()操作。所以,比較通用的做法如下:
- 在ChannelHanlder的Codec中進行編解碼,由I/O線程做CodeC;
- 將數據報反序列化成業務Object對象之后,將業務消息封裝到Task中,投遞到業務線程池中進行處理,I/O線程返回。 </ul>
- 開發簡單:開發業務ChannelHandler的不需要關注Netty的線程模型,只負責ChannelHandler的業務邏輯開發和編排即可,對開發人員的技能要求會低一些;
- 性能更高:因為減少了一次線程上下文切換,所以性能會更高。 </ol>
- 通信端讀取數據報、消息解碼和內部消息投遞、隊列排隊的時間
- 通信端編碼業務消息、在通信線程隊列排隊時間、消息發送到Socket的時間 </ul>
- 包含客戶端和服務端消息編碼和解碼的耗時
- 包含請求和應答消息在業務線程池隊列中的排隊時間;
- 包含請求和應答消息在通信線程發送隊列(數組)中的排隊時間 </ul>
- 每條鏈路接收的總字節數、周期T接收的字節數、消息接收CAPs
- 每條鏈路發送的總字節數、周期T發送的字節數、消息發送CAPs </ul>
- 業務ChannelHandler的執行時間
- ByteBuf在ChannelOutboundBuffer 數組中排隊時間
- NioEventLoop線程調度時間,它不僅僅只處理消息發送,還負責數據報讀取、定時任務執行以及業務定制的其它I/O任務
- JDK NIO類庫將ByteBuffer寫入到網絡的時間,包括單條消息的多次寫半包 </ul>
- 調用writeAndFlush方法之后獲取ChannelFuture;
- 新增消息發送ChannelFutureListener,監聽消息發送結果,如果消息寫入網絡Socket成功,則Netty會回調ChannelFutureListener的operationComplete方法;
- 在消息發送ChannelFutureListener的operationComplete方法中進行性能統計。 </ol>
- 客戶端每連接1個服務端,就會創建1個新的NioEventLoopGroup,并設置它的線程數為1;
- 現網有300個+節點,節點之間采用多鏈路(10個鏈路),由于業務采用了隨機路由,最終每個消費者需要跟其它200多個節點建立長連接,加上自己服務端也需要占用一些NioEventLoop線程,最終客戶端單進程線程數膨脹到了3000多個。 </ul>
不建議的做法:
圖1-1 不推薦業務和I/O線程共用同一個線程
推薦做法:
圖1-2 建議業務線程和I/O線程隔離
1.3. 問題總結
事實上,并不是說業務ChannelHandler一定不能由NioEventLoop線程執行,如果業務ChannelHandler處理邏輯比較簡單,執行時間是受控的,業務I/O線程的負載也不重,在這種應用場景下,業務ChannelHandler可以和I/O操作共享同一個線程。使用這種線程模型會帶來兩個優勢:
在實際項目開發中,一些開發人員往往喜歡照葫蘆畫瓢,并不會分析自己的ChannelHandler更適合在哪種線程模型下處理。如果在ChannelHandler中進行數據庫等同步I/O操作,很有可能會導致通信模塊被阻塞。所以,選擇什么樣的線程模型還需要根據項目的具體情況而定,一種比較好的做法是支持策略配置,例如阿里的Dubbo,支持通過配置化的方式讓用戶選擇業務在I/O線程池還是業務線程池中執行,比較靈活。
2. Netty客戶端連接問題
2.1. 問題描述
Netty客戶端想同時連接多個服務端,使用如下方式,是否可行,我簡單測試了下,暫時沒有發現問題。代碼如下:
EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) ......代碼省略 // Start the client. ChannelFuture f1 = b.connect(HOST, PORT); ChannelFuture f2 = b.connect(HOST2, PORT2); // Wait until the connection is closed. f1.channel().closeFuture().sync(); f2.channel().closeFuture().sync(); ......代碼省略 }
2.2. 答疑解惑
上述代碼沒有問題,原因是盡管Bootstrap自身不是線程安全的,但是執行Bootstrap的連接操作是串行執行的,而且connect(String inetHost, int inetPort)方法本身是線程安全的,它會創建一個新的NioSocketChannel,并從初始構造的EventLoopGroup中選擇一個NioEventLoop線程執行真正的Channel連接操作,與執行Bootstrap的線程無關,所以通過一個Bootstrap連續發起多個連接操作是安全的,它的原理如下:
圖2-1 Netty BootStrap工作原理
2.3. 問題總結
注意事項-資源釋放問題: 在同一個Bootstrap中連續創建多個客戶端連接,需要注意的是EventLoopGroup是共享的,也就是說這些連接共用一個NIO線程組EventLoopGroup,當某個鏈路發生異常或者關閉時,只需要關閉并釋放Channel本身即可,不能同時銷毀Channel所使用的NioEventLoop和所在的線程組EventLoopGroup,例如下面的代碼片段就是錯誤的:
ChannelFuture f1 = b.connect(HOST, PORT); ChannelFuture f2 = b.connect(HOST2, PORT2); f1.channel().closeFuture().sync(); } finally { group.shutdownGracefully(); }
線程安全問題: 需要指出的是Bootstrap不是線程安全的,因此在多個線程中并發操作Bootstrap是一件非常危險的事情,Bootstrap是I/O操作工具類,它自身的邏輯處理非常簡單,真正的I/O操作都是由EventLoop線程負責的,所以通常多線程操作同一個Bootstrap實例也是沒有意義的,而且容易出錯,錯誤代碼如下:
Bootstrap b = new Bootstrap(); { //多線程執行初始化、連接等操作 }
3. 性能數據統計不準確案例
3.1.問題描述
某生產環境在業務高峰期,偶現服務調用時延突刺問題,時延突然增大的服務沒有固定規律,比例雖然很低,但是對客戶的體驗影響很大,需要盡快定位出問題原因并解決。
3.2.問題分析
服務調用時延增大,但并不是異常,因此運行日志并不會打印ERROR日志,單靠傳統的日志無法進行有效問題定位。利用分布式消息跟蹤系統魔鏡,進行分布式環境的故障定界。
通過對服務調用時延進行排序和過濾,找出時延增大的服務調用鏈詳細信息,發現業務服務端處理很快,但是消費者統計數據卻顯示服務端處理非常慢,調用鏈兩端看到的數據不一致,怎么回事?
對調用鏈的埋點日志進行分析發現,服務端打印的時延是業務服務接口調用的時延,并沒有包含:
調用鏈的工作原理如下:
圖3-1 調用鏈工作原理
將調用鏈中的消息調度過程詳細展開,以服務端讀取請求消息為例進行說明,如下圖所示:
圖3-2 性能統計日志埋點
優化調用鏈埋點日志,措施如下:
同時,為了方便問題定位,我們需要打印輸出Netty的性能統計日志,主要包括:
優化之后,上線運行一天之后,我們通過分析比對Netty性能統計日志、調用鏈日志,發現雙方的數據并不一致,Netty性能統計日志統計到的數據與前端門戶看到的也不一致,因為懷疑是新增的性能統計功能存在BUG,繼續問題定位。
首先對消息發送功能進行CodeReview,發現代碼調用完writeAndFlush之后直接對發送的請求消息字節數進行計數,代碼如下:
實際上,調用writeAndFlush并不意味著消息已經發送到網絡上,它的功能分解如下:
圖3-3 writeAndFlush 工作原理圖
通過對writeAndFlush方法展開分析,我們發現性能統計代碼存在如下幾個問題:
由于性能統計遺漏了上述4個步驟的執行時間,因此統計出來的性能比實際值更高,這會干擾我們的問題定位。
3.3.問題總結
其它常見性能統計誤區匯總:
1. 調用write 方法之后就開始統計發送速率,示例代碼如下:
2. 消息編碼時進行性能統計,示例代碼如下:
編碼之后,獲取out可讀的字節數,然后做累加。編碼完成,ByteBuf并沒有被加入到發送隊列(數組)中,因此在此時做性能統計仍然是不準的。
正確的做法:
示例代碼如下:
問題定位出來之后,按照正確的做法對Netty性能統計代碼進行了修正,上線之后,結合調用鏈日志,很快定位出了業務高峰期偶現的部分服務時延毛刺較大問題,優化業務線程池參數配置之后問題得到解決。
3.4.舉一反三
除了消息發送性能統計之外,Netty數據報的讀取、消息接收QPS性能統計也非常容易出錯,我們第一版性能統計代碼消息接收CAPs也不準確,大家知道為什么嗎?這個留作問題,供大家自己思考。
4. Netty線程數膨脹案例
4.1. 問題描述
分布式服務框架在進行現網問題定位時,Dump 線程堆棧之后發現Netty的NIO線程竟然有3000多個,大量的NIO線程占用了系統的句柄資源、內存資源、CPU資源等,引發了一些其它問題,需要盡快查明原因并解決線程過多問題。
4.2. 問題分析
在研發環境中模擬現網組網和業務場景,使用jmc工具進行問題定位,
使用飛行記錄器對系統運行狀況做快照,模擬示例圖如下所示:
圖4-1 使用jmc工具進行問題定位
獲取到黑匣子數據之后,可以對系統的各種重要指標做分析,包括系統數據、內存、GC數據、線程運行狀態和數據等,如下圖所示:
圖4-2 獲取系統資源占用詳細數據
通過對線程堆棧分析,我們發現Netty的NioEventLoop線程超過了3000個!
圖4-3 Netty線程占用超過3000個
對服務框架協議棧的Netty客戶端和服務端源碼進行CodeReview,發現了問題所在:
業務的偽代碼如下:
for(Link linkE : links) { EventLoopGroup group = new NioEventLoopGroup(1); Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) // 此處省略..... b.connect(linkE.localAddress, linkE.remoteAddress); }
如果客戶端對每個鏈路連接都創建一個新的NioEventLoopGroup,則每個鏈路就會占用1個獨立的NIO線程,最終淪為 1連接:1線程 這種同步阻塞模式線程模型。隨著集群組網規模的不斷擴大,這會帶來嚴重的線程膨脹問題,最終會發生句柄耗盡無法創建新的線程,或者棧內存溢出。
從另一個角度看,1個NIO線程只處理一條鏈路也體現不出非阻塞I/O的優勢。案例中的錯誤線程模型如下所示:
圖4-4 錯誤的客戶端連接線程使用方式
4.3. 案例總結
無論是服務端監聽多個端口,還是客戶端連接多個服務端,都需要注意必須要重用NIO線程,否則就會導致線程資源浪費,在大規模組網時還會存在句柄耗盡或者棧溢出等問題。
Netty官方Demo僅僅是個Sample,對用戶而言,必須理解Netty的線程模型,否則很容易按照官方Demo的做法,在外層套個For循環連接多個服務端,然后,悲劇就這樣發生了。
修正案例中的問題非常簡單,原理如下:
圖4-5 正確的客戶端連接線程模型
5. 作者簡介
李林鋒,2007年畢業于東北大學,2008年進入華為公司從事高性能通信軟件的設計和開發工作,有7年NIO設計和開發經驗,精通Netty、Mina等NIO框架和平臺中間件,現任華為軟件平臺架構部架構師,《Netty權威指南》作者。目前從事華為下一代中間件和PaaS平臺的架構設計工作。
聯系方式:新浪微博 Nettying 微信:Nettying 微信公眾號:Netty之家
對于Netty學習中遇到的問題,或者認為有價值的Netty或者NIO相關案例,可以通過上述幾種方式聯系我。
感謝郭蕾對本文的策劃和審校。
給InfoQ中文站投稿或者參與內容翻譯工作,請郵件至editors@cn.infoq.com。也歡迎大家通過新浪微博(@InfoQ,@丁曉昀),微信(微信號: InfoQChina )關注我們,并與我們的編輯和其他讀者朋友交流(歡迎加入InfoQ讀者交流群 (已滿),InfoQ讀者交流群(#2)
)。