讀古今文學網 > MongoDB實戰 > 10.1 部署 >

10.1 部署

要成功部署MongoDB,你需要選擇正確的硬件以及合適的服務器拓撲。如果有遺留數據,則需要知道如何才能有效地進行導入(和導出)。最後,還要確保你的部署是安全的。我們會在隨後的小節中討論這些問題。

10.1.1 部署環境

本節將介紹為MongoDB選擇好的部署環境所要考慮的內容。我將討論具體的硬件要求,例如CPU、內存和磁盤要求,為優化操作系統環境做些推薦,並提供一些關於雲端部署的建議。

1. 架構

下面依次是兩點與硬件架構有關的說明。

首先,因為MongoDB會將所有數據文件映射到一個虛擬的地址空間裡,所以全部的生產部署都應該運行在64位的機器上。正如其他部分提到的那樣,32位的架構會將MongoDB限制為僅有2 GB存儲。開啟了Journaling日誌,該限制會減小到大約1.5 GB。這在生產環境裡是很危險的,因為如果超過這個限制,MongoDB的行為將無法預測。你可以隨意使用32位機器進行單元測試和預發佈,但在生產環境以及負載測試時,請嚴格使用64位架構。

其次,MongoDB必須運行於小端序(little-endian)機器上。這一點通常不難做到,但運行SPARC、PowerPC、PA-RISC以及其他大端序架構的用戶就只能望洋興歎了。1大多數的驅動同時支持小端與大端字節序,因此MongoDB的客戶端通常在這兩種架構上都能運行。

1. 如果你對核心服務器的大端序支持感興趣,請訪問https://jira.mongodb.org/browse/SERVER-1625。

2. CPU

MongoDB並不是特別CPU密集型的;數據庫操作很少是CPU密集型的。在優化MongoDB時,首要任務是確保該操作不是I/O密集型的(詳見後續關於內存和磁盤的兩部分內容)。

只有當索引和工作集都完全可放入內存時,你才可能遇到CPU的瓶頸。如果有一個MongoDB實例每秒鐘處理成千上萬(或數百)的查詢,你能想到提供更多的CPU內核來提升性能。對於那些不使用JavaScript的讀請求,MongoDB能夠利用全部可用內核。

如果碰巧看到讀請求造成CPU飽和,請檢查日誌中的慢查詢警告。可能是缺少合適的索引,因此強制進行了表掃瞄。如果你開啟了很多客戶端,每個客戶端都在運行表掃瞄,那麼掃瞄加上它所帶來的上下文切換會造成CPU飽和。這個問題的解決方案是增加必要的索引。

對於寫請求,MongoDB一次只會用到一個核,這是由於全局寫鎖的緣故。因此擴展寫負載的唯一方法是確保寫操作不是I/O密集型的,並且用分片進行水平擴展。這個問題在MongoDB v2.0里有所好轉,因為通常寫操作不會在頁錯誤時持有鎖,而是允許完成其他操作。目前,有很多並發方面的優化正在開發之中,可能實現的幾個選項是集合級鎖(collection-level locking)和基於範圍的鎖(extent-based locking)。請查看JIRA和最新的發佈說明以瞭解這些改進的開發狀態。

3. 內存

和其他數據庫一樣,MongoDB在有大量內存時性能最好。請一定選擇有足夠內存的硬件(虛擬的或其他),足夠容納常用的索引和工作數據集。隨著數據的增長,密切關注內存與工作數據集的比例。如果你讓工作集大小超過內存,就可能看到明顯的性能下降。從磁盤載入分頁及分頁這個過程本身不是問題,因為它是將數據載入內存的必要步驟。但是如果你對性能不滿意,過多的分頁可能就是問題所在。第7章詳細討論了工作集、索引大小和內存之間的關係。在本章末尾處,你會瞭解到識別內存不足的方法。

有一些情況下,你能安全地放任數據尺寸超出可用內存,但這僅僅是些例外,並非常見情況。一個例子是使用MongoDB進行歸檔,讀和寫都很少發生,並且不需要快速做出應答。在這種情況下,擁有和數據量一樣的內存可能代價高昂卻收效甚微,因為應用程序用不到那麼多內存。對於完整的數據集,關鍵是測試。對應用程序的典型原型進行測試,確保能夠得到所需的性能基線。

4. 磁盤

在選擇磁盤時,你需要考慮IOPS(每秒的輸入/輸出操作數)以及尋道時間。這裡不得不強調運行於單塊消費級硬盤、雲端虛擬盤(比方說EBS)以及高性能SAN之間的區別。一些應用程序在單塊由網絡連接的EBS捲上的性能還能接受,但是一些開銷較大的應用程序就會有更高要求。

出於一些原因,磁盤性能是非常重要的。第一,在向MongoDB寫入時,服務器默認每60 s與磁盤強制同步一次。這稱為後台刷新(background flush)。對於寫密集型應用與低速磁盤,後台刷新可能會因為速度慢對整體系統性能產生負面影響。第二,高速磁盤能讓你更快地預熱服務器。當需要重啟服務器時,還要將數據集加載到內存裡。這個過程是延時執行的;每次對MongoDB連續的讀或寫都會將一個新的虛擬內存頁加載到內存裡,直到物理內存被放滿為止。高速磁盤能讓這個過程執行得更快一些,這會提升MongoDB在冷重啟之後的性能。最後,高速磁盤能改變應用程序工作集與所需內存的比例。比方說,相比其他磁盤,使用固態硬盤在運行時所需的內存更少(能有更大的容量)。

無論使用哪種類型的磁盤,通常在部署時都會比較嚴肅,不會只用一塊硬盤,而是採用冗余磁盤陣列(RAID)。用戶一般會使用Linux的LVM(Logical Volume Manager,邏輯卷管理器)來管理RAID集群,RAID級別2為10。RAID 10能在保持可接受性能的同時提供一定冗余,常用於MongoDB部署之中。

2. 如想對RAID級別有個概要認識,請訪問http://en.wikipedia.org/wiki/Standard_RAID_levels。

如果數據分散在同一台MongoDB服務器的多個數據庫之中,還可以通過服務器的--directoryperdb標誌確保它們的容量,這將在數據文件路徑中為每個數據庫創建單獨的目錄。有了它,你還可以為每個數據庫掛載單獨的卷(無論是否是RAID)。這能讓你充分發揮各磁盤的性能,因為你可以從不同的磁盤組(或固態硬盤)上讀取數據。

5. 文件系統

如果運行在正確的文件系統上,你將從MongoDB中獲得最好的性能。特別是ext4和xfs這兩個文件系統,提供了高速、連續的磁盤分配。使用這些文件系統能夠提升常見的MongoDB預分配的速度。

一旦掛載了高速文件系統,還可以通過禁止修改文件的最後訪問時間(atime)來提升性能。通常情況下,每次文件有讀寫時操作系統都會修改文件的atime。在數據庫環境中,這帶來了很多不必要的工作。在Linux上關閉atime相對比較容易。首先,備份文件系統的配置文件;然後,用你喜歡的編輯器打開原來的文件:

sudo mv /etc/fstab /etc/fstab.bak
sudo vim /etc/fstab
  

針對每個掛載的卷,你會看到一個列對齊的設置列表。在options列裡,添加noatime指令:

# file-system mount type options dump pass
UUID=8309beda-bf62-43 /ssd ext4 noatime 0 2
  

保存修改內容,新的設置應該能立刻生效。

6. 文件描述符

一些Linux系統最多能打開1024個文件描述符。有時,這個限制對MongoDB而言太低了,在打開連接時會引起錯誤(在日誌裡能清楚地看到這點)。MongoDB順理成章地要求每個打開的文件和網絡連接都有一個文件描述符。假設將數據文件保存在一個文件夾裡,其中有data這個單詞,可以通過lsof和一些管道看到數據文件描述符的數量:

lsof | grep mongo | grep data | wc -l
  

統計網絡連接描述符數量的方法也很簡單:

lsof | grep mongo | grep TCP | wc -l
  

對於文件描述符,最佳策略是一開始就設定一個很高的限額,使得在生產環境中永遠都不會達到該值。可以使用ulimit工具檢查當前的限額:

ulimit -Hn
  

要永久提升限額,可以用編輯器打開limits.conf

sudo vim /etc/security/limits.conf
  

然後,設置軟、硬限額,這些限額是基於每個用戶指定的。下面的例子假設mongodb用戶將運行mongod進程:

mongod hard nofile 2048
mongod hard nofile 10240
  

新設置將在用戶下次登錄時生效。

7. 時鐘

事實證明,複製容易受到時鐘偏移(clock skew)的影響。如果托管了副本集中多個節點的機器的時鐘之間存在分歧,副本集就可能無法正常運作了。這可不是理想狀態,幸好存在解決方案。你要確保每台服務器都使用了NTP(Network Time Protocol,網絡時間協議),借此保持服務器時鐘的同步。在Unix類的系統上,也就是運行ntpd守護進程;在Windows上,Windows Time Services就能擔當這個角色。

8. 雲

有越來越多的用戶在虛擬化環境中運行MongoDB,這些環境統稱為雲。其中,亞馬遜的EC2因其易用性、廣泛的地理分佈以及強有力的價格成為了用戶的首選。EC2及其他類似的環境都能部署MongoDB,但你也要牢記它們的缺點,尤其是在應用程序要將MongoDB推向其極限之時。

EC2的第一個問題是你只能選擇幾種有限的實例類型。在本書編寫時,還沒有超過68 GB內存的虛擬實例。此類約束強迫你在工作集超過68 GB時對數據庫進行分片,這並非適用於所有應用程序。如果能運行於真實的硬件之上,你可以擁有更多內存;相同的硬件成本下,這能影響分片的決定。

另一個潛在問題是EC2從本質上來說是一個黑盒,你可能會遭遇服務中斷或實例變慢,卻無法進行診斷或補救。

第三個問題與存儲有關。EC2允許你掛載虛擬塊設備,稱為EBS卷。EBS卷提供了很大的靈活性,允許你按需添加存儲並在機器間移動卷。它還能讓你製作快照,以便用於備份。EBS卷的問題在於無法提供很高的吞吐量,尤其是在和物理磁盤進行比較時。為此,大多數MongoDB用戶在EC2上托管重要應用程序時,都會對EBS做RAID 10,以此提升讀吞吐量。這對高性能應用程序而言是必不可少的。

出於這些原因,比起處理EC2的限制和不可預測性,很多用戶更青睞於在自己的物理硬件上運行MongoDB。但是,EC2和其他雲環境非常方便,為很多用戶所廣泛接受。在正式使用雲存儲之前,要慎重考慮應用程序的情況,並在雲中進行測試。

10.1.2 服務器配置

一旦決定了部署環境,你需要確定總體服務器配置。這涉及選擇服務器的拓撲和決定是否使用Journaling日誌,以及如何使用。

1. 選擇一種拓撲結構

最小的推薦部署拓撲是三個成員的副本集。其中至少有兩個是數據存儲(非仲裁)副本,位於不同的機器上,第三個成員可以是另一個副本,也可以是仲裁節點。仲裁節點無需自己的機器,舉例來說,你可以把它放在應用服務器上。第8章裡有兩套合理的副本集部署配置。

如果從一開始你就預計到工作集大小會超過內存,那開始時就可以使用分片集群了,其中至少包含兩個副本集。第9章中有分片部署的詳細推薦配置,還有關於何時開始分片的建議。

你可以部署單台服務器來支持測試和預發佈環境。但對於生產環境部署而言,並不推薦採用單台服務器,就算開啟了Journaling日誌也是如此。只有一台服務器會為備份和恢復造成一定複雜性,當服務器發生故障時,無法進行故障轉移。

但是,在極少的幾種情況下也有例外。如果應用程序不需要高可用性或者快速恢復,數據集相對較小(比方說小於1 GB),那麼運行在一台服務器上也是可以的。即使如此,考慮到日益下降的硬件成本,以及複製所帶來的眾多好處,先前提到的單機方案確實沒什麼亮點。

2. Journaling日誌

MongoDB v1.8引入了Journaling日誌,而MongoDB v2.0會默認開啟Journaling日誌。當Journaling日誌開啟時,MongoDB在寫入核心數據文件時會先把所有寫操作提交到Journaling日誌文件裡。這能讓MongoDB服務器在發生非正常關閉時快速恢復並正常上線。

在v1.8之前沒有此類特性,因此非正常關閉經常會導致災難。這怎麼可能呢?我之前多次提到MongoDB把每個數據文件映射到虛擬內存裡。也就是說,當MongoDB執行寫操作時,它是寫入虛擬內存地址,而非直接寫入磁盤。OS內核週期性地將這些寫操作從虛擬內存同步到磁盤上,但是其頻率和完整性是不確定的,因此MongoDB使用fsync系統調用每60 s對所有數據文件做一次強制同步。這裡的問題在於,如果MongoDB進程在還有未同步的寫操作時被殺掉了,那麼則無法獲知數據文件的狀態。這就可能損壞數據文件。

在沒有開啟Journaling日誌的mongod進程發生非正常關閉時,想將數據文件恢復到一致狀態要運行一次修復。修復過程會重寫數據文件,拋棄所有它無法識別的內容(損壞的數據)。因為大家通常都不太能接受停機和數據丟失,所以這種修復途徑一般只能作為最後的恢復手段。從現有副本中重新同步數據幾乎總是比較方便可靠的方法,這也是運行複製如此重要的原因之一。

Journaling日誌讓你不再需要修復數據庫,因為MongoDB能用Journaling日誌將數據文件恢復到一致狀態。在MongoDB v2.0里,Journaling日誌是默認開啟的,但是你也可以通過-nojournal標誌禁用它:

$ mongod --nojournal
  

開啟Journaling日誌時,日誌文件被放在一個名為journal的目錄裡,該目錄位於主數據路徑下面。

如果你在運行MongoDB服務器時開啟了Journaling日誌,請牢記幾點。第一點,Journaling日誌會降低寫操作的性能。既想獲得最高寫入性能,又想有Journaling日誌保障的用戶有兩個選擇。其一,只在被動副本上開啟Journaling日誌,只要這些副本能和主節點保持一致,就無需犧牲性能。另一個解決方案,也許和前者是互補的,是為Journaling日誌掛載一塊單獨的磁盤,然後在journal目錄和輔助卷之間創建一個符號鏈接。輔助卷不用很大,一塊120 GB的磁盤就足夠了,這個大小的固態硬盤(SSD)的價格還是可以承受的。為Journaling日誌掛載一塊單獨的SSD能確保將它運行時的性能損耗降到最小。

第二點,Journaling日誌本身並不保證不會丟失寫操作,它只能保證MongoDB始終能恢復到一致狀態,重新上線。Journaling日誌的機制是每100 ms將寫緩衝和磁盤做一次同步。因此一次非正常關閉最多只會丟失100 ms裡的寫操作。如果你的應用程序無法接受這種風險,可以使用getlasterror命令的j選項,讓服務器在Journaling日誌同步後才返回:

db.runCommand({getlasterror: 1, j: true})
  

在應用程序層,可以用safe模式選項(與wwtimeout類似)。在Ruby裡,可以像這樣使用j選項:

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

一定要注意,每次寫操作都像這樣做是不明智的,因為這會強迫每次寫操作都等到下次Journaling日誌同步才返回。也就是說,所有的寫操作都可能要等100 ms才能返回。因此請謹慎使用本特性。3

3. MongoDB的未來版本裡會有更細粒度的Journaling日誌同步控制,請查看最新的發佈說明瞭解詳細情況。

10.1.3 數據的導入與導出

如果你正從現有系統遷移到MongoDB,或者需要從數據倉庫填充數據,那麼就需要一種有效的導入方法。你可能還需要一個好的導出策略,因為可能要從MongoDB裡將數據導出到外部處理任務中。例如,將數據導出到Hadoop進行批處理就已成為一種常見實踐。4

4. 對於這種特定用法,在http://github.com/mongodb/mongo-hadoop可以找到官方支持的MongoDB-Hadoop適配器。

有兩種途徑將數據導入和導出MongoDB,你可以使用自帶的工具——mongoimportmongoexport,或者使用某個驅動編寫一個簡單的程序。5

5. 注意,數據的導入和導出與備份有所不同,本章後面會討論備份相關的內容。

1. mongoimportmongoexport

MongoDB自帶了兩個導入、導出數據的工具:mongoimportmongoexport。你可以通過mongoimport導入JSON、CSV和TSV文件,這通常用於從關係型數據庫向MongoDB加載數據:

$ mongoimport -d stocks -c values --type csv --headerline stocks.csv
  

本例中,你將一個名為stocks.csv的CSV文件導入到了stocks數據庫的values集合裡。--headerline標誌表明了CSV的第一行包含字段名。可以通過mongoimport –help看到所有的導入選項。

可以通過mongoexport將一個集合的所有數據導出到一個JSON或CSV文件裡:

$ mongoexport -d stocks -c values -o stocks.csv
  

這條命令會將數據導出到stocks.csv文件裡。與mongoimport類似,你可以通過--help看到mongoexport的其他命令選項。

2. 自定義導入與導出腳本

當處理的數據相對扁平時,你可能會使用MongoDB的導入導出工具;一旦引入了子文檔和數組,CSV格式就有些「力不從心」了,因為它不是設計來表示內嵌數據的。當需要將富文檔導出到CSV或者從CSV導入一個富MongoDB文檔,也許構建一個自定義工具會更方便。你可以使用任意驅動實現這一目標。例如,MongoDB用戶通常會編寫一些腳本連接關係型數據庫,隨後將兩張表的數據整合到一個集合裡。

將數據移入和移出MongoDB是件很複雜的事:數據建模的方式會因系統而異。在這些情況下,要做好將驅動當成轉換工具的準備。

10.1.4 安全

大多數RDBMS都有一套複雜的安全子系統,可以對用戶和用戶組授權,進行細粒度的權限控制。與此相反,MongoDB v2.0只支持簡單的、針對每個數據庫的授權機制。這就讓運行MongoDB的機器的安全性變得更加重要了。此處我們會討論在安全環境裡運行MongoDB所需考慮的一些重點內容,並解釋身份驗證是如何進行的。

1. 安全環境

和所有數據庫一樣,MongoDB應該運行在一個安全環境裡。生產環境中,MongoDB的用戶必須利用現代操作系統的安全特性來確保數據的安全。在這些特性之中,也許最重要的就是防火牆了。在結合使用防火牆與MongoDB時,唯一潛在的難點是瞭解哪台機器需要和其他機器互相通信。還好,通信規則很簡單。在副本集中,每個節點都要能和其他節點通信。此外,所有數據庫客戶端都必須能連接到各個它可能會通信的副本集節點上。

分片集群中含有副本集,因此所有副本集的規則都能適用;在分片的情況下,客戶端是mongos路由器。除此之外:

  • 所有分片都必須能與其他分片直接通信;

  • 分片與mongos路由器都必須能連上配置服務器。

相關的安全關注點是綁定地址(bind address)。默認情況下,MongoDB會監聽本機的所有地址,但你可能只想監聽一個或幾個特殊的地址。為此,可以在啟動mongodmongos時帶上--bind_ip選項,它接受一個或多個逗號分隔的IP地址。例如,想要監聽loopback接口和內部IP地址10.4.1.55,可以像這樣啟動mongod

mongod --bind_ip 127.0.0.1,10.4.1.55
  

請注意,機器之間發送數據都使用明文,官方的SSL支持安排在MongoDB v2.2中發佈。

2. 身份驗證

MongoDB的身份驗證最早是為那些在共享環境下托管MongoDB服務器的用戶構建的。它的功能並不多,但在需要一些額外安全保障時還是很有用的。我們先討論一下身份驗證API,然後再描述如何在副本集和分片中使用該API。

  • 身份驗證API

要著手瞭解身份驗證,先創建一個管理員用戶,切換到admin數據庫,運行db.addUser,該方法接受兩個參數:一個用戶名和一個密碼。

> use admin
> db.addUser("boss", "supersecret")
  

管理員用戶能創建其他用戶,還能訪問服務器上的所有數據庫。有了它,你就能開啟身份驗證了,在重啟mongod實例時加上--auth選項:

$ mongod --auth
  

現在,只有通過身份驗證的用戶才能訪問數據庫。重啟Shell,隨後使用db.auth方法以管理員身份登錄:

> use admin
> db.auth("boss", "supersecret")
  

現在可以為個別數據庫創建用戶了。如果想要創建只讀用戶,將true作為db.addUser方法的最後一個參數。這裡將為stocks數據庫添加兩個用戶。第一個用戶擁有所有權限,第二個只能讀取數據庫的數據:

> use stocks
> db.addUser("trader", "moneyfornuthin")
> db.addUser("read-only-trader", "foobar", true)
  

現在,只有三個用戶能訪問stocks數據庫,他們是bosstraderread-only-trader。如果你希望查看擁有某個數據庫訪問權限的所有用戶的列表,可以查詢system.users集合:

> db.system.users.find
{ "_id" : ObjectId("4d82100a6dfa7bb906bc4df7"),
  "user" : "trader", "readOnly" : false,
  "pwd" : "e9ee53b89ef976c7d48bb3d4ea4bffc1" }
{ "_id" : ObjectId("4d8210176dfa7bb906bc4df8"),
  "user" : "read-only-trader", "readOnly" : true,
  "pwd" : "c335fd71fb5143d39698baab3fdc2b31" }
  

從該集合中刪除某個用戶,就能撤銷它對某個數據庫的訪問權限。如果你更青睞於輔助方法,可以使用Shell裡的db.removeUser方法,它的作用是一樣的。

你並不需要顯式註銷,中斷連接(關閉Shell)就行了。但是如果你需要,也有註銷命令可用:

> db.runCommand({logout: 1})
  

當然,你也可以通過驅動使用我們此處看到的全部身份驗證邏輯,請查看驅動的API瞭解更多詳情。

  • 副本集身份驗證

副本集也支持剛才介紹的身份驗證API,但是為副本集開啟身份驗證還需要額外的幾個步驟。開始時,創建一個文件,其中至少包含6個Base64字符集6中的字符。文件的內容會被作為某種密碼,每個副本集成員都會用它來和其他成員進行身份驗證。舉個例子,你可以創建一個名為secret.txt的文件,其內容如下:

6. Base64字符集由以下字符組成:英文字母中的全部大寫和小寫字母、數字0~9以及+和/。

tOps3cr3tpa55word
  

將該文件放到每個副本集成員的機器上,調整文件權限,以便只有文件的擁有者才能訪問它:

sudo chmod 600 /home/mongodb/secret.txt
  

最後,在啟動每個副本集成員時使用--keyFile選項指定密碼文件的位置:

mongod --keyFile /home/mongodb/secret.txt
  

現在副本集就已經開啟身份驗證了,你會希望事先創建一個管理員用戶,就像上一節裡那樣。

  • 分片身份驗證

分片身份驗證是副本集身份驗證的一個擴展。集群裡的每個副本集都已經像剛才介紹的那樣,通過密鑰文件保護起來了。此外,所有的配置服務器和每個mongos實例也都擁有一個包含相同密碼的密鑰文件。在啟動每個進程時都用--keyFile選項指定包含密碼的文件,整個分片集群都使用該密碼。完成這個步驟,整個集群就能使用身份驗證了。