讀古今文學網 > MongoDB實戰 > 8.4 驅動與複製 >

8.4 驅動與複製

如果正在構建應用程序,並且使用了MongoDB的複製功能,那麼你需要瞭解三個特定於應用的話題。第一個主題與連接和故障轉移有關;隨後是寫關注允許你決定在應用程序繼續下一步之前寫操作的複製程度;最後是讀擴展,允許應用程序將讀請求分佈在多個副本之間。我會依次討論這些話題。

8.4.1 連接與故障轉移

MongoDB的驅動提供了一套相對統一的界面來連接副本集。

1. 單節點連接

你總是可以連接到副本集裡的單個節點上。連接到副本集的主節點和連接到普通的單機節點(正如我們全書中的例子那樣)沒有什麼區別。這兩種情況下,驅動都會初始化一個TCP套接字連接,運行isMaster命令。這條命令會返回如下文檔:

{ \"ismaster\" : true, \"maxBsonObjectSize\" : 16777216, \"ok\" : 1 }
  

對於驅動而言,最重要的是該節點的isMaster字段是設置為true的,這表明指定節點可以是單機、主從複製裡的主節點或者副本集的主節點。1在所有這些情況裡,節點都能寫入,驅動的用戶能執行各種CRUD操作。

1. isMaster命令還會返回該版本服務器的最大BSON對像大小。隨後,驅動會在插入BSON對像前驗證所有這些對象是否滿足此限制。

但在直接連接到副本集的從節點時,必須標明你知道自己正在連接從節點(至少對大多數驅動而言需要如此)。在Ruby驅動裡,你可以帶上:slave_ok參數。於是,直接連接本章之前創建的第一個從節點的Ruby代碼是這樣的:

@con = Mongo::Connection.new(\'arete\', 40001, :slave_ok => true)
  

沒有:slave_ok參數,驅動會拋出一個異常,指出無法連接到主節點。這個檢查是為了避免無意中向從節點進行寫操作。雖然這種寫操作會被服務器拒絕,但你看不到任何異常,除非使用安全模式進行操作。

MongoDB假設你通常都會連接主節點;:slave_ok參數可以用來作為一道強制的健康檢查。

2. 副本集連接

雖然你能單獨連接副本集的各個成員,但一般都會希望連接整個副本集。這能讓驅動確定哪個節點是主節點,並在故障轉移時重新連接新的主節點。

大多數官方支持的驅動都提供了連接副本集的方法。在Ruby驅動裡,可以創建一個ReplSetConnection實例,傳入種子節點(seed node)列表:

Mongo::ReplSetConnection.new([\'arete\', 40000], [\'arete\', 40001])
  

驅動內部會嘗試連接各個種子節點,並調用isMaster命令,該命令會返回一些重要的集合細節:

> db.isMaster
{
  \"setName\" : \"myapp\",
  \"ismaster\" : true,
  \"secondary\" : false,
  \"hosts\" : [
    \"arete:40000\",
    \"arete:40001\"
  ],
  \"arbiters\" : [
    \"arete:40002\"
  ],
  \"maxBsonObjectSize\" : 16777216,
  \"ok\" : 1
}
  

一旦某個種子節點返回如上信息,驅動就拿到它需要的所有信息了。現在它能連接主節點,再次驗證該成員依然是主節點,然後允許用戶通過該節點進行讀寫操作。響應對像還允許驅動緩存剩餘的從節點和仲裁節點的地址。如果主節點上的操作失敗,那麼後續的請求中,驅動都會嘗試連接剩餘的某個節點,直到它能重新連上主節點。

請牢記一點,雖然副本集的故障轉移是自動的,但驅動不會隱藏發生故障這一事實。處理過程大致是這樣的:首先,主節點發生故障或者發生了新的選舉。後續的請求會顯示套接字連接已斷開,驅動就拋出一個連接異常,關閉那些打開的連接數據庫的套接字。隨後由應用程序開發者來決定該怎麼辦,這一決定依賴於要執行的操作和應用程序的特定需求。

請記住,在處理後續請求時,驅動會自動嘗試重新連接,讓我們想像幾個場景。首先,假設你只向數據庫發送讀請求。在這種情況下,重試失敗的讀操作不會產生危害,因為它不會改變數據庫的狀態。但是,再假設通常還會向數據庫發送寫請求。之前提到過多次,無論是否開啟安全模式,你都能寫數據庫。在安全模式下,驅動在每次寫入後會追加一次getlasterror命令調用,這能確保寫操作已安全到達並向應用程序報告各種服務器錯誤。不使用安全模式時,驅動只是簡單地向TCP套接字做寫操作。

如果應用在沒有使用安全模式時執行寫入並發生故障轉移,就會產生不確定的狀態。最近向服務器做了多少寫操作?有多少是丟失在套接字緩存裡的?向TCP套接字做寫操作的不確定性讓你無法回答這些問題。這個問題有多嚴重取決於應用程序。對日誌而言,不安全的寫入也許是可接受的,因為丟失幾條日誌不會影響日誌的全貌;但對於用戶創建的數據,這就是一場災難。

開啟安全模式後,只有最後一次的寫操作會有問題;可能它已經到服務器了,也可能沒有。有時可能會重試,也可能會拋出一個應用程序錯誤。驅動始終會拋出一個異常;然後,開發者能夠決定如何處理這些異常。

不管什麼情況,重試一個操作都會讓驅動嘗試重新連接副本集。由於不同的驅動在副本集的連接行為上稍有不同,你應該查看驅動的文檔瞭解詳細信息。

8.4.2 寫關注

現在情況已經很明朗了,默認運行安全模式對於大多數應用程序都是合理的,因為能夠知道寫操作正確無誤地到達主節點是很重要的。但人們通常都會希望有更高級別的保證,寫關注就能做到這點,它允許開發者指定應用程序執行後續操作前寫操作應該被複製的範圍。嚴格說來,你是通過getlastError命令的兩個參數來控制寫關注的:wwtimeout

第一個參數w,接受的值通常都是最近的寫操作應該被複製到的服務器的總數;第二個參數是超時,如果寫操作在指定毫秒內無法複製,該命令就會返回一個錯誤。

例如,如果你希望寫操作至少要複製到一台服務器上,可以將w指定為2。如果希望在500 ms內無法完成該複製就超時,可以將wtimeout指定為500。請注意,如果不指定wtimeout的值,而複製又出於某些原因一直沒有發生,那麼該操作會一直阻塞下去。

在使用驅動時,不是通過顯式調用getLastError開啟寫關注的,而是創建一個寫關注對象,或者設置合適的安全模式選項;這依賴於特定驅動的API。2在Ruby裡可以像這樣為一個操作設置寫關註:

2. 附錄D中包含在Java、PHP和C++裡設置寫關注的例子。

@collection.insert(doc, :safe => {:w => 2, :wtimeout => 200})
  

有時,你只是想確保寫操作被複製到了大部分可用節點上,這時可以簡單地將w值設置為majority

@collection.insert(doc, :safe => {:w => \"majority\"})
  

還有更高級的選項。舉例來說,如果已經開啟了Journaling日誌,還可以通過j選項強制讓Journaling日誌同步到磁盤上:

@collection.insert(doc, :safe => {:w => 2, :j => true})
  

很多驅動還支持為指定連接或數據庫設置寫關注的默認值。要瞭解如何在具體場景中設置寫關注,請查看所用驅動的文檔。附錄D中能找到更多語言的例子。

寫關注既能用於副本集,也能用於主從複製。如果查看local數據庫,你會看到兩個集合,從節點上的me和主節點上的slaves,它們就是用來實現寫關注的。每當從節點從主節點同步數據時,主節點都會在slaves集合裡記錄下應用到從節點上的最新oplog條目。因此,主節點總是能知道每個從節點複製了什麼東西,可以準確地響應帶getlastError命令的寫請求。

請記住,使用寫關注時w值大於1會引入額外的延時。可配置的寫關注讓你能夠在速度和持久性之間做出權衡。如果使用了Journaling日誌,那麼w等於1就已經能滿足大多數應用程序的需要了。另一方面,對於日誌或分析型的應用程序,你可能會選擇同時禁用Journaling日誌和寫關注,僅依靠複製來保證持久性,這在發生故障時可能會丟失一些寫入的數據。請仔細考慮這些因素,在設計應用程序時測試不同的場景。

8.4.3 讀擴展

經複製的數據庫能很好地適用於讀擴展。如果單台服務器無法承擔應用程序的讀負載,那麼可以將查詢路由到更多的副本上。大多數驅動都內置了將查詢發送到從節點的功能。在Ruby驅動中,ReplSetConnection構造方法的一個選項就提供了對該功能的支持:

Mongo::ReplSetConnection.new([\'arete\', 40000],
          [\'arete\', 40001], :read => :secondary )
  

:read參數被設置為:secondary時,連接對像會隨機選擇一個附近的從節點讀取數據。

其他驅動可以通過設置slaveOk選項進行配置,讀取從節點數據。當使用Java驅動連接副本集時,將slaveOk設置為true將以每個線程為基礎,開啟從節點的負載均衡。驅動中的負載均衡實現是為普通應用設計的,因此可能無法適用於所有應用。遇到這種情況時,用戶通常會定制自己的負載均衡實現。同樣的,請查看你的驅動文檔瞭解更多細節。

很多MongoDB用戶在生產環境中通過複製進行擴展。但是,有三種情況複製無法應對。第一種情況與所需的服務器數量有關,自MongoDB v2.0起,副本集最多支持12個成員,其中7個可以投票。如果需要更多副本來做擴展,可以使用主從複製。但如果既不想犧牲自動故障轉移,又要超過副本集的成員上限,那就需要遷移到分片集群上了。

第二種情況涉及那些寫負載較高的應用程序。正如本章開篇時所說的那樣,從節點必須跟上這個寫負載。向那些滿負荷做寫操作的從節點發送讀請求可能會妨礙複製。

第三種副本擴展無法處理的情況是一致性讀。因為複製是異步的,副本無法始終反映主節點最新的寫操作。因此,如果應用程序任意地從多個從節點讀取數據,那麼呈現給最終用戶的內容不能始終保證是完全一致的。對於那些主要用來顯示內容的應用程序而言,這幾乎從來都不是問題。但對於其他應用而言,用戶是在主動操作數據,這就要求一致性讀。在這些情況下,你有兩個選擇。第一是將那些需要一致性讀的應用程序部分從那些不需要的部分裡分離出來。前者總是從主節點讀取數據,後者可以從多個從節點讀取數據。當這種策略太複雜或者無法擴展時,就該採取分片策略。3

3. 注意,要從分片集群中獲得一致性讀,必須始終讀取每個分片的主節點,而且必須發起安全寫操作。

8.4.4 標籤

如果正在使用寫關注或者讀擴展,你可能會想要更細粒度地進行控制,控制哪個從節點接收寫或讀請求。例如,假設部署了一個五節點副本集,跨兩個數據中心:NY和FR。主數據中心NY包含三個節點,從數據中心FR包含剩下的兩個節點。假設希望通過寫關注阻塞請求,直到寫操作被複製到數據中心FR的至少一個節點上。以目前你所瞭解的寫關注知識來看,沒有什麼好辦法實現這一需求。w值為majority是沒用的,因為這會被翻譯成值3,最可能的情況是NY裡的三個節點先發出響應。也可以將值設置為4,但如果每個數據中心各損失一個節點,那這種方法也會有問題。

副本集標籤可以解決這個問題,它允許針對帶有特定標籤的副本集成員定義特殊的寫關注模式。要知道這是如何實現的,先要瞭解如何為副本集成員打標籤。在配置文檔裡,每個成員都有一個名為tags的鍵指向一個包含鍵值對的對象。下面就是一個例子:

{
  \"_id\" : \"myapp\",
  \"version\" : 1,
  \"members\" : [
    {
      \"_id\" : 0,
      \"host\" : \"ny1.myapp.com:30000\",
      \"tags\": { \"dc\": \"NY\", \"rackNY\": \"A\" }
    },
    {
      \"_id\" : 1,
      \"host\" : \"ny2.myapp.com:30000\",
      \"tags\": { \"dc\": \"NY\", \"rackNY\": \"A\" }
    },
    {
      \"_id\" : 2,
      \"host\" : \"ny3.myapp.com:30000\",
      \"tags\": { \"dc\": \"NY\", \"rackNY\": \"B\" }
    },
    {
      \"_id\" : 3,
      \"host\" : \"fr1.myapp.com:30000\",
      \"tags\": { \"dc\": \"FR\", \"rackFR\": \"A\" }
    },
    {
      \"_id\" : 4,
      \"host\" : \"fr2.myapp.com:30000\",
      \"tags\": { \"dc\": \"FR\", \"rackFR\": \"B\" }
    }
  ],
  settings: {
    getLastErrorModes: {
      multiDC: { dc :2}},
      multiRack: { rackNY:2}},
    }
  }
}
  

這個帶標籤的配置文檔適用於之前假設的跨兩個數據中心的副本集。請注意,每個成員的標籤文檔有兩個鍵值對:第一個標識了數據中心,第二個是指定節點服務器所在機架的名稱。請記住,這裡使用的名稱是完全任意的,而且僅在本應用程序的上下文中有意義;你可以在標籤文檔中放置任何東西。重要的是如何使用它。

這時getLastErrorModes該登場了。它們允許為getLastError命令定義模式,這些模式實現了特殊的寫關注要求。在本例中,你定義了兩個模式,第一個是multiDC,定義為{\"dc\":2},表示寫操作應該複製到至少有兩個不同dc值的節點上。如果這時檢查標籤,你會看到它能確保寫操作已經傳播到了兩個數據中心。第二個模式規定了至少要有兩個NY的機架接收到了寫操作。這同樣也能通過標籤加以實現。

一般來說,一個getLastErrorModes條目包含一個文檔,其中有一或多個鍵(本例中是dcrackNY),它們的值是整數。這些整數表示某個鍵的不同標籤值數量,在getLastError命令成功完成時必須滿足這些值。一旦定義好了這些模式,就能在應用程序裡將其用作w的值。例如,在Ruby中使用第一個模式,如下:

@collection.insert(doc, :safe => {:w => \"multiDC\"})
  

除了能讓寫關注更加精細,標籤還能提供更粒度化的控制,決定哪個副本用於讀擴展。可惜在本書編寫時,針對標籤進行讀操作的語義尚未定義或實現在官方MongoDB驅動裡。要瞭解最新進展,請查看Ruby驅動的JIRA問題單,參見https://jira.mongodb.org/browse/RUBY-326。