前台構建 foreground vs 后台構建 background
MongoDB 3.6 版本構建索引支持前台構建和后台構建,后台構建索引:
// 舉例
db.people.createIndex( { zipcode: 1}, {background: true} )
默認地,MongoDB 索引創建的 background 是 false,即前台創建。
對比如下:
方式 |
使用方法 |
構建速度和索引文件 |
對讀寫影響 |
前台構建 |
createIndex默認為前台構建 |
快,索引文件的數據結構更高效 |
阻塞該集合所在數據庫上的所有操作 |
后台構建 |
background參數,指定為true |
慢,索引文件的數據結構較低效 |
增量處理過程,期間允許讀寫操作 |
針對3.6版本,在大數據量集合構建索引,並且有業務讀寫的情況下,慎用前台構建索引的方式
MongoDB 4.2 版本忽略了background參數。在該版本上構建索引,只會在構建索引的開始和結束,獲取集合鎖,獲取鎖期間,會阻塞該集合上的讀寫操作,中間掃描集合文檔、構建索引的過程中,不會阻塞讀寫請求。官方解釋的性能對比:
MongoDB 4.2 索引構建性能至少與后台索引構建相當。對於在構建過程中工作負載較低的情況下,4.2 索引構建構建的速度可以與基於相同數據量的前台索引構建一樣快
構建索引對於數據庫性能的影響
在目標集合處於高寫入負載的時間段內構建索引可能會導致寫入性能降低,並且構建索引的時間更久。建議在業務寫入低峰期操作。可以參考下以下方式進行評估:
1、實例所在機器運行內存仍有1GB以上的剩余。
2、在過去一段時間cpu利用率無明顯激增到100%的情況。
副本集或者分片集群上構建索引的過程
在副本集的Primary節點上構建索引,或者在分片集群的mongos節點上構建索引,過程如下:
1、首先在副本集的Primary節點上構建索引。
2、之后重放在Secondary節點上。
查看索引進度
在構建索引的過程中,可以通過currentOp來查看構建索引的進度
// 輸入
db.currentOp(true).inprog.forEach(function(op){ if(op.msg!==undefined) print(op.msg) })
//輸出結果
Index Build (background) Index Build (background): 89391121/204453376 43%
展示更多信息的搜索方式(在構建多個索引的情況下)
db.adminCommand(
{
currentOp: true,
$or: [
{ op: "command", "command.createIndexes": { $exists: true } },
{ op: "none", "msg" : /^Index Build/ }
]
}
)
這個輸出的結果是在 Primary 節點上的進度,未體現 Secondary 節點上構建索引的進度,可以理解為“即使這里進度顯示100%,也只是代表 Primary 節點已完成索引構建,並不能代表整個副本集或者整個分片集群完成了索引構建”,所以最好通過日志確認下Secondary 節點的索引進度,特別是在需要構建多個索引的情況下,避免出現 secondary 節點索引構建未完成,而下一個索引又開始構建了的情況,這樣 secondary 節點的io壓力會增加,從而影響整個集群的性能。
Secondary 重放構建索引的過程,是插入索引的過程,日志如下:

終止索引構建
當在 Primary 節點上構建索引時,如要終止,可以使用db.killOp()方法,kill操作不是馬上生效,可能會在大部分索引構建完成后才會終止。
而構建索引在 Secondary 節點上重放時,則無法終止該過程。首先必須在 Primary 節點上執行刪除索引 dropIndex(),在 Secondary 節點索引構建完成后,再重放刪除索引操作。
構建索引過程中節點掛掉
單節點掛掉
單節點 mongod 在構建索引期間被關閉,索引構建任務和進度都將丟失。重新啟動 mongod 不會重新啟動索引構建。必須重新發出 createIndex() 操作以啟動索引構建。
Primary 節點掛掉
如果 Primary 節點在索引構建期間被關閉或停止運行,索引構建任務和進度都將丟失。重新啟動 mongod 不會重新啟動索引構建。必須重新發出 createIndex() 操作以啟動索引構建。
Secondary 節點掛掉
如果 Secondary 節點在索引構建期間被關閉,則索引構建任務將保留。重啟 mongod 將會從頭開始恢復索引構建。而在索引構建完成之前,mongod 將會一直停留在啟動階段。如下圖所示:

如果數據量較大,這個啟動過程會持續較久,而在這期間,所有操作都要等到索引構建完成,如果 oplog 無法覆蓋完成索引構建所需的時間,該節點就會與 Primary 節點丟失同步,變為 Recovering 節點,那么需要全量同步來恢復。所以此刻你需要在 Primary 節點上查看 oplog 信息,預估下 oplog 能夠覆蓋的時間。

需要注意的是,這個時間體現的是當前寫入速度下 oplog 覆蓋的時間,如業務之后的寫入速度出現增加或下降,則 oplog 能夠覆蓋的時間也會對應的降低或升高。所以只能參考這個信息進行預估,如果你預計 oplog 無法覆蓋,最好臨時調整下oplog size。
跳過索引構建並啟動
可以看到構建索引過程中 secondary 節點掛掉,重新拉起,啟動過程可能會非常的長,同時對於客戶端來說,該實例也是不可用的,可通過 --noIndexBuildRetry 命令行來跳過啟動時的索引構建。
需要注意的是,在 MongoDB 4.0版本之后,副本集節點不再支持該命令行。所以對於 4.0 版本及以上,需要提前評估下集群負載,最好在業務低峰或者停寫階段創建索引,避免出現索引創建過程中,Secondary 節點掛掉的情況。否則你只能等待索引構建完成,而在這期間,實例將處於不可用的狀態。
離線構建索引
為了減少構建索引的影響,可以使用“離線”構建索引的方式,這里的“離線”指的是 在副本集或者分片集群,通過脫離副本集的同步,進行索引的構建,構建完成后再加入副本集。"離線"構建索引,需要從中取出一個 secondary 節點來操作,每次只會影響副本集中的一個節點,而不是全部的 secondary 節點。相較於直接在副本集上構建索引,影響范圍更小,速度更快,且可控。
副本集離線構建索引
注意事項
唯一索引
如果要使用該流程創建唯一索引,必須在該過程中停止對該集合的寫入,否則可能會在副本集成員之間得到不一致的數據。如果不能停止寫入,不要使用該流程,仍需要在 Primary 節點使用 createIndex() 創建索引。
預估oplog並提前調整
確保 oplog 足夠大,以允許索引構建操作完成而不會落后太多,避免因無法趕上而出現recovering 導致全量同步的情況。
操作流程
1、停止一個 Secondary 節點,以單實例模式啟動
選擇一個 Secondary 節點,通過 shutdownServer() 停止。如果啟動實例,使用的是配置文件的話,則修改其配置文件的內容。修改如下:
- 注釋 replication.replSetName 選項。
- 修改端口號,可以先注釋舊的選項,然后加一個新的端口號。
- 在 setParameter 將參數 disableLogicalSessionCacheRefresh 設置為 true。
net:
port: 27218
# port: 27017
#replication:
# replSetName: myRepl
setParameter:
disableLogicalSessionCacheRefresh: true
其他配置保持不變,之后重啟該實例。
mongod -f configfile
如果啟動實例使用的命令行配置,則使用以下命令行啟動即可。
mongod --port 27217 --setParameter disableLogicalSessionCacheRefresh=true
更換端口是為了確保在構建索引時副本集的其他成員和所有客戶端不會聯系該成員
2、在單節點上構建索引
連接上該 mongod 實例,使用 createIndex() 創建索引,不必指定 background ,使用前台創建即可。
3、以副本集節點重啟
構建索引完成后,停止該節點,恢復配置文件到修改前,重啟。啟動后該節點會拉取 oplog 進行同步。直到追趕上。
4、對其他的 secondary 節點重復以上操作
5、在 Primary 節點上構建索引
通過 rs.stepDown() 或者修改 priority 優先級的方式,進行主從切換。主從切換成功,原 primary 節點此時變為 secondary 節點,重復 1-2-3 步驟。之后對於 多az集群,可能仍需要切主到該節點。
分片集群離線構建索引
注意事項
同副本集的注意事項,另外針對唯一索引,除了停止寫入之外,還要注意:
在創建索引之前,驗證集合中沒有任何文檔違反唯一索引約束(當然數據量較多的情況下,也沒什么特別高效率的方式來驗證,所以在大數據量集合中創建唯一索引,通常比較棘手)。如果集合分布在分片上並且有分片包含具有重復文檔,則構建索引可能導致 在沒有重復文檔的分片上成功,但不會在有重復的分片上成功,這樣就會出現分片之前索引不一致的情況。為避免跨分片不一致的索引,可以從 mongos 發出 db.collection.dropIndex() 以從集合中刪除索引,這樣會在存在索引的分片進行刪除。
操作流程
如果分片集群只有一個分片,則直接跳過1、2步驟。
1、關閉均衡器
登錄 mongos 節點執行以下命令關閉均衡器
sh.stopBalancer()
如果遷移正在進行中,系統將在停止平衡器之前完成正在進行的遷移。
要驗證平衡器是否已停止,執行 sh.getBalancerState(),如果平衡器已停止,則返回 false。
2、確定集合在各分片的分布
登錄 mongos 節點,刷新該 mongos 節點緩存的路由表,避免返回舊的路由信息。刷新之后,通過 db.collection.getShardDistribution() 命令獲取集合的分布。
// 刷新路由表
db.adminCommand( { flushRouterConfig: "test.records" } );
// 獲取集合分布
use test;
db.records.getShardDistribution();
集合分布的信息舉例如下:
Shard shardA at shardA/s1-mongo1.example.net:27018,s1-mongo2.example.net:27018,s1-mongo3.example.net:27018
data : 1KiB docs : 50 chunks : 1
estimated data per chunk : 1KiB
estimated docs per chunk : 50
Shard shardC at shardC/s3-mongo1.example.net:27018,s3-mongo2.example.net:27018,s3-mongo3.example.net:27018
data : 1KiB docs : 50 chunks : 1
estimated data per chunk : 1KiB
estimated docs per chunk : 50
Totals
data : 3KiB docs : 100 chunks : 2
Shard shardA contains 50% data, 50% docs in cluster, avg obj size on shard : 40B
Shard shardC contains 50% data, 50% docs in cluster, avg obj size on shard : 40B
從輸出可以看到集合數據分布在 shadA 和 shardC上。
3、在包含集合數據的分片構建索引
在每一個分片上執行以下操作,如上所示,只需要在 shadA 和 shardC上執行構建索引。
A.停止一個 Secondary 節點,以單實例模式啟動
選擇一個 Secondary 節點,通過 shutdownServer() 停止。如果啟動實例,使用的是配置文件的話,則修改其配置文件的內容。修改如下:
- 注釋 replication.replSetName 選項。
- 注釋 sharding.clusterRole 選項。
- 修改端口號,可以先注釋舊的選項,然后加一個新的端口號。
- 在 setParameter 將參數 disableLogicalSessionCacheRefresh 設置為 true。
- 在 setParameter 將參數 skipShardingConfigurationChecks 設置為 true。
net:
port: 27218
# port: 27018
#replication:
# replSetName: shardA
#sharding:
# clusterRole: shardsvr
setParameter:
skipShardingConfigurationChecks: true
disableLogicalSessionCacheRefresh: true
其他配置保持不變,之后重啟該實例
mongod -f configfile
如果啟動實例使用的命令行配置,則使用以下命令行啟動即可
mongod --port 27218 --setParameter skipShardingConfigurationChecks=true --setParameter disableLogicalSessionCacheRefresh=true
B.構建索引
連接上該 mongod 實例,使用 createIndex() 創建索引,不必指定 background ,使用前台創建即可。
C.以副本集節點重啟
構建索引完成后,停止該節點,恢復配置文件到修改前,重啟。啟動后該節點會拉取 oplog 進行同步。直到追趕上。
D.對其他的 secondary 節點重復以上操作
E.在 Primary 節點上構建索引
通過 rs.stepDown() 或者修改 priority 優先級的方式,進行主從切換。主從切換成功,原 primary 節點此時變為 secondary 節點,重復 1-2-3 步驟。之后對於 多az集群,可能仍需要切主到該節點。
4、開啟均衡器
連接 mongos 節點,執行以下命令開啟均衡器。
sh.startBalancer()
參考文檔
Index Builds on Populated Collections