全部產品
Search
文件中心

ApsaraDB for MongoDB:MongoDB 4.4功能概覽

更新時間:Nov 20, 2024

阿里雲作為MongoDB官方戰略夥伴,全網首次引入MongoDB 4.4版本並已於2020年11月發布。而MongoDB官方4.4版本已經在2020年7月30日正式發布。和往年的大版本不同,本次的4.4版本是以往版本的全面加強版,主要針對使用者呼聲最高的一些痛點重點進行了改進。

隱藏索引(Hidden Indexes)

眾所周知資料庫維護太多的索引會導致寫效能下降,但是往往業務上的複雜性導致了營運人員不敢輕易去刪除一個潛在的低效率索引,擔心誤刪除會帶來業務效能的抖動,而重建索引的代價也非常大。

為瞭解決上述問題,ApsaraDB for MongoDB版和MongoDB官方達成戰略合作後共同開發了Hidden Indexes功能。該功能支援通過collMod命令隱藏現有的索引,保證該索引在後續的查詢中不會被使用。在觀察一段時間後,確定業務沒有異常即可以放心刪除該索引。

參考代碼:

db.runCommand( {
   collMod: 'testcoll',
   index: {
      keyPattern: 'key_1',
      hidden: false
   }
} )

需要注意的是,索引被隱藏之後僅對MongoDB的執行計畫器不可見,這並不會改變索引本身的一些特殊行為,如唯一鍵約束、TTL淘汰等。

說明

索引在隱藏期間也不會停止更新,所以當需要該索引時,可以通過取消隱藏使其立刻可用。

重定義分區鍵(Refinable Shard Keys)

在MongoDB分區叢集中,一個好的Shard key至關重要,因為它決定了分區叢集在指定的Workload(工作量)下是否有良好的擴充性。但是在實際使用MongoDB的過程中,即使我們事先仔細斟酌了要選擇的Shard Key,也會因為Workload的變化而導致出現Jumbo Chunk(超過預設大小的Chunk),或者業務流量都打向單一分區的情況。

在4.0及之前的版本中,集合選定的Shard Key及其對應的Value都是不能更改的,到了4.2版本,雖然可以修改Shard Key的Value,但是資料的跨分區遷移以及基於分散式交易的實現機制導致效能開銷很大,而且並不能完全解決Jumbo Chunk或訪問熱點的問題。例如,現在有一個訂單表,Shard Key為{customer_id:1},在業務初期每個客戶不會有很多的訂單,這樣的Shard Key完全可以滿足需求,但是隨著業務的發展,某個大客戶累積的訂單越來越多,進而對這個客戶訂單的訪問成為某個單一分區的熱點,由於訂單和customer_id天然的關聯關係,修改customer_id並不能改善訪問不均的情況。

針對上述類似情境,在4.4版本中,您可以通過refineCollectionShardKey命令給現有的Shard Key增加一個或多個Suffix Field來改善現有的文檔在Chunk上的分布問題。例如在上面描述的訂單業務情境中,通過refineCollectionShardKey命令把Shard key更改為{customer_id:1, order_id:1},即可避免單一分區上的訪問熱點問題。

並且,refineCollectionShardKey命令的效能開銷非常低,僅更改Config Server節點上的中繼資料,不需要任何形式的資料移轉,資料的打散仍然在後續正常的Chunk自動分裂和遷移的流程中逐步進行。此外,Shard Key需要有對應的Index來支撐,因此refineCollectionShardKey命令要求提前建立新Shard Key所對應的Index。

由於並不是所有的文檔都存在新增的Suffix Field,因此在4.4版本中隱式支援了Missing Shard Key功能,即新插入的文檔可以不包含指定的Shard Key Field。但是由於很容易產生Jumbo Chunk,因此並不建議使用。

複合雜湊分區鍵(Compound Hashed Shard Keys)

在4.4之前的版本中,您只能指定單欄位的雜湊片鍵,原因是當時版本的MongoDB不支援複合雜湊索引,這樣就很容易導致集合資料在分區上分布不均勻。

在最新的4.4版本中加入了複合雜湊索引,即您可以在複合索引中指定單個雜湊欄位,位置不限,可以作為首碼,也可以作為尾碼,進而也就提供了對複合雜湊片鍵的支援。

參考代碼:

sh.shardCollection(
  "examples.compoundHashedCollection",
  { "region_id" : 1, "city_id": 1, field1" : "hashed" }
)
sh.shardCollection(
  "examples.compoundHashedCollection",
  { "_id" : "hashed", "fieldA" : 1}
)

複合雜湊索引有很多優點,例如如下兩個情境:

  • 應法律法規的要求,需要使用MongoDB的zone sharding功能,把資料盡量均勻打散在某個地區的分區上。

  • 集合指定的片鍵的值是遞增的,例如上文例子中的{customer_id:1, order_id:1}這個片鍵,如果customer_id是遞增的,並且業務也總是訪問最新顧客的資料,導致大部分的流量總是訪問單一分區。

在沒有複合雜湊片鍵支援的情況下,只能提前對需要的欄位進行雜湊值的計算,並將結果儲存到文檔中的某個特殊欄位中,然後再通過範圍分區的方式指定其作為片鍵來解決上述問題。

而在4.4版本中只需直接把目標欄位指定為雜湊即可輕鬆解決上述問題。例如,針對上述第二個情境,僅需將片鍵設定為{customer_id:'hashed', order_id:1}即可在極大程度上簡化商務邏輯的複雜性。

對沖讀(Hedged Reads)

頁面的響應速度和經濟損失直接掛鈎。Google有一個研究報告表明,如果網頁的載入時間超過3秒,使用者的跳出率會增加50%。針對這個問題,MongoDB在4.4版本中提供了Hedged Reads功能,即在分區叢集情境下,mongos節點會把一個讀請求同時發送給某個分區的兩個複本集成員,然後選擇最快的返回結果回複用戶端,來減少業務上的P95(指過去十秒內95%的請求延遲均在規定範圍內)和P99延遲(指過去十秒內99%的請求延遲均在規定範圍內)。

Hedged Reads功能作為Read Preference參數的一部分來提供, 因此可以在Operation粒度上進行配置,當Read Preference指定為nearest時,系統預設啟用Hedged Reads功能,當指定為primary時,不支援Hedged Reads功能,當指定為其他時,需要顯式地指定hedgeOptions才可以啟用Hedged Reads。如下所示:

db.collection.find({ }).readPref(
   "secondary",                      // mode
   [ { "datacenter": "B" },  { } ],  // tag set
   { enabled: true }                 // hedge options
)

此外,Hedged Reads也需要mongos開啟支援,配置readHedgingMode參數為on,使mongos開啟該功能支援。

參考代碼:

db.adminCommand( { setParameter: 1, readHedgingMode: "on" } )

降低複寫延遲

本次4.4的更新帶來了主備複寫延遲的降低。對於MongoDB來說,主備複製的延遲會對讀寫有非常大的影響。在某些特定的情境下,備庫需要及時地複製並應用主庫的累加式更新,才可以繼續進行讀寫操作。因此,更低的複寫延遲會帶來更好的一致性體驗。

流式複製(Streaming Replication)

在4.4之前的版本中,備庫需要通過不斷地輪詢upstream來擷取累加式更新操作。每次輪詢時,備庫主動給主庫發送一個getMore命令讀取Oplog集合,如果有資料,會返回一個最大16MB的Batch,如果沒有資料,備庫也會通過awaitData選項來控製備庫無謂的getMore開銷,同時能夠在有新的累加式更新時,第一時間擷取到對應的Oplog。拉取操作是通過單個OplogFetcher線程來完成,每個Batch的擷取都需要經歷一個完整的RTT(Round-Trip Time,往返時間),在複本集網路狀況不好的情況下,複製的效能就嚴重受限於網路延遲。

而在4.4版本中,增量的Oplog是不斷地主動流向備庫的,而不是被動地依靠備庫輪詢。相比於備庫輪詢的方式,至少在Oplog的擷取上節省了一半的RTT。在以下兩個情境中,Streaming Replication功能會大大提升效能:

  • 當使用者的寫操作指定了writeConcern參數為"majority"時,寫操作需要等待足夠多次數的“備庫返回複製成功”。而在新的複製機制下,高延遲的網路環境也可以平均提升50%的majority寫效能。

  • 當使用者使用了因果一致性(Causal Consistency)的情境下,為了保證可以在備庫讀到自己的寫操作(Read Your Write),同樣強依賴備庫對主庫Oplog的及時複製。

同步建索引(Simultaneous Indexing)

4.4 之前的版本中,索引的建立需要在主庫中完成之後,才會到備庫上執行。備庫上的建立動作在不同的版本中,因為建立機制和建立方式的不同,對備庫Oplog的影響也大有不同。

但即使在4.2版本中統一了前後台索引建立機制,使用了相當細粒度的加鎖機制(只在索引建立的開始和結束階段對集合加獨佔鎖),也會因為索引建立本身的CPU、IO效能開銷導致複寫延遲,或是因為一些特殊操作,例如使用collMod命令修改集合元資訊,而導致Oplog的應用阻塞,甚至會因為主庫歷史Oplog被覆蓋而進入Recovering狀態。

在4.4版本中,主庫和備庫上的索引建立操作是同時進行的,這樣可以大幅減少上述情況所帶來的主備延遲,即使在索引建立過程中,也可以保證備庫訪問到最新的資料。

此外,新的索引建立機制規定,只有在大多數具備投票許可權節點返回成功後,索引才會真正生效。所以,也可以減輕在讀寫分離情境下因為索引不同而導致的效能差異。

複製讀請求(Mirrored Reads)

在ApsaraDB for MongoDB版以往提供服務的過程中,有一個現象,即大多數使用者雖然購買的是三節點複本集執行個體,但是實際在使用過程中讀寫都是在Primary節點進行,其中一個可見的Secondary節點並未承載任何讀流量,導致在偶爾的宕機切換之後,使用者能明顯感受到業務的訪問延遲,經過一段時間後才會恢複到之前的水平,原因就在於新選舉出的主節點之前從未提供過讀服務,並不瞭解業務的訪問特徵,沒有針對性地對資料做緩衝,所以在突然提供服務後,讀操作會出現大量的緩衝未命中(Cache Miss),需要從磁碟重新載入資料,造成訪問延遲上升。在大記憶體執行個體的情況下,這個問題尤為明顯。

在4.4版本中,MongoDB針對上述問題實現了Mirrored Reads功能,即主節點會按一定的比例把讀流量複製到備庫上執行,來協助備庫預熱緩衝。這是一個非阻塞執行(Fire and Forgot)的行為,不會對主庫的效能產生任何實質性的影響,但是備庫負載會有一定程度的上升。

流量複製的比例是可動態配置的,通過mirrorReads參數設定,預設複製1%的流量。

參考代碼:

db.adminCommand( { setParameter: 1, mirrorReads: { samplingRate: 0.10 } } )

此外,還可以通過db.serverStatus( { mirroredReads: 1 } )來查看Mirrored Reads相關的統計資訊,如下所示:

SECONDARY> db.serverStatus( { mirroredReads: 1 } ).mirroredReads
{ "seen" : NumberLong(2), "sent" : NumberLong(0) }

可恢複的全量同步(Resumable Initial Sync)

在4.4之前的版本中,如果備庫在做全量同步時出現網路抖動而導致串連閃斷,那麼備庫需要從頭開始全量同步,導致之前的工作全部白費,這個情況在資料量比較大時會對業務造成巨大的影響。

在4.4版本中,MongoDB提供了從中斷位置繼續執行同步的能力。如果在閃斷後一直無法串連成功,系統會重新選擇一個同步源進行新的全量同步。該過程的預設逾時時間為24小時,您可以通過replication.initialSyncTransientErrorRetryPeriodSeconds在進程啟動時更改。

需要注意的是,對於全量同步過程中遇到的非網路異常導致的中斷,仍然需要重新發起全量同步。

基於時間保留Oplog(Time-Based Oplog Retention)

MongoDB中的Oplog集合記錄了所有資料的變更操作,除了用於複製,還可用於增量備份、資料移轉、資料訂閱等情境,是MongoDB資料生態的重要基礎設施。

Oplog是通過Capped Collection來實現的,雖然從3.6版本開始,MongoDB支援通過replSetResizeOplog命令動態修改Oplog集合的大小,但是往往不能準確反映下遊對Oplog增量資料的需求,您可以考慮如下情境:

  • 計劃在淩晨2~4點對某個Secondary節點進行停機維護,需要避免上遊Oplog被清理而觸發全量同步。

  • 下遊的資料訂閱組件可能會因為一些異常情況而停止服務,但是最慢會在3個小時之內恢複服務並繼續進行增量拉取,也需要避免上遊的增量缺失。

由此可見,在大部分應用情境下,需要保留最近一個時間段內的Oplog,而這個時間段內產生多少Oplog往往是很難確定的。

在4.4版本中,MongoDB支援通過storage.oplogMinRetentionHours參數定義需要保留的Oplog時間長度,也可以通過replSetResizeOplog命令線上修改這個值,參考代碼如下:

// First, show current configured value
db.getSiblingDB("admin").serverStatus().oplogTruncation.oplogMinRetentionHours
// Modify
db.adminCommand({
  "replSetResizeOplog" : 1,
  "minRetentionHours" : 2
})

多表聯合增強(Union)

在多表聯集查詢能力上,4.4之前的版本只提供了一個$lookup stage用於實作類別似於SQL中的left outer join功能,而4.4版本中新增了$unionWith stage用於實作類別似於SQL的union all功能,用於將兩個集合中的資料彙總到一個結果集中,然後做指定的查詢和過濾。區別於$lookup stage的是,$unionWith stage支援分區集合。在Aggregate Pipeline中使用多個$unionWith stage,可以對多個集合資料做彙總,使用方式如下:

{ $unionWith: { coll: "<collection>", pipeline: [ <stage1>, ... ] } }

您也可以在pipeline參數中指定不同的stage,用於在對集合資料做彙總前進行一定的過濾,使用起來非常靈活。例如,某個業務上對訂單資料按表拆分儲存到不同的集合中,第二季度有如下資料:

db.orders_april.insertMany([
  { _id:1, item: "A", quantity: 100 },
  { _id:2, item: "B", quantity: 30 },
]);
db.orders_may.insertMany([
  { _id:1, item: "C", quantity: 20 },
  { _id:2, item: "A", quantity: 50 },
]);
db.orders_june.insertMany([
  { _id:1, item: "C", quantity: 100 },
  { _id:2, item: "D", quantity: 10 },
]);

假設需要列出第二季度中不同產品的銷量,在4.4版本之前,可能需要業務自己把資料都讀出來,然後在應用程式層面做彙總才能解決這個問題,或者依賴某種資料倉儲產品來做分析,而在4.4版本中只需要如下一條Aggregate語句即可解決問題:

db.orders_april.aggregate( [
   { $unionWith: "orders_may" },
   { $unionWith: "orders_june" },
   { $group: { _id: "$item", total: { $sum: "$quantity" } } },
   { $sort: { total: -1 }}
] )

自訂Aggregation運算式(Custom Aggregation Expressions)

4.4之前的版本中,您可以通過find命令中的$where operator或者MapReduce功能來實現在Server端執行自訂的JavaScript指令碼,進而提供更為複雜的查詢能力,但是這兩個功能並沒有做到和Aggregation Pipeline在使用上的統一。

在4.4版本中,MongoDB提供了$accumulator$function這兩個新的Aggregation Pipeline Operator用來取代$where operator和MapReduce。藉助於Server Side JavaScript來實現自訂的Aggregation Expression,這樣做到複雜查詢的功能介面都集中到Aggregation Pipeline中,完善介面統一性和使用者體驗的同時,也可以把Aggregation Pipeline本身的執行模型利用上,實現一舉多得的效果。

$accumulator和MapReduce功能有些相似,會先通過init函數定義一個初始的狀態,然後根據指定的accumulate函數更新每一個輸入文檔的狀態,並且根據需要決定是否執行merge函數。

例如,假設在分區集合上使用了$accumulator operator,則需要將在不同分區上執行完成的結果做merge,並且如果指定了finalize函數,那麼在所有輸入文檔處理完成後,還會根據該函數將狀態轉換為最終的輸出。

$function$where operator在功能上基本一致,但其強大之處在於可以和其他Aggregation Pipeline Operator配合使用,此外也可以在find命令中藉助$expr operator來使用$function operator,等價於之前的$where operator,MongoDB官方在文檔中也建議優先使用$function operator

其他易用性增強

除了上述$accumulator$function operator,4.4版本中還新增了其他多個Aggregation Pipeline Operator,例如字串處理、擷取數組收尾元素、還有用來擷取文檔或二進位串大小的操作符,具體請參見下表:

操作符

說明

$accumulator

返回使用者定義的accumulator operator結果。

$binarySize

返回指定字串或位元據的大小(以位元組為單位)。

$bsonSize

返回編碼為BSON時指定文檔(即bsontype對象)的位元組大小。

$first

返回數組中的第一個元素。

$function

用來自訂aggregation運算式。

$last

返回數組中的最後一個元素。

$isNumber

如果指定的運算式類型為整數、十進位、雙精確度或長整型,則返回布爾值true。如果運算式類型為BSON類型、null或缺失欄位,則返回布爾值false

$replaceOne

替換第一個通過指定的字串匹配到的執行個體。

$replaceAll

替換所有通過指定的字串匹配到的執行個體。

Connection Monitoring and Pooling

4.4版本的Driver中增加了對用戶端串連池的行為監控和自訂配置,通過標準的API來訂閱和串連池相關的事件,包括串連的關閉和開啟、串連池的清理。也可以通過API來配置串連池的一些行為,例如擁有的最大或最小串連數、每個串連的最大空閑時間、線程等待可用串連時的逾時時間等。具體可以參見MongoDB官方文檔

Global Read and Write Concerns

在4.4之前的版本中,如果執行的操作沒有顯式地指定readConcern或者writeConcern,則會有預設行為。例如:readConcern預設為localwriteConcern預設為{w: 1}。但這個預設行為不可以變更,如果使用者想讓所有insert操作的writeConcern預設為 {w: majority},那麼只能在所有訪問MongoDB的代碼中顯式指定該值。

而在4.4版本中,可以通過setDefaultRWConcern命令來配置全域預設的readConcernwriteConcern。參考代碼:

db.adminCommand({
  "setDefaultRWConcern" : 1,
  "defaultWriteConcern" : {
    "w" : "majority"
  },
  "defaultReadConcern" : { "level" : "majority" }
})

您也可以通過getDefaultRWConcern命令擷取當前預設的readConcernwriteConcern

此外,在4.4版本中記錄慢日誌或診斷記錄的時候,會記錄當前操作的readConcern或者writeConcern設定的來源,兩者共通的來源有如下三種:

來源

說明

clientSupplied

由應用自己指定。

customDefault

由使用者通過setDefaultRWConcern命令指定。

implicitDefault

完全沒做任何配置,Server預設行為。

writeConcern還有如下一種來源:

來源

說明

getLastErrorDefaults

繼承自複本集的settings.getLastErrorDefaults配置。

New MongoDB Shell (beta)

對於MongoDB的營運人員來說,使用最多的工具可能就是Mongo Shell,4.4版本提供了新版本的Mongo Shell,增加了諸如代碼高亮、命令自動補全、更具可讀性的錯誤資訊等非常人性化的功能。目前提供的是beta版本,很多命令還未提供支援,僅供體驗。新版MongoShell

結語

本次發布的4.4版本主要是一個維護性的版本,除了上述解讀,還有很多其他小的最佳化,例如$indexStats最佳化、TCP Fast Open支援最佳化建連、索引刪除最佳化等等。還有一些相對大的增強,例如新的結構化日誌LogV2、新的安全機制支援等。詳情請參見官方的Release Notes