1、什麼是高併發html
高併發(High Concurrency)是互聯網分佈式系統架構設計中必須考慮的因素之一,它一般是指,經過設計保證系統可以同時並行處理不少請求。mysql
高併發相關經常使用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查詢率QPS(Query Per Second),併發用戶數等。nginx
響應時間:系統對請求作出響應的時間。例如系統處理一個HTTP請求須要200ms,這個200ms就是系統的響應時間。程序員
吞吐量:單位時間內處理的請求數量。web
QPS:每秒響應請求數。在互聯網領域,這個指標和吞吐量區分的沒有這麼明顯。redis
併發用戶數:同時承載正常使用系統功能的用戶數量。例如一個即時通信系統,同時在線量必定程度上表明瞭系統的併發用戶數。sql
2、如何提高系統的併發能力數據庫
互聯網分佈式架構設計,提升系統併發能力的方式,方法論上主要有兩種:垂直擴展(Scale Up)與水平擴展(Scale Out)。json
垂直擴展:提高單機處理能力。垂直擴展的方式又有兩種:後端
(1)加強單機硬件性能,例如:增長CPU核數如32核,升級更好的網卡如萬兆,升級更好的硬盤如SSD,擴充硬盤容量如2T,擴充系統內存如128G;
(2)提高單機架構性能,例如:使用Cache來減小IO次數,使用異步來增長單服務吞吐量,使用無鎖數據結構來減小響應時間;
在互聯網業務發展很是迅猛的早期,若是預算不是問題,強烈建議使用「加強單機硬件性能」的方式提高系統併發能力,由於這個階段,公司的戰略每每是發展業務搶時間,而「加強單機硬件性能」每每是最快的方法。
無論是提高單機硬件性能,仍是提高單機架構性能,都有一個致命的不足:單機性能老是有極限的。因此互聯網分佈式架構設計高併發終極解決方案仍是水平擴展。
水平擴展:只要增長服務器數量,就能線性擴充系統性能。水平擴展對系統架構設計是有要求的,如何在架構各層進行可水平擴展的設計,以及互聯網公司架構各層常見的水平擴展實踐,是本文重點討論的內容。
3、常見的互聯網分層架構
常見互聯網分佈式架構如上,分爲:
(1)客戶端層:典型調用方是瀏覽器browser或者手機應用APP
(2)反向代理層:系統入口,反向代理
(3)站點應用層:實現核心應用邏輯,返回html或者json
(4)服務層:若是實現了服務化,就有這一層
(5)數據-緩存層:緩存加速訪問存儲
(6)數據-數據庫層:數據庫固化數據存儲
整個系統各層次的水平擴展,又分別是如何實施的呢?
4、分層水平擴展架構實踐
反向代理層的水平擴展
反向代理層的水平擴展,是經過「DNS輪詢」實現的:dns-server對於一個域名配置了多個解析ip,每次DNS解析請求來訪問dns-server,會輪詢返回這些ip。
當nginx成爲瓶頸的時候,只要增長服務器數量,新增nginx服務的部署,增長一個外網ip,就能擴展反向代理層的性能,作到理論上的無限高併發。
站點層的水平擴展
站點層的水平擴展,是經過「nginx」實現的。經過修改nginx.conf,能夠設置多個web後端。
當web後端成爲瓶頸的時候,只要增長服務器數量,新增web服務的部署,在nginx配置中配置上新的web後端,就能擴展站點層的性能,作到理論上的無限高併發。
服務層的水平擴展
服務層的水平擴展,是經過「服務鏈接池」實現的。
站點層經過RPC-client調用下游的服務層RPC-server時,RPC-client中的鏈接池會創建與下游服務多個鏈接,當服務成爲瓶頸的時候,只要增長服務器數量,新增服務部署,在RPC-client處創建新的下游服務鏈接,就能擴展服務層性能,作到理論上的無限高併發。若是須要優雅的進行服務層自動擴容,這裏可能須要配置中內心服務自動發現功能的支持。
數據層的水平擴展
在數據量很大的狀況下,數據層(緩存,數據庫)涉及數據的水平擴展,將本來存儲在一臺服務器上的數據(緩存,數據庫)水平拆分到不一樣服務器上去,以達到擴充系統性能的目的。
互聯網數據層常見的水平拆分方式有這麼幾種,以數據庫爲例:
按照範圍水平拆分
每個數據服務,存儲必定範圍的數據,上圖爲例:
user0庫,存儲uid範圍1-1kw
user1庫,存儲uid範圍1kw-2kw
這個方案的好處是:
(1)規則簡單,service只需判斷一下uid範圍就能路由到對應的存儲服務;
(2)數據均衡性較好;
(3)比較容易擴展,能夠隨時加一個uid[2kw,3kw]的數據服務;
不足是:
(1) 請求的負載不必定均衡,通常來講,新註冊的用戶會比老用戶更活躍,大range的服務請求壓力會更大;
按照哈希水平拆分
每個數據庫,存儲某個key值hash後的部分數據,上圖爲例:
user0庫,存儲偶數uid數據
user1庫,存儲奇數uid數據
這個方案的好處是:
(1)規則簡單,service只需對uid進行hash能路由到對應的存儲服務;
(2)數據均衡性較好;
(3)請求均勻性較好;
不足是:
(1)不容易擴展,擴展一個數據服務,hash方法改變時候,可能須要進行數據遷移;
這裏須要注意的是,經過水平拆分來擴充系統性能,與主從同步讀寫分離來擴充數據庫性能的方式有本質的不一樣。
經過水平拆分擴展數據庫性能:
(1)每一個服務器上存儲的數據量是總量的1/n,因此單機的性能也會有提高;
(2)n個服務器上的數據沒有交集,那個服務器上數據的並集是數據的全集;
(3)數據水平拆分到了n個服務器上,理論上讀性能擴充了n倍,寫性能也擴充了n倍(其實遠不止n倍,由於單機的數據量變爲了原來的1/n);
經過主從同步讀寫分離擴展數據庫性能:
(1)每一個服務器上存儲的數據量是和總量相同;
(2)n個服務器上的數據都同樣,都是全集;
(3)理論上讀性能擴充了n倍,寫仍然是單點,寫性能不變;
緩存層的水平拆分和數據庫層的水平拆分相似,也是以範圍拆分和哈希拆分的方式居多,就再也不展開。
5、總結
高併發(High Concurrency)是互聯網分佈式系統架構設計中必須考慮的因素之一,它一般是指,經過設計保證系統可以同時並行處理不少請求。
提升系統併發能力的方式,方法論上主要有兩種:垂直擴展(Scale Up)與水平擴展(Scale Out)。前者垂直擴展能夠經過提高單機硬件性能,或者提高單機架構性能,來提升併發性,但單機性能老是有極限的,互聯網分佈式架構設計高併發終極解決方案仍是後者:水平擴展。
互聯網分層架構中,各層次水平擴展的實踐又有所不一樣:
(1)反向代理層能夠經過「DNS輪詢」的方式來進行水平擴展;
(2)站點層能夠經過nginx來進行水平擴展;
(3)服務層能夠經過服務鏈接池來進行水平擴展;
(4)數據庫能夠按照數據範圍,或者數據哈希的方式來進行水平擴展;
各層實施水平擴展後,可以經過增長服務器數量的方式來提高系統的性能,作到理論上的性能無限。
1)im系統,例如qq或者微博,每一個人都讀本身的數據(好友列表、羣列表、我的信息);
2)微博系統,每一個人讀你關注的人的數據,一我的讀多我的的數據;
3)秒殺系統,庫存只有一份,全部人會在集中的時間讀和寫這些數據,多我的讀一個數據。
例如:小米手機每週二的秒殺,可能手機只有1萬部,但瞬時進入的流量多是幾百幾千萬。
又例如:12306搶票,票是有限的,庫存一份,瞬時流量很是多,都讀相同的庫存。讀寫衝突,鎖很是嚴重,這是秒殺業務難的地方。那咱們怎麼優化秒殺業務的架構呢?
優化方向有兩個(今天就講這兩個點):
(1)將請求儘可能攔截在系統上游(不要讓鎖衝突落到數據庫上去)。傳統秒殺系統之因此掛,請求都壓倒了後端數據層,數據讀寫鎖衝突嚴重,併發高響應慢,幾乎全部請求都超時,流量雖大,下單成功的有效流量甚小。以12306爲例,一趟火車其實只有2000張票,200w我的來買,基本沒有人能買成功,請求有效率爲0。
(2)充分利用緩存,秒殺買票,這是一個典型的讀多些少的應用場景,大部分請求是車次查詢,票查詢,下單和支付纔是寫請求。一趟火車其實只有2000張票,200w我的來買,最多2000我的下單成功,其餘人都是查詢庫存,寫比例只有0.1%,讀比例佔99.9%,很是適合使用緩存來優化。好,後續講講怎麼個「將請求儘可能攔截在系統上游」法,以及怎麼個「緩存」法,講講細節。
常見的站點架構基本是這樣的(絕對不畫忽悠類的架構圖)
(1)瀏覽器端,最上層,會執行到一些JS代碼
(2)站點層,這一層會訪問後端數據,拼html頁面返回給瀏覽器
(3)服務層,向上遊屏蔽底層數據細節,提供數據訪問
(4)數據層,最終的庫存是存在這裏的,mysql是一個典型(固然還有會緩存)
這個圖雖然簡單,但能形象的說明大流量高併發的秒殺業務架構,你們要記得這一張圖。
後面細細解析各個層級怎麼優化。
第一層,客戶端怎麼優化(瀏覽器層,APP層)
問你們一個問題,你們都玩過微信的搖一搖搶紅包對吧,每次搖一搖,就會日後端發送請求麼?回顧咱們下單搶票的場景,點擊了「查詢」按鈕以後,系統那個卡呀,進度條漲的慢呀,做爲用戶,我會不自覺的再去點擊「查詢」,對麼?繼續點,繼續點,點點點。。。有用麼?無緣無故的增長了系統負載,一個用戶點5次,80%的請求是這麼多出來的,怎麼整?
(a)產品層面,用戶點擊「查詢」或者「購票」後,按鈕置灰,禁止用戶重複提交請求;
(b)JS層面,限制用戶在x秒以內只能提交一次請求;
APP層面,能夠作相似的事情,雖然你瘋狂的在搖微信,其實x秒才向後端發起一次請求。這就是所謂的「將請求儘可能攔截在系統上游」,越上游越好,瀏覽器層,APP層就給攔住,這樣就能擋住80%+的請求,這種辦法只能攔住普通用戶(但99%的用戶是普通用戶)對於羣內的高端程序員是攔不住的。firebug一抓包,http長啥樣都知道,js是萬萬攔不住程序員寫for循環,調用http接口的,這部分請求怎麼處理?
第二層,站點層面的請求攔截
怎麼攔截?怎麼防止程序員寫for循環調用,有去重依據麼?ip?cookie-id?…想複雜了,這類業務都須要登陸,用uid便可。在站點層面,對uid進行請求計數和去重,甚至不須要統一存儲計數,直接站點層內存存儲(這樣計數會不許,但最簡單)。一個uid,5秒只准透過1個請求,這樣又能攔住99%的for循環請求。
5s只透過一個請求,其他的請求怎麼辦?緩存,頁面緩存,同一個uid,限制訪問頻度,作頁面緩存,x秒內到達站點層的請求,均返回同一頁面。同一個item的查詢,例如車次,作頁面緩存,x秒內到達站點層的請求,均返回同一頁面。如此限流,既能保證用戶有良好的用戶體驗(沒有返回404)又能保證系統的健壯性(利用頁面緩存,把請求攔截在站點層了)。
頁面緩存不必定要保證全部站點返回一致的頁面,直接放在每一個站點的內存也是能夠的。優勢是簡單,壞處是http請求落到不一樣的站點,返回的車票數據可能不同,這是站點層的請求攔截與緩存優化。
好,這個方式攔住了寫for循環發http請求的程序員,有些高端程序員(黑客)控制了10w個肉雞,手裏有10w個uid,同時發請求(先不考慮實名制的問題,小米搶手機不須要實名制),這下怎麼辦,站點層按照uid限流攔不住了。
第三層 服務層來攔截(反正就是不要讓請求落到數據庫上去)
服務層怎麼攔截?大哥,我是服務層,我清楚的知道小米只有1萬部手機,我清楚的知道一列火車只有2000張車票,我透10w個請求去數據庫有什麼意義呢?沒錯,請求隊列!
對於寫請求,作請求隊列,每次只透有限的寫請求去數據層(下訂單,支付這樣的寫業務)
1w部手機,只透1w個下單請求去db
3k張火車票,只透3k個下單請求去db
若是均成功再放下一批,若是庫存不夠則隊列裏的寫請求所有返回「已售完」。
對於讀請求,怎麼優化?cache抗,無論是memcached仍是redis,單機抗個每秒10w應該都是沒什麼問題的。如此限流,只有很是少的寫請求,和很是少的讀緩存mis的請求會透到數據層去,又有99.9%的請求被攔住了。
固然,還有業務規則上的一些優化。回想12306所作的,分時分段售票,原來統一10點賣票,如今8點,8點半,9點,...每隔半個小時放出一批:將流量攤勻。
其次,數據粒度的優化:你去購票,對於餘票查詢這個業務,票剩了58張,仍是26張,你真的關注麼,其實咱們只關心有票和無票?流量大的時候,作一個粗粒度的「有票」「無票」緩存便可。
第三,一些業務邏輯的異步:例以下單業務與 支付業務的分離。這些優化都是結合 業務 來的,我以前分享過一個觀點「一切脫離業務的架構設計都是耍流氓」架構的優化也要針對業務。
好了,最後是數據庫層
瀏覽器攔截了80%,站點層攔截了99.9%並作了頁面緩存,服務層又作了寫請求隊列與數據緩存,每次透到數據庫層的請求都是可控的。db基本就沒什麼壓力了,閒庭信步,單機也能扛得住,仍是那句話,庫存是有限的,小米的產能有限,透這麼多請求來數據庫沒有意義。
所有透到數據庫,100w個下單,0個成功,請求有效率0%。透3k個到數據,所有成功,請求有效率100%。
上文應該描述的很是清楚了,沒什麼總結了,對於秒殺系統,再次重複下我我的經驗的兩個架構優化思路:
(1)儘可能將請求攔截在系統上游(越上游越好);
(2)讀多寫少的經常使用多使用緩存(緩存抗讀壓力);
瀏覽器和APP:作限速
站點層:按照uid作限速,作頁面緩存
服務層:按照業務作寫請求隊列控制流量,作數據緩存
數據層:閒庭信步
而且:結合業務作優化
1、緣起
不少時候,業務有定時任務或者定時超時的需求,當任務量很大時,可能須要維護大量的timer,或者進行低效的掃描。
例如:58到家APP實時消息通道系統,對每一個用戶會維護一個APP到服務器的TCP鏈接,用來實時收發消息,對這個TCP鏈接,有這樣一個需求:「若是連續30s沒有請求包(例如登陸,消息,keepalive包),服務端就要將這個用戶的狀態置爲離線」。
其中,單機TCP同時在線量約在10w級別,keepalive請求包大概30s一次,吞吐量約在3000qps。
通常來講怎麼實現這類需求呢?
「輪詢掃描法」
1)用一個Map<uid, last_packet_time>來記錄每個uid最近一次請求時間last_packet_time
2)當某個用戶uid有請求包來到,實時更新這個Map
3)啓動一個timer,當Map中不爲空時,輪詢掃描這個Map,看每一個uid的last_packet_time是否超過30s,若是超過則進行超時處理
「多timer觸發法」
1)用一個Map<uid, last_packet_time>來記錄每個uid最近一次請求時間last_packet_time
2)當某個用戶uid有請求包來到,實時更新這個Map,並同時對這個uid請求包啓動一個timer,30s以後觸發
3)每一個uid請求包對應的timer觸發後,看Map中,查看這個uid的last_packet_time是否超過30s,若是超過則進行超時處理
方案一:只啓動一個timer,但須要輪詢,效率較低
方案二:不須要輪詢,但每一個請求包要啓動一個timer,比較耗資源
特別在同時在線量很大時,很容易CPU100%,如何高效維護和觸發大量的定時/超時任務,是本文要討論的問題。
2、環形隊列法
廢話很少說,三個重要的數據結構:
1)30s超時,就建立一個index從0到30的環形隊列(本質是個數組)
2)環上每個slot是一個Set<uid>,任務集合
3)同時還有一個Map<uid, index>,記錄uid落在環上的哪一個slot裏
同時:
1)啓動一個timer,每隔1s,在上述環形隊列中移動一格,0->1->2->3…->29->30->0…
2)有一個Current Index指針來標識剛檢測過的slot
當有某用戶uid有請求包到達時:
1)從Map結構中,查找出這個uid存儲在哪個slot裏
2)從這個slot的Set結構中,刪除這個uid
3)將uid從新加入到新的slot中,具體是哪個slot呢 => Current Index指針所指向的上一個slot,由於這個slot,會被timer在30s以後掃描到
(4)更新Map,這個uid對應slot的index值
哪些元素會被超時掉呢?
Current Index每秒種移動一個slot,這個slot對應的Set<uid>中全部uid都應該被集體超時!若是最近30s有請求包來到,必定被放到Current Index的前一個slot了,Current Index所在的slot對應Set中全部元素,都是最近30s沒有請求包來到的。
因此,當沒有超時時,Current Index掃到的每個slot的Set中應該都沒有元素。
優點:
(1)只須要1個timer
(2)timer每1s只須要一次觸發,消耗CPU很低
(3)批量超時,Current Index掃到的slot,Set中全部元素都應該被超時掉
3、總結
這個環形隊列法是一個通用的方法,Set和Map中能夠是任何task,本文的uid是一個最簡單的舉例。