讀古今文學網 > MongoDB實戰 > 9.4 選擇分片鍵 >

9.4 選擇分片鍵

上面說的這些都依賴於正確選擇分片鍵。分片鍵選的不好,應用程序就無法利用分片集群所提供的諸多優勢。在這種情況下,插入和查詢的性能都會顯著下降。下決定時一定要嚴肅,一旦選定了分片鍵,就必須堅持選擇,分片鍵是不可修改的。1

1. 注意,一旦創建了分片鍵,沒有什麼好辦法來修改它。你最好用合適的鍵再創建一個新的分片集合,從老分片集合裡把數據導出來,再把它們還原到新集合裡。

要讓分片能提供好的體驗,部分源自瞭解怎樣才算一個好的分片鍵。因為這並不是很直觀,所以我會先描述一些不太好的分片鍵。這能很自然地引出對好分片鍵的討論。

9.4.1 低效的分片鍵

一些分片鍵的分佈性很差,而另一些則導致無法充分利用局部性原理,還有一些可能會妨礙塊的拆分。本節我們會看到一些產生這種不理想狀態的分片鍵。

1. 分佈性差

BSON對像ID是每個MongoDB文檔的默認主鍵。乍一看,一個與MongoDB核心如此接近的數據類型很有可能成為候選的分片鍵。然而,我們不能被表像蒙蔽。回想一下,所有對象ID中最重要的組成部分是時間戳,也就是說對像ID始終是升序的。遺憾的是,升序的值對分片鍵而言是很糟糕的。

要瞭解升序分片鍵的問題,你要牢記分片是基於範圍的。使用升序的分片鍵之後,所有最近插入的文檔都會落到某個很小的連續範圍內。用分片的術語來說,就是這些插入都會被路由到一個塊裡,也就是被路由到單個分片上。這實際上抵消了分片一個很大的好處:將插入的負載自動分佈到不同機器上。2結論已經很清楚了,如果想讓插入負載分不到多個分片上,就不能使用升序分片鍵,你需要某些隨機性更強的東西。

2. 注意,升序的分片鍵不會影響到更新,只要文檔都是隨機更新的。

2. 缺乏局部性

升序分片鍵有明確的方向,完全隨機的分片鍵則根本沒有方向。前者無法分散插入,而後者則可能是將插入分得太散。這點可能會違背你的直覺,因為分片的目的就是要分散讀寫操作。我們可以通過一個簡單的思想實驗對此做出說明。

假設分片集合裡的每個文檔都包含一個MD5,而且MD5字段就是分片鍵。因為MD5的值會隨著文檔的不同隨機變化,所以該分片鍵能確保插入的文檔均勻分佈在集群的所有分片上,這樣很好。但是再仔細一想,對每個分片的MD5字段索引進行的插入又會怎麼樣?因為MD5是完全隨機的,在每次插入過程中,索引中的每個虛擬內存分頁都有可能(同等可能性)被訪問到。實際上,這就意味著索引必須總是能裝在內存裡,如果索引和數據不斷增多,超出了物理內存的限制,那些會降低性能的頁錯誤是不可避免的。

這基本就是一個局部引用性(locality of reference)問題。局部的概念,至少在這裡是指任意給定時間間隔內所訪問的數據基本都是有關係的;這能用來進行相關優化。例如,雖然對像ID是個糟糕的分片鍵,但它們提供了很好的局部性,因為它們是升序的。也就是說,對索引的連續插入都會發生在最近使用的虛擬內存分頁裡;因此,在任意時刻內存裡只要有一小部分索引就可以了。

舉個不太抽像的例子,想像一下,假設你的應用程序允許用戶上傳照片,每張照片的元數據都保存在某個分片集合的一個文檔裡。現在,假設用戶批量上傳了100張照片。如果分片鍵是完全隨機的,那麼數據庫就無法利用局部性;對索引的插入會發生在100個隨機的地方。但是,如果我們假設分片鍵是用戶的ID,又會怎麼樣?此時,每次寫索引基本都會發生在同一個地方,因為插入的每個文檔都擁有相同的用戶ID值。這就利用到了局部性,你也能體會到潛在的顯著性能提升。

隨機分片鍵還有另一個問題:對這個鍵的任意一個有意義的範圍查詢都會被發送到所有分片上。還是剛才那個分片照片集合,如果你想讓應用顯示某個用戶最近創建的10張照片(這是一個很普通的查詢),隨機分片鍵仍會要求把該查詢發到所有的分片上。正如你將在下文裡看到的那樣,較粗粒度的分片鍵能讓這樣的範圍查詢落到單個分片上。

3. 無法拆分的塊

如果隨機分片鍵和升序分片鍵都不好用,那麼下一個顯而易見的選擇就是粗粒度分片鍵,用戶ID就是很好的例子。如果根據用戶ID對照片集合進行分片,你可以預料到插入會分佈在各個分片上,因為無法預知哪個用戶何時會插入數據。這樣一來,粗粒度分片鍵也能擁有隨機性,還能發揮分片集群的優勢。

粗粒度分片鍵的第二個好處是能通過局部引用性帶來效率的提升。當某個用戶插入100個照片元數據文檔,基於用戶ID字段的分片鍵能確保這些插入都落到同一個分片上,並幾乎能寫入索引的同一部分。這樣的效率很高。

粗粒度分片鍵在分佈性和局部性方面表現的都很好,但它也有一個很難解決的問題:塊有可能無限制地增長。這怎麼可能?想想基於用戶ID的示例分片鍵,它能提供的最小塊範圍是什麼?是用戶ID,不可能再小了。每個數據集都有可能存在異常情況,這時就會有問題。假設有幾個特殊用戶,他們保存的照片數量超過普通用戶數百萬。系統能將一個用戶的照片拆分到多個塊裡麼?答案是不能!這個塊不能拆分。這對分片集群是個危害,因為這會造成分片間數據不均衡的情況。

顯然,理想的分片鍵應該結合了粗粒度分片鍵與細粒度分片鍵兩者的優勢。下一節裡你就能一睹它的芳容。

9.4.2 理想的分片鍵

通讀上一節,你應該已經清楚地知道理想的分片鍵應該能夠:

  1. 將插入數據均勻分佈到各個分片上;

  2. 保證CRUD操作能夠利用局部性;

  3. 有足夠的粒度進行塊拆分。

滿足這些要求的分片鍵通常由兩個字段組成,第一個是粗粒度的,第二個粒度較細。電子錶格示例的分片鍵就是一個不錯的例子,你聲明了一個復合分片鍵{username: 1, _id: 1}。當不同的用戶向集群插入數據時,可以預計到大多數(並非全部)情況下,一個用戶的電子錶格會在單個分片上。就算某個用戶的文檔落在多個分片上,分片鍵裡那個唯一的_id字段也能保證對任意一個文檔的查詢和更新始終能指向單個分片。如果需要對某個用戶的數據執行更複雜的查詢,可以保證查詢只會被路由到包含該用戶數據的那些分片上。

最重要的是分片鍵{username: 1, _id: 1}保證了塊始終是能繼續拆分的,哪怕用戶創建了大量文檔,情況也是如此。

再舉個例子,假設正在構建一個網站分析系統。正如將在附錄B裡看到的那樣,針對此類系統,一個不錯的數據模型是每個網頁每月保存一個文檔。隨後,在那個文檔內保存該月每天的數據,每次訪問某個頁面就增加一些計數器字段的值等。下面是與分片鍵選擇有關的示例分析文檔字段:

{ _id: ObjectId(\"4d750a90c35169d10fc8c982\"),
  domain: \"org.mongodb\",
  url: \"/downloads\",
  period: \"2011-12\"
}
  

針對包含此類文檔的分片集群,最簡單的分片鍵包含每個網頁的域名,隨後是URL:{domain: 1, url: 1}。所有來自指定域的頁面通常都能落在一個分片上,但是一些特殊的域擁有大量頁面,在必要時仍會被拆分到多個分片上。