bigywater.tech
MongoDB · 62

游标:batchSize、getMore 与 10 分钟超时

游标一批多少个?batchSize 设小了会怎样?10 分钟超时到底怎么回事?遍历时别人改数据会读到什么?这篇把这些逐个验证。

101 个
find 返回的第一批文档数;之后的 getMore 最多约 16 MiB
944 对 55 ms
batch_size=10 与默认批次读完 10 万个文档的耗时
code 43
空闲超时后 getMore 返回的 CursorNotFound

批次

实测 · 本机执行输出
  共 3 条命令(1 个 find + 2 个 getMore);第一批 101 个;之后每批: [70538, 29361] …;最后一批 29361 个
  总共读到 100,000 个文档

第一批 101 个文档,之后每个 getMore 装满 16 MiB 为止;10 万个文档只要 1 个 find 加 2 个 getMore。

batchSize

实测 · 本机执行输出
    batch_size     命令数          耗时
            10   10001      944 ms
           100    1001      140 ms
          1000     101       62 ms
         10000      11       54 ms
            默认       3       55 ms

批次太小增加往返:10 要 10,001 条命令(944 ms),默认 3 条命令(55 ms)。

占服务端资源

实测 · 本机执行输出
  # 读之前
  test> db.serverStatus().metrics.cursor.open
  {
    multiTarget: Long('0'),
    noTimeout: Long('0'),
    pinned: Long('0'),
    queuedData: Long('0'),
    singleTarget: Long('0'),
    total: Long('0')
  }
  # 开了一个游标、只读了第一条之后
  test> db.serverStatus().metrics.cursor.open
  {
    multiTarget: Long('0'),
    noTimeout: Long('0'),
    pinned: Long('0'),
    queuedData: Long('0'),
    singleTarget: Long('0'),
    total: Long('1')
  }
  # cur.close() 之后
  test> db.serverStatus().metrics.cursor.open
  {
    multiTarget: Long('0'),
    noTimeout: Long('0'),
    pinned: Long('0'),
    queuedData: Long('0'),
    singleTarget: Long('0'),
    total: Long('0')
  }

没读完的游标计在 metrics.cursor.open 里,close() 后释放。

空闲超时

实测 · 本机执行输出
  # 默认值(毫秒)
  test> db.adminCommand({getParameter: 1, cursorTimeoutMillis: 1}).cursorTimeoutMillis
  Long('600000')
  test> var r = db.adminCommand({setParameter: 1, cursorTimeoutMillis: 3000}); r.was
  Long('600000')
  不带 lsid 的 find(batchSize 10)开了 5 个游标;每个游标只在指定的空闲时间点做一次 getMore(之前不碰它,因为每次 getMore 都会重置空闲计时):
    空闲  2.0 s 后 getMore:成功(返回 10 个)
    空闲  5.0 s 后 getMore:失败 code=43(CursorNotFound): cursor id 349517662137719466 not found
    空闲  8.0 s 后 getMore:失败 code=43(CursorNotFound): cursor id 6889896205663917514 not found
    空闲 12.0 s 后 getMore:失败 code=43(CursorNotFound): cursor id 8350820545976234763 not found
    空闲 20.0 s 后 getMore:失败 code=43(CursorNotFound): cursor id 8106019490526629753 not found
  对照:pymongo 的游标(隐式会话)空闲 15 秒后(超过 3 秒的游标超时)仍然可用:可以继续读(游标绑定在会话上,由会话超时管理,默认 30 分钟)

不绑定会话的游标空闲超过 cursorTimeoutMillis 后会被后台回收,再 getMore 报 43 CursorNotFound;每次 getMore 都会重置空闲计时。绑定在会话上的游标(驱动默认)由会话超时管理。

noCursorTimeout

实测 · 本机执行输出
  no_cursor_timeout 的游标空闲 8 秒(超过 cursorTimeoutMillis 3 秒)后仍可继续读
  # serverStatus 里 noTimeout 的游标数
  test> var o = db.serverStatus().metrics.cursor.open; ({noTimeout: o.noTimeout, total: o.total})
  { noTimeout: Long('1'), total: Long('1') }
  test> db.adminCommand({setParameter: 1, cursorTimeoutMillis: 600000}).was
  Long('3000')

不会被超时回收,但要自己关闭。

不是快照

实测 · 本机执行输出
  按 k 排序遍历 20,000 个文档时,另一线程把已经读过的文档的 k 改大:共读到 21,500 个,其中同一个 _id 重复出现 1,500 次(不同 _id 20,000 个)

遍历期间的修改游标看得到:被改到后面的文档会再读一次,遍历结果可能重复或遗漏。

分步演示

游标
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。单节点;游标超时用手写的 OP_MSG 客户端(不带 lsid)演示;重复次数每次运行都不同(这次 1,500)
    • 没有验证:超时回收线程的执行周期(只观察到超时 3 秒时 5 秒空闲就失败);会话超时(logicalSessionTimeoutMinutes)过期后会话里的游标;分片集群上 mongos 游标的超时;tailable 游标的超时;批次大小对内存的影响(服务端与客户端)
    京ICP备2026067502号-1京公网安备11010502063487号