高併發與高可用

来源:https://www.cnblogs.com/matd/archive/2019/03/02/10460947.html
-Advertisement-
Play Games

究竟啥才是互聯網架構“高併發” 一、什麼是高併發 高併發(High Concurrency)是互聯網分散式系統架構設計中必須考慮的因素之一,它通常是指,通過設計保證系統能夠同時並行處理很多請求。 高併發相關常用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查 ...


究竟啥才是互聯網架構“高併發”

一、什麼是高併發

高併發(High Concurrency)是互聯網分散式系統架構設計中必須考慮的因素之一,它通常是指,通過設計保證系統能夠同時並行處理很多請求。

 

高併發相關常用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查詢率QPS(Query Per Second),併發用戶數等。

 

響應時間:系統對請求做出響應的時間。例如系統處理一個HTTP請求需要200ms,這個200ms就是系統的響應時間。

吞吐量:單位時間內處理的請求數量。

QPS:每秒響應請求數。在互聯網領域,這個指標和吞吐量區分的沒有這麼明顯。

併發用戶數:同時承載正常使用系統功能的用戶數量。例如一個即時通訊系統,同時線上量一定程度上代表了系統的併發用戶數。

 

二、如何提升系統的併發能力

互聯網分散式架構設計,提高系統併發能力的方式,方法論上主要有兩種:垂直擴展(Scale Up)與水平擴展(Scale Out)。

垂直擴展:提升單機處理能力。垂直擴展的方式又有兩種:

(1)增強單機硬體性能,例如:增加CPU核數如32核,升級更好的網卡如萬兆,升級更好的硬碟如SSD,擴充硬碟容量如2T,擴充系統記憶體如128G;

(2)提升單機架構性能,例如:使用Cache來減少IO次數,使用非同步來增加單服務吞吐量,使用無鎖數據結構來減少響應時間;

 

在互聯網業務發展非常迅猛的早期,如果預算不是問題,強烈建議使用“增強單機硬體性能”的方式提升系統併發能力,因為這個階段,公司的戰略往往是發展業務搶時間,而“增強單機硬體性能”往往是最快的方法。

 

不管是提升單機硬體性能,還是提升單機架構性能,都有一個致命的不足:單機性能總是有極限的。所以互聯網分散式架構設計高併發終極解決方案還是水平擴展。

 

水平擴展:只要增加伺服器數量,就能線性擴充系統性能。水平擴展對系統架構設計是有要求的,如何在架構各層進行可水平擴展的設計,以及互聯網公司架構各層常見的水平擴展實踐,是本文重點討論的內容。

 

三、常見的互聯網分層架構


常見互聯網分散式架構如上,分為:

(1)客戶端層:典型調用方是瀏覽器browser或者手機應用APP

(2)反向代理層:系統入口,反向代理

(3)站點應用層:實現核心應用邏輯,返回html或者json

(4)服務層:如果實現了服務化,就有這一層

(5)數據-緩存層:緩存加速訪問存儲

(6)數據-資料庫層:資料庫固化數據存儲

整個系統各層次的水平擴展,又分別是如何實施的呢?

 

四、分層水平擴展架構實踐

反向代理層的水平擴展


反向代理層的水平擴展,是通過“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倍,寫仍然是單點,寫性能不變;

 

緩存層的水平拆分和資料庫層的水平拆分類似,也是以範圍拆分和哈希拆分的方式居多,就不再展開。

 

五、總結

高併發(High Concurrency)是互聯網分散式系統架構設計中必須考慮的因素之一,它通常是指,通過設計保證系統能夠同時並行處理很多請求。

提高系統併發能力的方式,方法論上主要有兩種:垂直擴展(Scale Up)與水平擴展(Scale Out)。前者垂直擴展可以通過提升單機硬體性能,或者提升單機架構性能,來提高併發性,但單機性能總是有極限的,互聯網分散式架構設計高併發終極解決方案還是後者:水平擴展。

互聯網分層架構中,各層次水平擴展的實踐又有所不同:

(1)反向代理層可以通過“DNS輪詢”的方式來進行水平擴展;

(2)站點層可以通過nginx來進行水平擴展;

(3)服務層可以通過服務連接池來進行水平擴展;

(4)資料庫可以按照數據範圍,或者數據哈希的方式來進行水平擴展;

各層實施水平擴展後,能夠通過增加伺服器數量的方式來提升系統的性能,做到理論上的性能無限。

 

 

 

高併發的常見應對方案

一、關於併發我們說的高併發是什麼?

在互聯網時代,高併發,通常是指,在某個時間點,有很多個訪問同時到來。

 

高併發,通常關心的系統指標與業務指標?

  • QPS:每秒鐘查詢量,廣義的,通常指指每秒請求數

  • 響應時間:從請求發出到收到響應花費的時間,例如:系統處理一個HTTP請求需要100ms,這個100ms就是系統的響應時間

  • 帶寬:計算帶寬大小需關註兩個指標,峰值流量和頁面的平均大小 

  • PV:綜合瀏覽量(Page View),即頁面瀏覽量或者點擊量,通常關註在24小時內訪問的頁面數量,即“日PV”

  • UV:獨立訪問(UniQue Visitor),即去重後的訪問用戶數,通常關註在24小時內訪問的用戶,即“日UV”

 

二、關於三種應對大併發的常見優化方案

【資料庫緩存】

為什麼是要使用緩存?

緩存數據是為了讓客戶端很少甚至不訪問資料庫,減少磁碟IO,提高併發量,提高應用數據的響應速度。

 

【CDN加速】

什麼是CDN?

CDN的全稱是Content Delivery Network,CDN系統能夠實時地根據網路流量和各節點的連接、負載狀況以及到用戶的距離等綜合信息將用戶的請求重新導向離用戶最近的服務節點上。

 

使用CDN的優勢?

CDN的本質是記憶體緩存,就近訪問,它提高了企業站點(尤其含有大量圖片和靜態頁面站點)的訪問速度,跨運營商的網路加速,保證不同網路的用戶都得到良好的訪問質量。

 

同時,減少遠程訪問的帶寬,分擔網路流量,減輕原站點WEB伺服器負載。

 

【伺服器的集群化,以及負載均衡】

什麼是七層負載均衡?

七層負載均衡,是基於http協議等應用信息的負載均衡,最常用的就是Nginx,它能夠自動剔除工作不正常的後端伺服器,上傳文件使用非同步模式,支持多種分配策略,可以分配權重,分配方式靈活。

 

內置策略:IP Hash、加權輪詢

擴展策略:fair策略、通用hash、一致性hash

 

什麼是加權輪詢策略?

首先將請求都分給高權重的機器,直到該機器的權值降到了比其他機器低,才開始將請求分給下一個高權重的機器,即體現了加權權重,又體現了輪詢。

 

 

 

究竟啥才是互聯網架構“高可用”

一、什麼是高可用

高可用HAHigh Availability)是分散式系統架構設計中必須考慮的因素之一,它通常是指,通過設計減少系統不能提供服務的時間。

假設系統一直能夠提供服務,我們說系統的可用性是100%。

如果系統每運行100個時間單位,會有1個時間單位無法提供服務,我們說系統的可用性是99%。

很多公司的高可用目標是4個9,也就是99.99%,這就意味著,系統的年停機時間為8.76個小時。

百度的搜索首頁,是業內公認高可用保障非常出色的系統,甚至人們會通過www.baidu.com 能不能訪問來判斷“網路的連通性”,百度高可用的服務讓人留下啦“網路通暢,百度就能訪問”,“百度打不開,應該是網路連不上”的印象,這其實是對百度HA最高的褒獎。

 

二、如何保障系統的高可用

我們都知道,單點是系統高可用的大敵,單點往往是系統高可用最大的風險和敵人,應該儘量在系統設計的過程中避免單點。方法論上,高可用保證的原則是“集群化”,或者叫“冗餘”:只有一個單點,掛了服務會受影響;如果有冗餘備份,掛了還有其他backup能夠頂上。

保證系統高可用,架構設計的核心準則是:冗餘。

有了冗餘之後,還不夠,每次出現故障需要人工介入恢復勢必會增加系統的不可服務實踐。所以,又往往是通過“自動故障轉移”來實現系統的高可用。

接下來我們看下典型互聯網架構中,如何通過冗餘+自動故障轉移來保證系統的高可用特性。

 

三、常見的互聯網分層架構


常見互聯網分散式架構如上,分為:

(1)客戶端層:典型調用方是瀏覽器browser或者手機應用APP

(2)反向代理層:系統入口,反向代理

(3)站點應用層:實現核心應用邏輯,返回html或者json

(4)服務層:如果實現了服務化,就有這一層

(5)數據-緩存層:緩存加速訪問存儲

(6)數據-資料庫層:資料庫固化數據存儲

整個系統的高可用,又是通過每一層的冗餘+自動故障轉移來綜合實現的。

 

四、分層高可用架構實踐

【客戶端層->反向代理層】的高可用


【客戶端層】到【反向代理層】的高可用,是通過反向代理層的冗餘來實現的。以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,這個切換過程對調用方是透明的。

 

五、總結

高可用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嗎,這顯然是不現實的。

 

三、MAP變Array+最細鎖粒度優化

假設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{

         // 第三個事務失敗,則執行第二、第一個事務的補償事務

         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();

這個小小的改動(改動成本極低),不能徹底解決多庫分散式事務數據一致性問題,但能大大降低數據不一致的概率,帶來的副作用是資料庫連接占用時間會增長,吞吐量會降低。對於一致性與吞吐量的折衷,還需要業務架構師謹慎權衡折衷。

 

 

 

 

線程數究竟設多少合理

一、需求緣起

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線程與工作線程通過隊列解耦類模型

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 本次做後臺管理系統,採用的是 AntD 框架。涉及到圖片的上傳,用的是AntD的 "upload" 組件。 前端做文件上傳這個功能,是很有技術難度的。既然框架給我們提供好了,那就直接用唄。結果用的時候,發現 upload 組件的很多bug。下麵來列舉幾個。 備註:本文寫於2019 03 02, ...
  • 背景 公司為提高客服部門溝通效率對接電話呼叫中心,調研後選擇了亞馬遜的Amazon Connect服務,因為是國外業務沒有選擇用阿裡雲,怕有坑。 Amazon Connect後臺 需要在後臺創建“聯繫流”,也就是用戶接通電話後我們提供的一系列功能選項,比如開始放一段歡迎語音,然後提示用戶選擇1,2, ...
  • 今天我們來詳解一下git的各種命令,此為git的第一篇,後續還會有好幾篇,希望大家看了能有所進步 Git Commit Git 倉庫中的提交記錄保存的是你的目錄下所有文件的快照,就像是把整個目錄複製,然後再粘貼一樣,但比複製粘貼優雅許多! Git 希望提交記錄儘可能地輕量,因此在你每次進行提交時,它 ...
  • 在JavaScript中,使用var創建變數,會創建全局變數或局部變數。 只有在非函數內創建的變數,才是全局變數,該變數可以在任何地方被讀取。 而在函數內創建變數時,只有在函數內部才可讀取。在函數外部時,調用函數也無法讀取局部變數。 function test(){ var g = 5; } // ...
  • 計算屬性 1.1 什麼是計算屬性: 插值表達式常用於簡單的運算,當其過長或邏輯複雜時,會難以維護,這時應該使用計算屬性。 插值表達式里的值是JS表達式 所有的計算屬性都以函數的形式寫在Vue實例內的computed選項內,最終返回計算後的結果。 1.2 計算屬性的用法 在一個計算屬性里可以完成各種復 ...
  • MVC設計模式:modle層,view層,controller層 以前學習的servlet其實就是一個java類,或者說經過規範的java類,實際進行跳轉時,還是要在web.xml文件中配置才能正常跳轉。 controller層可以放servlet,在SpringMVC中則可以創建java類通過@c ...
  • 1.hello 1.hello 1.hello 1.hello 消息生產者p將消息放入隊列 消費者監聽隊列,如果隊列中有消息,就消費掉,消息被拿走後,自動從隊列刪除(隱患,消息可能沒有被消費者正確處理,已經消失了,無法恢復) 應用場景:聊天室 案例: 1>.首先準備依賴 <dependency> < ...
  • 環境配置 部署環境 部署環境:windows server 2008 r2 enterprise 官方安裝部署文檔:http://www.rabbitmq.com/install-windows.html官方文檔說明 下載erlang 原因在於RabbitMQ服務端代碼是使用併發式語言erlang編 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...