精品国产一级在线观看,国产成人综合久久精品亚洲,免费一级欧美大片在线观看

Square從Netty 3升級到Netty 4的經驗

責任編輯:editor004

作者:金靈杰

2016-10-24 12:23:15

摘自:INFOQ

Tracon是Square公司的反向代理軟件,最初它主要用于協調后段架構從傳統單體架構向微服務架構的轉換。當從Netty 3升級到Netty 4時,如果有事件在事件循環外觸發時,必須特別注意這些事件會被異步的調度。

背景

Tracon是Square公司的反向代理軟件,最初它主要用于協調后段架構從傳統單體架構向微服務架構的轉換。作為反向代理前端,Tracon需要有非常優秀的性能,同時能夠支撐微服務架構下的各種功能定制,例如:服務發現、配置和生命周期管理等。因此Tracon網絡層基于Netty構建,以提供高效代理服務。

Tracon已經上線運行3年,其代碼行數也增加到30000行。基于Netty 3的代理模塊在如此龐大復雜的應用中運轉正常,并抽離成獨立模塊應用到Square內部認證代理服務中。

Netty 4?

Netty 4已經發布3年了,相比于Netty 3,Netty 4在內存模型和線程模型上都進行了修改。現在Netty 4已經非常成熟,并且對于Square公司來說,Netty 4還有一個重大特性:對HTTP/2協議的原生支持。Square期望其移動設備都使用HTTP/2協議,并且正在將后臺RPC框架切換到gRPC:一個基于HTTP/2協議的RPC框架。因此,Tracon作為代理服務,必須支持HTTP/2協議。

Tracon已經完成了到Netty 4的升級,整個升級過程也不是一帆風順的,以下著重介紹一些在升級過程中容易遇到的問題。

單線程channel

和Netty 3不同,Netty 4的inbound(數據輸入)事件和outbound(數據輸出)事件的所有處理器(handler)都在同一個線程中。這是得在編寫處理器的時候,可以移除線程安全相關的代碼。但是,這個變化也使得在升級過程中遇到條件競爭導致的問題。

在Netty 3中,針對pipeline的操作都是線程安全的,但是在Netty 4中,所有操作都會以事件的形式放入事件循環中異步執行。作為代理服務的Tracon,會有一個獨立的inbound channel和上游服務器進行交互,一個獨立的outbound channel和下游服務器進行交互。為了提高性能,和下游服務器的連接會被緩存起來,因此當事件循環中的事件觸發了寫操作時,這些寫操作可能會并發進行。這對于Netty 3來說沒有問題,每個寫操作都會完成后再返回;但是對于Netty 4,這些操作都進入了事件循環,可能會導致消息的亂序。

因此,在分塊測試中,偶爾會遇到發出去的數據不是按照順序到達,導致測試失敗。

當從Netty 3升級到Netty 4時,如果有事件在事件循環外觸發時,必須特別注意這些事件會被異步的調度。

連接何時真正建立?

Netty 3中,連接建立之后會發出channelConnected事件;而在Netty 4中,這個事件變成了channelActive。對于一般應用程序來說,這個改動變化不大,修改一下對應的事件處理方法即可。但是Tracon使用了雙向TLS認證以確認對方身份。

對于兩個版本的SslHandler,TLS握手完成消息處理方式完全不同。在Netty 3中,SslHandler在channelConnected事件處理方法中阻塞,并完成整個TLS握手。因此后續的處理器在channelConnected事件處理方法中就可以獲得完成握手的SSLSession。Netty 4則不同,由于其事件機制,SslHandler完成TLS握手也是異步進行的,因此直接在channelConnected事件中,是無法獲取到SSLSession的,此時TLS握手還沒有完成。對應的SslHandler會在TLS握手完成之后,發出自定義的SslHandshakeCompletionEvent事件。

對于Netty 4,TLS握手完成后的邏輯應該改成:

@Overridepublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws if (evt.equals(SslHandshakeCompletionEvent.SUCCESS)) { Principal peerPrincipal = engine.getSession().getPeerPrincipal(); // 身份驗證 // ... } super.userEventTriggered(ctx, evt);}

NIO內存泄漏

由于NIO使用direct內存,對于Netty這類網絡庫,監控direct內存是很有必要的,這可以通過使用JMX beanjava.nio:type=BufferPool,name=direct來進行。

Netty 4引入了基于線程局部變量的回收器(thread-local Recyler)來回收對象池。默認情況下,一個回收器可以最多持有262k個對象,對于ByteBuf來說,小于64k的對象都默認共用緩存。也就是說,每個回收器最多可以持有17G的direct內存。

通常情況下,NIO緩存足夠應付瞬間的數據量。但是如果有一個讀取速度很慢的后端,會大大增加內存使用。另外,當緩存中的NIO內存在被其他線程讀寫時,分配該內存的線程會無法回收這些內存。

對于回收器無法回收導致內存耗盡的問題,Netty項目也做了一些修正,以解決限制對象增長的問題:

從升級Netty 4的經驗來看,建議所有開發者基于可用內存和線程數來配置回收器。回收器最大持有對象數可以通過-Dio.netty.recycler.maxCapacity參數設置,共用內存最大限制可以通過-Dio.netty.threadLocalDirectBufferSize參數設置。如果要完全關閉回收器,可以將-Dio.netty.recycler.maxCapacity設置為0,從Tracon的使用過程來看,使用回收器并沒有對性能又多大的提升。

Tracon在內存泄漏上還做了一個小的改動:當JVM拋出錯誤時,通過一個全局的異常處理類(UncaughtExceptionHandler)直接退出應用。因為通常情況下,當應用程序遇到了OutOfMemoryError錯誤時,已經無法自我恢復。

class LoggingExceptionHandler implements Thread.UncaughtExceptionHandler { private static final Logger logger = Logger.getLogger(LoggingExceptionHandler.class); /** 注冊成默認處理器 */ static void registerAsDefault() { Thread.setDefaultUncaughtExceptionHandler(new LoggingExceptionHandler()); } @Override public void uncaughtException(Thread t, Throwable e) { if (e instanceof Exception) { logger.error("Uncaught exception killed thread named '" + t.getName() + "'.", e); } else { logger.fatal("Uncaught error killed thread named '" + t.getName() + "'." + " Exiting now.", e); System.exit(1); } }}

限制回收器使用解決了泄漏問題,但是一個讀取速度很慢的后端還是會消耗大量緩存。Tracon中通過使用channelWritabilityChanged事件來緩解寫入緩存壓力。通過增加如下處理器,可以關聯兩個channel的讀寫:

/*** 監聽當前inbound管道是否可寫,設置關聯的channel是否自動讀取。* 這可以讓代理通知另外一端當前channel有一個讀取很慢的消費者,* 僅當消費者準備完成后再進行數據讀取。*/public class WritabilityHandler extends ChannelInboundHandlerAdapter { private final Channel otherChannel; public WritabilityHandler(Channel otherChannel) { this.otherChannel = otherChannel; } @Override public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { boolean writable = ctx.channel().isWritable(); otherChannel.config().setOption(ChannelOption.AUTO_READ, writable); super.channelWritabilityChanged(ctx); }}

當發送緩存到達高水位線時,將被標記為不可寫,當發送緩存降低到低水位線時,重新被標記為可寫。默認情況下,高水位線為64kb,低水位線為32kb。這些參數可以根據實際情況進行修改。

避免寫異常丟失

當發生寫操作失敗時,如果沒有對promise設置監聽器,寫操作失敗會被忽略,這對于系統穩定性的分析會有很大影響。為了避免這種情況的發生,針對promise的監聽器非常重要,但是如果每次創建promise時都需要設置一個日志記錄的監聽器,成本比較高,也容易遺忘。針對這種情況,Tracon中針對outbound事件設置了專門的處理器,統一為寫操作的promise設置日志記錄監聽器:

@Singleton@Sharablepublic class PromiseFailureHandler extends ChannelOutboundHandlerAdapter { private final Logger logger = Logger.getLogger(PromiseFailureHandler.class); @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { promise.addListener(future -> { if (!future.isSuccess()) { logger.info("Write on channel %s failed", promise.cause(), ctx.channel()); } }); super.write(ctx, msg, promise); }}

這樣,只需要在pipeline中添加該處理器即可記錄所有的寫異常日志。

HTTP解碼器重構

Netty 4對HTTP解碼器做了重構,特別完善了對分塊數據的支持。HTTP消息體被拆分成HttpContent對象,如果HTTP數據通過分塊的方式傳輸,會有多個HttpContent順序到達,當數據塊傳輸結束時,會有一個LastHttpContent對象達到。這里需要特別注意的是,LastHttpContent繼承自HttpContent,千萬不能用以下方式來處理:

if (msg instanceof HttpContent) { ...}if (msg instanceof LastHttpContent) { … // 最后一個分塊會重復處理,前面的if已經包含了LastHttpContent}

對于LastHttpContent還有一個需要注意的是,接收到這個對象時,HTTP消息體可能已經傳輸完了,此時LastHttpContent只是作為HTTP傳輸的結束符(類似EOF)。

灰度發布

這次升級Netty 4,涉及到100多個文件共8000多行代碼。并且,由于線程模型和內存模型的修改,Tracon的替換必須非常小心。

在完成了發布前的單元測試、集成測試之后,首先需要部署到生產環境,并關閉流量。這樣,代理服務能夠和后端服務交互,同時避免用戶真實流量導入。此時,需要正對這些服務做最終的確認,確保和線上后端服務交互沒有任何問題。

完成驗證之后,才能夠開始逐步引入用戶流量,最終完成Netty 4版本的Tracon升級。經過實際驗證,使用UnpooledByteBufAllocator分配內存和之前Netty 3版本性能基本相同,期待以后使用PooledByteBufAllocator會有更好的性能。

總結

從Netty 3升級升級到Netty 4,在帶來了性能提升和新特性的同時,對原有代碼的修改需要特別注意Netty 4線程模型和內存模型的改變。以上這些遇到的問題,希望能夠作為參考,避免在Netty 4應用開發過程中再遇到類似問題。

鏈接已復制,快去分享吧

企業網版權所有?2010-2024 京ICP備09108050號-6京公網安備 11010502049343號

  • <menuitem id="jw4sk"></menuitem>

    1. <form id="jw4sk"><tbody id="jw4sk"><dfn id="jw4sk"></dfn></tbody></form>
      主站蜘蛛池模板: 沁阳市| 长泰县| 嵊州市| 苏尼特左旗| 嘉义县| 尼勒克县| 邳州市| 鱼台县| 彭泽县| 甘洛县| 宁蒗| 乐至县| 西和县| 文水县| 加查县| 鄂托克旗| 佛学| 临海市| 哈尔滨市| 许昌市| 邢台县| 淄博市| 普兰县| 大厂| 河池市| 丽江市| 交口县| 美姑县| 玉屏| 正阳县| 昌吉市| 贵港市| 平罗县| 内江市| 府谷县| 深泽县| 武定县| 林周县| 饶河县| 寿阳县| 上饶县|