如今儲存資料的方式有很多,而硬碟因為價格和資料保護方面的優勢,是大部分使用者的首選。但是,硬碟和記憶體相比在 IO 讀寫上慢了好幾個數量級,那為什麼會更偏好硬碟呢?
首先需要提到的是,操作磁碟之所以慢主要是因為對磁碟的讀寫耗時。讀寫主要有三部分耗時: 尋道時間+旋轉時間+傳輸時間,其中尋道時間是最久的。因為尋道需要移動磁頭到對應的磁軌上,通過馬達驅動磁臂移動,是一種機械運動因此耗時較長。同時我們對磁碟的操作通常都是隨機讀寫,也就需要頻繁移動磁頭到對應的磁軌,這就讓耗時延長了,顯得效能比較低。
這樣看來如果要讓磁碟讀寫速度變快,只要不使用隨機讀寫,或者減少隨機的次數,就可以有效提升磁碟讀寫速度了。那具體要如何操作呢?
順序讀寫
先來聊聊第一個方法,如何使用順序讀寫,而不是隨機讀寫?上面提到尋道時間是耗時最久的,所以最直觀的思路就是省去這部分時間,而順序 IO 正好可以滿足需求。
追加寫就是一種典型的順序 IO,使用這個思路優化的典型的產品就是訊息佇列。以熱門的 Kafka 為例,Kafka 為了實現高效能 IO,用了很多優化的方法,其中就使用了順序寫這種優化方法。
Kafka 以時間複雜度為 O(1) 的方式提供訊息持久化能力,即使對 TB 級以上資料也能保證常數時間複雜度的訪問效能。對於每個分割槽,它把從 Producer 收到的訊息,順序地寫入對應的 log 檔案中,一個檔案寫滿後,才開啟一個新的檔案。消費的時候,也是從某個全域性的位置開始,也就是某一個 log 檔案中的某個位置開始,按順序地讀出訊息。
減少隨機次數
看完了順序方式,我們再來看看減少隨機寫次數的方法。在很多場景中,為了方便我們後續對資料的讀取和操作,我們要求寫入硬碟的資料是有序的。比如在 MySQL 中,索引在 InnoDB 引擎中是以 B+ 樹方式來組織的,而 MySQL 主鍵是聚簇索引(一種索引型別,資料與索引資料放在一起),既然資料和索引資料放在一起,那麼在資料插入或者更新的時候,我們需要找到要插入的位置,再把資料寫到特定的位置上,這就產生了隨機的 IO。所以,如果我們每次插入、更新資料都把資料寫入至 .ibd 檔案的話,然後磁碟也要找到對應的那條記錄,然後再更新,整個過程 IO 成本、查詢成本都很高,資料庫的效能和效率都會大打折扣。
為了解決寫入效能問題,InnoDB 引入了 WAL 機制,更確切的說,就是 redo log。下面我再簡單介紹下 Redo Log。
InnoDB redo log 是一個順序寫入的、大小固定的環形日誌。主要作用有兩個:
- 提高 InnoDB 儲存引擎寫入資料的效率
- 保證 crash-safe 能力
在這裡我們只關心它是如何提高寫入資料的效率的。下圖是 redo log 的示意圖。
從圖中可以看出,red olog 的寫入是順序寫入的,不需要找到某一個具體的索引位置,而是簡單地從 write-pos 指標位置追加。
其次當一個寫事務或者更新事務執行時,InnoDB 首先取出對應的 Page,然後進行修改。當事務提交時,將位於記憶體中的 redo log buffer 強制重新整理至硬碟中,如果不考慮 binlog 的話,我們可以認為事務執行可以返回成功了,寫入 DB 的操作由另外的執行緒非同步進行。
再然後,可由 InnoDB 的 Master Thread 定時地將緩衝池中的髒頁,也就是上面兒我們修改的頁,重新整理至磁碟,此時被修改資料真正的寫入至 . ibd 檔案。
總結
很多時候,我們不得不把資料儲存到硬碟上,但是由於硬碟相對於記憶體來說實在是太“慢”了。所以我們就得想辦法提升效能。文章裡總結了兩個方法,第一個是追加寫,利用順序 IO 來實現快速寫;第二個是很多資料庫中為了提升效能都會引入的 WAL 機制。文章裡分別拿了 Kafka 和 MySQL 的 InnoDB 儲存引擎舉例。除了這兩個,感興趣的同學也可以看下 LSM 樹,etcd 等是怎麼實現快速寫的。