這邊有個性能要求極高的api要上線,這個服務端是golang http模塊實現的。在上線之前我們理所當然的要做壓力測試。起初是 “小白同學” 起頭進行壓力測試,但當我看到那壓力測試的結果時,我也是逗樂了。 現象是,直接訪問Golang http api是每秒可以到3.5W的訪問, 為了理論承受更強的QPS,多開了幾個go http api進程端口,又在這前面加了層nginx負載均衡,結果往nginx壓測的結果是每秒才可以解決1.5w的訪問量。 這結果讓高級黑 “小白” 把nginx又給鄙視了。
該文章寫的有些亂,歡迎來噴 ! 另外文章后續不斷更新中,請到原文地址查看更新.
雖然哥平時開發任務很飽和,又因為帶幾個新人的原因,有點心累。 但哥還是抽出寶貴的時間來解決nginx在壓力測試下性能上不去的問題。 哈哈,這里肯定有人要打我了。 說實話,做運維雖然能時常碰一些負載均衡調度器,但由於很多時候配置都標准化了,新開一個業務線,把配置一scp,然后選擇性的修改域名及location就可以了,還真是沒遇到過這次的問題。
我們在尋找性能瓶頸的是時候,會頻繁的使用后面的工具進行監控,推薦大家使用tmux或者screen開啟多個終端監控,用top可以看到nginx及go api的cpu占用率,load值,run數,各個cpu核心的百分比,處理網絡的中斷。用dstat可以看到流量及上下文切換的測試。 ss + netstat 查看連接數。
首先是壓力測試的方法問題
以前做運維的時候,我們一般不會用簡單的ab來進行壓測,這樣會造成壓力源過熱的情況,正常的針對服務端測試的方法是,分布式壓力測試,一個主機壓測的結果很是不准,當然前提是 服務端的性能夠高,別尼瑪整個python django就用分布式壓測,隨便找個webbench,ab , boom這類的http壓測就可以了。
關於客戶端壓測過熱的情況有幾個元素,最主要的元素是端口占用情況。首先我們需要明確幾個點, 作為服務端只是消耗fd而已,但是客戶端是需要占用端口來發起請求。 如果你自己同時作為服務端和客戶端,會被受限於65535-1024的限制,1024內一般是常規的系統保留端口。 如果你按照65535-1024計算的話,你可以占用64511端口數,但如果你是自己壓力測試nginx,然后nginx又反向代理幾個golang http api。 那么這端口被嚴重的縮水了。 當你壓測的數目才6w以上,很明顯報錯,不想報錯,那么只能進行排隊阻塞,好讓客戶端完成該請求。
另外一點是nginx 配置問題。
這一點很重要,也是最基本的要求,如果nginx worker連接數過少的化,你的請求連接就算沒有被阻塞到backlog隊列外,nginx worker也會因為過載保護不會處理新的請求。nginx的最大連接數是worker num * worker_connections, 默認worker_connections是1024, 直接干到10w就可以了。
在我們配置調整之后,訪問的速度有明顯的提升,但還是沒有達到我們的預期。 接着通過lsof追了下進程,發現nginx 跟 后端創建了大量的連接。 這很明顯是沒有使用http1.1長連接導致的,使用tcpdump抓包分析了下,果然是http1.0短鏈接,雖然我們在sysctl內核里做了一些網絡tcp回收的優化,但那也趕不上壓力測試帶來的頻繁創建tcp的消耗。 果然在upstream加了keepalive。
# xiaorui.cc COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME python 538 ruifengyun 9u IPv4 595559383 0t0 TCP 58.215.141.194:46665->58.215.141.83:9001 (ESTABLISHED) test_dic4 7476 ruifengyun 5u IPv6 660251515 0t0 TCP *:9001 (LISTEN) test_dic4 7476 ruifengyun 10u IPv6 660870187 0t0 TCP localhost:9001->localhost:46679 (ESTABLISHED) test_dic4 7476 ruifengyun 13u IPv6 660870138 0t0 TCP localhost:9001->localhost:46608 (ESTABLISHED) test_dic4 7476 ruifengyun 14u IPv6 660870137 0t0 TCP localhost:9001->localhost:46607 (ESTABLISHED) test_dic4 7476 ruifengyun 22u IPv6 660870153 0t0 TCP localhost:9001->localhost:46632 (ESTABLISHED) test_dic4 7476 ruifengyun 23u IPv6 660870143 0t0 TCP localhost:9001->localhost:46618 (ESTABLISHED) test_dic4 7476 ruifengyun 27u IPv6 660870166 0t0 TCP localhost:9001->localhost:46654 (ESTABLISHED) test_dic4 7476 ruifengyun 73u IPv6 660870191 0t0 TCP localhost:9001->localhost:46685 (ESTABLISHED) test_dic4 7476 ruifengyun 85u IPv6 660870154 0t0 TCP localhost:9001->localhost:46633 (ESTABLISHED) test_dic4 7476 ruifengyun 87u IPv6 660870147 0t0 TCP localhost:9001->localhost:46625 (ESTABLISHED) ....
摘錄官方文檔的說明如下。該參數開啟與上游服務器之間的連接池,其數值為每個nginx worker可以保持的最大連接數,默認不設置,即nginx作為客戶端時keepalive未生效。
Activates cache of connections to upstream servers
The connections parameter sets the maximum number of idle keepalive connections to upstream servers that are retained in the cache per one worker process. When this number is exceeded, the least recently used connections are closed
# xiaorui.cc upstream http_backend { server 127.0.0.1:8080; keepalive 256; } server { ... location /http/ { proxy_pass http://http_backend; proxy_http_version 1.1; proxy_set_header Connection ""; ... } }
繼續進行壓力測試,返現這訪問量還是那樣,沒有什么提升,通過排除問題確認又是連接數大引起的,這長連接不生效呀。 以前我在線上也是這么調配的,應該沒問題。 最后通過nginx error log找到了原因。 這Nginx版本居然不支持keepalive 長連接,沒招,換個版本再次測試。
2016/06/24 16:34:12 [error] 15419#0: *9421660 connect() failed (111: Connection refused) while connecting to upstream, client: 10.1.1.58, server: , request: "GET / HTTP/1.0", upstream: "http://127.0.0.1:9001/", host: "10.1.1.63" 2016/06/24 16:34:12 [error] 15418#0: *9423639 connect() failed (111: Connection refused) while connecting to upstream, client: 10.1.1.58, server: , request: "GET / HTTP/1.0", upstream: "http://127.0.0.1:9004/", host: "10.1.1.63" 2016/06/24 16:34:12 [error] 15418#0: *9423639 no live upstreams while connecting to upstream, client: 10.1.1.58, server: , request: "GET / HTTP/1.0", upstream: "http://test_servers/", host: "10.1.1.63" 2016/06/24 16:34:12 [error] 15418#0: *9393899 connect() failed (111: Connection refused) while connecting to upstream, client: 10.1.1.58, server: , request: "GET / HTTP/1.0", upstream: "http://127.0.0.1:9004/", host: "10.1.1.63" 2016/06/24 16:58:13 [notice] 26449#26449: signal process started 2016/06/24 16:58:13 [emerg] 27280#0: unknown directive "keepalive" in /etc/nginx/conf.d/test_multi.conf:7 2016/06/24 17:02:18 [notice] 3141#3141: signal process started 2016/06/24 17:02:18 [emerg] 27280#0: unknown directive "keepalive" in /etc/nginx/conf.d/test_multi.conf:7 2016/06/24 17:02:44 [notice] 4079#4079: signal process started 2016/06/24 17:02:44 [emerg] 27280#0: unknown directive "keepalive" in /etc/nginx/conf.d/test_multi.conf:7
簡單描述下nginx upstream keepalive是個怎么一回事?
默認情況下 Nginx 訪問后端都是用的短連接(HTTP1.0),一個請求來了,Nginx 新開一個端口和后端建立連接,請求結束連接回收。
如過配置了http 1.1長連接,那么Nginx會以長連接保持后端的連接,如果並發請求超過了 keepalive 指定的最大連接數,Nginx 會啟動新的連接 來轉發請求,新連接在請求完畢后關閉,而且新建立的連接是長連接。
下圖是nginx upstream keepalive長連接的實現原理.
首先每個進程需要一個connection pool,里面都是長連接,多進程之間是不需要共享這個連接池的。 一旦與后端服務器建立連接,則在當前請求連接結束之后不會立即關閉連接,而是把用完的連接保存在一個keepalive connection pool里面,以后每次需要建立向后連接的時候,只需要從這個連接池里面找,如果找到合適的連接的話,就可以直接來用這個連接,不需要重新創建socket或者發起connect()。這樣既省下建立連接時在握手的時間消耗,又可以避免TCP連接的slow start。如果在keepalive連接池找不到合適的連接,那就按照原來的步驟重新建立連接。 我沒有看過nginx在連接池中查找可用連接的代碼,但是我自己寫過redis,mysqldb的連接池代碼,邏輯應該都是一樣的。誰用誰pop,用完了再push進去,這樣時間才O(1)。
如果你的連接池的數控制在128,但因為你要應對更多的並發請求,所以臨時又加了很多的連接,但這臨時的連接是短連接和長連接要看你的nginx版本,我這1.8是長連接,那他如何被收回,兩地保證,一點是他會主動去釋放,另一點是keepalive timeout的時間。
Golang的http模塊貌似對http spdy支持不怎么好, 要不然可以直接用淘寶的tengine upstream spdy的方式連接后端Server。 他的速度要比keepalive要好的多,畢竟省去了等待上次返回的結果的過程。