Netty實戰:如何讓單機下Netty支持百萬長連接?

單機下能不能讓我們的網絡應用支持百萬連接?可以,但是有很多的工作要做。而且要考慮到單機的系統資源消耗能否支撐百萬併發

一、操作系統優化

首先就是要突破操作系統的限制。

在Linux平臺上,無論編寫客戶端程序還是服務端程序,在進行高併發TCP連接處理時,最高的併發數量都要受到系統對用戶單一進程同時可打開文件數量的限制(這是因爲系統爲每個TCP連接都要創建一個socket句柄,每個socket句柄同時也是一個文件句柄)。

可使用ulimit命令查看系統允許當前用戶進程打開的文件數限制:$ ulimit -n 1024

這表示當前用戶的每個進程最多允許同時打開1024個文件,這1024個文件中還得除去每個進程必然打開的標準輸入,標準輸出,標準錯誤,服務器監聽 socket,進程間通訊的unix域socket等文件,那麼剩下的可用於客戶端socket連接的文件數就只有大概1024-10=1014個左右。也就是說缺省情況下,基於Linux的通訊程序最多允許同時1014個TCP併發連接。

對於想支持更高數量的TCP併發連接的通訊處理程序,就必須修改Linux對當前用戶的進程同時打開的文件數量。

修改單個進程打開最大文件數限制的最簡單的辦法就是使用ulimit命令:$ ulimit –n 1000000

如果系統回顯類似於"Operation not permitted"之類的話,說明上述限制修改失敗,實際上是因爲在中指定的數值超過了Linux系統對該用戶打開文件數的軟限制或硬限制。因此,就需要修改Linux系統對用戶的關於打開文件數的軟限制和硬限制。

軟限制(soft limit):是指Linux在當前系統能夠承受的範圍內進一步限制用戶同時打開的文件數;

硬限制(hardlimit):是根據系統硬件資源狀況(主要是系統內存)計算出來的系統最多可同時打開的文件數量。

第一步,修改/etc/security/limits.conf文件,在文件中添加如下行:


* soft nofile 1000000

 * hard nofile 1000000

'*'號表示修改所有用戶的限制;

soft或hard指定要修改軟限制還是硬限制;1000000則指定了想要修改的新的限制值,即最大打開文件數(請注意軟限制值要小於或等於硬限制)。修改完後保存文件。

第二步,修改/etc/pam.d/login文件,在文件中添加如下行:


session required /lib/security/pam_limits.so

這是告訴Linux在用戶完成系統登錄後,應該調用pam_limits.so模塊來設置系統對該用戶可使用的各種資源數量的最大限制(包括用戶可打開的最大文件數限制),而pam_limits.so模塊就會從/etc/security/limits.conf文件中讀取配置來設置這些限制值。修改完後保存此文件。

第三步,查看Linux系統級的最大打開文件數限制,使用如下命令:


[root@VM_0_15_centos ~]# cat /proc/sys/fs/file-max

98566

這表明這臺Linux系統最多允許同時打開(即包含所有用戶打開文件數總和)98566個文件,是Linux系統級硬限制,所有用戶級的打開文件數限制都不應超過這個數值。通常這個系統級硬限制是Linux系統在啓動時根據系統硬件資源狀況計算出來的最佳的最大同時打開文件數限制,如果沒有特殊需要,不應該修改此限制,除非想爲用戶級打開文件數限制設置超過此限制的值。

如何修改這個系統最大文件描述符的限制呢?修改sysctl.conf文件


vi /etc/sysctl.conf
 # 在末尾添加

fs.file_max = 1000000
 # 立即生效

sysctl -p 

二、Netty調優

1、設置合理的線程數

對於線程池的調優,主要集中在用於接收海量設備TCP連接、TLS握手的 Acceptor線程池( Netty通常叫 boss NioEventLoop Group)上,以及用於處理網絡數據讀寫、心跳發送的1O工作線程池(Nety通常叫 work Nio EventLoop Group)上。

對於Nety服務端,通常只需要啓動一個監聽端口用於端側設備接入即可,但是如果服務端集羣實例比較少,甚至是單機(或者雙機冷備)部署,在端側設備在短時間內大量接入時,需要對服務端的監聽方式和線程模型做優化,以滿足短時間內(例如30s)百萬級的端側設備接入的需要。

服務端可以監聽多個端口,利用主從 Reactor線程模型做接入優化,前端通過SLB做4層門7層負載均衡。

主從 Reactor線程模型特點如下:服務端用於接收客戶端連接的不再是一個單獨的NO線程,而是一個獨立的NIO線程池; Acceptor接收到客戶端TCP連接請求並處理後(可能包含接入認證等),將新創建的 Socketchanne註冊到I/O線程池(subReactor線程池)的某個IO線程,由它負責 Socketchannel的讀寫和編解碼工作; Acceptor線程池僅用於客戶端的登錄、握手和安全認證等,一旦鏈路建立成功,就將鏈路註冊到後端 sub reactor線程池的IO線程,由IO線程負責後續的IO操作。

對於IO工作線程池的優化,可以先採用系統默認值(即CPU內核數×2)進行性能測試,在性能測試過程中採集IO線程的CPU佔用大小,看是否存在瓶頸對於O工作線程池的優化,可以先採用系統默認值(即CPU內核數×2)進行性能

測試,在性能測試過程中採集IO線程的CPU佔用大小,看是否存在瓶頸, 具體可以觀察線程堆棧,如果連續採集幾次進行對比,發現線程堆棧都停留在 Selectorlmpl. lock AndDoSelect,則說明IO線程比較空閒,無須對工作線程數做調整。

如果發現IO線程的熱點停留在讀或者寫操作,或者停留在 Channelhandler的執行處,則可以通過適當調大 Nio EventLoop線程的個數來提升網絡的讀寫性能。

2、心跳優化

針對海量設備接入的服務端,心跳優化策略如下。

  1. 要能夠及時檢測失效的連接,並將其剔除,防止無效的連接句柄積壓,導致OOM等問題

  2. 設置合理的心跳週期,防止心跳定時任務積壓,造成頻繁的老年代GC(新生代和老年代都有導致STW的GC,不過耗時差異較大),導致應用暫停

  3. 使用Nety提供的鏈路空閒檢測機制,不要自己創建定時任務線程池,加重系統的負擔,以及增加潛在的併發安全問題。

當設備突然掉電、連接被防火牆擋住、長時間GC或者通信線程發生非預期異常時,會導致鏈路不可用且不易被及時發現。特別是如果異常發生在凌晨業務低谷期間,當早晨業務高峯期到來時,由於鏈路不可用會導致瞬間大批量業務失敗或者超時,這將對系統的可靠性產生重大的威脅。

從技術層面看,要解決鏈路的可靠性問題,必須週期性地對鏈路進行有效性檢測。目前最流行和通用的做法就是心跳檢測。心跳檢測機制分爲三個層面

  1. TCP層的心跳檢測,即TCP的 Keep-Alive機制,它的作用域是整個TCP協議棧。

  2. 協議層的心跳檢測,主要存在於長連接協議中,例如MQTT。

  3. 應用層的心跳檢測,它主要由各業務產品通過約定方式定時給對方發送心跳消息實現。

心跳檢測的目的就是確認當前鏈路是否可用,對方是否活着並且能夠正常接收和發送消息。作爲高可靠的NIO框架,Nety也提供了心跳檢測機制。

一般的心跳檢測策略如下。

  1. 連續N次心跳檢測都沒有收到對方的Pong應答消息或者Ping請求消息,則認爲鏈路已經發生邏輯失效,這被稱爲心跳超時。

  2. 在讀取和發送心跳消息的時候如果直接發生了IO異常,說明鏈路已經失效,這被稱爲心跳失敗。無論發生心跳超時還是心跳失敗,都需要關閉鏈路,由客戶端發起重連操作,保證鏈路能夠恢復正常。

Nety提供了三種鏈路空閒檢測機制,利用該機制可以輕鬆地實現心跳檢測

  1. 讀空閒,鏈路持續時間T沒有讀取到任何消息。

  2. 寫空閒,鏈路持續時間T沒有發送任何消息

  3. 讀寫空閒,鏈路持續時間T沒有接收或者發送任何消息

對於百萬級的服務器,一般不建議很長的心跳週期和超時時長

3、接收和發送緩衝區調優

在一些場景下,端側設備會週期性地上報數據和發送心跳,單個鏈路的消息收發量並不大,針對此類場景,可以通過調小TCP的接收和發送緩衝區來降低單個TCP連接的資源佔用率

當然對於不同的應用場景,收發緩衝區的最優值可能不同,用戶需要根據實際場景,結合性能測試數據進行鍼對性的調優

4、合理使用內存池

隨着JVM虛擬機和JT即時編譯技術的發展,對象的分配和回收是一個非常輕量級的工作。但是對於緩衝區 Buffer,情況卻稍有不同,特別是堆外直接內存的分配和回收,是一個耗時的操作。

爲了儘量重用緩衝區,Nety提供了基於內存池的緩衝區重用機制。

在百萬級的情況下,需要爲每個接入的端側設備至少分配一個接收和發送緩衝區對象,採用傳統的非池模式,每次消息讀寫都需要創建和釋放 ByteBuf對象,如果有100萬個連接,每秒上報一次數據或者心跳,就會有100萬次/秒的 ByteBuf對象申請和釋放,即便服務端的內存可以滿足要求,GC的壓力也會非常大。

以上問題最有效的解決方法就是使用內存池,每個 NioEventLoop線程處理N個鏈路,在線程內部,鏈路的處理是串行的。假如A鏈路首先被處理,它會創建接收緩衝區等對象,待解碼完成,構造的POJO對象被封裝成任務後投遞到後臺的線程池中執行,然後接收緩衝區會被釋放,每條消息的接收和處理都會重複接收緩衝區的創建和釋放。如果使用內存池,則當A鏈路接收到新的數據報時,從 NioEventLoop的內存池中申請空閒的 ByteBuf,解碼後調用 release將 ByteBuf釋放到內存池中,供後續的B鏈路使用。

Nety內存池從實現上可以分爲兩類:堆外直接內存和堆內存。由於 Byte Buf主要用於網絡IO讀寫,因此採用堆外直接內存會減少一次從用戶堆內存到內核態的字節數組拷貝,所以性能更高。由於 DirectByteBuf的創建成本比較高,因此如果使用 DirectByteBuf,則需要配合內存池使用,否則性價比可能還不如 Heap Byte。

Netty默認的IO讀寫操作採用的都是內存池的堆外直接內存模式,如果用戶需要額外使用 ByteBuf,建議也採用內存池方式;如果不涉及網絡IO操作(只是純粹的內存操作),可以使用堆內存池,這樣內存的創建效率會更高一些。

5、IO線程和業務線程分離

如果服務端不做複雜的業務邏輯操作,僅是簡單的內存操作和消息轉發,則可以通過調大 NioEventLoop工作線程池的方式,直接在IO線程中執行業務 Channelhandler,這樣便減少了一次線程上下文切換,性能反而更高。

如果有複雜的業務邏輯操作,則建議IO線程和業務線程分離,對於IO線程,由於互相之間不存在鎖競爭,可以創建一個大的 NioEvent Loop Group線程組,所有 Channel都共享同一個線程池。

對於後端的業務線程池,則建議創建多個小的業務線程池,線程池可以與IO線程綁定,這樣既減少了鎖競爭,又提升了後端的處理性能。

針對端側併發連接數的流控

無論服務端的性能優化到多少,都需要考慮流控功能。當資源成爲瓶頸,或者遇到端側設備的大量接入,需要通過流控對系統做保護。流控的策略有很多種,比如針對端側連接數的流控:

在Nety中,可以非常方便地實現流控功能:新增一個FlowControlchannelhandler,然後添加到 ChannelPipeline靠前的位置,覆蓋 channelActiveO方法,創建TCP鏈路後,執行流控邏輯,如果達到流控閾值,則拒絕該連接,調用 ChannelHandler Context的 close(方法關閉連接。

三、JVM層面相關性能優化

當客戶端的併發連接數達到數十萬或者數百萬時,系統一個較小的抖動就會導致很嚴重的後果,例如服務端的GC,導致應用暫停(STW)的GC持續幾秒,就會導致海量的客戶端設備掉線或者消息積壓,一旦系統恢復,會有海量的設備接入或者海量的數據發送很可能瞬間就把服務端沖垮。

JVM層面的調優主要涉及GC參數優化,GC參數設置不當會導致頻繁GC,甚至OOM異常,對服務端的穩定運行產生重大影響。

1、確定GC優化目標

GC(垃圾收集)有三個主要指標。

  1. 吞吐量:是評價GC能力的重要指標,在不考慮GC引起的停頓時間或內存消耗時,吞吐量是GC能支撐應用程序達到的最高性能指標。

  2. 延遲:GC能力的最重要指標之一,是由於GC引起的停頓時間,優化目標是縮短延遲時間或完全消除停頓(STW),避免應用程序在運行過程中發生抖動。

  3. 內存佔用:GC正常時佔用的內存量。

JVM GC調優的三個基本原則如下。

  1. Minor go回收原則:每次新生代GC回收儘可能多的內存,減少應用程序發生Full gc的頻率。

  2. GC內存最大化原則:垃圾收集器能夠使用的內存越大,垃圾收集效率越高,應用程序運行也越流暢。但是過大的內存一次 Full go耗時可能較長,如果能夠有效避免FullGC,就需要做精細化調優。

  3. 3選2原則:吞吐量、延遲和內存佔用不能兼得,無法同時做到吞吐量和暫停時間都最優,需要根據業務場景做選擇。對於大多數應用,吞吐量優先,其次是延遲。當然對於時延敏感型的業務,需要調整次序。

2、確定服務端內存佔用

在優化GC之前,需要確定應用程序的內存佔用大小,以便爲應用程序設置合適的內存,提升GC效率。內存佔用與活躍數據有關,活躍數據指的是應用程序穩定運行時長時間存活的Java對象。活躍數據的計算方式:通過GC日誌採集GC數據,獲取應用程序穩定時老年代佔用的Java堆大小,以及永久代(元數據區)佔用的Java堆大小,兩者之和就是活躍數據的內存佔用大小。

3、GC優化過程

  1. GC數據的採集和研讀

  2. 設置合適的JVM堆大小

  3. 選擇合適的垃圾回收器和回收策略

GC調優會是一個需要多次調整的過程,期間不僅有參數的變化,更重要的是需要調整業務代碼。

作者:Dark_King_
原文鏈接:https://blog.csdn.net/b379685397/article/details/104042536

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章