(原創) 使用pymongo 3.6.0連接MongoDB的正確姿勢

0.疑惑

前兩天使用pymongo連接MongoDB的時候發現了一個奇怪的現象:我本機MongoDB并沒有打開,但是使用pymong.MongoClient()進行連接時,并沒有異常,我的服務端也正常跑起來了,直到收到請求,進行數據庫查詢操作的時候,等了相當長的一段時間之后,服務端才由于MongoDB連接不上報異常。

  Note: 本機環境pymongo 3.6.0,MongoDB 3.4.6

不信?可以打開ipython,輸入如下命令:

from pymongo import MongoClient
client = MongoClient('aaa', 1234)
db = client.database
task = db.task

怎么樣?是不是一直ok
再執行下面這條命令呢?

task.count()

等待相當長的一段時候后報錯了:

ServerSelectionTimeoutError: aaa:1234: [Errno 11001] getaddrinfo failed

如圖1所示


圖1

1.解答

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

圖2

簡單翻譯一下:
從pymongo3.0版本開始,MongoClient的構造函數就不會再阻塞等待MongoDB連接的建立,即使連接不上也不會上報ConnectionFailure,用戶提交的資格證書(估計是用戶名密碼或者cert證書)是錯誤的也不會上報ConfigurationError。相反,構造函數會立即返回并在后臺線程中加載處理連接數據的進程。如果想確認返回的client是否真實可用,可以如下操作:

# The ismaster command is cheap and does not require auth.
    client.admin.command('ismaster')

這說明至少3.0之前版本的設計和我的想法是一樣的,那現在為什么換了高級玩法呢?還是不太明白

至于為什么執行命令時上報異常的時間比較長呢?


圖3

圖3和圖1中的時間差達到了近40s
因為兩個參數:
connectTimeoutMS,連接mongo的超時機制, 默認20s
serverSelectionTimeoutMS,連接database的超時機制, 默認30s

雖然上面說過MongoClient的構造函數不再阻塞建立連接,但那個note上面還有一句話:

Note:  MongoClient creation will block waiting for answers from DNS when mongodb+srv:// URIs are used.

當使用"mongodb+srv://"形式的URI連接數據庫服務器時,MongoClient將會阻塞等到DNS的域名解析結果,但同樣不是數據庫的連接。估計這種"mongodb+srv://"形式的URI是用來連接MongoDB去年提出的云服務吧,要不哪來的DNS解析需求呢。

2.更新匯總

簡單說一下MongoClient中提到的一些更新要點:

  • 版本3.6:新增mongodb+srv://形式的URI,新增retryWrites 關鍵字變量和URI選項
  • 版本3.5:新增'username'和'password'兩個選項。新增'authSource'、'authMechanism'、'authMechanismProperties' 三個選項的文檔。舍棄'socketKeepAlive'關鍵字變量和URI選項。 socketKeepAlive默認值改為True。
  • 版本3.0:"pymongo.mongo_client.MongoClient"現在是唯一的client類,應用于獨立的Mongo服務器、多臺Mongos、Mongo集群。它兼容了“MongoReplicaSetClient”的功能,可以連接Mongo集群、尋找集群成員等操作,后者已被棄用。
  • MongoClient的構造函數就不會再阻塞等待MongoDB連接的建立,即使連接不上也不會上報ConnectionFailure,用戶提交的資格證書(估計是用戶名密碼或者cert證書)是錯誤的也不會上報ConfigurationError。相反,構造函數會立即返回并在后臺線程中加載處理連接數據的進程。
  • 因此“alive”方法也棄用了,因為它不再能提供有效信息;如果服務器連接斷開了,在執行下一次操作的時候異常就會被發現。
  • 在Pymongo 2.x中,MongoClient可以接受單例數據庫的地址列表做參數,并自動連接第一個可用的數據庫。
MongoClient(['host1.com:27017', 'host2.com:27017'])

不再支持多服務器的地址列表,如果要給列表的話,這些服務器一定要配置在同一個集群中。

  • 在mongo集群中行為不再講究“高可用”,而是“負載均衡”。因為以前只是在連的時候優先連接最低負載的服務器,除非網絡異常才會連別的,但實際上這個“最低”可能只是一時的。而在Pymongo 3.x中,改為統一監控集群網絡實時負載了。
  • 新增“connect” URI選項(True立即連接,false第一個操作時才連接)
connect參數我試過并沒有作用,或許在3.6版本中一并失效了吧
  • “start_request”、“in_request”、“end_request ”三個方法和“auto_start_request ”選項都被移除了。
  • “copy_database ”方法被移除了
  • MongoClient.disconnect()被移除了,它和close()一樣的。
  • MongoClient不再支持以實例屬性方式讀取下劃線開頭命名的數據庫屬性了,必須以字典形式讀取。
YES: client['__my_database__'],  NO: client.__my_database__

3.總結

  • 1)兩種基本的連接方法,一種是使用keyword argument(關鍵字變量),另一種是MongoDB URI format(URI參數)
from pymongo import MongoClient
client = MongoClient()
# keyword argument
client = MongoClient('localhost', 27017)
# MongoDB URI
client = MongoClient('mongodb://localhost:27017/')
  • 2)用戶名密碼驗證
    note: MongoDB 3.0(對應pymongo2.8)之后默認使用“SCRAM-SHA-1”加解密;之前使用的是“MONGODB-CR”,可以使用authMechanism指定;同時可以使用authSource指定應用加解密的database,默認是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
    假設本地有如下的集群配置
config = {'_id': 'foo', 'members': [
     {'_id': 0, 'host': 'localhost:27017'},
     {'_id': 1, 'host': 'localhost:27018'},
     {'_id': 2, 'host': 'localhost:27019'}]}

可以通過replSet參數指定集群名稱(_id),主庫、從庫等都可以讀取到,這里就不細說了

>>> 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只支持一個hostname,對應DNS server,進行SRV record查詢,它也支持replSet和authSource(TXT record),需要注意的是它默認使用TLS,即ssl=True。
      具體的說明還是看initial-dns-seedlist-discovery
      假設我們使用
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"

那么對應解析結果就是:

mongodb://mongodb1.mongodb.com:27317,mongodb2.mongodb.com:27107/?ssl=true&replicaSet=replProduction&authSource=authDB
    1. SSL
      一堆的參數,還沒有用過,自行看文檔吧。

4.代碼示例

此處給出一份使用pymongo3.6連接MongoDB的代碼示例,分別是OPTION和URI兩種方式
主要考慮集群配置和密碼校驗兩個方面,假設配置文件如下

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為空則不需要進行密碼校驗
db不給出則默認為“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,默認為'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設置
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'])

# 處理密碼校驗
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)

假設db中有collection名為TEST_COL,可以如下驗證client的有效性:

    database = client[MONGODB['db']]
    print(database.TEST_COL.count())
    # client.run.command({'count': 'TEST_COL'})    # 需要權限
    # client.admin.command('ismaster')             # 不支持副本集環境

4.配置副本集讀寫分離

from pymongo import ReadPreference

db = conn.get_database(MONGODB['db'], read_preference=ReadPreference.SECONDARY_PREFERRED)

副本集ReadPreference有5個選項:

  • PRIMARY:默認選項,從primary節點讀取數據
  • PRIMARY_PREFERRED:優先從primary節點讀取,如果沒有primary節點,則從集群中可用的secondary節點讀取
  • SECONDARY:從secondary節點讀取數據
  • SECONDARY_PREFERRED:優先從secondary節點讀取,如果沒有可用的secondary節點,則從primary節點讀取
  • NEAREST:從集群中可用的節點讀取數據

5. 參考

PyMongo 3.6.0 Documentation
利用python測試mongodb副本集數據同步延遲

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容