Netty 系列之 Netty 百萬級推送服務設計要點

代官山發表於2019-01-14

Netty 系列之 Netty 百萬級推送服務設計要點

閱讀數:524902015 年 1 月 4 日

話題:語言 & 開發架構

1. 背景

1.1. 話題來源

最近很多從事移動網際網路和物聯網開發的同學給我發郵件或者微博私信我,諮詢推送服務相關的問題。問題五花八門,在幫助大家答疑解惑的過程中,我也對問題進行了總結,大概可以歸納為如下幾類:

  1. Netty 是否可以做推送伺服器?
  2. 如果使用 Netty 開發推送服務,一個伺服器最多可以支撐多少個客戶端?
  3. 使用 Netty 開發推送服務遇到的各種技術問題。

由於諮詢者眾多,關注點也比較集中,我希望通過本文的案例分析和對推送服務設計要點的總結,幫助大家在實際工作中少走彎路。

1.2. 推送服務

移動網際網路時代,推送 (Push) 服務成為 App 應用不可或缺的重要組成部分,推送服務可以提升使用者的活躍度和留存率。我們的手機每天接收到各種各樣的廣告和提示訊息等大多數都是通過推送服務實現的。

隨著物聯網的發展,大多數的智慧家居都支援移動推送服務,未來所有接入物聯網的智慧裝置都將是推送服務的客戶端,這就意味著推送服務未來會面臨海量的裝置和終端接入。

1.3. 推送服務的特點

移動推送服務的主要特點如下:

  1. 使用的網路主要是運營商的無線行動網路,網路質量不穩定,例如在地鐵上訊號就很差,容易發生網路閃斷;
  2. 海量的客戶端接入,而且通常使用長連線,無論是客戶端還是服務端,資源消耗都非常大;
  3. 由於谷歌的推送框架無法在國內使用,Android 的長連線是由每個應用各自維護的,這就意味著每檯安卓裝置上會存在多個長連線。即便沒有訊息需要推送,長連線本身的心跳訊息量也是非常巨大的,這就會導致流量和耗電量的增加;
  4. 不穩定:訊息丟失、重複推送、延遲送達、過期推送時有發生;
  5. 垃圾訊息滿天飛,缺乏統一的服務治理能力。

為了解決上述弊端,一些企業也給出了自己的解決方案,例如京東雲推出的推送服務,可以實現多應用單服務單連線模式,使用 AlarmManager 定時心跳節省電量和流量。

2. 智慧家居領域的一個真實案例

2.1. 問題描述

智慧家居 MQTT 訊息服務中介軟體,保持 10 萬使用者線上長連線,2 萬使用者併發做訊息請求。程式執行一段時間之後,發現記憶體洩露,懷疑是 Netty 的 Bug。其它相關資訊如下:

  1. MQTT 訊息服務中介軟體伺服器記憶體 16G,8 個核心 CPU;
  2. Netty 中 boss 執行緒池大小為 1,worker 執行緒池大小為 6,其餘執行緒分配給業務使用。該分配方式後來調整為 worker 執行緒池大小為 11,問題依舊;
  3. Netty 版本為 4.0.8.Final。

2.2. 問題定位

首先需要 dump 記憶體堆疊,對疑似記憶體洩露的物件和引用關係進行分析,如下所示:

我們發現 Netty 的 ScheduledFutureTask 增加了 9076%,達到 110W 個左右的例項,通過對業務程式碼的分析發現使用者使用 IdleStateHandler 用於在鏈路空閒時進行業務邏輯處理,但是空閒時間設定的比較大,為 15 分鐘。

Netty 的 IdleStateHandler 會根據使用者的使用場景,啟動三類定時任務,分別是:ReaderIdleTimeoutTask、WriterIdleTimeoutTask 和 AllIdleTimeoutTask,它們都會被加入到 NioEventLoop 的 Task 佇列中被排程和執行。

由於超時時間過長,10W 個長連結鏈路會建立 10W 個 ScheduledFutureTask 物件,每個物件還儲存有業務的成員變數,非常消耗記憶體。使用者的持久代設定的比較大,一些定時任務被老化到持久代中,沒有被 JVM 垃圾回收掉,記憶體一直在增長,使用者誤認為存在記憶體洩露。

事實上,我們進一步分析發現,使用者的超時時間設定的非常不合理,15 分鐘的超時達不到設計目標,重新設計之後將超時時間設定為 45 秒,記憶體可以正常回收,問題解決。

2.3. 問題總結

如果是 100 個長連線,即便是長週期的定時任務,也不存在記憶體洩露問題,在新生代通過 minor GC 就可以實現記憶體回收。正是因為十萬級的長連線,導致小問題被放大,引出了後續的各種問題。

事實上,如果使用者確實有長週期執行的定時任務,該如何處理?對於海量長連線的推送服務,程式碼處理稍有不慎,就滿盤皆輸,下面我們針對 Netty 的架構特點,介紹下如何使用 Netty 實現百萬級客戶端的推送服務。

3. Netty 海量推送服務設計要點

作為高效能的 NIO 框架,利用 Netty 開發高效的推送服務技術上是可行的,但是由於推送服務自身的複雜性,想要開發出穩定、高效能的推送服務並非易事,需要在設計階段針對推送服務的特點進行合理設計。

3.1. 最大控制程式碼數修改

百萬長連線接入,首先需要優化的就是 Linux 核心引數,其中 Linux 最大檔案控制程式碼數是最重要的調優引數之一,預設單程式開啟的最大控制程式碼數是 1024,通過 ulimit -a 可以檢視相關引數,示例如下:

[root@lilinfeng ~]# ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 256324
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024

...... 後續輸出省略

當單個推送服務接收到的連結超過上限後,就會報“too many open files”,所有新的客戶端接入將失敗。

通過 vi /etc/security/limits.conf 新增如下配置引數:修改之後儲存,登出當前使用者,重新登入,通過 ulimit -a 檢視修改的狀態是否生效。

*  soft  nofile  1000000
*  hard  nofile  1000000

需要指出的是,儘管我們可以將單個程式開啟的最大控制程式碼數修改的非常大,但是當控制程式碼數達到一定數量級之後,處理效率將出現明顯下降,因此,需要根據伺服器的硬體配置和處理能力進行合理設定。如果單個伺服器效能不行也可以通過叢集的方式實現。

3.2. 當心 CLOSE_WAIT

從事移動推送服務開發的同學可能都有體會,移動無線網路可靠性非常差,經常存在客戶端重置連線,網路閃斷等。

在百萬長連線的推送系統中,服務端需要能夠正確處理這些網路異常,設計要點如下:

  1. 客戶端的重連間隔需要合理設定,防止連線過於頻繁導致的連線失敗(例如埠還沒有被釋放);
  2. 客戶端重複登陸拒絕機制;
  3. 服務端正確處理 I/O 異常和解碼異常等,防止控制程式碼洩露。

最後特別需要注意的一點就是 close_wait 過多問題,由於網路不穩定經常會導致客戶端斷連,如果服務端沒有能夠及時關閉 socket,就會導致處於 close_wait 狀態的鏈路過多。close_wait 狀態的鏈路並不釋放控制程式碼和記憶體等資源,如果積壓過多可能會導致系統控制程式碼耗盡,發生“Too many open files”異常,新的客戶端無法接入,涉及建立或者開啟控制程式碼的操作都將失敗。

下面對 close_wait 狀態進行下簡單介紹,被動關閉 TCP 連線狀態遷移圖如下所示:

圖 3-1 被動關閉 TCP 連線狀態遷移圖

close_wait 是被動關閉連線是形成的,根據 TCP 狀態機,伺服器端收到客戶端傳送的 FIN,TCP 協議棧會自動傳送 ACK,連結進入 close_wait 狀態。但如果伺服器端不執行 socket 的 close() 操作,狀態就不能由 close_wait 遷移到 last_ack,則系統中會存在很多 close_wait 狀態的連線。通常來說,一個 close_wait 會維持至少 2 個小時的時間(系統預設超時時間的是 7200 秒,也就是 2 小時)。如果服務端程式因某個原因導致系統造成一堆 close_wait 消耗資源,那麼通常是等不到釋放那一刻,系統就已崩潰。

導致 close_wait 過多的可能原因如下:

  1. 程式處理 Bug,導致接收到對方的 fin 之後沒有及時關閉 socket,這可能是 Netty 的 Bug,也可能是業務層 Bug,需要具體問題具體分析;
  2. 關閉 socket 不及時:例如 I/O 執行緒被意外阻塞,或者 I/O 執行緒執行的使用者自定義 Task 比例過高,導致 I/O 操作處理不及時,鏈路不能被及時釋放。

下面我們結合 Netty 的原理,對潛在的故障點進行分析。

設計要點 1:不要在 Netty 的 I/O 執行緒上處理業務(心跳傳送和檢測除外)。Why? 對於 Java 程式,執行緒不能無限增長,這就意味著 Netty 的 Reactor 執行緒數必須收斂。Netty 的預設值是 CPU 核數 * 2,通常情況下,I/O 密集型應用建議執行緒數儘量設定大些,但這主要是針對傳統同步 I/O 而言,對於非阻塞 I/O,執行緒數並不建議設定太大,儘管沒有最優值,但是 I/O 執行緒數經驗值是 [CPU 核數 + 1,CPU 核數 *2 ] 之間。

假如單個伺服器支撐 100 萬個長連線,伺服器核心數為 32,則單個 I/O 執行緒處理的連結數 L = 100/(32 * 2) = 15625。 假如每 5S 有一次訊息互動(新訊息推送、心跳訊息和其它管理訊息),則平均 CAPS = 15625 / 5 = 3125 條 / 秒。這個數值相比於 Netty 的處理效能而言壓力並不大,但是在實際業務處理中,經常會有一些額外的複雜邏輯處理,例如效能統計、記錄介面日誌等,這些業務操作效能開銷也比較大,如果在 I/O 執行緒上直接做業務邏輯處理,可能會阻塞 I/O 執行緒,影響對其它鏈路的讀寫操作,這就會導致被動關閉的鏈路不能及時關閉,造成 close_wait 堆積。

設計要點 2:在 I/O 執行緒上執行自定義 Task 要當心。Netty 的 I/O 處理執行緒 NioEventLoop 支援兩種自定義 Task 的執行:

  1. 普通的 Runnable: 通過呼叫 NioEventLoop 的 execute(Runnable task) 方法執行;
  2. 定時任務 ScheduledFutureTask: 通過呼叫 NioEventLoop 的 schedule(Runnable command, long delay, TimeUnit unit) 系列介面執行。

為什麼 NioEventLoop 要支援使用者自定義 Runnable 和 ScheduledFutureTask 的執行,並不是本文要討論的重點,後續會有專題文章進行介紹。本文重點對它們的影響進行分析。

在 NioEventLoop 中執行 Runnable 和 ScheduledFutureTask,意味著允許使用者在 NioEventLoop 中執行非 I/O 操作類的業務邏輯,這些業務邏輯通常用訊息報文的處理和協議管理相關。它們的執行會搶佔 NioEventLoop I/O 讀寫的 CPU 時間,如果使用者自定義 Task 過多,或者單個 Task 執行週期過長,會導致 I/O 讀寫操作被阻塞,這樣也間接導致 close_wait 堆積。

所以,如果使用者在程式碼中使用到了 Runnable 和 ScheduledFutureTask,請合理設定 ioRatio 的比例,通過 NioEventLoop 的 setIoRatio(int ioRatio) 方法可以設定該值,預設值為 50,即 I/O 操作和使用者自定義任務的執行時間比為 1:1。

我的建議是當服務端處理海量客戶端長連線的時候,不要在 NioEventLoop 中執行自定義 Task,或者非心跳類的定時任務。

設計要點 3:IdleStateHandler 使用要當心。很多使用者會使用 IdleStateHandler 做心跳傳送和檢測,這種用法值得提倡。相比於自己啟定時任務傳送心跳,這種方式更高效。但是在實際開發中需要注意的是,在心跳的業務邏輯處理中,無論是正常還是異常場景,處理時延要可控,防止時延不可控導致的 NioEventLoop 被意外阻塞。例如,心跳超時或者發生 I/O 異常時,業務呼叫 Email 傳送介面告警,由於 Email 服務端處理超時,導致郵件傳送客戶端被阻塞,級聯引起 IdleStateHandler 的 AllIdleTimeoutTask 任務被阻塞,最終 NioEventLoop 多路複用器上其它的鏈路讀寫被阻塞。

對於 ReadTimeoutHandler 和 WriteTimeoutHandler,約束同樣存在。

3.3. 合理的心跳週期

百萬級的推送服務,意味著會存在百萬個長連線,每個長連線都需要靠和 App 之間的心跳來維持鏈路。合理設定心跳週期是非常重要的工作,推送服務的心跳週期設定需要考慮移動無線網路的特點。

當一臺智慧手機連上行動網路時,其實並沒有真正連線上 Internet,運營商分配給手機的 IP 其實是運營商的內網 IP,手機終端要連線上 Internet 還必須通過運營商的閘道器進行 IP 地址的轉換,這個閘道器簡稱為 NAT(NetWork Address Translation),簡單來說就是手機終端連線 Internet 其實就是移動內網 IP,埠,外網 IP 之間相互對映。

GGSN(GateWay GPRS Support Note) 模組就實現了 NAT 功能,由於大部分的移動無線網路運營商為了減少閘道器 NAT 對映表的負荷,如果一個鏈路有一段時間沒有通訊時就會刪除其對應表,造成鏈路中斷,正是這種刻意縮短空閒連線的釋放超時,原本是想節省通道資源的作用,沒想到讓網際網路的應用不得以遠高於正常頻率傳送心跳來維護推送的長連線。以中移動的 2.5G 網路為例,大約 5 分鐘左右的基帶空閒,連線就會被釋放。

由於移動無線網路的特點,推送服務的心跳週期並不能設定的太長,否則長連線會被釋放,造成頻繁的客戶端重連,但是也不能設定太短,否則在當前缺乏統一心跳框架的機制下很容易導致信令風暴(例如微信心跳信令風暴問題)。具體的心跳週期並沒有統一的標準,180S 也許是個不錯的選擇,微信為 300S。

在 Netty 中,可以通過在 ChannelPipeline 中增加 IdleStateHandler 的方式實現心跳檢測,在建構函式中指定鏈路空閒時間,然後實現空閒回撥介面,實現心跳的傳送和檢測,程式碼如下:

public void initChannel({@link Channel} channel) {
 channel.pipeline().addLast("idleStateHandler", new {@link   IdleStateHandler}(0, 0, 180));
 channel.pipeline().addLast("myHandler", new MyHandler());
}
攔截鏈路空閒事件並處理心跳:
 public class MyHandler extends {@link ChannelHandlerAdapter} {
     {@code @Override}
      public void userEventTriggered({@link ChannelHandlerContext} ctx, {@link Object} evt) throws {@link Exception} {
          if (evt instanceof {@link IdleStateEvent}} {
              // 心跳處理 
          }
      }
  }

3.4. 合理設定接收和傳送緩衝區容量

對於長連結,每個鏈路都需要維護自己的訊息接收和傳送緩衝區,JDK 原生的 NIO 類庫使用的是 java.nio.ByteBuffer, 它實際是一個長度固定的 Byte 陣列,我們都知道陣列無法動態擴容,ByteBuffer 也有這個限制,相關程式碼如下:

public abstract class ByteBuffer
    extends Buffer
    implements Comparable
{
    final byte[] hb; // Non-null only for heap buffers
    final int offset;
    boolean isReadOnly;

容量無法動態擴充套件會給使用者帶來一些麻煩,例如由於無法預測每條訊息報文的長度,可能需要預分配一個比較大的 ByteBuffer,這通常也沒有問題。但是在海量推送服務系統中,這會給服務端帶來沉重的記憶體負擔。假設單條推送訊息最大上限為 10K,訊息平均大小為 5K,為了滿足 10K 訊息的處理,ByteBuffer 的容量被設定為 10K,這樣每條鏈路實際上多消耗了 5K 記憶體,如果長連結鏈路數為 100 萬,每個鏈路都獨立持有 ByteBuffer 接收緩衝區,則額外損耗的總記憶體 Total(M) = 1000000 * 5K = 4882M。記憶體消耗過大,不僅僅增加了硬體成本,而且大記憶體容易導致長時間的 Full GC,對系統穩定性會造成比較大的衝擊。

實際上,最靈活的處理方式就是能夠動態調整記憶體,即接收緩衝區可以根據以往接收的訊息進行計算,動態調整記憶體,利用 CPU 資源來換記憶體資源,具體的策略如下:

  1. ByteBuffer 支援容量的擴充套件和收縮,可以按需靈活調整,以節約記憶體;
  2. 接收訊息的時候,可以按照指定的演算法對之前接收的訊息大小進行分析,並預測未來的訊息大小,按照預測值靈活調整緩衝區容量,以做到最小的資源損耗滿足程式正常功能。

幸運的是,Netty 提供的 ByteBuf 支援容量動態調整,對於接收緩衝區的記憶體分配器,Netty 提供了兩種:

  1. FixedRecvByteBufAllocator:固定長度的接收緩衝區分配器,由它分配的 ByteBuf 長度都是固定大小的,並不會根據實際資料包的大小動態收縮。但是,如果容量不足,支援動態擴充套件。動態擴充套件是 Netty ByteBuf 的一項基本功能,與 ByteBuf 分配器的實現沒有關係;
  2. AdaptiveRecvByteBufAllocator:容量動態調整的接收緩衝區分配器,它會根據之前 Channel 接收到的資料包大小進行計算,如果連續填充滿接收緩衝區的可寫空間,則動態擴充套件容量。如果連續 2 次接收到的資料包都小於指定值,則收縮當前的容量,以節約記憶體。

相對於 FixedRecvByteBufAllocator,使用 AdaptiveRecvByteBufAllocator 更為合理,可以在建立客戶端或者服務端的時候指定 RecvByteBufAllocator,程式碼如下:

 Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .option(ChannelOption.TCP_NODELAY, true)
             .option(ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator.DEFAULT)

如果預設沒有設定,則使用 AdaptiveRecvByteBufAllocator。

另外值得注意的是,無論是接收緩衝區還是傳送緩衝區,緩衝區的大小建議設定為訊息的平均大小,不要設定成最大訊息的上限,這會導致額外的記憶體浪費。通過如下方式可以設定接收緩衝區的初始大小:

/**
	 * Creates a new predictor with the specified parameters.
	 * 
	 * @param minimum
	 *            the inclusive lower bound of the expected buffer size
	 * @param initial
	 *            the initial buffer size when no feed back was received
	 * @param maximum
	 *            the inclusive upper bound of the expected buffer size
	 */
	public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum) 

對於訊息傳送,通常需要使用者自己構造 ByteBuf 並編碼,例如通過如下工具類建立訊息傳送緩衝區:

圖 3-2 構造指定容量的緩衝區

3.5. 記憶體池

推送伺服器承載了海量的長連結,每個長連結實際就是一個會話。如果每個會話都持有心跳資料、接收緩衝區、指令集等資料結構,而且這些例項隨著訊息的處理朝生夕滅,這就會給伺服器帶來沉重的 GC 壓力,同時消耗大量的記憶體。

最有效的解決策略就是使用記憶體池,每個 NioEventLoop 執行緒處理 N 個鏈路,線上程內部,鏈路的處理時序列的。假如 A 鏈路首先被處理,它會建立接收緩衝區等物件,待解碼完成之後,構造的 POJO 物件被封裝成 Task 後投遞到後臺的執行緒池中執行,然後接收緩衝區會被釋放,每條訊息的接收和處理都會重複接收緩衝區的建立和釋放。如果使用記憶體池,則當 A 鏈路接收到新的資料包之後,從 NioEventLoop 的記憶體池中申請空閒的 ByteBuf,解碼完成之後,呼叫 release 將 ByteBuf 釋放到記憶體池中,供後續 B 鏈路繼續使用。

使用記憶體池優化之後,單個 NioEventLoop 的 ByteBuf 申請和 GC 次數從原來的 N = 1000000/64 = 15625 次減少為最少 0 次(假設每次申請都有可用的記憶體)。

下面我們以推特使用 Netty4 的 PooledByteBufAllocator 進行 GC 優化作為案例,對記憶體池的效果進行評估,結果如下:

垃圾生成速度是原來的 1/5,而垃圾清理速度快了 5 倍。使用新的記憶體池機制,幾乎可以把網路頻寬壓滿。

Netty4 之前的版本問題如下:每當收到新資訊或者使用者傳送資訊到遠端端,Netty 3 均會建立一個新的堆緩衝區。這意味著,對應每一個新的緩衝區,都會有一個 new byte[capacity]。這些緩衝區會導致 GC 壓力,並消耗記憶體頻寬。為了安全起見,新的位元組陣列分配時會用零填充,這會消耗記憶體頻寬。然而,用零填充的陣列很可能會再次用實際的資料填充,這又會消耗同樣的記憶體頻寬。如果 Java 虛擬機器(JVM)提供了建立新位元組陣列而又無需用零填充的方式,那麼我們本來就可以將記憶體頻寬消耗減少 50%,但是目前沒有那樣一種方式。

在 Netty 4 中實現了一個新的 ByteBuf 記憶體池,它是一個純 Java 版本的 jemalloc (Facebook 也在用)。現在,Netty 不會再因為用零填充緩衝區而浪費記憶體頻寬了。不過,由於它不依賴於 GC,開發人員需要小心記憶體洩漏。如果忘記在處理程式中釋放緩衝區,那麼記憶體使用率會無限地增長。

Netty 預設不使用記憶體池,需要在建立客戶端或者服務端的時候進行指定,程式碼如下:

Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .option(ChannelOption.TCP_NODELAY, true)
             .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)

使用記憶體池之後,記憶體的申請和釋放必須成對出現,即 retain() 和 release() 要成對出現,否則會導致記憶體洩露。

值得注意的是,如果使用記憶體池,完成 ByteBuf 的解碼工作之後必須顯式的呼叫 ReferenceCountUtil.release(msg) 對接收緩衝區 ByteBuf 進行記憶體釋放,否則它會被認為仍然在使用中,這樣會導致記憶體洩露。

3.6. 當心“日誌隱形殺手”

通常情況下,大家都知道不能在 Netty 的 I/O 執行緒上做執行時間不可控的操作,例如訪問資料庫、傳送 Email 等。但是有個常用但是非常危險的操作卻容易被忽略,那便是記錄日誌。

通常,在生產環境中,需要實時列印介面日誌,其它日誌處於 ERROR 級別,當推送服務發生 I/O 異常之後,會記錄異常日誌。如果當前磁碟的 WIO 比較高,可能會發生寫日誌檔案操作被同步阻塞,阻塞時間無法預測。這就會導致 Netty 的 NioEventLoop 執行緒被阻塞,Socket 鏈路無法被及時關閉、其它的鏈路也無法進行讀寫操作等。

以最常用的 log4j 為例,儘管它支援非同步寫日誌(AsyncAppender),但是當日志佇列滿之後,它會同步阻塞業務執行緒,直到日誌佇列有空閒位置可用,相關程式碼如下:

 synchronized (this.buffer) {
      while (true) {
        int previousSize = this.buffer.size();
        if (previousSize < this.bufferSize) {
          this.buffer.add(event);
          if (previousSize != 0) break;
          this.buffer.notifyAll(); break;
        }
        boolean discard = true;
        if ((this.blocking) && (!Thread.interrupted()) && (Thread.currentThread() != this.dispatcher)) // 判斷是業務執行緒 
        {
          try
          {
            this.buffer.wait();// 阻塞業務執行緒 
            discard = false;
          }
          catch (InterruptedException e)
          {
            Thread.currentThread().interrupt();
          }

        }

類似這類 BUG 具有極強的隱蔽性,往往 WIO 高的時間持續非常短,或者是偶現的,在測試環境中很難模擬此類故障,問題定位難度非常大。這就要求讀者在平時寫程式碼的時候一定要當心,注意那些隱性地雷。

3.7. TCP 引數優化

常用的 TCP 引數,例如 TCP 層面的接收和傳送緩衝區大小設定,在 Netty 中分別對應 ChannelOption 的 SO_SNDBUF 和 SO_RCVBUF,需要根據推送訊息的大小,合理設定,對於海量長連線,通常 32K 是個不錯的選擇。

另外一個比較常用的優化手段就是軟中斷,如圖所示:如果所有的軟中斷都執行在 CPU0 相應網路卡的硬體中斷上,那麼始終都是 cpu0 在處理軟中斷,而此時其它 CPU 資源就被浪費了,因為無法並行的執行多個軟中斷。

圖 3-3 中斷資訊

大於等於 2.6.35 版本的 Linux kernel 核心,開啟 RPS,網路通訊效能提升 20% 之上。RPS 的基本原理:根據資料包的源地址,目的地址以及目的和源埠,計算出一個 hash 值,然後根據這個 hash 值來選擇軟中斷執行的 cpu。從上層來看,也就是說將每個連線和 cpu 繫結,並通過這個 hash 值,來均衡軟中斷執行在多個 cpu 上,從而提升通訊效能。

3.8. JVM 引數

最重要的引數調整有兩個:

  • -Xmx:JVM 最大記憶體需要根據記憶體模型進行計算並得出相對合理的值;
  • GC 相關的引數: 例如新生代和老生代、永久代的比例,GC 的策略,新生代各區的比例等,需要根據具體的場景進行設定和測試,並不斷的優化,儘量將 Full GC 的頻率降到最低。

4. 作者簡介

李林鋒,2007 年畢業於東北大學,2008 年進入華為公司從事高效能通訊軟體的設計和開發工作,有 6 年 NIO 設計和開發經驗,精通 Netty、Mina 等 NIO 框架。Netty 中國社群創始人,《Netty 權威指南》作者。

聯絡方式:新浪微博 Nettying 微信:Nettying

相關文章