zoukankan      html  css  js  c++  java
  • (原创) 使用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:从集群中可用的节点读取数据


    作者:mona_alwyn
    链接:https://www.jianshu.com/p/d9918b0a3ebc
    来源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
  • 相关阅读:
    day 66 crm(3) 自创组件stark界面展示数据
    day 65 crm(2) admin源码解析,以及简单的仿造admin组件
    用 Python+nginx+django 打造在线家庭影院
    django -admin 源码解析
    day 64 crm项目(1) admin组件的初识别以及应用
    云链接 接口不允许 情况 解决方法 mysql Host is not allowed to connect to this MySQL server解决方法
    day 56 linux的安装python3 ,虚拟环境,mysql ,redis
    day55 linux 基础以及系统优化
    Codeforces 989 P循环节01构造 ABCD连通块构造 思维对云遮月参考系坐标轴转换
    Codeforces 990 调和级数路灯贪心暴力 DFS生成树两子树差调水 GCD树连通块暴力
  • 原文地址:https://www.cnblogs.com/ExMan/p/10735136.html
Copyright © 2011-2022 走看看