2015/12/28

Production Cluster Architecture in MongoDB: Replica Sets + Sharding

為了建立一個完整的高可用性的 MongoDB 資料庫,我們必須要整合使用 Replica Sets 跟 Sharding。


  1. Shard Server: 2 個以上的 shard server,以 replica sets 建立 shard server,確保每一個節點都有備份,自動容錯及復原的能力
  2. Config Server: 設定 3 個 config server,一定要剛好 3個
  3. Route Server: 1 個以上的 query routers,通常可以在每一個 application server 都配置一個 mongos instance


MongoDB in Trend


在這篇文章中 趨勢科技導入MongoDB 追蹤管理全球10萬個行動裝置,MongoDB 的架構圖其實跟官方提出的架構概念是一致的。



如果是以 Application 的角度來看 MongoDB 的架構,概念上是接近以下這個架構圖。



3 physical servers


因為三個 config server 的基本要求,因此我們最低的硬體要求,是需要三台實體的 server。另外考量到整個 cluster 環境的完整的備援。
MongoDB的Replica Sets+Sharding架構 這篇文章提出了一個架構。



Production Note


MongoDB 官方在 Production Notes 提供了一些在正式環境應該要注意的事項。


  • Storage Engine
    有兩種 MMAPv1 及 WiredTiger,預設是使用 MMAPv1。 mongod 在啟動時,會檢查 dbPath 裡面既有的資料,是不是指定了不同的 storage engine。

  • OS
    Mac OS X, Linux, Windows Server 2012, Windows Server 2008 R2 64bit, Windows 7 (64 bit), Windows Vista, and Solaris 都可以用,但 Production 環境建議要使用 64bits OS。

  • Concurrency
    MMAPv1 提供 collection-level locking,所有的 collections 都有一個唯一的 read-write lock,允許多個 clients 同時在不同的 collections 裡面修改文件。


    WiredTiger 在一個 collection 裡面 readers 與 writers 同時存取文件的機制,clients 可在其他 write operations 進行過程中,還能讀取文件,多個 threads 可以同時修改在某一個 collection中不同的文件。

  • Data Consistency


    1. Journaling: MongoDB 採取了 write ahead logging to an on-disk journal 的機制,可在 mongod crash 時,還能處理尚未儲存到 data files 的資料。最好要保持 journaling 的功能開啟的狀態。

    2. Write Concern: 當 MongoDB 處理 write operation 時,可設定 guarantee 的強度,當以 weak write concern 執行 insert,update,delete 時,這些 operation 會快速完成,但風險是當發生錯誤時,就有可能會發生資料錯誤的狀況。

  • Networking


    1. Use Trusted Networking Environments
      MongoDB 預設沒有啟用 authorization 機制,MongoDB 是以 Role-Based Access Control 的方式,處理 client 端授權的問題。

    2. Disable HTTP Interfaces
      MongoDB 有提供透過 HTTP 查詢 server status 的機制,正式環境要把這個功能關掉。

    3. Manage Connection Pool Sizes
      避免在單一個 mongod 或 mongos,使用太多 connection resources,確保 client 只使用了合理的 connection pool sizes,通常設定為 concurrent db requests 總量的 110%~115% 就可以了。connPoolStats 指令可查詢 open connections 的資訊。

  • HW Considerations


    1. MongoDB 核心是使用 litte-endian 硬體,主要是 x86/x86_64 的 CPU,而 client 可以使用 big 或 little endian 系統

    2. 配置足夠的 RAM 及 CPU
      MMAPv1 不需要太多 CPU cores,增加 RAM 可減少 MongoDB 產生 page faults 的頻率。


      WiredTiger 需要很多 CPU cores 支援,active threads(concurrent operations) 跟 CPU 總數有關係。


      storage.wiredTiger.engineConfig.cacheSizeGB 會限制 cache 的上限。


      可透過 mongostat 指令查看 ar|aw 欄位,得知 active reads/writes 的數量。

    3. 使用 Solid State Disk (SSD)

    4. NUMA (Non-Uniform Access Memory) HW
      NUMA 是一種記憶體的實作方式,CPU 存取 local memory 的速度會比存取 shard memory 還快。MongoDB 在 NUMA 環境中使用,會遇到很多問題,可能會越來越慢。

  • Disk and Storage System


    • Swap


    1. 為系統設定 swap,當記憶體不夠用時,就不會發生OOM(Out of Memoty) killer 把 mongod 刪除的問題
    2. 因為 MMAPv1 是以 map files to memory 的方式,確保 OS 不會將 MongoDB 資料存在 swap space 裡面。


    • Raid


    1. 大多數的 MongoDB deployments 應該使用 Raid-10
    2. Raid-5 與 Raid-6 的效能表現不是很好
    3. 不要用 Raid-0,Raid-0 的 write operation 表現好,但 read operation 就比較差,在 Amazon EBS volumes 上會發生這個問題。


    • Remote File System


    1. MMAPv1 不建議使用 NFS(Network File System),data 與 journal 都可能會有效能問題,建議將 jornal journal 放在 local or iscsi 硬碟會比較快。

    2. 如果真的要使用 NFS,建議在 /etc/fstab 檔案中加上 bg, nolock, noatime 這些設定。


    • Separate Components onto Different Storage Devices


    1. 為了改善效能,可考慮將 data, journal, log 分開放在不同的 stoage devices上。
    2. WiredTiger 可將 indexes 放在不同的 storage device 上。
  • Architecture


    • Replica Sets

    • Shared Clusters

  • Compression
    WiredTiger 可使用 snappy 或 zlib 壓縮 data,snappy 壓縮比例較低,但效能耗費較少,zlib 壓縮比例高,但效能耗費也高。


    WiredTiger 預設使用 snappy,可修改設定值 storage.wiredTiger.collectionConfig.blockCompressor

  • Platform Specific Considerations
    MongoDB 使用 GNU C Library(glibc),建議使用 glibc-2.12-1.2.el6 以上的版本

  • Kernel and File System
    建議使用 kernel 2.6.36 以上的 kernel


    MMAPv1 中,MongoDB 會 preallocate db files,產生比較大的 data files,建議使用 XFS 與 EXT4 file system。


    WiredTiger 強烈建議要使用 XFS,避免效能問題。


    XFS file system 至少使用 linux kernal 2.6.25 以上的版本。
    EXT4 file system 至少使用 linux kernal 2.6.23 以上的版本。


    以下是一些 Linux Distribution建議的 files sytem 及 kernel 版本


Linux Distribution Filesystem Kernel Version
CentOS 5.5 ext4, xfs 2.6.18-194.el5
CentOS 5.6 ext4, xfs 2.6.18-3.0.el5
CentOS 5.8 ext4, xfs 2.6.18-308.8.2.el5
CentOS 6.1 ext4, xfs 2.6.32-131.0.15.el6.x86_64
RHEL 5.6 ext4 2.6.18-3.0
RHEL 6.0 xfs 2.6.32-71
Ubuntu 10.04.4 LTS ext4, xfs 2.6.32-38-server
Amazon Linux AMI release 2012.03 ext4 3.2.12-3.2.4.amzn1.x86_64

  • fsync() on Directories
    MongoDB 需要 file system 支援 fsync on directories 的功能,HGFS 與 Virtual Box 並不支援這個功能。

  • Recommended Configuration


    1. 在儲存 data files 的硬碟,要關掉 atime
    2. 設定 file descriptor limit (-n) 以及 user process limit(ulimit) 超過 20000
    3. disable Transparent Huge Pages,MongoDB 在 4096 bytes virtual memory pages 的效能較好


      echo never > /sys/kernel/mm/redhat_transparent_hugepage/defrag
      echo never > /sys/kernel/mm/redhat_transparent_hugepage/enabled
      echo never > /sys/kernel/mm/transparent_hugepage/enabled
      echo never > /sys/kernel/mm/transparent_hugepage/defrag
    4. disable NUMA in BIOS

    5. 設定 SELinux


    在 MMAPv1
    1. 檢查針對 block device 的 readahead 設定。一般設定為 32(16kb) 就可以了


    注意:所有 MongoDB 系統都要用 NTP 同步時間。

  • Linux System
    mongod 及 mongos 在他們自己的 socket 裡面限制 TCP keep alive setting 最大值為 300 seconds。


    查詢設定


    sysctl net.ipv4.tcp_keepalive_time
    cat /proc/sys/net/ipv4/tcp_keepalive_time

    修改設定


    sudo sysctl -w net.ipv4.tcp_keepalive_time=<value>
    echo <value> | sudo tee /proc/sys/net/ipv4/tcp_keepalive_time

    永久修改設定


    vi /etc/sysctl.conf
    net.ipv4.tcp_keepalive_time = <value>

noPadding


mysql遷移到mongodb shared架構的過程中踩到的一個坑
MongoDB 2.x 在儲存資料時,會自動預留一些空間,所以原本在 mysql 259GB 的資料,移到 MongoDB 變成了 1197.416 GB。這個問題在 MongDB 3.x 採用了 WiredTiger 以後,已經有改善了。


就算是使用預設的 MMAPv1 Storage Engine 在 3.0 以後已經可以在建立 collection 時,就加上 noPadding 選項設定,避免 MongoDB 用掉太多預留空間。

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

2015/12/14

Replica Sets in MongoDB

Replication 的目的是提供資料 redundancy 並確保 availability,在不同的 DB server 保留數個資料副本,可避免因為單一機器失效而遺失資料。

有時我們可以利用 replication 增加資料讀取的效率,clients 可以讓不同的 server,分開處以讀取及寫入的 operations,我們也可以利用不同的 data centers,讓全球的使用者可連接最接近的 data center,以提高 AP 的效能。

mongd instances in Replication Sets

replication set 裡面 mongd daemon 主要有兩個角色: Primary 以及 Secondary,clients 主要跟 Primary node 溝通,所有的 Secondary nodes 會跟著 Primary 執行一樣的 operations,因此可以得到完全一樣的 data set。

每一個 replica set 只能有一個唯一的 Primary node,為了支援 replication,Promary node 必須在 oplog 裡面保留所有資料異動的記錄。

Primary 是唯一可以同時接受 Read/Write 的節點,雖然 Primary 與 Secondary 都可以接受 Read,但預設只有 Primary 可以接受 Read。


Secondary 會複製 Primary 的 oplog,並在自己的 data set 裡面再執行一次。


如果 Primary 超過 10s 無法跟其他節點溝通時,replica set 將會選擇一個 Secondary 作為下一任的 Primary。第一個收到多數投票的,就成為下一個 Primary。


Secondary 可設定作為其他的特殊用途,例如可以設定是否可以參與仲裁投票,或是將 priority 設定為 0,也就是單純只備份資料,不會變成 Primary node。

replica set 裡面有第三個角色:Arbiter,Arbiter 並不會儲存資料的備份,主要的功能是當有偶數個 Secondary 需要在其中選擇一個變成 Primary 的時候,就需要 Arbiter 出面進行投票仲裁。

一個 replica set 裡面最多可以有 12 個成員,但有投票權的最多只有有 7 個。一個 replica set 的最低成員要求為:一個 Primary、一個 Secondary、一個 Arbiter。但通常都是 deploy 一個 Primary加上兩個 Secondary。


我們可以增加獨立的 mongod instance 擔任 replica set 的 Arbiter,Arbiter 不需要儲存資料,只需要維護一個 replica set 的 quorum,並遴選出下一個 Primary node。Arbiter 並不需要獨立的硬體機器才能運作。

Secondary node 複製資料的方式,是採用 asynchronous replication,換句話說,如果固定由 Secondary 讀取資料,有可能會發生讀取到舊資料的狀況,

當我們需要跨越 data center 部屬 replica sets 的時候,以提高 data redundancy 時,參考Geographically Distributed Replica Sets 的作法:

  1. 在 main data center 建立一個 Primary,設定 priority 為 1
  2. 在 main data center 建立一個 Secondary,設定 priority 為 1
  3. 在 second data center 建立一個 Secondary,設定 priority 為 0

因為 second data center 的 Secondary node 的優先權為 0,所以不會變成下一任的 Primary node。當 main data center 失效時,


單機測試 replica sets

雖然實際上運作的環境必須要把 Primary, Secondary and Arbiter 放在不同的機器甚至是不同的 data centers。第一次測試 replica sets ,就先以同一台機器進行測試。

  • 建立資料及 log 儲存路徑

      mkdir -p /usr/share/mongodb/repl/data/r0
      mkdir -p /usr/share/mongodb/repl/data/r1
      mkdir -p /usr/share/mongodb/repl/data/r2
      mkdir -p /usr/share/mongodb/repl/logs
    
  • 建立 key files
    在 replica set 中用來互相驗證,這個部份也可以不設定,只要在啟動 mongod 時,不要加上 --keyFile 參數就好了,注意一定要將 key file 的檔案權限改為 600

      mkdir -p /usr/share/mongodb/repl/key
      echo "rs1 keyfile" > /usr/share/mongodb/repl/key/r0
      echo "rs1 keyfile" > /usr/share/mongodb/repl/key/r1
      echo "rs1 keyfile" > /usr/share/mongodb/repl/key/r2
      chmod 600 /usr/share/mongodb/repl/key/r*
    
  • 啟動三個 mongod daemon

在 cli 啟動三個 mongd

/usr/share/mongodb/bin/mongod --replSet rs1 --keyFile /usr/share/mongodb/repl/key/r0 --fork --port 27017 --dbpath /usr/share/mongodb/repl/data/r0 --logpath /usr/share/mongodb/repl/logs/r0.log --logappend

/usr/share/mongodb/bin/mongod --replSet rs1 --keyFile /usr/share/mongodb/repl/key/r1 --fork --port 28017 --dbpath /usr/share/mongodb/repl/data/r1 --logpath /usr/share/mongodb/repl/logs/r1.log --logappend

/usr/share/mongodb/bin/mongod --replSet rs1 --keyFile /usr/share/mongodb/repl/key/r2 --fork --port 29017 --dbpath /usr/share/mongodb/repl/data/r2 --logpath /usr/share/mongodb/repl/logs/r2.log --logappend

可以用 ps aux 查看 mongod daemons

# ps aux|grep mongod
root     13478  1.2  1.3 531552 50020 ?        Sl   14:24   0:02 /usr/share/mongodb/bin/mongod --replSet rs1 --keyFile /usr/share/mongodb/repl/key/r0 --fork --port 27017 --dbpath /usr/share/mongodb/repl/data/r0 --logpath /usr/share/mongodb/repl/logs/r0.log --logappend
root     13623  1.7  1.3 531556 52800 ?        Sl   14:25   0:02 /usr/share/mongodb/bin/mongod --replSet rs1 --keyFile /usr/share/mongodb/repl/key/r1 --fork --port 28017 --dbpath /usr/share/mongodb/repl/data/r1 --logpath /usr/share/mongodb/repl/logs/r1.log --logappend
root     13665  3.2  1.3 531552 53232 ?        Sl   14:27   0:02 /usr/share/mongodb/bin/mongod --replSet rs1 --keyFile /usr/share/mongodb/repl/key/r2 --fork --port 29017 --dbpath /usr/share/mongodb/repl/data/r2 --logpath /usr/share/mongodb/repl/logs/r2.log --logappend
  • 連接到 r0,並初始化 replica sets

以 mongo 連接到 r0 這個 node,然後透過 rs.initiate 進行 replica set 初始化,最後用 rs.stats() 查看狀態。

mongo -port 27017

config_rs1={
    _id: 'rs1',
    members: [
        {_id:0, host:'localhost:27017', priority:1},
        {_id:1, host:'localhost:28017'},
        {_id:2, host:'localhost:29017'}
    ]
};

rs.initiate(config_rs1);

rs.status();

rs.isMaster();

以下是測試的過程

# mongo -port 27017
MongoDB shell version: 3.0.7
connecting to: 127.0.0.1:27017/test
> config_rs1={
... _id: 'rs1',
... members: [
... {_id:0, host:'localhost:27017', priority:1},
... {_id:1, host:'localhost:28017'},
... {_id:2, host:'localhost:29017'}
... ]
... };
{
    "_id" : "rs1",
    "members" : [
        {
            "_id" : 0,
            "host" : "localhost:27017",
            "priority" : 1
        },
        {
            "_id" : 1,
            "host" : "localhost:28017"
        },
        {
            "_id" : 2,
            "host" : "localhost:29017"
        }
    ]
}
> rs.status();
{
    "info" : "run rs.initiate(...) if not yet done for the set",
    "ok" : 0,
    "errmsg" : "no replset config has been received",
    "code" : 94
}
> rs.initiate(config_rs1);
{ "ok" : 1 }
rs1:SECONDARY> rs.status();
{
    "set" : "rs1",
    "date" : ISODate("2015-11-12T06:29:01.774Z"),
    "myState" : 1,
    "members" : [
        {
            "_id" : 0,
            "name" : "localhost:27017",
            "health" : 1,
            "state" : 1,
            "stateStr" : "PRIMARY",
            "uptime" : 280,
            "optime" : Timestamp(1447309732, 1),
            "optimeDate" : ISODate("2015-11-12T06:28:52Z"),
            "electionTime" : Timestamp(1447309734, 1),
            "electionDate" : ISODate("2015-11-12T06:28:54Z"),
            "configVersion" : 1,
            "self" : true
        },
        {
            "_id" : 1,
            "name" : "localhost:28017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 9,
            "optime" : Timestamp(1447309732, 1),
            "optimeDate" : ISODate("2015-11-12T06:28:52Z"),
            "lastHeartbeat" : ISODate("2015-11-12T06:29:00.389Z"),
            "lastHeartbeatRecv" : ISODate("2015-11-12T06:29:00.533Z"),
            "pingMs" : 0,
            "configVersion" : 1
        },
        {
            "_id" : 2,
            "name" : "localhost:29017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 9,
            "optime" : Timestamp(1447309732, 1),
            "optimeDate" : ISODate("2015-11-12T06:28:52Z"),
            "lastHeartbeat" : ISODate("2015-11-12T06:29:00.425Z"),
            "lastHeartbeatRecv" : ISODate("2015-11-12T06:29:00.659Z"),
            "pingMs" : 0,
            "configVersion" : 1
        }
    ],
    "ok" : 1
}
rs1:PRIMARY> rs.isMaster();
{
    "setName" : "rs1",
    "setVersion" : 1,
    "ismaster" : true,
    "secondary" : false,
    "hosts" : [
        "localhost:27017",
        "localhost:28017",
        "localhost:29017"
    ],
    "primary" : "localhost:27017",
    "me" : "localhost:27017",
    "electionId" : ObjectId("564431a624f5e9c6e3c56242"),
    "maxBsonObjectSize" : 16777216,
    "maxMessageSizeBytes" : 48000000,
    "maxWriteBatchSize" : 1000,
    "localTime" : ISODate("2015-11-12T06:29:09.040Z"),
    "maxWireVersion" : 3,
    "minWireVersion" : 0,
    "ok" : 1
}

oplog

MongoDB 是透過 oplog 記錄資料異動過程的,replica sets 實際上是複製 oplog,oplog.rs 是一個固定長度的 Capped Collection,存放在 local 資料庫中,可透過 --oplogSize 調整這個資料庫的大小。

查閱 oplog 的資料,必須先切換到 admin 的身份。

首先建立一個 admin 的帳號。

mongo --port 27017

db.createUser({
    user: "admin",
    pwd: "pass",
    roles: [ { role: "root", db: "admin" } ]
});
exit;

然後再重新以 admin 登入到 mongod

mongo --port 27017 -u admin -p pass --authenticationDatabase admin

接下來就可以用以下的指令查閱 oplog

use local;

show collections;

db.oplog.rs.find();

db.printReplicationInfo();

db.printSlaveReplicationInfo();

db.system.replset.find();

測試過程如下

rs1:PRIMARY> use local;
switched to db local
rs1:PRIMARY> show collections;
me
oplog.rs
startup_log
system.indexes
system.replset

查閱 oplog.rs 裡面的資料

rs1:PRIMARY> db.oplog.rs.find();
{ "ts" : Timestamp(1447309732, 1), "h" : NumberLong(0), "v" : 2, "op" : "n", "ns" : "", "o" : { "msg" : "initiating set" } }
{ "ts" : Timestamp(1447310547, 1), "h" : NumberLong("-5005889511402504175"), "v" : 2, "op" : "c", "ns" : "admin.$cmd", "o" : { "create" : "system.version" } }
{ "ts" : Timestamp(1447310547, 2), "h" : NumberLong("7158333189499425797"), "v" : 2, "op" : "i", "ns" : "admin.system.version", "o" : { "_id" : "authSchema", "currentVersion" : 5 } }
{ "ts" : Timestamp(1447310547, 3), "h" : NumberLong("-1384756181233850446"), "v" : 2, "op" : "c", "ns" : "admin.$cmd", "o" : { "create" : "system.users" } }
{ "ts" : Timestamp(1447310547, 4), "h" : NumberLong("-8251644809692327494"), "v" : 2, "op" : "i", "ns" : "admin.system.users", "o" : { "_id" : "admin.admin", "user" : "admin", "db" : "admin", "credentials" : { "SCRAM-SHA-1" : { "iterationCount" : 10000, "salt" : "YfZzd27A323g5bWhk+TAcA==", "storedKey" : "27CwlfcUPxr92bB6yr+AoExiBmE=", "serverKey" : "SxiNruT9vznek+uSkM8xIKlK3a8=" } }, "roles" : [ { "role" : "root", "db" : "admin" } ] } }

查看 replication 的資訊,主要就是看 oplog 裡面的資訊

rs1:PRIMARY> db.printReplicationInfo();
configured oplog size:   990MB
log length start to end: 815secs (0.23hrs)
oplog first event time:  Thu Nov 12 2015 14:28:52 GMT+0800 (CST)
oplog last event time:   Thu Nov 12 2015 14:42:27 GMT+0800 (CST)
now:                     Thu Nov 12 2015 14:50:58 GMT+0800 (CST)

查看 slave 同步的狀態

rs1:PRIMARY> db.printSlaveReplicationInfo();
source: localhost:28017
    syncedTo: Thu Nov 12 2015 14:42:27 GMT+0800 (CST)
    0 secs (0 hrs) behind the primary 
source: localhost:29017
    syncedTo: Thu Nov 12 2015 14:42:27 GMT+0800 (CST)
    0 secs (0 hrs) behind the primary

local 資料庫中,還有另一個 system.replset 裡面有記錄 replica set 的資訊

rs1:PRIMARY> db.system.replset.find();
{ "_id" : "rs1", "version" : 1, "members" : [ { "_id" : 0, "host" : "localhost:27017", "arbiterOnly" : false, "buildIndexes" : true, "hidden" : false, "priority" : 1, "tags" : {  }, "slaveDelay" : 0, "votes" : 1 }, { "_id" : 1, "host" : "localhost:28017", "arbiterOnly" : false, "buildIndexes" : true, "hidden" : false, "priority" : 1, "tags" : {  }, "slaveDelay" : 0, "votes" : 1 }, { "_id" : 2, "host" : "localhost:29017", "arbiterOnly" : false, "buildIndexes" : true, "hidden" : false, "priority" : 1, "tags" : {  }, "slaveDelay" : 0, "votes" : 1 } ], "settings" : { "chainingAllowed" : true, "heartbeatTimeoutSecs" : 10, "getLastErrorModes" : {  }, "getLastErrorDefaults" : { "w" : 1, "wtimeout" : 0 } } }

讓 Secondary 也能讀取資料

Secondary 預設是不允許進行資料查詢的,因為非同步複製的關係,資料有可能在查詢時,還沒有複製過來。但如果能容忍這種非同步的錯誤,讓 Secondary 也能進行資料查詢,對系統來說,可達到讀寫分離,可以減輕 Primary 的 loading。

因為剛剛為了處理 oplog 加上了 user 權限,我們必須先建立一個帳號,可以讀寫 test 資料庫

mongo --port 27017 -u admin -p pass --authenticationDatabase admin

use test
db.createUser(
    {
      user: "test",
      pwd: "pass",
      roles: [
         { role: "readWrite", db: "test" }
      ]
    }
);

db.getUsers();

連接到 r0 (port 27017),然後 insert 一筆資料到 temp

mongo --port 27017 -u test -p pass --authenticationDatabase test

db.temp.insert({age:22});
db.temp.find();

連接到 Secondary,執行 show collections 指令時,會發生錯誤。

# mongo --port 28017 -u test -p pass --authenticationDatabase test
MongoDB shell version: 3.0.7
connecting to: 127.0.0.1:28017/test
rs1:SECONDARY> show collections
2015-11-12T15:31:34.394+0800 E QUERY    Error: listCollections failed: { "note" : "from execCommand", "ok" : 0, "errmsg" : "not master" }
    at Error (<anonymous>)
    at DB._getCollectionInfosCommand (src/mongo/shell/db.js:646:15)
    at DB.getCollectionInfos (src/mongo/shell/db.js:658:20)
    at DB.getCollectionNames (src/mongo/shell/db.js:669:17)
    at shellHelper.show (src/mongo/shell/utils.js:625:12)
    at shellHelper (src/mongo/shell/utils.js:524:36)
    at (shellhelp2):1:1 at src/mongo/shell/db.js:646

必須要先 db.getMongo().setSlaveOk(); 就可以從 Slave 讀取資料。

rs1:SECONDARY> db.getMongo().setSlaveOk();
rs1:SECONDARY> show collections;
system.indexes
temp
rs1:SECONDARY> db.temp.find();
{ "_id" : ObjectId("56443fedeabb66f27f42b08a"), "age" : 22 }

Primary node 轉移

replica sets 中發生故障節點的時候,會有自動轉移的程序。如果把 replica sets 中的 Primary 節點停止,剩下來的節點會自動選出下一個 Primary node。

首先查看目前的三個 mongod

# ps aux|grep mongod
root     13478  0.4  2.6 3366244 99916 ?       Sl   14:24   0:20 /usr/share/mongodb/bin/mongod --replSet rs1 --keyFile /usr/share/mongodb/repl/key/r0 --fork --port 27017 --dbpath /usr/share/mongodb/repl/data/r0 --logpath /usr/share/mongodb/repl/logs/r0.log --logappend
root     13623  0.4  2.4 3353924 95288 ?       Sl   14:25   0:19 /usr/share/mongodb/bin/mongod --replSet rs1 --keyFile /usr/share/mongodb/repl/key/r1 --fork --port 28017 --dbpath /usr/share/mongodb/repl/data/r1 --logpath /usr/share/mongodb/repl/logs/r1.log --logappend
root     13665  0.4  2.4 3352892 93024 ?       Sl   14:27   0:19 /usr/share/mongodb/bin/mongod --replSet rs1 --keyFile /usr/share/mongodb/repl/key/r2 --fork --port 29017 --dbpath /usr/share/mongodb/repl/data/r2 --logpath /usr/share/mongodb/repl/logs/r2.log --logappend

直接將 port 27017 的 process 砍掉

kill -9 13478

當我們連結到 r1 (port 28017),以 rs.status() 查看 replica sets 狀態時,會發現 r1 (port 28017) 已經自動成為 Primary node。

mongo --port 28017 -u admin -p pass --authenticationDatabase admin
rs1:SECONDARY> rs.status();
{
    "set" : "rs1",
    "date" : ISODate("2015-11-12T07:44:33.873Z"),
    "myState" : 1,
    "members" : [
        {
            "_id" : 0,
            "name" : "localhost:27017",
            "health" : 0,
            "state" : 8,
            "stateStr" : "(not reachable/healthy)",
            "uptime" : 0,
            "optime" : Timestamp(0, 0),
            "optimeDate" : ISODate("1970-01-01T00:00:00Z"),
            "lastHeartbeat" : ISODate("2015-11-12T07:44:33.798Z"),
            "lastHeartbeatRecv" : ISODate("2015-11-12T07:44:29.359Z"),
            "pingMs" : 0,
            "lastHeartbeatMessage" : "Failed attempt to connect to localhost:27017; couldn't connect to server localhost:27017 (127.0.0.1), connection attempt failed",
            "configVersion" : -1
        },
        {
            "_id" : 1,
            "name" : "localhost:28017",
            "health" : 1,
            "state" : 1,
            "stateStr" : "PRIMARY",
            "uptime" : 4734,
            "optime" : Timestamp(1447313389, 1),
            "optimeDate" : ISODate("2015-11-12T07:29:49Z"),
            "electionTime" : Timestamp(1447314272, 1),
            "electionDate" : ISODate("2015-11-12T07:44:32Z"),
            "configVersion" : 1,
            "self" : true
        },
        {
            "_id" : 2,
            "name" : "localhost:29017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 4541,
            "optime" : Timestamp(1447313389, 1),
            "optimeDate" : ISODate("2015-11-12T07:29:49Z"),
            "lastHeartbeat" : ISODate("2015-11-12T07:44:33.521Z"),
            "lastHeartbeatRecv" : ISODate("2015-11-12T07:44:33.713Z"),
            "pingMs" : 0,
            "configVersion" : 1
        }
    ],
    "ok" : 1
}

如果再把 r0 (port 27017) 執行起來

/usr/share/mongodb/bin/mongod --replSet rs1 --keyFile /usr/share/mongodb/repl/key/r0 --fork --port 27017 --dbpath /usr/share/mongodb/repl/data/r0 --logpath /usr/share/mongodb/repl/logs/r0.log --logappend

r1 (port 28017) 成為 Primary node 的身份還是不變,差別只是 r0 (port 27017) 的 health 由 0 變回 1,身份維持是 SECONDARY。

rs1:PRIMARY> rs.status();
{
    "set" : "rs1",
    "date" : ISODate("2015-11-12T07:49:30.493Z"),
    "myState" : 1,
    "members" : [
        {
            "_id" : 0,
            "name" : "localhost:27017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 46,
            "optime" : Timestamp(1447313389, 1),
            "optimeDate" : ISODate("2015-11-12T07:29:49Z"),
            "lastHeartbeat" : ISODate("2015-11-12T07:49:29.975Z"),
            "lastHeartbeatRecv" : ISODate("2015-11-12T07:49:30.440Z"),
            "pingMs" : 0,
            "configVersion" : 1
        },
        {
            "_id" : 1,
            "name" : "localhost:28017",
            "health" : 1,
            "state" : 1,
            "stateStr" : "PRIMARY",
            "uptime" : 5031,
            "optime" : Timestamp(1447313389, 1),
            "optimeDate" : ISODate("2015-11-12T07:29:49Z"),
            "electionTime" : Timestamp(1447314272, 1),
            "electionDate" : ISODate("2015-11-12T07:44:32Z"),
            "configVersion" : 1,
            "self" : true
        },
        {
            "_id" : 2,
            "name" : "localhost:29017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 4837,
            "optime" : Timestamp(1447313389, 1),
            "optimeDate" : ISODate("2015-11-12T07:29:49Z"),
            "lastHeartbeat" : ISODate("2015-11-12T07:49:29.722Z"),
            "lastHeartbeatRecv" : ISODate("2015-11-12T07:49:29.909Z"),
            "pingMs" : 0,
            "configVersion" : 1
        }
    ],
    "ok" : 1
}

replica sets 的節點維護

如果三台機器已經沒有辦法負荷壓力,可以透過增刪節點的方式,維護 replica sets。

首先產生兩個新的 mongod 節點 r3, r4。

mkdir -p /usr/share/mongodb/repl/data/r3
mkdir -p /usr/share/mongodb/repl/data/r4
echo "rs1 keyfile" > /usr/share/mongodb/repl/key/r3
echo "rs1 keyfile" > /usr/share/mongodb/repl/key/r4
chmod 600 /usr/share/mongodb/repl/key/r*

/usr/share/mongodb/bin/mongod --replSet rs1 --keyFile /usr/share/mongodb/repl/key/r3 --fork --port 30017 --dbpath /usr/share/mongodb/repl/data/r3 --logpath /usr/share/mongodb/repl/logs/r3.log --logappend

/usr/share/mongodb/bin/mongod --replSet rs1 --keyFile /usr/share/mongodb/repl/key/r4 --fork --port 31017 --dbpath /usr/share/mongodb/repl/data/r4 --logpath /usr/share/mongodb/repl/logs/r4.log --logappend

連接到 Primary node

mongo --port 28017 -u admin -p pass --authenticationDatabase admin

以指令 rs.add 新增節點

rs.add("localhost:30017");

以指令 rs.status(); 查看 rs 狀態

rs1:PRIMARY> rs.status();
{
    "set" : "rs1",
    "date" : ISODate("2015-11-12T08:55:57.447Z"),
    "myState" : 1,
    "members" : [
        {
            "_id" : 0,
            "name" : "localhost:27017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 4033,
            "optime" : Timestamp(1447313389, 1),
            "optimeDate" : ISODate("2015-11-12T07:29:49Z"),
            "lastHeartbeat" : ISODate("2015-11-12T08:55:56.100Z"),
            "lastHeartbeatRecv" : ISODate("2015-11-12T08:55:57.086Z"),
            "pingMs" : 0,
            "configVersion" : 1
        },
        {
            "_id" : 1,
            "name" : "localhost:28017",
            "health" : 1,
            "state" : 1,
            "stateStr" : "PRIMARY",
            "uptime" : 9018,
            "optime" : Timestamp(1447318556, 1),
            "optimeDate" : ISODate("2015-11-12T08:55:56Z"),
            "electionTime" : Timestamp(1447314272, 1),
            "electionDate" : ISODate("2015-11-12T07:44:32Z"),
            "configVersion" : 2,
            "self" : true
        },
        {
            "_id" : 2,
            "name" : "localhost:29017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 8824,
            "optime" : Timestamp(1447313389, 1),
            "optimeDate" : ISODate("2015-11-12T07:29:49Z"),
            "lastHeartbeat" : ISODate("2015-11-12T08:55:56.101Z"),
            "lastHeartbeatRecv" : ISODate("2015-11-12T08:55:56.550Z"),
            "pingMs" : 0,
            "configVersion" : 1
        },
        {
            "_id" : 3,
            "name" : "localhost:30017",
            "health" : 1,
            "state" : 0,
            "stateStr" : "STARTUP",
            "uptime" : 1,
            "optime" : Timestamp(0, 0),
            "optimeDate" : ISODate("1970-01-01T00:00:00Z"),
            "lastHeartbeat" : ISODate("2015-11-12T08:55:56.116Z"),
            "lastHeartbeatRecv" : ISODate("2015-11-12T08:55:56.201Z"),
            "pingMs" : 16,
            "configVersion" : -2
        }
    ],
    "ok" : 1
}

rs1:PRIMARY> rs.status();
{
    "set" : "rs1",
    "date" : ISODate("2015-11-12T08:55:58.609Z"),
    "myState" : 1,
    "members" : [
        {
            "_id" : 0,
            "name" : "localhost:27017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 4034,
            "optime" : Timestamp(1447318556, 1),
            "optimeDate" : ISODate("2015-11-12T08:55:56Z"),
            "lastHeartbeat" : ISODate("2015-11-12T08:55:58.100Z"),
            "lastHeartbeatRecv" : ISODate("2015-11-12T08:55:57.086Z"),
            "pingMs" : 0,
            "syncingTo" : "localhost:28017",
            "configVersion" : 2
        },
        {
            "_id" : 1,
            "name" : "localhost:28017",
            "health" : 1,
            "state" : 1,
            "stateStr" : "PRIMARY",
            "uptime" : 9019,
            "optime" : Timestamp(1447318556, 1),
            "optimeDate" : ISODate("2015-11-12T08:55:56Z"),
            "electionTime" : Timestamp(1447314272, 1),
            "electionDate" : ISODate("2015-11-12T07:44:32Z"),
            "configVersion" : 2,
            "self" : true
        },
        {
            "_id" : 2,
            "name" : "localhost:29017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 8826,
            "optime" : Timestamp(1447318556, 1),
            "optimeDate" : ISODate("2015-11-12T08:55:56Z"),
            "lastHeartbeat" : ISODate("2015-11-12T08:55:58.102Z"),
            "lastHeartbeatRecv" : ISODate("2015-11-12T08:55:58.564Z"),
            "pingMs" : 0,
            "syncingTo" : "localhost:28017",
            "configVersion" : 2
        },
        {
            "_id" : 3,
            "name" : "localhost:30017",
            "health" : 1,
            "state" : 5,
            "stateStr" : "STARTUP2",
            "uptime" : 2,
            "optime" : Timestamp(0, 0),
            "optimeDate" : ISODate("1970-01-01T00:00:00Z"),
            "lastHeartbeat" : ISODate("2015-11-12T08:55:58.116Z"),
            "lastHeartbeatRecv" : ISODate("2015-11-12T08:55:58.201Z"),
            "pingMs" : 12,
            "configVersion" : 2
        }
    ],
    "ok" : 1
}
rs1:PRIMARY> rs.status();
{
    "set" : "rs1",
    "date" : ISODate("2015-11-12T08:56:07.981Z"),
    "myState" : 1,
    "members" : [
        {
            "_id" : 0,
            "name" : "localhost:27017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 4044,
            "optime" : Timestamp(1447318556, 1),
            "optimeDate" : ISODate("2015-11-12T08:55:56Z"),
            "lastHeartbeat" : ISODate("2015-11-12T08:56:06.100Z"),
            "lastHeartbeatRecv" : ISODate("2015-11-12T08:56:07.089Z"),
            "pingMs" : 0,
            "syncingTo" : "localhost:28017",
            "configVersion" : 2
        },
        {
            "_id" : 1,
            "name" : "localhost:28017",
            "health" : 1,
            "state" : 1,
            "stateStr" : "PRIMARY",
            "uptime" : 9028,
            "optime" : Timestamp(1447318556, 1),
            "optimeDate" : ISODate("2015-11-12T08:55:56Z"),
            "electionTime" : Timestamp(1447314272, 1),
            "electionDate" : ISODate("2015-11-12T07:44:32Z"),
            "configVersion" : 2,
            "self" : true
        },
        {
            "_id" : 2,
            "name" : "localhost:29017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 8835,
            "optime" : Timestamp(1447318556, 1),
            "optimeDate" : ISODate("2015-11-12T08:55:56Z"),
            "lastHeartbeat" : ISODate("2015-11-12T08:56:06.119Z"),
            "lastHeartbeatRecv" : ISODate("2015-11-12T08:56:06.566Z"),
            "pingMs" : 0,
            "syncingTo" : "localhost:28017",
            "configVersion" : 2
        },
        {
            "_id" : 3,
            "name" : "localhost:30017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 11,
            "optime" : Timestamp(1447318556, 1),
            "optimeDate" : ISODate("2015-11-12T08:55:56Z"),
            "lastHeartbeat" : ISODate("2015-11-12T08:56:06.119Z"),
            "lastHeartbeatRecv" : ISODate("2015-11-12T08:56:06.205Z"),
            "pingMs" : 4,
            "configVersion" : 2
        }
    ],
    "ok" : 1
}

將 r4 (port 31017) 加入 replica sets 也會看到類似的過程

rs.add("localhost:31017");

連接到新加入的 r4,可以發現資料已經複製過來了。

]# mongo --port 31017 -u test -p pass --authenticationDatabase test
MongoDB shell version: 3.0.7
connecting to: 127.0.0.1:31017/test
rs1:SECONDARY> db.temp.find();
Error: error: { "$err" : "not master and slaveOk=false", "code" : 13435 }
rs1:SECONDARY> db.setSlaveOk();
rs1:SECONDARY> db.temp.find();
{ "_id" : ObjectId("56443fedeabb66f27f42b08a"), "age" : 22 }

以指令 rs.remove(); 就可以移除節點

rs.remove("localhost:30017");
rs.remove("localhost:31017");

References

Replication

Database MongoDB 製作複本 ( Replica Sets )

Replication Concepts—複製的概念

2015/12/7

如何在 MongoDB 處理最佳化的問題

MongoDB 有許多查詢的機制,但需要搭配使用一些工具,了解怎麼讓查詢的效能最佳化。

索引

MongoDB 可針對所有文件類型建立索引,但查詢時是否有用到索引的條件是根據索引建立的順序來決定的。

針對 age 欄位建立索引

> db.mate.ensureIndex({age:1});
{
    "createdCollectionAutomatically" : false,
    "numIndexesBefore" : 1,
    "numIndexesAfter" : 2,
    "ok" : 1
}

取得 collection 所有 indexes

> db.mate.getIndexes();
[
    {
        "v" : 1,
        "key" : {
            "_id" : 1
        },
        "name" : "_id_",
        "ns" : "test.mate"
    },
    {
        "v" : 1,
        "key" : {
            "age" : 1
        },
        "name" : "age_1",
        "ns" : "test.mate"
    }
]

建立複合索引

> db.mate.ensureIndex({age:1, room:1});
{
    "createdCollectionAutomatically" : false,
    "numIndexesBefore" : 2,
    "numIndexesAfter" : 3,
    "ok" : 1
}

> db.mate.find().sort( {age:1, room:1});
{ "_id" : ObjectId("5641980c997bc79c93b12e7a"), "userid" : "danny", "username" : "Danny Huang", "city" : "Taipei" }
{ "_id" : ObjectId("5641980c997bc79c93b12e7b"), "userid" : "johnny", "username" : "Johnny Lo", "city" : "Taipei", "age" : null }
{ "_id" : ObjectId("5641980c997bc79c93b12e76"), "userid" : "muder", "username" : "Muder Yen", "city" : "Taichung", "age" : 19, "room" : "301" }
{ "_id" : ObjectId("5641980c997bc79c93b12e75"), "userid" : "john", "username" : "John Lin", "city" : "Taipei", "age" : 20, "room" : "301" }
{ "_id" : ObjectId("5641980c997bc79c93b12e78"), "userid" : "celina", "username" : "Celina Lin", "city" : "Taichung", "age" : 20, "room" : "601" }
{ "_id" : ObjectId("5641980c997bc79c93b12e79"), "userid" : "lunar", "username" : "Lunar Wang", "city" : "I Lan", "age" : 22, "room" : "302" }
{ "_id" : ObjectId("5641980c997bc79c93b12e77"), "userid" : "mary", "username" : "Mary Wang", "city" : "Tainan", "age" : 23, "room" : "601" }
> db.mate.find().sort( {age:-1, room:1});
{ "_id" : ObjectId("5641980c997bc79c93b12e77"), "userid" : "mary", "username" : "Mary Wang", "city" : "Tainan", "age" : 23, "room" : "601" }
{ "_id" : ObjectId("5641980c997bc79c93b12e79"), "userid" : "lunar", "username" : "Lunar Wang", "city" : "I Lan", "age" : 22, "room" : "302" }
{ "_id" : ObjectId("5641980c997bc79c93b12e75"), "userid" : "john", "username" : "John Lin", "city" : "Taipei", "age" : 20, "room" : "301" }
{ "_id" : ObjectId("5641980c997bc79c93b12e78"), "userid" : "celina", "username" : "Celina Lin", "city" : "Taichung", "age" : 20, "room" : "601" }
{ "_id" : ObjectId("5641980c997bc79c93b12e76"), "userid" : "muder", "username" : "Muder Yen", "city" : "Taichung", "age" : 19, "room" : "301" }
{ "_id" : ObjectId("5641980c997bc79c93b12e7a"), "userid" : "danny", "username" : "Danny Huang", "city" : "Taipei" }
{ "_id" : ObjectId("5641980c997bc79c93b12e7b"), "userid" : "johnny", "username" : "Johnny Lo", "city" : "Taipei", "age" : null }

刪除所有索引,除了 _id 無法刪除之外

> db.mate.dropIndexes();
{
    "nIndexesWas" : 3,
    "msg" : "non-_id indexes dropped for collection",
    "ok" : 1
}

以 unique 指定該索引值必須要是 唯一的,不能有重複的紀錄

> db.mate.ensureIndex({userid:1}, {unique:true});
{
    "createdCollectionAutomatically" : false,
    "numIndexesBefore" : 1,
    "numIndexesAfter" : 2,
    "ok" : 1
}
> db.mate.ensureIndex({age:1}, {unique:true});
{
    "createdCollectionAutomatically" : false,
    "numIndexesBefore" : 2,
    "errmsg" : "exception: E11000 duplicate key error index: test.mate.$age_1 dup key: { : null }",
    "code" : 11000,
    "ok" : 0
}

以下為所有測試指令的集合

db.mate.ensureIndex({age:1});
db.mate.getIndexes();

db.mate.dropIndexes();
db.mate.ensureIndex({age:1, room:1});
db.mate.find().sort( {age:1, room:1});
db.mate.find().sort( {age:-1, room:1});
db.mate.getIndexes();

db.mate.dropIndexes();
db.mate.ensureIndex({userid:1}, {unique:true});
db.mate.ensureIndex({age:1}, {unique:true});

查詢最佳化

首先確認 db 有為 age, room 建立 index

> db.mate.dropIndexes();
{
    "nIndexesWas" : 2,
    "msg" : "non-_id indexes dropped for collection",
    "ok" : 1
}
> db.mate.ensureIndex({age:1, room:1});
{
    "createdCollectionAutomatically" : false,
    "numIndexesBefore" : 1,
    "numIndexesAfter" : 2,
    "ok" : 1
}
> db.mate.getIndexes();
[
    {
        "v" : 1,
        "key" : {
            "_id" : 1
        },
        "name" : "_id_",
        "ns" : "test.mate"
    },
    {
        "v" : 1,
        "key" : {
            "age" : 1,
            "room" : 1
        },
        "name" : "age_1_room_1",
        "ns" : "test.mate"
    }
]

執行 find 條件查詢,可以用 explain() 確認是否有使用到 index。Explain Results 頁面可查看裡面每個欄位的意義。

結果中主要應該是看 winningPlan,這是 query optimizer 最後所選擇的 query plan。

Stage 表示該階段的 operation,有下列幾種

  1. COLLSCAN: collection scan,掃描整個 collection,如果有產生這個 stage,就表示這個查詢的速度會很慢
  2. IXSCAN: scanning index keys,掃描 index 的 keys
  3. FETCH: retrieveing documents,取得文件
  4. SHARD_MERGE: 由 shards 整合結果
  5. PROJECTION: Projections 表示針對collection的某些欄位進行篩檢
  6. KEEP_MUTATIONS: 還沒查到說明的頁面
    > db.mate.find( {age:{ $gt: 22}, username:{ $regex:/^M.*/ }} );
    { "_id" : ObjectId("5641980c997bc79c93b12e77"), "userid" : "mary", "username" : "Mary Wang", "city" : "Tainan", "age" : 23, "room" : "601" }
    > db.mate.find( {age:{ $gt: 22}, username:{ $regex:/^M.*/ }} ).explain();
    {
     "queryPlanner" : {
         "plannerVersion" : 1,
         "namespace" : "test.mate",
         "indexFilterSet" : false,
         "parsedQuery" : {
             "$and" : [
                 {
                     "age" : {
                         "$gt" : 22
                     }
                 },
                 {
                     "username" : /^M.*/
                 }
             ]
         },
         "winningPlan" : {
             "stage" : "KEEP_MUTATIONS",
             "inputStage" : {
                 "stage" : "FETCH",
                 "filter" : {
                     "username" : /^M.*/
                 },
                 "inputStage" : {
                     "stage" : "IXSCAN",
                     "keyPattern" : {
                         "age" : 1,
                         "room" : 1
                     },
                     "indexName" : "age_1_room_1",
                     "isMultiKey" : false,
                     "direction" : "forward",
                     "indexBounds" : {
                         "age" : [
                             "(22.0, inf.0]"
                         ],
                         "room" : [
                             "[MinKey, MaxKey]"
                         ]
                     }
                 }
             }
         },
         "rejectedPlans" : [ ]
     },
     "serverInfo" : {
         "host" : "server",
         "port" : 27017,
         "version" : "3.0.7",
         "gitVersion" : "6ce7cbe8c6b899552dadd907604559806aa2e9bd"
     },
     "ok" : 1
    }
    

搜尋的另一個例子

> db.mate.find( {age:{ $gt: 22}}, {username:1} );
{ "_id" : ObjectId("5641980c997bc79c93b12e77"), "username" : "Mary Wang" }
> db.mate.find( {age:{ $gt: 22}}, {username:1} ).explain();
{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "test.mate",
        "indexFilterSet" : false,
        "parsedQuery" : {
            "age" : {
                "$gt" : 22
            }
        },
        "winningPlan" : {
            "stage" : "PROJECTION",
            "transformBy" : {
                "username" : 1
            },
            "inputStage" : {
                "stage" : "FETCH",
                "inputStage" : {
                    "stage" : "IXSCAN",
                    "keyPattern" : {
                        "age" : 1,
                        "room" : 1
                    },
                    "indexName" : "age_1_room_1",
                    "isMultiKey" : false,
                    "direction" : "forward",
                    "indexBounds" : {
                        "age" : [
                            "(22.0, inf.0]"
                        ],
                        "room" : [
                            "[MinKey, MaxKey]"
                        ]
                    }
                }
            }
        },
        "rejectedPlans" : [ ]
    },
    "serverInfo" : {
        "host" : "kokola",
        "port" : 27017,
        "version" : "3.0.7",
        "gitVersion" : "6ce7cbe8c6b899552dadd907604559806aa2e9bd"
    },
    "ok" : 1
}

由於沒有 city 欄位的 index,因此產生了 COLLSCAN,當資料量大,這個查詢的速度將會變得很慢。

> db.mate.find( {city:{ $regex:/^T.*/ } } );
{ "_id" : ObjectId("5641980c997bc79c93b12e75"), "userid" : "john", "username" : "John Lin", "city" : "Taipei", "age" : 20, "room" : "301" }
{ "_id" : ObjectId("5641980c997bc79c93b12e76"), "userid" : "muder", "username" : "Muder Yen", "city" : "Taichung", "age" : 19, "room" : "301" }
{ "_id" : ObjectId("5641980c997bc79c93b12e77"), "userid" : "mary", "username" : "Mary Wang", "city" : "Tainan", "age" : 23, "room" : "601" }
{ "_id" : ObjectId("5641980c997bc79c93b12e78"), "userid" : "celina", "username" : "Celina Lin", "city" : "Taichung", "age" : 20, "room" : "601" }
{ "_id" : ObjectId("5641980c997bc79c93b12e7a"), "userid" : "danny", "username" : "Danny Huang", "city" : "Taipei" }
{ "_id" : ObjectId("5641980c997bc79c93b12e7b"), "userid" : "johnny", "username" : "Johnny Lo", "city" : "Taipei", "age" : null }
> db.mate.find( {city:{ $regex:/^T.*/ } } ).explain();
{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "test.mate",
        "indexFilterSet" : false,
        "parsedQuery" : {
            "city" : /^T.*/
        },
        "winningPlan" : {
            "stage" : "COLLSCAN",
            "filter" : {
                "city" : /^T.*/
            },
            "direction" : "forward"
        },
        "rejectedPlans" : [ ]
    },
    "serverInfo" : {
        "host" : "kokola",
        "port" : 27017,
        "version" : "3.0.7",
        "gitVersion" : "6ce7cbe8c6b899552dadd907604559806aa2e9bd"
    },
    "ok" : 1
}

以下為所有測試指令的集合

db.mate.dropIndexes();
db.mate.ensureIndex({age:1});
db.mate.ensureIndex({room:1});
db.mate.getIndexes();


db.mate.find( {age:{ $gt: 22}, username:{ $regex:/^M.*/ }} );
db.mate.find( {age:{ $gt: 22}, username:{ $regex:/^M.*/ }} ).explain();

db.mate.find( {age:{ $gt: 22}}, {username:1} );
db.mate.find( {age:{ $gt: 22}}, {username:1} ).explain();

db.mate.find( {city:{ $regex:/^T.*/ } } );
db.mate.find( {city:{ $regex:/^T.*/ } } ).explain();

Profiler

MongoDB 中,可以啟動 profiler,用來記錄一些速度較慢的查詢指令,這可以用來作為最佳化資料庫的資訊來源。

有兩種方式可以啟動 profile 的功能,第一種是直接在 mongod 的參數中,加上 -profile ,第二種是透過 db.setProfilingLevel()來啟動 profiler。

profiler level 有三種

  • 0: 不啟動
  • 1: 記錄慢指令(預設為 > 100ms)
  • 2: 記錄所有指令

可用以下指令設定 ProfilingLevel

db.setProfilingLevel(1);

也可以同時指定慢指令的時間,例如設定為 10ms

db.setProfilingLevel(1,10);

查看執行時間超過 5ms 的 profiler 記錄

db.system.profile.find({millis:{$gt:5}});

查看最新的 profiler 記錄

db.system.profile.find().sort({$natural:-1}).limit(5);

> db.system.profile.find().sort({$natural:-1}).limit(5);
{ "op" : "query", "ns" : "test.mate", "query" : { "age" : { "$gt" : 22 }, "username" : { "$regex" : /^M.*/ } }, "ntoreturn" : 0, "ntoskip" : 0, "nscanned" : 1, "nscannedObjects" : 1, "keyUpdates" : 0, "writeConflicts" : 0, "numYield" : 0, "locks" : { "Global" : { "acquireCount" : { "r" : NumberLong(2) } }, "MMAPV1Journal" : { "acquireCount" : { "r" : NumberLong(1) } }, "Database" : { "acquireCount" : { "r" : NumberLong(1) } }, "Collection" : { "acquireCount" : { "R" : NumberLong(1) } } }, "nreturned" : 1, "responseLength" : 127, "millis" : 0, "execStats" : { "stage" : "KEEP_MUTATIONS", "nReturned" : 1, "executionTimeMillisEstimate" : 0, "works" : 2, "advanced" : 1, "needTime" : 0, "needFetch" : 0, "saveState" : 0, "restoreState" : 0, "isEOF" : 1, "invalidates" : 0, "inputStage" : { "stage" : "FETCH", "filter" : { "username" : /^M.*/ }, "nReturned" : 1, "executionTimeMillisEstimate" : 0, "works" : 2, "advanced" : 1, "needTime" : 0, "needFetch" : 0, "saveState" : 0, "restoreState" : 0, "isEOF" : 1, "invalidates" : 0, "docsExamined" : 1, "alreadyHasObj" : 0, "inputStage" : { "stage" : "IXSCAN", "nReturned" : 1, "executionTimeMillisEstimate" : 0, "works" : 1, "advanced" : 1, "needTime" : 0, "needFetch" : 0, "saveState" : 0, "restoreState" : 0, "isEOF" : 1, "invalidates" : 0, "keyPattern" : { "age" : 1 }, "indexName" : "age_1", "isMultiKey" : false, "direction" : "forward", "indexBounds" : { "age" : [ "(22.0, inf.0]" ] }, "keysExamined" : 1, "dupsTested" : 0, "dupsDropped" : 0, "seenInvalidated" : 0, "matchTested" : 0 } } }, "ts" : ISODate("2015-11-11T03:22:04.503Z"), "client" : "127.0.0.1", "allUsers" : [ ], "user" : "" }

最佳化的策略方案

  1. 建立索引
    針對查詢條件的欄位建立索引
  2. 限制回傳結果的筆數
    使用 limit() 限制筆數,可減少 db 的資源消耗並減少網路傳輸量
  3. 只查詢使用到的欄位,不查詢所有欄位
    只查詢使用到的欄位
  4. 使用 Capped Collection
    Capped Collection 比普通的 Collection 的讀寫效率高,它有以下的特性
    • 固定大小:必須手動建立並設定固定的大小,db.createcollection("testc", {capped:true, size:1000000});
    • 可以 insert 與 update 但不能 delete
    • 只能用 drop 刪除整個 collection
    • 預設以 insert 的順序排序
    • FIFO,如果超過了限制的大小,依照 FIFO 原則,自動刪除最舊的資料
  5. 使用 Server Side Code Execution
    類似關聯式資料庫的 Stored Procedure
  6. hint
    一般情況下使用 query optimizer 就好了,有時候可以用 hint() 強制要求查詢要使用某個 index。例如在多個條件的查詢中,如果其中一個欄位有索引,可以用 hint

     > db.mate.find( {age:{ $gt: 22}}, {username:1} ).hint({age:1});
     { "_id" : ObjectId("5641980c997bc79c93b12e77"), "username" : "Mary Wang" }
     > db.mate.find( {age:{ $gt: 22}}, {username:1} ).hint({age:1}).explain)(;
     2015-11-11T11:35:22.023+0800 E QUERY    SyntaxError: Unexpected token )
     > db.mate.find( {age:{ $gt: 22}}, {username:1} ).hint({age:1}).explain();
     {
         "queryPlanner" : {
             "plannerVersion" : 1,
             "namespace" : "test.mate",
             "indexFilterSet" : false,
             "parsedQuery" : {
                 "age" : {
                     "$gt" : 22
                 }
             },
             "winningPlan" : {
                 "stage" : "PROJECTION",
                 "transformBy" : {
                     "username" : 1
                 },
                 "inputStage" : {
                     "stage" : "FETCH",
                     "inputStage" : {
                         "stage" : "IXSCAN",
                         "keyPattern" : {
                             "age" : 1
                         },
                         "indexName" : "age_1",
                         "isMultiKey" : false,
                         "direction" : "forward",
                         "indexBounds" : {
                             "age" : [
                                 "(22.0, inf.0]"
                             ]
                         }
                     }
                 }
             },
             "rejectedPlans" : [ ]
         },
         "serverInfo" : {
             "host" : "server",
             "port" : 27017,
             "version" : "3.0.7",
             "gitVersion" : "6ce7cbe8c6b899552dadd907604559806aa2e9bd"
         },
         "ok" : 1
     }
    
  7. 使用 profiling
    利用 profiling 記錄一些比較慢的查詢,用來作為後續最佳化的參考資料

資料庫監控

  • mongosniff
    binary 安裝包裡面並沒有包含這個工具,必須要自己從 source code 編譯,這個工具可以監聽 27017 port,記錄所有封包。

  • mongostat
    可查詢某個 mongod 的統計資訊

    [root@kokola bin]# ./mongostat
    insert query update delete getmore command flushes mapped vsize   res faults qr|qw ar|aw netIn netOut conn     time
      *0    *0     *0     *0       0     1|0       0 160.0M  1.7G 73.0M      0   0|0   0|0   79b    10k    2 11:42:14
      *0    *0     *0     *0       0     1|0       0 160.0M  1.7G 73.0M      0   0|0   0|0   79b    10k    2 11:42:15
      *0    *0     *0     *0       0     1|0       0 160.0M  1.7G 73.0M      0   0|0   0|0   79b    10k    2 11:42:16
      *0    *0     *0     *0       0     1|0       0 160.0M  1.7G 73.0M      0   0|0   0|0   79b    10k    2 11:42:17
      *0    *0     *0     *0       0     2|0       0 160.0M  1.7G 73.0M      0   0|0   0|0  133b    10k    2 11:42:18
      *0    *0     *0     *0       0     1|0       0 160.0M  1.7G 73.0M      0   0|0   0|0   79b    10k    2 11:42:19
    
      insert
          每秒 insert 的operation數量
      query
      update
      delete
      getmore
          每秒進行 get more (ex: cursor batch) operations 的數量
      command
          每秒執行的指令數量
      flushes
          如果是 WiredTiger Storage Engine就是 WiredTiger checkpoints triggered 的數量,如果是 MMAPv1 Storage Engine,就是 fsync operations 的數量
      dirty
          只有 WiredTiger Storage Engine 會有,這是 WiredTiger cache with dirty bytes 的比例
      used
          WiredTiger Storage Engine 才有,這是 WiredTiger cache 的使用比例
      mapped
          MMAPv1 Storage Engine 才有,總資料量,單位是 MegaBytes
      vsize
          mongod proecess 使用的 virtual memory 數量,單位是 MegaBytes
      non-mapped
          (optional) MMAPv1 Storage Engine 才有,去掉所有 mapped memory 的 virtual memory 總量
      res
          process 使用的 resident memory 總量,單位是 MegaBytes
      faults
          MMAPv1 Storage Engine 才有,每秒產生的 page faults 數量
      idx miss
          MMAPv1 Storage Engine才有,載入一個 btree node 所產生的 page fault,所需要使用的 index 比例
      qr
          所有 clients 等待讀取資料的 queue 的長度
      qw
          所有 clients 等待寫入資料的 queue 的長度
      ar
          執行 read operations 的 active clients 數量
      aw
          執行 write operations 的 active clients 數量
      netIn
          mongod 由網路接收到的資料量,單位為 bytes,包含 mongostat 本身的資料量
      netOut
          mongod 由網路發送的資料量,單位為 bytes
      conn
          總連線數
      set
          replica set 的名稱
      repl
          replication status of the members
          (1) M: master
          (2) SEC: secondary
          (3) REC: recovering
          (4) UNK: unknown
          (5) SLV: slave
          (6) RTR: mongos process(router)
          (7) ARB: arbiter
    

db.serverStatus

透過 db.serverStatus() 指令,我們可以即時檢視資料庫的狀態。

> db.serverStatus();
{
    "host" : "server",
    "version" : "3.0.7",    // MongoDB 版本
    "process" : "mongod",
    "pid" : NumberLong(12533), // processid
    "uptime" : 160841,    // 啟動的時間(seconds)
    "uptimeMillis" : NumberLong(160841652),
    "uptimeEstimate" : 159741,
    "localTime" : ISODate("2015-11-11T06:27:31.655Z"),
    "asserts" : {    // server 產生 error or assertions 的數量
        "regular" : 0,
        "warning" : 0,
        "msg" : 0,
        "user" : 9,
        "rollovers" : 0
    },
    "backgroundFlushing" : {    // 將資料寫入 disk 的 report
        "flushes" : 2680,
        "total_ms" : 4357,
        "average_ms" : 1.6257462686567163,
        "last_ms" : 1,
        "last_finished" : ISODate("2015-11-11T06:26:50.271Z")
    },
    "connections" : {    // 目前clients的連線狀況
        "current" : 1,    // active client 連線數量
        "available" : 52427,    // 空閒的連線數量
        "totalCreated" : NumberLong(4)
    },
    "cursors" : {    // 目前的 cursor 使用狀況
        "note" : "deprecated, use server status metrics",
        "clientCursors_size" : 0,
        "totalOpen" : 0,
        "pinned" : 0,
        "totalNoTimeout" : 0,
        "timedOut" : 3
    },
    "dur" : {    // journaling 的 report
        "commits" : 22,
        "journaledMB" : 0,
        "writeToDataFilesMB" : 0,
        "compression" : 0,
        "commitsInWriteLock" : 0,
        "earlyCommits" : 0,
        "timeMs" : {
            "dt" : 0,
            "prepLogBuffer" : 0,
            "writeToJournal" : 0,
            "writeToDataFiles" : 0,
            "remapPrivateView" : 0,
            "commits" : 0,
            "commitsInWriteLock" : 0
        }
    },
    "extra_info" : {
        "note" : "fields vary by platform",
        "heap_usage_bytes" : 63700688,
        "page_faults" : 65
    },
    "globalLock" : {    // global system lock 的 report
        "totalTime" : NumberLong("160841653000"),
        "currentQueue" : {
            "total" : 0,    // 全部 queue 的數量
            "readers" : 0,
            "writers" : 0
        },
        "activeClients" : {
            "total" : 9,    // client連線數量
            "readers" : 0,
            "writers" : 0
        }
    },
    "locks" : {        // 每個 lock type 與 mode 的統計報告
        "Global" : {
            "acquireCount" : {
                "r" : NumberLong(539117),
                "w" : NumberLong(559),
                "W" : NumberLong(50)
            }
        },
        "MMAPV1Journal" : {
            "acquireCount" : {
                "r" : NumberLong(269221),
                "w" : NumberLong(1214),
                "R" : NumberLong(1573490),
                "W" : NumberLong(6)
            },
            "acquireWaitCount" : {
                "w" : NumberLong(1),
                "R" : NumberLong(2)
            },
            "timeAcquiringMicros" : {
                "w" : NumberLong(18),
                "R" : NumberLong(173851)
            }
        },
        "Database" : {
            "acquireCount" : {
                "r" : NumberLong(269199),
                "w" : NumberLong(391),
                "R" : NumberLong(55),
                "W" : NumberLong(201)
            }
        },
        "Collection" : {
            "acquireCount" : {
                "R" : NumberLong(281400),
                "W" : NumberLong(442)
            }
        },
        "Metadata" : {
            "acquireCount" : {
                "W" : NumberLong(180)
            }
        }
    },
    "network" : {    // 網路的使用報告
        "bytesIn" : NumberLong(111181),
        "bytesOut" : NumberLong(354420),
        "numRequests" : NumberLong(1227)
    },
    "opcounters" : {    // 這個 instance 處理了幾個 operations
        "insert" : 200,
        "query" : 158,
        "update" : 3,
        "delete" : 2,
        "getmore" : 3,
        "command" : 973
    },
    "opcountersRepl" : {    // 處理了幾個 replicated operations
        "insert" : 0,
        "query" : 0,
        "update" : 0,
        "delete" : 0,
        "getmore" : 0,
        "command" : 0
    },
    "storageEngine" : {        // 目前使用那一種 storage engine
        "name" : "mmapv1"
    },
    "writeBacksQueued" : false,
    "mem" : {    // 目前記憶體的使用報告
        "bits" : 64,    // 64 bits
        "resident" : 73,    // 佔用的物理記憶體總量
        "virtual" : 1750,    // 虛擬記憶體總量
        "supported" : true,    // 是否支援擴充記憶體
        "mapped" : 160,
        "mappedWithJournal" : 320
    },
    "metrics" : {    // 監控 mongod 的 state 與 workload 的一些 operational metrics
        "commands" : {
            "<UNKNOWN>" : NumberLong(1),
            "collStats" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(2)
            },
            "count" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(2)
            },
            "createIndexes" : {
                "failed" : NumberLong(2),
                "total" : NumberLong(13)
            },
            "delete" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(2)
            },
            "drop" : {
                "failed" : NumberLong(63),
                "total" : NumberLong(85)
            },
            "dropIndexes" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(7)
            },
            "explain" : {
                "failed" : NumberLong(1),
                "total" : NumberLong(18)
            },
            "getLog" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(3)
            },
            "getnonce" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(1)
            },
            "insert" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(200)
            },
            "isMaster" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(740)
            },
            "listCollections" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(38)
            },
            "listIndexes" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(11)
            },
            "mapReduce" : {
                "failed" : NumberLong(1),
                "total" : NumberLong(16)
            },
            "ping" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(3)
            },
            "profile" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(2)
            },
            "renameCollection" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(15)
            },
            "replSetGetStatus" : {
                "failed" : NumberLong(3),
                "total" : NumberLong(3)
            },
            "serverStatus" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(11)
            },
            "update" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(3)
            },
            "whatsmyuri" : {
                "failed" : NumberLong(0),
                "total" : NumberLong(3)
            }
        },
        "cursor" : {
            "timedOut" : NumberLong(3),
            "open" : {
                "noTimeout" : NumberLong(0),
                "pinned" : NumberLong(0),
                "total" : NumberLong(0)
            }
        },
        "document" : {
            "deleted" : NumberLong(1),
            "inserted" : NumberLong(199),
            "returned" : NumberLong(1279),
            "updated" : NumberLong(3)
        },
        "getLastError" : {
            "wtime" : {
                "num" : 0,
                "totalMillis" : 0
            },
            "wtimeouts" : NumberLong(0)
        },
        "operation" : {
            "fastmod" : NumberLong(0),
            "idhack" : NumberLong(0),
            "scanAndOrder" : NumberLong(7),
            "writeConflicts" : NumberLong(0)
        },
        "queryExecutor" : {
            "scanned" : NumberLong(34),
            "scannedObjects" : NumberLong(1824)
        },
        "record" : {
            "moves" : NumberLong(0)
        },
        "repl" : {
            "apply" : {
                "batches" : {
                    "num" : 0,
                    "totalMillis" : 0
                },
                "ops" : NumberLong(0)
            },
            "buffer" : {
                "count" : NumberLong(0),
                "maxSizeBytes" : 268435456,
                "sizeBytes" : NumberLong(0)
            },
            "network" : {
                "bytes" : NumberLong(0),
                "getmores" : {
                    "num" : 0,
                    "totalMillis" : 0
                },
                "ops" : NumberLong(0),
                "readersCreated" : NumberLong(0)
            },
            "preload" : {
                "docs" : {
                    "num" : 0,
                    "totalMillis" : 0
                },
                "indexes" : {
                    "num" : 0,
                    "totalMillis" : 0
                }
            }
        },
        "storage" : {
            "freelist" : {
                "search" : {
                    "bucketExhausted" : NumberLong(0),
                    "requests" : NumberLong(611),
                    "scanned" : NumberLong(0)
                }
            }
        },
        "ttl" : {
            "deletedDocuments" : NumberLong(0),
            "passes" : NumberLong(2680)
        }
    },
    "ok" : 1
}

db.stats

db.stats 指令是用來查看 dbStats 資料庫的狀態

> db.stats();
{
    "db" : "test",
    "collections" : 5,    // connection 數量
    "objects" : 28,        // 物件數量
    "avgObjSize" : 307.42857142857144,    // 平均物件大小
    "dataSize" : 8608,    // 資料大小
    "storageSize" : 1110016,    // storage大小
    "numExtents" : 6,    // 事件數量
    "indexes" : 4,        // 索引數量
    "indexSize" : 32704,    // 索引大小
    "fileSize" : 67108864,    // 檔案大小
    "nsSizeMB" : 16,
    "extentFreeList" : {
        "num" : 3,
        "totalSize" : 147456
    },
    "dataFileVersion" : {
        "major" : 4,
        "minor" : 22
    },
    "ok" : 1
}