资讯专栏INFORMATION COLUMN

有坑勿踩(二)——关于游标

bawn / 3265人阅读

摘要:本质上所有查询的数据都是从游标来的。的作用是从游标中提取一批数据,具体提取多少则是由决定。同时注意我们已经有了一个游标。为了便于理解,我们下面还是称之为游标超时。

前言

聊一聊一个最基本的问题,游标的使用。可能你从来没有注意过它,但其实它在MongoDB的使用中是普遍存在的,也存在一些常见的坑需要引起我们的注意。

在写这个系列文章时,我会假设读者已经对MongoDB有了最基础的了解,因此一些基本名词和概念就不做过多的解释,请自己查阅相关资料。

使用场景

可能你以为你并没有经常在使用游标,但是其实只要在做查询,几乎时时刻刻都在用它。本质上所有查询的数据都是从游标来的。你说你用toArray()?不存在的,它也是在遍历游标然后返回给你一个数组而已。正是因为这样,就出现了第一个问题:除非你确定返回数据量有限,否则不要随便toArray()
这里说的toArray()包括:

shell中的toArray()。例如: var result = db.coll.find().toArray();

node中的toArray()。例如:var result = await db.collection("coll").find().toArray();

python中的list()。例如:result = list(db.coll.find());

Java中的toArray()。例如:DBCursor.toArray();

因为无论游标里有多少数据,toArray()都会给你挖出来放到内存里,变成数组返回给你。慢不说,内存也占用了很多。所以在可能的情况下,还是尽可能使用hasNext()/next()来得更好。

游标主要来自两个地方:

find

aggregation

注意二者返回的虽然都是“游标”,但又是两种不同的游标,使用上API也不完全相同,使用的时候请先查阅API(特别是使用NodeJS之类的动态语言的时候不要想当然)。

batchSize与getmore

说完从哪里来,下面就该说说怎么用的问题。
可能你已经从什么地方看到过getmore,比如mongostat的结果中。getmore的作用是从游标中提取一批数据,具体提取多少则是由batchSize决定。
所以当程序进行查询的时候,实际上在后台发生的事情包括:

驱动在后台获取batchSize条数据并自己缓存起来;

每次程序调用游标的next()方法时,从这些缓存中提取一条并返回;

batchSize条数据都返回完之后,驱动再次通过getmore获取batchSize条数据。

我们可以通过shell来观察这一过程:

先插入一批数据:

use foo
for(var i = 0; i < 1000; i++) {
    db.bar.insert({i: i});
}

强制日志记录所有操作:

db.setProfilingLevel(0, 0)

跟踪日志:

tail -f mongod.log

现在执行一条find语句:

replset:PRIMARY> db.bar.find().batchSize(50);
2018-12-29T16:01:29.587+0800 I COMMAND  [conn12] command test.bar appName: "MongoDB Shell" command: find { find: "bar", filter: {}, batchSize: 50.0, $clusterTime: { clusterTime: Timestamp(1546070474, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } }, $db: "test" } planSummary: COLLSCAN cursorid:77199395767 keysExamined:0 docsExamined:50 numYields:0 nreturned:50 reslen:2062 locks:{ Global: { acquireCount: { r: 1 } }, Database: { acquireCount: { r: 1 } }, Collection: { acquireCount: { r: 1 } } } protocol:op_msg 0ms

虽然我们在shell中只输出了20条结果,但实际上我们已经从这个游标中获取了50条数据(日志中的黑体部分)。所以当我们继续遍历这个游标时是暂时不需要再次从数据库中取数据的。同时注意我们已经有了一个游标cursor:77199395767
但当我们第三次遍历20条数据时,则会出现getmore日志:

replset:PRIMARY> it
2018-12-29T16:03:46.007+0800 I COMMAND  [conn12] command test.bar appName: "MongoDB Shell" command: getMore { getMore: 77199395767, collection: "bar", batchSize: 50.0, $clusterTime: { clusterTime: Timestamp(1546070594, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } }, $db: "test" } originatingCommand: { find: "bar", filter: {}, batchSize: 50.0, $clusterTime: { clusterTime: Timestamp(1546070474, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } }, $db: "test" } planSummary: COLLSCAN cursorid:77199395767 keysExamined:0 docsExamined:50 numYields:0 nreturned:50 reslen:2061 locks:{ Global: { acquireCount: { r: 1 } }, Database: { acquireCount: { r: 1 } }, Collection: { acquireCount: { r: 1 } } } protocol:op_msg 0ms
2018-12-29T16:03:46.010+0800 I COMMAND [conn12] command admin.$cmd appName: "MongoDB Shell" command: replSetGetStatus { replSetGetStatus: 1.0, forShell: 1.0, $clusterTime: { clusterTime: Timestamp(1546070624, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } }, $db: "admin" } numYields:0 reslen:896 locks:{} protocol:op_msg 0ms

它通过同一个游标再次提取了50条数据供使用。当我们用完缓存中的数据之前都是不会再看到新的getmore指令的。

游标超时

上面已经了解了游标与驱动是如何配合工作的,那么游标超时是怎么发生的呢?条件很简单,2次getmore之间间隔了超过10分钟,即一个游标在服务端超过10分钟无人访问,则会被回收掉。这时候如果你再针对这个游标进行getmore,就会得到游标不存在的错误(是的,超时的游标在数据库中是不存在的,你得到的错误不会是超时,而是游标不存在。为了便于理解,我们下面还是称之为“游标超时”)。
那么假设你通过游标读取数据的时候是为了进行一系列分析处理,那么下一次getmore在什么时候发生将取决于你的应用在多长时间内消耗完了当前缓存中的数据。换句话说,你的应用处理得越慢,下一次getmore发生的时间就越晚。很多驱动中batchSize的默认值是1000,这也代表着你的应用必须至少能够在10分钟内处理1000条数据,否则就会得到游标超时错误。所以诸如每一条数据需要查询其他数据库1次,需要通过RESTful API到互联网上获取相关的数据,或者需要进行一系列复杂的运算,这样的场景下,问题的关键其实不在于MongoDB怎么样,而在于你的应用到底能够处理多快。
假设问题还是发生了,你的应用遇到了游标超时错误,怎么办呢?你至少可以有以下一些选择:

延长游标超时时间,请参考cursorTimeoutMillis;

加速应用的处理速度,处理得快了,下一次getmore自然就发生得更早;

不是那么直观,但是减小batchSize也可以达到同样的目的;

禁用超时时间(noCursorTimeout)——绝对不推荐使用。虽然可以达到目的,你也可以说我会在最后主动关闭游标的,但事实上总会发生这样那样的意外,导致你最终没有正确关闭游标,最后服务器上塞满了游标的情况也是很常见的。

例外情况

上面已经解释过,在游标超时的时候你得到的实际是“游标不存在”错误,而不是超时。那么反过来是不是也成立呢,“游标不存在”一定是超时了吗?离散数学告诉我们,一个命题的逆命题不一定成立。事实上也是如此。“游标不存在”的另一种可能性是有些用户热衷于在MongoDB前面加上负载均衡/自动故障恢复的软/硬件。我们已经知道游标是存在于一台服务器上的,如果你的负载均衡毫无原则地将请求转发到任意服务器上,getmore同时会因为找不到游标而出现“游标不存在”的错误。
事实上MongoDB和其驱动本身就已经能够完成高可用和负载均衡,并不需要额外画蛇添足。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/19441.html

相关文章

  • 有坑勿踩(三)——关于数据更新

    摘要:前言数据更新,中的,对任何数据库而言都是最基本的操作。你并不能保证数据在被你读出来到写回去期间是否有别人已经改了数据库中的记录,这就是第一个风险,操作存在潜在的可能性会覆盖掉别人更新过的数据。 前言 数据更新,CRUD中的U,对任何数据库而言都是最基本的操作。看似简单的更新操作中会藏着哪些坑?今天聊一聊这个话题。 在写这个系列文章时,我会假设读者已经对MongoDB有了最基础的了解,因...

    mengera88 评论0 收藏0
  • 有坑勿踩(一):MongoDB PSS vs PSA

    摘要:注意记住的作用始终是把集群中具有投票权的节点总数凑成奇数用,防止脑裂。其代表的意义是集群中必须有大多数节点收到并确认了一个写操作,这个写操作才算成功。无论源或者目标片中不能够满足大多数时,迁移都会失败。在有可能的情况下,应尽量使用代替。 前言 在技术社区混了这么长时间,因为一些常见的技术问题反复被问到,总是想写写文章把它们讲清楚。无奈很多时候看似基础的技术问题背后都隐藏着很深的原因,想...

    Freelander 评论0 收藏0
  • python中操作mysql的pymysql模块详解

    摘要:简述是中操作的模块,其使用方法和几乎相同。但目前支持而后者不支持版本。因此要避免这种情况需使用提供的参数化查询。使用存储过程动态执行防注入使用存储过程自动提供防注入,动态传入到存储过程执行语句。 简述 pymsql是Python中操作MySQL的模块,其使用方法和MySQLdb几乎相同。但目前pymysql支持python3.x而后者不支持3.x版本。本文测试python版本:3.5....

    shiweifu 评论0 收藏0

发表评论

0条评论

bawn

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<