正文

  使用MongoDB的開(kāi)發(fā)人員應(yīng)該都聽(tīng)說(shuō)過(guò)孤兒文檔(orphaned document)這回事兒,可謂聞著沉默,遇者流淚。本文基于MongoDB3.0來(lái)看看怎么產(chǎn)生一個(gè)orphaned document,要求MongoDB的運(yùn)行方式需要是sharded cluster,如果對(duì)這一部分還不是很了解,可以參考一下這篇文章。

  在MongoDB的官方文檔中,對(duì)orphaned document的描述非常簡(jiǎn)單:

  In a sharded cluster, orphaned documents are those documents on a shard that also exist in chunks on other shards as a result of failed migrations or incomplete migration cleanup due to abnormal shutdown. Delete orphaned documents using cleanupOrphaned to reclaim disk space and reduce confusion

  可以看到,orphaned document是指在sharded cluster環(huán)境下,一些同時(shí)存在于不同shard上的document。我們知道,在mongodb sharded cluster中,分布在不同shard的數(shù)據(jù)子集是正交的,即理論上一個(gè)document只能出現(xiàn)在一個(gè)shard上,document與shard的映射關(guān)系維護(hù)在config server中。官方文檔指出了可能產(chǎn)生orphaned document的情況:在chunk遷移的過(guò)程中,mongod實(shí)例異常宕機(jī),導(dǎo)致遷移過(guò)程失敗或者部分完成。文檔中還指出,可以使用 cleanupOrphaned 來(lái)刪除orphaned document。

  新聞報(bào)道災(zāi)難、事故的時(shí)候,一般都有這么一個(gè)潛規(guī)則:內(nèi)容越短,事情也嚴(yán)重。不知道MongoDB對(duì)于orphaned document是不是也采用了這個(gè)套路,一來(lái)對(duì)orphaned document發(fā)生的可能原因描述不夠詳盡,二來(lái)也沒(méi)有提供檢測(cè)是否存在orphaned document的方法。對(duì)于cleanupOrphaned,要在生產(chǎn)環(huán)境使用也需要一定的勇氣。

orphaned document產(chǎn)生原因

回到頂部

   作為一個(gè)沒(méi)有看過(guò)MongoDB源碼的普通應(yīng)用開(kāi)發(fā)人員,拍腦袋想想,chuck的遷移應(yīng)該有以下三個(gè)步驟:將數(shù)據(jù)從源shard拷貝到目標(biāo)shard,更新config server中的metadata,從源shard刪除數(shù)據(jù)。當(dāng)然,這三個(gè)步驟的順序不一定是上面的順序。這三個(gè)步驟,如果能保證原子性,那么理論上是不會(huì)出問(wèn)題的。但是,orphaned document具體怎么出現(xiàn)的一直不是很清楚。

  前些天在瀏覽官方文檔的時(shí)候,發(fā)現(xiàn)有對(duì)遷移過(guò)程描述(chunk-migration-procedure),大致過(guò)程翻譯如下:

  1.   balancer向源shard發(fā)送moveChunk命令

  2.   源shard內(nèi)部執(zhí)行moveChunk命令,并保證在遷移的過(guò)程中,新插入的document還是寫(xiě)入源shard

  3.   如果需要的話,目標(biāo)shard創(chuàng)建需要的索引

  4.   目標(biāo)shard從源shard請(qǐng)求數(shù)據(jù);注意,這里是一個(gè)copy操作,而不是move操作

  5.   在接收完chunk的最后一個(gè)文檔后,目標(biāo)shard啟動(dòng)一個(gè)同步拷貝進(jìn)程,保證拷貝到在遷移過(guò)程中又寫(xiě)入源shard上的相關(guān)文檔

  6.   完全同步之后,目標(biāo)shard向config server報(bào)告新的metadata(chunk的新位置信息)

  7.   在上一步完成之后,源shard開(kāi)始刪除舊的document

  如果能保證以上操作的原子性,在任何步驟出問(wèn)題應(yīng)該都沒(méi)問(wèn)題;如果不能保證,那么在第4,5,6,7步出現(xiàn)機(jī)器宕機(jī),都有可能出問(wèn)題。對(duì)于出問(wèn)題的原因,官網(wǎng)(chunk-migration-queuing )是這么解釋的:

  the balancer does not wait for the current migration’s delete phase to complete before starting the next chunk migration

  This queuing behavior allows shards to unload chunks more quickly in cases of heavily imbalanced cluster, such as when performing initial data loads without pre-splitting and when adding new shards.

  If multiple delete phases are queued but not yet complete, a crash of the replica set’s primary can orphan data from multiple migrations.

  簡(jiǎn)而言之,為了加速chunk 遷移的速度(比如在新的shard加入的時(shí)候,有大量的chunk遷移),因此delete phase(第7步)不會(huì)立刻執(zhí)行,而是放入一個(gè)隊(duì)列,異步執(zhí)行,此時(shí)如果crash,就可能產(chǎn)生孤兒文檔

產(chǎn)生一個(gè)orphaned document

回到頂部

  基于官方文檔,如何產(chǎn)生一個(gè)orphaned document呢? 我的想法很簡(jiǎn)單:監(jiān)控MongoDB日志,在出現(xiàn)標(biāo)志遷移過(guò)程的日志出現(xiàn)的時(shí)候,kill掉shard中的primary!

預(yù)備知識(shí)

  在《通過(guò)一步步創(chuàng)建sharded cluster來(lái)認(rèn)識(shí)mongodb》一文中,我詳細(xì)介紹了如何搭建一個(gè)sharded cluster,在我的實(shí)例中,使用了兩個(gè)shard,其中每個(gè)shard包括一個(gè)primary、一個(gè)secondary,一個(gè)arbiter。另外,創(chuàng)建了一個(gè)允許sharding的db -- test_db, 然后sharded_col這個(gè)集合使用_id分片,本文基于這個(gè)sharded cluster進(jìn)行實(shí)驗(yàn)。但需要注意的是在前文中,為了節(jié)省磁盤(pán)空間,我禁用了mongod實(shí)例的journal機(jī)制(啟動(dòng)選項(xiàng)中的 --nojourbal),但在本文中,為了盡量符合真實(shí)情況,在啟動(dòng)mongod的時(shí)候使用了--journal來(lái)啟用journal機(jī)制。

 

   另外,再補(bǔ)充兩點(diǎn),第一個(gè)是chunk遷移的條件,只有當(dāng)shards之間chunk的數(shù)目差異達(dá)到一定程度才會(huì)發(fā)生遷移:

Number of ChunksMigration Threshold
Fewer than 202
20-794
80 and greater8

  第二個(gè)是,如果沒(méi)有在document中包含_id,那么mongodb會(huì)自動(dòng)添加這個(gè)字段,其value是一個(gè)ObjectId,ObjectId由一下部分組成:

  • a 4-byte value representing the seconds since the Unix epoch,

  • a 3-byte machine identifier,

  • a 2-byte process id, and

  • a 3-byte counter, starting with a random value.

  因此,在沒(méi)有使用hash sharding key(默認(rèn)是ranged sharding key)的情況下,在短時(shí)間內(nèi)插入大量沒(méi)有攜帶_id字段的ducoment,會(huì)插入到同一個(gè)shard,這也有利于出現(xiàn)chunk分裂和遷移的情況。

準(zhǔn)備

   首先,得知道chunk遷移的時(shí)候日志是什么樣子的,因此我用python腳本插入了一些記錄,通過(guò)sh.status()發(fā)現(xiàn)有chunk分裂、遷移的時(shí)候去查看mongodb日志,在rs1(sharded_col這個(gè)集合的primary shard)的primary(rs1_1.log)里面發(fā)現(xiàn)了如下的輸出:

   

1
2
3
4
5
6
7
8
9
10
11
12
13
34 2017-07-06T21:43:21.629+0800 I NETWORK  [conn6] starting new replica set monitor for replica set rs2 with seeds 127.0.0.1:27021,127.0.0.1:27022
48 2017-07-06T21:43:23.685+0800 I SHARDING [conn6] moveChunk data transfer progress: { active: true, ns: "test_db.sharded_col", from: "rs1/127.0.0.1:27018,127.0.0.1:27019"    , min: { _id: ObjectId('595e3e74d71ffd5c7be8c8b7') }, max: { _id: MaxKey }, shardKeyPattern: { _id: 1.0 }, state: "steady", counts: { cloned: 1, clonedBytes: 83944, cat    chup: 0, steady: 0 }, ok: 1.0, $gleStats: { lastOpTime: Timestamp 0|0, electionId: ObjectId('595e3b0ff70a0e5c3d75d684') } } my mem used: 0
52 -017-07-06T21:43:23.977+0800 I SHARDING [conn6] moveChunk migrate commit accepted by TO-shard: { active: false, ns: "test_db.sharded_col", from: "rs1/127.0.0.1:27018,12    7.0.0.1:27019", min: { _id: ObjectId('595e3e74d71ffd5c7be8c8b7') }, max: { _id: MaxKey }, shardKeyPattern: { _id: 1.0 }, state: "done", counts: { cloned: 1, clonedBytes    : 83944, catchup: 0, steady: 0 }, ok: 1.0, $gleStats: { lastOpTime: Timestamp 0|0, electionId: ObjectId('595e3b0ff70a0e5c3d75d684') } }
53 w017-07-06T21:43:23.977+0800 I SHARDING [conn6] moveChunk updating self version to: 3|1||590a8d4cd2575f23f5d0c9f3 through { _id: ObjectId('5937e11f48e2c04f793b1242') }     -> { _id: ObjectId('595b829fd71ffd546f9e5b05') } for collection 'test_db.sharded_col'
54 2017-07-06T21:43:23.977+0800 I NETWORK  [conn6] SyncClusterConnection connecting to [127.0.0.1:40000]
55 2017-07-06T21:43:23.978+0800 I NETWORK  [conn6] SyncClusterConnection connecting to [127.0.0.1:40001]
56 2017-07-06T21:43:23.978+0800 I NETWORK  [conn6] SyncClusterConnection connecting to [127.0.0.1:40002]
57 2017-07-06T21:43:24.413+0800 I SHARDING [conn6] about to log metadata event: { _id: "xxx-2017-07-06T13:43:24-595e3e7c0db0d72b7244e620", server: "xxx", clientAddr: "127.0.0.1:52312", time: new Date(1499348604413), what: "moveChunk.commit", ns: "test_db.sharded_col", details: { min: { _id: ObjectId(    '595e3e74d71ffd5c7be8c8b7') }, max: { _id: MaxKey }, from: "rs1", to: "rs2", cloned: 1, clonedBytes: 83944, catchup: 0, steady: 0 } }
58 2017-07-06T21:43:24.417+0800 I SHARDING [conn6] MigrateFromStatus::done About to acquire global lock to exit critical section
59 2017-07-06T21:43:24.417+0800 I SHARDING [conn6] forking for cleanup of chunk data
60 2017-07-06T21:43:24.417+0800 I SHARDING [conn6] MigrateFromStatus::done About to acquire global lock to exit critical section
61 2017-07-06T21:43:24.417+0800 I SHARDING [RangeDeleter] Deleter starting delete for: test_db.sharded_col from { _id: ObjectId('595e3e74d71ffd5c7be8c8b7') } -> { _id: MaxKey }, with opId: 6
62 2017-07-06T21:43:24.417+0800 I SHARDING [RangeDeleter] rangeDeleter deleted 1 documents for test_db.sharded_col from { _id: ObjectId('595e3e74d71ffd5c7be8c8b7') } -> { _id: MaxKey }

  上面第59行,“forking for cleanup of chunk data”,看起來(lái)是準(zhǔn)備刪除舊的數(shù)據(jù)了

  于是我寫(xiě)了一個(gè)shell腳本: 在rs1_1.log日志中出現(xiàn)“forking for cleanup of chunk data”時(shí)kill掉rs1_1這個(gè)進(jìn)程,腳本如下:

  

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開(kāi)發(fā),動(dòng)畫(huà)培訓(xùn)

check_loop()
{    echo 'checking'
    ret=`grep -c 'forking for cleanup of chunk data' /home/mongo_db/log/rs1_1.log`    if [ $ret -gt 0 ]; then
         echo "will kill rs1 primary"
         kill -s 9 `ps aux | grep rs1_1 | awk '{print $2}'`
         exit 0
    fi

    ret=`grep -c 'forking for cleanup of chunk data' /home/mongo_db/log/rs2_1.log`    if [ $ret -gt 0 ]; then
         echo "will kill rs2 primary"
         kill -s 9 `ps aux | grep rs2_1 | awk '{print $2}'`
         exit 0
    fi

    sleep 0.1
    check_loop
}
check_loop

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開(kāi)發(fā),動(dòng)畫(huà)培訓(xùn)

 

第一次嘗試

     第一次嘗試就是使用的上面的腳本。

  首先運(yùn)行上面的shell腳本,然后另起一個(gè)終端開(kāi)始插入數(shù)據(jù),在shell腳本kill掉進(jìn)程之后,立即登上rs1和rs2查看統(tǒng)計(jì)數(shù)據(jù),發(fā)現(xiàn)并沒(méi)有產(chǎn)生orphaned document(怎么檢測(cè)看第二次嘗試)

  再回看前面的日志,幾乎是出現(xiàn)“forking for cleanup of chunk data”的同一時(shí)刻就出現(xiàn)了“rangeDeleter deleted 1 documents for test_db.sharded_col from”,后者表明數(shù)據(jù)已經(jīng)被刪除。而shell腳本0.1s才檢查一次,很可能在遷移過(guò)程已經(jīng)完成之后才發(fā)出kill信號(hào)。于是將kill的時(shí)機(jī)提前,在shell腳本中檢查“moveChunk migrate commit accepted”(上述文檔中的第52行)

  對(duì)shell腳本的修改也很簡(jiǎn)單,替換一下grep的內(nèi)容:

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開(kāi)發(fā),動(dòng)畫(huà)培訓(xùn) View Code

 

第二次嘗試

    在進(jìn)行第二次嘗試之前,清空了sharded_col中的記錄,一遍更快產(chǎn)生chunk遷移。

    重復(fù)之間的步驟:?jiǎn)?dòng)shell腳本,然后插入數(shù)據(jù),等待shell腳本kill掉進(jìn)程后終止

    很快,shell腳本就終止了,通過(guò)ps aux | grep mongo 也證實(shí)了rs1_1被kill掉了,登錄到mongos(mongo --port 27017)

  mongos> db.sharded_col.find().count()

  4

 

  再登錄到rs1 primary(此時(shí)由于rs1原來(lái)的primary被kill掉,新的primary是rs1-2,端口號(hào)27019)

  rs1:PRIMARY> db.sharded_col.find({}, {'_id': 1})

  { "_id" : ObjectId("595ef413d71ffd4a82dea30d") }

  { "_id" : ObjectId("595ef413d71ffd4a82dea30e") }

  { "_id" : ObjectId("595ef413d71ffd4a82dea30f") }

 

  再登錄到rs2 primary

  rs2:PRIMARY> db.sharded_col.find({}, {'_id': 1})

  { "_id" : ObjectId("595ef413d71ffd4a82dea30f") }

 

  很明顯,產(chǎn)生了orphaned docuemnt,ObjectId("595ef413d71ffd4a82dea30f") 這條記錄存在于兩個(gè)shard上,那么mongodb sharded cluter認(rèn)為這條記錄應(yīng)該存在于哪個(gè)shard上呢,簡(jiǎn)單的辦法直接用sh.status()查看,不過(guò)這里忘了截圖。另外一種方式,給這條記錄新加一個(gè)字段,然后分別在兩個(gè)shard上查詢(xún)

  mongos> db.sharded_col.update({'_id': ObjectId("595ef413d71ffd4a82dea30f") }, {$set:{'newattr': 10}})

  WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

 

  rs1:PRIMARY> db.sharded_col.find({'_id': ObjectId("595ef413d71ffd4a82dea30f")}, {'newattr': 1})

  { "_id" : ObjectId("595ef413d71ffd4a82dea30f"), "newattr" : 10 }

 

  rs2:PRIMARY> db.sharded_col.find({'_id': ObjectId("595ef413d71ffd4a82dea30f")}, {'newattr': 1})

  { "_id" : ObjectId("595ef413d71ffd4a82dea30f") }

  

  這證實(shí)了這條記錄在rs1這個(gè)shard上

 

  此時(shí),重新啟動(dòng)rs1_1, 通過(guò)在rs.status()查看rs1這個(gè)shard正常之后,重新查看sh.status(),發(fā)現(xiàn)結(jié)果還是一樣的。據(jù)此推斷,并沒(méi)有journal信息恢復(fù)被終止的遷移過(guò)程。

     因此在本次實(shí)驗(yàn)中,ObjectId("595ef413d71ffd4a82dea30f")這條記錄本來(lái)是要從rs遷移到rs2,由于人為kill掉了rs1的primary,導(dǎo)致遷移只進(jìn)行了一部分,產(chǎn)生了orphaned document?;仡櫱懊嫣岬降倪w移過(guò)程,本次實(shí)驗(yàn)中對(duì)rs1 primary的kill發(fā)生在第6步之前(目標(biāo)shard在config server上更新metadata之前)

使用cleanupOrphaned

回到頂部

   orphaned document的影響在于某些查詢(xún)會(huì)多出一些記錄:多出這些孤兒文檔,比如前面的count操作,事實(shí)上只有3條記錄,但返回的是4條記錄。如果查詢(xún)的時(shí)候沒(méi)用使用sharding key(這里的_id)精確匹配,也會(huì)返回多余的記錄,另外,即使使用了sharding key,但是如果使用了$in,或者范圍查詢(xún),都可能出錯(cuò)。比如: 

  mongos> db.sharded_col.find({'_id': {$in:[ ObjectId("595ef413d71ffd4a82dea30f")]}}).count()

  1

  mongos> db.sharded_col.find({'_id': {$in:[ ObjectId("595ef413d71ffd4a82dea30f"), ObjectId("595ef413d71ffd4a82dea30d")]}}).count()

  3

  上面第二條查詢(xún)語(yǔ)句,使用了$in,理論上應(yīng)該返回兩條記錄,單因?yàn)楣聝何臋n,返回了三條。

  本質(zhì)上,如果一個(gè)操作要路由到多個(gè)shard,存在orphaned document的情況下都可能出錯(cuò)。這讓?xiě)?yīng)用開(kāi)發(fā)人員防不勝防,也不可能在邏輯里面兼容孤兒文檔這種異常情況。

  MongoDB提供了解決這個(gè)問(wèn)題的終極武器,cleanupOrphaned,那我們來(lái)試試看,按照官方的文檔(remove-all-orphaned-documents-from-a-shard),刪除所有的orphaned document。注意,cleanupOrphaned要在shard的primary上執(zhí)行;

 

 

 

 

  Run cleanupOrphaned in the admin database directly o

本文版權(quán)歸作者xybaby(博http://www.cnblogs.com/xybaby/p/7131776.html文地址:http://www.cnblogs.com/xybaby/)所有,歡迎轉(zhuǎn)載和商用,請(qǐng)?jiān)谖恼马?yè)面明顯位置給出原文鏈接并保留此段聲明,否則保留追究法律責(zé)任的權(quán)利,其他事項(xiàng),可留言咨詢(xún)。

http://www.cnblogs.com/xybaby/p/7131776.html