0.疑惑
前兩天使用pymongo連接MongoDB的時(shí)候發(fā)現(xiàn)了一個(gè)奇怪的現(xiàn)象:我本機(jī)MongoDB并沒(méi)有打開(kāi),但是使用pymong.MongoClient()進(jìn)行連接時(shí),并沒(méi)有異常,我的服務(wù)端也正常跑起來(lái)了,直到收到請(qǐng)求,進(jìn)行數(shù)據(jù)庫(kù)查詢操作的時(shí)候,等了相當(dāng)長(zhǎng)的一段時(shí)間之后,服務(wù)端才由于MongoDB連接不上報(bào)異常。
Note: 本機(jī)環(huán)境pymongo 3.6.0,MongoDB 3.4.6
不信?可以打開(kāi)ipython,輸入如下命令:
from pymongo import MongoClient
client = MongoClient('aaa', 1234)
db = client.database
task = db.task
怎么樣?是不是一直ok
再執(zhí)行下面這條命令呢?
task.count()
等待相當(dāng)長(zhǎng)的一段時(shí)候后報(bào)錯(cuò)了:
ServerSelectionTimeoutError: aaa:1234: [Errno 11001] getaddrinfo failed
如圖1所示

1.解答
那這是為什么呢?
按一般的理解,MongoClient接口實(shí)現(xiàn)的時(shí)候肯定要考慮MongoDB連接異常的情況,只有確保連接成功建立,才能一步一步往下取database、取collection、基于collection進(jìn)行增刪改查操作等等。
這里為什么不呢 ?我發(fā)現(xiàn)了一個(gè)大BUG?不應(yīng)該啊,pymongo發(fā)布使用的時(shí)間比我用Python寫(xiě)代碼都要長(zhǎng),這要是bug早該解決了。
如何使用pymongo連接MongoDB,網(wǎng)上確實(shí)有很多很多博客,不過(guò)絕大多數(shù)都很簡(jiǎn)單,基本就我上面那幾行的正確使用而已,頂多再提醒一下安裝pymongo的時(shí)候不能安裝第三方bson,因?yàn)閜ymongo自帶bson,兩者不匹配,講如何進(jìn)行密碼驗(yàn)證的都很少,更別說(shuō)replSet參數(shù)的使用。
終于還是在官網(wǎng)的API接口文檔中MongoClient的解釋中找到了答案:

簡(jiǎn)單翻譯一下:
從pymongo3.0版本開(kāi)始,MongoClient的構(gòu)造函數(shù)就不會(huì)再阻塞等待MongoDB連接的建立,即使連接不上也不會(huì)上報(bào)ConnectionFailure,用戶提交的資格證書(shū)(估計(jì)是用戶名密碼或者cert證書(shū))是錯(cuò)誤的也不會(huì)上報(bào)ConfigurationError。相反,構(gòu)造函數(shù)會(huì)立即返回并在后臺(tái)線程中加載處理連接數(shù)據(jù)的進(jìn)程。如果想確認(rèn)返回的client是否真實(shí)可用,可以如下操作:
# The ismaster command is cheap and does not require auth.
client.admin.command('ismaster')
這說(shuō)明至少3.0之前版本的設(shè)計(jì)和我的想法是一樣的,那現(xiàn)在為什么換了高級(jí)玩法呢?還是不太明白
至于為什么執(zhí)行命令時(shí)上報(bào)異常的時(shí)間比較長(zhǎng)呢?

圖3和圖1中的時(shí)間差達(dá)到了近40s
因?yàn)閮蓚€(gè)參數(shù):
connectTimeoutMS,連接mongo的超時(shí)機(jī)制, 默認(rèn)20s
serverSelectionTimeoutMS,連接database的超時(shí)機(jī)制, 默認(rèn)30s
雖然上面說(shuō)過(guò)MongoClient的構(gòu)造函數(shù)不再阻塞建立連接,但那個(gè)note上面還有一句話:
Note: MongoClient creation will block waiting for answers from DNS when mongodb+srv:// URIs are used.
當(dāng)使用"mongodb+srv://"形式的URI連接數(shù)據(jù)庫(kù)服務(wù)器時(shí),MongoClient將會(huì)阻塞等到DNS的域名解析結(jié)果,但同樣不是數(shù)據(jù)庫(kù)的連接。估計(jì)這種"mongodb+srv://"形式的URI是用來(lái)連接MongoDB去年提出的云服務(wù)吧,要不哪來(lái)的DNS解析需求呢。
2.更新匯總
簡(jiǎn)單說(shuō)一下MongoClient中提到的一些更新要點(diǎn):
- 版本3.6:新增mongodb+srv://形式的URI,新增retryWrites 關(guān)鍵字變量和URI選項(xiàng)
- 版本3.5:新增'username'和'password'兩個(gè)選項(xiàng)。新增'authSource'、'authMechanism'、'authMechanismProperties' 三個(gè)選項(xiàng)的文檔。舍棄'socketKeepAlive'關(guān)鍵字變量和URI選項(xiàng)。
socketKeepAlive默認(rèn)值改為True。 - 版本3.0:"pymongo.mongo_client.MongoClient"現(xiàn)在是唯一的client類,應(yīng)用于獨(dú)立的Mongo服務(wù)器、多臺(tái)Mongos、Mongo集群。它兼容了“MongoReplicaSetClient”的功能,可以連接Mongo集群、尋找集群成員等操作,后者已被棄用。
- MongoClient的構(gòu)造函數(shù)就不會(huì)再阻塞等待MongoDB連接的建立,即使連接不上也不會(huì)上報(bào)ConnectionFailure,用戶提交的資格證書(shū)(估計(jì)是用戶名密碼或者cert證書(shū))是錯(cuò)誤的也不會(huì)上報(bào)ConfigurationError。相反,構(gòu)造函數(shù)會(huì)立即返回并在后臺(tái)線程中加載處理連接數(shù)據(jù)的進(jìn)程。
- 因此“alive”方法也棄用了,因?yàn)樗辉倌芴峁┯行畔?;如果服?wù)器連接斷開(kāi)了,在執(zhí)行下一次操作的時(shí)候異常就會(huì)被發(fā)現(xiàn)。
- 在Pymongo 2.x中,MongoClient可以接受單例數(shù)據(jù)庫(kù)的地址列表做參數(shù),并自動(dòng)連接第一個(gè)可用的數(shù)據(jù)庫(kù)。
MongoClient(['host1.com:27017', 'host2.com:27017'])
不再支持多服務(wù)器的地址列表,如果要給列表的話,這些服務(wù)器一定要配置在同一個(gè)集群中。
- 在mongo集群中行為不再講究“高可用”,而是“負(fù)載均衡”。因?yàn)橐郧爸皇窃谶B的時(shí)候優(yōu)先連接最低負(fù)載的服務(wù)器,除非網(wǎng)絡(luò)異常才會(huì)連別的,但實(shí)際上這個(gè)“最低”可能只是一時(shí)的。而在Pymongo 3.x中,改為統(tǒng)一監(jiān)控集群網(wǎng)絡(luò)實(shí)時(shí)負(fù)載了。
- 新增“connect” URI選項(xiàng)(True立即連接,false第一個(gè)操作時(shí)才連接)
connect參數(shù)我試過(guò)并沒(méi)有作用,或許在3.6版本中一并失效了吧
- “start_request”、“in_request”、“end_request ”三個(gè)方法和“auto_start_request ”選項(xiàng)都被移除了。
- “copy_database ”方法被移除了
- MongoClient.disconnect()被移除了,它和close()一樣的。
- MongoClient不再支持以實(shí)例屬性方式讀取下劃線開(kāi)頭命名的數(shù)據(jù)庫(kù)屬性了,必須以字典形式讀取。
YES: client['__my_database__'], NO: client.__my_database__
3.總結(jié)
- 1)兩種基本的連接方法,一種是使用keyword argument(關(guān)鍵字變量),另一種是MongoDB URI format(URI參數(shù))
from pymongo import MongoClient
client = MongoClient()
# keyword argument
client = MongoClient('localhost', 27017)
# MongoDB URI
client = MongoClient('mongodb://localhost:27017/')
- 2)用戶名密碼驗(yàn)證
note: MongoDB 3.0(對(duì)應(yīng)pymongo2.8)之后默認(rèn)使用“SCRAM-SHA-1”加解密;之前使用的是“MONGODB-CR”,可以使用authMechanism指定;同時(shí)可以使用authSource指定應(yīng)用加解密的database,默認(rèn)是admin。
# since MongoDB 3.0, SCRAM-SHA-1
from pymongo import MongoClient
# keyword argument
client = MongoClient('example.com',
username='user',
password='password',
authSource='the_database',
authMechanism='SCRAM-SHA-1')
# MongoDB URI
uri = "mongodb://user:password@example.com/the_database?authMechanism=SCRAM-SHA-1"
client = MongoClient(uri)
- 3)replSet
假設(shè)本地有如下的集群配置
config = {'_id': 'foo', 'members': [
{'_id': 0, 'host': 'localhost:27017'},
{'_id': 1, 'host': 'localhost:27018'},
{'_id': 2, 'host': 'localhost:27019'}]}
可以通過(guò)replSet參數(shù)指定集群名稱(_id),主庫(kù)、從庫(kù)等都可以讀取到,這里就不細(xì)說(shuō)了
>>> MongoClient('localhost', replicaset='foo')
MongoClient(host=['localhost:27017'], replicaset='foo', ...)
>>> MongoClient('localhost:27018', replicaset='foo')
MongoClient(['localhost:27018'], replicaset='foo', ...)
>>> MongoClient('localhost', 27019, replicaset='foo')
MongoClient(['localhost:27019'], replicaset='foo', ...)
>>> MongoClient('mongodb://localhost:27017,localhost:27018/?replicaSet=foo')
MongoClient(['localhost:27017', 'localhost:27018'], replicaset='foo', ...)
- "mongodb+srv://"
這種形式的URL只支持一個(gè)hostname,對(duì)應(yīng)DNS server,進(jìn)行SRV record查詢,它也支持replSet和authSource(TXT record),需要注意的是它默認(rèn)使用TLS,即ssl=True。
具體的說(shuō)明還是看initial-dns-seedlist-discovery
假設(shè)我們使用
- "mongodb+srv://"
mongodb+srv://server.mongodb.com/
而DNS server(_mongodb._tcp.server.mongodb.com)上有如下SRV record
Record TTL Class Priority Weight Port Target
_mongodb._tcp.server.mongodb.com. 86400 IN SRV 0 5 27317 mongodb1.mongodb.com.
_mongodb._tcp.server.mongodb.com. 86400 IN SRV 0 5 27017 mongodb2.mongodb.com.
且server.mongdb.com存在如下Txt records
Record TTL Class Text
server.mongodb.com. 86400 IN TXT "replicaSet=replProduction&authSource=authDB"
那么對(duì)應(yīng)解析結(jié)果就是:
mongodb://mongodb1.mongodb.com:27317,mongodb2.mongodb.com:27107/?ssl=true&replicaSet=replProduction&authSource=authDB
- SSL
一堆的參數(shù),還沒(méi)有用過(guò),自行看文檔吧。
- SSL
4.代碼示例
此處給出一份使用pymongo3.6連接MongoDB的代碼示例,分別是OPTION和URI兩種方式
主要考慮集群配置和密碼校驗(yàn)兩個(gè)方面,假設(shè)配置文件如下
MONGODB = {
'host': '127.0.0.1',
'port': '27017',
'user': '',
'pwd': '',
'db': 'test',
'replicaSet': {
'name': 'abc',
"members": [
{
"host": "localhost",
"port": "27017"
},
{
"host": "localhost",
"port": "27027"
},
{
"host": "localhost",
"port": "27037"
}
]
}
}
約定如下:
replicaSet的name為空則不使用集群配置
user和pwd為空則不需要進(jìn)行密碼校驗(yàn)
db不給出則默認(rèn)為“admin”
則OPTION方式:
import urllib.parse
import pymongo
from config import MONGODB
if MONGODB['replicaSet']['name']:
host_opt = []
for m in MONGODB['replicaSet']['members']:
host_opt.append('%s:%s' % (m['host'], m['port']))
replicaSet = MONGODB['replicaSet']['name']
else:
host_opt = '%s:%s' % (MONGODB['host'], MONGODB['port'])
replicaSet = None
option = {
'host': host_opt,
'authSource': MONGODB['db'] or 'admin', # 指定db,默認(rèn)為'admin'
'replicaSet': replicaSet,
}
if MONGODB['user'] and MONGODB['pwd']:
# py2中為urllib.quote_plus
option['username'] = urllib.parse.quote_plus(MONGODB['user'])
option['password'] = urllib.parse.quote_plus(MONGODB['pwd'])
option['authMechanism'] = 'SCRAM-SHA-1'
client = pymongo.MongoClient(**option)
URI方式
import urllib.parse
import pymongo
from config import MONGODB
# mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
params = []
host_info = ''
# 處理replicaSet設(shè)置
if MONGODB['replicaSet']['name']:
host_opt = []
for m in MONGODB['replicaSet']['members']:
host_opt.append('%s:%s' % (m['host'], m['port']))
host_info = (',').join(host_opt)
replicaSet_str = 'replicaSet=%s' % MONGODB['replicaSet']['name']
params.append(replicaSet_str)
else:
host_info = '%s:%s' % (MONGODB['host'], MONGODB['port'])
# 處理密碼校驗(yàn)
if MONGODB['user'] and MONGODB['pwd']:
# py2中為urllib.quote_plus
username = urllib.parse.quote_plus(MONGODB['user'])
password = urllib.parse.quote_plus(MONGODB['pwd'])
auth_str = '%s:%s@' % (username, password)
params.append('authMechanism=SCRAM-SHA-1')
else:
auth_str = ''
if params:
param_str = '?' + '&'.join(params)
else:
param_str = ''
uri = 'mongodb://%s%s/%s%s' % (auth_str, host_info, MONGODB['db'], param_str)
client = pymongo.MongoClient(uri)
假設(shè)db中有collection名為TEST_COL,可以如下驗(yàn)證client的有效性:
database = client[MONGODB['db']]
print(database.TEST_COL.count())
# client.run.command({'count': 'TEST_COL'}) # 需要權(quán)限
# client.admin.command('ismaster') # 不支持副本集環(huán)境
4.配置副本集讀寫(xiě)分離
from pymongo import ReadPreference
db = conn.get_database(MONGODB['db'], read_preference=ReadPreference.SECONDARY_PREFERRED)
副本集ReadPreference有5個(gè)選項(xiàng):
- PRIMARY:默認(rèn)選項(xiàng),從primary節(jié)點(diǎn)讀取數(shù)據(jù)
- PRIMARY_PREFERRED:優(yōu)先從primary節(jié)點(diǎn)讀取,如果沒(méi)有primary節(jié)點(diǎn),則從集群中可用的secondary節(jié)點(diǎn)讀取
- SECONDARY:從secondary節(jié)點(diǎn)讀取數(shù)據(jù)
- SECONDARY_PREFERRED:優(yōu)先從secondary節(jié)點(diǎn)讀取,如果沒(méi)有可用的secondary節(jié)點(diǎn),則從primary節(jié)點(diǎn)讀取
- NEAREST:從集群中可用的節(jié)點(diǎn)讀取數(shù)據(jù)
5. 參考
PyMongo 3.6.0 Documentation
利用python測(cè)試mongodb副本集數(shù)據(jù)同步延遲