自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

MongoDB 系列 - 數(shù)據(jù)查詢游標(biāo)你用對了嗎?

運(yùn)維 數(shù)據(jù)庫運(yùn)維 MongoDB
MongoDB 里面的游標(biāo),有點(diǎn)類似于在 Node.js 里使用 Stream 處理文件數(shù)據(jù),相比把整個文件讀入內(nèi)存在處理這種模式,Stream 帶來的收益是很大的。

本文轉(zhuǎn)載自微信公眾號「編程界」,作者五月君  。轉(zhuǎn)載本文請聯(lián)系編程界公眾號。

幾個話題

本文會根據(jù)以下幾個話題進(jìn)行討論與講解,文中的目錄不完全和這幾個話題一致,但當(dāng)你閱讀完本文后,相信這些答案應(yīng)該也有了,都在文中。

  • 為什么要使用游標(biāo)、什么時候使用?
  • 關(guān)注服務(wù)器內(nèi)存,游標(biāo)什么時候關(guān)閉?
  • 需要注意的游標(biāo)超時與容錯處理
  • 為什么不要隨意調(diào)整 batchSize 數(shù)量?
  • 使用時需注意 Mongoose 與原生 Node.js MongoDB 驅(qū)動程序的不同之處
  • 解答群友問題時發(fā)現(xiàn)的一個關(guān)于游標(biāo)的 Bug
  • 擴(kuò)展 - 為什么可以使用 for await of 遍歷游標(biāo)對象?

為什么要使用游標(biāo)?

這樣的寫法 collection.find().toArray(),大家在學(xué)習(xí) MongoDB 時應(yīng)該見的也不少,它的原理是客戶端驅(qū)動程序會自動把返回的所有數(shù)據(jù)一次性加載到應(yīng)用程序內(nèi)存中,理解起來相對簡單些,如果數(shù)據(jù)量小是沒問題的,在一些數(shù)據(jù)處理的場景中,具體有多少數(shù)據(jù)也許是未知的,有可能返回大量的數(shù)據(jù),如果全部 hold 在內(nèi)存,在服務(wù)端內(nèi)存寸土寸金的地方,白白消耗服務(wù)內(nèi)存不說,內(nèi)存占用過高還可能造成服務(wù) OOM。

MongoDB 里面的游標(biāo),有點(diǎn)類似于在 Node.js 里使用 Stream 處理文件數(shù)據(jù),相比把整個文件讀入內(nèi)存在處理這種模式,Stream 帶來的收益是很大的。

很形象的一個圖,來源:https://www.cnblogs.com/vajoy/p/6349817.html[1]

游標(biāo)基本工作原理

當(dāng)我們使用 collection.find() 或 collection.aggregate() 返回的是一個指向該集合的指針,也稱為游標(biāo)(cursor),是不能直接訪問數(shù)據(jù)的,只有當(dāng)循環(huán)迭代這個游標(biāo)時才會真正的從數(shù)據(jù)庫集合讀取數(shù)據(jù)。

在 Node.js 中使用很簡單,只要支持 for await of 語法,即可遍歷游標(biāo)返回的數(shù)據(jù)集,和正常使用 for of 遍歷數(shù)組很相似,區(qū)別是 for await of 遍歷的數(shù)據(jù)源是異步的。當(dāng)循環(huán)迭代開始時驅(qū)動程序會使用 getMore() 命令批量從數(shù)據(jù)庫集合中獲取一批數(shù)據(jù)先緩存起來,例如 Node.js MongoDB 驅(qū)動程序每次默認(rèn)批量獲取 1000 條(注意,第一次 getMore() 時實(shí)際請求是 101 條),取決于 batchSize[2] 參數(shù)設(shè)置,待這批數(shù)據(jù)處理完成之后,在向 MongoDB Server 執(zhí)行 getMore() 繼續(xù)請求直到游標(biāo)耗盡。

以下為 Node.js 中的兩種使用示例,個人比較推薦 for await of 這種寫法。方法二 while 循環(huán)這種寫法在一個 MongoDB Node.js 驅(qū)動程序版本中存在一個 Bug 下文會介紹。

  1. const userCursor = await collection.find(); 
  2.  
  3. // 如果沒有返回數(shù)據(jù),需要做一些特殊處理的,可以使用 userCursor.count() 或 userCursor.hasNext() 
  4. if (!await userCursor.count()) { 
  5.   // TODO: 提前結(jié)束,做一些其它操作 
  6.   return
  7.  
  8. // 方法一: 
  9. for await (const user of userCursor) { 
  10.  
  11. // 方法二: 
  12. while (await userCursor.hasNext()) { 
  13.  const doc = userCursor.next(); 

例如,數(shù)據(jù)庫集合有 10000 條數(shù)據(jù),每次批量獲取 1000 條,I/O 消耗應(yīng)該也為 10 次。終端鏈接至 MongoDB Server 設(shè)置 db.setProfilingLevel(0, { slowms: 0 })記錄所有的操作日志,之后在打開 MongoDB Server 控制臺日志,執(zhí)行應(yīng)用程序之后會看到如下日志信息,每次 getMore 都指向了同一個游標(biāo) ID getMore: 5098682199385946244。

游標(biāo)讀取結(jié)果.png

如果需要修改 batchSize 結(jié)果的,通過 options 指定 batchSize 屬性或調(diào)用 batchSize 方法都可以。

  1. collection.find().batchSize(1100) 
  2. // 或以下方法 
  3. collection.find({}, { 
  4.   batchSize: 1100 
  5. }) 

切記不要將 batchSize 設(shè)置為 1,例如,10000 條數(shù)據(jù)每獲取一條數(shù)據(jù),客戶端都將連接服務(wù)器讀取,這將會產(chǎn)生 10000 次網(wǎng)絡(luò) IO,下圖使用 mongostat 監(jiān)控,展示了每秒查詢游標(biāo)時的 getMore 次數(shù)。

游標(biāo)超時

如果一個游標(biāo)在一定時間內(nèi)無人訪問,超時之后會被回收,防止產(chǎn)生內(nèi)存泄漏,啟動時可通過 mongod --setParameter cursorTimeoutMillis=300000 參數(shù)設(shè)置,默認(rèn)超時為 10 分鐘,參見文檔 cursorTimeoutMillis#Default: 600000 (10 minutes)[3]。

例如,總共查詢 10000 條數(shù)據(jù),第一次 getmore() 默認(rèn)批量獲取 1000 條數(shù)據(jù),如果在默認(rèn)的 10 分鐘內(nèi)沒有處理完成這 1000 條數(shù)據(jù),游標(biāo)會被關(guān)閉,待下次執(zhí)行 getmore() 就會報錯 cursor id 4011961159809892672 not found,一般稱之為游標(biāo)超時。

如有遇到游標(biāo)超時,可通過調(diào)整 cursorTimeoutMillis 參數(shù)或減少 batchSize 數(shù)量選擇適合于自己的程序配置,通常默認(rèn)配置是不需要調(diào)整的。例如,在遍歷游標(biāo)數(shù)據(jù)時調(diào)了一個外部接口,由于接口超時導(dǎo)致的游標(biāo)超時這種外部業(yè)務(wù)原因的,應(yīng)先去優(yōu)化業(yè)務(wù)本身,再考慮調(diào)整配置。

為了解決游標(biāo)超時,你可能還見到過 cursor.addCursorFlag('noCursorTimeout', true) 這樣的配置,這會禁用掉游標(biāo)的超時限制,只有等到游標(biāo)耗盡或手動關(guān)閉 cursor.close() 游標(biāo)才可能被釋放,禁用超時時間這種做法,很不推薦使用,每個游標(biāo)都存在額外的內(nèi)存占用消耗,如果因?yàn)槭韬鐾浭謩雨P(guān)閉游標(biāo)導(dǎo)致的 MongoDB Server 內(nèi)存泄漏就得不償失了。

游標(biāo)狀態(tài)

登陸 MongoDB 客戶端,執(zhí)行 db.serverStatus().metrics.cursor 命令,查看當(dāng)前游標(biāo)使用狀態(tài)。如果真的出現(xiàn)游標(biāo)導(dǎo)致的 MongoDB 服務(wù)器內(nèi)存泄漏,以下幾個數(shù)據(jù)指標(biāo),做為運(yùn)維人員在排查問題時,會有幫助。

  • timedOut:指 MongoDB Server 進(jìn)程啟動到現(xiàn)在所有的游標(biāo)超時數(shù)量,此指標(biāo)反映了應(yīng)用程序因?yàn)樘幚砗臅r任務(wù) 或 游標(biāo)打開后因?yàn)閳箦e沒有顯示關(guān)閉游標(biāo) 這兩種情況導(dǎo)致的游標(biāo)超時數(shù)量。
  • open.noTimeout:為了防止游標(biāo)超時,MongoDB 提供了一個配置 DBQuery.Option.noTimeout[4] 設(shè)置永不超時,但如果處理完畢忘記顯示關(guān)閉游標(biāo),會導(dǎo)致游標(biāo)常駐內(nèi)存,數(shù)量越大內(nèi)存泄漏的風(fēng)險也越大,建議是盡量不要設(shè)置 noTimeout。
  • open.pinned:“固定” 打開游標(biāo)的數(shù)量。
  • open.total:MongoDB Server 當(dāng)前為客戶端打開的游標(biāo)數(shù)量,當(dāng)有游標(biāo)耗盡,total 的數(shù)量也會不斷的減少。
  1.  "timedOut" : NumberLong(4), 
  2.  "open" : { 
  3.   "noTimeout" : NumberLong(0), 
  4.   "pinned" : NumberLong(0), 
  5.   "total" : NumberLong(0) 
  6.  } 

游標(biāo)與異步迭代器

JavaScript 在 ES6 語法提供了一個功能叫迭代器,定義了一套統(tǒng)一的接口,只要實(shí)現(xiàn)了該接口的數(shù)據(jù)類型,都可使用 for of 關(guān)鍵詞遍歷,例如數(shù)組、Map、Set 類型等,這些類型上有一個方法 Symbol.iterator 返回的就是一個迭代器對象,迭代器對象的 next() 方法返回值包含了 vlaue、done 兩個屬性,如果 done 為 true 表示數(shù)據(jù)已遍歷完成,但 Symbol.iterator 只支持同步的數(shù)據(jù)源。

而我們從數(shù)據(jù)庫集合獲取數(shù)據(jù)涉及到網(wǎng)絡(luò) I/O,這是一個異步的操作,Symbol.iterator 就無法支持了,在ECMAScript 2018 標(biāo)準(zhǔn)中提供了一個新的屬性 Symbol.asyncIterator,這是一個異步迭代器,與 Symbol.iterator 不同的是 Symbol.asyncIterator 的 next() 方法返回的是一個包含 { value, done } 的 Promise 對象,如果一個對象設(shè)置了該屬性,它就是異步可迭代對象,相應(yīng)的我們可使用 for await...of 循環(huán)遍歷數(shù)據(jù)。

下面看下 MonogoDB Node.js 驅(qū)動程序在 v4.2.2 版本中的實(shí)現(xiàn),同樣也提供了 Symbol.asyncIterator 接口,這也就是為什么我們可以使用 for await...of 循環(huán)遍歷。

  1. // mongodb/lib/cursor/abstract_cursor.js 
  2. class AbstractCursor extends mongo_types_1.TypedEventEmitter { 
  3.   [Symbol.asyncIterator]( "Symbol.asyncIterator") { 
  4.     return { 
  5.       next: () => this.next().then(value => value != null ? { value, done: false }: { value: undefined, done: true }) 
  6.     }; 
  7.   } 

容錯處理

在遍歷游標(biāo)的過程中,for 循環(huán)體內(nèi)如果出現(xiàn)一些錯誤導(dǎo)致循環(huán)提前終止,這個時候游標(biāo)并不會被立刻銷毀,可以選擇手動關(guān)閉游標(biāo)或等待超過默認(rèn)的游標(biāo)超時時間后,游標(biāo)也會被銷毀。

如果設(shè)置了 noCursorTimeout 屬性為永不超時,這個時候就一定記得要關(guān)閉游標(biāo),因此在上面也建議盡量不要做這個設(shè)置。

  1. const userCursor = await collection.find(); 
  2. try { 
  3.   for await (const user of userCursor) { 
  4.     // 可能拋出錯誤 throw new Error('124'
  5.   } 
  6. } catch (e) { 
  7.   // 處理錯誤 
  8. } finally { 
  9.  userCursor.close();   

Mongoose 需要注意的地方

使用 mongoose 和原生支持的 mongodb 模塊還是有很多差異的,mongoose 的 find() 方法默認(rèn)不會返回游標(biāo)對象,需要在 find 后顯示調(diào)用 cursor() 方法,且沒有 cursor.count()、cursor.hasNext() 方法支持,對于一些想判斷如果游標(biāo)沒有數(shù)據(jù)做一些特殊處理,處理起來不是很友好。

  1. const userCursor = await User.find({}).cursor(); 
  2.  
  3. for await (const user of userCursor) { 

一個關(guān)于游標(biāo)的 Bug

在 Node.js 群里,一個群友發(fā)來消息使用游標(biāo)遇到了問題,后來也對這個問題做了一些查找和驗(yàn)證,下文會介紹,基于一個特定版本和特定的應(yīng)用場景才會出現(xiàn)這個問題,放在這里也是希望用到的朋友能少踩一個坑。

MongoDB Node.js 驅(qū)動程序在 3.5.4 版本基于游標(biāo)迭代查詢數(shù)據(jù)時,如果用了 limit 限制返回的數(shù)據(jù)條目,并且使用 hasNext(),存在一個 Bug,首先是從返回的游標(biāo)對象取出的 count 數(shù)不對,其次是遍歷出的數(shù)據(jù)條目與實(shí)際 limit count 數(shù)對不上,如果 limit 為奇數(shù)還會收到 MongoError: Cursor is closed 錯誤。

如果需要調(diào)整每一次的 getMore() 數(shù)量,游標(biāo)可以結(jié)合 batchSize 使用。為什么用了游標(biāo)還要使用 limit?這個也可以思考下。

  1. const userCursor = await collection.find({}).limit(5); 
  2. console.log('cursor count: ', await userCursor.count()); 
  3. try { 
  4.   while (await userCursor.hasNext()) { 
  5.     const doc = await userCursor.next(); 
  6.     console.log(doc); 
  7.   } 
  8. } catch (err) { 
  9.   console.error(err.stack); 
  10. userCursor.close(); 

mongodb@^3.5.4 版本輸出結(jié)果:

  1. cursor count:  10000 
  2. { _id: 61d6590b92058ddefbac6a14, userID: 0 } 
  3. { _id: 61d6590b92058ddefbac6a15, userID: 1 } 
  4. null 
  5. MongoError: Cursor is closed 
  6.     at Function.create (/test/node_modules/mongodb/lib/core/error.js:43:12) 
  7.     at Cursor.hasNext (/test/node_modules/mongodb/lib/cursor.js:197:24) 
  8.     at file:///test/index.mjs:42:27 
  9.     at processTicksAndRejections (internal/process/task_queues.js:93:5) 

NPM 包 mongodb 受影響版本為 3.5.4 參見 issue jira.mongodb.org/browse/NODE-2483[5]NPM 包 mongoose 受影響版本為 5.9.4 參見 issue github.com/Automattic/mongoose/issues/8664[6]

參考資料

[1]https://www.cnblogs.com/vajoy/p/6349817.html: https://www.cnblogs.com/vajoy/p/6349817.html

[2]batchSize: https://docs.mongodb.com/manual/tutorial/iterate-a-cursor/#cursor-batches

[3]cursorTimeoutMillis#Default: 600000 (10 minutes): https://docs.mongodb.com/manual/reference/parameters/#mongodb-parameter-param.cursorTimeoutMillis

[4]DBQuery.Option.noTimeout: https://docs.mongodb.com/manual/reference/method/cursor.addOption/#mongodb-data-DBQuery.Option.noTimeout

[5]NPM 包 mongodb 受影響版本為 3.5.4 參見 issue jira.mongodb.org/browse/NODE-2483: https://jira.mongodb.org/browse/NODE-2483

[6]NPM 包 mongoose 受影響版本為 5.9.4 參見 issue github.com/Automattic/mongoose/issues/8664: https://github.com/Automattic/mongoose/issues/8664

 

 

責(zé)任編輯:武曉燕 來源: 編程界
相關(guān)推薦

2017-11-09 13:56:46

數(shù)據(jù)庫MongoDB水平擴(kuò)展

2024-12-17 15:00:00

字符串Java

2024-09-18 10:08:37

2022-05-09 07:27:50

ThreadLocaJava

2024-02-23 09:36:57

C#工具并行處理

2019-05-28 11:52:43

可視化圖表數(shù)據(jù)

2017-10-10 15:30:20

JavaScript

2019-09-10 10:25:47

數(shù)據(jù)庫管理工具Valentina S

2023-11-29 07:38:33

JavaScript異步處理

2024-12-10 13:00:00

C++引用

2018-07-01 08:34:09

緩存數(shù)據(jù)服務(wù)

2020-09-18 06:39:18

hashMap循環(huán)數(shù)據(jù)

2013-07-30 17:28:45

2015-01-26 10:55:56

云服務(wù)器PowerEdge C

2013-07-30 09:16:59

2024-08-13 08:30:13

2019-12-26 14:07:19

隨機(jī)數(shù)偽隨機(jī)多線程

2017-09-01 09:52:20

PythonPandas數(shù)據(jù)分析

2011-07-15 11:15:29

上網(wǎng)行為管理
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號