首頁 > 軟體

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

2020-06-16 17:30:03

背景

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專案也做了一些修正,以解決限制物件增長的問題:

從升級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


IT145.com E-mail:sddin#qq.com