歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> Linux教程

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中,SslHandlerchannelConnected事件處理方法中阻塞,並完成整個TLS握手。因此後續的處理器在channelConnected事件處理方法中就可以獲得完成握手的SSLSession。Netty 4則不同,由於其事件機制,SslHandler完成TLS握手也是異步進行的,因此直接在channelConnected事件中,是無法獲取到SSLSession的,此時TLS握手還沒有完成。對應的SslHandler會在TLS握手完成之後,發出自定義的SslHandshakeCompletionEvent事件。

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

@Override
public 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項目也做了一些修正,以解決限制對象增長的問題:

  • 允許設置每個線程的WeakOrderQueue實例的最大數量;
  • 回收器中引入內存分配/共用比例;
  • 從Netty 3.10移植SendBufferPooled,當使用非共用ByteBuf分配器(ByteBufAllocator)時使用;

從升級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
@Sharable
public 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應用開發過程中再遇到類似問題。

Netty權威指南 PDF完整版帶目錄書簽+源碼  http://www.linuxidc.com/Linux/2016-07/133575.htm

運用Spring注解實現Netty服務器端UDP應用程序  http://www.linuxidc.com/Linux/2013-09/89780.htm

Netty源碼學習筆記 http://www.linuxidc.com/Linux/2013-09/89778.htm

Netty使用實例 http://www.linuxidc.com/Linux/2013-09/89779.htm

Java NIO框架--Netty4的簡單示例  http://www.linuxidc.com/Linux/2015-01/111335.htm

Netty 的詳細介紹:請點這裡
Netty 的下載地址:請點這裡

Copyright © Linux教程網 All Rights Reserved