一、背景:
基於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 的大小可以在內存消耗和性能方面取得更好的平衡。