(原創(chuàng)) 使用pymongo 3.6.0連接MongoDB的正確姿勢(shì)

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

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的解釋中找到了答案:

圖2

簡(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

圖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', ...)
    1. "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://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
    1. SSL
      一堆的參數(shù),還沒(méi)有用過(guò),自行看文檔吧。

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ù)同步延遲

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容