僅提煉課程小結,分享設計思路,有興趣的也可以去購買學習該門課程 《高併發系統設計》
高併發系統:它的通用設計方法是什麼?
高併發系統設計的三種通用方法:Scale-out、快取和非同步。
這三種方法可以在做方案設計時靈活地運用,但它不是具體實施的方案,而是三種思想,在實際運用中會千變萬化。
就拿 Scale-out 來說,資料庫一主多從、分庫分表、儲存分片都是它的實際應用方案。
需要注意的是,在應對高併發大流量的時候,系統是可以透過增加機器來承擔流量衝擊的,至於要採用什麼樣的方案還是要具體問題具體分析。
架構分層:我們為什麼一定要這麼做?
分層架構是軟體設計思想的外在體現,是一種實現方式。我們熟知的一些軟體設計原則都在分層架構中有所體現。
比方說,單一職責原則規定每個類只有單一的功能,在這裡可以引申為每一層擁有單一職責,且層與層之間邊界清晰;
迪米特法則原意是一個物件應當對其它物件有儘可能少的瞭解,在分層架構的體現是資料的互動不能跨層,只能在相鄰層之間進行;
開閉原則要求軟體對擴充套件開放,對修改關閉。它的含義其實就是將抽象層和實現層分離,抽象層是對實現層共有特徵的歸納總結,不可以修改,但是具體的實現是可以無限擴充套件,隨意替換的。
掌握這些設計思想會自然而然地明白分層架構設計的妙處,同時也能幫助我們做出更好的設計方案。
系統設計目標(一):如何提升系統效能?
有時候你在遇到效能問題的時候會束手無策,需要強調幾點:
- 資料優先,做一個新的系統在上線之前一定要把效能監控系統做好;
- 掌握一些效能最佳化工具和方法,這就需要在工作中不斷的積累;
- 計算機基礎知識很重要,比如說網路知識、作業系統知識等等,掌握了基礎知識才能讓你在最佳化過程中抓住效能問題的關鍵,也能在效能最佳化過程中游刃有餘。
效能最佳化是一個很大的話題,比方說如何用快取最佳化系統的讀取效能,如何使用訊息佇列最佳化系統的寫入效能等等。
系統設計目標(二):系統怎樣做到高可用?
從開發和運維角度上來看,提升可用性的方法是不同的:
開發注重的是如何處理故障,關鍵詞是冗餘和取捨。冗餘指的是有備用節點,叢集來頂替出故障的服務,比如文中提到的故障轉移,還有多活架構等等;取捨指的是丟卒保車,保障主體服務的安全。
從運維角度來看則更偏保守,注重的是如何避免故障的發生,比如更關注變更管理以及如何做故障的演練。
兩者結合起來才能組成一套完善的高可用體系。
需要注意的是,提高系統的可用性有時候是以犧牲使用者體驗或者是犧牲系統效能為前提的,也需要大量人力來建設相應的系統,完善機制。所以要把握一個度,不該做過度的最佳化。
另外,一般的系統或者元件都是追求極致的效能的,那麼有沒有不追求效能,只追求極致的可用性的呢?
答案是有的。
比如配置下發的系統,它只需要在其它系統啟動時提供一份配置即可,所以秒級返回也可,十秒鐘也 OK,無非就是增加了其它系統的啟動時間而已。但是,它對可用性的要求是極高的,甚至會到六個九,原因是配置可以獲取的慢,但是不能獲取不到。這個例子說明,可用性和效能有時候是需要做取捨的,但如何取捨就要視不同的系統而定,不能一概而論了。
系統設計目標(三):如何讓系統易於擴充套件?
未做拆分的系統雖然可擴充套件性不強,但是卻足夠簡單,無論是系統開發還是執行維護都不需要投入很大的精力。
拆分之後,需求開發需要橫跨多個系統多個小團隊,排查問題也需要涉及多個系統,執行維護上,可能每個子系統都需要有專人來負責,對於團隊是一個比較大的考驗。
池化技術:如何減少頻繁建立資料庫連線的效能損耗?
案列:垂直電商系統中,在遇到資料庫查詢效能下降的問題時,使用資料庫連線池解決了頻繁建立連線帶來的效能問題,使用執行緒池提升了並行查詢資料庫的效能。
其實,連線池和執行緒池你並不陌生,本章強調的重點是:
池子的最大值和最小值的設定很重要,初期可以依據經驗來設定,後面還是需要根據實際執行情況做調整。
池子中的物件需要在使用之前預先初始化完成,這叫做池子的預熱,比方說使用執行緒池時就需要預先初始化所有的核心執行緒。如果池子未經過預熱可能會導致系統重啟後產生比較多的慢請求。
池化技術核心是一種空間換時間最佳化方法的實踐,所以要關注空間佔用情況,避免出現空間過度使用出現記憶體洩露或者頻繁垃圾回收等問題。
資料庫最佳化方案(一):查詢請求增加時,如何做主從分離?
查詢量增加時,可以透過主從分離和一主多從部署抵抗增加的資料庫流量的,除了掌握主從複製的技術之外,還需要了解主從分離會帶來什麼問題以及它們的解決辦法。本章明確的要點主要有:
- 主從讀寫分離以及部署一主多從可以解決突發的資料庫讀流量,是一種資料庫橫向擴充套件的方法;
- 讀寫分離後,主從的延遲是一個關鍵的監控指標,可能會造成寫入資料之後立刻讀的時候讀取不到的情況;
- 業界有很多的方案可以遮蔽主從分離之後資料庫訪問的細節,讓開發人員像是訪問單一資料庫一樣,包括有像 TDDL、Sharding-JDBC 這樣的嵌入應用內部的方案,也有像 Mycat 這樣的獨立部署的代理方案。
其實,我們可以把主從複製引申為儲存節點之間互相複製儲存資料的技術,它可以實現資料的冗餘,以達到備份和提升橫向擴充套件能力的作用。在使用主從複製這個技術點時,你一般會考慮兩個問題:
主從的一致性和寫入效能的權衡,如果要保證所有從節點都寫入成功,那麼寫入效能一定會受影響;如果只寫入主節點就返回成功,那麼從節點就有可能出現資料同步失敗的情況,從而造成主從不一致,而在網際網路的專案中,一般會優先考慮效能而不是資料的強一致性。
主從的延遲問題,很多詭異的讀取不到資料的問題都可能會和它有關,如果遇到這類問題不妨先看看主從延遲的資料。
很多元件都會使用到這個技術,比如,
Redis 也是透過主從複製實現讀寫分離;Elasticsearch 中儲存的索引分片也可以被複制到多個節點中;寫入到 HDFS 中檔案也會被複制到多個 DataNode 中。
只是不同的元件對於複製的一致性、延遲要求不同,採用的方案也不同。不過,這種設計的思想是通用的。
資料庫最佳化方案(二):寫入資料量增加時,如何實現分庫分表?
總的來說,在面對資料庫容量瓶頸和寫併發量大的問題時,可以採用垂直拆分和水平拆分來解決,不過要注意,這兩種方式雖然能夠解決問題,但是也會引入諸如查詢資料必須帶上分割槽鍵,列表總數需要單獨冗餘儲存等問題。
而且,需要了解的是在實現分庫分表過程中,資料從單庫單表遷移多庫多表是一件即繁雜又容易出錯的事情,而且如果初期沒有規劃得當,後面要繼續增加資料庫數或者表數時,還要經歷這個遷移的過程。所以,對於分庫分表的原則主要有以下幾點:
- 如果在效能上沒有瓶頸點那麼就儘量不做分庫分表;
- 如果要做,就儘量一次到位,比如說 16 庫,每個庫 64 表就基本能夠滿足為了幾年內你的業務的需求。
- 很多的 NoSQL 資料庫,例如 Hbase,MongoDB 都提供 auto sharding 的特性,如果團隊內部對於這些元件比較熟悉,有較強的運維能力,那麼也可以考慮使用這些 NoSQL 資料庫替代傳統的關係型資料庫。
其實,有很多人並沒有真正從根本上搞懂為什麼要拆分,拆分後會帶來哪些問題,只是一味地學習大廠現有的拆分方法,從而導致問題頻出。所以,在使用一個方案解決一個問題的時候一定要弄清楚原理,搞清楚這個方案會帶來什麼問題,要如何來解決,要知其然也知其所以然,這樣才能在解決問題的同時避免踩坑。
發號器:如何保證分庫分表後ID的全域性唯一性?
使用 Snowflake 演算法解決分庫分表後資料庫 ID 的全域性唯一的問題,
生成的 ID 需要滿足單調遞增性,以及要具有一定業務含義的特性。
本章重點在於如何將 Snowflake 演算法落地,以及在落地過程中遇到了哪些坑,如何去解決它。
Snowflake 的演算法並不複雜,在使用的時候可以不考慮獨立部署的問題,先想清楚按照自身的業務場景,需要如何設計 Snowflake 演算法中的每一部分佔的二進位制位數。比如你的業務會部署幾個 IDC,應用伺服器要部署多少臺機器,每秒鐘發號個數的要求是多少等等,然後在業務程式碼中實現一個簡單的版本先使用,等到應用伺服器數量達到一定規模,再考慮獨立部署的問題就可以了。這樣可以避免多維護一套發號器服務,減少了運維上的複雜
NoSQL:在高併發場景下,資料庫和NoSQL如何做到互補?
NoSQL 資料庫在效能、擴充套件性上的優勢,以及它的一些特殊功能特性,主要有以下幾點:
- 在效能方面,NoSQL 資料庫使用一些演算法將對磁碟的隨機寫轉換成順序寫,提升了寫的效能;
- 在某些場景下,比如全文搜尋功能,關係型資料庫並不能高效地支援,需要 NoSQL 資料庫的支援;
- 在擴充套件性方面,NoSQL 資料庫天生支援分散式,支援資料冗餘和資料分片的特性。
這些都讓它成為傳統關係型資料庫的良好的補充,需要了解的是,NoSQL 可供選型的種類很多,每一個元件都有各自的特點。在做選型的時候需要對它的實現原理有比較深入的瞭解,最好在運維方面對它有一定的熟悉,這樣在出現問題時才能及時找到解決方案。否則,盲目跟從地上了一個新的 NoSQL 資料庫,最終可能導致會出了故障無法解決,反而成為整體系統的拖累。
案例:曾經使用 Elasticsearch 作為持久儲存,支撐社群的 feed 流功能,初期開發的時候確實很爽,可以針對 feed 中的任何欄位做靈活高效地查詢,業務功能迭代迅速,程式碼也簡單易懂。可是到了後期流量上來之後,由於缺少對於 Elasticsearch 成熟的運維能力,造成故障頻出,尤其到了高峰期就會出現節點不可用的問題,而由於業務上的巨大壓力又無法分出人力和精力對 Elasticsearch 深入的學習和了解,最後不得不做大的改造切回熟悉的 MySQL。所以,對於開源元件的使用,不能只停留在只會“hello world”的階段,而應該對它有足夠的運維上的把控能力。
快取:資料庫成為瓶頸後,動態資料的查詢要如何加速?
瞭解快取的定義,常見快取的分類以及快取的不足。本章主要強調以下幾點:
快取可以有多層,比如上面提到的靜態快取處在負載均衡層,分散式快取處在應用層和資料庫層之間,本地快取處在應用層。我們需要將請求儘量擋在上層,因為越往下層,對於併發的承受能力越差;
快取命中率是我們對於快取最重要的一個監控項,越是熱點的資料,快取的命中率就越高。
還需要理解的是,快取不僅僅是一種元件的名字,更是一種設計思想,你可以認為任何能夠加速讀請求的元件和設計方案都是快取思想的體現。而這種加速通常是透過兩種方式來實現:
- 使用更快的介質,比方說課程中提到的記憶體;
- 快取複雜運算的結果,比方說前面 TLB 的例子就是快取地址轉換的結果。
在實際工作中碰到“慢”的問題時,快取就是第一時間需要考慮的。
快取的使用姿勢(一):如何選擇快取的讀寫策略?
瞭解快取使用的幾種策略,以及每種策略適用的使用場景是怎樣的。本章重點是:
Cache Aside 是我們在使用分散式快取時最常用的策略,你可以在實際工作中直接拿來使用。
Read/Write Through 和 Write Back 策略需要快取元件的支援,所以比較適合你在實現本地快取元件的時候使用;
Write Back 策略是計算機體系結構中的策略,不過寫入策略中的只寫快取,非同步寫入後端儲存的策略倒是有很多的應用場景。
需要注意,以上提到的策略都是標準的使用姿勢,在實際開發過程中需要結合實際的業務特點靈活使用甚至加以改造。
這些業務特點包括但不僅限於:整體的資料量級情況,訪問的讀寫比例的情況,對於資料的不一致時間的容忍度,對於快取命中率的要求等等。理論結合實踐,具體情況具體分析,才能得到更好的解決方案。
快取的使用姿勢(二):快取如何做到高可用?
本章重點是:
分散式快取的高可用方案主要有三種,首先是客戶端方案,一般也稱為 Smart Client。我們透過制定一些資料分片和資料讀寫的策略,可以實現快取高可用。這種方案的好處是效能沒有損耗,缺點是客戶端邏輯複雜且在多語言環境下不能複用。
其次,中間代理方案在客戶端和快取節點之間增加了中間層,在效能上會有一些損耗,在代理層會有一些內建的高可用方案,比如 Codis 會使用 Codis Ha 或者 Sentinel。
最後,服務端方案依賴於元件的實現,Memcached 就只支援單機版沒有分散式和 HA 的方案,而 Redis 在 2.4 版本提供了 Sentinel 方案可以自動進行主從切換。服務端方案會在運維上增加一些複雜度。
總體而言,分散式快取的三種方案各有所長,
- 有些團隊可能在開發過程中已經積累了 Smart Client 上的一些經驗;而有些團隊在 Redis 運維上經驗豐富,就可以推進 Sentinel 方案;有些團隊在儲存研發方面有些積累,就可以推進中間代理層方案,甚至可以自研適合自己業務場景的代理層元件,
具體的選擇還是要看團隊的實際情況而定。
快取的使用姿勢(三):快取穿透了怎麼辦?
瞭解一些解決快取穿透的方案,可以在發現自己的快取系統命中率下降時,從中得到一些借鑑的思路。本章重點是:
回種空值是一種最常見的解決思路,實現起來也最簡單,如果評估空值快取佔據的快取空間可以接受,那麼可以優先使用這種方案;
布隆過濾器會引入一個新的元件,也會引入一些開發上的複雜度和運維上的成本。所以只有在存在海量查詢資料庫中,不存在資料的請求時才會使用,在使用時也要關注布隆過濾器對記憶體空間的消耗;
對於極熱點快取資料穿透造成的“狗樁效應”,可以透過設定分散式鎖或者後臺執行緒定時載入的方式來解決。
除此之外,還需要了解的是,資料庫是一個脆弱的資源,它無論是在擴充套件性、效能還是承擔併發的能力上,相比快取都處於絕對的劣勢,所以我們解決快取穿透問題的核心目標在於減少對於資料庫的併發請求。瞭解了這個核心的思想,也許就能在日常工作中找到其他更好的解決快取穿透問題的方案。
CDN:靜態資源如何加速?
瞭解 CDN 對靜態資源進行加速的原理和使用的核心技術,本章重點是:
- DNS 技術是 CDN 實現中使用的核心技術,可以將使用者的請求對映到 CDN 節點上;
- DNS 解析結果需要做本地快取,降低 DNS 解析過程的響應時間;
- GSLB 可以給使用者返回一個離著他更近的節點,加快靜態資源的訪問速度。
作為一個服務端開發人員,我們可能會忽略 CDN 的重要性,對於偶爾出現的 CDN 問題嗤之以鼻,覺得這個不是我們應該關心的內容,這種想法是錯的。
CDN 是我們系統的門面,其快取的靜態資料,如圖片和影片資料的請求量很可能是介面請求資料的幾倍甚至更高,一旦發生故障,對於整體系統的影響是巨大的。
另外 CDN 的頻寬歷來是我們研發成本的大頭,尤其是目前處於小影片和直播風口上,大量的小影片和直播研發團隊都在絞盡腦汁地減少 CDN 的成本。
由此看出,CDN 是我們整體系統至關重要的組成部分,而它作為一種特殊的快取,其命中率和可用性也是我們服務端開發人員需要重點關注的指標。
資料的遷移應該如何做?
本章重點是:
雙寫的方案是資料庫、Redis 遷移的通用方案,你可以在實際工作中直接加以使用。雙寫方案中最重要的,是透過資料校驗來保證資料的一致性,這樣就可以在遷移過程中隨時回滾;
如果你需要將自建機房的資料遷移到雲上,那麼也可以考慮使用級聯複製的方案,這種方案會造成資料的短暫停寫,需要在業務低峰期執行;
快取的遷移重點,是保證雲上快取的命中率,你可以使用改進版的副本組方式來遷移,在快取寫入的時候,非同步寫入雲上的副本組,在讀取時放少量流量到雲上副本組,從而又可以遷移部分資料到雲上副本組,又能儘量減少穿透給自建機房造成專線延遲的問題。
如果你作為專案的負責人,那麼在遷移的過程中,你一定要制定周密的計劃:如果是資料庫的遷移,那麼資料的校驗應該是你最需要花費時間來解決的問題。
如果是自建機房遷移到雲上,那麼專線的頻寬一定是你遷移過程中的一個瓶頸點,你需要在遷移之前梳理清楚,有哪些呼叫需要經過專線,佔用頻寬的情況是怎樣的,頻寬的延時是否能夠滿足要求。你的方案中也需要儘量做到在遷移過程中,同機房的服務,呼叫同機房的快取和資料庫,儘量減少對於專線頻寬資源的佔用。
訊息佇列:秒殺時如何處理每秒上萬次的下單請求?
本章重點是:
- 削峰填谷是訊息佇列最主要的作用,但是會造成請求處理的延遲。
- 非同步處理是提升系統效能的神器,但是你需要分清同步流程和非同步流程的邊界,同時訊息存在著丟失的風險,我們需要考慮如何確保訊息一定到達。
- 解耦合可以提升你的整體系統的魯棒性(Robust)。
當然,在使用訊息佇列之後雖然可以解決現有的問題,但是系統的複雜度也會上升。比如上面提到的業務流程中,
- 同步流程和非同步流程的邊界在哪裡?
- 訊息是否會丟失,是否會重複?
- 請求的延遲如何能夠減少?
- 訊息接收的順序是否會影響到業務流程的正常執行?
- 如果訊息處理流程失敗了之後是否需要補發?
後面章節會講解兩個主要問題:一個是如何處理訊息的丟失和重複,另一個是如何減少訊息的延遲。
引入了訊息佇列的同時也會引入了新的問題,需要新的方案來解決,這就是系統設計的挑戰,也是系統設計獨有的魅力。
訊息投遞:如何保證訊息僅僅被消費一次?
本章重點是:
- 訊息的丟失可以透過生產端的重試、訊息佇列配置叢集模式,以及消費端合理處理消費進度三個方式來解決。
- 為了解決訊息的丟失通常會造成效能上的問題以及訊息的重複問題。
- 透過保證訊息處理的冪等性可以解決訊息的重複問題。
並不是說訊息丟失一定不能被接受,應該說,在允許訊息丟失的情況下,訊息佇列的效能更好,方案實現的複雜度也最低。比如像是日誌處理的場景,日誌存在的意義在於排查系統的問題,而系統出現問題的機率不高,偶發的丟失幾條日誌是可以接受的。
所以方案設計看場景,這是一切設計的原則,不能把所有的訊息佇列都配置成防止訊息丟失的方式,也不能要求所有的業務處理邏輯都要支援冪等性,這樣會給開發和運維帶來額外的負擔。
訊息佇列:如何降低訊息佇列系統中訊息的延遲?
瞭解如何提升訊息佇列的效能來降低訊息消費的延遲,本章重點是:
- 我們可以使用訊息佇列提供的工具,或者透過傳送監控訊息的方式,來監控訊息的延遲情況;
- 橫向擴充套件消費者是提升消費處理能力的重要方式;
- 選擇高效能的資料儲存方式,配合零複製技術,可以提升訊息的消費效能。
其實,佇列是一種常用的元件,只要涉及到佇列,任務的堆積就是一個不可忽視的問題,很多故障都是源於此。
案例:某個故障,前期只是因為資料庫效能衰減有少量的慢請求,結果這些慢請求佔滿了 Tomcat 執行緒池,導致整體服務的不可用。如果能對 Tomcat 執行緒池的任務堆積情況有實時地監控,或者說對執行緒池有一些保護策略,比方說執行緒全部使用之後丟棄請求,也許就會避免故障的發生。因此實際工作中,只要有佇列就要監控它的堆積情況,把問題消滅在萌芽之中。
本作品採用《CC 協議》,轉載必須註明作者和本文連結