文档菜单

文档首页开发应用程序Python 驱动PyMongo

故障排除

本页内容

  • 连接
  • 读写操作
  • 游标
  • 投影
  • 索引
  • 数据格式
  • TLS
  • 客户端操作超时
  • 进程复制

本页中,您可以找到使用 PyMongo 与 MongoDB 遇到的常见问题的解决方案。

如果您尝试连接到 MongoDB 服务器 v3.4 或更早版本,PyMongo 可能会引发以下错误

pymongo.errors.ConfigurationError: Server at localhost:27017 reports wire version 5, but this version of PyMongo requires at least 6 (MongoDB 3.6).

这发生在驱动程序版本对连接的服务器来说太新时。要解决此问题,请将您的 MongoDB 部署升级到 v3.6 或更高版本,或者降级到支持 MongoDB 服务器 v2.6 及更高版本的 PyMongo v3.x。

一个自动重连 异常表示一个故障转移已发生。这意味着PyMongo已失去与副本集原始主成员的连接,其最后操作可能已失败。

当出现此错误时,PyMongo会自动尝试为新操作查找新的主成员。为了处理此错误,您的应用程序必须采取以下操作之一

  • 重试可能已失败的操作

  • 继续运行,理解操作可能已失败

重要

在副本集选举新的主成员之前,PyMongo将在所有操作上抛出AutoReconnect错误。

如果您尝试通过SSH隧道连接到MongoDB副本集,您将收到以下错误

File "/Library/Python/2.7/site-packages/pymongo/collection.py", line 1560, in count
return self._count(cmd, collation, session)
File "/Library/Python/2.7/site-packages/pymongo/collection.py", line 1504, in _count
with self._socket_for_reads() as (connection, slave_ok):
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/contextlib.py", line 17, in __enter__
return self.gen.next()
File "/Library/Python/2.7/site-packages/pymongo/mongo_client.py", line 982, in _socket_for_reads
server = topology.select_server(read_preference)
File "/Library/Python/2.7/site-packages/pymongo/topology.py", line 224, in select_server
address))
File "/Library/Python/2.7/site-packages/pymongo/topology.py", line 183, in select_servers
selector, server_timeout, address)
File "/Library/Python/2.7/site-packages/pymongo/topology.py", line 199, in _select_servers_loop
self._error_message(selector))
pymongo.errors.ServerSelectionTimeoutError: localhost:27017: timed out

这是因为PyMongo通过isMaster命令的响应发现副本集成员,其中包含其他副本集成员的地址和端口。但是,您无法通过SSH隧道访问这些地址和端口。

相反,您可以使用SSH隧道并使用directConnection=True选项直接连接到单个MongoDB节点。

如果您在读取优先级中指定了tag-sets,而MongoDB无法找到具有指定标签的副本集成员,则会收到此错误。为了避免此错误,在标签集列表末尾包含一个空字典({})。这指示PyMongo在无法找到匹配的标签时从任何匹配读取参考模式的成员读取。

PyMongo不再支持count()方法。相反,使用Collection类中的count_documents()方法。

重要

count_documents()方法属于Collection类。如果您尝试调用Cursor.count_documents(),PyMongo将抛出以下错误

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Cursor' object has no attribute 'count'

提供无效的关键字参数名称会导致驱动程序抛出此错误。

请确保您指定的关键字参数存在且拼写正确。

在Web应用程序中,将文档的ObjectId编码到URL中很常见,如下面的代码示例所示

"/posts/50b3bda58a02fb9a84d8991e"

您的Web框架将URL中的ObjectId部分作为一个字符串传递给请求处理器。在将字符串传递给find_one()方法之前,您必须将其转换为ObjectId实例。

以下代码示例展示了如何在Flask应用程序中执行此转换。对于其他Web框架,此过程类似。

from pymongo import MongoClient
from bson.objectid import ObjectId
from flask import Flask, render_template
client = MongoClient()
app = Flask(__name__)
@app.route("/posts/<_id>")
def show_post(_id):
# NOTE!: converting _id from string to ObjectId before passing to find_one
post = client.db.posts.find_one({'_id': ObjectId(_id)})
return render_template('post.html', post=post)
if __name__ == "__main__":
app.run()

在始终位于第一位的_id字段之后,BSON文档中的键值对可以以任何顺序排列。Mongo shell在读取和写入数据时保留键顺序,如下面的代码示例中的“b”和“a”字段所示

// mongo shell
db.collection.insertOne( { "_id" : 1, "subdocument" : { "b" : 1, "a" : 1 } } )
// Returns: WriteResult({ "nInserted" : 1 })
db.collection.findOne()
// Returns: { "_id" : 1, "subdocument" : { "b" : 1, "a" : 1 } }

PyMongo默认将BSON文档表示为Python字典,字典中键的顺序未定义。在Python中,以“a”键首先声明的字典与以“b”键首先声明的字典相同。在以下示例中,无论在print语句中键的顺序如何,都会显示相同的顺序

print({'a': 1.0, 'b': 1.0})
# Returns: {'a': 1.0, 'b': 1.0}
print({'b': 1.0, 'a': 1.0})
# Returns: {'a': 1.0, 'b': 1.0}

同样,Python字典可能不会按它们在BSON中存储的顺序显示键。以下示例显示了在前面示例中插入的文档的打印结果

print(collection.find_one())
# Returns: {'_id': 1.0, 'subdocument': {'a': 1.0, 'b': 1.0}}

要保留读取BSON时的键顺序,请使用SON类,这是一个记住其键顺序的字典。

以下代码示例展示了如何创建一个配置为使用SON类的集合

from bson import CodecOptions, SON
opts = CodecOptions(document_class=SON)
CodecOptions(document_class=...SON..., tz_aware=False, uuid_representation=UuidRepresentation.UNSPECIFIED, unicode_decode_error_handler='strict', tzinfo=None, type_registry=TypeRegistry(type_codecs=[], fallback_encoder=None), datetime_conversion=DatetimeConversion.DATETIME)
collection_son = collection.with_options(codec_options=opts)

当您找到前面的子文档时,驱动程序使用SON对象表示查询结果,并保留键顺序

print(collection_son.find_one())
SON([('_id', 1.0), ('subdocument', SON([('b', 1.0), ('a', 1.0)]))])

子文档的实际存储布局现在可见:“b”在“a”之前。

由于Python字典的键顺序未定义,因此您无法预测它将如何序列化为BSON。但是,MongoDB只有在子文档的键顺序相同时才认为它们相等。如果您使用Python字典查询子文档,它可能不会匹配

collection.find_one({'subdocument': {'b': 1.0, 'a': 1.0}}) is None
True

由于Python认为这两个字典相同,因此在查询中交换键顺序没有任何区别

collection.find_one({'subdocument': {'b': 1.0, 'a': 1.0}}) is None
True

您可以通过两种方式解决这个问题。首先,您可以按字段匹配子文档

collection.find_one({'subdocument.a': 1.0,
'subdocument.b': 1.0})
{'_id': 1.0, 'subdocument': {'a': 1.0, 'b': 1.0}}

查询匹配任何具有1.0“a”和1.0“b”的子文档,无论您在Python中指定它们的顺序如何,或者它们在BSON中存储的顺序如何。此查询现在还匹配具有“a”和“b”以外的附加键的子文档,而前面的查询需要精确匹配。

第二种解决方案是使用一个~bson.son.SON对象来指定键的顺序

query = {'subdocument': SON([('b', 1.0), ('a', 1.0)])}
collection.find_one(query)
{'_id': 1.0, 'subdocument': {'a': 1.0, 'b': 1.0}}

在将~bson.son.SON序列化为BSON并用作查询时,驱动程序会保留您创建时的键顺序。因此,您可以创建一个与集合中子文档完全匹配的子文档。

注意

有关子文档匹配的更多信息,请参阅MongoDB服务器文档中的查询嵌入式/嵌套文档指南。

PyMongo v3.8或更早版本,如果您向Cursor构造函数提供无效参数,则会引发一个TypeError和一个AttributeError。这个AttributeError是无关紧要的,但这个TypeError包含了如下示例中的调试信息。

Exception ignored in: <function Cursor.__del__ at 0x1048129d8>
...
AttributeError: 'Cursor' object has no attribute '_Cursor__killed'
...
TypeError: __init__() got an unexpected keyword argument '<argument>'

为了解决这个问题,请确保您提供了正确的关键字参数。您还可以升级到PyMongo v3.9或更高版本,这会移除无关的错误。

如果游标在MongoDB服务器上长时间打开而没有进行任何操作,则MongoDB的游标可能会超时。当您尝试遍历游标时,这可能导致一个CursorNotFound异常。

如果您尝试在单个投影中包含和排除字段,驱动程序会返回一个带有此消息的OperationFailure。请确保您的投影只指定要包含的字段或要排除的字段。

如果您执行了一个写入操作,该操作存储了一个违反唯一索引的重复值,则驱动程序会引发一个DuplicateKeyException,MongoDB会抛出一个类似于以下错误的消息。

E11000 duplicate key error index

此错误是由于在UUID表示为UNSPECIFIED时尝试将原生UUID对象编码为Binary对象而导致的,如下面的代码示例所示

unspecified_collection.insert_one({'_id': 'bar', 'uuid': uuid4()})
Traceback (most recent call last):
...
ValueError: cannot encode native uuid.UUID with UuidRepresentation.UNSPECIFIED.
UUIDs can be manually converted to bson.Binary instances using bson.Binary.from_uuid()
or a different UuidRepresentation can be configured. See the documentation for
UuidRepresentation for more information.

相反,您必须通过使用Binary.from_uuid()方法显式地将原生UUID转换为Binary对象,如下面的示例所示

explicit_binary = Binary.from_uuid(uuid4(), UuidRepresentation.STANDARD)
unspec_collection.insert_one({'_id': 'bar', 'uuid': explicit_binary})

PyMongo将BSON datetime值解码为Python的datetime.datetime类的实例。这些实例的限制是年份在datetime.MINYEAR(1)和datetime.MAXYEAR(9999)之间。一些MongoDB驱动程序可以存储BSON日期时间,其年份值远远超出了datetime.datetime支持的范围。

有几种方法可以解决这个问题。从PyMongo 4.3版本开始,bson.decode可以以四种方式之一解码BSON datetime值。您可以通过使用~bson.codec_options.CodecOptionsdatetime_conversion参数来指定转换方法。

默认转换选项是~bson.codec_options.DatetimeConversion.DATETIME,它将尝试将值解码为datetime.datetime,允许对于超出范围的日期发生~builtin.OverflowError。而~bson.codec_options.DatetimeConversion.DATETIME_AUTO将改变此行为,当表示超出范围时,返回~bson.datetime_ms.DatetimeMS,而返回之前作为~datetime.datetime对象

from datetime import datetime
from bson.datetime_ms import DatetimeMS
from bson.codec_options import DatetimeConversion
from pymongo import MongoClient
client = MongoClient(datetime_conversion=DatetimeConversion.DATETIME_AUTO)
client.db.collection.insert_one({"x": datetime(1970, 1, 1)})
client.db.collection.insert_one({"x": DatetimeMS(2**62)})
for x in client.db.collection.find():
print(x)
{'_id': ObjectId('...'), 'x': datetime.datetime(1970, 1, 1, 0, 0)}
{'_id': ObjectId('...'), 'x': DatetimeMS(4611686018427387904)}

有关其他选项,请参阅DateTimeConversion类的API文档。

另一个不涉及设置datetime_conversion的选项是过滤掉超出~datetime.datetime支持的文档值

from datetime import datetime
coll = client.test.dates
cur = coll.find({'dt': {'$gte': datetime.min, '$lte': datetime.max}})

如果您不需要datetime的值,您可以只过滤掉该字段

cur = coll.find({}, projection={'dt': False})

以下类似的错误信息表示 OpenSSL 无法验证服务器的证书

[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed

这通常是因为 OpenSSL 无法访问系统的根证书,或者因为证书已过时。

如果您使用 Linux,请确保您已从 Linux 供应商那里安装了最新的根证书更新。

如果您使用 macOS,并且您从 python.org 下载了 Python v3.7 或更高版本,请运行以下命令来安装根证书

open "/Applications/Python <YOUR PYTHON VERSION>/Install Certificates.command"

提示

有关此问题的更多信息,请参阅 Python 问题 29065。

如果您使用便携式 pypy,可能需要设置一个环境变量来告诉 OpenSSL 从哪里找到根证书。以下代码示例显示了如何从 PyPi 安装 certifi 模块 并导出 SSL_CERT_FILE 环境变量

$ pypy -m pip install certifi
$ export SSL_CERT_FILE=$(pypy -c "import certifi; print(certifi.where())")

提示

有关此问题的更多信息,请参阅portable-pypy问题15。

以下类似的错误消息表示Python使用的OpenSSL版本不支持足够的TLS协议来连接服务器

[SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version

行业最佳实践建议,某些法规要求在某些MongoDB部署中禁用较旧的TLS协议。某些部署可能会禁用TLS 1.0,而其他部署可能会禁用TLS 1.0和TLS 1.1。

PyMongo使用最新的TLS版本不需要进行任何应用程序更改,但某些操作系统版本可能无法提供足够的OpenSSL版本来支持它们。

如果您使用的是macOS v10.12(High Sierra)或更早版本,请从python.org、homebrew、macports或类似来源安装Python。

如果您使用Linux或其他非macOS Unix,请使用以下命令检查您的OpenSSL版本

$ openssl version

如果前面的命令显示的版本号小于1.0.1,则不支持TLS 1.1或更高版本。升级到新版本或联系您的操作系统供应商以获得解决方案。

要检查Python解释器的TLS版本,请安装requests模块并执行以下代码

python -c "import requests; print(requests.get('https://www.howsmyssl.com/a/check', verify=False).json()['tls_version'])"

您应该看到TLS 1.1或更高版本。

以下类似的错误消息表示证书吊销检查失败

[('SSL routines', 'tls_process_initial_server_flight', 'invalid status response')]

有关更多详细信息,请参阅本指南的OCSP部分。

当使用Python v3.10或更高版本与MongoDB版本低于v4.0时,您可能会看到以下类似的错误消息

SSL handshake failed: localhost:27017: [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:997)
SSL handshake failed: localhost:27017: EOF occurred in violation of protocol (_ssl.c:997)

MongoDB服务器日志也可能显示以下错误

2021-06-30T21:22:44.917+0100 E NETWORK [conn16] SSL: error:1408A0C1:SSL routines:ssl3_get_client_hello:no shared cipher

Python 3.10 版本中对 ssl 模块的更改可能导致与 v4.0 之前的 MongoDB 版本不兼容。为了解决这个问题,请尝试以下步骤之一或多个

  • 将 Python 版本降级到 3.9 或更早版本

  • 将 MongoDB 服务器升级到 4.2 或更高版本

  • 使用依赖 PyOpenSSL 的 OCSP 选项安装 PyMongo

此错误表示客户端在给定超时时间内找不到可运行操作的可用服务器

pymongo.errors.ServerSelectionTimeoutError: No servers found yet, Timeout: -0.00202266700216569s, Topology Description: <TopologyDescription id: 63698e87cebfd22ab1bd2ae0, topology_type: Unknown, servers: [<ServerDescription ('localhost', 27017) server_type: Unknown, rtt: None>]>

此错误表示客户端在给定超时时间内无法建立连接,或者操作已发送但服务器没有及时响应

pymongo.errors.NetworkTimeout: localhost:27017: timed out

此错误可能表示服务器取消操作,因为它超出了给定超时。即使 PyMongo 抛出此异常,操作也可能在服务器上部分完成。

pymongo.errors.ExecutionTimeout: operation exceeded time limit, full error: {'ok': 0.0, 'errmsg': 'operation exceeded time limit', 'code': 50, 'codeName': 'MaxTimeMSExpired'}

它还可能表示客户端取消操作,因为无法在给定超时内完成它。

pymongo.errors.ExecutionTimeout: operation would exceed time limit, remaining timeout:0.00196 <= network round trip time:0.00427

此错误表示服务器无法在给定超时时间内并遵循指定的写入关注策略完成请求的写入操作

pymongo.errors.WTimeoutError: operation exceeded time limit, full error: {'code': 50, 'codeName': 'MaxTimeMSExpired', 'errmsg': 'operation exceeded time limit', 'errInfo': {'writeConcern': {'w': 1, 'wtimeout': 0}}}

此错误表示服务器无法在给定超时时间内并遵循指定的写入关注策略完成 insert_many()bulk_write() 方法

pymongo.errors.BulkWriteError: batch op errors occurred, full error: {'writeErrors': [], 'writeConcernErrors': [{'code': 50, 'codeName': 'MaxTimeMSExpired', 'errmsg': 'operation exceeded time limit', 'errInfo': {'writeConcern': {'w': 1, 'wtimeout': 0}}}], 'nInserted': 2, 'nUpserted': 0, 'nMatched': 0, 'nModified': 0, 'nRemoved': 0, 'upserted': []}

一个 MongoClient 实例会启动多个线程来运行后台任务,例如监控已连接的服务器。这些线程共享由 threading.Lock 类的实例保护的状态,而 threading.Lock 类的实例本身是不安全的进程分割。PyMongo 也会受到与使用 threading.Lock 类或其他互斥锁的任何多线程代码相同的限制。

这些限制之一是调用 fork() 方法后锁变得无用了。当 fork() 执行时,驱动程序会将父进程的所有锁复制到子进程中,状态与父进程中的状态相同。如果它们在父进程中是锁定的,它们在子进程中也是锁定的。由 fork() 创建的子进程只有一个线程,因此父进程中其他线程创建的任何锁在子进程中都不会释放。下一次子进程尝试获取这些锁时,就会发生死锁。

从PyMongo 4.3版本开始,在您调用 os.fork() 方法之后,驱动程序会使用 os.register_at_fork() 方法来重置子进程中的锁和其他共享状态。尽管这减少了死锁的可能性,但PyMongo依赖于在多线程应用程序中不安全的库,包括 OpenSSLgetaddrinfo(3).。因此,死锁仍然可能发生。

Linux手册页中关于 fork(2) 的限制如下

在一个多线程程序中进行 fork() 后,子进程只能安全地调用异步信号安全函数(参见signal-safety(7)),直到它调用execve(2).

因为 PyMongo 依赖于非异步信号安全函数,当在子进程中运行时,可能会导致死锁或崩溃。

提示

关于子进程中死锁的示例,请参见 Jira 中的PYTHON-3406

有关Python在多线程环境中使用fork()函数导致的锁定问题更多信息,请参阅Python问题跟踪器中的问题 6721

← 常见问题解答