(原创) 使用pymongo 3.6.0连接MongoDB的正确姿势

1.3版本的PHP MongoDB
driver重写了连接处理库,和以前版本相比,在持久连接和连接池方面,都有了重大的变化。

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

前言

1.2版本的连接管理

1.2版本的驱动引入了连接池,在执行任何查询时,都会从连接池中请求一个连接,完成之后再归还给连接池。这里的完成是指持有该连接的变量离开了它的作用域,下面是一个示例。

最简单的版本:

<?php
$m = new MongoClient();    // ← 从连接池请求连接
$c = $m->demo->test;
$c->insert( array( 'test' => 'yes' ) );
?>

← $m离开作用域,连接归还给连接池

在函数中:

<?php
function doQuery()
{
        $m = new MongoClient();    // ← 从连接池请求连接
        $c = $m->demo->test;
        $c->insert( array( 'test' => 'yes' ) );
} // ← $m离开作用域,连接归还给连接池
?>

在某些情况下,系统可能会产生大量的连接,比如在ORMs/ODMs的某个复杂结构中引用连接对象,如下例子:

<?php
for ( $i = 0; $i < 5; $i++ )
{
        $conns[] = new MongoClient();
}// ← 现在有5个连接
?>

1.解答

那这是为什么呢?
按一般的理解,MongoClient接口实现的时候肯定要考虑MongoDB连接异常的情况,只有确保连接成功建立,才能一步一步往下取database、取collection、基于collection进行增删改查操作等等。
这里为什么不呢
?我发现了一个大BUG?不应该啊,pymongo发布使用的时间比我用Python写代码都要长,这要是bug早该解决了。
如何使用pymongo连接MongoDB,网上确实有很多很多博客,不过绝大多数都很简单,基本就我上面那几行的正确使用而已,顶多再提醒一下安装pymongo的时候不能安装第三方bson,因为pymongo自带bson,两者不匹配,讲如何进行密码验证的都很少,更别说replSet参数的使用。
终于还是在官网的API接口文档中MongoClient的解释中找到了答案:

图片 2

图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

图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解析需求呢。

MongoDB
Node.js驱动程序是被官方所支持的原生node.js驱动程序,他是至今为止最好的实现,
并且得到了MongoDB官方的支持。MongoDB团队已经采用MongoDB
Node.js驱动程序作为标准方法。

1.3版本的连接管理

在1.3版本中,连接管理做了很大改动。每个worker进程(线程、PHP-FPM或Apache
worker)中,驱动把连接管理和Mongo*对象分离,降低驱动的复杂度。下面以单个节点的MongoDB实例来说明驱动如何处理连接。

当一个worker进程启动,MongoDB驱动会为之初始化连接管理器管理连接,并且默认没有连接。

在第一个请求调用new
MongoClient();时,驱动创建一个新连接,并且以一个哈希值标识这个连接。这个哈希值包括以下参数:主机名、端口,进程ID和可选的replica
set名,如果是密码验证的连接,则还包括数据库名、用户名和密码的哈希值(对于密码验证的连接,我们后面再详细讨论)。调用MongoClient::getConnections()方法,可以查看连接对应的哈希值:

<?php
$m = new MongoClient( 'mongodb://whisky:27017/' );
var_dump( $m->getConnections()[0]['hash'] );
?>

输出:

string(22) “whisky:27017;-;X;22835″

输出中的”-”表示该连接不属于某个replica
set,”X”是没有用户名、数据库和密码时的占位符,22835是当前进程的进程ID。

然后该连接会在连接管理器中注册: 图片 4

在需要连接的任何时候,包括插入、删除、更新、查找或执行命令,驱动都会向管理器请求一个合适的连接来执行。请求连接时会用到new
MongoClient()的参数和当前进程的ID。每个worker进程/线程,连接管理器都会有一个连接列表,而每个PHP
worker同一时刻,只会运行一个请求,因此和每个MongoDB之间只需要一个连接,不断重用,直到PHP
worker终止或显式调用MongoClient::close()关闭连接。

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__
npm install mongodb@1.4.3 // MongoDB Node.js驱动程序npm install mongoose@3.8.8 //mongoose模块

Replica sets

在存在复制集的环境中,情形有点不一样。new
MongoClient()的连接字符串中,需要指定多个hosts,并标示当前正在实用复制集:

$m = new
MongoClient(“mongodb://whisky:13000,whisky:13001/?replicaSet=seta”);

其中的replicaSet参数不能省略,否则驱动会认为你是准备连接三个不同的mongos进程。

在实例化时,驱动会检查复制集的拓扑结构。下面例子的输出,显示在调用new
MongoClient()之后,复制集中所有可见的数据节点都会在管理器中注册一个连接:

<?php
$m = new MongoClient( 'mongodb://whisky:13001/?replicaSet=seta' );
foreach ( $m->getConnections() as $c )
{
    echo $c['hash'], "n";
}
?>

输出:

whisky:13001;seta;X;32315 whisky:13000;seta;X;32315

虽然连接字符串中没有whisky:13000节点,但是管理器中已经注册了两个连接:

图片 5

管理器不仅包含连接的哈希值和TCP/IP
socket,还保存哪个节点是主节点,以及每个节点的“距离”。下面的脚本显示了这些额外的信息;

<?php
$m = new MongoClient( 'mongodb://whisky:13001/?replicaSet=seta' );
foreach ( $m->getConnections() as $c )
{
    echo $c['hash'], ":n",
        " - {$c['connection']['connection_type_desc']}, ",
        "{$c['connection']['ping_ms']} msn";
}
?>

输出:

whisky:13001;seta;X;5776: – SECONDARY, 1 ms whisky:13000;seta;X;5776:
– PRIMARY, 0 ms

驱动把操作分为两种类型:写操作,包括插入、更新、删除和命令;读操作,包括find和findOne。默认情况下,如果没有设置读偏好参数,管理器会一直返回主节点的连接。读偏好参数可以通过setSlaveOkay()设置,也可以在连接字符串中设置:

$m = new MongoClient("mongodb://whisky:13000,whisky:13001/?replicaSet=seta&readPreference=secondaryPreferred");

加上这些参数后,连接字符串变得特别长,因此PHP驱动允许将选项放在数组中,作为第二个参数传入:

$options = array(
        'replicaSet' => 'seta',
        'readPreference' => 'secondaryPreferred',
);
$m = new MongoClient("mongodb://whisky:13000,whisky:13001/", $options);

对于每个操作,驱动向管理器请求获取一个合适的连接。对于写操作,会一直返回主节点的连接;对于读操作,如果辅助节点可用且“距离”不远的话,则会返回该辅助节点的连接。

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
      一堆的参数,还没有用过,自行看文档吧。

要从Node.js连接MongoDB数据库我们有两种方法可选择:

验证的连接

如果MongoDB启用验证功能,那么连接的哈希值会包含验证相关的哈希值。这样不同脚本,使用不同的用户名、密码连接同一个MongoDB上的不同的数据库时,能够相互区分,而不会误用连接。下面示例使用admin用户名连接admin数据库,然后观察hash值的变化:

<?php
$m = new MongoClient( 'mongodb://admin:admin@whisky:27017/admin' );
var_dump( $m->getConnections()[0]['hash'] );
?>

输出:

string(64)
“whisky:27017;-;admin/admin/bda5cc70cd5c23f7ffa1fda978ecb class=”wp_keywordlink”>D30;8697″

以前示例中的”X”部分已经替换为一个包含数据库名admin、用户名admin和哈希值bda5cc70cd5c23f7ffa1fda978ecbd30,该哈希值是根据用户名、数据库名和密码哈希值计算得来。

为了验证能够正确工作,需要在连接字符串中包含数据库名,否则会默认为admin。

在建立连接后要使用数据库,需要先选择该数据库,如:

$collection = $m->demoDb->collection; $collection->findOne();

如果选择的数据库是连接字符串中指定的数据库,或者连接字符串中的数据库是admin,那么一切都会正常运行。否则,驱动会创建一个新的连接,从而防止验证被绕过,如下所示:

<?php
$m = new MongoClient( 'mongodb://user:user@whisky:27017/test' );

$db = $m->test2;
$collection = $db->collection;
var_dump( $collection->findOne() );
?>

输出:

Fatal error: Uncaught exception ‘MongoCursorException’ with message
‘whisky:27017: unauthorized db:test2 ns:test2.collection lock type:0
client:127.0.0.1′ in …/mongo-connect-5.php.txt:6

因为我们的连接并没有执行test2数据库的授权验证,因而失败。如果我们执行验证,就会正常运行:

<?php
$m = new MongoClient( 'mongodb://user:user@whisky:27017/test' );

$db = $m->test2;
$db->authenticate('user2', 'user2' );
$collection = $db->collection;
$collection->findOne();

foreach ( $m->getConnections() as $c )
{
    echo $c['hash'], "n";
}
?>

输出:

whisky:27017;-;test/user/602b672e2fdcda7b58a042aeeb034376;26983
whisky:27017;-;test2/user2/984b6b4fd6c33f49b73f026f8b47c0de;26983

现在管理器中有两个已验证的连接:

图片 6

顺便提一句,如果你打开了E_DEPRECATED级别的错误提示,则会看到:

Deprecated: Function MongoDB::authenticate() is deprecated in
…/mongo-connect-6.php.txt on line 5

驱动建议通过创建两个MongoClient对象完成该类任务:

<?php
$mTest1 = new MongoClient( 'mongodb://user:user@whisky:27017/test', array( 'connect' => false ) );
$mTest2 = new MongoClient( 'mongodb://user2:user2@whisky:27017/test2', array( 'connect' => false ) );

$mTest1->test->test->findOne();
$mTest2->test2->test->findOne();

foreach ( $mTest2->getConnections() as $c )
{
    echo $c['hash'], "n";
}
?>

单个MongoDB服务器能支持的并发连接相当有限,如果使用PHP-FPM的话,每个worker进程有自己独立的连接池,那么很容易达到连接数的上限。因此,在生产环境中,不管有没有使用复制集,都要部署mongos,然后PHP-FPM连接mongos,这样可以减少mongod的连接数,并且PHP-FPM和mongos之间可以使用短连接(即每个请求结束时都显式调用close函数关闭MongoDB连接)。

4. 参考

PyMongo 3.6.0
Documentation

通过实例化mongodb模块中提供的mongodbClient类,然后使用这个实例化的对象来创建和管理mongodb连接;
使用字符串进行连接;

1、通过client对象连接到MongoDB

通过实例化一个MongoClient对象连接MongoDB数据库是最常用也是最佳的方式。

创建MongoClient对象实例的语法:

MongoClient;

server : 一个serverd对象; options : 数据库连接选项;

如上图所示,MongoClient连接利用了后台的Server对象。这个对象的功能就是定义了MongoDB驱动程序怎么连接到服务器。

下面,看一个示例:

var MongoClient = require.MongoClient, Server = require.server;// 创建客户端连接对象var client = new MongoClient( new Server('localhost', 27017, { socketOpations: { connectTimeoutMS: 500 }, poolSize: 5, auto_reconnect: true }, { numberOfRetries: 3, retryMilliSeconds: 500 }));// 打开对服务器端MongoDB数据库的连接client.open { if  { console.log; } else { var db = client.db; // 建立到数据库blogdb的连接 if  { console.log; db.authenticate('username', 'pwd', function { // 对用户数据库身份进行验证 if  { console.log; client.close(); // 关闭对MongoDB的连接 console.log; } else { console.log; db.logout(function  { // 关闭对数据库的连接,即退出数据库 if  { console.log; } client.close(); // 关闭对MongoDB的连接 console.log; } }); } }});

注意:
要注销数据库,要使用数据库对象上的logout()方法。这将关闭到该数据库上的连接,你不在可以使用Db对象。例如:
db.logout();而要关闭到MongoDB的连接,要在客户端连接上调用close()方法,例如:
client.close()

写入关注

首先,我们在连接数据库的时候会用到一个关于写入关注级别的问题,说白了,个人理解的话就是相当于出现问题的一个处理优先顺序,你可以选择写入数据库之前是否需要你确认之类的,或者发生错误是否忽略之类的等等,如下图:

写 入 级 别

说 明

-1 网络错误被忽略 0 写确认是不必要的 1 请求写确认 2
写确认请求跨主服务器和副本集中的一个辅助服务器 majority
写确认是从副本集的主服务器请求的

用来创建MongoClient连接的Server对象的选项,如下:

用来创建MongoClient连接的数据库连接选项,如下:

2、通过一个连接字符串连接到MongoDB

这种方式要调用MongoClient类的connect方法。connect使用语法如下:

MongoClient.connect(connString, options, callback)

connString字符串的语法如下:

mongodb://username:password@host:port/database?opations

MongoClient连接字符串组件:

选 项

说 明

mongodb:// 指定字符串使用mongodb的连接格式 username
验证时使用的用户名。可选 password 身份验证时使用的密码。可选 host
MongoDB服务器主机名或者域名。它可以是多个host:port组合来连接多个MongoDB服务器。
例如:mongodb://host1:270017, host2://270017, host3:270017/testDB port
连接MongoDB服务器时使用的端口。默认值是27017 database
要连接的数据库的名字。 默认为admin options
连接时所使用的选项的键值对。可以在dbOpt和serverOpt参数上指定这些选项

下面,看一个使用连接字符串方法连接MongoDB数据库的示例:

var MongoClient = require.MongoClient;MongoClient.connect('mongodb://mongodb:test@localhost:27017/blogdb', { db: { w: 1, native_parser: false }, server: { poolSize: 5, socketOpations: { connectTimeoutMS: 500 }, auto_reconnect: true }, replSet: {}, mongos: {} }, function { console.log; } else { console.log; // 注销数据库 db.logout { if  { console.log; } db.close(); // 关闭连接 console.log;

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

发表评论

电子邮件地址不会被公开。 必填项已用*标注