高併發與高可用知識總結html
參考:mysql
微信公衆號:架構師之路nginx
1、什麼是高併發程序員
高併發(High Concurrency)是互聯網分佈式系統架構設計中必須考慮的因素之一,它一般是指,經過設計保證系統可以同時並行處理不少請求。web
高併發相關經常使用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查詢率QPS(Query Per Second),併發用戶數等。redis
響應時間:系統對請求作出響應的時間。例如系統處理一個HTTP請求須要200ms,這個200ms就是系統的響應時間。sql
吞吐量:單位時間內處理的請求數量。數據庫
QPS:每秒響應請求數。在互聯網領域,這個指標和吞吐量區分的沒有這麼明顯。編程
併發用戶數:同時承載正常使用系統功能的用戶數量。例如一個即時通信系統,同時在線量必定程度上表明瞭系統的併發用戶數。json
2、如何提高系統的併發能力
互聯網分佈式架構設計,提升系統併發能力的方式,方法論上主要有兩種:垂直擴展(Scale Up)與水平擴展(Scale Out)。
垂直擴展:提高單機處理能力。垂直擴展的方式又有兩種:
(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、關於併發咱們說的高併發是什麼?
在互聯網時代,高併發,一般是指,在某個時間點,有不少個訪問同時到來。
高併發,一般關心的系統指標與業務指標?
QPS:每秒鐘查詢量,廣義的,一般指指每秒請求數
響應時間:從請求發出到收到響應花費的時間,例如:系統處理一個HTTP請求須要100ms,這個100ms就是系統的響應時間
帶寬:計算帶寬大小需關注兩個指標,峯值流量和頁面的平均大小
PV:綜合瀏覽量(Page View),即頁面瀏覽量或者點擊量,一般關注在24小時內訪問的頁面數量,即「日PV」
UV:獨立訪問(UniQue Visitor),即去重後的訪問用戶數,一般關注在24小時內訪問的用戶,即「日UV」
2、關於三種應對大併發的常見優化方案
【數據庫緩存】
爲何是要使用緩存?
緩存數據是爲了讓客戶端不多甚至不訪問數據庫,減小磁盤IO,提升併發量,提升應用數據的響應速度。
【CDN加速】
什麼是CDN?
CDN的全稱是Content Delivery Network,CDN系統可以實時地根據網絡流量和各節點的鏈接、負載情況以及到用戶的距離等綜合信息將用戶的請求從新導向離用戶最近的服務節點上。
使用CDN的優點?
CDN的本質是內存緩存,就近訪問,它提升了企業站點(尤爲含有大量圖片和靜態頁面站點)的訪問速度,跨運營商的網絡加速,保證不一樣網絡的用戶都獲得良好的訪問質量。
同時,減小遠程訪問的帶寬,分擔網絡流量,減輕原站點WEB服務器負載。
【服務器的集羣化,以及負載均衡】
什麼是七層負載均衡?
七層負載均衡,是基於http協議等應用信息的負載均衡,最經常使用的就是Nginx,它可以自動剔除工做不正常的後端服務器,上傳文件使用異步模式,支持多種分配策略,能夠分配權重,分配方式靈活。
內置策略:IP Hash、加權輪詢
擴展策略:fair策略、通用hash、一致性hash
什麼是加權輪詢策略?
首先將請求都分給高權重的機器,直到該機器的權值降到了比其餘機器低,纔開始將請求分給下一個高權重的機器,即體現了加權權重,又體現了輪詢。
1、什麼是高可用
高可用HA(High Availability)是分佈式系統架構設計中必須考慮的因素之一,它一般是指,經過設計減小系統不能提供服務的時間。
假設系統一直可以提供服務,咱們說系統的可用性是100%。
若是系統每運行100個時間單位,會有1個時間單位沒法提供服務,咱們說系統的可用性是99%。
不少公司的高可用目標是4個9,也就是99.99%,這就意味着,系統的年停機時間爲8.76個小時。
百度的搜索首頁,是業內公認高可用保障很是出色的系統,甚至人們會經過www.baidu.com 能不能訪問來判斷「網絡的連通性」,百度高可用的服務讓人留下啦「網絡通暢,百度就能訪問」,「百度打不開,應該是網絡連不上」的印象,這實際上是對百度HA最高的褒獎。
2、如何保障系統的高可用
咱們都知道,單點是系統高可用的大敵,單點每每是系統高可用最大的風險和敵人,應該儘可能在系統設計的過程當中避免單點。方法論上,高可用保證的原則是「集羣化」,或者叫「冗餘」:只有一個單點,掛了服務會受影響;若是有冗餘備份,掛了還有其餘backup可以頂上。
保證系統高可用,架構設計的核心準則是:冗餘。
有了冗餘以後,還不夠,每次出現故障須要人工介入恢復勢必會增長系統的不可服務實踐。因此,又每每是經過「自動故障轉移」來實現系統的高可用。
接下來咱們看下典型互聯網架構中,如何經過冗餘+自動故障轉移來保證系統的高可用特性。
3、常見的互聯網分層架構
常見互聯網分佈式架構如上,分爲:
(1)客戶端層:典型調用方是瀏覽器browser或者手機應用APP
(2)反向代理層:系統入口,反向代理
(3)站點應用層:實現核心應用邏輯,返回html或者json
(4)服務層:若是實現了服務化,就有這一層
(5)數據-緩存層:緩存加速訪問存儲
(6)數據-數據庫層:數據庫固化數據存儲
整個系統的高可用,又是經過每一層的冗餘+自動故障轉移來綜合實現的。
4、分層高可用架構實踐
【客戶端層->反向代理層】的高可用
【客戶端層】到【反向代理層】的高可用,是經過反向代理層的冗餘來實現的。以nginx爲例:有兩臺nginx,一臺對線上提供服務,另外一臺冗餘以保證高可用,常見的實踐是keepalived存活探測,相同virtual IP提供服務。
自動故障轉移:當nginx掛了的時候,keepalived可以探測到,會自動的進行故障轉移,將流量自動遷移到shadow-nginx,因爲使用的是相同的virtual IP,這個切換過程對調用方是透明的。
【反向代理層->站點層】的高可用
【反向代理層】到【站點層】的高可用,是經過站點層的冗餘來實現的。假設反向代理層是nginx,nginx.conf裏可以配置多個web後端,而且nginx可以探測到多個後端的存活性。
自動故障轉移:當web-server掛了的時候,nginx可以探測到,會自動的進行故障轉移,將流量自動遷移到其餘的web-server,整個過程由nginx自動完成,對調用方是透明的。
【站點層->服務層】的高可用
【站點層】到【服務層】的高可用,是經過服務層的冗餘來實現的。「服務鏈接池」會創建與下游服務多個鏈接,每次請求會「隨機」選取鏈接來訪問下游服務。
自動故障轉移:當service掛了的時候,service-connection-pool可以探測到,會自動的進行故障轉移,將流量自動遷移到其餘的service,整個過程由鏈接池自動完成,對調用方是透明的(因此說RPC-client中的服務鏈接池是很重要的基礎組件)。
【服務層>緩存層】的高可用
【服務層】到【緩存層】的高可用,是經過緩存數據的冗餘來實現的。
緩存層的數據冗餘又有幾種方式:第一種是利用客戶端的封裝,service對cache進行雙讀或者雙寫。
緩存層也能夠經過支持主從同步的緩存集羣來解決緩存層的高可用問題。
以redis爲例,redis自然支持主從同步,redis官方也有sentinel哨兵機制,來作redis的存活性檢測。
自動故障轉移:當redis主掛了的時候,sentinel可以探測到,會通知調用方訪問新的redis,整個過程由sentinel和redis集羣配合完成,對調用方是透明的。
說完緩存的高可用,這裏要多說一句,業務對緩存並不必定有「高可用」要求,更多的對緩存的使用場景,是用來「加速數據訪問」:把一部分數據放到緩存裏,若是緩存掛了或者緩存沒有命中,是能夠去後端的數據庫中再取數據的。
這類容許「cache miss」的業務場景,緩存架構的建議是:
將kv緩存封裝成服務集羣,上游設置一個代理(代理能夠用集羣冗餘的方式保證高可用),代理的後端根據緩存訪問的key水平切分紅若干個實例,每一個實例的訪問並不作高可用。
緩存實例掛了屏蔽:當有水平切分的實例掛掉時,代理層直接返回cache miss,此時緩存掛掉對調用方也是透明的。key水平切分實例減小,不建議作re-hash,這樣容易引起緩存數據的不一致。
【服務層>數據庫層】的高可用
大部分互聯網技術,數據庫層都用了「主從同步,讀寫分離」架構,因此數據庫層的高可用,又分爲「讀庫高可用」與「寫庫高可用」兩類。
【服務層>數據庫層「讀」】的高可用
【服務層】到【數據庫讀】的高可用,是經過讀庫的冗餘來實現的。
既然冗餘了讀庫,通常來講就至少有2個從庫,「數據庫鏈接池」會創建與讀庫多個鏈接,每次請求會路由到這些讀庫。
自動故障轉移:當讀庫掛了的時候,db-connection-pool可以探測到,會自動的進行故障轉移,將流量自動遷移到其餘的讀庫,整個過程由鏈接池自動完成,對調用方是透明的(因此說DAO中的數據庫鏈接池是很重要的基礎組件)。
【服務層>數據庫層「寫」】的高可用
【服務層】到【數據庫寫】的高可用,是經過寫庫的冗餘來實現的。
以mysql爲例,能夠設置兩個mysql雙主同步,一臺對線上提供服務,另外一臺冗餘以保證高可用,常見的實踐是keepalived存活探測,相同virtual IP提供服務。
自動故障轉移:當寫庫掛了的時候,keepalived可以探測到,會自動的進行故障轉移,將流量自動遷移到shadow-db-master,因爲使用的是相同的virtual IP,這個切換過程對調用方是透明的。
5、總結
高可用HA(High Availability)是分佈式系統架構設計中必須考慮的因素之一,它一般是指,經過設計減小系統不能提供服務的時間。
方法論上,高可用是經過冗餘+自動故障轉移來實現的。
整個互聯網分層系統架構的高可用,又是經過每一層的冗餘+自動故障轉移來綜合實現的,具體的:
(1)【客戶端層】到【反向代理層】的高可用,是經過反向代理層的冗餘實現的,常見實踐是keepalived + virtual IP自動故障轉移
(2)【反向代理層】到【站點層】的高可用,是經過站點層的冗餘實現的,常見實踐是nginx與web-server之間的存活性探測與自動故障轉移
(3)【站點層】到【服務層】的高可用,是經過服務層的冗餘實現的,常見實踐是經過service-connection-pool來保證自動故障轉移
(4)【服務層】到【緩存層】的高可用,是經過緩存數據的冗餘實現的,常見實踐是緩存客戶端雙讀雙寫,或者利用緩存集羣的主從數據同步與sentinel保活與自動故障轉移;更多的業務場景,對緩存沒有高可用要求,可使用緩存服務化來對調用方屏蔽底層複雜性
(5)【服務層】到【數據庫「讀」】的高可用,是經過讀庫的冗餘實現的,常見實踐是經過db-connection-pool來保證自動故障轉移
(6)【服務層】到【數據庫「寫」】的高可用,是經過寫庫的冗餘實現的,常見實踐是keepalived + virtual IP自動故障轉移
互聯網公司,這樣的場景是否似曾相識:
場景一:pm要作一個很大的運營活動,技術老大殺過來,問了兩個問題:
(1)機器能抗住麼?
(2)若是扛不住,須要加多少臺機器?
場景二:系統設計階段,技術老大殺過來,又問了兩個問題:
(1)數據庫須要分庫麼?
(2)若是須要分庫,須要分幾個庫?
技術上來講,這些都是系統容量預估的問題,容量設計是架構師必備的技能之一。常見的容量評估包括數據量、併發量、帶寬、CPU/MEM/DISK等,今天分享的內容,就以【併發量】爲例,看看如何回答好這兩個問題。
【步驟一:評估總訪問量】
如何知道總訪問量?對於一個運營活動的訪問量評估,或者一個系統上線後PV的評估,有什麼好的方法?
答案是:詢問業務方,詢問運營同窗,詢問產品同窗,看對運營活動或者產品上線後的預期是什麼。
舉例:58要作一個APP-push的運營活動,計劃在30分鐘內完成5000w用戶的push推送,預計push消息點擊率10%,求push落地頁系統的總訪問量?
回答:5000w*10% = 500w
【步驟二:評估平均訪問量QPS】
如何知道平均訪問量QPS?
答案是:有了總量,除以總時間便可,若是按照天評估,一天按照4w秒計算。
舉例1:push落地頁系統30分鐘的總訪問量是500w,求平均訪問量QPS
回答:500w/(30*60) = 2778,大概3000QPS
舉例2:主站首頁估計日均pv 8000w,求平均訪問QPS
回答:一天按照4w秒算,8000w/4w=2000,大概2000QPS
提問:爲何一天按照4w秒計算?
回答:一天共24小時*60分鐘*60秒=8w秒,通常假設全部請求都發生在白天,因此通常來講一天只按照4w秒評估
【步驟三:評估高峯QPS】
系統容量規劃時,不能只考慮平均QPS,而是要抗住高峯的QPS,如何知道高峯QPS呢?
答案是:根據業務特性,經過業務訪問曲線評估
舉例:日均QPS爲2000,業務訪問趨勢圖以下圖,求峯值QPS預估?
回答:從圖中能夠看出,峯值QPS大概是均值QPS的2.5倍,日均QPS爲2000,因而評估出峯值QPS爲5000。
說明:有一些業務例如「秒殺業務」比較難畫出業務訪問趨勢圖,這類業務的容量評估不在此列。
【步驟四:評估系統、單機極限QPS】
如何評估一個業務,一個服務單機能的極限QPS呢?
答案是:壓力測試
在一個服務上線前,通常來講是須要進行壓力測試的(不少創業型公司,業務迭代很快的系統可能沒有這一步,那就悲劇了),以APP-push運營活動落地頁爲例(日均QPS2000,峯值QPS5000),這個系統的架構多是這樣的:
1)訪問端是APP
2)運營活動H5落地頁是一個web站點
3)H5落地頁由緩存cache、數據庫db中的數據拼裝而成
經過壓力測試發現,web層是瓶頸,tomcat壓測單機只能抗住1200的QPS(通常來講,1%的流量到數據庫,數據庫500QPS仍是能輕鬆抗住的,cache的話QPS能抗住,須要評估cache的帶寬,假設不是瓶頸),咱們就獲得了web單機極限的QPS是1200。通常來講,線上系統是不會跑滿到極限的,打個8折,單機線上容許跑到QPS1000。
【步驟五:根據線上冗餘度回答兩個問題】
好了,上述步驟1-4已經獲得了峯值QPS是5000,單機QPS是1000,假設線上部署了2臺服務,就能自信自如的回答技術老大提出的問題了:
(1)機器能抗住麼? -> 峯值5000,單機1000,線上2臺,扛不住
(2)若是扛不住,須要加多少臺機器? -> 須要額外3臺,提早預留1臺更好,給4臺更穩
除了併發量的容量預估,數據量、帶寬、CPU/MEM/DISK等評估亦可遵循相似的步驟。
互聯網架構設計如何進行容量評估:
【步驟一:評估總訪問量】 -> 詢問業務、產品、運營
【步驟二:評估平均訪問量QPS】-> 除以時間,一天算4w秒
【步驟三:評估高峯QPS】 -> 根據業務曲線圖來
【步驟四:評估系統、單機極限QPS】 -> 壓測很重要
【步驟五:根據線上冗餘度回答兩個問題】 -> 估計冗餘度與線上冗餘度差值
【業務場景】
有一類寫多讀少的業務場景:大部分請求是對數據進行修改,少部分請求對數據進行讀取。
例子1:滴滴打車,某個司機地理位置信息的變化(可能每幾秒鐘有一個修改),以及司機地理位置的讀取(用戶打車的時候查看某個司機的地理位置)。
void SetDriverInfo(long driver_id, DriverInfoi); // 大量請求調用修改司機信息,可能主要是GPS位置的修改
DriverInfo GetDriverInfo(long driver_id); // 少許請求查詢司機信息
例子2:統計計數的變化,某個url的訪問次數,用戶某個行爲的反做弊計數(計數值在不停的變)以及讀取(只有少數時刻會讀取這類數據)。
void AddCountByType(long type); // 大量增長某個類型的計數,修改比較頻繁
long GetCountByType(long type); // 少許返回某個類型的計數
【底層實現】
具體到底層的實現,每每是一個Map(本質是一個定長key,定長value的緩存結構)來存儲司機的信息,或者某個類型的計數。
Map<driver_id, DriverInfo>
Map<type, count>
【臨界資源】
這個Map存儲了全部信息,當併發讀寫訪問時,它做爲臨界資源,在讀寫以前,通常要進行加鎖操做,以司機信息存儲爲例:
void SetDriverInfo(long driver_id, DriverInfoinfo){
WriteLock (m_lock);
Map<driver_id>= info;
UnWriteLock(m_lock);
}
DriverInfo GetDriverInfo(long driver_id){
DriverInfo t;
ReadLock(m_lock);
t= Map<driver_id>;
UnReadLock(m_lock);
return t;
}
【併發鎖瓶頸】
假設滴滴有100w司機同時在線,每一個司機沒5秒更新一次經緯度狀態,那麼每秒就有20w次寫併發操做。假設滴滴日訂單1000w個,平均每秒大概也有300個下單,對應到查詢併發量,多是1000級別的併發讀操做。
上述實現方案沒有任何問題,但在併發量很大的時候(每秒20w寫,1k讀),鎖m_lock會成爲潛在瓶頸,在這類高併發環境下寫多讀少的業務倉井,如何來進行優化,是本文將要討論的問題。
上文中之因此鎖衝突嚴重,是由於全部司機都公用一把鎖,鎖的粒度太粗(能夠認爲是一個數據庫的「庫級別鎖」),是否可能進行水平拆分(相似於數據庫裏的分庫),把一個庫鎖變成多個庫鎖,來提升併發,下降鎖衝突呢?顯然是能夠的,把1個Map水平切分紅多個Map便可:
void SetDriverInfo(long driver_id, DriverInfoinfo){
i= driver_id % N; // 水平拆分紅N份,N個Map,N個鎖
WriteLock (m_lock [i]); //鎖第i把鎖
Map[i]<driver_id>= info; // 操做第i個Map
UnWriteLock (m_lock[i]); // 解鎖第i把鎖
}
每一個Map的併發量(變成了1/N)和數據量都下降(變成了1/N)了,因此理論上,鎖衝突會成平方指數下降。
分庫以後,仍然是庫鎖,有沒有辦法變成數據庫層面所謂的「行級鎖」呢,難道要把x條記錄變成x個Map嗎,這顯然是不現實的。
假設driver_id是遞增生成的,而且緩存的內存比較大,是能夠把Map優化成Array,而不是拆分紅N個Map,是有可能把鎖的粒度細化到最細的(每一個記錄一個鎖)。
void SetDriverInfo(long driver_id, DriverInfoinfo){
index= driver_id;
WriteLock (m_lock [index]); //超級大內存,一條記錄一個鎖,鎖行鎖
Array[index]= info; //driver_id就是Array下標
UnWriteLock (m_lock[index]); // 解鎖行鎖
}
和上一個方案相比,這個方案使得鎖衝突降到了最低,但鎖資源大增,在數據量很是大的狀況下,通常不這麼搞。數據量比較小的時候,能夠一個元素一個鎖的(典型的是鏈接池,每一個鏈接有一個鎖表示鏈接是否可用)。
上文中提到的另外一個例子,用戶操做類型計數,操做類型是有限的,即便一個type一個鎖,鎖的衝突也多是很高的,尚未方法進一步提升併發呢?
【無鎖的結果】
void AddCountByType(long type /*, int count*/){
//不加鎖
Array[type]++; // 計數++
//Array[type] += count; // 計數增長count
}
若是這個緩存不加鎖,固然能夠達到最高的併發,可是多線程對緩存中同一塊定長數據進行操做時,有可能出現不一致的數據塊,這個方案爲了提升性能,犧牲了一致性。在讀取計數時,獲取到了錯誤的數據,是不能接受的(做爲緩存,容許cache miss,卻不容許讀髒數據)。
【髒數據是如何產生的】
這個併發寫的髒數據是如何產生的呢,詳見下圖:
1)線程1對緩存進行操做,對key想要寫入value1
2)線程2對緩存進行操做,對key想要寫入value2
3)若是不加鎖,線程1和線程2對同一個定長區域進行一個併發的寫操做,可能每一個線程寫成功一半,致使出現髒數據產生,最終的結果即不是value1也不是value2,而是一個亂七八糟的不符合預期的值value-unexpected。
【數據完整性問題】
併發寫入的數據分別是value1和value2,讀出的數據是value-unexpected,數據的篡改,這本質上是一個數據完整性的問題。一般如何保證數據的完整性呢?
例子1:運維如何保證,從中控機分發到上線機上的二進制沒有被篡改?
回答:md5
例子2:即時通信系統中,如何保證接受方收到的消息,就是發送方發送的消息?
回答:發送方除了發送消息自己,還要發送消息的簽名,接收方收到消息後要校驗簽名,以確保消息是完整的,未被篡改。
噹噹噹當 => 「簽名」是一種常見的保證數據完整性的常見方案。
【加上簽名以後的流程】
加上簽名以後,不但緩存要寫入定長value自己,還要寫入定長簽名(例如16bitCRC校驗):
1)線程1對緩存進行操做,對key想要寫入value1,寫入簽名v1-sign
2)線程2對緩存進行操做,對key想要寫入value2,寫入簽名v2-sign
3)若是不加鎖,線程1和線程2對同一個定長區域進行一個併發的寫操做,可能每一個線程寫成功一半,致使出現髒數據產生,最終的結果即不是value1也不是value2,而是一個亂七八糟的不符合預期的值value-unexpected,但簽名,必定是v1-sign或者v2-sign中的任意一個
4)數據讀取的時候,不但要取出value,還要像消息接收方收到消息同樣,校驗一下簽名,若是發現簽名不一致,緩存則返回NULL,即cache miss。
固然,對應到司機地理位置,與URL訪問計數的case,除了內存緩存以前,確定須要timer對緩存中的數據按期落盤,寫入數據庫,若是cache miss,能夠從數據庫中讀取數據。
在【超高併發】,【寫多讀少】,【定長value】的【業務緩存】場景下:
1)能夠經過水平拆分來下降鎖衝突
2)能夠經過Map轉Array的方式來最小化鎖衝突,一條記錄一個鎖
3)能夠把鎖去掉,最大化併發,但帶來的數據完整性的破壞
4)能夠經過簽名的方式保證數據的完整性,實現無鎖緩存
咱們常用事務來保證數據庫層面數據的ACID特性。
舉個栗子,用戶下了一個訂單,須要修改餘額表,訂單表,流水錶,因而會有相似的僞代碼:
start transaction;
CURDtable t_account; any Exception rollback;
CURDtable t_order; any Exceptionrollback;
CURDtable t_flow; any Exceptionrollback;
commit;
若是對餘額表,訂單表,流水錶的SQL操做所有成功,則所有提交,若是任何一個出現問題,則所有回滾,以保證數據的一致性。
互聯網的業務特色,數據量較大,併發量較大,常用拆庫的方式提高系統的性能。若是進行了拆庫,餘額、訂單、流水可能分佈在不一樣的數據庫上,甚至不一樣的數據庫實例上,此時就不能用事務來保證數據的一致性了。這種狀況下如何保證數據的一致性,是今天要討論的話題。
補償事務是一種在業務端實施業務逆向操做事務,來保證業務數據一致性的方式。
舉個栗子,修改餘額表事務爲
int Do_AccountT(uid, money){
start transaction;
//餘額改變money這麼多
CURDtable t_account with money; anyException rollback return NO;
commit;
return YES;
}
那麼補償事務能夠是:
int Compensate_AccountT(uid, money){
//作一個money的反向操做
returnDo_AccountT(uid, -1*money){
}
同理,訂單表操做爲
Do_OrderT,新增一個訂單
Compensate_OrderT,刪除一個訂單
要保重餘額與訂單的一致性,可能要寫這樣的代碼:
// 執行第一個事務
int flag = Do_AccountT();
if(flag=YES){
//第一個事務成功,則執行第二個事務
flag= Do_OrderT();
if(flag=YES){
// 第二個事務成功,則成功
returnYES;
}
else{
// 第二個事務失敗,執行第一個事務的補償事務
Compensate_AccountT();
}
}
該方案的不足是:
(1)不一樣的業務要寫不一樣的補償事務,不具有通用性
(2)沒有考慮補償事務的失敗
(3)若是業務流程很複雜,if/else會嵌套很是多層
例如,若是上面的例子加上流水錶的修改,加上Do_FlowT和Compensate_FlowT,可能會變成一個這樣的if/else:
// 執行第一個事務
int flag = Do_AccountT();
if(flag=YES){
//第一個事務成功,則執行第二個事務
flag= Do_OrderT();
if(flag=YES){
// 第二個事務成功,則執行第三個事務
flag= Do_FlowT();
if(flag=YES){
//第三個事務成功,則成功
returnYES;
}
else{
// 第三個事務失敗,則執行第2、第一個事務的補償事務
flag =Compensate_OrderT();
if … else … // 補償事務執行失敗?
flag= Compensate_AccountT();
if … else … // 補償事務執行失敗?
}
}
else{
// 第二個事務失敗,執行第一個事務的補償事務
Compensate_AccountT();
if … else … // 補償事務執行失敗?
}
}
單庫是用這樣一個大事務保證一致性:
start transaction;
CURDtable t_account; any Exception rollback;
CURDtable t_order; any Exceptionrollback;
CURDtable t_flow; any Exceptionrollback;
commit;
拆分紅了多個庫,大事務會變成三個小事務:
start transaction1;
//第一個庫事務執行
CURDtable t_account; any Exception rollback;
…
// 第一個庫事務提交
commit1;
start transaction2;
//第二個庫事務執行
CURDtable t_order; any Exceptionrollback;
…
// 第二個庫事務提交
commit2;
start transaction3;
//第三個庫事務執行
CURDtable t_flow; any Exceptionrollback;
…
// 第三個庫事務提交
commit3;
一個事務,分紅執行與提交兩個階段,執行的時間實際上是很長的,而commit的執行實際上是很快的,因而整個執行過程的時間軸以下:
第一個事務執行200ms,提交1ms;
第二個事務執行120ms,提交1ms;
第三個事務執行80ms,提交1ms;
那在何時系統出現問題,會出現不一致呢?
回答:第一個事務成功提交以後,最後一個事務成功提交以前,若是出現問題(例如服務器重啓,數據庫異常等),均可能致使數據不一致。
若是改變事務執行與提交的時序,變成事務先執行,最後一塊兒提交,狀況會變成什麼樣呢:
第一個事務執行200ms;
第二個事務執行120ms;
第三個事務執行80ms;
第一個事務執行1ms;
第二個事務執行1ms;
第三個事務執行1ms;
那在何時系統出現問題,會出現不一致呢?
問題的答案與以前相同:第一個事務成功提交以後,最後一個事務成功提交以前,若是出現問題(例如服務器重啓,數據庫異常等),均可能致使數據不一致。
這個變化的意義是什麼呢?
方案一總執行時間是303ms,最後202ms內出現異常均可能致使不一致;
方案二總執行時間也是303ms,但最後2ms內出現異常纔會致使不一致;
雖然沒有完全解決數據的一致性問題,但不一致出現的機率大大下降了!
事務提交後置下降了數據不一致的出現機率,會帶來什麼反作用呢?
回答:事務提交時會釋放數據庫的鏈接,第一種方案,第一個庫事務提交,數據庫鏈接就釋放了,後置事務提交的方案,全部庫的鏈接,要等到全部事務執行完才釋放。這就意味着,數據庫鏈接佔用的時間增加了,系統總體的吞吐量下降了。
trx1.exec();
trx1.commit();
trx2.exec();
trx2.commit();
trx3.exec();
trx3.commit();
優化爲:
trx1.exec();
trx2.exec();
trx3.exec();
trx1.commit();
trx2.commit();
trx3.commit();
這個小小的改動(改動成本極低),不能完全解決多庫分佈式事務數據一致性問題,但能大大下降數據不一致的機率,帶來的反作用是數據庫鏈接佔用時間會增加,吞吐量會下降。對於一致性與吞吐量的折衷,還須要業務架構師謹慎權衡折衷。
1、需求緣起
Web-Server一般有個配置,最大工做線程數,後端服務通常也有個配置,工做線程池的線程數量,這個線程數的配置不一樣的業務架構師有不一樣的經驗值,有些業務設置爲CPU核數的2倍,有些業務設置爲CPU核數的8倍,有些業務設置爲CPU核數的32倍。
「工做線程數」的設置依據是什麼,到底設置爲多少可以最大化CPU性能,是本文要討論的問題。
在進行進一步深刻討論以前,先以提問的方式就一些共性認知達成一致。
提問:工做線程數是否是設置的越大越好?
回答:確定不是的
1)一來服務器CPU核數有限,同時併發的線程數是有限的,1核CPU設置10000個工做線程沒有意義
2)線程切換是有開銷的,若是線程切換過於頻繁,反而會使性能下降
提問:調用sleep()函數的時候,線程是否一直佔用CPU?
回答:不佔用,等待時會把CPU讓出來,給其餘須要CPU資源的線程使用
不止調用sleep()函數,在進行一些阻塞調用,例如網絡編程中的阻塞accept()【等待客戶端鏈接】和阻塞recv()【等待下游回包】也不佔用CPU資源
提問:若是CPU是單核,設置多線程有意義麼,能提升併發性能麼?
回答:即便是單核,使用多線程也是有意義的
1)多線程編碼可讓咱們的服務/代碼更加清晰,有些IO線程收發包,有些Worker線程進行任務處理,有些Timeout線程進行超時檢測
2)若是有一個任務一直佔用CPU資源在進行計算,那麼此時增長線程並不能增長併發,例如這樣的一個代碼
while(1){ i++; }
該代碼一直不停的佔用CPU資源進行計算,會使CPU佔用率達到100%
3)一般來講,Worker線程通常不會一直佔用CPU進行計算,此時即便CPU是單核,增長Worker線程也可以提升併發,由於這個線程在休息的時候,其餘的線程能夠繼續工做
瞭解常見的服務線程模型,有助於理解服務併發的原理,通常來講互聯網常見的服務線程模型有以下兩種
IO線程與工做線程經過隊列解耦類模型
如上圖,大部分Web-Server與服務框架都是使用這樣的一種「IO線程與Worker線程經過隊列解耦」類線程模型:
1)有少數幾個IO線程監聽上游發過來的請求,並進行收發包(生產者)
2)有一個或者多個任務隊列,做爲IO線程與Worker線程異步解耦的數據傳輸通道(臨界資源)
3)有多個工做線程執行正真的任務(消費者)
這個線程模型應用很廣,符合大部分場景,這個線程模型的特色是,工做線程內部是同步阻塞執行任務的(回想一下tomcat線程中是怎麼執行Java程序的,dubbo工做線程中是怎麼執行任務的),所以能夠經過增長Worker線程數來增長併發能力,今天要討論的重點是「該模型Worker線程數設置爲多少能達到最大的併發」。
純異步線程模型
任何地方都沒有阻塞,這種線程模型只須要設置不多的線程數就可以作到很高的吞吐量,Lighttpd有一種單進程單線程模式,併發處理能力很強,就是使用的的這種模型。該模型的缺點是:
1)若是使用單線程模式,難以利用多CPU多核的優點
2)程序員更習慣寫同步代碼,callback的方式對代碼的可讀性有衝擊,對程序員的要求也更高
3)框架更復雜,每每須要server端收發組件,server端隊列,client端收發組件,client端隊列,上下文管理組件,有限狀態機組件,超時管理組件的支持
however,這個模型不是今天討論的重點。
瞭解工做線程的工做模式,對量化分析線程數的設置很是有幫助:
上圖是一個典型的工做線程的處理過程,從開始處理start到結束處理end,該任務的處理共有7個步驟:
1)從工做隊列裏拿出任務,進行一些本地初始化計算,例如http協議分析、參數解析、參數校驗等
2)訪問cache拿一些數據
3)拿到cache裏的數據後,再進行一些本地計算,這些計算和業務邏輯相關
4)經過RPC調用下游service再拿一些數據,或者讓下游service去處理一些相關的任務
5)RPC調用結束後,再進行一些本地計算,怎麼計算和業務邏輯相關
6)訪問DB進行一些數據操做
7)操做完數據庫以後作一些收尾工做,一樣這些收尾工做也是本地計算,和業務邏輯相關
分析整個處理的時間軸,會發現:
1)其中1,3,5,7步驟中【上圖中粉色時間軸】,線程進行本地業務邏輯計算時須要佔用CPU
2)而2,4,6步驟中【上圖中橙色時間軸】,訪問cache、service、DB過程當中線程處於一個等待結果的狀態,不須要佔用CPU,進一步的分解,這個「等待結果」的時間共分爲三部分:
2.1)請求在網絡上傳輸到下游的cache、service、DB
2.2)下游cache、service、DB進行任務處理
2.3)cache、service、DB將報文在網絡上傳回工做線程
最後一塊兒來回答工做線程數設置爲多少合理的問題。
經過上面的分析,Worker線程在執行的過程當中,有一部計算時間須要佔用CPU,另外一部分等待時間不須要佔用CPU,經過量化分析,例如打日誌進行統計,能夠統計出整個Worker線程執行過程當中這兩部分時間的比例,例如:
1)時間軸1,3,5,7【上圖中粉色時間軸】的計算執行時間是100ms
2)時間軸2,4,6【上圖中橙色時間軸】的等待時間也是100ms
獲得的結果是,這個線程計算和等待的時間是1:1,即有50%的時間在計算(佔用CPU),50%的時間在等待(不佔用CPU):
1)假設此時是單核,則設置爲2個工做線程就能夠把CPU充分利用起來,讓CPU跑到100%
2)假設此時是N核,則設置爲2N個工做現場就能夠把CPU充分利用起來,讓CPU跑到N*100%
結論:
N核服務器,經過執行業務的單線程分析出本地計算時間爲x,等待時間爲y,則工做線程數(線程池線程數)設置爲 N*(x+y)/x,能讓CPU的利用率最大化。
經驗:
通常來講,非CPU密集型的業務(加解密、壓縮解壓縮、搜索排序等業務是CPU密集型的業務),瓶頸都在後端數據庫,本地CPU計算的時間不多,因此設置幾十或者幾百個工做線程也都是可能的。
N核服務器,經過執行業務的單線程分析出本地計算時間爲x,等待時間爲y,則工做線程數(線程池線程數)設置爲 N*(x+y)/x,能讓CPU的利用率最大化。