Pymongo 偶尔报 CursorNotFound 问题定位
/ / 阅读数:2104摘要 :使用 Pymongo Cursor 分批查询数据时,偶尔出现 CursorNotFound 异常, 原来是 LoadBalancer HA 机制导致 Cursor 访问到了不同的 mongos 后端。
问题描述
线上某服务偶尔会抛出CursorNotFound
Exception,导致接口返回 500 错误。
该接口有一个 MongoDB 数据库查询,每当查询结果超过 100 条时,就有一定的概率抛该异常。
报错时的异常堆栈如下:
Traceback (most recent call last): File "/usr/local/lib/python2.7/dist-packages/nameko_cacheback.py", line 153, in fetch *args, **kwargs) File "/usr/local/lib/python2.7/dist-packages/eventlet/timeout.py", line 141, in with_timeout return function(*args, **kwds) File "./service/xxx_service.py", line 1150, in xxx return [data['xxx'] for data in res if 'xxx' in data] File "/usr/local/lib/python2.7/dist-packages/pymongo/cursor.py", line 1189, in next if len(self.__data) or self._refresh(): File "/usr/local/lib/python2.7/dist-packages/pyzipkin/patcher/pymongo.py", line 179, in _patched_refresh res = __org_refresh__(self, *args, **kwargs) File "/usr/local/lib/python2.7/dist-packages/pymongo/cursor.py", line 1126, in _refresh self.__send_message(g) File "/usr/local/lib/python2.7/dist-packages/pymongo/cursor.py", line 978, in __send_message codec_options=self.__codec_options) File "/usr/local/lib/python2.7/dist-packages/pymongo/cursor.py", line 1067, in _unpack_response return response.unpack_response(cursor_id, codec_options) File "/usr/local/lib/python2.7/dist-packages/pymongo/message.py", line 1418, in unpack_response self.raw_response(cursor_id) File "/usr/local/lib/python2.7/dist-packages/pymongo/message.py", line 1384, in raw_response raise CursorNotFound(msg, 43, errobj) CursorNotFound: Cursor not found, cursor id: 7615467649098 |
问题定位
Google CursorNotFound 原因
通过网上 Googlepymongo CursorNotFound
,mongo 获取数据时,不是一次性把所有数据都读下来,
而是分批按需去获取。
一般以下情况会触发该异常:
- Cursor 默认存活时期为 10 分钟 超过这个时间 mongos 会清理掉这个 Cursor。
- Cursor 迭代完了以后,重新开始迭代
当 Cursor 中的数据迭代完以后会自动关闭 Cursor,重新使用会报
CursorNotFound
。 - 使用 LoadBalancer 连接多个 mongos 后端 当多个 mongos 部署在 loadbalancer(lb) 后方,使用 Cursor 从 mongos 多次读取数据的时使用了不同的链接。
- 使用域名访问多个 mongos 后端 动态 DNS 返回多个 mongos 节点,使用 Cursor 从 mongos 多次读取数据的时使用了不同的链接。
上面 4 种情况,符合我们的情况的是第 3 条。
验证 LoadBalander 导致 CursorNotFound
为了验证线上服务确实触发了第 3 种情况,先找一个文档数会超过 100 的记录,然后写个脚本周期性的去请求接口。 使用 tcpdump 抓取连接到 lb 的流量包,将结果存到 cap 文件中,然后使用 Wireshark 进行分析。
Wireshark 支持 Mongo 的协议分析:编辑->首选项->Protocols
选择MONGO
。
Mongo 的协议参看: https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/ 。
某次请求的抓包情况如下:
其中第 212 条响应中含有CursorNotFound
错误。可以看到,第一次成功Get More
源端口是 42984,而异常的端口是 50031,
显然确实出现了相同的 Cursor 两次请求使用了不同 TCP 连接去访问 lb 及后端 mongos,导致CursorNotFound
问题。
问题原因
至于为什么同一个 Cursor 会使用不同的连接去请求数据,这个涉及到 pymongo(当前版本是 v3.7.2)的实现:
client = MongoClient('mongodb://host1,host2,host3/?localThresholdMS=30') |
- pymongo 支持同时连接多个 mongos(每个 host 对应一个 mongos) 表示一个 MongoClient 客户端可以和多个 mongos 同时建立连接。
- 每个 mongos 连接都由一个 server 对象表示,server 对象包含一个连接池 表示一个 MongoClient 客户端可以和每个 mongos 同时建立多个连接,在 pymongo 看来,这些连接都是对等的。
- 当使用 find 获取一批时,会先创建一个 Curosr 对象
- Cursor 对象会关联到当前网络延迟小的 server 对象
- server 对象随机从连接池获取一个 mongos 连接
- mongos 端创建并维护 cursor id, 返回一批数据
- 当继续枚举下一批数据时,Cursor 使用关联的 server
- server 会再随机从连接池选一个 mongos 连接(问题就出现在这里,这里可能选到不同的 mongos)
- 如果选到原来的 mongos,则返回正常,否则抛
CursorNotFound
异常
解决方案
- 按官方推荐的采用写死多个 mongos 的方案
- 采用 LoadBalancer,但是需要支持按客户端来源 IP 分配固定的后端 mongos
- 采用域名方式,但是客户端解析多个 IP,并固定使用某个 IP