[轉]系統負載能力淺析

http://www.rowkey.me/blog/2015/09/09/load-analysis/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io


系統負載能力淺析

—本文於2015.12.23號最新更新—

互聯網時代,高併發是一個老生常談的話題。無論對於一個web站點還是app應用,高峯時能承載的併發請求都是衡量一個系統性能的關鍵標誌。像阿里雙十一頂住了上億的峯值請求、訂單也確實體現了阿里的技術水平(當然有錢也是一個原因)。

那麼,何爲系統負載能力?怎麼衡量?相關因素有哪些?又如何優化呢?

一. 衡量指標

用什麼來衡量一個系統的負載能力呢?有一個概念叫做每秒請求數(Requests per second),指的是每秒能夠成功處理請求的數目。比如說,你可以配置tomcat服務器的maxConnection爲無限大,但是受限於服務器系統或者硬件限制,很多請求是不會在一定的時間內得到響應的,這並不作爲一個成功的請求,其中成功得到響應的請求數即爲每秒請求數,反應出系統的負載能力。

通常的,對於一個系統,增加併發用戶數量時每秒請求數量也會增加。然而,我們最終會達到這樣一個點,此時併發用戶數量開始“壓倒”服務器。如果繼續增加併發用戶數量,每秒請求數量開始下降,而反應時間則會增加。這個併發用戶數量開始“壓倒”服務器的臨界點非常重要,此時的併發用戶數量可以認爲是當前系統的最大負載能力。

二. 相關因素

一般的,和系統併發訪問量相關的幾個因素如下:

  • 帶寬
  • 硬件配置
  • 系統配置
  • 應用服務器配置
  • 程序邏輯
  • 系統架構

其中,帶寬和硬件配置是決定系統負載能力的決定性因素。這些只能依靠擴展和升級提高。我們需要重點關注的是在一定帶寬和硬件配置的基礎上,怎麼使系統的負載能力達到最大。

2.1 帶寬

毋庸置疑,帶寬是決定系統負載能力的一個至關重要的因素,就好比水管一樣,細的水管同一時間通過的水量自然就少(這個比喻解釋帶寬可能不是特別合適)。一個系統的帶寬首先就決定了這個系統的負載能力,其單位爲Mbps,表示數據的發送速度。

2.2 硬件配置

系統部署所在的服務器的硬件決定了一個系統的最大負載能力,也是上限。一般說來,以下幾個配置起着關鍵作用:

  • cpu頻率/核數:cpu頻率關係着cpu的運算速度,核數則影響線程調度、資源分配的效率。
  • 內存大小以及速度:內存越大,那麼可以在內存中運行的數據也就越大,速度自然而然就快;內存的速度從原來的幾百hz到現在幾千hz,決定了數據讀取存儲的速度。
  • 硬盤速度:傳統的硬盤是使用磁頭進行尋址的,io速度比較慢,使用了SSD的硬盤,其尋址速度大大較快。

很多系統的架構設計、系統優化,最終都會加上這麼一句:使用ssd存儲解決了這些問題。

可見,硬件配置是決定一個系統的負載能力的最關鍵因素。

2.3 系統配置

一般來說,目前後端系統都是部署在Linux主機上的。所以拋開win系列不談,對於Linux系統來說一般有以下配置關係着系統的負載能力。

  • 文件描述符數限制:Linux中所有東西都是文件,一個socket就對應着一個文件描述符,因此係統配置的最大打開文件數以及單個進程能夠打開的最大文件數就決定了socket的數目上限。
  • 進程/線程數限制: 對於apache使用的prefork等多進程模式,其負載能力由進程數目所限制。對tomcat多線程模式則由線程數所限制。
  • tcp內核參數:網絡應用的底層自然離不開tcp/ip,Linux內核有一些與此相關的配置也決定了系統的負載能力。

2.3.1 文件描述符數限制

  • 系統最大打開文件描述符數:/proc/sys/fs/file-max中保存了這個數目,修改此值

      臨時性
          echo 1000000 > /proc/sys/fs/file-max
      永久性:在/etc/sysctl.conf中設置
          fs.file-max = 1000000
    
  • 進程最大打開文件描述符數:這個是配單個進程能夠打開的最大文件數目。可以通過ulimit -n查看/修改。如果想要永久修改,則需要修改/etc/security/limits.conf中的nofile。

通過讀取/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

2.3.2 進程/線程數限制

  • 進程數限制: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可以查看/修改線程棧的大小,即每開啓一個新的線程需要分配給此線程的一部分內存。減小此值可以增加可以打開的線程數目。

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 13
SYN_RECV 468
ESTABLISHED 90
FIN_WAIT1 259
FIN_WAIT2 40
CLOSING 34
TIME_WAIT 28322

這裏,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 時間。

這裏需要注意的一點就是當打開了tcp_tw_recycle,就會檢查時間戳,移動環境下的發來的包的時間戳有些時候是亂跳的,會把帶了“倒退”的時間戳的包當作是“recycle的tw連接的重傳數據,不是新的請求”,於是丟掉不回包,造成大量丟包。另外,當前面有LVS,並且採用的是NAT機制時,開啓tcp_tw_recycle會造成一些異常,可見:http://www.pagefault.info/?p=416。如果這種情況下仍然需要開啓此選項,那麼可以考慮設置net.ipv4.tcp_timestamps=0,忽略掉報文的時間戳即可。

此外,還可以通過優化tcp/ip的可使用端口的範圍,進一步提升負載能力。,如下:

  • 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拖死。

2.4 應用服務器配置

說到應用服務器配置,這裏需要提到應用服務器的幾種工作模式,也叫併發策略。

  • multi process:多進程方式,一個進程處理一個請求。
  • prefork:類似於多進程的方式,但是會預先fork出一些進程供後續使用,是一種進程池的理念。
  • worker:一個線程對應一個請求,相比多進程的方式,消耗資源變少,但同時一個線程的崩潰會引起整個進程的崩潰,穩定性不如多進程。
  • master/worker:採用的是非阻塞IO的方式,只有兩種進程:worker和master,master負責worker進程的創建、管理等,worker進程採用基於事件驅動的多路複用IO處理請求。mater進程只需要一個,woker進程根據cpu核數設置數目。

前三者是傳統應用服務器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的配置,需要注意的有這麼幾點:

  • worker數目要和cpu(核)的數目相適應
  • keepalive timout要設置適當
  • worker_rlimit_nofile最大文件描述符要增大
  • upstream可以使用http 1.1的keepalive

典型配置可見:https://github.com/superhj1987/awesome-config/blob/master/nginx/nginx.conf

2.4.2 tomcat

tomcat的關鍵配置總體上有兩大塊:jvm參數配置和connector參數配置。

  • jvm參數配置:

    • 堆的最小值:Xms
    • 堆的最大值:Xmx
    • 新生代大小: Xmn
    • 永久代大小: XX:PermSize:
    • 永久代最大大小: XX:MaxPermSize:
    • 棧大小:-Xss或-XX:ThreadStackSize

    這裏對於棧大小有一點需要注意的是:在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

  • connector參數配置

    • protocol: 有三個選項:bio;nio;apr。建議使用apr選項,性能爲最高。
    • connectionTimeout:連接的超時時間
    • maxThreads:最大線程數,此值限制了bio的最大連接數
    • minSpareThreads: 最大空閒線程數
    • acceptCount:可以接受的最大請求數目(未能得到處理的請求排隊)
    • maxConnection: 使用nio或者apr時,最大連接數受此值影響。

    典型配置可見:https://github.com/superhj1987/awesome-config/blob/master/tomcat/connector.conf

    一般的當一個進程有500個線程在跑的話,那性能已經是很低很低了。Tomcat默認配置的最大請求數是150。當某個應用擁有250個以上併發的時候,應考慮應用服務器的集羣。

    另外,並非是無限調大maxTreads和maxConnection就能無限調高併發能力的。線程越多,那麼cpu花費在線程調度上的時間越多,同時,內存消耗也就越大,那麼就極大影響處理用戶的請求。受限於硬件資源,併發值是需要設置合適的值的。

對於tomcat這裏有一個爭論就是:使用大內存tomcat好還是多個小的tomcat集羣好?(針對64位服務器以及tomcat來說)

其實,這個要根據業務場景區別對待的。通常,大內存tomcat有以下問題:

  • 一旦發生full gc,那麼會非常耗時
  • 一旦gc,dump出的堆快照太大,無法分析

因此,如果可以保證一定程度上程序的對象大部分都是朝生夕死的,老年代不會發生gc,那麼使用大內存tomcat也是可以的。但是在伸縮性和高可用卻比不上使用小內存(相對來說)tomcat集羣。

使用小內存tomcat集羣則有以下優勢:

  • 可以根據系統的負載調整tc的數量,以達到資源的最大利用率,
  • 可以防止單點故障。

2.4.3 數據庫

mysql

mysql是目前最常用的關係型數據庫,支持複雜的查詢。但是其負載能力一般,很多時候一個系統的瓶頸就發生在mysql這一點,當然有時候也和sql語句的效率有關。比如,牽扯到聯表的查詢一般說來效率是不會太高的。

影響數據庫性能的因素一般有以下幾點:

  • 硬件配置:這個無需多說
  • 數據庫設置:max_connection的一些配置會影響數據庫的連接數
  • 數據表的設計:使用冗餘字段避免聯表查詢;使用索引提高查詢效率
  • 查詢語句是否合理:這個牽扯到的是個人的編碼素質。比如,查詢符合某個條件的記錄,我見過有人把記錄全部查出來,再去逐條對比
  • 引擎的選擇:myisam和innodb兩者的適用場景不同,不存在絕對的優劣

拋開以上因素,當數據量單表突破千萬甚至百萬時(和具體的數據有關),需要對mysql數據庫進行優化,一種常見的方案就是分表:

  • 垂直分表:在列維度的拆分
  • 水平分表:行維度的拆分

此外,對於數據庫,可以使用讀寫分離的方式提高性能,尤其是對那種讀頻率遠大於寫頻率的業務場景。這裏一般採用master/slave的方式實現讀寫分離,前面用程序控制或者加一個proxy層。可以選擇使用MySQL Proxy,編寫lua腳本來實現基於proxy的mysql讀寫分離;也可以通過程序來控制,根據不同的sql語句選擇相應的數據庫來操作,這個也是筆者公司目前在用的方案。由於此方案和業務強綁定,是很難有一個通用的方案的,其中比較成熟的是阿里的TDDL,但是由於未全部開源且對其他組件有依賴性,不推薦使用。

現在很多大的公司對這些分表、主從分離、分佈式都基於mysql做了自己的二次開發,形成了自己公司的一套分佈式數據庫系統。比如阿里的Cobar、網易的DDB、360的Atlas等。當然,很多大公司也研發了自己的mysql分支,比較出名的就是姜承堯帶領研發的InnoSQL。

redis

當然,對於系統中併發很高並且訪問很頻繁的數據,關係型數據庫還是不能妥妥應對。這時候就需要緩存數據庫出馬以隔離對mysql的訪問,防止mysql崩潰。

其中,redis是目前用的比較多的緩存數據庫(當然,也有直接把redis當做數據庫使用的)。redis是單線程基於內存的數據庫,讀寫性能遠遠超過mysql。一般情況下,對redis做讀寫分離主從同步就可以應對大部分場景的應用。但是這樣的方案缺少ha,尤其對於分佈式應用,是不可接受的。目前,redis集羣的實現方案有以下幾個:

  • redis cluster:這是一種去中心化的方案,是redis的官方實現。是一種非常“重”的方案,已經不是Redis單實例的“簡單、可依賴”了。目前應用案例還很少,貌似國內的芒果臺用了,結局不知道如何。
  • twemproxy:這是twitter開源的redis和memcached的proxy方案。比較成熟,目前的應用案例比較多,但也有一些缺陷,尤其在運維方面。比如無法平滑的擴容/縮容,運維不友好等。
  • codis: 這個是豌豆莢開源的redis proxy方案,能夠兼容twemproxy,並且對其做了很多改進。由豌豆莢於2014年11月開源,基於Go和C開發。現已廣泛用於豌豆莢的各種Redis業務場景。現在比Twemproxy快近100%。目前據我所知除了豌豆莢之外,hulu也在使用這套方案。當然,其升級項目reborndb號稱比codis還要厲害。

2.5 系統架構

影響性能的系統架構一般會有這幾方面:

  • 負載均衡
  • 同步 or 異步
  • 28原則

2.5.1 負載均衡

負載均衡在服務端領域中是一個很關鍵的技術。可以分爲以下兩種:

  • 硬件負載均衡
  • 軟件負載均衡

其中,硬件負載均衡的性能無疑是最優的,其中以F5爲代表。但是,與高性能並存的是其成本的昂貴。所以對於很多初創公司來說,一般是選用軟件負載均衡的方案。

軟件負載均衡中又可以分爲四層負載均衡和七層負載均衡。 上文在應用服務器配置部分講了nginx的反向代理功能即七層的一種成熟解決方案,主要針對的是七層http協議(雖然最新的發佈版本已經支持四層負載均衡)。對於四層負載均衡,目前應用最廣泛的是lvs。其是阿里的章文嵩博士帶領的團隊所研發的一款linux下的負載均衡軟件,本質上是基於iptables實現的。分爲三種工作模式:

  • NAT: 修改數據包destination ip,in和out都要經過lvs。
  • DR:修改數據包mac地址,lvs和realserver需要在一個vlan。
  • IP TUUNEL:修改數據包destination ip和源ip,realserver需要支持ip tunnel協議。lvs和realserver不需要在一個vlan。

三種模式各有優缺點,目前還有阿里開源的一個FULL NAT是在NAT原來的DNAT上加入了SNAT的功能。

此外,haproxy也是一款常用的負載均衡軟件。但限於對此使用較少,在此不做講述。

2.5.2 同步 or 異步

對於一個系統,很多業務需要面對使用同步機制或者是異步機制的選擇。比如,對於一篇帖子,一個用戶對其分享後,需要記錄用戶的分享記錄。如果你使用同步模式(分享的同時記錄此行爲),那麼響應速度肯定會受到影響。而如果你考慮到分享過後,用戶並不會立刻去查看自己的分享記錄,犧牲這一點時效性,可以先完成分享的動作,然後異步記錄此行爲,會提高分享請求的響應速度(當然,這裏可能會有事務準確性的問題)。有時候在某些業務邏輯上,在充分理解用戶訴求的基礎上,是可以犧牲某些特性來滿足用戶需求的。

這裏值得一提的是,很多時候對於一個業務流程,是可以拆開劃分爲幾個步驟的,然後有些步驟完全可以異步併發執行,能夠極大提高處理速度。

2.5.3 28原則

對於一個系統,20%的功能會帶來80%的流量。這就是28原則的意思,當然也是我自己的一種表述。因此在設計系統的時候,對於80%的功能,其面對的請求壓力是很小的,是沒有必要進行過度設計的。但是對於另外20%的功能則是需要設計再設計、reivew再review,能夠做負載均衡就做負載均衡,能夠緩存就緩存,能夠做分佈式就分佈式,能夠把流程拆開異步化就異步化。

當然,這個原則適用於生活中很多事物。

三. 一般架構

一般的Java後端系統應用架構如下圖所示:LVS+Nginx+Tomcat+MySql/DDB+Redis/Codis

web-arch

其中,虛線部分是數據庫層,採用的是主從模式。也可以使用redis cluster(codis等)以及mysql cluster(Cobar等)來替換。

如需轉載,請註明來自: http://superhj1987.github.com

版權聲明:本文爲博主原創文章,未經博主允許不得轉載

本文部分參考自網絡相關資料,由於時間太久,無法追朔原作者。如有侵權,請聯繫[email protected]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章