系統負載能力淺析
互聯網時代,高并發是一個老生常談的話題。無論對于一個web站點還是app應用,高峰時能承載的并發請求都是衡量一個系統性能的關鍵標志。像阿里雙十一頂住了上億的峰值請求、訂單也確實體現了阿里的技術水平(當然有錢也是一個原因)。
那么,何為系統負載能力?怎么衡量?相關因素有哪些?又如何優化呢?
一. 衡量指標
用什么來衡量一個系統的負載能力呢?有一個概念叫做每秒請求數(Requests per second),指的是每秒能夠成功處理請求的數目。比如說,你可以配置tomcat服務器的maxConnection為無限大,但是受限于服務器系統 或者硬件限制,很多請求是不會在一定的時間內得到響應的,這并不作為一個成功的請求,其中成功得到響應的請求數即為每秒請求數,反應出系統的負載能力。
通常的,對于一個系統,增加并發用戶數量時每秒請求數量也會增加。然而,我們最終會達到這樣一個點,此時并發用戶數量開始“壓倒”服務器。如果繼續 增加并發用戶數量,每秒請求數量開始下降,而反應時間則會增加。這個并發用戶數量開始“壓倒”服務器的臨界點非常重要,此時的并發用戶數量可以認為是當前 系統的最大負載能力。
二. 相關因素
一般的,和系統并發訪問量相關的幾個因素如下:
- 帶寬
- 硬件配置
- 系統配置
- 應用服務器配置
- 程序邏輯 </ul>
- cpu頻率/核數:cpu頻率關系著cpu的運算速度,核數則影響線程調度、資源分配的效率。
- 內存大小以及速度:內存越大,那么可以在內存中運行的數據也就越大,速度自然而然就快;內存的速度從原來的幾百hz到現在幾千hz,決定了數據讀取存儲的速度。
- 硬盤速度:傳統的硬盤是使用磁頭進行尋址的,io速度比較慢,使用了SSD的硬盤,其尋址速度大大較快。 </ul>
- 文件描述符數限制:Linux中所有東西都是文件,一個socket就對應著一個文件描述符,因此系統配置的最大打開文件數以及單個進程能夠打開的最大文件數就決定了socket的數目上限。
- 進程/線程數限制: 對于apache使用的prefork等多進程模式,其負載能力由進程數目所限制。對tomcat多線程模式則由線程數所限制。
- tcp內核參數:網絡應用的底層自然離不開tcp/ip,Linux內核有一些與此相關的配置也決定了系統的負載能力。 </ul>
-
系統最大打開文件描述符數:/proc/sys/fs/file-max中保存了這個數目,修改此值
臨時性 echo 1000000 > /proc/sys/fs/file-max 永久性:在/etc/sysctl.conf中設置 fs.file-max = 1000000
</li> -
進程最大打開文件描述符數:這個是配單個進程能夠打開的最大文件數目。可以通過ulimit -n查看/修改。如果想要永久修改,則需要修改/etc/security/limits.conf中的nofile。
</li> </ul>通過讀取/proc/sys/fs/file-nr可以看到當前使用的文件描述符總數。另外,對于文件描述符的配置,需要注意以下幾點:
- 所有進程打開的文件描述符數不能超過/proc/sys/fs/file-max
- 單個進程打開的文件描述符數不能超過user limit中nofile的soft limit
- nofile的soft limit不能超過其hard limit
- nofile的hard limit不能超過/proc/sys/fs/nr_open </ul>
- 進程數限制:ulimit -u可以查看/修改單個用戶能夠打開的最大進程數。/etc/security/limits.conf中的noproc則是系統的最大進程數。
-
線程數限制
- 可以通過/proc/sys/kernel/threads-max查看系統總共可以打開的最大線程數。
- 單個進程的最大線程數和PTHREAD_THREADS_MAX有關,此限制可以在/usr/include/bits/local_lim.h中查看,但是如果想要修改的話,需要重新編譯。
- 這里需要提到一點的是,Linux內核2.4的線程實現方式為linux threads,是輕量級進程,都會首先創建一個管理線程,線程數目的大小是受PTHREAD_THREADS_MAX影響的。但Linux2.6內核的 線程實現方式為NPTL,是一個改進的LWP實現,最大一個區別就是,線程公用進程的pid(tgid),線程數目大小只受制于資源。
- 線程數的大小還受線程棧大小的制約:使用ulimit -s可以查看/修改線程棧的大小,即每開啟一個新的線程需要分配給此線程的一部分內存。減小此值可以增加可以打開的線程數目。 </ul> </li> </ul>
2.3.3 tcp內核參數
在一臺服務器CPU和內存資源額定有限的情況下,最大的壓榨服務器的性能,是最終的目的。在節省成本的情況下,可以考慮修改Linux的內核 TCP/IP參數,來最大的壓榨服務器的性能。如果通過修改內核參數也無法解決的負載問題,也只能考慮升級服務器了,這是硬件所限,沒有辦法的事。
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
使用上面的命令,可以得到當前系統的各個狀態的網絡連接的數目。如下:
LAST_ACK 14 SYN_RECV 348 ESTABLISHED 70 FIN_WAIT1 229 FIN_WAIT2 30 CLOSING 33 TIME_WAIT 18122
這里,TIME_WAIT的連接數是需要注意的一點。此值過高會占用大量連接,影響系統的負載能力。需要調整參數,以盡快的釋放time_wait連接。
一般tcp相關的內核參數在/etc/sysctl.conf文件中。為了能夠盡快釋放time_wait狀態的連接,可以做以下配置:
- net.ipv4.tcp_syncookies = 1 //表示開啟SYN Cookies。當出現SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉;
- net.ipv4.tcp_tw_reuse = 1 //表示開啟重用。允許將TIME-WAIT sockets重新用于新的TCP連接,默認為0,表示關閉;
- net.ipv4.tcp_tw_recycle = 1 //表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認為0,表示關閉;
- net.ipv4.tcp_fin_timeout = 30 //修改系統默認的 TIMEOUT 時間。 </ul>
- net.ipv4.tcp_keepalive_time = 1200 //表示當keepalive起用的時候,TCP發送keepalive消息的頻度。缺省是2小時,改為20分鐘。
- net.ipv4.ip_local_port_range = 10000 65000 //表示用于向外連接的端口范圍。缺省情況下很小:32768到61000,改為10000到65000。(注意:這里不要將最低值設的太低,否則可能會占用掉正常的端口!)
- net.ipv4.tcp_max_syn_backlog = 8192 //表示SYN隊列的長度,默認為1024,加大隊列長度為8192,可以容納更多等待連接的網絡連接數。
- net.ipv4.tcp_max_tw_buckets = 5000 //表示系統同時保持TIME_WAIT的最大數量,如果超過這個數字,TIME_WAIT將立刻被清除并打印警告信息。默認為180000,改為 5000。對于Apache、Nginx等服務器,上幾行的參數可以很好地減少TIME_WAIT套接字數量,但是對于Squid,效果卻不大。此項參數 可以控制TIME_WAIT的最大數量,避免Squid服務器被大量的TIME_WAIT拖死。 </ul>
- multi process:多進程方式,一個進程處理一個請求。
- prefork:類似于多進程的方式,但是會預先fork出一些進程供后續使用,是一種進程池的理念。
- worker:一個線程對應一個請求,相比多進程的方式,消耗資源變少,但同時一個線程的崩潰會引起整個進程的崩潰,穩定性不如多進程。
- master/worker:采用的是非阻塞IO的方式,只有兩種進程:worker和master,master負責worker進程的創建、 管理等,worker進程采用基于事件驅動的多路復用IO處理請求。mater進程只需要一個,woker進程根據cpu核數設置數目。 </ul>
- worker數目要和cpu(核)的數目相適應
- keepalive timout要設置適當
- worker_rlimit_nofile最大文件描述符要增大
- upstream可以使用http 1.1的keepalive </ul>
-
jvm參數配置:
- 堆的最小值:Xms
- 堆的最大值:Xmx
- 新生代大小: Xmn
- 永久代大小: XX:PermSize:
- 永久代最大大小: XX:MaxPermSize:
- 棧大小:-Xss或-XX:ThreadStackSize </ul>
-
connector參數配置
- protocol: 有三個選項:bio;nio;apr。建議使用apr選項,性能為最高。
- connectionTimeout:連接的超時時間
- maxThreads:最大線程數,此值限制了bio的最大連接數
- minSpareThreads: 最大空閑線程數
- acceptCount:可以接受的最大請求數目(未能得到處理的請求排隊)
- maxConnection: 使用nio或者apr時,最大連接數受此值影響。 </ul>
- 一旦發生full gc,那么會非常耗時
- 一旦gc,dump出的堆快照太大,無法分析 </ul>
- 可以根據系統的負載調整tc的數量,以達到資源的最大利用率,
- 可以防止單點故障。 </ul>
- 垂直分表:在列維度的拆分
- 水平分表:行維度的拆分 </ul>
典型配置可見:https://github.com/superhj1987/awesome-config/blob/master/tomcat/connector.conf
一般的當一個進程有500個線程在跑的話,那性能已經是很低很低了。Tomcat默認配置的最大請求數是150。當某個應用擁有250個以上并發的時候,應考慮應用服務器的集群。
另外,并非是無限調大maxTreads和maxConnection就能無限調高并發能力的。線程越多,那么cpu花費在線程調度上的時間越多,同時,內存消耗也就越大,那么就極大影響處理用戶的請求。受限于硬件資源,并發值是需要設置合適的值的。
</li> </ul>對于tomcat這里有一個爭論就是:使用大內存tomcat好還是多個小的tomcat集群好?(針對64位服務器以及tomcat來說)
其實,這個要根據業務場景區別對待的。通常,大內存tomcat有以下問題:
因此,如果可以保證一定程度上程序的對象大部分都是朝生夕死的,老年代不會發生gc,那么使用大內存tomcat也是可以的。但是在伸縮性和高可用卻比不上使用小內存(相對來說)tomcat集群。
使用小內存tomcat集群則有以下優勢:
2.4.3 數據庫
mysql是目前最常用的關系型數據庫,支持復雜的查詢。但是其負載能力一般。很多時候一個系統的瓶頸就發生在mysql這一點,當然有時候也和sql語句的效率有關。比如,牽扯到聯表的查詢一般說來效率是不會太高的。
當數據量單表突破千萬甚至百萬時(和具體的數據有關),查詢和插入效率都會受到影響。此時,需要對mysql數據庫進行優化,一種常見的方案就是分表:
對于系統中并發很高并且訪問很頻繁的數據,會放到緩存數據庫中,以隔離對mysql的訪問,防止mysql崩潰。
redis是目前用的比較多的緩存數據庫(當然,也有直接把redis當做數據庫使用的)。
此外,對于數據庫,可以使用讀寫分離的方式提高性能,尤其是對那種讀頻率遠大于寫頻率的業務場景。這里采用master/slave的方式實現讀寫分離,前面用程序控制或者加一個proxy層。
三. 一般架構
一般的web應用架構如下圖所示:lvs+nginx+tomcat+mysql+redis
本文對LVS沒有做相關說明,后續會補充進來。
</div> 來自: http://www.rowkey.me/blog/2015/09/09/load-analysis/
這里對于棧大小有一點需要注意的是:在Linux x64上ThreadStackSize的默認值就是1024KB,給Java線程創建棧會用這個參數指定的大小。如果把-Xss或者 -XX:ThreadStackSize設為0,就是使用“系統默認值”。而在Linux x64上HotSpot VM給Java棧定義的“系統默認”大小也是1MB。所以普通Java線程的默認棧大小怎樣都是1MB。這里有一個需要注意的地方就是java的棧大小和 之前提到過的操作系統的操作系統棧大小(ulimit -s):這個配置只影響進程的初始線程;后續用pthread_create創建的線程都可以指定棧大小。HotSpot VM為了能精確控制Java線程的棧大小,特意不使用進程的初始線程(primordial thread)作為Java線程。
其他還要根據業務場景,選擇使用那種垃圾回收器,回收的策略。另外,當需要保留GC信息時,也需要做一些設置。
典型配置可見:https://github.com/superhj1987/awesome-config/blob/master/tomcat/java_opts.conf
</li>
這里需要注意的一點就是當打開了tcp_tw_reccycle,就會檢查時間戳,移動環境下的發來的包的時間戳有些時候是亂跳的,會把帶了“倒退”的時間戳的包當作是“recycle的tw連接的重傳數據,不是新的請求”,于是丟掉不回包,造成大量丟包。
此外,還可以通過優化tcp/ip的可使用端口的范圍,進一步提升負載能力。,如下:
2.4 應用服務器配置
說到應用服務器配置,這里需要提到應用服務器的幾種工作模式,也叫并發策略。
前三者是傳統應用服務器apache和tomcat采用的方式,最后一種是nginx采用的方式。當然這里需要注意的是應用服務器和nginx這種 做反向代理服務器(暫且忽略nginx+cgi做應用服務器的功能)的區別。應用服務器是需要處理應用邏輯的,有時候是耗cup資源的;而反向代理主要用 作IO,是IO密集型的應用。使用事件驅動的這種網絡模型,比較適合IO密集型應用,而并不適合CPU密集型應用。對于后者,多進程/線程則是一個更好地 選擇。
當然,由于nginx采用的基于事件驅動的多路IO復用的模型,其作為反向代理服務器時,可支持的并發是非常大的。淘寶tengine團隊曾有一個測試結果是“24G內存機器上,處理并發請求可達200萬”。
2.4.1 nginx/tengine
ngixn是目前使用最廣泛的反向代理軟件,而tengine是阿里開源的一個加強版nginx,其基本實現了nginx收費版本的一些功能,如:主動健康檢查、session sticky等。對于nginx的配置,需要注意的有這么幾點:
典型配置可見:https://github.com/superhj1987/awesome-config/blob/master/nginx/nginx.conf
2.4.2 tomcat
tomcat的關鍵配置總體上有兩大塊:jvm參數配置和connector參數配置。
2.3.2 進程/線程數限制
其中,帶寬和硬件配置是決定系統負載能力的決定性因素。這些只能依靠擴展和升級提高。我們需要重點關注的是在一定帶寬和硬件配置的基礎上,怎么使系統的負載能力達到最大。
2.1 帶寬
毋庸置疑,帶寬是決定系統負載能力的一個至關重要的因素,就好比水管一樣,細的水管同一時間通過的水量自然就少(這個比喻解釋帶寬可能不是特別合適)。一個系統的帶寬首先就決定了這個系統的負載能力,其單位為Mbps,表示數據的發送速度。
2.2 硬件配置
系統部署所在的服務器的硬件決定了一個系統的最大負載能力,也是上限。一般說來,以下幾個配置起著關鍵作用:
很多系統的架構設計、系統優化,最終都會加上這么一句:使用ssd存儲解決了這些問題。
可見,硬件配置是決定一個系統的負載能力的最關鍵因素。
2.3 系統配置
一般來說,目前后端系統都是部署在Linux主機上的。所以拋開win系列不談,對于Linux系統來說一般有以下配置關系著系統的負載能力。