一. 衡量指標 java
用什麼來衡量一個系統的負載能力呢?有一個概念叫作每秒請求數(Requests per second),指的是每秒可以成功處理請求的數目。好比說,你能夠配置tomcat服務器的maxConnection爲無限大,可是受限於服務器系統或者硬件限制,不少請求是不會在必定的時間內獲得響應的,這並不做爲一個成功的請求,其中成功獲得響應的請求數即爲每秒請求數,反應出系統的負載能力。 mysql
一般的,對於一個系統,增長併發用戶數量時每秒請求數量也會增長。然而,咱們最終會達到這樣一個點,此時併發用戶數量開始「壓倒」服務器。若是繼續增長併發用戶數量,每秒請求數量開始降低,而反應時間則會增長。這個併發用戶數量開始「壓倒」服務器的臨界點很是重要,此時的併發用戶數量能夠認爲是當前系統的最大負載能力。 linux
二. 相關因素 nginx
通常的,和系統併發訪問量相關的幾個因素以下: git
· github
帶寬 redis
· sql
硬件配置 數據庫
· apache
系統配置
·
應用服務器配置
·
程序邏輯
·
系統架構
其中,帶寬和硬件配置是決定系統負載能力的決定性因素。這些只能依靠擴展和升級提升。咱們須要重點關注的是在必定帶寬和硬件配置的基礎上,怎麼使系統的負載能力達到最大。
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查看系統總共能夠打開的最大線程數。
o
單個進程的最大線程數和PTHREAD_THREADS_MAX有關,此限制能夠在/usr/include/bits/local_lim.h中查看,可是若是想要修改的話,須要從新編譯。
o
這裏須要提到一點的是,Linux內核2.4的線程實現方式爲linux threads,是輕量級進程,都會首先建立一個管理線程,線程數目的大小是受PTHREAD_THREADS_MAX影響的。但Linux2.6內核的線程實現方式爲NPTL,是一個改進的LWP實現,最大一個區別就是,線程公用進程的pid(tgid),線程數目大小隻受制於資源。
o
線程數的大小還受線程棧大小的制約:使用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
o
堆的最大值:Xmx
o
新生代大小: Xmn
o
永久代大小: XX:PermSize:
o
永久代最大大小: XX:MaxPermSize:
o
棧大小:-Xss或-XX:ThreadStackSize
o
這裏對於棧大小有一點須要注意的是:在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選項,性能爲最高。
o
connectionTimeout:鏈接的超時時間
o
maxThreads:最大線程數,此值限制了bio的最大鏈接數
o
minSpareThreads: 最大空閒線程數
o
acceptCount:能夠接受的最大請求數目(未能獲得處理的請求排隊)
o
maxConnection: 使用nio或者apr時,最大鏈接數受此值影響。
o
典型配置可見: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
其中,虛線部分是數據庫層,採用的是主從模式。也可使用redis cluster(codis等)以及mysql cluster(Cobar等)來替換。