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

簡單翻譯一下:
從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和圖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', ...)
-
- "mongodb+srv://"
這種形式的URL只支持一個hostname,對應DNS server,進行SRV record查詢,它也支持replSet和authSource(TXT record),需要注意的是它默認使用TLS,即ssl=True。
具體的說明還是看initial-dns-seedlist-discovery
假設我們使用
- "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"
那么對應解析結果就是:
mongodb://mongodb1.mongodb.com:27317,mongodb2.mongodb.com:27107/?ssl=true&replicaSet=replProduction&authSource=authDB
-
- SSL
一堆的參數,還沒有用過,自行看文檔吧。
- 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:從集群中可用的節點讀取數據
作者:mona_alwyn
鏈接:https://www.jianshu.com/p/d9918b0a3ebc
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。