利用MongoDB的SplitVector命令實現併發資料遷移

明儼發表於2017-07-07

背景

資料遷移是資料庫運維中一個很常見的場景。資料遷移分為全量和增量。為了追求速度,通常我們會採用併發的方式對資料進行全量遷移。在全量匯出資料時,通常都會選擇做到記錄級的併發,因此通常會涉及到對需要匯出的某個表(集合)按照併發度進行切分(分割槽)的過程。現有常用做法是通過若干個skip加limit來找到一些分割槽點,然後就可以併發同時匯出多個分割槽。事實上MongoDB還有一個SplitVector命令特別適合用來做集合的分割槽。本文將介紹一下如何利用這個命令來對集合做分割槽,實現併發資料遷移。

命令簡介

SplitVector命令原是在sharding中chunk分裂時需要用的一個內部命令,是mongos在準備分裂某個chunk前發給這個chunk所在shard以計算分裂點(SplitPoint)時使用的。但是這個命令也可以用於普通的副本集,我們可以把副本集中的集合看作一個唯一的chunk,利用這個命令來為這個chunk計算分裂點,從而達到為某個集合進行分割槽的目的。

SplitVector命令的使用在官方文件中沒有介紹,只說明瞭其實一個內部命令,但是使用命令的Help卻可以看到:

db.runCommand({splitVector:"test.test", help:1})
{
 "help" : "help for: splitVector Internal command.
examples:
  { splitVector : "blog.post" , keyPattern:{x:1} , min:{x:10} , max:{x:20}, maxChunkSize:200 }
  maxChunkSize unit in MBs
  May optionally specify `maxSplitPoints` and `maxChunkObjects` to avoid traversing the whole chunk
  
  { splitVector : "blog.post" , keyPattern:{x:1} , min:{x:10} , max:{x:20}, force: true }
  `force` will produce one split point even if data is small; defaults to false
NOTE: This command may take a while to run",
 "lockType" : 0,
 "ok" : 1
}

從幫助文件中可以大致看到,這個命令大致是這麼使用的:

db.runCommand({splitVector:"blog.post", keyPattern:{x:1}, min{x:10}, max:{x:20}, maxChunkSize:200})

接下來介紹一下各個引數及其含義。

欄位 型別 描述
splitVector string splitVector的操作物件集合名
keyPattern document chunk分裂使用的分割槽鍵,必須擁有索引,在sharding中就是shard key,在副本集中通常就指定成主鍵_id索引
min document 可選引數,分割槽資料集的最小值,如果沒有指定,那麼使用MinKey
max document 可選引數,分割槽資料集的最大值,如果沒有指定,那麼使用MaxKey
maxChunkSize integer 可選引數,和『force』引數二者必須指定一個。分割槽後每個chunk的最大大小
maxSplitPoints integer 可選引數,分裂點個數上限
maxChunkObjects integer 可選引數,分割槽後每個chunk最大包含的記錄數,預設為250000
force boolean 可選引數,和『maxChunkSize』引數二者必須指定一個。預設情況下如果當前chunk的資料大小小於maxChunkSize則不會進行分裂。如果指定了『force』為true,那麼會強制在當前chunk的中位點進行分裂,返回一個分裂點。預設為false。

這麼多引數到底怎麼用呢?我怎麼知道出來的結果是怎樣的?沒有更詳細的文件,只有啃原始碼了。​

原理

SplitVector的原理是遍歷指定的『keyPattern』索引,根據指定的『maxChunkSize』找到滿足以下條件的n個分裂點:分裂後的每個新的chunk的大小約為『maxChunkSize』的一半。如果集合當前大小比『maxChunkSize』小或者集合記錄數為空,那麼返回一個空的分裂點集合。如果指定了『force: true』,那麼會忽略傳入的『maxChunkSize』引數,並強制在集合的中位點進行分片,這時候只會產生一個分裂點。
在尋找分裂點時首先會根據集合的平均文件大小計算一個分裂後每個chunk所包含的文件數:

​​keyCount = maxChunkSize / (2 * avgObjSize)

​​如果指定了『maxChunkObjects』引數,並且『maxChunkObjects』比keyCount小,會使用『maxChunkObjects』作為keyCount。接下來就是遍歷索引,每遍歷keyCount個key,就得到一個分裂點(第keyCount+1個key),直到達到『maxSplitPoints』(若有指定)或遍歷結束。因此最終得到的分裂點個數:

​​splitPointCount = keyTotalCount / (keyCount + 1)

​​其中keyTotalCount為索引的key總數。

使用案例

知道了原理後,就知道如何去傳引數了,如果要精確控制得到的分裂點個數(以便控制併發數),這裡可以給出一個公式及推導過程。現在我們有以下公式:

​1. splitPointCount = partitionCount - 1
2. splitPointCount = keyTotalCount / (keyCount + 1)
3. keyCount = maxChunkSize / (2 * avgObjSize)

由上述公式可以推匯出

maxChunkSize = (keyTotalCount / (partionCount - 1) - 1) * 2 * avgObjSize

由於所有集合都有_id欄位上的唯一索引,並且每個文件都有_id欄位,因此我們可以直接利用集合文件的個數docCount作為索引key的個數。文件個數和avgObjSize都可以通過collStats命令得到。注意引數中的『maxChunkSize』是以MB為單位的,最終傳到命令的時候需要轉換一下,並且在服務端中事實上會將『maxChunkSize』做個向下取整,因此最終計算出來的keyCount可能比我們設想的要小,這樣就會導致最終得到的分裂點個數比我們想要的多。為了達到我們的需求,最好加上『maxSplitPoints』這個可選引數對分裂點進行限制,這樣我們允許最後一個分割槽比其他分割槽包含更多的文件數。

接下來舉個具體的例子,假設現在需要將某個集合分成10個分割槽以支援10個併發同時對外匯出資料,這個集合共有10240條文件,avgObjSize是1024,那麼根據上述公式可以計算得到:

​maxChunkSize = (10240 / (10 - 1) - 1) * 2 * 1024 = 2MB

這樣我們執行如下命令:

db.runCommand({splitVector:"test.test", keyPattern:{_id:1}, maxChunkSize:2, maxSplitPoints:9})

​這樣就會只得到9個分裂點。


相關文章