微服務架構下數據如何存儲?有考慮過嗎?

關注Java後端技術棧

回覆“面試”獲取最新資料

回覆“加羣”邀您進技術交流羣

前言

微服務架構下,很適合用 DDD(Domain-Drive Design)思維來設計各個微服務,使用領域驅動設計的理念,工程師們的關注點需要從 CRUD 思維中跳出來,更多關注通用語言的設計、實體以及值對象的設計。至於數據倉庫,會有更多樣化的選擇。分佈式系統中數據存儲服務是基礎,微服務的領域拆分、領域建模可以讓數據存儲方案的選擇更具靈活性。

不一定所有的微服務都需要有一個底層的關係型數據庫作爲實體對象實例的存儲。以一個簡單的電商系統爲例:“用戶微服務”和“商品微服務”都分別需要關係型數據庫存儲結構化的關聯數據。但比如有一個“關聯推薦微服務“需要進行用戶購買、加購物車、訂單、瀏覽等多維度的數據整合,這個服務不能將其他所有訂單,用戶、商品等服務的數據冗餘進來,這種場景可以考慮使用圖形數據庫。又比如有一個“驗證碼微服務”,存儲手機驗證碼、或者一些類似各種促銷活動發的活動碼、口令等,這種簡單的數據結構,而且讀多寫少,不需長期持久化的場景,可以只使用一個 K-V(鍵值對)數據庫服務。

本文先簡單介紹下適合微服務架構體系的一些分佈式數據存儲方案,然後深入介紹下這些存儲服務的數據結構實現,知其然知其所以然。後續文章會繼續介紹分佈式數據存儲的複製、分區。

數據存儲類型介紹

不同的數據存儲引擎有着不同的特徵,也適合不同的微服務。在做最初的選型時,需要先根據對整體業務範圍的判斷,選擇儘量普適於大多數微服務的存儲。例如,初創型企業,需要綜合考慮成本節約以及團隊的知識掌握度等問題,MySQL 是比較常見的選擇,電商類型的微服務應用更適合 InnoDB 引擎(事務、外鍵的支持、行鎖的性能),雖然 InnoDB 的讀性能會比 MyISAM 差,但是讀場景有很多可以優化的方案,如搜索引擎、分佈式緩存、本地緩存等。

下面會以不同場景爲例,整理一部分常用的數據存儲引擎,實際的企業應用中會針對不同場景、服務特徵綜合使用多種存儲引擎。

關係型數據庫

存儲結構化數據,以及需要更多維度關聯,需要給用戶提供豐富的實時查詢場景時,應該使用關係型數據庫。從開源以及可部署高可用性集羣的方面來看,MySQLPostgreSQL 都是不錯的選擇。PostgreSQL 的歷史更爲悠久,兩者都有很多大互聯網公司如 Twitter、Facebook、Yahoo 等部署着大規模分佈式存儲集羣,集羣的複製、分區方案會在後續文章詳細介紹。

NoSQL

NoSQL 即 Not Only SQL,其概念比關係型數據庫更新,NoSQL 爲數據的查詢提供了更靈活、豐富的場景。下面簡單列舉了一些 NoSQL 數據庫及其應用場景。工程師不一定需要掌握所有的 NoSQL 數據庫的細節,對於不同的領域模型的設計,能有更多的靈感會更好。

KeyValue 存儲

KeyValue 可以說是 NoSQL 中比較簡單的一族,大多數操作只有 get()、put(),基礎的數據格式也都是簡單的 Key-Value。

目前比較流行的鍵值存儲服務有 RedisMemcached 以及上篇文中提到的 Dynamo。其中 Redis 有 Redis Cluster 提供了支持 Master 選舉的高可用性集羣。Dynamo 也有分佈式高可用集羣,基於 Gossip 協議的節點間故障檢測,以及支持節點暫時、永久失效的故障恢復,這兩者爲了保證高可用以及性能,犧牲了強一致性的保證,但是都支持最終一致性。Memcached 提供了高性能的純基於內存的 KV 存儲,並且提供 CAS 操作來支持分佈式一致性,但 Memcached 沒有官方提供的內置集羣方案,需要使用一些代理中間件,如 Magento 來部署集羣。

在實際選擇時,如果需要高速緩存的性能並且可以接受緩存不被命中的情況,以及可以接受 Memcached 服務實例重啓後數據全部丟失,可以選擇 Memcached。用 Memcached 做二級緩存來抗住一些高 QPS 的請求是很適合的,比如對於一些 Hot 商品的信息,可以放到 Memcached 中,緩解 DB 壓力。

如果既需要有數據持久化的需求,也希望有好的緩存性能,並且會有一些全局排序、數據集合並等需求,可以考慮使用 Redis。Redis 除了支持 K-V 結構的數據,還支持 list、set、hash、zset 等數據結構,可以使用 Redis 的 SET key value 操作實現一些類似手機驗證碼的存儲,對於需要按照 key 值排序的 kv 數據可以用 ZADD key score member。利用 Redis 的單線程以及持久化特性,還可以實現簡單的分佈式鎖,具體可以參考筆者之前寫的這篇 基於 Redis 實現分佈式鎖實現 文章。

文檔型數據庫

面向文檔的數據庫可以理解成 Value 是一個文檔類型數據的 KV 存儲,如果領域模型是個文件類型的數據、並且結構簡單,可以使用文檔型數據庫,比較有代表性的有 MongoDBCouchDB。MongoDB 相比可用性,更關注一致性,Value 存儲格式是內置的 BSON 結構,CouchDB 支持內置 JSON 存儲,通過 MVCC 實現最終一致性,但保證高可用性。

如果你需要的是一個高可用的多數據中心,或者需要 Master-Master,並且需要能承受數據節點下線的情況,可以考慮用 CouchDB。如果你需要一個高性能的,類似存儲文檔類型數據的 Cache 層,尤其寫入更新比較多的場景,那就用 MongoDB 吧。另外,2018 年夏天可以期待下,MongoDB 官方宣佈即將發佈的 4.0 版本,支持跨副本集(Replica set)的 ACID 事務,4.2 版本將支持跨集羣的事務,詳情可以關注 MongoDB 的 Beta 計劃

圖形數據庫

在現實世界中,一個圖形的構成主要有“點”和“邊”,在圖形數據庫中也是一樣,只不過點和邊有了抽象的概念,“點”代表着一個實體、節點,“邊”代表着關係。開源的 Neo4j 是可以支持大規模分佈式集羣的圖形數據庫。一般被廣泛用於道路交通應用、SNS 應用等,Neo4j 提供了獨特的查詢語言 CypherQueryLanguage

爲了直觀瞭解 Neo4j 的數據結構,可以看下這個示例(在運行 Neo4j 後,官方的內置數據示例),圖中綠色節點代表“Person”實體,中間的有向的剪頭連線就是代表節點之間的關係“Knows”。

通過以下 CQL 語句就可以查詢所有 Knows、Mike 的節點以及關係:

MATCH p=()-[r:KNOWS]->(g) where g.name ='Mike' RETURN p LIMIT 25

以上只是單個點和單維度關係的例子,在實際中 Person 實體間可能還存在 Follow、Like 等關係,如果想找到 Knows 並且 Like Mike,同時又被 Jim Follow 的 Person。在沒有圖形數據庫的情況下,用關係型數據庫雖然也可以查詢各種關聯數據,但這需要各種表 join、union,性能差而且需要寫很多 SQL 代碼,用 CQL 只要一行即可。

在 SpringBoot 工程中,使用 Springboot-data 項目,可以很簡單地和 Neo4j 進行集成,官方示例可以直接 checkout 查看 java-spring-data-neo4j

文檔數據庫一般都是很少有數據間的關聯的,圖形數據庫就是爲了讓你快速查詢一切你想要的關聯。如果想更進一步瞭解 Neo4j,可以直接下載 Neo4j 桌面客戶端,一鍵啓動、然後在瀏覽器輸入 http://localhost:7474/browser/ 就可以用起來了。

列族數據庫

列族數據庫一般都擁有大規模的分佈式集羣,可以用來做靈活的數據分析、處理數據報表,尤其適合寫多讀少的場景。列族和關係型數據庫的差別,從應用角度來看,主要是列族沒有 Schema 的概念,不像關係型數據庫,需要建表的時候定義好每個列的字段名、字段類型、字段大小等。

列族數據庫中目前比較廣泛應用的有 Hbase,Hbase 是基於 Google BigTable 設計思想的開源版。BigTable 雖然沒開源,但是其論文 Bigtable: A Distributed Storage System for Structured Data 提供了很多設布式列族 DB 的實現邏輯。另外 Facebook Cassandra 也是一個寫性能很好的列族數據庫,其參考了 Dynamo 的分佈式設計以及 BigTable 的數據存儲結構,支持最終一致性,適合跨地域的多數據中心的分佈式存儲。不過 Cassandra 中文社區相對薄弱,國內還是 Hbase 的集羣更爲廣泛被部署。

存儲服務的數據結構

在瞭解了一些分佈式數據存儲的產品之後,爲了能更深地理解,下面會對分佈式存儲引擎的一些常用數據結構做進一步介紹。一臺計算機,可以作爲數據存儲的地方就是內存、磁盤。分佈式的數據存儲就是將各個計算機(Node)的內存和磁盤結合起來,不同類型的存儲服務使用的核心數據結構也會不同。

哈希表

哈希表是一種比較簡單 K-V 存儲結構,通過哈希函數將 Key 散列開,Key 哈希值相同的 Value 一般會以單鏈表結構存儲。哈希表查找效率很高,常用於內存型存儲服務如 Memcached、Redis。Redis 除了哈希表,因爲其支持的操作的數據類型很多,所以還有像 Skiplist、SDS、鏈表等存儲結構,並且 Redis 的哈希表結構可以通過自動再哈希進行擴容。

哈希表一般存儲在內存中,隨着哈希表數據增多,會影響查詢效率,並且內存結構也沒法像磁盤那樣可以持久化以及進行數據恢復。Redis 默認提供了 RDB 持久化方案,定時持久化數據到 RDB。用 RDB 來做數據恢復、備份是很合適的方案,但是因爲其定期執行,所以無法保證恢復數據的一致性、完整性。Redis 還支持另一種持久化方案——基於 AOF(Append Only File) 方式,對每一次寫操作進行持久化,AOF 默認不啓用,可以通過修改 redis.conf 啓用,AOF 增加了 IO 負荷,比較影響寫性能,適合需要保證一致性的場景。

SSTable

在我們平常在 Linux 上分析日誌文件的時候,比如用 grep、cat、tail 等命令,其實可以想象成在 Query 一個持久化在磁盤的 log 文件。我們可以用命令輕鬆查詢以及分析磁盤文件,查詢一個記錄的時間複雜度是 O(n) 的話(因爲要遍歷文件),查詢兩個記錄就是 2*O(n),並且如果文件很大,我們沒法把文件 load 到內存進行解析,也沒法進行範圍查詢。

SSTable(Sorted String Table) 就解決了排序和範圍查詢的問題,SSTable 將文件分成一個一個 Segment(段),不同的 Segment File 可能有相同的值,但每個 Segement File 內部是按照順序存儲的。不過雖然只是將文件分段,並且按照內容順序(Sorted String)存儲可以解決排序,但是查詢磁盤文件的效率是很低的。

爲了能快速查詢文件數據,可以在內存中附加一個 KV 結構的索引:(key-offset)。key 值是索引的值並且也是有序的,Offset 指向 Segment File 的實際存儲位置(地址偏移)。

如下圖簡單畫了一個有內存 KV 存儲的 SSTable 數據結構:

這個 k-v 結構的存儲結構又叫 Memtable,因爲 Memtable 的 key 也是有序的,所以爲了實現內存快速檢索,Memtable 本身可以使用紅黑樹、平衡二叉樹、skip list 等數據結構來實現。Ps:B-Tree、B+Tree 的結構適合做大於內存的數據的索引存儲(如 MySQL 使用 B+ 樹實現索引文件的存儲),所以其更適合磁盤文件系統,一般不會用來實現 Memtable。

SSTable 也是有些侷限性的,內存的空間是有限的,隨着文件數越來越多,查詢效率會逐漸降低。爲了優化查詢,可以將 Segment File 進行合併,減少磁盤 IO,並且一定程度持久化 Memtable(提高內存查詢效率)——這就是 LSM-tree(Log-structured Merge-Tree)。LSM-tree 最初由 Google 發佈的 Bigtable 的設計論文 提出,目前已經被廣泛用於列族數據庫如 HBase、Cassandra,並且 Google 的 LevelDB 也是用 LMS-tree 實現,LevelDB 的 Memtable 使用的是 skip list 數據結構。

這種提供 SSTable 合併、壓縮以及定期 flush Memtable 到磁盤的優化,使 LMS-tree 的寫入吞吐量高,適合高寫場景。下面以 Cassandra 爲例介紹下 LMS-tree 的典型數據流。

(1)Cassandra LMS-tree 寫

數據先寫到 Commit Log 文件中(Commit Log 用 WAL 實現)WAL 保證了故障時,可以恢復內存中 Memtable 的數據。

數據順序寫入 Memtable 中。

隨着 Memtable Size 達到一定閥值或者時間達到閥值時,會 flush 到 SSTable 中進行持久化,並且在 Memtable 數據持久化到 SSTable 之後,SSTables 都是不可再改變的

後臺進程會進行 SSTable 之間的壓縮、合併,Cassendra 支持兩種合併策略:對於多寫的數據可以使用 SizeTiered 合併策略(小的、新的 SSTable 合併到大的、舊的 SSTable 中),對於多讀的數據可以使用 Leveled 合併策略(因爲分層壓縮的 IO 比較多,寫多的話會消耗 IO),詳情可以參考 when-to-use-leveled-compaction

(2)Cassandra LMS-tree 讀

先從 Memtable 中查詢數據。

Bloom Filter 中讀取 SStable 中數據標記,Bloom Filter 可以簡單理解爲一個內存的 set 結構,存儲着被“刪除”的數據,因爲剛纔介紹到 SSTable 不能改變,所以一些刪除之後的數據放到這個 set 中,讀請求需要從這個標記着拋棄對象們的集合中讀取“不存在”的對象,並在結果中過濾。對於 SSTables 中一些過期的,會在合併時被清除掉。

從多個 SSTables 中讀取數據。

合併結果集、返回。另外,對於“更新”操作,是直接更新在 Memtable 中的,所以結果集會優先返回 Memtable 中的數據。

BTree、B + Tree

BTree 和 B + Tree 比較適合磁盤文件的檢索,一般用於關係型數據庫的索引數據的存儲,如 Mysql-InnoDB、PostgreSQL。爲了提高可用性,一般 DB 中都會有一個 append-only 的 WAL(一般也叫 redo-log)用來恢復數據,比如 MySQL InnoDB 中用 binlog 記錄所有寫操作,binlog 還可以用於數據同步、複製。

使用 Btree、B+Tree 的索引需要每個數據都寫兩次,一次寫入 redo-log、一次將數據寫入 Tree 中對應的數據頁(Page)裏。LMS-tree 結構其實需要寫入很多次,因爲會有多次的數據合併(後臺進程),因爲都是順序寫入,所以寫入的吞吐量更好,並且磁盤空間利用率更高。而 B 樹會有一些空的 Page 沒有數據寫入、空間利用率較低。讀取的效率來說,Btree 更高,同樣的 key 在 Btree 中存儲在一個固定的 Page 中,但是 LSM-tree 可能會有多個 Segment File 中存儲着同個 Key 對應的值。

圖解 MySQL 索引:B-樹、B+樹

小結

本篇介紹了很多分佈式存儲服務,在實際的開發中,需要結合領域服務的特點選擇。有的微服務可能只需要一個Neo4j,有的微服務只需要 Redis。微服務的架構應該可以讓領域服務的存儲更加靈活和豐富,在選擇時可以更加契合領域模型以及服務邊界。

文章後半部分介紹了部分存儲服務的數據結構。瞭解了實現的數據結構可以讓我們更深刻理解存儲引擎本身。從最簡單的 append-only 的文件存儲,再到哈希表、SSTable、BTree,基本涵蓋了目前流行的存儲服務的主流數據結構。如果想深入理解 LSM-tree,可以讀一下 BigTable 的那篇經典論文。

除了數據庫服務,像 Lucene 提供了全文索引的搜索引擎服務,也使用了類似 SSTable 的結構。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章