來源: 《從0開始學架構(gòu)》(極客時間) ---李運華
讀寫分離
讀寫分離的基本原理是將數(shù)據(jù)庫讀寫操作分散到不同的節(jié)點上
讀寫分離的基本實現(xiàn)是:
- 數(shù)據(jù)庫服務(wù)器搭建主從集群,一主一從、一主多從都可以。
- 數(shù)據(jù)庫主機負責讀寫操作,從機只負責讀操作。
- 數(shù)據(jù)庫主機通過復(fù)制將數(shù)據(jù)同步到從機,每臺數(shù)據(jù)庫服務(wù)器都存儲了所有的業(yè)務(wù)數(shù)據(jù)。
- 業(yè)務(wù)服務(wù)器將寫操作發(fā)給數(shù)據(jù)庫主機,將讀操作發(fā)給數(shù)據(jù)庫從機。
讀寫分離的實現(xiàn)邏輯并不復(fù)雜,但有兩個細節(jié)點將引入設(shè)計復(fù)雜度:主從復(fù)制延遲和分配機制。
復(fù)制延遲
主從復(fù)制延遲會帶來一個問題:如果業(yè)務(wù)服務(wù)器將數(shù)據(jù)寫入到數(shù)據(jù)庫主服務(wù)器后立刻(1 秒內(nèi))進行讀取,此時讀操作訪問的是從機,主機還沒有將數(shù)據(jù)復(fù)制過來,到從機讀取數(shù)據(jù)是讀不到最新數(shù)據(jù)的,業(yè)務(wù)上就可能出現(xiàn)問題。
解決主從復(fù)制延遲有幾種常見的方法:
- 寫操作后的讀操作指定發(fā)給數(shù)據(jù)庫主服務(wù)器
- 讀從機失敗后再讀一次主機
- 關(guān)鍵業(yè)務(wù)讀寫操作全部指向主機,非關(guān)鍵業(yè)務(wù)采用讀寫分離
分配機制
將讀寫操作區(qū)分開來,然后訪問不同的數(shù)據(jù)庫服務(wù)器,一般有兩種方式:程序代碼封裝和中間件封裝。
- 程序代碼封裝
程序代碼封裝指在代碼中抽象一個數(shù)據(jù)訪問層(所以有的文章也稱這種方式為“中間層封裝”),實現(xiàn)讀寫操作分離和數(shù)據(jù)庫服務(wù)器連接的管理。具有以下特點:
- 實現(xiàn)簡單,而且可以根據(jù)業(yè)務(wù)做較多定制化的功能。
- 每個編程語言都需要自己實現(xiàn)一次,無法通用,如果一個業(yè)務(wù)包含多個編程語言寫的多個子系統(tǒng),則重復(fù)開發(fā)的工作量比較大。
- 故障情況下,如果主從發(fā)生切換,則可能需要所有系統(tǒng)都修改配置并重啟。
- 中間件封裝
中間件封裝指的是獨立一套系統(tǒng)出來,實現(xiàn)讀寫操作分離和數(shù)據(jù)庫服務(wù)器連接的管理。中間件對業(yè)務(wù)服務(wù)器提供 SQL 兼容的協(xié)議,業(yè)務(wù)服務(wù)器無須自己進行讀寫分離。對于業(yè)務(wù)服務(wù)器來說,訪問中間件和訪問數(shù)據(jù)庫沒有區(qū)別,事實上在業(yè)務(wù)服務(wù)器看來,中間件就是一個數(shù)據(jù)庫服務(wù)器。具有以下特點:
- 能夠支持多種編程語言,因為數(shù)據(jù)庫中間件對業(yè)務(wù)服務(wù)器提供的是標準 SQL 接口。
- 數(shù)據(jù)庫中間件要支持完整的 SQL 語法和數(shù)據(jù)庫服務(wù)器的協(xié)議,實現(xiàn)比較復(fù)雜,細節(jié)特別多,很容易出現(xiàn) bug,需要較長的時間才能穩(wěn)定。
- 數(shù)據(jù)庫中間件自己不執(zhí)行真正的讀寫操作,但所有的數(shù)據(jù)庫操作請求都要經(jīng)過中間件,中間件的性能要求也很高。
- 數(shù)據(jù)庫主從切換對業(yè)務(wù)服務(wù)器無感知,數(shù)據(jù)庫中間件可以探測數(shù)據(jù)庫服務(wù)器的主從狀態(tài)。
分庫分表
讀寫分離分散了數(shù)據(jù)庫讀寫操作的壓力,但沒有分散存儲壓力,當數(shù)據(jù)量達到千萬甚至上億條的時候,單臺數(shù)據(jù)庫服務(wù)器的存儲能力會成為系統(tǒng)的瓶頸,主要體現(xiàn)在這幾個方面:
- 數(shù)據(jù)量太大,讀寫的性能會下降,即使有索引,索引也會變得很大,性能同樣會下降。
- 數(shù)據(jù)文件會變得很大,數(shù)據(jù)庫備份和恢復(fù)需要耗費很長時間。
- 數(shù)據(jù)文件越大,極端情況下丟失數(shù)據(jù)的風險越高。
業(yè)務(wù)分庫
業(yè)務(wù)分庫指的是按照業(yè)務(wù)模塊將數(shù)據(jù)分散到不同的數(shù)據(jù)庫服務(wù)器。
雖然業(yè)務(wù)分庫能夠分散存儲和訪問壓力,但同時也帶來了新的問題:
- join操作問題
- 事務(wù)問題
- 成本問題
對于小公司初創(chuàng)業(yè)務(wù),并不建議一開始就這樣拆分。
分表
將不同業(yè)務(wù)數(shù)據(jù)分散存儲到不同的數(shù)據(jù)庫服務(wù)器,能夠支撐百萬甚至千萬用戶規(guī)模的業(yè)務(wù),但如果業(yè)務(wù)繼續(xù)發(fā)展,同一業(yè)務(wù)的單表數(shù)據(jù)也會達到單臺數(shù)據(jù)庫服務(wù)器的處理瓶頸。
單表數(shù)據(jù)拆分有兩種方式:垂直分表和水平分表。
垂直切分
表的記錄數(shù)相同但包含不同的列。
垂直分表適合將表中某些不常用且占了大量空間的列拆分出去。
水平切分
表的列相同但包含不同的行數(shù)據(jù)。
水平分表相比垂直分表,會引入更多的復(fù)雜性,主要表現(xiàn)在下面幾個方面:
1. 路由
水平分表后,某條數(shù)據(jù)具體屬于哪個切分后的子表,需要增加路由算法進行計算,這個算法會引入一定的復(fù)雜性。常見的路由算法有:
- 范圍路由
范圍路由設(shè)計的復(fù)雜點主要體現(xiàn)在分段大小的選取上,分段太小會導致切分后子表數(shù)量過多,增加維護復(fù)雜度;分段太大可能會導致單表依然存在性能問題。范圍路由的一個比較隱含的缺點是分布不均勻,這個需要根據(jù)業(yè)務(wù)來實際判斷。
- Hash 路由
選取某個列(或者某幾個列組合也可以)的值進行 Hash 運算,然后根據(jù) Hash 結(jié)果分散到不同的數(shù)據(jù)庫表中。
優(yōu)點是分布比較均勻,缺點是擴充新的表很麻煩。
- 配置路由
配置路由就是路由表,用一張獨立的表來記錄路由信息。
配置路由的缺點就是必須多查詢一次,會影響整體性能;而且路由表本身如果太大,性能同樣可能成為瓶頸,如果我們再次將路由表分庫分表,則又面臨一個死循環(huán)式的路由算法選擇問題。
2. join操作
水平分表后,數(shù)據(jù)分散在多個表中,如果需要與其他表進行 join 查詢,需要在業(yè)務(wù)代碼或者數(shù)據(jù)庫中間件中進行多次 join 查詢,然后將結(jié)果合并。
3. count操作
常用的方法有:
- count()相加,缺點是性能比較低
- 記錄數(shù)表,具體做法是新建一張表,每次插入或者刪除子表數(shù)據(jù)成功后,都更新“記錄數(shù)表”。缺點是增加寫操作的復(fù)雜度,同時異常情況下容易導致數(shù)據(jù)不一致。
4. order by操作
水平分表后,數(shù)據(jù)分散到多個子表中,排序操作無法在數(shù)據(jù)庫中完成,只能由業(yè)務(wù)代碼或者數(shù)據(jù)庫中間件分別查詢每個子表中的數(shù)據(jù),然后匯總進行排序。
NoSQL
關(guān)系數(shù)據(jù)庫存在如下缺點:
- 關(guān)系數(shù)據(jù)庫存儲的是行記錄,無法存儲數(shù)據(jù)結(jié)構(gòu)
- 關(guān)系數(shù)據(jù)庫的 schema 擴展很不方便
- 關(guān)系數(shù)據(jù)庫在大數(shù)據(jù)場景下 I/O 較高
如果對一些大量數(shù)據(jù)的表進行統(tǒng)計之類的運算,關(guān)系數(shù)據(jù)庫的 I/O 會很高,因為即使只針對其中某一列進行運算,關(guān)系數(shù)據(jù)庫也會將整行數(shù)據(jù)從存儲設(shè)備讀入內(nèi)存。
- 關(guān)系數(shù)據(jù)庫的全文搜索功能比較弱
針對上述問題,分別誕生了不同的 NoSQL 解決方案,這些方案與關(guān)系數(shù)據(jù)庫相比,在某些應(yīng)用場景下表現(xiàn)更好。但世上沒有免費的午餐,NoSQL 方案帶來的優(yōu)勢,本質(zhì)上是犧牲 ACID 中的某個或者某幾個特性,因此我們不能盲目地迷信 NoSQL 是銀彈,而應(yīng)該將 NoSQL 作為 SQL 的一個有力補充,NoSQL != No SQL,而是 NoSQL = Not Only SQL。
常見的NoSQL方案分為4類:
- K-V 存儲:解決關(guān)系數(shù)據(jù)庫無法存儲數(shù)據(jù)結(jié)構(gòu)的問題,以 Redis 為代表。
- 文檔數(shù)據(jù)庫:解決關(guān)系數(shù)據(jù)庫強 schema 約束的問題,以 MongoDB 為代表。
- 列式數(shù)據(jù)庫:解決關(guān)系數(shù)據(jù)庫大數(shù)據(jù)場景下的 I/O 問題,以 HBase 為代表。
- 全文搜索引擎:解決關(guān)系數(shù)據(jù)庫的全文搜索性能問題,以 Elasticsearch 為代表。
K-V存儲
K-V 存儲的全稱是 Key-Value 存儲,其中 Key 是數(shù)據(jù)的標識,和關(guān)系數(shù)據(jù)庫中的主鍵含義一樣,Value 就是具體的數(shù)據(jù)。
Redis的缺點是不支持完整的ACID事務(wù),只支持ACI,不完全支持D(由持久化模式?jīng)Q定,要支持的話代價比較大)
文檔數(shù)據(jù)庫
為了解決關(guān)系數(shù)據(jù)庫 schema 帶來的問題,文檔數(shù)據(jù)庫應(yīng)運而生。文檔數(shù)據(jù)庫最大的特點就是 no-schema,可以存儲和讀取任意的數(shù)據(jù)。目前絕大部分文檔數(shù)據(jù)庫存儲的數(shù)據(jù)格式是 JSON(或者 BSON),因為 JSON 數(shù)據(jù)是自描述的,無須在使用前定義字段,讀取一個 JSON 中不存在的字段也不會導致 SQL 那樣的語法錯誤。
文檔數(shù)據(jù)庫 no-schema 的特性帶來的這些優(yōu)勢也是有代價的,最主要的代價就是不支持事務(wù)。其次是不支持join操作。
列式數(shù)據(jù)庫
列式數(shù)據(jù)庫就是按照列來存儲數(shù)據(jù)的數(shù)據(jù)庫,與之對應(yīng)的傳統(tǒng)關(guān)系數(shù)據(jù)庫被稱為“行式數(shù)據(jù)庫”,因為關(guān)系數(shù)據(jù)庫是按照行來存儲數(shù)據(jù)的。
列式數(shù)據(jù)庫的優(yōu)勢是有更高的存儲壓縮比,因為單個列的數(shù)據(jù)相似度相比行來說更高,能夠達到更高的壓縮率。
劣勢是列式存儲將不同列存儲在磁盤上不連續(xù)的空間,導致更新多個列時磁盤是隨機寫操作。
基于上述列式存儲的優(yōu)缺點,一般將列式存儲應(yīng)用在離線的大數(shù)據(jù)分析和統(tǒng)計場景中,因為這種場景主要是針對部分列單列進行操作,且數(shù)據(jù)寫入后就無須再更新刪除。
全文搜索引擎
傳統(tǒng)的關(guān)系型數(shù)據(jù)庫通過索引來達到快速查詢的目的,但是在全文搜索的業(yè)務(wù)場景下,索引也無能為力。
全文搜索基本原理
全文搜索引擎的技術(shù)原理被稱為“倒排索引”(Inverted index),也常被稱為反向索引、置入檔案或反向檔案,是一種索引方法,其基本原理是建立單詞到文檔的索引。之所以被稱為“倒排”索引,是和“正排“索引相對的,“正排索引”的基本原理是建立文檔到單詞的索引。
全文搜索的使用方式
全文搜索引擎的索引對象是單詞和文檔,而關(guān)系數(shù)據(jù)庫的索引對象是鍵和行,兩者的術(shù)語差異很大,不能簡單地等同起來。因此,為了讓全文搜索引擎支持關(guān)系型數(shù)據(jù)的全文搜索,需要做一些轉(zhuǎn)換操作,即將關(guān)系型數(shù)據(jù)轉(zhuǎn)換為文檔數(shù)據(jù)。
目前常用的轉(zhuǎn)換方式是將關(guān)系型數(shù)據(jù)按照對象的形式轉(zhuǎn)換為 JSON 文檔,然后將 JSON 文檔輸入全文搜索引擎進行索引。
緩存架構(gòu)
緩存雖然能夠大大減輕存儲系統(tǒng)的壓力,但同時也給架構(gòu)引入了更多復(fù)雜性。架構(gòu)設(shè)計時如果沒有針對緩存的復(fù)雜性進行處理,某些場景下甚至會導致整個系統(tǒng)崩潰。
緩存穿透
緩存穿透是指緩存沒有發(fā)揮作用,業(yè)務(wù)系統(tǒng)雖然去緩存查詢數(shù)據(jù),但緩存中沒有數(shù)據(jù),業(yè)務(wù)系統(tǒng)需要再次去存儲系統(tǒng)查詢數(shù)據(jù)。
- 存儲數(shù)據(jù)不存在
這種情況的解決辦法比較簡單,如果查詢存儲系統(tǒng)的數(shù)據(jù)沒有找到,則直接設(shè)置一個默認值(可以是空值,也可以是具體的值)存到緩存中,這樣第二次讀取緩存時就會獲取到默認值,而不會繼續(xù)訪問存儲系統(tǒng)。
- 緩存數(shù)據(jù)生成耗費大量時間或者資源
存儲系統(tǒng)中存在數(shù)據(jù),但生成緩存數(shù)據(jù)需要耗費較長時間或者耗費大量資源。如果剛好在業(yè)務(wù)訪問的時候緩存失效了,那么也會出現(xiàn)緩存沒有發(fā)揮作用,訪問壓力全部集中在存儲系統(tǒng)上的情況。這種情況并沒有太好的解決方案,一般是發(fā)現(xiàn)問題及時處理。
緩存雪崩
緩存雪崩是指當緩存失效(過期)后引起系統(tǒng)性能急劇下降的情況。當緩存過期被清除后,業(yè)務(wù)系統(tǒng)需要重新生成緩存,因此需要再次訪問存儲系統(tǒng),再次進行運算,這個處理步驟耗時幾十毫秒甚至上百毫秒。而對于一個高并發(fā)的業(yè)務(wù)系統(tǒng)來說,幾百毫秒內(nèi)可能會接到幾百上千個請求。由于舊的緩存已經(jīng)被清除,新的緩存還未生成,并且處理這些請求的線程都不知道另外有一個線程正在生成緩存,因此所有的請求都會去重新生成緩存,都會去訪問存儲系統(tǒng),從而對存儲系統(tǒng)造成巨大的性能壓力。這些壓力又會拖慢整個系統(tǒng),嚴重的會造成數(shù)據(jù)庫宕機,從而形成一系列連鎖反應(yīng),造成整個系統(tǒng)崩潰。
緩存雪崩的常見解決方法有兩種:更新鎖機制和后臺更新機制。
-
更新鎖
對緩存更新操作進行加鎖保護,保證只有一個線程能夠進行緩存更新,未能獲取更新鎖的線程要么等待鎖釋放后重新讀取緩存,要么就返回空值或者默認值。 -
后臺更新
由后臺線程來更新緩存,而不是由業(yè)務(wù)線程來更新緩存,緩存本身的有效期設(shè)置為永久,后臺線程定時更新緩存。
后臺更新既適應(yīng)單機多線程的場景,也適合分布式集群的場景,相比更新鎖機制要簡單一些。
后臺更新機制還適合業(yè)務(wù)剛上線的時候進行緩存預(yù)熱。緩存預(yù)熱指系統(tǒng)上線后,將相關(guān)的緩存數(shù)據(jù)直接加載到緩存系統(tǒng),而不是等待用戶訪問才來觸發(fā)緩存加載。
緩存熱點
雖然緩存系統(tǒng)本身的性能比較高,但對于一些特別熱點的數(shù)據(jù),如果大部分甚至所有的業(yè)務(wù)請求都命中同一份緩存數(shù)據(jù),則這份數(shù)據(jù)所在的緩存服務(wù)器的壓力也很大。
緩存熱點的解決方案就是復(fù)制多份緩存副本,將請求分散到多個緩存服務(wù)器上,減輕緩存熱點導致的單臺緩存服務(wù)器壓力。
緩存副本設(shè)計有一個細節(jié)需要注意,就是不同的緩存副本不要設(shè)置統(tǒng)一的過期時間,否則就會出現(xiàn)所有緩存副本同時生成同時失效的情況,從而引發(fā)緩存雪崩效應(yīng)。正確的做法是設(shè)定一個過期時間范圍,不同的緩存副本的過期時間是指定范圍內(nèi)的隨機值。
單服務(wù)器高性能模式
站在架構(gòu)師的角度,當然需要特別關(guān)注高性能架構(gòu)的設(shè)計。高性能架構(gòu)設(shè)計主要集中在兩方面:
- 盡量提升單服務(wù)器的性能,將單服務(wù)器的性能發(fā)揮到極致。
- 如果單服務(wù)器無法支撐性能,設(shè)計服務(wù)器集群方案。
除了以上兩點,最終系統(tǒng)能否實現(xiàn)高性能,還和具體的實現(xiàn)及編碼相關(guān)。但架構(gòu)設(shè)計是高性能的基礎(chǔ),如果架構(gòu)設(shè)計沒有做到高性能,則后面的具體實現(xiàn)和編碼能提升的空間是有限的。形象地說,架構(gòu)設(shè)計決定了系統(tǒng)性能的上限,實現(xiàn)細節(jié)決定了系統(tǒng)性能的下限。
單服務(wù)器高性能的關(guān)鍵之一就是服務(wù)器采取的并發(fā)模型。
PPC
PPC 是 Process Per Connection 的縮寫,其含義是指每次有新的連接就新建一個進程去專門處理這個連接的請求,這是傳統(tǒng)的 UNIX 網(wǎng)絡(luò)服務(wù)器所采用的模型。
PPC 模式中,當連接進來時才 fork 新進程來處理連接請求,由于 fork 進程代價高,用戶訪問時可能感覺比較慢,prefork 模式的出現(xiàn)就是為了解決這個問題。
顧名思義,prefork 就是提前創(chuàng)建進程(pre-fork)。系統(tǒng)在啟動的時候就預(yù)先創(chuàng)建好進程,然后才開始接受用戶的請求,當有新的連接進來的時候,就可以省去 fork 進程的操作,讓用戶訪問更快、體驗更好。
TPC
TPC 是 Thread Per Connection 的縮寫,其含義是指每次有新的連接就新建一個線程去專門處理這個連接的請求。與進程相比,線程更輕量級,創(chuàng)建線程的消耗比進程要少得多;同時多線程是共享進程內(nèi)存空間的,線程通信相比進程通信更簡單。
TPC 模式中,當連接進來時才創(chuàng)建新的線程來處理連接請求,雖然創(chuàng)建線程比創(chuàng)建進程要更加輕量級,但還是有一定的代價,而 prethread 模式就是為了解決這個問題。
和 prefork 類似,prethread 模式會預(yù)先創(chuàng)建線程,然后才開始接受用戶的請求,當有新的連接進來的時候,就可以省去創(chuàng)建線程的操作,讓用戶感覺更快、體驗更好。
Reactor
為了解決PPC和TPC的問題,很自然的想到了I/O多路復(fù)用技術(shù),主要包括以下兩個關(guān)鍵實現(xiàn)點:
- 當多條連接共用一個阻塞對象后,進程只需要在一個阻塞對象上等待,而無須再輪詢所有連接,常見的實現(xiàn)方式有 select、epoll、kqueue 等。
- 當某條連接有新的數(shù)據(jù)可以處理時,操作系統(tǒng)會通知進程,進程從阻塞狀態(tài)返回,開始進行業(yè)務(wù)處理。
I/O多路復(fù)用結(jié)合線程池,人們給它啟了一個很牛的名字:Reactor,中文是“反應(yīng)堆”。聯(lián)想到“核反應(yīng)堆”,聽起來就很嚇人,實際上這里的“反應(yīng)”不是聚變、裂變反應(yīng)的意思,而是“事件反應(yīng)”的意思,可以通俗地理解為“來了一個事件我就有相應(yīng)的反應(yīng)”,這里的“我”就是 Reactor,具體的反應(yīng)就是我們寫的代碼,Reactor 會根據(jù)事件類型來調(diào)用相應(yīng)的代碼進行處理。Reactor 模式也叫 Dispatcher 模式.
Reacotr模式主要包括三個典型的實現(xiàn)方案。
單Reacotr單進程/線程
該方案的主要流程為:
- Reactor 對象通過 select 監(jiān)控連接事件,收到事件后通過 dispatch 進行分發(fā)。
- 如果是連接建立的事件,則由 Acceptor 處理,Acceptor 通過 accept 接受連接,并創(chuàng)建一個 Handler 來處理連接后續(xù)的各種事件。
- 如果不是連接建立事件,則 Reactor 會調(diào)用連接對應(yīng)的 Handler來進行響應(yīng)。
- Handler 會完成 read-> 業(yè)務(wù)處理 ->send 的完整業(yè)務(wù)流程。
單 Reactor 單進程的模式優(yōu)點就是很簡單,沒有進程間通信,沒有進程競爭,全部都在同一個進程內(nèi)完成。但其缺點也是非常明顯,具體表現(xiàn)有:
- 只有一個進程,無法發(fā)揮多核 CPU 的性能。
- Handler 在處理某個連接上的業(yè)務(wù)時,整個進程無法處理其他連接的事件,很容易導致性能瓶頸。
目前比較著名的實現(xiàn)是Redis。
單Reactor多線程
該方案的主要流程為:
- 主線程中,Reactor 對象通過 select 監(jiān)控連接事件,收到事件后通過 dispatch 進行分發(fā)。
- 如果是連接建立的事件,則由 Acceptor 處理,Acceptor 通過 accept 接受連接,并創(chuàng)建一個 Handler 來處理連接后續(xù)的各種事件。
- 如果不是連接建立事件,則 Reactor 會調(diào)用連接對應(yīng)的 Handler來進行響應(yīng)。
- Handler 只負責響應(yīng)事件,不進行業(yè)務(wù)處理;Handler 通過 read 讀取到數(shù)據(jù)后,會發(fā)給 Processor 進行業(yè)務(wù)處理。
- Processor 會在獨立的子線程中完成真正的業(yè)務(wù)處理,然后將響應(yīng)結(jié)果發(fā)給主進程的 Handler 處理;Handler 收到響應(yīng)后通過 send 將響應(yīng)結(jié)果返回給 client。
單 Reator 多線程方案能夠充分利用多核多 CPU 的處理能力,但同時也存在下面的問題:
- 多線程數(shù)據(jù)共享和訪問比較復(fù)雜。
- Reactor 承擔所有事件的監(jiān)聽和響應(yīng),只在主線程中運行,瞬間高并發(fā)時會成為性能瓶頸。
這里只說了單Reactor多線程,沒有說單Reactor/多進程,是因為如果采用多進程,進程間的通信會很麻煩。
多Reactor多進程/線程
該方案的主要流程為:
- 父進程中 mainReactor 對象通過 select 監(jiān)控連接建立事件,收到事件后通過 Acceptor 接收,將新的連接分配給某個子進程。
- 子進程的 subReactor 將 mainReactor 分配的連接加入連接隊列進行監(jiān)聽,并創(chuàng)建一個 Handler 用于處理連接的各種事件。
- 當有新的事件發(fā)生時,subReactor 會調(diào)用連接對應(yīng)的 Handler來進行響應(yīng)。
- Handler 完成 read→業(yè)務(wù)處理→send 的完整業(yè)務(wù)流程。
多 Reactor 多進程 / 線程的方案看起來比單 Reactor 多線程要復(fù)雜,但實際實現(xiàn)時反而更加簡單,主要原因是:
- 父進程和子進程的職責非常明確,父進程只負責接收新連接,子進程負責完成后續(xù)的業(yè)務(wù)處理。
- 父進程和子進程的交互很簡單,父進程只需要把新連接傳給子進程,子進程無須返回數(shù)據(jù)。
- 子進程之間是互相獨立的,無須同步共享之類的處理。
目前比較著名的實現(xiàn)為Nginx,但是實現(xiàn)有所差異,Nginx沒有創(chuàng)建mainReactor來accept連接,而是由子進程的Reactor來accept連接,通過鎖控制只有一個子進程能accept連接(解決了驚群效應(yīng))。
Proactor
Reactor 是非阻塞同步網(wǎng)絡(luò)模型,因為真正的 read 和 send 操作都需要用戶進程同步操作。這里的“同步”指用戶進程在執(zhí)行 read 和 send 這類 I/O 操作的時候是同步的,如果把 I/O 操作改為異步就能夠進一步提升性能,這就是異步網(wǎng)絡(luò)模型 Proactor。
Proactor 中文翻譯為“前攝器”比較難理解,與其類似的單詞是 proactive,含義為“主動的”,因此我們照貓畫虎翻譯為“主動器”反而更好理解。Reactor 可以理解為“來了事件我通知你,你來處理”,而 Proactor 可以理解為“來了事件我來處理,處理完了我通知你”。這里的“我”就是操作系統(tǒng)內(nèi)核,“事件”就是有新連接、有數(shù)據(jù)可讀、有數(shù)據(jù)可寫的這些 I/O 事件,“你”就是我們的程序代碼。
理論上 Proactor 比 Reactor 效率要高一些,異步 I/O 能夠充分利用 DMA 特性,讓 I/O 操作與計算重疊,但要實現(xiàn)真正的異步 I/O,操作系統(tǒng)需要做大量的工作。目前 Windows 下通過 IOCP 實現(xiàn)了真正的異步 I/O,而在 Linux 系統(tǒng)下的 AIO 并不完善,因此在 Linux 下實現(xiàn)高并發(fā)網(wǎng)絡(luò)編程時都是以 Reactor 模式為主。所以即使 Boost.Asio 號稱實現(xiàn)了 Proactor 模型,其實它在 Windows 下采用 IOCP,而在 Linux 下是用 Reactor 模式(采用 epoll)模擬出來的異步模型。
高性能集群
單服務(wù)器無論如何優(yōu)化,無論采用多好的硬件,總會有一個性能天花板,當單服務(wù)器的性能無法滿足業(yè)務(wù)需求時,就需要設(shè)計高性能集群來提升系統(tǒng)整體的處理性能。
高性能集群的本質(zhì)很簡單,通過增加更多的服務(wù)器來提升系統(tǒng)整體的計算能力。由于計算本身存在一個特點:同樣的輸入數(shù)據(jù)和邏輯,無論在哪臺服務(wù)器上執(zhí)行,都應(yīng)該得到相同的輸出。因此高性能集群設(shè)計的復(fù)雜度主要體現(xiàn)在任務(wù)分配這部分,需要設(shè)計合理的任務(wù)分配策略,將計算任務(wù)分配到多臺服務(wù)器上執(zhí)行。
高性能集群的復(fù)雜性主要體現(xiàn)在需要增加一個任務(wù)分配器,以及為任務(wù)選擇一個合適的任務(wù)分配算法。對于任務(wù)分配器,現(xiàn)在更流行的通用叫法是“負載均衡器”。但這個名稱有一定的誤導性,會讓人潛意識里認為任務(wù)分配的目的是要保持各個計算單元的負載達到均衡狀態(tài)。而實際上任務(wù)分配并不只是考慮計算單元的負載均衡,不同的任務(wù)分配算法目標是不一樣的,有的基于負載考慮,有的基于性能(吞吐量、響應(yīng)時間)考慮,有的基于業(yè)務(wù)考慮。
負載均衡分類
DNS負載均衡
DNS 負載均衡的本質(zhì)是 DNS 解析同一個域名可以返回不同的 IP 地址。
DNS 是最簡單也是最常見的負載均衡方式,一般用來實現(xiàn)地理級別的均衡。DNS 負載均衡實現(xiàn)簡單、成本低,但也存在粒度太粗、負載均衡算法少等缺點。
優(yōu)點有:
- 簡單、成本低:負載均衡工作交給 DNS 服務(wù)器處理,無須自己開發(fā)或者維護負載均衡設(shè)備。
- 就近訪問,提升訪問速度:DNS 解析時可以根據(jù)請求來源 IP,解析成距離用戶最近的服務(wù)器地址,可以加快訪問速度,改善性能。
缺點有:
- 更新不及時:DNS 緩存的時間比較長,修改 DNS 配置后,由于緩存的原因,還是有很多用戶會繼續(xù)訪問修改前的 IP,這樣的訪問會失敗,達不到負載均衡的目的,并且也影響用戶正常使用業(yè)務(wù)。
- 擴展性差:DNS 負載均衡的控制權(quán)在域名商那里,無法根據(jù)業(yè)務(wù)特點針對其做更多的定制化功能和擴展特性。
- 分配策略比較簡單:DNS 負載均衡支持的算法少;不能區(qū)分服務(wù)器的差異(不能根據(jù)系統(tǒng)與服務(wù)的狀態(tài)來判斷負載);也無法感知后端服務(wù)器的狀態(tài)。
針對 DNS 負載均衡的一些缺點,對于時延和故障敏感的業(yè)務(wù),有一些公司自己實現(xiàn)了 HTTP-DNS 的功能,即使用 HTTP 協(xié)議實現(xiàn)一個私有的 DNS 系統(tǒng)。這樣的方案和通用的 DNS 優(yōu)缺點正好相反。
硬件負載均衡
硬件負載均衡是通過單獨的硬件設(shè)備來實現(xiàn)負載均衡功能。
目前業(yè)界典型的硬件負載均衡設(shè)備有兩款:F5 和 A10。這類設(shè)備性能強勁、功能強大,但價格都不便宜,一般只有“土豪”公司才會考慮使用此類設(shè)備。
優(yōu)點為:
- 功能強大:全面支持各層級的負載均衡,支持全面的負載均衡算法,支持全局負載均衡。
- 性能強大:對比一下,軟件負載均衡支持到 10 萬級并發(fā)已經(jīng)很厲害了,硬件負載均衡可以支持 100 萬以上的并發(fā)。
- 穩(wěn)定性高:商用硬件負載均衡,經(jīng)過了良好的嚴格測試,經(jīng)過大規(guī)模使用,穩(wěn)定性高。
- 支持安全防護:硬件均衡設(shè)備除具備負載均衡功能外,還具備防火墻、防 DDoS 攻擊等安全功能。
缺點為:
- 價格昂貴。
- 擴展能力差:硬件設(shè)備,可以根據(jù)業(yè)務(wù)進行配置,但無法進行擴展和定制。
軟件負載均衡
軟件負載均衡通過負載均衡軟件來實現(xiàn)負載均衡功能
常見的有 Nginx 和 LVS,其中 Nginx 是軟件的 7 層負載均衡,LVS 是 Linux 內(nèi)核的 4 層負載均衡。
優(yōu)點為:
- 簡單:無論是部署還是維護都比較簡單。
- 便宜:只要買個 Linux 服務(wù)器,裝上軟件即可。
- 靈活:4 層和 7 層負載均衡可以根據(jù)業(yè)務(wù)進行選擇;也可以根據(jù)業(yè)務(wù)進行比較方便的擴展。
缺點為:
- 性能一般:一個 Nginx 大約能支撐 5 萬并發(fā)。
- 功能沒有硬件負載均衡那么強大。
- 一般不具備防火墻和防 DDoS 攻擊等安全功能。
負載均衡典型架構(gòu)
實際應(yīng)用中,一般會針對以上方案進行組合使用。組合的基本原則為:
DNS 負載均衡用于實現(xiàn)地理級別的負載均衡;硬件負載均衡用于實現(xiàn)集群級別的負載均衡;軟件負載均衡用于實現(xiàn)機器級別的負載均衡。
負載均衡算法
輪詢
負載均衡系統(tǒng)收到請求后,按照順序輪流分配到服務(wù)器上。
加權(quán)輪詢
負載均衡系統(tǒng)根據(jù)服務(wù)器權(quán)重進行任務(wù)分配,這里的權(quán)重一般是根據(jù)硬件配置進行靜態(tài)配置的,采用動態(tài)的方式計算會更加契合業(yè)務(wù),但復(fù)雜度也會更高。
負載最低優(yōu)先
負載均衡系統(tǒng)將任務(wù)分配給當前負載最低的服務(wù)器,這里的負載根據(jù)不同的任務(wù)類型和業(yè)務(wù)場景,可以用不同的指標來衡量。
負載最低優(yōu)先的算法解決了輪詢算法中無法感知服務(wù)器狀態(tài)的問題,由此帶來的代價是復(fù)雜度要增加很多。
性能最優(yōu)類
優(yōu)先將任務(wù)分配給處理速度最快的服務(wù)器,通過這種方式達到最快響應(yīng)客戶端的目的。
負載最低優(yōu)先類算法是站在服務(wù)器的角度來進行分配的,而性能最優(yōu)優(yōu)先類算法則是站在客戶端的角度來進行分配的。
和負載最低優(yōu)先類算法類似,性能最優(yōu)優(yōu)先類算法本質(zhì)上也是感知了服務(wù)器的狀態(tài),只是通過響應(yīng)時間這個外部標準來衡量服務(wù)器狀態(tài)而已。因此性能最優(yōu)優(yōu)先類算法存在的問題和負載最低優(yōu)先類算法類似,復(fù)雜度都很高。
Hash類
負載均衡系統(tǒng)根據(jù)任務(wù)中的某些關(guān)鍵信息進行 Hash 運算,將相同 Hash 值的請求分配到同一臺服務(wù)器上。