天天看點

後端技術雜談

網際網路時代,高并發是一個老生常談的話題。無論對于一個web站點還是app應用,高峰時能承載的并發請求都是衡量一個系統性能的關鍵标志。像阿裡雙十一頂住了上億的峰值請求、訂單也确實展現了阿裡的技術水準(當然有錢也是一個原因)。

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

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

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

一般的,和系統并發通路量相關的幾個因素如下:

帶寬

硬體配置

系統配置

應用伺服器配置

程式邏輯

系統架構

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

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

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

cpu頻率/核數:cpu頻率關系着cpu的運算速度,核數則影響線程排程、資源配置設定的效率。

記憶體大小以及速度:記憶體越大,那麼可以在記憶體中運作的資料也就越大,速度自然而然就快;記憶體的速度從原來的幾百hz到現在幾千hz,決定了資料讀取存儲的速度。

硬碟速度:傳統的硬碟是使用磁頭進行尋址的,io速度比較慢,使用了ssd的硬碟,其尋址速度大大較快。

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

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

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

檔案描述符數限制:linux中所有東西都是檔案,一個socket就對應着一個檔案描述符,是以系統配置的最大打開檔案數以及單個程序能夠打開的最大檔案數就決定了socket的數目上限。

程序/線程數限制: 對于apache使用的prefork等多程序模式,其負載能力由程序數目所限制。對tomcat多線程模式則由線程數所限制。

tcp核心參數:網絡應用的底層自然離不開tcp/ip,linux核心有一些與此相關的配置也決定了系統的負載能力。

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

程序最大打開檔案描述符數:這個是配單個程序能夠打開的最大檔案數目。可以通過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

程序數限制: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可以檢視/修改線程棧的大小,即每開啟一個新的線程需要配置設定給此線程的一部分記憶體。減小此值可以增加可以打開的線程數目。

在一台伺服器cpu和記憶體資源額定有限的情況下,最大的壓榨伺服器的性能,是最終的目的。在節省成本的情況下,可以考慮修改linux的核心tcp/ip參數,來最大的壓榨伺服器的性能。如果通過修改核心參數也無法解決的負載問題,也隻能考慮更新伺服器了,這是硬體所限,沒有辦法的事。

使用上面的指令,可以得到目前系統的各個狀态的網絡連接配接的數目。如下:

這裡,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拖死。

說到應用伺服器配置,這裡需要提到應用伺服器的幾種工作模式,也叫并發政策。

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萬”。

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

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

jvm參數配置:

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

堆的最小值:xms

堆的最大值:xmx

新生代大小: xmn

永久代大小: xx:permsize:

永久代最大大小: xx:maxpermsize:

棧大小:-xss或-xx:threadstacksize

connector參數配置

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

一般的當一個程序有500個線程在跑的話,那性能已經是很低很低了。tomcat預設配置的最大請求數是150。當某個應用擁有250個以上并發的時候,應考慮應用伺服器的叢集。

另外,并非是無限調大maxtreads和maxconnection就能無限調高并發能力的。線程越多,那麼cpu花費線上程排程上的時間越多,同時,記憶體消耗也就越大,那麼就極大影響處理使用者的請求。受限于硬體資源,并發值是需要設定合适的值的。

protocol: 有三個選項:bio;nio;apr。建議使用apr選項,性能為最高。

connectiontimeout:連接配接的逾時時間

maxthreads:最大線程數,此值限制了bio的最大連接配接數

minsparethreads: 最大空閑線程數

acceptcount:可以接受的最大請求數目(未能得到處理的請求排隊)

maxconnection: 使用nio或者apr時,最大連接配接數受此值影響。

對于tomcat這裡有一個争論就是:使用大記憶體tomcat好還是多個小的tomcat叢集好?(針對64位伺服器以及tomcat來說)

其實,這個要根據業務場景差別對待的。通常,大記憶體tomcat有以下問題:

一旦發生full gc,那麼會非常耗時

一旦gc,dump出的堆快照太大,無法分析

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

使用小記憶體tomcat叢集則有以下優勢:

可以根據系統的負載調整tc的數量,以達到資源的最大使用率,

可以防止單點故障。

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。

當然,對于系統中并發很高并且通路很頻繁的資料,關系型資料庫還是不能妥妥應對。這時候就需要緩存資料庫出馬以隔離對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還要厲害。

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

負載均衡

同步 or 異步

28原則

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

硬體負載均衡

軟體負載均衡

其中,硬體負載均衡的性能無疑是最優的,其中以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也是一款常用的負載均衡軟體。但限于對此使用較少,在此不做講述。

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

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

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

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

一般的java後端系統應用架構如下圖所示:lvs+nginx+tomcat+mysql/ddb+redis/codis

後端技術雜談

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

繼續閱讀