[原創]百萬長連接壓測問題排查分析


一、背景:
  基於WebSocket長連接的消息服務進行全鏈路壓測,目標是實現最少100W長連接下壓測服務的各個接口TPS,QPS及其穩定性和資源消耗情況。
 
二、全鏈路架構圖:
  

三、遇到的問題總結:
  問題一:Jmeter肉雞連接達到1w左右時,出現OOM。
  問題二:心跳超時導致連接斷開。
  問題三:達到50w並發時,出現連接大批量掉線問題。
  問題四:達到72w並發時,出現連接數上不去的問題。
  問題五:達到100w並發穩定建立並保持時,出現發送數據掉線問題,此時Nginx OOM。
 
  其中肉雞的內核參數設置如下:
  
# cat >> /etc/sysctl.conf << EOF
net.ipv4.tcp_max_tw_buckets = 200000
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 11000   61000
fs.file-max = 1000000
net.ipv4.ip_conntrack_max = 2000000
net.ipv4.netfilter.ip_conntrack_max = 2000000
net.nf_conntrack_max = 2000000
net.netfilter.nf_conntrack_max = 2000000
net.ipv4.tcp_max_orphans = 500000
net.ipv4.tcp_mem = 786432 2097152 3145728
net.ipv4.tcp_rmem = 4096 4096 16777216
net.ipv4.tcp_wmem = 4096 4096 16777216
EOF
# sysctl -p

//設置文件句柄數,其實不需要設置100w這么大,根據肉雞的連接數設置合理即可
# sed -i 's/65535/1000000/g' /etc/security/limits.conf
 
四、壓測過程問題排查分析:
  在搭建,調試好全鏈路壓測環境后啟動一台Jmeter肉雞進行測試,發現當肉雞連接數達到1w時出現OOM。報錯如下:
  此時的jmeter啟動參數如下: 
# cd /root/apache-jmeter-5.1.1/bin/ && HEAP="-Xms15g -Xmx15g" ./jmeter-server -Djava.rmi.server.hostname=xxx.xxx.xxx.xxx -Jserver.rmi.ssl.disable=true &> /tmp/jmeter.log &
  發現jvm設置的內存很大,有15g,百度谷歌一番,得知:
  於是,將jmeter的jvm設置成4g,如下:
# cd /root/apache-jmeter-5.1.1/bin/ && HEAP="-Xms4g -Xmx4g" ./jmeter-server -Djava.rmi.server.hostname=xxx.xxx.xxx.xxx -Jserver.rmi.ssl.disable=true &> /tmp/jmeter.log &
  調整之后單台jmeter肉雞連接數能達到2w並且內存還很充足。后續所有肉雞都是用此參數啟動進程。到此,開始進行壓測。
開始壓測50w的並發建連,建立連接后3分鍾左右出現斷線,進行分析是因為在沒有數據發送的情況下,Nginx配置了180s的超時時間。超過180s后主動斷掉連接。通過和開發溝通,將proxy_connect_timeout,proxy_send_timeout和proxy_read_timeout都設置為900s。如下:
  
  reload nginx生效后問題解決。
  繼續壓測,使用50台肉雞,每台啟動1w線程建連。在連接數達到50w保持心跳連接時,開始發送數據出現大批量掉線(發送的數據會造成使得在同一房間的連接都會收到消息,即:廣播)。
  首先,使用一下命令查看一層Nginx和Ingres的連接狀況:
  
# netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
  發現Nginx和Ingress都出現了大量TIME_WAIT,說明連接是代理層主動斷開的(主動斷開連接的一方會進入TIME_WAIT狀態),此時查看Nginx和Ingress日志並沒有發現任何的報錯。詢問消費服務網關開發同學是否有報錯日志,開發同學反饋是客戶端主動斷開了連接,但是沒有更加具體的報錯。查看肉雞Jmeter的日志,有如下報錯:
  
  這是jmeter的第三方websocket的jar報出來的錯,也是顯示連接不可用。日志都沒有具體的問題,那么到底是什么原因導致連接被斷掉呢?開始在整條鏈路上抓包分析,每一個節點都抓取上下游的包,抓包命令如下:
  
# tcpdump -i any host xxx.xxx.xxx.xxx -v -w client.pcap
//tcpdump抓取的報文通常會很大,可以使用wireshark自動的editcap和mergecap工具根據時間來分隔和合並報文
  分析報文,發現肉雞發送大量的窗口滿的報文,由Nginx和Ingress代理到后端服務網關。如下是Ingress到服務網關的報文:
  Ingress發送TCP ZeroWindow的報文,最后會RST連接。那么肉雞為什么會發送窗口滿的報文呢?查看全鏈路的帶寬情況:
  查看監控發現網絡和帶寬都是沒有問題的。於是將重點指向肉雞,初步懷疑是肉雞的websocket jar包在處理網絡數據的機制上有問題。經過一番搜索,發現如下:
  在jmeter-websocket-samplers-1.2.2.jar的官網搜索到作者的最新版本說解決了該問題,於是替換jmeter-websocket-samplers-1.2.2.jar包為最新的JMeterWebSocketSamplers-1.2.6.jar版本。實測無效,問題依舊。再次重點分析肉雞,查看監控,發現肉雞在出現掉線的時候負載很高,load average高達200+。判斷是肉雞負載過高,處理不過來導致tcp滑動窗口滿,最終斷開連接的問題。
  於是,增加肉雞到100台,每台肉雞還是開啟1w線程,看只建立100w連接不發送數據的情況下是否穩定。發現在連接數達到72w左右時連接數上不去了。於是,分析全鏈路能支持的並發數。要計算全鏈路支持的並發數需要了解以下知識點:
  
TCP連接知識點: 
1.一個TCP連接的套接字對(socket pari)是一個定義該連接的兩個端點的四元組,即本地IP地址、本地TCP端口號、外地IP地址、外地TCP端口號。套接字對唯一標識一個網絡上的每個TCP連接。 
2.linux socket使用16bit無符號整型表示端口號,最大到65535。也就是說一台客戶端的機器上的一個IP對應有65535個端口號可以用於對服務端建立TCP連接,而服務器的服務端口號一般是啟用端口復用的,
  也就是一個服務端口可以支持多個TCP連接,epoll模式理論上支持的連接數沒有上限。 
3.使用nginx作為反向代理時,nginx即是服務端,又是客戶端。作為服務端,服務的端口號對客戶端是復用的,然后作為客戶端使用本機的其他1024~65535端口號和后端的服務器建立連接實現代理。
  這樣,一個TCP連接在反向代理的nginx機器上表現為有兩個TCP連接,即占用兩個socket文件句柄數。 
4.計算nginx或者ingress支持的TCP連接數計算方法,以nginx為例,根據tcp連接四元組可知:Nginx的IP數 * Nginx開啟的隨機端口數 * Ingress的IP數 * Ingress服務端口數 = 1 * 65535 * 1 * 1 = 65535 正常情況下理論上是支持65535個TCP連接的,
  但是隨機端口數0~1024一般作為服務端口被占用,所以需要去除掉一些常用的端口,並預留一部分端口。所以開啟10240~65000大概5.5w個端口數。 
5.在充分利用機器資源的情況下,支持50w+的TCP連接數的瓶頸:
  第一,壓測到Nginx服務端,瓶頸在於增加壓測機的數量;
  第二,Nginx到Ingress,增加Ingress服務端口數,開啟多個服務加到Nginx的upstream中來擴充四元組中的Ingress服務端口數;
  第三,Ingress到服務端,增加服務端的pod數加到Ingress的upstream來擴充四元組的服務端端口數。所以需要關注這三個點的TCP連接數的支持情況。
    所以解決nginx端口耗盡的問題可以在nginx上增加upstream數量,upstream可以是不同的ip+port,也可以是同一個ip下的不同port,還有就是可以在nginx主機上增加IP地址,然后使用nginx的proxy_bind指定源地址。
  於是查看一層Nginx的/proc/sys/net/ipv4/ip_local_port_range的值,設置為21000-61000,端口數為4w,后端后5個Ingress,也就是每個Nginx能支持20w的連接,一共4個Nginx,也就是:4*20w=80w。排除其他和壓測無關的連接后,和72w相差不大,於是調整改參數為:1024-65530,理論上估算能支持:4*5*6w=120w並發連接。但是,Nginx的連接數還取決於worker_rlimit_nofile和worker_connections兩個參數,如下:
  
  其中worker_rlimit_nofile是文件句柄數,設置該值會覆蓋系統的/etc/security/limits.conf的最大文件數。可以通過查看nginx進程的限制來驗證:
  並且由於worker_connections這個參數會在Nginx啟動時預先分配內存,所以這個值並不是設置的越大越好,應該根據實際場景來設置大小。可以通過調整改值后重啟Nginx時通過# free -m 查看nginx的初始占用內存大小來驗證。在32個woker下,改值設置10w時,初始化內存大概為3G;設置100w時,初始化內存大概14G。
  優化完參數后重啟Nginx,並發數能穩定支持100w。
  繼續壓測,當連接穩定在100w時開始發送數據,出現Nginx內存飆升,最后頻繁OOM,伴隨着TCP重傳率高達40%-50%。報錯和監控如下(原本Nginx是64G內存,后因為該問題升級到128G內存后問題依舊):
  
[Fri Mar 13 18:46:44 2020] Out of memory: Kill process 28258 (nginx) score 30 or sacrifice child
[Fri Mar 13 18:46:44 2020] Killed process 28258 (nginx) total-vm:1092198764kB, anon-rss:3943668kB, file-rss:736kB, shmem-rss:4kB
 
  此時,再次全鏈路抓包,查看服務器負載和帶寬情況(說明系統監控的重要性,我們使用的是Grafana+Prometheus+Alertmanager+node_exporter監控棧)。
在jmeter客戶端抓到的包可以看到有較多的零窗口,如下所示:
  
  此時查看Nginx和肉雞兩端的網絡連接狀態,使用 # ss -tn 命令可以看到大量 ESTABLISHED 狀態連接的 Send-Q 堆積很大,客戶端的 Recv-Q 堆積很大。Nginx 端的 ss 部分輸出如下所示:
  
  並使用# dstat 命令查看系統性能狀態:
  
  可以看到,最后兩列中系統CPU中斷和上下文切換開銷都很大。系統負載高。
  此時,定位到是jmeter肉雞處理能力有限,有較多的消息堆積在中轉的Nginx中,導致Nginx內存不斷飆升直到OOM。於是,增加肉雞到200台,每台肉雞線程數從1w降到5000。此時發現,壓測能正常進行,但是Nginx內存仍然在上升,只是對比之前上升的稍微緩慢一些。再次抓包分析,肉雞還是偶爾出現零窗口。於是想到,Nginx是否可以不緩存消息?通過分析Nginx的配置參數,發現proxy_buffers這個值設置很大,如下:
  查看官網相關配置項,關閉proxy_buffering,調小proxy_buffer_size 和 proxy_buffers,注釋proxy_busy_buffers_size。如下:
  
proxy_buffering off;
proxy_buffer_size 4k;
proxy_buffers 4 8k;
#proxy_busy_buffers_size 256M;
  經過實測,在壓測環境修改了這個值以后,以及調小了 proxy_buffer_size 的值以后,內存穩定在了 20G 左右,沒有再飆升過。后面可以開啟 proxy_buffering,調整 proxy_buffers 的大小可以在內存消耗和性能方面取得更好的平衡。
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM