1. 程式人生 > >億級Web系統搭建:快取機制的建立和優化

億級Web系統搭建:快取機制的建立和優化

好記憶不如爛筆頭,能記下點東西,就記下點,有時間拿出來看看,也會發覺不一樣的感受。


這篇文章不錯,做系統的時候,很多地方都用到了!

Web負載均衡(請戳我)中我們講完了Web系統的負載均衡策略,現在我們開始關注我們Web系統自身的效能問題。我們的Web站點隨著訪問量的上升,會遇到很多的挑戰,解決這些問題不僅僅是擴容機器這麼簡單,建立和使用合適的快取機制才是根本。

最開始,我們的Web系統架構可能是這樣的,每個環節,都可能只有1臺機器

我們從最根本的資料儲存開始看哈。


Mysql資料庫內部快取使用

MySQL的快取機制,就從先從MySQL內部開始,下面的內容將以最常見的InnoDB儲存引擎

為主。

1. 建立恰當的索引

最簡單的是建立索引,索引在表資料比較大的時候,起到快速檢索資料的作用,但是也是有壞處的。首先,佔用了一定的磁碟空間,其中組合索引最突出,使用需要謹慎,它產生的索引甚至會比源資料更大。其次,建立索引之後的資料insert/update/delete等操作,因為需要更新原來的索引,耗時會增加。當然,實際上我們的系統從總體來說,是以select查詢操作居多,因此,索引的使用仍然對系統性能有大幅提升的作用。

2. 資料庫連線執行緒池快取

如果,每一個數據庫操作請求都需要建立和銷燬連線的話,對資料庫來說,無疑也是一種巨大的開銷。為了減少這型別的開銷,可以在MySQL中配置thread_cache_size

來表示保留多少執行緒用於複用。執行緒不夠的時候,再建立,空閒過多的時候,則銷燬。

其實,還有更為激進一點的做法,使用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資料庫表一般承受資料量在百萬級別,再往上增長,各項效能將會出現大幅度下降,因此,當我們預見資料量會超過這個量級的時候,建議進行分庫/分表/分割槽等操作。最好的做法,是服務在搭建之初就設計為分庫分表的儲存模式,從根本上杜絕中後期的風險。不過,會犧牲一些便利性,例如列表式的查詢,同時,也增加了維護的複雜度。不過,到了資料量千萬級別或者以上的時候,我們會發現,它們都是值得的。


mysql資料庫多臺伺服器搭建

1臺MySQL機器,實際上是高風險的單點,因為如果它掛了,我們Web服務就不可用了。而且,隨著Web系統訪問量繼續增加,終於有一天,我們發現1臺MySQL伺服器無法支撐下去,我們開始需要使用更多的MySQL機器。當引入多臺MySQL機器的時候,很多新的問題又將產生。

1. 建立MySQL主從,從庫作為備份

這種做法純粹為了解決“單點故障”的問題,在主庫出故障的時候,切換到從庫。不過,這種做法實際上有點浪費資源,因為從庫實際上被閒著了。

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

兩臺資料庫做讀寫分離主庫負責入類的操作,從庫負責的操作。並且,如果主庫發生故障,仍然不影響讀的操作,同時也可以將全部讀寫都臨時切換到從庫中(需要注意流量,可能會因為流量過大,把從庫也拖垮)

3. 主主互備

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

不過,這種方案,只能用在兩臺機器的場景。如果業務拓展還是很快的話,可以選擇將業務分離,建立多個主主互備。


             

Mysql資料庫機器之間資料同步

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

於是,解決同步問題,就是我們下一步需要關注的點。

1. MySQL自帶多執行緒同步

MySQL5.6開始支援主庫和從庫資料同步,走多執行緒。但是,限制也是比較明顯的,只能以庫為單位。MySQL資料同步是通過binlog日誌,主庫寫入到binlog日誌的操作,是具有順序的,尤其當SQL操作中含有對於表結構的修改等操作,對於後續的SQL語句操作是有影響的。因此,從庫同步資料,必須走單程序。

2. 自己實現解析binlog,多執行緒寫入。

以資料庫的表為單位,解析binlog多張表同時做資料同步。這樣做的話,的確能夠加快資料同步的效率,但是,如果表和表之間存在結構關係或者資料依賴的話,則同樣存在寫入順序的問題。這種方式,可用於一些比較穩定並且相對獨立的資料表。

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


在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中,這樣的話,如果還有空節點查詢,則在快取這一層就被阻擋了。


總結

Web系統會隨著訪問規模的增長,漸漸地從1臺伺服器可以滿足需求,一直成長為“龐然大物”的大叢集。而這個Web系統變大的過程,實際上就是我們解決問題的過程。在不同的階段,解決不同的問題,而新的問題又誕生在舊的解決方案之上。系統的優化是沒有極限的,軟體和系統架構也一直在快速發展,新的方案解決了老的問題,同時也帶來新的挑戰。

from : https://mp.weixin.qq.com/s?__biz=MzIwNTc4NTEwOQ==&mid=2247484263&idx=1&sn=a77a68dd0d89de44eb936769456ba547&chksm=972ad21da05d5b0b59966d33238566a2166bf96b5e55334488b31bf212156a91935f9c84b323&mpshare=1&scene=23&srcid=1225o88YMZFMjwxT4cyTIgQq#rd