數據庫分片

在這篇文章中,我們將討論如何擴展數據庫以應對高寫入吞吐量和存儲空間的挑戰。爲此,我們將使用一種叫做分片(sharding)的方法。我們從零開始,假設數據庫只有一個單節點。

我們有多個客戶端,它們都在從該數據庫讀取和寫入數據,但由於數據庫僅有一個節點,所有的讀寫操作都集中在這個單一設備上。如果我們的客戶端數量非常多,並且它們的讀寫操作都非常快速,我們很快就會遇到存儲空間不足的情況,而且數據庫的處理速度可能也不足以同時處理這麼多的讀寫請求。這個問題可能是由於網絡限制或機器資源不足導致的。

因此,擴展數據庫的一個簡單方法是將其複製到多個節點上。這意味着我們將擁有多個數據庫副本,每個副本都包含數據集的 100%。

因爲數據庫本身是一個有狀態的系統,我們必須在這三個數據庫節點之間保持同步,這意味着它們需要相互通信,以確保對任何一個節點的寫入操作能夠複製到其他節點。因此,這個方法解決了我們的讀取吞吐量問題,因爲我們的任何一個客戶端都可以從任意一個數據庫副本讀取數據。當然,這些副本包含的是相同的數據集,因此任何副本對讀取來說都是等效的。

然而,這並沒有解決寫入問題。任何對某一個節點的寫入操作都必須複製到其他兩個節點,否則我們就無法確定數據的存儲位置,不同節點的讀取可能會導致返回不同的數據。因此,這些數據庫節點仍然需要處理所有的寫入吞吐量和數據存儲空間。所以,如果我們的系統主要是讀取密集型的,這種方法可能有效。但如果我們有大量數據,或者寫入操作非常頻繁,那麼我們就需要超越簡單的讀複製,開始考慮使用分片技術。

分片意味着,我們不再在每個數據庫節點上存儲完整的數據集,而是將數據集拆分到多個節點上。一個簡單的例子是,如果我們有少量客戶(例如一個面向企業客戶的軟件產品),並且我們希望將每個客戶的數據存儲在獨立的數據庫節點上。例如,如果我們有客戶 A、B 和 C,我們可以將客戶 A 的數據存儲在第一個分片(shard 1)中,客戶 B 的數據存儲在第二個分片(shard 2)中,客戶 C 的數據存儲在第三個分片(shard 3)中,前提是客戶的數據完全隔離。

這種方式非常適合將數據有效地分佈到各個節點上,前提是所有客戶的數據量差不多,所有數據庫節點的存儲空間、寫入吞吐量和讀取吞吐量都差不多。隨着客戶數量的增加,我們可以簡單地增加更多的數據庫節點,從而有效地處理擴展問題。

當然,這只是一個簡單的例子,其中所有的數據都完全隔離,每個數據庫物理節點和客戶之間有一一對應的關係。在大多數情況下,情況不會如此簡單。爲了擴展這種模式,我們需要將多個客戶的數據存儲在一個數據庫節點上,這樣可以更高效地利用資源。例如,如果我們新增一個客戶 D,我們可以將其數據存儲到這三個節點中的一個,且需要某種算法來有效地控制哪些客戶的數據存儲在哪些節點上,確保每個節點的存儲空間和寫入吞吐量大致相同。一旦我們開始這樣做,我們就需要一個系統,它可以隨時告訴我們的客戶端某個數據存儲在哪個數據庫節點上。例如,如果我們要查找客戶 A 的數據,我們必須請求一個外部服務,告訴我們客戶 A 的數據存儲在 shard 1 上,然後客戶端就可以去 shard 1 查詢數據。

對於這個簡單的分片模型,它確實能很好地工作,許多面向企業客戶的軟件產品就採用類似的模型,因爲這種方法非常簡單,並且不需要額外的數據庫基礎設施。然而,許多數據模型並沒有那麼理想的鍵可以用來分片。我們來看一下,如果我們使用文檔 ID 作爲分片鍵會發生什麼。一種簡單的方法是爲每個分片設定一個值的範圍。例如,如果我們的數據庫中有 30,000 條記錄,我們可以將 ID 爲 0 到 10,000 的記錄存儲在第一個分片中,ID 爲 10,000 到 20,000 的記錄存儲在第二個分片中,ID 爲 20,000 到 30,000 的記錄存儲在第三個分片中。這樣每個分片就存儲了三分之一的數據集,這很好。

但是,我們在處理寫入吞吐量方面做得不好。例如,如果我們要插入 25,000 ID 以後的 1,000 條記錄,所有這些記錄都會進入第三個分片,這意味着在任何時刻,只有一個分片在處理所有的寫入吞吐量。因此,雖然這種方法能夠擴展存儲空間,但對於寫入吞吐量的擴展效果不佳。當我們有遞增 ID 時,這種基於範圍的分片就失效了。

爲了得到一個更通用的解決方案,我們來看一下基於哈希的分片。通過哈希,我們可以使用某種函數,將 ID 映射到一個固定的值集合。例如,我們將 ID 輸入哈希函數,得到一個介於 0 到 99 之間的值來表示這個 ID。如果我們輸入相同的 ID 兩次,得到的哈希值也是相同的。所以哈希值實際上代表了輸入的 ID,但它並不直接映射回 ID,而是一個隨機值。通過哈希值,我們可以使用模運算將數據分佈到不同的分片上。例如,哈希值爲 0-36 的記錄可以存儲在 shard 1 上,哈希值爲 37-73 的記錄可以存儲在 shard 2 上,哈希值爲 74-99 的記錄可以存儲在 shard 3 上。這樣每個分片存儲了三分之一的數據集,而且這些數據是隨機分佈的。

這有幾個優點。首先,不管 ID 的值是否遞增,我們的寫入都會被隨機分配到一個節點上,這樣就能均勻分配寫入吞吐量。其次,我們不再需要一箇中心化的服務來告訴我們數據存儲在哪個分片上,客戶端可以使用這個哈希函數自己查詢數據所在的分片。

然而,當我們按用戶維度去讀取數據會遇到了一些問題。由於我們的數據是隨機分佈在各個分片上的,查詢數據時,我們需要查詢每個分片,以確定數據的存儲位置。因此,如果我們要執行某個查詢,就需要跨所有節點進行拆分,每個數據庫節點只處理部分查詢。

這比沒有分片要好,但我們仍然需要掃描整個數據庫的數據才能找到我們要的數據。哈希算法對於這種情況基本無用,因爲我們事先不知道 ID。所以,如果查找特定用戶的數據是一個常見的查詢模式,我們可能會考慮使用用戶作爲分片鍵,而不是 ID,這樣可以更高效地映射到查詢模式,從而使得用戶數據可以物理上集中存儲在一個機器上。

這種方法的優點是,當查詢模式匹配時,我們可以更高效地找到數據。而且,所有與單個用戶相關的數據都會存儲在一個數據庫節點上。如果用戶數量較少,且每個用戶的數據量非常大,我們就可以有效地減少跨節點的查詢。

如果我們有大量的用戶,分片會更加平衡,避免出現某個分片負載過重的情況。然而,如果某個分片的某些鍵有很高的頻率(例如某個用戶的數據量特別大),可能會導致 “熱點”,即某個節點需要處理遠高於其他節點的吞吐量。

總之,選擇一個合適的分片鍵非常重要,我們需要確保分片鍵有高的基數(即有足夠的不同值),並且頻率低,這樣就能有效地分配數據,避免出現 “熱點” 或節點資源過於閒置的情況。同時,分片鍵需要符合我們的查詢模式,否則,即使分片鍵本身設計得很好,也可能在大規模數據訪問時遇到問題。

一個有趣的分片方法是地理分片。通過使用用戶的地理位置作爲分片鍵,我們可以將來自特定地區的數據物理上分組在一起。如果我們將數據庫節點部署在這些地區,用戶訪問自己數據時的延遲會更低。例如,美國用戶的所有數據可以存儲在 shard 1 上,而歐洲用戶的數據存儲在 shard 2 上。

這樣,美國用戶訪問美國的數據時會較快,但訪問歐洲的數據時就需要跨大洋,延遲可能很高。結合地理分片和其他分片模式,可以有效減少用戶訪問數據時的延遲。

總之,選擇一個合適的分片鍵非常重要,它能夠確保我們的查詢模式能夠高效執行。雖然分片鍵可以幫助我們高效擴展存儲和吞吐量,但它不能取代索引的作用。我們的查詢模式可能涉及多個不同的屬性,因此需要確保能夠處理這些查詢。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bzQ1x35gJ8D3xlFnyLJtQg