Netty入門比較簡單,主要緣由有以下幾點:編程
Netty的API封裝比較簡單,將複雜的網絡通訊經過BootStrap等工具類作了二次封裝,用戶使用起來比較簡單;安全
Netty源碼自帶的Demo比較多,經過Demo能夠很快入門;服務器
Netty社區資料、相關學習書籍也比較多,學習資料比較豐富。網絡
可是不少入門以後的Netty學習者遇到了不少困惑,例如不知道在實際項目中如何使用Netty、遇到Netty問題以後無從定位等,這些問題嚴重製約了對Netty的深刻掌握和實際項目應用。多線程
Netty相關問題比較難定位的主要緣由以下:併發
1) NIO編程自身的複雜性,涉及到大量NIO類庫、Netty自身封裝的類庫等,當你須要打開黑盒定位問題時,必須對這些類庫瞭如指掌;不然即使定位到問題所在,也不知因此然,更沒法修復;異步
2) Netty複雜的多線程模型,用戶在實際使用Netty時,會涉及到Netty本身封裝的線程組、線程池、NIO線程,以及業務線程,通訊鏈路的建立、I/O消息的讀寫會涉及到複雜的線程切換,這會讓初學者雲山霧繞,調試起來很是痛苦,甚至都不知道從哪裏調試;分佈式
3) Netty版本的跨度大,從實際商用狀況看,涉及到了Netty 3.X、4.X和5.X等多個版本,每一個Major版本之間特性變化很是大,即使是Minor版本都存在一些差別,這些功能特性和類庫差別會給使用者帶來不少問題,版本升級以後稍有不慎就會掉入陷阱。工具
Netty案例集錦的案例來源於做者在實際項目中遇到的問題總結、以及Netty社區網友的反饋,大多數案例都來源於實際項目,也有少部分是讀者在學習Netty中遭遇的比較典型的問題。oop
學習和掌握Netty多線程模型是個難點,在實際項目中如何使用好Netty多線程更加困難,不少網上問題和事故都來源於對Netty線程模型瞭解不透徹所致。鑑於此,Netty案例集錦系列就首先從多線程方面開始。
業務代碼升級Netty 3到Netty4以後,運行一段時間,Java進程就會宕機,查看系統運行日誌發現系統發生了內存泄露(示例堆棧):
圖2-1 內存泄漏堆棧
對內存進行監控(切換使用堆內存池,方便對內存進行監控),發現堆內存一直飆升,以下所示(示例堆內存監控):
圖2-2 堆內存監控示例
使用jmap -dump:format=b,file=netty.bin PID 將堆內存dump出來,經過IBM的HeapAnalyzer工具進行分析,發現ByteBuf發生了泄露。
由於使用了Netty 4的內存池,因此首先懷疑是否是申請的ByteBuf沒有被釋放致使?查看代碼,發現消息發送完成以後,Netty底層已經調用ReferenceCountUtil.release(message)對內存進行了釋放。這是怎麼回事呢?難道Netty 4.X的內存池有Bug,調用release操做釋放內存失敗?
考慮到Netty 內存池自身Bug的可能性不大,首先從業務的使用方式入手分析:
1)內存的分配是在業務代碼中進行,因爲使用到了業務線程池作I/O操做和業務操做的隔離,實際上內存是在業務線程中分配的;
2)內存的釋放操做是在outbound中進行,按照Netty 3的線程模型,downstream(對應Netty 4的outbound,Netty 4取消了upstream和downstream)的handler也是由業務調用者線程執行的,也就是說申請和釋放在同一個業務線程中進行。初次排查並無發現致使內存泄露的根因,繼續分析Netty內存池的實現原理。
Netty 內存池實現原理分析:查看Netty的內存池分配器PooledByteBufAllocator的源碼實現,發現內存池實際是基於線程上下文實現的,相關代碼以下:
圖2-3
也就是說內存的申請和釋放必須在同一線程上下文中,不能跨線程。跨線程以後實際操做的就不是同一起內存區域,這會致使不少嚴重的問題,內存泄露即是其中之一。內存在A線程申請,切換到B線程釋放,實際是沒法正確回收的。
Netty 4修改了Netty 3的線程模型:在Netty 3的時候,upstream是在I/O線程裏執行的,而downstream是在業務線程裏執行。當Netty從網絡讀取一個數據報投遞給業務handler的時候,handler是在I/O線程裏執行;而當咱們在業務線程中調用write和writeAndFlush向網絡發送消息的時候,handler是在業務線程裏執行,直到最後一個Header handler將消息寫入到發送隊列中,業務線程才返回。
Netty4修改了這一模型,在Netty 4裏inbound(對應Netty 3的upstream)和outbound(對應Netty 3的downstream)都是在NioEventLoop(I/O線程)中執行。當咱們在業務線程裏經過ChannelHandlerContext.write發送消息的時候,Netty 4在將消息發送事件調度到ChannelPipeline的時候,首先將待發送的消息封裝成一個Task,而後放到NioEventLoop的任務隊列中,由NioEventLoop線程異步執行。後續全部handler的調度和執行,包括消息的發送、I/O事件的通知,都由NioEventLoop線程負責處理。
在本案例中,ByteBuf在業務線程中申請,在後續的ChannelHandler中釋放,ChannelHandler是由Netty的I/O線程(EventLoop)執行的,所以內存的申請和釋放不在同一個線程中,致使內存泄漏。
Netty 3的I/O事件處理流程:
圖2-4 Netty 3的I/O線程模型
Netty 4的I/O消息處理流程:
圖2-5 Netty 4 I/O線程模型
Netty 4.X版本新增的內存池確實很是高效,可是若是使用不當則會致使各類嚴重的問題。諸如內存泄露這類問題,功能測試並無異常,若是相關接口沒有進行壓測或者穩定性測試而直接上線,則會致使嚴重的線上問題。
內存池PooledByteBuf的使用建議:
1)申請以後必定要記得釋放,Netty自身Socket讀取和發送的ByteBuf系統會自動釋放,用戶不須要作二次釋放;若是用戶使用Netty的內存池在應用中作ByteBuf的對象池使用,則須要本身主動釋放;
2)避免錯誤的釋放:跨線程釋放、重複釋放等都是非法操做,要避免。特別是跨線程申請和釋放,每每具備隱蔽性,問題定位難度較大;
3)防止隱式的申請和分配:以前曾經發生過一個案例,爲了解決內存池跨線程申請和釋放問題,有用戶對內存池作了二次包裝,以實現多線程操做時,內存始終由包裝的管理線程申請和釋放,這樣能夠屏蔽用戶業務線程模型和訪問方式的差別。誰知運行一段時間以後再次發生了內存泄露,最後發現原來調用ByteBuf的write操做時,若是內存容量不足,會自動進行容量擴展。擴展操做由業務線程執行,這就繞過了內存池管理線程,發生了「引用逃逸」;
4)避免跨線程申請和使用內存池,因爲存在「引用逃逸」等隱式的內存建立,實際上跨線程申請和使用內存池是很是危險的行爲。儘管從技術角度看能夠實現一個跨線程協調的內存池機制,甚至重寫PooledByteBufAllocator,可是這無疑會增長不少複雜性,一般也使用不到。若是確實存在跨線程的ByteBuf傳遞,並且沒法保證ByteBuf在另外一個線程中會從新分配大小等操做,最簡單保險的方式就是在線程切換點作一次ByteBuf的拷貝,但這會形成性能降低。
比較好的一種方案就是若是存在跨線程的ByteBuf傳遞,對ByteBuf的寫操做要在分配線程完成,另外一個線程只能作讀操做。操做完成以後發送一個事件通知分配線程,由分配線程執行內存釋放操做。
業務代碼升級Netty 3到Netty4以後,並無給產品帶來預期的性能提高,有些甚至還發生了很是嚴重的性能降低,這與Netty 官方給出的數據並不一致。
Netty 官方性能測試對比數據:咱們比較了兩個分別創建在Netty 3和4基礎上echo協議服務器。(Echo很是簡單,這樣,任何垃圾的產生都是Netty的緣由,而不是協議的緣由)。我使它們服務於相同的分佈式echo協議客戶端,來自這些客戶端的16384個併發鏈接重複發送256字節的隨機負載,幾乎使千兆以太網飽和。
根據測試結果,Netty 4:
GC中斷頻率是原來的1/5: 45.5 vs. 9.2次/分鐘
垃圾生成速度是原來的1/5: 207.11 vs 41.81 MiB/秒
首先經過JMC等性能分析工具對性能熱點進行分析,示例以下(信息安全等緣由,只給出分析過程示例截圖):
圖3-1 性能熱點線程堆棧
經過對熱點方法的分析,發如今消息發送過程當中,有兩處熱點:
1)消息發送性能統計相關Handler;
2)編碼Handler。
對使用Netty 3版本的業務產品進行性能對比測試,發現上述兩個Handler也是熱點方法。既然都是熱點,爲啥切換到Netty4以後性能降低這麼厲害呢?
經過方法的調用樹分析發現了兩個版本的差別:在Netty 3中,上述兩個熱點方法都是由業務線程負責執行;而在Netty 4中,則是由NioEventLoop(I/O)線程執行。對於某個鏈路,業務是擁有多個線程的線程池,而NioEventLoop只有一個,因此執行效率更低,返回給客戶端的應答時延就大。時延增大以後,天然致使系統併發量下降,性能降低。
找出問題根因以後,針對Netty 4的線程模型對業務進行專項優化,將耗時的編碼等操做遷移到業務線程中執行,爲I/O線程減負,性能達到預期,遠超過了Netty 3老版本的性能。
Netty 3的業務線程調度模型圖以下所示:充分利用了業務多線程並行編碼和Handler處理的優點,週期T內能夠處理N條業務消息:
圖3-2 Netty 3 Handler執行線程模型
切換到Netty 4以後,業務耗時Handler被I/O線程串行執行,所以性能發生比較大的降低:
圖3-3 Netty 4 Handler執行線程模型
該問題的根因仍是因爲Netty 4的線程模型變動引發,線程模型變動以後,不只影響業務的功能,甚至對性能也會形成很大的影響。
對Netty的升級須要從功能、兼容性和性能等多個角度進行綜合考慮,切不可只盯着API變動這個芝麻,而丟掉了性能這個西瓜。API的變動會致使編譯錯誤,可是性能降低卻隱藏於無形之中,稍不留意就會中招。
對於講究快速交付、敏捷開發和灰度發佈的互聯網應用,升級的時候更應該要小心。
個人服務碰到一個問題,常常有請求上來到MessageDecoder就結束了,沒有繼續往LogicServerHandler裏面送,以爲很奇怪,是否是線程池滿了?我想請教:
1)netty 5如何打印executor線程的佔用狀況,如空閒線程數?
2)executor設置的大小通常如何進行計算的?
業務代碼示例以下:
從服務端初始化代碼來看,並無什麼問題,業務LogicServerHandler沒有接收到消息,有以下幾種可能:
1)客戶端並無將消息發送到服務端,能夠在服務端LoggingHandler中打印日誌查看;
2)服務端部分消息解碼發生異常,致使消息被丟棄/忽略,沒有走到LogicServerHandler中;
3)執行業務Handler的DefaultEventExecutor中的線程太繁忙,致使任務隊列積壓,長時間得不處處理。
經過抓包結合日誌分析,可能致使問題的緣由1和2排除,須要繼續對可能緣由3進行排查。
Netty 5如何打印executor線程的佔用狀況,如空閒線程數?回答這些問題,首先要了解Netty的線程組和線程池機制。
Netty的EventExecutorGroup實際就是一組EventExecutor,它的定義以下:
一般經過它的next方法從線程組中獲取一個線程池,代碼以下:
Netty EventExecutor的典型實現有兩個:DefaultEventExecutor和SingleThreadEventLoop,在本案例中,由於使用的是DefaultEventExecutorGroup,因此實際執行業務Handler的線程池就是DefaultEventExecutor,它繼承自SingleThreadEventExecutor,從名稱就能夠看出它是個單線程的線程池。它的工做原理以下:
1)DefaultEventExecutor聚合JDK的Executor和Thread, 首次執行Task的時候啓動線程,將線程池狀態修改成運行態;
2)Thread run方法循環從隊列中獲取Task執行,若是隊列爲空,則同步阻塞,線程無限循環執行,直到接收到退出信號。
圖4-1 DefaultEventExecutor工做原理
用戶想經過Netty提供的DefaultEventExecutorGroup來併發執行業務Handler,但實際上倒是單線程SingleThreadEventExecutor在串行執行業務邏輯,當服務端消息接收速度超過業務邏輯執行速度時,就會致使業務消息積壓在SingleThreadEventExecutor的消息隊列中得不到及時處理,現象就是業務Handler好像得不到執行,部分業務消息丟失。
講解完Netty線程模型後,問題緣由也定位出來了。其實咱們發現,能夠經過EventExecutor獲取EventExecutorGroup的信息,而後獲取整個EventExecutor線程組信息,最後打印線程負載信息,代碼以下:
執行結果以下:
事實上,Netty爲了防止多線程執行某個Handler(Channel)引發線程安全問題,實際只有一個線程會執行某個Handler,代碼以下:
須要指出的是,SingleThreadEventExecutor的pendingTasks多是個耗時的操做,所以調用的時候須要注意:
實際就像JDK的線程池,不一樣的業務場景、硬件環境和性能標就會有不一樣的配置,沒法給出標準的答案。須要進行實際測試、評估和調優來靈活調整。
最後再總結回顧下問題,對於案例中的代碼,實際上在使用單線程處理某個Handler的LogicServerHandler,做者可能想併發多線程執行這個Handler,提高業務處理性能,但實際並無達到設計效果。
若是業務性能存在問題,並不奇怪,由於業務實際是單線程串行處理的!固然,若是業務存在多個Channel,則每一個/多個Channel會對應一個線程(池),也能夠實現多線程處理,這取決於客戶端的接入數。
案例中代碼的線程處理模型以下所示(單個鏈路模型):
圖4-3 單線程執行業務邏輯線程模型圖
我有一個非線程安全的類ThreadUnsafeClass,這個類會在channelRead方法中被調用。我下面這樣的調用方法在多線程環境下安全嗎?謝謝!
代碼示例以下:
Netty 4優化了Netty 3的線程模型,其中一個很是大的優化就是用戶不須要再擔憂ChannelHandler會被併發調用,總結以下:
1)ChannelHandler's的方法不會被Netty併發調用;
2)用戶再也不須要對ChannelHandler的各個方法作同步保護;
3)ChannelHandler實例不容許被屢次添加到ChannelPiple中,不然線程安全將得不到保證。
根據上述分析,MyHandler的channelRead方法不會被併發調用,所以不存在線程安全問題。
ChannelHandler的線程安全存在幾個特例,總結以下:
1)若是ChannelHandler被註解爲 @Sharable,全局只有一個handler實例,它會被多個Channel的Pipeline共享,會被多線程併發調用,所以它不是線程安全的;
2)若是存在跨ChannelHandler的實例級變量共享,須要特別注意,它可能不是線程安全的。
非線程安全的跨ChannelHandler變量原理以下:
圖5-1 串行調用,線程安全
Netty支持在添加ChannelHandler的時候,指定執行該Handler的EventExecutorGroup,這就意味着在整個ChannelPipeline執行過程當中,可能會發生線程切換。此時,若是同一個對象在多個ChannelHandler中被共享,可能會被多線程併發操做,原理以下:
圖5-2 並行調用,多Handler共享成員變量,非線程安全