想設計億萬級高併發架構,你要先知道高併發是什麼?

qwer1030274531發表於2020-09-03

  當前,數字化在給企業帶來業務創新,推動企業高速發展的同時,也給企業的IT軟體系統帶來了嚴峻的挑戰。面對流量高峰,不同的企業是如何透過技術手段解決高併發難題的呢?

  0、引言

  軟體系統有三個追求:高效能、高併發、高可用,俗稱三高。三者既有區別也有聯絡,門門道道很多,全面討論需要三天三夜,本篇討論高併發。

  高併發(High Concurrency)。併發是作業系統領域的一個概念,指的是一段時間內多工流交替執行的現象,後來這個概念被泛化,高併發用來指大流量、高請求的業務情景,比如春運搶票,電商雙十一,秒殺大促等場景。

  很多程式設計師每天忙著搬磚,平時接觸不到高併發,哪天受不了跑去面試,還常常會被面試官犀利的高併發問題直接KO,其實吧,高併發系統也不高深,我保證任何一個智商線上的看過這篇文章後,都能戰勝恐懼,重拾生活的信心。

  本文先介紹高併發系統的度量指標,然後講述高併發系統的設計思路,再梳理高併發的關鍵技術,最後結合作者的經驗做一些延伸探討。

  1、高併發的度量指標

  既然是高併發系統,那併發一定要高,不然就名不副實。併發的指標一般有QPS、TPS、IOPS,這幾個指標都是可歸為系統吞吐率,QPS越高系統能hold住的請求數越多,但光關注這幾個指標不夠,我們還需要關注RT,即響應時間,也就是從發出request到收到response的時延,這個指標跟吞吐往往是此消彼長的,我們追求的是一定時延下的高吞吐。

  比如有100萬次請求,99萬次請求都在10毫秒內響應,其他次數10秒才響應,平均時延不高,但時延高的使用者受不了,所以,就有了TP90/TP99指標,這個指標不是求平均,而是把時延從小到大排序,取排名90%/99%的時延,這個指標越大,對慢請求越敏感。

  除此之外,有時候,我們也會關注可用性指標,這可歸到穩定性。

  一般而言,使用者感知友好的高併發系統,時延應該控制在250毫秒以內。

  什麼樣的系統才能稱為高併發?這個不好回答,因為它取決於系統或者業務的型別。不過我可以告訴你一些眾所周知的指標,這樣能幫助你下次在跟人扯淡的時候稍微靠點兒譜,不至於貽笑大方。

  通常,資料庫單機每秒也就能抗住幾千這個量級,而做邏輯處理的服務單臺每秒抗幾萬、甚至幾十萬都有可能,而訊息佇列等中介軟體單機每秒處理個幾萬沒問題,所以我們經常聽到每秒處理數百萬、數千萬的訊息中介軟體叢集,而像阿某的API閘道器,每日百億請求也有可能。

  2、高併發的設計思路

  高併發的設計思路有兩個方向:

  垂直方向擴充套件,也叫豎向擴充套件

  水平方向擴充套件,也叫橫向擴充套件

  垂直方向:提升單機能力

  提升單機處理能力又可分為硬體和軟體兩個方面:

  硬體方向,很好理解,花錢升級機器,更多核更高主頻更大儲存空間更多頻寬

  軟體方向,包括用各快的資料結構,改進架構,應用多執行緒、協程,以及上效能最佳化各種手段,但這玩意兒天花板低,就像提升個人產出一樣,996、007、最多24 X 7。

  水平方向:分散式叢集

  為了解決分散式系統的複雜性問題,一般會用到架構分層和服務拆分,透過分層做隔離,透過微服務解耦。

  這個理論上沒有上限,只要做好層次和服務劃分,加機器擴容就能滿足需求,但實際上並非如此,一方面分散式會增加系統複雜性,另一方面叢集規模上去之後,也會引入一堆AIOps、服務發現、服務治理的新問題。

  因為垂直向的限制,所以,我們通常更關注水平擴充套件,高併發系統的實施也主要圍繞水平方向展開。

  3、高併發的關鍵技術

  玩具式的網路服務程式,使用者可以直連伺服器,甚至不需要資料庫,直接寫磁碟檔案。但春運購票系統顯然不能這麼做,它肯定扛不住這個壓力,那一般的高併發系統是怎麼做呢?比如某寶這樣的正經系統是怎麼處理高併發的呢?

  其實大的思路都差不多,層次劃分 + 功能劃分。可以把層次劃分理解為水平方向的劃分,而功能劃分理解為垂直方向的劃分。

  首先,使用者不能直連伺服器,要做分散式就要解決“分”的問題,有多個服務例項就需要做負載均衡,有不同服務型別就需要服務發現。

  叢集化:負載均衡

  負載均衡就是把負載(request)均衡分配到不同的服務例項,利用叢集的能力去對抗高併發,負載均衡是服務叢集化的實施要素,它分3種:

  DNS負載均衡,客戶端透過URL發起網路服務請求的時候,會去DNS伺服器做域名解釋,DNS會按一定的策略(比如就近策略)把URL轉換成IP地址,同一個URL會被解釋成不同的IP地址,這便是DNS負載均衡,它是一種粗粒度的負載均衡,它只用URL前半部分,因為DNS負載均衡一般採用就近原則,所以通常能降低時延,但DNS有cache,所以也會更新不及時的問題。

  硬體負載均衡,透過佈置特殊的負載均衡裝置到機房做負載均衡,比如F5,這種裝置貴,效能高,可以支撐每秒百萬併發,還能做一些安全防護,比如防火牆。

  軟體負載均衡,根據工作在ISO 7層網路模型的層次,可分為四層負載均衡(比如章文嵩博士的LVS)和七層負載均衡(NGINX),軟體負載均衡配置靈活,擴充套件性強,阿某雲的SLB作為服務對外售賣,Nginx可以對URL的後半部做解釋承擔API閘道器的職責。

  所以,完整的負載均衡鏈路是 client <-> DNS負載均衡 -> F5 -> LVS/SLB -> NGINX

  不管選擇哪種LB策略,或者組合LB策略,邏輯上,我們都可以視為負載均衡層,透過新增負載均衡層,我們將負載均勻分散到了後面的服務叢集,具備基礎的高併發能力,但這只是萬里長征第一步。

  資料庫層面:分庫分表+讀寫分離

  前面透過負載均衡解決了無狀態服務的水平擴充套件問題,但我們的系統不全是無狀態的,後面通常還有有狀態的資料庫,所以解決了前面的問題,儲存有可能成為系統的瓶頸,我們需要對有狀態儲存做分片路由。

  資料庫的單機QPS一般不高,也就幾千,顯然滿足不了高併發的要求。

  所以,我們需要做分庫分表 + 讀寫分離。

  就是把一個庫分成多個庫,部署在多個資料庫服務上,主庫承載寫請求,從庫承載讀請求。從庫可以掛載多個,因為很多場景寫的請求遠少於讀的請求,這樣就把對單個庫的壓力降下來了。

  如果寫的請求上升就繼續分庫分表,如果讀的請求上升就掛更多的從庫,但資料庫天生不是很適合高併發,而且資料庫對機器配置的要求一般很高,導致單位服務成本高,所以,這樣加機器抗壓力成本太高,還得另外想招。

  讀多寫少:快取

  快取的理論依據是區域性性原理。

  一般系統的寫入請求遠少於讀請求,針對寫少讀多的場景,很適合引入快取叢集。

  在寫資料庫的時候同時寫一份資料到快取叢集裡,然後用快取叢集來承載大部分的讀請求,因為快取叢集很容易做到高效能,所以,這樣的話,透過快取叢集,就可以用更少的機器資源承載更高的併發。

  快取的命中率一般能做到很高,而且速度很快,處理能力也強(單機很容易做到幾萬併發),是理想的解決方案。

  CDN本質上就是快取,被使用者大量訪問的靜態資源快取在CDN中是目前的通用做法。

  快取也有很多需要謹慎處理的問題:

  一致性問題:(a)更新db成功+更新cache失敗 -> 不一致 (b)更新db失敗+更新cache成功 -> 不一致 ©更新db成功+淘汰快取失敗 -> 不一致

  快取穿透:查詢一定不存在的資料,會穿透快取直接壓到資料庫,從而導致快取失去作用,如果有人利用這個漏洞,大量查詢一定不存在的資料,會對資料庫造成壓力,甚至打掛資料庫。解決方案:布隆過濾器 或者 簡單的方案,查詢不存在的key,也把空結果寫入快取(設定較短的過期淘汰時間),從而降低命失

  快取雪崩:如果大量快取在一個時刻同時失效,則請求會轉到DB,則對DB形成壓迫,導致雪崩。簡單的解決方案是為快取失效時間新增隨機值,降低同一時間點失效淘汰快取數,避免集體失效事件發生

  但快取是針對讀,如果寫的壓力很大,怎麼辦?

  高寫入:訊息中介軟體

  同理,透過跟主庫加機器,耗費的機器資源是很大的,這個就是資料庫系統的特點所決定的。

  相同的資源下,資料庫系統太重太複雜,所以併發承載能力就在幾千/s的量級,所以此時你需要引入別的一些技術。

  比如說訊息中介軟體技術,也就是MQ叢集,它是非常好的做寫請求非同步化處理,實現削峰填谷的效果。

  訊息佇列能做解耦,在只需要最終一致性的場景下,很適合用來配合做流控。

  假如說,每秒是1萬次寫請求,其中比如5千次請求是必須請求過來立馬寫入資料庫中的,但是另外5千次寫請求是可以允許非同步化等待個幾十秒,甚至幾分鐘後才落入資料庫內的。

  那麼此時完全可以引入訊息中介軟體叢集,把允許非同步化的每秒5千次請求寫入MQ,然後基於MQ做一個削峰填谷。比如就以平穩的1000/s的速度消費出來然後落入資料庫中即可,此時就會大幅度降低資料庫的寫入壓力。

  業界有很多著名的訊息中介軟體,比如ZeroMQ,rabbitMQ,kafka等。

  訊息佇列本身也跟快取系統一樣,可以用很少的資源支撐很高的併發請求,用它來支撐部分允許非同步化的高併發寫入是很合適的,比使用資料庫直接支撐那部分高併發請求要減少很多的機器使用量。

  避免擠兌:流控

  再強大的系統,也怕流量短事件內集中爆發,就像銀行怕擠兌一樣,所以,高併發另一個必不可少的模組就是流控。

  流控的關鍵是流控演算法,有4種常見的流控演算法。

  計數器演算法(固定視窗):計數器演算法是使用計數器在週期內累加訪問次數,當達到設定的限流值時,觸發限流策略,下一個週期開始時,進行清零,重新計數,實現簡單。計數器演算法方式限流對於週期比較長的限流,存在很大的弊端,有嚴重的臨界問題。

  滑動視窗演算法:將時間週期分為N個小週期,分別記錄每個小週期內訪問次數,並且根據時間滑動刪除過期的小週期,當滑動視窗的格子劃分的越多,那麼滑動視窗的滾動就越平滑,限流的統計就會越精確。此演算法可以很好的解決固定視窗演算法的臨界問題。

  漏桶演算法:訪問請求到達時直接放入漏桶,如當前容量已達到上限(限流值),則進行丟棄(觸發限流策略)。漏桶以固定的速率進行釋放訪問請求(即請求透過),直到漏桶為空。分散式環境下實施難度高。

  令牌桶演算法:程式以r(r=時間週期/限流值)的速度向令牌桶中增加令牌,直到令牌桶滿,請求到達時向令牌桶請求令牌,如獲取到令牌則透過請求,否則觸發限流策略。分散式環境下實施難度高。

  4、高併發的實踐經驗

  接入-邏輯-儲存是經典的網際網路後端分層,但隨著業務規模的提高,邏輯層的複雜度也上升了,所以,針對邏輯層的架構設計也出現很多新的技術和思路,常見的做法包括系統拆分,微服務。

  除此之外,也有很多業界的優秀實踐,包括某信伺服器透過協程(無侵入,已開源libco)改造,極大的提高了系統的併發度和穩定性,另外,快取預熱,預計算,批次讀寫(減少IO),池技術等也廣泛應用在實踐中,有效的提升了系統併發能力。

  為了提升併發能力,邏輯後端對請求的處理,一般會用到生產者-消費者多執行緒模型,即I/O執行緒負責網路IO,協議編解碼,網路位元組流被解碼後產生的協議物件,會被包裝成task投入到task queue,然後worker執行緒會從該佇列取出task執行,有些系統會用多程式而非多執行緒,透過共享儲存,維護2個方向的shm queue,一個input q,一個output q,為了提高併發度,有時候會引入協程,協程是使用者執行緒態的多執行流,它的切換成本更低,通常有更好的排程效率。

  另外,構建漏斗型業務或者系統,從客戶端請求到接入層,到邏輯層,到DB層,層層遞減,過濾掉請求,Fail Fast(儘早發現儘早過濾),嘴大屁眼小,哈哈。

  漏斗型系統不僅僅是一個技術模型,它也可以是一個產品思維,配合產品的使用者分流,邏輯分離,可以構建全方位的立體模型。

  5、小結

  莫讓浮雲遮望眼,除去繁華識真顏。我們不能掌握了大方案,吹完了牛皮,而忽視了程式設計最本質的東西,掌握最基本最核心的程式設計能力,比如資料架構和演算法,設計,慣用法,培養技術的審美,也是很重要的,既要致高遠,又要盡精微。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/30239065/viewspace-2716872/,如需轉載,請註明出處,否則將追究法律責任。

相關文章