2015年12月21日

Sharding in MongoDB

Sharding 是資料庫的一種水平擴充的技術,中文稱為「分片」,就是將資料庫分區塊放在不同的資料庫節點上。大量的針對單一個 database daemon 進行 query,會消耗掉很多 CPU 的運算 loading。大量的 data sets 也可能會超過單一台機器所能提供的硬碟容量。

vertical/horizontal scaling

有兩種基本的方式可 scale up Database: vertical scaling and sharding。

  • Vertical scaling 為機器增加 CPU 以及 Storage,但這種方式雖然簡便還是有極限,不可能無止境的將一台機器的 CPU, Storage 一直往上增加。
  • Horizontal scaling: Sharding 將資料分散到多個 server (shards) 上,每一個 shard 都有獨立的資料庫。

    當 sharding cluster 增加 shards,將會減少每一個 shard 處理的 operations 數量,最終可增加整個 cluster 的容量及 throughput。同時也會減少每一個 shard 所要儲存的資料量。

shard, router, config server

MongoDB 的 Sharding 機制中有三個角色

  • Shards 儲存分片後的部份資料,可能是一個獨立的 server 或是 replica set。在正式環境中,最好都是使用 repica sets。
  • Query Routers 每個 router 都是一個 mongos instance,可接受來自 application 的 read/write,並轉向到 shards,application 並不會直接存取 shards。
  • Config Servers 每一個 config server 都是一個 mongod instance,儲存這個 cluster 相關的 metadata,metadata中記錄 chunks 對應到 shards 的資訊。分片後的資料稱為 chunk,每個 chunk 都是 collection 中一段連續的資料記錄,通常最大是 200MB。正式環境必須恰好要有三個 config servers。

Sharded cluster

當系統遇到以下的問題時,就必須要採用 sharded cluster

  1. data set 接近或超過單一 MongoDB instance 所能儲存的容量
  2. 系統 working set (MongoDB 最常使用的資料,通常會儲存在 RAM 或 SSD裡面) 即將超過系統的 RAM 容量上限
  3. 單一 MongoDB instance 無法滿足大量 write operaions 需求時

Shard Key

為了要建置 collection 的 sharding,必須要有一個 shard key,在 collection 中要選擇一個 indexed field 或是 indexed compund filed 作為 shard key。MongoDB 會以 range/hash based partitioning 的方式將shard key values 切割為 chunks。

  • Range Based Sharding 現在如果有一個 numeric shard key,畫出一條由負無限大到正無限大的數線。shard key 上每一個 key 的數值都會落在數線上某一點,MongoDB會將這條線切割成幾個不會互相重疊的線段,也就是 chunks。兩個數值很接近的 shard key,會放在同一個 chunk 中。

  • Hash Based Sharding 計算 shard key 的 hash value,使用這個 hash 建立 chunks,兩個很數值很接近的 shard key,不一定會區分在同一個 chunk 中。

  • Tag Aware Sharding MongoDB 另外允許管理者使用 tag aware sharding 的機制,管理者可根據 shard key 的區間建立 tags,然後指定哪些 tags 放在那個 shards。這種機制非常適合存放有地區特性的資料,也就是可以選擇最接近的 data center。

如果是對 shard key 的 range queries,range based partitioning 效能會比較好,因為查詢的資料大部分都在同一個 chunk 裡面。

但是 range based partitioning 可能會造成資料不平均的狀況。舉例來說,如果是跟著時間線性增加的欄位,針對 time range 進行的查詢,就會落在同一個 chunk 中。但也因此會造成某一個 chunk 會收到較多 request,所以整個系統的 scalibility 表現會差一些。

hashed based partitioning 會確保資料會平均散布在 chunks 之間,因此 range query 就會動用到所有的 chunks。

Balanced Data Distribution

持續增加的資料會造成資料不平均的散布狀況,也就是某一個 shard 存放了比較多的 chunks。MongoDB 使用兩個背景的 process: splitting, balancer 來確保整個 cluster 能保持 balanced。

  • Splitting splitting 可避免 chunks 增加地太大,當某一個 chunk 超過 MongoDB 指定的 chunk size 時,就會將 chunk 分成兩塊。

    在處理 splits 時,MongoDB 將不會移動任何資料,也不會修改 shards。

  • Balancer balancer 是處理 chunk migration 的背景 process,balancer 可由 cluster 中任何一個 query router 執行。

    當 cluter 裡面的 sharded collection 資料散布不平均時,balancer 會在 shard 之間移動 chunks,通常就是從有最多個 chunks 的 shard 移動到最少的 shard。

    在 chunks migration 期間,來源 shard 的文件會傳送到目標的 shard,接下來會處理移動期間對資料的所有異動 operations,最後再更新 config server 上關於這個 chunk 位置的 metadata。

    如果在 migration 期間發生任何錯誤,balancer 會放棄已經更新的資料,並保留原始 chunks 的資料,MongoDB 只會在 migration process 處理完成時,才會將來源 chunk 裡面的資料刪除。

由於新增或移除 shards 都會造成 imbalance,因此在新增 shard 之後,MongoDB 就會馬上進行 data migration,會需要花一些時間,才能達到 cluster balanced。移除 shard 時,balancer 會先將這個 shard 的所有 chunks 移動到其他 shard,在處理完成之後,才能正確地移除 shard。

測試 Sharding

首先啟動兩個 shard server,分別使用 Port 20000 及 20001

mkdir -p /home/mongodb/shard/data/s1
mkdir -p /home/mongodb/shard/data/s2
mkdir -p /home/mongodb/shard/logs

/usr/share/mongodb/bin/mongod --port 20000 --fork --dbpath /home/mongodb/shard/data/s1 --logpath /home/mongodb/shard/logs/s1.log --logappend --shardsvr --directoryperdb

/usr/share/mongodb/bin/mongod --port 20001 --fork --dbpath /home/mongodb/shard/data/s2 --logpath /home/mongodb/shard/logs/s2.log --logappend --shardsvr --directoryperdb

然後在 Port 30000 啟動 Config Server

mkdir -p /home/mongodb/shard/data/config
/usr/share/mongodb/bin/mongod --configsvr --fork --port 30000 --dbpath /home/mongodb/shard/data/config --logpath /home/mongodb/shard/logs/config.log --logappend

在 Port 40000 啟動 Router

/usr/share/mongodb/bin/mongos --port 40000 --configdb localhost:30000 --fork  --logpath /home/mongodb/shard/logs/route.log --logappend --chunkSize 1

chunkSize 是 sharded cluster 每一個 chunk 的大小,單位是 MegaBytes,預設值為 64,這個參數只會在一開始初始化 cluster 時,設定後才有作用,如果要修改一個既有的 Sharded Cluster 的 Chunk Size 要參考 Modify Chunk Size in a Sharded Cluster

以 mongo client 連線到 Router

mongo admin --port 40000

增加兩個 shard server

db.runCommand({addshard:"localhost:20000"});
db.runCommand({addshard:"localhost:20001"});

讓資料庫 test 以 sharding 方式儲存

db.runCommand({enablesharding:"test"});

設定 test.users 以 _id 作為 sharded key,進行 sharding

db.runCommand({shardcollection:"test.users", key:{_id:1}});

直接寫入 500000 筆測試資料

use test

for(var i=1;i<500000; i++) {
    db.users.insert({
        userid:"user_"+i,
        username:"name_"+i,
        age: NumberInt(_rand()*100)
    })
}

以 db.usrs.stats() 查看 sharding 的結果

mongos> db.users.stats();
{
    "sharded" : true,   // 有分片
    "paddingFactorNote" : "paddingFactor is unused and unmaintained in 3.0. It remains hard coded to 1.0 for compatibility only.",
    "userFlags" : 1,
    "capped" : false,
    "ns" : "test.users",
    "count" : 499999,   // 499999 筆資料
    "numExtents" : 16,
    "size" : 55999888,
    "storageSize" : 75595776,
    "totalIndexSize" : 16278416,
    "indexSizes" : {
        "_id_" : 16278416
    },
    "avgObjSize" : 112,
    "nindexes" : 1,
    "nchunks" : 95,     // 共有 95個 chunks
    "shards" : {
        "shard0000" : {
            "ns" : "test.users",
            "count" : 246899,   // 第一個 shard 儲存了 246899 筆資料
            "size" : 27652688,
            "avgObjSize" : 112,
            "numExtents" : 8,
            "storageSize" : 37797888,
            "lastExtentSize" : 15290368,
            "paddingFactor" : 1,
            "paddingFactorNote" : "paddingFactor is unused and unmaintained in 3.0. It remains hard coded to 1.0 for compatibility only.",
            "userFlags" : 1,
            "capped" : false,
            "nindexes" : 1,
            "totalIndexSize" : 8028832,
            "indexSizes" : {
                "_id_" : 8028832
            },
            "ok" : 1
        },
        "shard0001" : {
            "ns" : "test.users",
            "count" : 253100,   // 第二個 shard 儲存了 253100 筆資料
            "size" : 28347200,
            "avgObjSize" : 112,
            "numExtents" : 8,
            "storageSize" : 37797888,
            "lastExtentSize" : 15290368,
            "paddingFactor" : 1,
            "paddingFactorNote" : "paddingFactor is unused and unmaintained in 3.0. It remains hard coded to 1.0 for compatibility only.",
            "userFlags" : 1,
            "capped" : false,
            "nindexes" : 1,
            "totalIndexSize" : 8249584,
            "indexSizes" : {
                "_id_" : 8249584
            },
            "ok" : 1
        }
    },
    "ok" : 1
}

查詢一下,就會發現 users 不會按照 insert 的順序回傳回來

mongos> db.users.find();
{ "_id" : ObjectId("56457eb55b043e713fa43441"), "userid" : "user_10", "username" : "name_10", "age" : 33 }
{ "_id" : ObjectId("56457eb55b043e713fa43438"), "userid" : "user_1", "username" : "name_1", "age" : 0 }
{ "_id" : ObjectId("56457eb65b043e713fa43442"), "userid" : "user_11", "username" : "name_11", "age" : 25 }
{ "_id" : ObjectId("56457eb55b043e713fa43439"), "userid" : "user_2", "username" : "name_2", "age" : 79 }
{ "_id" : ObjectId("56457eb65b043e713fa43443"), "userid" : "user_12", "username" : "name_12", "age" : 55 }
{ "_id" : ObjectId("56457eb55b043e713fa4343a"), "userid" : "user_3", "username" : "name_3", "age" : 83 }
{ "_id" : ObjectId("56457eb65b043e713fa43444"), "userid" : "user_13", "username" : "name_13", "age" : 24 }
{ "_id" : ObjectId("56457eb55b043e713fa4343b"), "userid" : "user_4", "username" : "name_4", "age" : 10 }
{ "_id" : ObjectId("56457eb65b043e713fa43445"), "userid" : "user_14", "username" : "name_14", "age" : 33 }
{ "_id" : ObjectId("56457eb55b043e713fa4343c"), "userid" : "user_5", "username" : "name_5", "age" : 76 }
{ "_id" : ObjectId("56457eb65b043e713fa43446"), "userid" : "user_15", "username" : "name_15", "age" : 54 }
{ "_id" : ObjectId("56457eb55b043e713fa4343d"), "userid" : "user_6", "username" : "name_6", "age" : 63 }
{ "_id" : ObjectId("56457eb65b043e713fa43447"), "userid" : "user_16", "username" : "name_16", "age" : 27 }
{ "_id" : ObjectId("56457eb55b043e713fa4343e"), "userid" : "user_7", "username" : "name_7", "age" : 42 }
{ "_id" : ObjectId("56457eb65b043e713fa43448"), "userid" : "user_17", "username" : "name_17", "age" : 78 }
{ "_id" : ObjectId("56457eb55b043e713fa4343f"), "userid" : "user_8", "username" : "name_8", "age" : 99 }
{ "_id" : ObjectId("56457eb65b043e713fa43449"), "userid" : "user_18", "username" : "name_18", "age" : 34 }
{ "_id" : ObjectId("56457eb55b043e713fa43440"), "userid" : "user_9", "username" : "name_9", "age" : 67 }
{ "_id" : ObjectId("56457eb65b043e713fa4344a"), "userid" : "user_19", "username" : "name_19", "age" : 25 }
{ "_id" : ObjectId("56457eba5b043e713fa45ff1"), "userid" : "user_11194", "username" : "name_11194", "age" : 64 }
Type "it" for more

mongos> db.users.find().sort({userid:1}).limit(5);
{ "_id" : ObjectId("56457eb55b043e713fa43438"), "userid" : "user_1", "username" : "name_1", "age" : 0 }
{ "_id" : ObjectId("56457eb55b043e713fa43441"), "userid" : "user_10", "username" : "name_10", "age" : 33 }
{ "_id" : ObjectId("56457eb65b043e713fa4349b"), "userid" : "user_100", "username" : "name_100", "age" : 50 }
{ "_id" : ObjectId("56457eb75b043e713fa4381f"), "userid" : "user_1000", "username" : "name_1000", "age" : 67 }
{ "_id" : ObjectId("56457eba5b043e713fa45b47"), "userid" : "user_10000", "username" : "name_10000", "age" : 69 }

Sharding 的維護

列出所有 shard servers

mongos> use admin;
switched to db admin
mongos> db.runCommand({listshards:1});
{
    "shards" : [
        {
            "_id" : "shard0000",
            "host" : "localhost:20000"
        },
        {
            "_id" : "shard0001",
            "host" : "localhost:20001"
        }
    ],
    "ok" : 1
}

列印 sharding 的狀態

mongos> printShardingStatus();
--- Sharding Status --- 
  sharding version: {
    "_id" : 1,
    "minCompatibleVersion" : 5,
    "currentVersion" : 6,
    "clusterId" : ObjectId("56457d8ca361603fe6aa49ce")
}
  shards:
    {  "_id" : "shard0000",  "host" : "localhost:20000" }
    {  "_id" : "shard0001",  "host" : "localhost:20001" }
  balancer:
    Currently enabled:  yes
    Currently running:  no
    Failed balancer rounds in last 5 attempts:  0
    Migration Results for the last 24 hours: 
        47 : Success
  databases:
    {  "_id" : "admin",  "partitioned" : false,  "primary" : "config" }
    {  "_id" : "test",  "partitioned" : true,  "primary" : "shard0000" }
        test.users
            shard key: { "_id" : 1 }
            chunks:
                shard0000   48
                shard0001   47
            too many chunks to print, use verbose if you want to force print

判斷是否是 sharding cluster mongos> db.runCommand({isdbgrid:1}) { "isdbgrid" : 1, "hostname" : "server", "ok" : 1 }

新增或移除 shard server

首先在另一個 console 產生第三個 shard server mkdir -p /home/mongodb/shard/data/s3 /usr/share/mongodb/bin/mongod --port 20002 --fork --dbpath /home/mongodb/shard/data/s3 --logpath /home/mongodb/shard/logs/s3.log --logappend --shardsvr --directoryperdb

回到 mongo route server 的 client

mongo admin --port 40000

將第三個 shard server 增加到 cluster

db.runCommand({addshard:"localhost:20002"})

printShardingStatus() 查看 sharding 的狀態,可以發現現在 MongoDB 已經開始進行 balancer 的處理

mongos> printShardingStatus();
--- Sharding Status --- 
  sharding version: {
    "_id" : 1,
    "minCompatibleVersion" : 5,
    "currentVersion" : 6,
    "clusterId" : ObjectId("56457d8ca361603fe6aa49ce")
}
  shards:
    {  "_id" : "shard0000",  "host" : "localhost:20000" }
    {  "_id" : "shard0001",  "host" : "localhost:20001" }
    {  "_id" : "shard0002",  "host" : "localhost:20002" }
  balancer:
    Currently enabled:  yes
    Currently running:  yes
        Balancer lock taken at Fri Nov 13 2015 14:47:40 GMT+0800 (CST) by kokola:40000:1447394699:1804289383:Balancer:1681692777
    Collections with active migrations: 
        test.users started at Fri Nov 13 2015 14:47:40 GMT+0800 (CST)
    Failed balancer rounds in last 5 attempts:  0
    Migration Results for the last 24 hours: 
        55 : Success
  databases:
    {  "_id" : "admin",  "partitioned" : false,  "primary" : "config" }
    {  "_id" : "test",  "partitioned" : true,  "primary" : "shard0000" }
        test.users
            shard key: { "_id" : 1 }
            chunks:
                shard0000   43
                shard0001   44
                shard0002   8
            too many chunks to print, use verbose if you want to force print

最後完成時,三個 shard server 分別儲存了 32, 32, 31 個 chunks

mongos> printShardingStatus();
--- Sharding Status --- 
  sharding version: {
    "_id" : 1,
    "minCompatibleVersion" : 5,
    "currentVersion" : 6,
    "clusterId" : ObjectId("56457d8ca361603fe6aa49ce")
}
  shards:
    {  "_id" : "shard0000",  "host" : "localhost:20000" }
    {  "_id" : "shard0001",  "host" : "localhost:20001" }
    {  "_id" : "shard0002",  "host" : "localhost:20002" }
  balancer:
    Currently enabled:  yes
    Currently running:  no
    Failed balancer rounds in last 5 attempts:  0
    Migration Results for the last 24 hours: 
        78 : Success
  databases:
    {  "_id" : "admin",  "partitioned" : false,  "primary" : "config" }
    {  "_id" : "test",  "partitioned" : true,  "primary" : "shard0000" }
        test.users
            shard key: { "_id" : 1 }
            chunks:
                shard0000   32
                shard0001   32
                shard0002   31
            too many chunks to print, use verbose if you want to force print

如果用 db.users.stats() 也會看到現在的資料已經分散到三個 shard servers

use test;
db.user.stats();

要移除第三個 shard server,必須持續執行這個指令

db.runCommand({removeshard:"localhost:20002"})

我們可以發現 chunks 慢慢變少,直到最後產生 error

mongos> db.runCommand({removeshard:"localhost:20002"})
{
    "msg" : "draining started successfully",
    "state" : "started",
    "shard" : "shard0002",
    "ok" : 1
}
mongos> db.runCommand({removeshard:"localhost:20002"})
{
    "msg" : "draining ongoing",
    "state" : "ongoing",
    "remaining" : {
        "chunks" : NumberLong(29),
        "dbs" : NumberLong(0)
    },
    "ok" : 1
}
mongos> db.runCommand({removeshard:"localhost:20002"})
{
    "msg" : "draining ongoing",
    "state" : "ongoing",
    "remaining" : {
        "chunks" : NumberLong(2),
        "dbs" : NumberLong(0)
    },
    "ok" : 1
}
mongos> db.runCommand({removeshard:"localhost:20002"})
{
    "msg" : "removeshard completed successfully",
    "state" : "completed",
    "shard" : "shard0002",
    "ok" : 1
}
mongos> db.runCommand({removeshard:"localhost:20002"})
{
    "code" : 13129,
    "ok" : 0,
    "errmsg" : "exception: can't find shard for: localhost:20002"
}

printShardingStatus() 的結果,會看到又變回兩個 shard servers 的狀態。

mongos> printShardingStatus();
--- Sharding Status --- 
  sharding version: {
    "_id" : 1,
    "minCompatibleVersion" : 5,
    "currentVersion" : 6,
    "clusterId" : ObjectId("56457d8ca361603fe6aa49ce")
}
  shards:
    {  "_id" : "shard0000",  "host" : "localhost:20000" }
    {  "_id" : "shard0001",  "host" : "localhost:20001" }
  balancer:
    Currently enabled:  yes
    Currently running:  no
    Failed balancer rounds in last 5 attempts:  0
    Migration Results for the last 24 hours: 
        109 : Success
  databases:
    {  "_id" : "admin",  "partitioned" : false,  "primary" : "config" }
    {  "_id" : "test",  "partitioned" : true,  "primary" : "shard0000" }
        test.users
            shard key: { "_id" : 1 }
            chunks:
                shard0000   48
                shard0001   47
            too many chunks to print, use verbose if you want to force print