NoSQL:在高并發(fā)場景下,數(shù)據(jù)庫和NoSQL如何做到互補?
針對存儲服務的優(yōu)化,我們通常會著手兩個方面:
1. 提升讀寫性能,特別是優(yōu)化讀取效率。因為我們的產(chǎn)品多數(shù)是讀取頻率高于寫入頻率的。例如,微信朋友圈、微博和淘寶等服務,它們的查詢 QPS明顯高于寫入 QPS。
2. 加強存儲系統(tǒng)的擴展性,以滿足不斷增長的數(shù)據(jù)存儲需求。
NoSQL,No SQL?
NoSQL是一種非關系型數(shù)據(jù)庫,與傳統(tǒng)的關系型數(shù)據(jù)庫有所不同。NoSQL數(shù)據(jù)庫不使用SQL作為查詢語言,而是提供了其他方式來操作數(shù)據(jù)。它們通常具有出色的橫向擴展能力和高性能的讀寫操作,非常適合處理互聯(lián)網(wǎng)項目中的大數(shù)據(jù)量和高并發(fā)訪問的需求。因此,許多大型公司,如小米、微博和陌陌,都傾向于選擇NoSQL數(shù)據(jù)庫作為其處理高并發(fā)大容量數(shù)據(jù)的存儲服務。
Redis、LevelDB等鍵值存儲數(shù)據(jù)庫,以其極高的讀寫性能而著稱,通常在對性能有較高要求的場景下得到廣泛應用。Hbase、Cassandra等列式存儲數(shù)據(jù)庫則以其以列為存儲單位而聞名,特別適用于離線數(shù)據(jù)統(tǒng)計等場景。至于MongoDB、CouchDB等文檔型數(shù)據(jù)庫,其主要特點在于Schema Free,即數(shù)據(jù)表的字段可以靈活擴展,這使得它們在處理具有多變字段結構的數(shù)據(jù),比如電商系統(tǒng)中的商品信息,更加簡單和靈活
使用 NoSQL 提升寫入性能
數(shù)據(jù)庫系統(tǒng)通常使用傳統(tǒng)的機械硬盤進行存儲,而機械硬盤的訪問方式主要有兩種:隨機IO和順序IO。隨機IO需要花費大量時間進行磁盤尋道,因此其讀寫效率通常比順序IO低兩到三個數(shù)量級。為了提升寫入性能,需要盡量減少隨機IO的發(fā)生。
以MySQL的InnoDB存儲引擎為例,更新binlog、redo log、undo log等操作通常采用順序IO,而更新datafile和索引文件則需要進行隨機IO。盡管關系數(shù)據(jù)庫進行了許多優(yōu)化,比如先將寫入數(shù)據(jù)存入內存,然后批量刷新到磁盤上,但隨機IO仍然難以避免。
在InnoDB引擎中,索引通常采用B+樹結構組織。由于MySQL的主鍵是聚簇索引,即數(shù)據(jù)和索引數(shù)據(jù)存儲在一起,因此在數(shù)據(jù)插入或更新時需要找到合適的位置并寫入特定位置,這就會引發(fā)隨機IO。此外,一旦發(fā)生頁分裂,就不可避免地會涉及數(shù)據(jù)的移動,進一步降低寫入性能。
NoSQL 數(shù)據(jù)庫是怎么解決這個問題的呢?
許多NoSQL數(shù)據(jù)庫都采用基于LSM樹的存儲引擎,這種算法在性能上取得了很大突破,因此在這里我想深入探討一下LSM樹的工作原理。LSM樹(Log-Structured Merge Tree)通過犧牲一定的讀性能來實現(xiàn)高效的寫入操作,因此像Hbase、Cassandra和LevelDB等數(shù)據(jù)庫都采用了這種存儲引擎。
LSM樹的核心思想很簡單:數(shù)據(jù)首先寫入到一個稱為MemTable的內存結構中,并按照寫入的鍵(Key)進行排序。為了避免因機器掉電或重啟而丟失MemTable中的數(shù)據(jù),通常會通過Write Ahead Log的方式將數(shù)據(jù)備份到磁盤上。當MemTable累積到一定規(guī)模時,會將其刷新為一個新文件,我們稱之為SSTable(Sorted String Table)。當SSTable數(shù)量達到一定閾值時,會將它們進行合并,以減少文件數(shù)量,因為SSTable都是有序的,所以合并速度非常快。
在進行LSM樹的數(shù)據(jù)讀取時,首先從MemTable中查找數(shù)據(jù),如果未找到,則從SSTable中查找。由于數(shù)據(jù)是有序存儲的,因此查詢效率非常高。然而,由于數(shù)據(jù)被拆分成多個SSTable,讀取效率可能低于B+樹索引。
圖片
提升擴展性
此外,在可擴展性方面,許多NoSQL數(shù)據(jù)庫具有天然的優(yōu)勢。以您的垂直電商系統(tǒng)為例,您已經(jīng)添加了評論系統(tǒng),最初對系統(tǒng)的評估比較樂觀,認為評論量級不會迅速增長,因此將數(shù)據(jù)庫分成了8個庫,每個庫又分成了16張表。但是評論系統(tǒng)上線后,存儲量迅速增長,您不得不將數(shù)據(jù)庫進一步分割成更多的庫和表,而數(shù)據(jù)也必須重新遷移到新的庫表中,這個過程非常痛苦且容易出錯。在這種情況下,您是否考慮過使用NoSQL數(shù)據(jù)庫來徹底解決可擴展性問題呢?經(jīng)過調查,您會發(fā)現(xiàn)NoSQL數(shù)據(jù)庫在設計之初就考慮到了分布式和大數(shù)據(jù)存儲的場景,比如像MongoDB就具備三個關鍵的擴展性特性。
另一個關鍵特性是Replica,也稱為副本集。您可以將其視為主從復制,即通過將數(shù)據(jù)復制多份來確保在主節(jié)點故障時數(shù)據(jù)不會丟失。同時,副本還可以分擔讀請求。在副本中,主節(jié)點負責處理寫請求,并將數(shù)據(jù)變更記錄到oplog中(類似于binlog);從節(jié)點接收oplog后,會修改自身的數(shù)據(jù)以保持與主節(jié)點的一致性。一旦主節(jié)點故障,MongoDB將從從節(jié)點中選取一個節(jié)點作為主節(jié)點,繼續(xù)提供寫入數(shù)據(jù)的服務。
第二個特性是Shard,也稱為分片,您可以將其視為分庫分表,即根據(jù)某種規(guī)則將數(shù)據(jù)拆分成多份,存儲在不同的機器上。MongoDB的Sharding特性通常需要三個角色來支持:Shard Server,實際存儲數(shù)據(jù)的節(jié)點,是一個獨立的Mongod進程;Config Server,也是一組Mongod進程,主要存儲一些元信息,例如哪些分片存儲了哪些數(shù)據(jù)等;最后是Route Server,它不實際存儲數(shù)據(jù),僅用作路由,從Config Server獲取元信息后,將請求路由到正確的Shard Server。
圖片
另外一個關鍵特性是負載均衡,即當MongoDB發(fā)現(xiàn)Shard之間數(shù)據(jù)分布不均勻時,會啟動Balancer進程對數(shù)據(jù)進行重新分配,以確保不同Shard Server的數(shù)據(jù)盡可能均衡。當Shard Server的存儲空間不足需要擴容時,數(shù)據(jù)會自動遷移到新的Shard Server上,從而減少了數(shù)據(jù)遷移和驗證的成本。
在性能方面,NoSQL數(shù)據(jù)庫利用一些算法將磁盤上的隨機寫操作轉換為順序寫,從而提升了寫入性能。在某些場景下,如全文搜索功能,傳統(tǒng)的關系型數(shù)據(jù)庫無法有效支持,而需要借助NoSQL數(shù)據(jù)庫的特性。
就擴展性而言,NoSQL數(shù)據(jù)庫天生支持分布式架構,具備數(shù)據(jù)冗余和數(shù)據(jù)分片的特性。這些特點使得NoSQL成為傳統(tǒng)關系型數(shù)據(jù)庫的有效補充。在選擇NoSQL數(shù)據(jù)庫時,需要深入了解各種組件的實現(xiàn)原理,并具備一定的運維經(jīng)驗。否則,盲目地引入新的NoSQL數(shù)據(jù)庫可能導致故障無法解決,成為整個系統(tǒng)的負擔。
我曾在以前的項目中使用Elasticsearch作為持久存儲,支持社區(qū)的動態(tài)流功能。初期開發(fā)進展順利,Elasticsearch提供了靈活高效的查詢功能,業(yè)務功能得以快速迭代,代碼也簡潔易懂。然而,隨著流量的增加,由于缺乏成熟的Elasticsearch運維能力,頻繁出現(xiàn)故障。尤其是在高峰期,節(jié)點不穩(wěn)定的問題更加突出。由于業(yè)務壓力巨大,無法投入足夠的人力和時間深入學習和理解Elasticsearch,最終不得不做出重大改動,回歸熟悉的MySQL。因此,對于開源組件的使用,不應止步于簡單的入門階段,而應具備足夠的運維能力。