單機下能不能讓我們的網絡應用支持百萬連接?可以,但是有很多的工作要做。而且要考慮到單機的系統資源消耗能否支撐百萬并發
一、操作系統優化
首先就是要突破操作系統的限制。
在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-max98566
這表明這臺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、心跳優化
針對海量設備接入的服務端,心跳優化策略如下。
- 要能夠及時檢測失效的連接,并將其剔除,防止無效的連接句柄積壓,導致OOM等問題
- 設置合理的心跳周期,防止心跳定時任務積壓,造成頻繁的老年代GC(新生代和老年代都有導致STW的GC,不過耗時差異較大),導致應用暫停
- 使用Nety提供的鏈路空閑檢測機制,不要自己創建定時任務線程池,加重系統的負擔,以及增加潛在的并發安全問題。
當設備突然掉電、連接被防火墻擋住、長時間GC或者通信線程發生非預期異常時,會導致鏈路不可用且不易被及時發現。特別是如果異常發生在凌晨業務低谷期間,當早晨業務高峰期到來時,由于鏈路不可用會導致瞬間大批量業務失敗或者超時,這將對系統的可靠性產生重大的威脅。
從技術層面看,要解決鏈路的可靠性問題,必須周期性地對鏈路進行有效性檢測。目前最流行和通用的做法就是心跳檢測。心跳檢測機制分為三個層面
- TCP層的心跳檢測,即TCP的 Keep-Alive機制,它的作用域是整個TCP協議棧。
- 協議層的心跳檢測,主要存在于長連接協議中,例如MQTT。
- 應用層的心跳檢測,它主要由各業務產品通過約定方式定時給對方發送心跳消息實現。
心跳檢測的目的就是確認當前鏈路是否可用,對方是否活著并且能夠正常接收和發送消息。作為高可靠的NIO框架,Nety也提供了心跳檢測機制。
一般的心跳檢測策略如下。
- 連續N次心跳檢測都沒有收到對方的Pong應答消息或者Ping請求消息,則認為鏈路已經發生邏輯失效,這被稱為心跳超時。
- 在讀取和發送心跳消息的時候如果直接發生了IO異常,說明鏈路已經失效,這被稱為心跳失敗。無論發生心跳超時還是心跳失敗,都需要關閉鏈路,由客戶端發起重連操作,保證鏈路能夠恢復正常。
Nety提供了三種鏈路空閑檢測機制,利用該機制可以輕松地實現心跳檢測
- 讀空閑,鏈路持續時間T沒有讀取到任何消息。
- 寫空閑,鏈路持續時間T沒有發送任何消息
- 讀寫空閑,鏈路持續時間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(垃圾收集)有三個主要指標。
- 吞吐量:是評價GC能力的重要指標,在不考慮GC引起的停頓時間或內存消耗時,吞吐量是GC能支撐應用程序達到的最高性能指標。
- 延遲:GC能力的最重要指標之一,是由于GC引起的停頓時間,優化目標是縮短延遲時間或完全消除停頓(STW),避免應用程序在運行過程中發生抖動。
- 內存占用:GC正常時占用的內存量。
JVM GC調優的三個基本原則如下。
- Minor go回收原則:每次新生代GC回收盡可能多的內存,減少應用程序發生Full gc的頻率。
- GC內存最大化原則:垃圾收集器能夠使用的內存越大,垃圾收集效率越高,應用程序運行也越流暢。但是過大的內存一次 Full go耗時可能較長,如果能夠有效避免FullGC,就需要做精細化調優。
- 3選2原則:吞吐量、延遲和內存占用不能兼得,無法同時做到吞吐量和暫停時間都最優,需要根據業務場景做選擇。對于大多數應用,吞吐量優先,其次是延遲。當然對于時延敏感型的業務,需要調整次序。
2、確定服務端內存占用
在優化GC之前,需要確定應用程序的內存占用大小,以便為應用程序設置合適的內存,提升GC效率。內存占用與活躍數據有關,活躍數據指的是應用程序穩定運行時長時間存活的JAVA對象。活躍數據的計算方式:通過GC日志采集GC數據,獲取應用程序穩定時老年代占用的Java堆大小,以及永久代(元數據區)占用的Java堆大小,兩者之和就是活躍數據的內存占用大小。
3、GC優化過程
- GC數據的采集和研讀
- 設置合適的JVM堆大小
- 選擇合適的垃圾回收器和回收策略
GC調優會是一個需要多次調整的過程,期間不僅有參數的變化,更重要的是需要調整業務代碼。
作者:Dark_King_
原文鏈接:https://blog.csdn.net/b379685397/article/details/104042536