MongoDB副本集學習(三):效能和優化相關

weixin_34402090發表於2013-08-19

Read Preferences/讀寫分離

有時候為了考慮應用程式的效能或響應性,為了提高讀取操作的吞吐率,一個常見的措施就是進行讀寫分離,MongoDB副本集對讀寫分離的支援是通過Read Preferences特性進行支援的,這個特性非常複雜和靈活。以下幾種應用場景可能會考慮對副本集進行讀寫分離:

1)操作不影響前端應用程式,比如備份或者報表;

2)在一個物理上分佈的副本叢集中,為了減少應用程式的延遲,可能會優先選擇離應用程式更近的secondary節點而不是遠在千里之外機房的主節點;

3)故障發生時候能夠提供一個優雅的降級。副本集primary節點當機後再選出新的primary節點這段時間內(10秒或更長時間)能夠依然響應客戶端應用的讀請求;

4)應用能夠容忍一定程度的資料不一致性。

 

Read References:

應用程式驅動通過read reference來設定如何對副本集進行讀取操作,預設的,客戶端驅動所有的讀操作都是直接訪問primary節點的,從而保證了資料的嚴格一致性。

但有時為了緩解主節點的壓力,我們可能需要直接從secondary節點讀取,只需要保證最終一致性就可以了。

MongoDB 2.0之後支援五種的read preference模式:

 

primary:預設,只從主節點上進行讀取操作;

primaryPreferred:在絕大部分的情形都是從主節點上讀取資料的,只有當主節點不可用的時候,比如在進行failover的10秒或更長的時間內會從secondary節點讀取資料。

警告:2.2版本之前的MongoDB對Read Preference支援的還不完全,如果客戶端驅動採用primaryPreferred實際上讀取操作都會被路由到secondary節點。

secondary:只從secondary節點上進行讀取操作,存在的問題是secondary節點的資料會比primary節點資料“舊”。

secondaryPreferred:優先從secondary節點進行讀取操作;

nearest:既有可能從primary,也有可能從secondary節點讀取,這個決策是通過一個叫member selection過程處理的。

 

MongoDB允許在不同的粒度上指定這些模式:連線、資料庫、集合甚至單次的操作。不同語言的驅動基本都支援這些粒度。

 

OpLog

oplog是一種特殊的capped collection,用來滾動的儲存MongoDB中所有資料操作的日誌。副本集中secondary節點非同步的從primary節點同步oplog然後重新執行它記錄的操作,以此達到了資料同步的作用。這就要求oplog必須是冪等的,也就是重複執行相同的oplog記錄得到的資料結構必須是相同的。

事實上副本集中所有節點之間都相互進行heartbeat來維持聯絡,任何節點都能從其它節點複製oplog。

capped collection是MongoDB中一種提供高效能插入、讀取和刪除操作的固定大小集合。當集合被填滿的時候,新的插入的文件會覆蓋老的文件。因為oplog是capped collection所以指定它的大小非常重要。如果太小那麼老的文件很快就被覆蓋了,那麼當機的節點就很容易出現無法同步資料的結果,但也不是越大越好,MongoDB在初始化副本集的時候都會有一個預設的oplog大小:

  • 在64位的Linux,Solaris,FreeBSD以及Windows系統上,MongoDB會分配磁碟剩餘空間的5%作為oplog的大小,如果這部分小於1GB則分配1GB的空間。
  • 在64的OS X系統上會分配183MB。
  • 在32位的系統上則只分配48MB。

首先生產環境使用MongoDB毫無疑問必須的是64為作業系統。其次大多數情況下預設的大小是比較適合的。舉個例子,如果oplog大小為空閒磁碟的5%,它在24H內能被填滿,也就是說secondary節點可以停止複製oplog達24H後仍然能夠catch up上primary節點。而且通常的MongoDB副本集的操作量要比這低得多。

 

oplog資料結構

oplog的資料結構如下所示:

{ ts : ..., op: ..., ns: ..., o: ... o2: ...  }

  • ts: 8位元組的時間戳,由4位元組unix timestamp + 4位元組自增計數表示。這個值很重要,在選舉(如master當機時)新primary時,會選擇ts最大的那個secondary作為新primary。
  • op:1位元組的操作型別,例如i表示insert,d表示delete。
  • ns:操作所在的namespace。
  • o:操作所對應的document,即當前操作的內容(比如更新操作時要更新的的欄位和值)
  • o2: 在執行更新操作時的where條件,僅限於update時才有該屬性

其中op有以下幾個值:

  • "i": insert
  • "u": update
  • "d": delete
  • "c": db cmd
  • "db":宣告當前資料庫 (其中ns 被設定成為=>資料庫名稱+ '.')
  • "n":  no op,即空操作,其會定期執行以確保時效性

:關於oplog有兩個常見的錯誤timestamp error和duplicate error,參看這裡:http://docs.mongodb.org/manual/tutorial/troubleshoot-replica-sets/#replica-set-troubleshooting-check-oplog-size

 

檢視oplog大小

通過db.printReplicationInfo() 可以檢視副本集節點的oplog狀態:

1.    rs0:PRIMARY> db.printReplicationInfo() 
2.    configured oplog size:   1793.209765625MB
3.    log length start to end: 12.643999999854714secs (0hrs)
4.    oplog first event time:  Sat Jan 17 1970 06:22:38 GMT+0800 (CST)
5.    oplog last event time:   Sat Jan 17 1970 06:22:51 GMT+0800 (CST)
6.    now:                     Sat Aug 17 2013 18:02:12 GMT+0800 (CST)

以我之前搭建的副本集為例,oplog的大小是1793MB,其中持有的資料時間區間只有12秒。

修改oplog大小

可以在啟動mongod的時候指定--oplogSize,單位MB:

7.    ./bin/mongod --fork --dbpath data/rs0-0/ --logpath log/rs0-0/rs0-0.log --rest --replSet rs0 --oplogSize 500 --port 37017

但有的時候我們可能需要修改現有副本集的oplog大小。這個本人非常不推薦,官網有詳細的教程,這裡我就不贅述了,可以看這裡:http://docs.mongodb.org/manual/tutorial/change-oplog-size/

在現有的副本集中修改oplog的大小是相當麻煩的而且影響副本集效能,因此我們最好是預先根據應用的情況評估好oplog的大小:如果應用程式是讀多寫少,那麼預設的大小已經足夠了。如果你的應用下面幾種場景很多可能考慮需要更大的oplog:

  • 在同一個時刻更新多個文件:oplog為了維持冪等性必須將mutil-updates翻譯成一個個獨立的操作,這會用去大量的oplog空間,但資料庫中的資料量卻沒有相對稱的增加。
  • 多文件同時更新從1.1.3就有的特性,在mongo shell執行類似如下的命令,第四個引數必須制定為true:
  • db.test.update({foo: "bar"}, {$set: {test: "success!"}}, false, true);
  • 在插入時同時刪除相同大小資料:和上面的結果一樣在資料量沒有增加的情況下卻消耗了大量的oplog空間。
  • 大量的In-Place更新操作:In-Place更新是指更新文件中原有的部分,但並不增加文件的大小。

上面三點總結起來就是消耗了大量的oplog但是資料量卻沒有等量的增加

 

資料同步

資料滯後:

前面已經提到MongoDB副本集中secondary節點是通過oplog來同步primary節點資料的,那具體的細節是怎麼樣的?在說資料如何同步之間先介紹一下replication lag,因為存在資料同步那必然存在一定程度的落後。這個問題對於整個MongoDB副本集的部署是至關重要的。

1.    rs0:PRIMARY> db.printSlaveReplicationInfo()
2.    source:   192.168.129.129:37019
3.         syncedTo: Thu Aug 15 2013 20:59:45 GMT+0800 (CST)
4.             = 172971 secs ago (48.05hrs)
5.    source:   192.168.129.129:37020
6.         syncedTo: Thu Jan 01 1970 08:00:00 GMT+0800 (CST)
7.             = 1376744556 secs ago (382429.04hrs)

當前叢集的狀況是,37017埠是primary節點,37019和37020是secondary節點,其中37020已經當機,可以看到37019同步資料是在兩天前(因為這兩天我沒有對副本集有任何資料操作),而當機的節點顯示的同步時間是一個很早時間點。

現在重新啟動37020後再執行命令:

1.    rs0:PRIMARY> db.printSlaveReplicationInfo()
2.    source:   192.168.129.129:37019
3.         syncedTo: Thu Aug 15 2013 20:59:45 GMT+0800 (CST)
4.             = 175566 secs ago (48.77hrs)
5.    source:   192.168.129.129:37020
6.         syncedTo: Thu Aug 15 2013 20:59:45 GMT+0800 (CST)
7.             = 175566 secs ago (48.77hrs)

可以看到兩個secondary節點的同步時間是一致的,我們向叢集中插入幾條資料後再執行db.printSlaveReplicationInfo():

1.    rs0:PRIMARY> db.test.insert({"name":"zhanjindong","age":23})
2.    rs0:PRIMARY> db.printSlaveReplicationInfo()
3.    source:   192.168.129.129:37019
4.         syncedTo: Sat Aug 17 2013 21:48:31 GMT+0800 (CST)
5.             = 6 secs ago (0hrs)
6.    source:   192.168.129.129:37020
7.         syncedTo: Sat Aug 17 2013 21:48:31 GMT+0800 (CST)
8.             = 6 secs ago (0hrs)

可以看到很快就引發了primary和secondary之間的資料同步操作。

“滯後”是不可避免的,需要做的就是儘可能減小這種滯後,主要涉及到以下幾點:

  • 網路延遲:這是所有分散式系統都存在的問題。我們能做的就是儘可能減小副本集節點之間的網路延遲。
  • 磁碟吞吐量:secondary節點上資料刷入磁碟的速度比primary節點上慢的話會導致secondary節點很難跟上primary節點的節奏。
  • 併發:併發大的情況下,primary節點上的一些耗時操作會阻塞secondary節點的複製操作,導致複製操作跟不上主節點的寫入負荷。解決方法是通過設定操作的write concern(參看這裡:http://docs.mongodb.org/manual/core/write-concern/#replica-set-write-concern)預設的副本集中寫入操作只關心primary節點,但是可以指定寫入操作同時傳播到其他secondary節點,代價就是嚴重影響叢集的併發性。
    • 注意:而且這裡還存在一個問題如果,如果寫入操作關心的某個節點當機了,那麼操作將會一直被阻塞直到節點恢復。
  • 適當的write concern:我們為了提高叢集寫操作的吞吐量經常會將writer concern設定為unacknowledged write concern,這導致primary節點的寫操作很快而secondary節點複製操作跟不上。解決方法和第三點是類似的就是在效能和一致性之間做權衡。

資料同步:

副本集中資料同步有兩個階段。

初始化(initial sync):這個過程發生在當副本集中建立一個新的資料庫或其中某個節點剛從當機中恢復,或者向副本集中新增新的成員的時候,預設的,副本集中的節點會從離它最近的節點複製oplog來同步資料,這個最近的節點可以是primary也可以是擁有最新oplog副本的secondary節點。這可以防止兩個secondary節點之間相互進行同步操作。

複製(replication):在初始化後這個操作會一直持續的進行著,以保持各個secondary節點之間的資料同步。

在MongoDB2.0以後的版本中,一旦初始化中確定了一個同步的目標節點後,只有當和同步節點之間的連線斷開或連線過程中產生異常才可能會導致同步目標的變動,並且具有就近原則。考慮兩種場景:

  • 1) 有兩個secondary節點在一個機房,primary在另外一個機房。假設幾乎在同一時間啟動這三個例項(之前都沒有資料和oplog),那麼兩個secondary節點應該都是從primary節點同步資料,因為他們之前見都不會擁有比對方更新的oplog。如果重啟其中一個secondary,那麼它的同步目標將會變成另一個secondary,因為就近原則。
  • 2) 如果有一個primary和一個secondary分別在不同的機房,那麼在之前secondary所在的機房中向副本集中新加一個節點時,那麼新節點必然是從原先的那個secondary節點同步資料的。

在2.2版本以後,資料同步增加了一些額外的行為:

  • 1) secondary節點只有當叢集中沒有其他選擇的時候才會從delayed節點同步資料;
  • 2) secondary節點絕不會從hidden節點同步資料;
  • 3) 當一個節點新加入副本集中會有一個recovering過程,在這段時間內secondary不會進行資料同步操作;
  • 4) 當一個節點從另一個節點同步資料的時候,需要保證兩個節點的local.system.replset.members[n].buildIndexes值是一樣的,要不都是false,要不都是true。

:buildIndexes指定副本集中成員是否可以建立索引(某些情況下比如沒有讀操作或者為了提高寫效能可以省略索引的建立)。當然即使該值為false,MongoDB還是可以在_id上建立索引以為複製操作服務。

重新資料同步

有時當secondary節點落後太多無法追趕上primary節點的時候,這時候可能需要考慮重新同步資料(Resync data)。

有兩種方法一種是指定一個空的目錄重新啟動落後的節點,這很簡單,但是資料量大的情況下回花費很長的時間。另一種方法是基於另一個節點的資料作為“種子”進行重新同步,關於這兩種方法在後面向一個現有副本集中新增成員一節會有詳細說明。

 

Elction

在以下幾種情景發生的時候,副本集通過“選舉”來決定副本集中的primary節點:

  • 當第一次初始化一個副本集的時候;
  • primary幾點steps down的時候,出現這種情況可能是因為執行了replSetStepDown命令,或者是叢集中出現了更適合當primary的節點,比如當primary節點和叢集中其他大部分節點無法通訊的時候,當primary steps down的時候,它會關閉所有客戶端的連線。
  • 當叢集中一個secondary節點無法和primary節點建立連線的時候也會導致一次election發生。
  • 一次failover。
  • 執行rs.conf()命令。

 

在一次選舉中包括hidden節點、仲裁者甚至正處於recovering狀態的節點都具有“投票權”。預設配置中所有參與選舉的節點具有相等的權利,當然在一些特定情況下,應明確的指定某些secondary會優先成為primary,比如一個遠在千里之外異地機房的節點就不應該成為primary節點,選舉的權重通過設定priority來調節,預設該值都是1,在前面簡單副本集的搭建中已經介紹過了如何修改該值。

叢集中任何一個節點都可以否決選舉,即使它是non-voting member:

  • 如果發起選舉的節點不具有選舉權(priority為0的成員);
  • 發起選舉的節點資料落後太多;
  • 發起選舉的節點的priority值比叢集中其他某一個節點的小;
  • 如果當前的primary節點比發起選舉的節點擁有更新或同等新的資料(也就“optime”值相等或更大)。
  • 當前的primary節點會否決,如果它擁有比發起選舉的節點更新或相同新的資料。

首先獲取最多選票的成員(實際上要超過半數)才會成為primary節點,這也說明了為什麼當有兩個節點的叢集中primary節點當機後,剩下的只能成為secondary,當primary宕掉,此時副本集只剩下一個secondary,它只有1票,不超過總節點數的半數,它不會選舉自己為primary。

 

要想更詳細的瞭解選舉細節,參看這篇原始碼分析的文章:http://nosql-db.com/topic/514e6d9505c3fa4d47017da6

 

索引

……

 

最近太忙,有時間再整理。

相關文章