從0開始學架構.高性能篇
14 | 高性能資料庫叢集:讀寫分離
讀寫分離原理
讀寫分離的基本原理是将資料庫讀寫操作分散到不同的節點上,下面是其基本架構圖。

讀寫分離的基本實作是:
- 資料庫伺服器搭建主從叢集,一主一從、一主多從都可以。
- 資料庫主機負責讀寫操作,從機隻負責讀操作。
- 資料庫主機通過複制将資料同步到從機,每台資料庫伺服器都存儲了所有的業務資料。
- 業務伺服器将寫操作發給資料庫主機,将讀操作發給資料庫從機。
需要注意的是,這裡用的是“主從叢集”,而不是“主備叢集”。“從機”的“從”可以了解為“仆從”,仆從是要幫主人幹活的,“從機”是需要提供讀資料的功能的;而“備機”一般被認為僅僅提供備份功能,不提供通路功能。是以使用“主從”還是“主備”,是要看場景的,這兩個詞并不是完全等同的。
讀寫分離的實作邏輯并不複雜,但有兩個細節點将引入設計複雜度:主從複制延遲和配置設定機制
複制延遲
解決主從複制延遲有幾種常見的方法:
- 寫操作後的讀操作指定發給資料庫主伺服器。
- 讀從機失敗後再讀一次主機。“二次讀取”
- 關鍵業務讀寫操作全部指向主機,非關鍵業務采用讀寫分離
配置設定機制
将讀寫操作區分開來,然後通路不同的資料庫伺服器,一般有兩種方式:程式代碼封裝和中間件封裝。
- 程式代碼封裝
程式代碼封裝指在代碼中抽象一個資料通路層(是以有的文章也稱這種方式為“中間層封裝”),實作讀寫操作分離和資料庫伺服器連接配接的管理。例如,基于 Hibernate 進行簡單封裝,就可以實作讀寫分離,基本架構是:
目前開源的實作方案中,淘寶的 TDDL(Taobao Distributed Data Layer,外号: 頭都大了)是比較有名的。
- 中間件封裝
中間件封裝指的是獨立一套系統出來,實作讀寫操作分離和資料庫伺服器連接配接的管理。中間件對業務伺服器提供 SQL 相容的協定,業務伺服器無須自己進行讀寫分離。對于業務伺服器來說,通路中間件和通路資料庫沒有差別,事實上在業務伺服器看來,中間件就是一個資料庫伺服器。其基本架構是:
目前的開源資料庫中間件方案中,MySQL 官方先是提供了 MySQL Proxy,但 MySQL Proxy 一直沒有正式 GA,現在 MySQL 官方推薦 MySQL Router。
奇虎 360 公司也開源了自己的資料庫中間件 Atlas,Atlas 是基于 MySQL Proxy 實作的。
思考
問:資料庫讀寫分離一般應用于什麼場景?能支撐多大的業務規模?
答:并不是說一有性能問題就上讀寫分離,而是應該先優化,例如優化慢查詢,調整不合理的業務邏輯,引入緩存等,隻有确定系統沒有優化空間後,才考慮讀寫分離或者叢集
15 | 高性能資料庫叢集:分庫分表
常見的分散存儲的方法分庫分表,其中包括“分庫”和“分表”兩大類。
業務分庫
業務分庫指的是按照業務子產品将資料分散到不同的資料庫伺服器。
例如,一個簡單的電商網站,包括使用者、商品、訂單三個業務子產品,我們可以将使用者資料、商品資料、訂單資料分開放到三台不同的資料庫伺服器上,而不是将所有資料都放在一台資料庫伺服器上。
雖然業務分庫能夠分散存儲和通路壓力,但同時也帶來了新的問題,接下來我進行詳細分析。
- join 操作問題
- 事務問題
- 成本問題
分表
單表資料拆分有兩種方式:垂直分表和水準分表。
- 垂直分表
垂直分表适合将表中某些不常用且占了大量空間的列拆分出去。例如,前面示意圖中的 nickname 和 description 字段,假設我們是一個婚戀網站,使用者在篩選其他使用者的時候,主要是用 age 和 sex 兩個字段進行查詢,而 nickname 和 description 兩個字段主要用于展示,一般不會在業務查詢中用到。description 本身又比較長,是以我們可以将這兩個字段獨立到另外一張表中,這樣在查詢 age 和 sex 時,就能帶來一定的性能提升。
垂直分表引入的複雜性主要展現在表操作的數量要增加。例如,原來隻要一次查詢就可以擷取 name、age、sex、nickname、description,現在需要兩次查詢,一次查詢擷取 name、age、sex,另外一次查詢擷取 nickname、description。
- 水準分表
水準分表适合表行數特别大的表,有的公司要求單表行數超過 5000 萬就必須進行分表,這個數字可以作為參考,但并不是絕對标準,關鍵還是要看表的通路性能。對于一些比較複雜的表,可能超過 1000 萬就要分表了;而對于一些簡單的表,即使存儲資料超過 1 億行,也可以不分表。但不管怎樣,當看到表的資料量達到千萬級别時,作為架構師就要警覺起來,因為這很可能是架構的性能瓶頸或者隐患。
水準分表相比垂直分表,會引入更多的複雜性,主要表現在下面幾個方面:
- 路由。常見的路由算法有:範圍路由(一般建議分段大小在 100 萬至 2000 萬之間),Hash 路由,配置路由
- join 操作。水準分表後,資料分散在多個表中,如果需要與其他表進行 join 查詢,需要在業務代碼或者資料庫中間件中進行多次 join 查詢,然後将結果合并。
- count() 操作。常見的處理方式有下面兩種:count() 相加,記錄數表
- order by 操作。
實作方法
和資料庫讀寫分離類似,分庫分表具體的實作方式也是“程式代碼封裝”和“中間件封裝”,但實作會更複雜。讀寫分離實作時隻要識别 SQL 操作是讀操作還是寫操作,通過簡單的判斷 SELECT、UPDATE、INSERT、DELETE 幾個關鍵字就可以做到,而分庫分表的實作除了要判斷操作類型外,還要判斷 SQL 中具體需要操作的表、操作函數(例如 count 函數)、order by、group by 操作等,然後再根據不同的操作進行不同的處理。例如 order by 操作,需要先從多個庫查詢到各個庫的資料,然後再重新 order by 才能得到最終的結果。
16 | 高性能NoSQL
關系資料庫經過幾十年的發展後已經非常成熟,強大的 SQL 功能和 ACID 的屬性,使得關系資料庫廣泛應用于各式各樣的系統中,但這并不意味着關系資料庫是完美的,關系資料庫存在如下缺點。
- 關系資料庫存儲的是行記錄,無法存儲資料結構
- 關系資料庫的 schema 擴充很不友善
- 關系資料庫在大資料場景下 I/O 較高
- 關系資料庫的全文搜尋功能比較弱
針對上述問題,分别誕生了不同的 NoSQL 解決方案,這些方案與關系資料庫相比,在某些應用場景下表現更好。但世上沒有免費的午餐,NoSQL 方案帶來的優勢,本質上是犧牲 ACID 中的某個或者某幾個特性,是以我們不能盲目地迷信 NoSQL 是銀彈,而應該将 NoSQL 作為 SQL 的一個有力補充,NoSQL != No SQL,而是 NoSQL = Not Only SQL。
常見的 NoSQL 方案分為 4 類:
- K-V 存儲:解決關系資料庫無法存儲資料結構的問題,以 Redis 為代表。
- 文檔資料庫:解決關系資料庫強 schema 限制的問題,以 MongoDB 為代表。目前絕大部分文檔資料庫存儲的資料格式是 JSON。
- 列式資料庫:解決關系資料庫大資料場景下的 I/O 問題,以 HBase 為代表。
- 全文搜尋引擎:解決關系資料庫的全文搜尋性能問題,以 Elasticsearch 為代表。
全文搜尋基本原理
全文搜尋引擎的技術原理被稱為“反向索引”(Inverted index),也常被稱為反向索引、置入檔案或反向檔案,是一種索引方法,其基本原理是建立單詞到文檔的索引。之是以被稱為“倒排”索引,是和“正排“索引相對的,“正排索引”的基本原理是建立文檔到單詞的索引。我們通過一個簡單的樣例來說明這兩種索引的差異。
- 正排索引适用于根據文檔名稱來查詢文檔内容。例如,使用者在網站上單擊了“面向對象葵花寶典是什麼”,網站根據文章标題查詢文章的内容展示給使用者。
- 反向索引适用于根據關鍵詞來查詢文檔内容。例如,使用者隻是想看“設計”相關的文章,網站需要将文章内容中包含“設計”一詞的文章都搜尋出來展示給使用者。
全文搜尋的使用方式
全文搜尋引擎的索引對象是單詞和文檔,而關系資料庫的索引對象是鍵和行,兩者的術語差異很大,不能簡單地等同起來。是以,為了讓全文搜尋引擎支援關系型資料的全文搜尋,需要做一些轉換操作,即将關系型資料轉換為文檔資料。
目前常用的轉換方式是将關系型資料按照對象的形式轉換為 JSON 文檔,然後将 JSON 文檔輸入全文搜尋引擎進行索引。
全文搜尋引擎能夠基于 JSON 文檔建立全文索引,然後快速進行全文搜尋。以 Elasticsearch 為例,其索引基本原理如下:
Elastcisearch 是分布式的文檔存儲方式。它能存儲和檢索複雜的資料結構——序列化成為 JSON 文檔——以實時的方式。在 Elasticsearch 中,每個字段的所有資料都是預設被索引的。即每個字段都有為了快速檢索設定的專用反向索引。而且,不像其他多數的資料庫,它能在相同的查詢中使用所有反向索引,并以驚人的速度傳回結果。
17 | 高性能緩存架構
雖然我們可以通過各種手段來提升存儲系統的性能,但在某些複雜的業務場景下,單純依靠存儲系統的性能提升不夠的,典型的場景有:
- 需要經過複雜運算後得出的資料,存儲系統無能為力
- 讀多寫少的資料,存儲系統有心無力
緩存就是為了彌補存儲系統在這些複雜業務場景下的不足,其基本原理是将可能重複使用的資料放到記憶體中,一次生成、多次使用,避免每次使用都去通路存儲系統。
緩存能夠帶來性能的大幅提升,以 Memcache 為例,單台 Memcache 伺服器簡單的 key-value 查詢能夠達到 TPS 50000 以上,其基本的架構是:
緩存穿透
緩存穿透是指緩存沒有發揮作用,業務系統雖然去緩存查詢資料,但緩存中沒有資料,業務系統需要再次去存儲系統查詢資料。通常情況下有兩種情況:
- 存儲資料不存在
- 緩存資料生成耗費大量時間或者資源。如:爬蟲
緩存雪崩
緩存雪崩是指當**緩存失效(過期)**後引起系統性能急劇下降的情況。當緩存過期被清除後,業務系統需要重新生成緩存,是以需要再次通路存儲系統,再次進行運算,這個處理步驟耗時幾十毫秒甚至上百毫秒。而對于一個高并發的業務系統來說,幾百毫秒内可能會接到幾百上千個請求。由于舊的緩存已經被清除,新的緩存還未生成,并且處理這些請求的線程都不知道另外有一個線程正在生成緩存,是以所有的請求都會去重新生成緩存,都會去通路存儲系統,進而對存儲系統造成巨大的性能壓力。這些壓力又會拖慢整個系統,嚴重的會造成資料庫當機,進而形成一系列連鎖反應,造成整個系統崩潰。
緩存雪崩的常見解決方法有兩種:更新鎖機制和背景更新機制。
更新鎖:對于采用分布式叢集的業務系統,由于存在幾十上百台伺服器,即使單台伺服器隻有一個線程更新緩存,但幾十上百台伺服器一起算下來也會有幾十上百個線程同時來更新緩存,同樣存在雪崩的問題。是以分布式叢集的業務系統要實作更新鎖機制,需要用到分布式鎖,如 ZooKeeper。
背景更新機制:背景更新既适應單機多線程的場景,也适合分布式叢集的場景,相比更新鎖機制要簡單一些。背景更新機制還适合業務剛上線的時候進行緩存預熱。緩存預熱指系統上線後,将相關的緩存資料直接加載到緩存系統,而不是等待使用者通路才來觸發緩存加載。
緩存熱點
雖然緩存系統本身的性能比較高,但對于一些特别熱點的資料,如果大部分甚至所有的業務請求都命中同一份緩存資料,則這份資料所在的緩存伺服器的壓力也很大。例如,某明星微網誌釋出“我們”來宣告戀愛了,短時間内上千萬的使用者都會來圍觀。
緩存熱點的解決方案就是複制多份緩存副本,将請求分散到多個緩存伺服器上,減輕緩存熱點導緻的單台緩存伺服器壓力。以微網誌為例,對于粉絲數超過 100 萬的明星,每條微網誌都可以生成 100 份緩存,緩存的資料是一樣的,通過在緩存的 key 裡面加上編号進行區分,每次讀緩存時都随機讀取其中某份緩存。
緩存副本設計有一個細節需要注意,就是不同的緩存副本不要設定統一的過期時間,否則就會出現所有緩存副本同時生成同時失效的情況,進而引發緩存雪崩效應。正确的做法是設定一個過期時間範圍,不同的緩存副本的過期時間是指定範圍内的随機值。
實作方式
由于緩存的各種通路政策和存儲的通路政策是相關的,是以上面的各種緩存設計方案通常情況下都是內建在存儲通路方案中,可以采用“程式代碼實作”的中間層方式,也可以采用獨立的中間件來實作。
18 | 單伺服器高性能模式:PPC與TPC
站在架構師的角度,當然需要特别關注高性能架構的設計。高性能架構設計主要集中在兩方面:
- 盡量提升單伺服器的性能,将單伺服器的性能發揮到極緻。
- 如果單伺服器無法支撐性能,設計伺服器叢集方案。
以上兩個設計點最終都和作業系統的 I/O 模型及程序模型相關。
- I/O 模型:阻塞、非阻塞、同步、異步。
- 程序模型:單程序、多程序、多線程。
PPC
PPC 是 Process Per Connection 的縮寫,其含義是指每次有新的連接配接就建立一個程序去專門處理這個連接配接的請求,這是傳統的 UNIX 網絡伺服器所采用的模型。基本的流程圖是:
PPC 模式實作簡單,比較适合伺服器的連接配接數沒那麼多的情況,例如資料庫伺服器。對于普通的業務伺服器,在網際網路興起之前,由于伺服器的通路量和并發量并沒有那麼大,這種模式其實運作得也挺好,世界上第一個 web 伺服器 CERN httpd 就采用了這種模式(具體你可以參考)。網際網路興起後,伺服器的并發和通路量從幾十劇增到成千上萬,這種模式的弊端就凸顯出來了,主要展現在這幾個方面:
- fork 代價高
- 父子程序通信複雜
- 支援的并發連接配接數量有限
prefork
PPC 模式中,當連接配接進來時才 fork 新程序來處理連接配接請求,由于 fork 程序代價高,使用者通路時可能感覺比較慢,prefork 模式的出現就是為了解決這個問題。
顧名思義,prefork 就是提前建立程序(pre-fork)。系統在啟動的時候就預先建立好程序,然後才開始接受使用者的請求,當有新的連接配接進來的時候,就可以省去 fork 程序的操作,讓使用者通路更快、體驗更好。prefork 的基本示意圖是:
TPC
TPC 是 Thread Per Connection 的縮寫,其含義是指每次有新的連接配接就建立一個線程去專門處理這個連接配接的請求。與程序相比,線程更輕量級,建立線程的消耗比程序要少得多;同時多線程是共享程序記憶體空間的,線程通信相比程序通信更簡單。是以,TPC 實際上是解決或者弱化了 PPC fork 代價高的問題和父子程序通信複雜的問題。
TPC 的基本流程是:
TPC 雖然解決了 fork 代價高和程序通信複雜的問題,但是也引入了新的問題,具體表現在:
- 建立線程雖然比建立程序代價低,但并不是沒有代價,高并發時(例如每秒上萬連接配接)還是有性能問題。
- 無須程序間通信,但是線程間的互斥和共享又引入了複雜度,可能一不小心就導緻了死鎖問題。
- 多線程會出現互相影響的情況,某個線程出現異常時,可能導緻整個程序退出(例如記憶體越界)。
除了引入了新的問題,TPC 還是存在 CPU 線程排程和切換代價的問題。是以,TPC 方案本質上和 PPC 方案基本類似,在并發幾百連接配接的場景下,反而更多地是采用 PPC 的方案,因為 PPC 方案不會有死鎖的風險,也不會多程序互相影響,穩定性更高。
prethread
TPC 模式中,當連接配接進來時才建立新的線程來處理連接配接請求,雖然建立線程比建立程序要更加輕量級,但還是有一定的代價,而 prethread 模式就是為了解決這個問題。
prethread 理論上可以比 prefork 支援更多的并發連接配接,Apache 伺服器 MPM worker 模式預設支援 16 × 25 = 400 個并發處理線程。
19 | 單伺服器高性能模式:Reactor與Proactor
我介紹了單伺服器高性能的 PPC 和 TPC 模式,它們的優點是實作簡單,缺點是都無法支撐高并發的場景,尤其是網際網路發展到現在,各種海量使用者業務的出現,PPC 和 TPC 完全無能為力。今天我将介紹可以應對高并發場景的單伺服器高性能架構模式:Reactor 和 Proactor。
Reactor
PPC 模式最主要的問題就是每個連接配接都要建立程序(為了描述簡潔,這裡隻以 PPC 和程序為例,實際上換成 TPC 和線程,原理是一樣的),連接配接結束後程序就銷毀了,這樣做其實是很大的浪費。為了解決這個問題,一個自然而然的想法就是資源複用,即不再單獨為每個連接配接建立程序,而是建立一個程序池,将連接配接配置設定給程序,一個程序可以處理多個連接配接的業務。
引入資源池的處理方式後,會引出一個新的問題:程序如何才能高效地處理多個連接配接的業務?當一個連接配接一個程序時,程序可以采用“read -> 業務處理 -> write”的處理流程,如果目前連接配接沒有資料可以讀,則程序就阻塞在 read 操作上。這種阻塞的方式在一個連接配接一個程序的場景下沒有問題,但如果一個程序處理多個連接配接,程序阻塞在某個連接配接的 read 操作上,此時即使其他連接配接有資料可讀,程序也無法去處理,很顯然這樣是無法做到高性能的。
解決這個問題的最簡單的方式是将 read 操作改為非阻塞,然後程序不斷地輪詢多個連接配接。這種方式能夠解決阻塞的問題,但解決的方式并不優雅。首先,輪詢是要消耗 CPU 的;其次,如果一個程序處理幾千上萬的連接配接,則輪詢的效率是很低的。
為了能夠更好地解決上述問題,很容易可以想到,隻有當連接配接上有資料的時候程序才去處理,這就是 I/O 多路複用技術的來源。
I/O 多路複用技術歸納起來有兩個關鍵實作點:
- 當多條連接配接共用一個阻塞對象後,程序隻需要在一個阻塞對象上等待,而無須再輪詢所有連接配接,常見的實作方式有 select、epoll、kqueue 等。
- 當某條連接配接有新的資料可以處理時,作業系統會通知程序,程序從阻塞狀态傳回,開始進行業務處理。
I/O 多路複用結合線程池,完美地解決了 PPC 和 TPC 的問題,而且“大神們”給它取了一個很牛的名字:Reactor,中文是“反應堆”。聯想到“核反應堆”,聽起來就很吓人,實際上這裡的“反應”不是聚變、裂變反應的意思,而是“事件反應”的意思,可以通俗地了解為“來了一個事件我就有相應的反應”,這裡的“我”就是 Reactor,具體的反應就是我們寫的代碼,Reactor 會根據事件類型來調用相應的代碼進行處理。Reactor 模式也叫 Dispatcher 模式(在很多開源的系統裡面會看到這個名稱的類,其實就是實作 Reactor 模式的),更加貼近模式本身的含義,即** I/O 多路複用統一監聽事件,收到事件後配置設定(Dispatch)給某個程序**。
大白話:***“來了一個事件我就有相應的反應”***
Reactor 模式的核心組成部分包括 Reactor 和處理資源池(程序池或線程池),其中 Reactor 負責監聽和配置設定事件,處理資源池負責處理事件。初看 Reactor 的實作是比較簡單的,但實際上結合不同的業務場景,Reactor 模式的具體實作方案靈活多變,主要展現在:
- Reactor 的數量可以變化:可以是一個 Reactor,也可以是多個 Reactor。
- 資源池的數量可以變化:以程序為例,可以是單個程序,也可以是多個程序(線程類似)。
最終 Reactor 模式有這三種典型的實作方案:
- 單 Reactor 單程序 / 線程
- 單 Reactor 多線程
- 多 Reactor 多程序 / 線程
以上方案具體選擇程序還是線程,更多地是和程式設計語言及平台相關。例如,Java 語言一般使用線程(例如,Netty),C 語言使用程序和線程都可以。例如,Nginx 使用程序,Memcache 使用線程。
單 Reactor 單程序 / 線程
是以,單 Reactor 單程序的方案在實踐中應用場景不多,隻适用于業務處理非常快速的場景,目前比較著名的開源軟體中使用單 Reactor 單程序的是 Redis。
需要注意的是,C 語言編寫系統的一般使用單 Reactor 單程序,因為沒有必要在程序中再建立線程;而 Java 語言編寫的一般使用單 Reactor 單線程,因為 Java 虛拟機是一個程序,虛拟機中有很多線程,業務線程隻是其中的一個線程而已。
單 Reactor 單程序的模式優點就是很簡單,沒有程序間通信,沒有程序競争,全部都在同一個程序内完成。但其缺點也是非常明顯,具體表現有:
- 隻有一個程序,無法發揮多核 CPU 的性能;隻能采取部署多個系統來利用多核 CPU,但這樣會帶來運維複雜度,本來隻要維護一個系統,用這種方式需要在一台機器上維護多套系統。
- Handler 在處理某個連接配接上的業務時,整個程序無法處理其他連接配接的事件,很容易導緻性能瓶頸。
單 Reactor 多線程
單 Reator 多線程方案能夠充分利用多核多 CPU 的處理能力,但同時也存在下面的問題:
- 多線程資料共享和通路比較複雜。例如,子線程完成業務處理後,要把結果傳遞給主線程的 Reactor 進行發送,這裡涉及共享資料的互斥和保護機制。以 Java 的 NIO 為例,Selector 是線程安全的,但是通過 Selector.selectKeys() 傳回的鍵的集合是非線程安全的,對 selected keys 的處理必須單線程處理或者采取同步措施進行保護。
- Reactor 承擔所有事件的監聽和響應,隻在主線程中運作,瞬間高并發時會成為性能瓶頸。
你可能會發現,我隻列出了“單 Reactor 多線程”方案,沒有列出“單 Reactor 多程序”方案,這是什麼原因呢?主要原因在于如果采用多程序,子程序完成業務處理後,将結果傳回給父程序,并通知父程序發送給哪個 client,這是很麻煩的事情。因為父程序隻是通過 Reactor 監聽各個連接配接上的事件然後進行配置設定,子程序與父程序通信時并不是一個連接配接。如果要将父程序和子程序之間的通信模拟為一個連接配接,并加入 Reactor 進行監聽,則是比較複雜的。而采用多線程時,因為多線程是共享資料的,是以線程間通信是非常友善的。雖然要額外考慮線程間共享資料時的同步問題,但這個複雜度比程序間通信的複雜度要低很多。
多 Reactor 多程序 / 線程
多 Reactor 多程序 / 線程的方案看起來比單 Reactor 多線程要複雜,但實際實作時反而更加簡單,主要原因是:
- 父程序和子程序的職責非常明确,父程序隻負責接收新連接配接,子程序負責完成後續的業務處理。
- 父程序和子程序的互動很簡單,父程序隻需要把新連接配接傳給子程序,子程序無須傳回資料。
- 子程序之間是互相獨立的,無須同步共享之類的處理(這裡僅限于網絡模型相關的 select、read、send 等無須同步共享,“業務處理”還是有可能需要同步共享的)。
目前著名的開源系統 Nginx 采用的是多 Reactor 多程序,采用多 Reactor 多線程的實作有 Memcache 和 Netty。
我多說一句,Nginx 采用的是多 Reactor 多程序的模式,但方案與标準的多 Reactor 多程序有差異。具體差異表現為主程序中僅僅建立了監聽端口,并沒有建立 mainReactor 來“accept”連接配接,而是由子程序的 Reactor 來“accept”連接配接,通過鎖來控制一次隻有一個子程序進行“accept”,子程序“accept”新連接配接後就放到自己的 Reactor 進行處理,不會再配置設定給其他子程序,更多細節請查閱相關資料或閱讀 Nginx 源碼。
Proactor
Reactor 是非阻塞同步網絡模型,因為真正的 read 和 send 操作都需要使用者程序同步操作。這裡的“同步”指使用者程序在執行 read 和 send 這類 I/O 操作的時候是同步的,如果把 I/O 操作改為異步就能夠進一步提升性能,這就是異步網絡模型 Proactor。
Proactor 中文翻譯為“前攝器”比較難了解,與其類似的單詞是 proactive,含義為“主動的”,是以我們照貓畫虎翻譯為“主動器”反而更好了解。Reactor 可以了解為“來了事件我通知你,你來處理”,而 Proactor 可以了解為“來了事件我來處理,處理完了我通知你”。這裡的“我”就是作業系統核心,“事件”就是有新連接配接、有資料可讀、有資料可寫的這些 I/O 事件,“你”就是我們的程式代碼。
大白話:***“來了事件我來處理,處理完了我通知你”***
Proactor 模型示意圖是:
Reactor & Proactor
理論上 Proactor 比 Reactor 效率要高一些,異步 I/O 能夠充分利用 DMA 特性,讓 I/O 操作與計算重疊,但要實作真正的異步 I/O,作業系統需要做大量的工作。目前 Windows 下通過 IOCP 實作了真正的異步 I/O,而在 Linux 系統下的 AIO 并不完善,是以在 Linux 下實作高并發網絡程式設計時都是以 Reactor 模式為主。是以即使 Boost.Asio 号稱實作了 Proactor 模型,其實它在 Windows 下采用 IOCP,而在 Linux 下是用 Reactor 模式(采用 epoll)模拟出來的異步模型。
20 | 高性能負載均衡:分類及架構
單伺服器無論如何優化,無論采用多好的硬體,總會有一個性能天花闆,當單伺服器的性能無法滿足業務需求時,就需要設計高性能叢集來提升系統整體的處理性能。
高性能叢集的複雜性主要展現在需要增加一個任務配置設定器,以及為任務選擇一個合适的任務配置設定算法。對于任務配置設定器,現在更流行的通用叫法是“負載均衡器”。
負載均衡分類
- DNS 負載均衡
- 硬體負載均衡
- 軟體負載均衡
DNS 負載均衡
DNS 是最簡單也是最常見的負載均衡方式,一般用來實作地理級别的均衡。
硬體負載均衡
硬體負載均衡是通過單獨的硬體裝置來實作負載均衡功能,這類裝置和路由器、交換機類似,可以了解為一個用于負載均衡的基礎網絡裝置。目前業界典型的硬體負載均衡裝置有兩款:F5 和 A10。這類裝置性能強勁、功能強大,但價格都不便宜,一般隻有“土豪”公司才會考慮使用此類裝置。普通業務量級的公司一是負擔不起,二是業務量沒那麼大,用這些裝置也是浪費。
軟體負載均衡
軟體負載均衡通過負載均衡軟體來實作負載均衡功能,常見的有 Nginx 和 LVS,其中 Nginx 是軟體的 7 層負載均衡,LVS 是 Linux 核心的 4 層負載均衡。4 層和 7 層的差別就在于協定和靈活性,Nginx 支援 HTTP、E-mail 協定;而 LVS 是 4 層負載均衡,和協定無關,幾乎所有應用都可以做,例如,聊天、資料庫等。
軟體和硬體的最主要差別就在于性能,硬體負載均衡性能遠遠高于軟體負載均衡性能。Ngxin 的性能是萬級,一般的 Linux 伺服器上裝一個 Nginx 大概能到 5 萬 / 秒;LVS 的性能是十萬級,據說可達到 80 萬 / 秒;而 F5 性能是百萬級,從 200 萬 / 秒到 800 萬 / 秒都有(資料來源網絡,僅供參考,如需采用請根據實際業務場景進行性能測試)。當然,軟體負載均衡的最大優勢是便宜,一台普通的 Linux 伺服器批發價大概就是 1 萬元左右,相比 F5 的價格,那就是自行車和寶馬的差別了。
下面是 Nginx 的負載均衡架構示意圖:
負載均衡典型架構
前面我們介紹了 3 種常見的負載均衡機制:DNS 負載均衡、硬體負載均衡、軟體負載均衡,每種方式都有一些優缺點,但并不意味着在實際應用中隻能基于它們的優缺點進行非此即彼的選擇,反而是基于它們的優缺點進行組合使用。具體來說,組合的基本原則為:DNS 負載均衡用于實作地理級别的負載均衡;硬體負載均衡用于實作叢集級别的負載均衡;軟體負載均衡用于實作機器級别的負載均衡。
示例:
21 | 高性能負載均衡:算法
負載均衡算法數量較多,而且可以根據一些業務特性進行定制開發,抛開細節上的差異,根據算法期望達到的目的,大體上可以分為下面幾類。
- 任務平分類:負載均衡系統将收到的任務平均配置設定給伺服器進行處理,這裡的“平均”可以是絕對數量的平均,也可以是比例或者權重上的平均。
- 負載均衡類:負載均衡系統根據伺服器的負載來進行配置設定,這裡的負載并不一定是通常意義上我們說的“CPU 負載”,而是系統目前的壓力,可以用 CPU 負載來衡量,也可以用連接配接數、I/O 使用率、網卡吞吐量等來衡量系統的壓力。
- 性能最優類:負載均衡系統根據伺服器的響應時間來進行任務配置設定,優先将新任務配置設定給響應最快的伺服器。
- Hash 類:負載均衡系統根據任務中的某些關鍵資訊進行 Hash 運算,将相同 Hash 值的請求配置設定到同一台伺服器上。常見的有源位址 Hash、目标位址 Hash、session id hash、使用者 ID Hash 等。
輪詢
負載均衡系統收到請求後,按照順序輪流配置設定到伺服器上。
權重輪詢
負載均衡系統根據伺服器權重進行任務配置設定,這裡的權重一般是根據硬體配置進行靜态配置的,采用動态的方式計算會更加契合業務,但複雜度也會更高。
解決不同伺服器處理能力有差異的問題。
例如,叢集中有新的機器是 32 核的,老的機器是 16 核的,那麼理論上我們可以假設新機器的處理能力是老機器的 2 倍,負載均衡系統就可以按照 2:1 的比例配置設定更多的任務給新機器,進而充分利用新機器的性能。
負載最低優先
負載均衡系統将任務配置設定給目前負載最低的伺服器,這裡的負載根據不同的任務類型和業務場景,可以用不同的名額來衡量。例如:
- LVS 這種 4 層網絡負載均衡裝置,可以以“連接配接數”來判斷伺服器的狀态,伺服器連接配接數越大,表明伺服器壓力越大。
- Nginx 這種 7 層網絡負載系統,可以以“HTTP 請求數”來判斷伺服器狀态(Nginx 内置的負載均衡算法不支援這種方式,需要進行擴充)。
- 如果我們自己開發負載均衡系統,可以根據業務特點來選擇名額衡量系統壓力。如果是 CPU 密集型,可以以“CPU 負載”來衡量系統壓力;如果是 I/O 密集型,可以以“I/O 負載”來衡量系統壓力。
負載最低優先的算法解決了輪詢算法中無法感覺伺服器狀态的問題,由此帶來的代價是複雜度要增加很多。
性能最優類
負載最低優先類算法是站在伺服器的角度來進行配置設定的,而性能最優優先類算法則是站在用戶端的角度來進行配置設定的,優先将任務配置設定給處理速度最快的伺服器,通過這種方式達到最快響應用戶端的目的。
和負載最低優先類算法類似,性能最優優先類算法本質上也是感覺了伺服器的狀态,隻是通過響應時間這個外部标準來衡量伺服器狀态而已。是以性能最優優先類算法存在的問題和負載最低優先類算法類似,複雜度都很高,主要展現在:
- 負載均衡系統需要收集和分析每個伺服器每個任務的響應時間,在大量任務處理的場景下,這種收集和統計本身也會消耗較多的性能。
- 為了減少這種統計上的消耗,可以采取采樣的方式來統計,即不統計所有任務的響應時間,而是抽樣統計部分任務的響應時間來估算整體任務的響應時間。采樣統計雖然能夠減少性能消耗,但使得複雜度進一步上升,因為要确定合适的采樣率,采樣率太低會導緻結果不準确,采樣率太高會導緻性能消耗較大,找到合适的采樣率也是一件複雜的事情。
- 無論是全部統計還是采樣統計,都需要選擇合适的周期:是 10 秒内性能最優,還是 1 分鐘内性能最優,還是 5 分鐘内性能最優……沒有放之四海而皆準的周期,需要根據實際業務進行判斷和選擇,這也是一件比較複雜的事情,甚至出現系統上線後需要不斷地調優才能達到最優設計。
Hash 類
負載均衡系統根據任務中的某些關鍵資訊進行 Hash 運算,将相同 Hash 值的請求配置設定到同一台伺服器上,這樣做的目的主要是為了滿足特定的業務需求。例如:
- 源位址 Hash。将來源于同一個源 IP 位址的任務配置設定給同一個伺服器進行處理,适合于存在事務、會話的業務。例如,當我們通過浏覽器登入網上銀行時,會生成一個會話資訊,這個會話是臨時的,關閉浏覽器後就失效。網上銀行背景無須持久化會話資訊,隻需要在某台伺服器上臨時儲存這個會話就可以了,但需要保證使用者在會話存在期間,每次都能通路到同一個伺服器,這種業務場景就可以用源位址 Hash 來實作。
- ID Hash。将某個 ID 辨別的業務配置設定到同一個伺服器中進行處理,這裡的 ID 一般是臨時性資料的 ID(如 session id)。例如,上述的網上銀行登入的例子,用 session id hash 同樣可以實作同一個會話期間,使用者每次都是通路到同一台伺服器的目的。
微信搶紅包的高并發架構,應該采取什麼樣的負載均衡算法?談談你的分析和了解。
微信搶紅包架構應該至少包含兩個負載均衡,一個是應用伺服器的負載均衡,用于将任務請求分發到不同應用伺服器,這裡可以采用輪詢或加速輪詢的算法,因為這種速度快,适合搶紅包的業務場景。
另一起負載均衡是資料伺服器的負載均衡,這裡更适合根據紅包ID進行hash負載均衡,将所有資料請求在同一台伺服器上進行,防止多台伺服器間的不同步問題。