數據庫從0到0.1 (一): LSM-Tree VS B-Tree

數據庫最基本兩個功能:數據的存儲和數據的查詢。 當咱們寫入數據時,數據庫能夠存儲數據;當咱們須要訪問數據時,數據庫能夠給咱們想要的數據。 數據庫會經過特定的數據模型和數據結構存儲數據,並支持經過特定的查詢語言訪問數據。本文將從最簡單的數據庫開始,討論數據庫如何存儲數據,如何查詢數據。本文將討論兩種存儲引擎:log-structured 存儲引擎和以B+樹爲表明的page-oriented存儲引擎。html

1 最簡單的數據庫

#!/bin/bash

#key,value對追加寫入文件的最後一行
db_set () {    
    echo "$1,$2" >> database
}

#查找指定key的最後一行的最新的value
db_get () {    
    grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
}

上面兩個Shell函數實現了最簡單的Key-Value數據庫。 調用db_set能夠寫入數據,調用db_get能夠查詢數據,數據的物理存儲格式是逗號分隔的普通文本文件。mysql

bash-3.2$ db_set 1 kks
bash-3.2$ db_get 1
kks
bash-3.2$ db_set 2 kangkaisen
bash-3.2$ db_get 2
kangkaisen
bash-3.2$ db_set 1 KKS
bash-3.2$ db_get 1
KKS
bash-3.2$ cat database
1,kks
2,kangkaisen
1,KKS

其中db_set函數擁有很好的寫入性能,由於是追加寫;可是db_get函數的性能十分糟糕,其時間複雜度是O(n),咱們每次必須全表Scan。git

2 Index

爲了可以快速找到特定Key對應的Value, 咱們須要引入一個數據結構:Index。 所謂Index,就是咱們在數據庫中增長額外的元數據,而後Index像路標同樣能夠快速知道咱們須要訪問數據的位置和偏移量。 Index相似漢語字典中的索引和通常書籍中的目錄。若是咱們須要按照不一樣的方式訪問相同的數據,咱們有可能須要多種不一樣的索引,好比按照Key查詢和按照Value查詢,咱們會分別須要針對Key的索引和針對Value的索引。github

Index是基於原始數據衍生的附加的數據結構,增長索引必然意味着下降數據寫入速度,增大存儲空間,因此Index是以數據寫入時的處理成本和存儲的空間成原本換取查詢的加速。這也是數據庫設計的一個trade-off,不一樣索引的查詢加速比,寫入時的處理成本,存儲的空間成本每每是不一樣的,因此在設計數據庫時選擇何種索引是一個很重要的點。算法

3 Hash Index

下面就讓咱們用Index加速以前最簡單的Key-Value DB。以前咱們db_get方法查詢特定Key必須全表Scan的緣由,是由於咱們不知道特定Key在文件中的Offest,假如咱們知道了每一個Key的Offest,咱們就能夠直接Seek到Key對應的Offest,直接讀取Key對應的Value。而Key到Offest的映射咱們天然會想起到咱們熟悉的數據結構HashMap,咱們能夠在內存中維護一個HashMap,HashMap的Key就是Key-Value DB的每一條記錄的Key,HashMap的Value就是每一條記錄在文件中的Offest。sql

屏幕快照 2018-05-01 下午5.56.01.png-298kB

有了HashMap後,咱們每次寫數據後就必需要更新HashMap,查詢數據時先從HashMap獲取特定Key的Offest,再直接Seek到文件對應Offest的位置,讀取數據。 事實上Bitcask(Riak的默認存儲引擎)就是這樣作的。數據庫

不過顯然Hash Index有兩個缺陷:bash

  1. 內存的大小必須能夠放下Hash Table
  2. Range Scan的效率十分低下

4 Segment

目前爲止,咱們都是把數據寫到一個文件中,這顯然是不合理的。 一個常見的作法就是將文件按照大小拆爲爲Segment,每一個Segment是不可變的。 Segment的概念很常見,好比Kylin和Druid中都有Segment的概念,指必定大小或者必定時間內不可變的文件。數據結構

第1部分咱們知道,咱們同一個Key的Value的更新只是追加寫入,並無刪除舊的Value。 當咱們有了多個Segment後,咱們天然就能夠按期在後臺執行Compaction操做,將同一個Key的舊Value刪除,更進一步,若是咱們數據庫支持delete的話,咱們能夠在一開始只進行標記,並不實際刪除,等到Compaction的時候,咱們再進行實際刪除。 總之一句話,基於log-structured的存儲引擎,咱們能夠經過後臺的Compaction來實現update和delete,Compaction時依然能夠進行數據的寫入和查詢。架構

屏幕快照 2018-05-01 下午6.10.49.png-239.1kB

至此,每一個Segment文件都在內存中有了對應的Hash table。 咱們查詢時爲了找到特定Key對應的Value,咱們依次查詢每一個Segment文件便可,查詢每一個Segment文件的過程和以前同樣。

這種Append-only Log-structured的存儲引擎的優勢:

  1. 順序寫的效率遠高於隨機寫
  2. 併發控制和故障恢復十分簡單,由於Segment文件是不可變的,且是Append-only的,

爲何再也不對Segment文件作索引呢?

這樣咱們就不須要順序遍歷每一個Segment文件了,有了索引咱們就只須要訪問包含特定Key的Segment文件。

5 SSTables and LSM-Trees

如今對Segment文件的格式作個簡單的改變:咱們要求全部的 key-value對必須按照Key排序。 這種格式咱們稱之爲Sorted String Table, 簡稱爲SSTable。 咱們也要求在每一個已經Merged的Segment文件中1個Key只會出現一次,Compaction過程保證了這一點。

SSTable相比Log Segments + Hash Indexes 有如下幾個明顯的優點:

  1. Segment的Merge會更加簡單和高效,即便合併的全部文件比內存還大。 由於每一個Segment是有序的,Sort Merge的成本比較低。
  2. 爲了查找特定Key,咱們再也不須要在內存中維護一個很大的Hash Map。由於全部的key-value對是按照Key排序的,因此咱們能夠維護一個Segment文件的稀疏索引,索引的Key是每一個Segment文件的Start Key,Value就是每一個Segment文件的位置。 其次,在Segment內部,因爲Segment有序,咱們再也不須要針對每一個key-value對都構建索引,咱們能夠針對Block(幾百或者幾千行數據)粒度作稀疏索引,Block內存則進行二次查詢。
  3. 因爲咱們的讀取的最小粒度是Block,咱們也能夠基於Blcok粒度作壓縮,減少磁盤空間和IO。
  4. SSTable不只能夠較好的支持Point Query,也能夠很好的支持Range Scan。

屏幕快照 2018-05-01 下午6.26.22.png-292.6kB

那麼咱們如何保證Segment文件有序呢? 由於數據寫入通常都要通過內存,在內存中咱們能夠利用Red-black tree 或者AVL tree保證有序。

至此,咱們基於SSTable的存儲引擎能夠這樣Run起來:

  1. 當一條數據寫入時,咱們將其插入到基於內存的平衡樹中(Red-black tree)。 內存中的樹咱們稱之爲Memtable
  2. 當Memtable的大小超過必定閾值時,咱們將Memtable Flush到磁盤,轉爲SSTable
  3. 當咱們查詢時,須要同時查詢內存中的Memtable和磁盤中的SSTable。
  4. 週期性的在後臺進行異步的Merge和Compaction操做。
  5. 爲了防止Memtable在Flush到磁盤前機器故障致使數據丟失,咱們能夠在磁盤上維護一個只追加寫的log文件,稱之爲Write-Ahead-Log,當集羣故障後能夠從log中恢復出Memtable。 因此咱們在每次寫入Memtable,須要先寫入WAL。當Memtable flush到磁盤後,對應的WAL文件就能夠刪除。

至此,LSM-Tree(Log-Structured Merge-Tree)的3個組件:SSTable,Memtable,Write-Ahead-Log終於全了。 從開始最簡單的Key-Value 數據庫 講到如今,我相信你已經理解了LSM-Tree的核心思想。

LSM-Tree 已經被普遍使用,好比LevelDB,RocksDB,Cassandra,HBase等,其中的SSTable也是被普遍借鑑,好比ClickHouse,Palo等。

6 磁盤簡介

磁盤結構

如圖,一個磁盤由多個盤片組成。

磁盤結構

如圖,1個盤片由一個個的同心圓組成,一個同心圓就是一個磁道,每一個磁道由多個扇區組成,每一個磁道的扇區數量是一個常量,每一個扇區的大小通常是4KB,扇區是磁盤基本的物理單元

一次磁盤IO的耗時主要由三部分組成:尋道時間 + 旋轉延遲 + 數據傳輸時間

  1. 尋道時間: 將讀寫磁頭移動至正確的磁道上所須要的時間。 目前磁盤的平均尋道時間通常在3-15ms。
  2. 旋轉延遲: 盤片旋轉將請求數據所在的扇區移動到讀寫磁盤下方所須要的時間。旋轉延遲取決於磁盤轉速,轉速爲15000rpm的磁盤其平均旋轉延遲爲2ms。
  3. 數據傳輸時間:傳輸實際數據所須要的時間,它取決於數據傳輸率,其值等於數據大小除以數據傳輸率。目前IDE/ATA能達到133MB/s,SATA II可達到300MB/s的接口數據傳輸率,數據傳輸時間一般遠小於前兩部分消耗時間

提升磁盤讀寫速度方法就是儘可能減少尋道時間和旋轉延遲,而減小尋道時間和旋轉延遲的方法就是減小磁盤的隨機IO,這就是爲何磁盤順序讀寫的性能遠高於隨機讀寫的緣由。

7 B-Trees

前面咱們從零開始瞭解了LSM-Tree的核心原理,可是在數據庫領域使用最普遍的索引結構是B-tree及其變種。

其實以前咱們爲最簡單的數據庫增長索引的時候,若是咱們同時但願提升查詢性能,支持原地更新和刪除,支持Point query和Scan query, 保持高效的插入性能,咱們就會比較天然的想到二叉查找樹, 平衡二叉查找樹,紅黑樹,B-Tree 及其最多見的變種B+Tree等樹結構, 若是再考慮到面向磁盤,以及更好地支持Scan query,咱們就會選擇B+Tree。B+Tree具備較低的深度,這樣就減小了磁盤 Seek操做的次數。

相似LSM-Tree,B-Tree也能夠提供高效地Point query和Scan query。 可是二者的設計哲學是徹底不一樣的:LSM-Tree是將數據拆分爲幾百M大小的Segments,並是順序寫入;B-Tree則是面向磁盤,將數據拆分爲固定大小的Block或Page, 通常是4KB大小,和磁盤一個扇區的大小對應,Page是讀寫的最小單位。

屏幕快照 2018-05-01 下午6.43.19.png-310kB

在數據的更新和刪除方面,B-Tree能夠作到原地更新和刪除,但因爲LSM-Tree只能追加寫,因此只能在Segment Compaction的時候進行真正地更新和刪除。

你們能夠經過B+Tree 可視化理解B+Tree的插入,查找,更新和刪除過程。

關於B+Tree更詳細的原理能夠參考此文MySQL索引背後的數據結構及算法原理

8 B-Tree VS LSM-Tree

通常而言, LSM-tree的寫更加高效(追加順序寫),B-tree的讀更加高效(LSM-tree須要訪問幾個不一樣的數據結構)。

LSM-Tree的優勢:

  1. 高吞吐的寫
  2. 能夠高效的壓縮,更節省磁盤(B-Tree通常會爲Page的分裂預留一些空間)

LSM-Tree的缺點:

  1. Compaction會影響正常數據的讀寫。 阿里爲了優化這個問題,X-DB的Compaction使用了FPGA來進行。
  2. 數據量越大,Compaction須要的磁盤帶寬就越多。
  3. B-Tree中一個Key只會出如今一個Page,可是LSM-tree中一個key可能出如今多個Segment,因此B-Tree實現事務更加簡單。

9 參考資料

[1] InnoDB事務及索引原理 

https://bit.ly/2JinKeo

[2] MySQL B+樹索引和哈希索引的區別  

http://blogread.cn/it/article/7630?f=wb_blogread

[3] 十問 TiDB :關於架構設計的一些思考 

https://mp.weixin.qq.com/s/m2_Mf0-x_KpPHbnOawyy2A

[4] 黃東旭:TiDB 數據庫的四大應用場景分析 

https://mp.weixin.qq.com/s/t8SA4tlfTjJ77CRynbRm-Q

[5] SnappyData 原理和架構: Streaming Processing,OLTP,OLAP 

https://blog.bcmeng.com/post/snappydata.html

[6] 架構選型之痛,如何構造 HTAP 數據庫來收斂技術棧?

https://mp.weixin.qq.com/s/ivJCEXmstbVAdSVgIfv54Q

[7] 暢想TiDB應用場景和HTAP演進之路

https://blog.bcmeng.com/post/tidb-application-htap.html

[8] 數據庫從0到0.1 (二): OLTP VS OLAP VS HTAP

https://blog.bcmeng.com/post/oltp-olap-htap.html

[9] CS_Offer/DataStructure/README.md

https://github.com/xuelangZF/CS_Offer/blob/master/DataStructure/README.md

[10] SkipList的那點事兒

https://sylvanassun.github.io/2017/12/31/2017-12-31-skip_list/

[11] key / value 數據庫的選型

https://www.keakon.net/2018/07/13/key%20/%20value%20%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E9%80%89%E5%9E%8B

相關文章
相關標籤/搜索