【問底】徐漢彬:億級Web系統搭建——單機到分佈式集羣(二)

Web系統的緩存機制的創建和優化

剛剛咱們講完了Web系統的外部網絡環境,如今咱們開始關注咱們Web系統自身的性能問題。咱們的Web站點隨着訪問量的上升,會遇到不少的挑戰,解決這些問題不只僅是擴容機器這麼簡單,創建和使用合適的緩存機制纔是根本。html

最開始,咱們的Web系統架構多是這樣的,每一個環節,均可能只有1臺機器。 前端

 

咱們從最根本的數據存儲開始看哈。redis

1、 MySQL數據庫內部緩存使用sql

MySQL的緩存機制,就從先從MySQL內部開始,下面的內容將以最多見的InnoDB存儲引擎爲主。數據庫

1. 創建恰當的索引後端

最簡單的是創建索引,索引在表數據比較大的時候,起到快速檢索數據的做用,可是成本也是有的。首先,佔用了必定的磁盤空間,其中組合索引最突出,使用須要謹慎,它產生的索引甚至會比源數據更大。其次,創建索引以後的數據insert/update/delete等操做,由於須要更新原來的索引,耗時會增長。固然,實際上咱們的系統從整體來講,是以select查詢操做居多,所以,索引的使用仍然對系統性能有大幅提高的做用。緩存

2. 數據庫鏈接線程池緩存服務器

若是,每個數據庫操做請求都須要建立和銷燬鏈接的話,對數據庫來講,無疑也是一種巨大的開銷。爲了減小這類型的開銷,能夠在MySQL中配置thread_cache_size來表示保留多少線程用於複用。線程不夠的時候,再建立,空閒過多的時候,則銷燬。 swoole

 

其實,還有更爲激進一點的作法,使用pconnect(數據庫長鏈接),線程一旦建立在很長時間內都保持着。可是,在訪問量比較大,機器比較多的狀況下,這種用法極可能會致使「數據庫鏈接數耗盡」,由於創建鏈接並不回收,最終達到數據庫的max_connections(最大鏈接數)。所以,長鏈接的用法一般須要在CGI和MySQL之間實現一個「鏈接池」服務,控制CGI機器「盲目」建立鏈接數。 網絡

 

創建數據庫鏈接池服務,有不少實現的方式,PHP的話,我推薦使用swoole(PHP的一個網絡通信拓展)來實現。

3. Innodb緩存設置(innodb_buffer_pool_size)

innodb_buffer_pool_size這是個用來保存索引和數據的內存緩存區,若是機器是MySQL獨佔的機器,通常推薦爲機器物理內存的80%。在取表數據的場景中,它能夠減小磁盤IO。通常來講,這個值設置越大,cache命中率會越高。

4. 分庫/分表/分區。

MySQL數據庫表通常承受數據量在百萬級別,再往上增加,各項性能將會出現大幅度降低,所以,當咱們預見數據量會超過這個量級的時候,建議進行分庫/分表/分區等操做。最好的作法,是服務在搭建之初就設計爲分庫分表的存儲模式,從根本上杜絕中後期的風險。不過,會犧牲一些便利性,例如列表式的查詢,同時,也增長了維護的複雜度。不過,到了數據量千萬級別或者以上的時候,咱們會發現,它們都是值得的。 

2、 MySQL數據庫多臺服務搭建

1臺MySQL機器,其實是高風險的單點,由於若是它掛了,咱們Web服務就不可用了。並且,隨着Web系統訪問量繼續增長,終於有一天,咱們發現1臺MySQL服務器沒法支撐下去,咱們開始須要使用更多的MySQL機器。當引入多臺MySQL機器的時候,不少新的問題又將產生。

1. 創建MySQL主從,從庫做爲備份

這種作法純粹爲了解決「單點故障」的問題,在主庫出故障的時候,切換到從庫。不過,這種作法實際上有點浪費資源,由於從庫實際上被閒着了。

 

2. MySQL讀寫分離,主庫寫,從庫讀。

兩臺數據庫作讀寫分離,主庫負責寫入類的操做,從庫負責讀的操做。而且,若是主庫發生故障,仍然不影響讀的操做,同時也能夠將所有讀寫都臨時切換到從庫中(須要注意流量,可能會由於流量過大,把從庫也拖垮)。 

 

3. 主主互備。

兩臺MySQL之間互爲彼此的從庫,同時又是主庫。這種方案,既作到了訪問量的壓力分流,同時也解決了「單點故障」問題。任何一臺故障,都還有另一套可供使用的服務。 

 

不過,這種方案,只能用在兩臺機器的場景。若是業務拓展仍是很快的話,能夠選擇將業務分離,創建多個主主互備。

3、 MySQL數據庫機器之間的數據同步

每當咱們解決一個問題,新的問題必然誕生在舊的解決方案上。當咱們有多臺MySQL,在業務高峯期,極可能出現兩個庫之間的數據有延遲的場景。而且,網絡和機器負載等,也會影響數據同步的延遲。咱們曾經遇到過,在日訪問量接近1億的特殊場景下,出現,從庫數據須要數日才能同步追上主庫的數據。這種場景下,從庫基本失去效用了。

因而,解決同步問題,就是咱們下一步須要關注的點。

1. MySQL自帶多線程同步

MySQL5.6開始支持主庫和從庫數據同步,走多線程。可是,限制也是比較明顯的,只能以庫爲單位。MySQL數據同步是經過binlog日誌,主庫寫入到binlog日誌的操做,是具備順序的,尤爲當SQL操做中含有對於表結構的修改等操做,對於後續的SQL語句操做是有影響的。所以,從庫同步數據,必須走單進程。

2. 本身實現解析binlog,多線程寫入。

以數據庫的表爲單位,解析binlog多張表同時作數據同步。這樣作的話,的確可以加快數據同步的效率,可是,若是表和表之間存在結構關係或者數據依賴的話,則一樣存在寫入順序的問題。這種方式,可用於一些比較穩定而且相對獨立的數據表。 

 

國內一線互聯網公司,大部分都是經過這種方式,來加快數據同步效率。還有更爲激進的作法,是直接解析binlog,忽略以表爲單位,直接寫入。可是這種作法,實現複雜,使用範圍就更受到限制,只能用於一些場景特殊的數據庫中(沒有表結構變動,表和表之間沒有數據依賴等特殊表)。 

4、 在Web服務器和數據庫之間創建緩存

實際上,解決大訪問量的問題,不能僅僅着眼於數據庫層面。根據「二八定律」,80%的請求只關注在20%的熱點數據上。所以,咱們應該創建Web服務器和數據庫之間的緩存機制。這種機制,能夠用磁盤做爲緩存,也能夠用內存緩存的方式。經過它們,將大部分的熱點數據查詢,阻擋在數據庫以前。 

 

1. 頁面靜態化

用戶訪問網站的某個頁面,頁面上的大部份內容在很長一段時間內,可能都是沒有變化的。例如一篇新聞報道,一旦發佈幾乎是不會修改內容的。這樣的話,經過CGI生成的靜態html頁面緩存到Web服務器的磁盤本地。除了第一次,是經過動態CGI查詢數據庫獲取以外,以後都直接將本地磁盤文件返回給用戶。

 

在Web系統規模比較小的時候,這種作法看似完美。可是,一旦Web系統規模變大,例如當我有100臺的Web服務器的時候。那樣這些磁盤文件,將會有100份,這個是資源浪費,也很差維護。這個時候有人會想,能夠集中一臺服務器存起來,呵呵,不如看看下面一種緩存方式吧,它就是這樣作的。

2. 單臺內存緩存

經過頁面靜態化的例子中,咱們能夠知道將「緩存」搭建在Web機器本機是很差維護的,會帶來更多問題(實際上,經過PHP的apc拓展,可經過Key/value操做Web服務器的本機內存)。所以,咱們選擇搭建的內存緩存服務,也必須是一個獨立的服務。

內存緩存的選擇,主要有redis/memcache。從性能上說,二者差異不大,從功能豐富程度上說,Redis更勝一籌。 

 

3. 內存緩存集羣

當咱們搭建單臺內存緩存完畢,咱們又會面臨單點故障的問題,所以,咱們必須將它變成一個集羣。簡單的作法,是給他增長一個slave做爲備份機器。可是,若是請求量真的不少,咱們發現cache命中率不高,須要更多的機器內存呢?所以,咱們更建議將它配置成一個集羣。例如,相似redis cluster。

Redis cluster集羣內的Redis互爲多組主從,同時每一個節點均可以接受請求,在拓展集羣的時候比較方便。客戶端能夠向任意一個節點發送請求,若是是它的「負責」的內容,則直接返回內容。不然,查找實際負責Redis節點,而後將地址告知客戶端,客戶端從新請求。 

 

對於使用緩存服務的客戶端來講,這一切是透明的。

 

內存緩存服務在切換的時候,是有必定風險的。從A集羣切換到B集羣的過程當中,必須保證B集羣提早作好「預熱」(B集羣的內存中的熱點數據,應該儘可能與A集羣相同,不然,切換的一瞬間大量請求內容,在B集羣的內存緩存中查找不到,流量直接衝擊後端的數據庫服務,極可能致使數據庫宕機)。

4. 減小數據庫「寫」

上面的機制,都實現減小數據庫的「讀」的操做,可是,寫的操做也是一個大的壓力。寫的操做,雖然沒法減小,可是能夠經過合併請求,來起到減輕壓力的效果。這個時候,咱們就須要在內存緩存集羣和數據庫集羣之間,創建一個修改同步機制。

先將修改請求生效在cache中,讓外界查詢顯示正常,而後將這些sql修改放入到一個隊列中存儲起來,隊列滿或者每隔一段時間,合併爲一個請求到數據庫中更新數據庫。 

 

除了上述經過改變系統架構的方式提高寫的性能外,MySQL自己也能夠經過配置參數innodb_flush_log_at_trx_commit來調整寫入磁盤的策略。若是機器成本容許,從硬件層面解決問題,能夠選擇老一點的RAID(Redundant Arrays of independent Disks,磁盤列陣)或者比較新的SSD(Solid State Drives,固態硬盤)。

5. NoSQL存儲

無論數據庫的讀仍是寫,當流量再進一步上漲,終會達到「人力有窮時」的場景。繼續加機器的成本比較高,而且不必定能夠真正解決問題的時候。這個時候,部分核心數據,就能夠考慮使用NoSQL的數據庫。NoSQL存儲,大部分都是採用key-value的方式,這裏比較推薦使用上面介紹過Redis,Redis自己是一個內存cache,同時也能夠當作一個存儲來使用,讓它直接將數據落地到磁盤。

這樣的話,咱們就將數據庫中某些被頻繁讀寫的數據,分離出來,放在咱們新搭建的Redis存儲集羣中,又進一步減輕原來MySQL數據庫的壓力,同時由於Redis自己是個內存級別的Cache,讀寫的性能都會大幅度提高。 

 

國內一線互聯網公司,架構上採用的解決方案不少是相似於上述方案,不過,使用的cache服務卻不必定是Redis,他們會有更豐富的其餘選擇,甚至根據自身業務特色開發出本身的NoSQL服務。

6. 空節點查詢問題

當咱們搭建完前面所說的所有服務,認爲Web系統已經很強的時候。咱們仍是那句話,新的問題仍是會來的。空節點查詢,是指那些數據庫中根本不存在的數據請求。例如,我請求查詢一個不存在人員信息,系統會從各級緩存逐級查找,最後查到到數據庫自己,而後才得出查找不到的結論,返回給前端。由於各級cache對它無效,這個請求是很是消耗系統資源的,而若是大量的空節點查詢,是能夠衝擊到系統服務的。

 

在我曾經的工做經歷中,曾深受其害。所以,爲了維護Web系統的穩定性,設計適當的空節點過濾機制,很是有必要。

咱們當時採用的方式,就是設計一張簡單的記錄映射表。將存在的記錄存儲起來,放入到一臺內存cache中,這樣的話,若是還有空節點查詢,則在緩存這一層就被阻擋了。 

 

相關文章
相關標籤/搜索