Netty案例集錦之多線程篇(續)

jopen 9年前發布 | 32K 次閱讀 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>

    不建議的做法:

    Netty案例集錦之多線程篇(續)

    圖1-1 不推薦業務和I/O線程共用同一個線程

    推薦做法:

    Netty案例集錦之多線程篇(續)

    圖1-2 建議業務線程和I/O線程隔離

    1.3. 問題總結

    事實上,并不是說業務ChannelHandler一定不能由NioEventLoop線程執行,如果業務ChannelHandler處理邏輯比較簡單,執行時間是受控的,業務I/O線程的負載也不重,在這種應用場景下,業務ChannelHandler可以和I/O操作共享同一個線程。使用這種線程模型會帶來兩個優勢:

    1. 開發簡單:開發業務ChannelHandler的不需要關注Netty的線程模型,只負責ChannelHandler的業務邏輯開發和編排即可,對開發人員的技能要求會低一些;
    2. 性能更高:因為減少了一次線程上下文切換,所以性能會更高。
    3. </ol>

      在實際項目開發中,一些開發人員往往喜歡照葫蘆畫瓢,并不會分析自己的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連續發起多個連接操作是安全的,它的原理如下:

      Netty案例集錦之多線程篇(續)

      圖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日志,單靠傳統的日志無法進行有效問題定位。利用分布式消息跟蹤系統魔鏡,進行分布式環境的故障定界。

      通過對服務調用時延進行排序和過濾,找出時延增大的服務調用鏈詳細信息,發現業務服務端處理很快,但是消費者統計數據卻顯示服務端處理非常慢,調用鏈兩端看到的數據不一致,怎么回事?

      對調用鏈的埋點日志進行分析發現,服務端打印的時延是業務服務接口調用的時延,并沒有包含:

      • 通信端讀取數據報、消息解碼和內部消息投遞、隊列排隊的時間
      • 通信端編碼業務消息、在通信線程隊列排隊時間、消息發送到Socket的時間
      • </ul>

        調用鏈的工作原理如下:

        Netty案例集錦之多線程篇(續)

        圖3-1 調用鏈工作原理

        將調用鏈中的消息調度過程詳細展開,以服務端讀取請求消息為例進行說明,如下圖所示:

        Netty案例集錦之多線程篇(續)

        圖3-2 性能統計日志埋點

        優化調用鏈埋點日志,措施如下:

        • 包含客戶端和服務端消息編碼和解碼的耗時
        • 包含請求和應答消息在業務線程池隊列中的排隊時間;
        • 包含請求和應答消息在通信線程發送隊列(數組)中的排隊時間
        • </ul>

          同時,為了方便問題定位,我們需要打印輸出Netty的性能統計日志,主要包括:

          • 每條鏈路接收的總字節數、周期T接收的字節數、消息接收CAPs
          • 每條鏈路發送的總字節數、周期T發送的字節數、消息發送CAPs
          • </ul>

            優化之后,上線運行一天之后,我們通過分析比對Netty性能統計日志、調用鏈日志,發現雙方的數據并不一致,Netty性能統計日志統計到的數據與前端門戶看到的也不一致,因為懷疑是新增的性能統計功能存在BUG,繼續問題定位。

            首先對消息發送功能進行CodeReview,發現代碼調用完writeAndFlush之后直接對發送的請求消息字節數進行計數,代碼如下:

            Netty案例集錦之多線程篇(續)

            實際上,調用writeAndFlush并不意味著消息已經發送到網絡上,它的功能分解如下:

            Netty案例集錦之多線程篇(續)

            圖3-3 writeAndFlush 工作原理圖

            通過對writeAndFlush方法展開分析,我們發現性能統計代碼存在如下幾個問題:

            • 業務ChannelHandler的執行時間
            • ByteBuf在ChannelOutboundBuffer 數組中排隊時間
            • NioEventLoop線程調度時間,它不僅僅只處理消息發送,還負責數據報讀取、定時任務執行以及業務定制的其它I/O任務
            • JDK NIO類庫將ByteBuffer寫入到網絡的時間,包括單條消息的多次寫半包
            • </ul>

              由于性能統計遺漏了上述4個步驟的執行時間,因此統計出來的性能比實際值更高,這會干擾我們的問題定位。

              3.3.問題總結

              其它常見性能統計誤區匯總:

              1. 調用write 方法之后就開始統計發送速率,示例代碼如下:

              Netty案例集錦之多線程篇(續)

              2. 消息編碼時進行性能統計,示例代碼如下:

              Netty案例集錦之多線程篇(續)

              編碼之后,獲取out可讀的字節數,然后做累加。編碼完成,ByteBuf并沒有被加入到發送隊列(數組)中,因此在此時做性能統計仍然是不準的。

              正確的做法:

              1. 調用writeAndFlush方法之后獲取ChannelFuture;
              2. 新增消息發送ChannelFutureListener,監聽消息發送結果,如果消息寫入網絡Socket成功,則Netty會回調ChannelFutureListener的operationComplete方法;
              3. 在消息發送ChannelFutureListener的operationComplete方法中進行性能統計。
              4. </ol>

                示例代碼如下:

                Netty案例集錦之多線程篇(續)

                問題定位出來之后,按照正確的做法對Netty性能統計代碼進行了修正,上線之后,結合調用鏈日志,很快定位出了業務高峰期偶現的部分服務時延毛刺較大問題,優化業務線程池參數配置之后問題得到解決。

                3.4.舉一反三

                除了消息發送性能統計之外,Netty數據報的讀取、消息接收QPS性能統計也非常容易出錯,我們第一版性能統計代碼消息接收CAPs也不準確,大家知道為什么嗎?這個留作問題,供大家自己思考。

                4. Netty線程數膨脹案例

                4.1. 問題描述

                分布式服務框架在進行現網問題定位時,Dump 線程堆棧之后發現Netty的NIO線程竟然有3000多個,大量的NIO線程占用了系統的句柄資源、內存資源、CPU資源等,引發了一些其它問題,需要盡快查明原因并解決線程過多問題。

                4.2. 問題分析

                在研發環境中模擬現網組網和業務場景,使用jmc工具進行問題定位,

                使用飛行記錄器對系統運行狀況做快照,模擬示例圖如下所示:

                Netty案例集錦之多線程篇(續)

                圖4-1 使用jmc工具進行問題定位

                獲取到黑匣子數據之后,可以對系統的各種重要指標做分析,包括系統數據、內存、GC數據、線程運行狀態和數據等,如下圖所示:

                Netty案例集錦之多線程篇(續)

                圖4-2 獲取系統資源占用詳細數據

                Netty案例集錦之多線程篇(續)

                通過對線程堆棧分析,我們發現Netty的NioEventLoop線程超過了3000個!

                圖4-3 Netty線程占用超過3000個

                對服務框架協議棧的Netty客戶端和服務端源碼進行CodeReview,發現了問題所在:

                • 客戶端每連接1個服務端,就會創建1個新的NioEventLoopGroup,并設置它的線程數為1;
                • 現網有300個+節點,節點之間采用多鏈路(10個鏈路),由于業務采用了隨機路由,最終每個消費者需要跟其它200多個節點建立長連接,加上自己服務端也需要占用一些NioEventLoop線程,最終客戶端單進程線程數膨脹到了3000多個。
                • </ul>

                  業務的偽代碼如下:

                  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的優勢。案例中的錯誤線程模型如下所示:

                  Netty案例集錦之多線程篇(續)

                  圖4-4 錯誤的客戶端連接線程使用方式

                  4.3. 案例總結

                  無論是服務端監聽多個端口,還是客戶端連接多個服務端,都需要注意必須要重用NIO線程,否則就會導致線程資源浪費,在大規模組網時還會存在句柄耗盡或者棧溢出等問題。

                  Netty官方Demo僅僅是個Sample,對用戶而言,必須理解Netty的線程模型,否則很容易按照官方Demo的做法,在外層套個For循環連接多個服務端,然后,悲劇就這樣發生了。

                  修正案例中的問題非常簡單,原理如下:

                  Netty案例集錦之多線程篇(續)

                  圖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讀者交流群 Netty案例集錦之多線程篇(續) (已滿),InfoQ讀者交流群(#2) Netty案例集錦之多線程篇(續) )。

                  </div>

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!