背景

最近新上线的一个服务,偶尔会有超时告警,其主要逻辑仅仅只是简单的读/写mongodb,而且服务上线初期,流量并不大,因而理论上来说,每次请求都应该很快才对,事实上分析日志也证实90%以上的请求都在100ms内返回,大部分请求耗时都在10ms内,但是依然有1%不到的请求会显示耗时超过1s,极端个例耗时可达2-3s,这几天相对比较有空,于是决心仔细研究一下原因,最终定位到是由于对mongoegine的model机制中的QuerySet使用机制不够了解而踩坑了,这里记录一下。

问题浮现

mongoengine的使用非常简单,用户通过简单的定义一个继承于mongonengine.Document的子类,即可方便的通过该类实现对对应mongodb中collection的各种操作,官方tutorial中的一个例子:

首先需要告诉程序需要连接到哪个mongod实例,使用下面的connect函数:

from mongoengine import *

connect('tumblelog')

在不显示指定host和port的情况下,默认连接localhost的27017端口,显示指定则加上host和port参数即可:

connect('project1', username='webapp', password='pwd123')

定义下面一个User类,即可访问对应mongod数据库中的user colletion了。

from datetime import datetime
class User(Document):
email = StringField(required=True)
first_name = StringField(max_length=50)
last_name = StringField(max_length=50)
ctime = DateTimeField(default=datetime.utcnow)

如果要获取mongodb中first_name为Jack的记录,一下代码即可轻松实现:

records = User.objects(first_name='Jack').order_by('ctime')

而后通过获取到的records就可以对其进行各种操作了:

if not records: # 判断数据是否为空
if len(records) > 1: # 判断记录条数是否符合要求
pk = records[0].pk # 获取首条记录的pk
pk = records[0]['ctime'] # 获取收条记录的创建时间

这样的使用逻辑是没有问题的,从业务逻辑上来说完全可以达到编码者的目的,然而却存在一个耗时较高、对mongodb访问过于平凡的隐藏坑。

之前一直以为向上面的一段代码,程序在开始定义records时,即已经通过User.objects指定查找和排序条件执行完将记录结果赋值给了records,因而后面的records使用都是直接访问已经存在本地的返回结果进行的操作,然而事实上,上面这段代码,每一行都会触发一次对mongodb实时网络访问,总共是四次网络访问,至于records定义赋值的哪一行,反而没有对mongodb进行访问,远超多次的网络访问,不但增加了服务本身处理请求的总耗时,使其对网络和mongodb的波动更加敏感,而且使mongodb的访问量毫无必要的增加了好几倍,危害甚大。

mongoengine的这种表现,着实有些出乎自己的直觉预期,在网上尝试找寻相关资料,却并没有找到关于这一问题描述的合适资料,于是深究了一下其源码。

源码剖析

首先要弄清楚的是,为什么这行代码不会马上触发网络访问,从mongodb获取到按ctime排序的查询结果呢?

records = User.objects(first_name='Jack').order_by('ctime') #records实际类型为:<class 'mongoengine.queryset.queryset.QuerySet'>

其实这行代码的赋值对象records命名存在误导性,让人容易误以为返回的结果就是已经按条件查询好的一个记录list,然而实际返回的对象并非如此,而是一个QuerySet,这个QuerySet对象的创建,并不需要去远程调用mongodb,而只是把查询的相关条件(first_name='Jack'、order_by排序条件等)记录在QuerySet对象之中,在后面真正需要访问具体的记录属性时,才会根据条件去远程查询mongodb。

在QuerySet这个类之中,重载了对QuerySet对象执行len()函数时的行为,相关代码如下:

#source file: mongoengine/queryset/queryset.py
class QuerySet(BaseQuerySet):
"""The default queryset, that builds queries and handles a set of results
returned from a query. Wraps a MongoDB cursor, providing :class:`~mongoengine.Document` objects as
the results.
...
def __len__(self):
"""Since __len__ is called quite frequently (for example, as part of
list(qs) we populate the result cache and cache the length.
"""
if self._len is not None:
return self._len
if self._has_more:
# populate the cache
list(self._iter_results()) self._len = len(self._result_cache)
return self._len

QuerySet继承于BaseQuerySet,其中定义了根据下标/slice语法访问对象行为的内部函数__getitem__()、对象转化为布尔值时的执行的内部函数(__nonzero__/__bool__),以及使对象具备可调用属性的__call__函数。

# source file: mongoengine/queryset/base.py
class BaseQuerySet(object):
"""A set of results returned from a query. Wraps a MongoDB cursor,
providing :class:`~mongoengine.Document` objects as the results.
"""
...
def __init__(self, document, collection):
self._document = document
self._collection_obj = collection
self._mongo_query = None
self._query_obj = Q()
self._initial_query = {}
self._where_clause = None
self._loaded_fields = QueryFieldList()
self._ordering = None
self._snapshot = False
self._timeout = True
self._class_check = True
self._slave_okay = False
self._read_preference = None
self._iter = False
self._scalar = []
self._none = False
self._as_pymongo = False
self._as_pymongo_coerce = False
self._search_text = None # If inheritance is allowed, only return instances and instances of
# subclasses of the class being used
if document._meta.get('allow_inheritance') is True:
if len(self._document._subclasses) == 1:
self._initial_query = {"_cls": self._document._subclasses[0]}
else:
self._initial_query = {
"_cls": {"$in": self._document._subclasses}}
self._loaded_fields = QueryFieldList(always_include=['_cls'])
self._cursor_obj = None
self._limit = None
self._skip = None
self._hint = -1 # Using -1 as None is a valid value for hint
self.only_fields = []
self._max_time_ms = None def __call__(self, q_obj=None, class_check=True, read_preference=None,
**query):
"""Filter the selected documents by calling the
:class:`~mongoengine.queryset.QuerySet` with a query. :param q_obj: a :class:`~mongoengine.queryset.Q` object to be used in
the query; the :class:`~mongoengine.queryset.QuerySet` is filtered
multiple times with different :class:`~mongoengine.queryset.Q`
objects, only the last one will be used
:param class_check: If set to False bypass class name check when
querying collection
:param read_preference: if set, overrides connection-level
read_preference from `ReplicaSetConnection`.
:param query: Django-style query keyword arguments
"""
query = Q(**query)
if q_obj:
# make sure proper query object is passed
if not isinstance(q_obj, QNode):
msg = ("Not a query object: %s. "
"Did you intend to use key=value?" % q_obj)
raise InvalidQueryError(msg)
query &= q_obj if read_preference is None:
queryset = self.clone()
else:
# Use the clone provided when setting read_preference
queryset = self.read_preference(read_preference) queryset._query_obj &= query
queryset._mongo_query = None
queryset._cursor_obj = None
queryset._class_check = class_check return queryset
...
def __getitem__(self, key):
"""Support skip and limit using getitem and slicing syntax.
"""
queryset = self.clone() # Slice provided
if isinstance(key, slice):
try:
queryset._cursor_obj = queryset._cursor[key]
queryset._skip, queryset._limit = key.start, key.stop
if key.start and key.stop:
queryset._limit = key.stop - key.start
except IndexError, err:
# PyMongo raises an error if key.start == key.stop, catch it,
# bin it, kill it.
start = key.start or 0
if start >= 0 and key.stop >= 0 and key.step is None:
if start == key.stop:
queryset.limit(0)
queryset._skip = key.start
queryset._limit = key.stop - start
return queryset
raise err
# Allow further QuerySet modifications to be performed
return queryset
# Integer index provided
elif isinstance(key, int):
if queryset._scalar:
return queryset._get_scalar(
queryset._document._from_son(queryset._cursor[key],
_auto_dereference=self._auto_dereference,
only_fields=self.only_fields)) if queryset._as_pymongo:
return queryset._get_as_pymongo(queryset._cursor[key])
return queryset._document._from_son(queryset._cursor[key],
_auto_dereference=self._auto_dereference,
only_fields=self.only_fields) raise AttributeError
...
def _has_data(self):
""" Retrieves whether cursor has any data. """ queryset = self.order_by()
return False if queryset.first() is None else True def __nonzero__(self):
""" Avoid to open all records in an if stmt in Py2. """ return self._has_data() def __bool__(self):
""" Avoid to open all records in an if stmt in Py3. """ return self._has_data() # Core functions def all(self):
"""Returns all documents."""
return self.__call__() def filter(self, *q_objs, **query):
"""An alias of :meth:`~mongoengine.queryset.QuerySet.__call__`
"""
return self.__call__(*q_objs, **query)
...
@property
def _cursor(self):
if self._cursor_obj is None: # In PyMongo 3+, we define the read preference on a collection
# level, not a cursor level. Thus, we need to get a cloned
# collection object using `with_options` first.
if IS_PYMONGO_3 and self._read_preference is not None:
self._cursor_obj = self._collection\
.with_options(read_preference=self._read_preference)\
.find(self._query, **self._cursor_args)
else:
self._cursor_obj = self._collection.find(self._query,
**self._cursor_args)
# Apply where clauses to cursor
if self._where_clause:
where_clause = self._sub_js_fields(self._where_clause)
self._cursor_obj.where(where_clause) if self._ordering:
# Apply query ordering
self._cursor_obj.sort(self._ordering)
elif self._ordering is None and self._document._meta['ordering']:
# Otherwise, apply the ordering from the document model, unless
# it's been explicitly cleared via order_by with no arguments
order = self._get_order_by(self._document._meta['ordering'])
self._cursor_obj.sort(order) if self._limit is not None:
self._cursor_obj.limit(self._limit) if self._skip is not None:
self._cursor_obj.skip(self._skip) if self._hint != -1:
self._cursor_obj.hint(self._hint) return self._cursor_obj ...

从代码可以看出来了,实际QuerySet对象创建时并不会立即去查询mongodb获取结果,而是在真正使用时,根据重载的使用行为再去查询mongodb,其查询mongodb的相关代码位于BaseQuerySet基类的_cursor函数中。

所以对于User.objects返回结果其实更好的命名风格应该是:

query_set = User.objects(first_name='Jack').order_by('ctime')
or
qs = User.objects(first_name='Jack').order_by('ctime')

而为了避免之后对queryset的直接操作导致预期之外的多次网络请求mongodb,在获取query_set后,可以直接执行一次查询将其所有查询到的结果单独存到一个本地list对象中:

records = [rc for rc in qs]

之后对records操作就不会再触发对mongodb的网络访问了,这么做相比之前唯一的缺陷就是如果本地使用records时耗时过长,可能导致本地数据和mongodb端不一致,不过对于绝大部分场景,查询完mongodb后,对其结果的引用应该会在很短的时间内完成,基本不会存在不一致的问题,应用者根据场景评估下即可。

mongoengine中queryset触发网络访问机制剖析的更多相关文章

  1. mongoengine中collection名称自动生成机制浅探

    项目碰到要使用mongodb的场景,以前只听过这一强大的文档数据库,但一直没有真正使用过,参考一下项目中已有的使用代码,是通过import mongoengine这一模块实现python服务对db中c ...

  2. Java网络编程和NIO详解1:JAVA 中原生的 socket 通信机制

    Java网络编程和NIO详解1:JAVA 中原生的 socket 通信机制 JAVA 中原生的 socket 通信机制 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.co ...

  3. 已禁用对分布式事务管理器(MSDTC)的网络访问。请使用组件服务管理工具启用 DTC 以便在 MSDTC 安全配置中进行网络访问。

    今天写ASP.NET程序,在网页后台的c#代码里写了个事务,事务内部对一张表进行批量插入,对另外一张表进行查询与批量插入. 结果第二张表查询后foreach迭代操作时报错:已禁用对分布式事务管理器(M ...

  4. Android中使用http协议访问网络

    HTTP协议的工作原理:客户端向服务器端发送http请求,服务器端收到请求后返回一下数据给客户端,客户端接受消息并进行解析. 在Android中发送http请求的方式有两种,第一种是通过HttpURL ...

  5. 【转】非教育网中IPv4网络访问IPv6资源

    1. 背景知识 随着个人电脑.移动终端.乃至物联网的不断发展,有很大的IP地址需求.由于IPv4协议设计时没有料到日后网络会如此发达,IPv4网络中的IP数量相对今天的需求来说,显得捉襟见肘.加上IP ...

  6. 游戏中的网络同步机制——Lockstep(帧同步)

    本文来自: https://bindog.github.io/blog/2015/03/10/synchronization-in-multiplayer-networked-game-lockste ...

  7. 【Azure Redis 缓存 Azure Cache For Redis】在创建高级层Redis(P1)集成虚拟网络(VNET)后,如何测试VNET中资源如何成功访问及配置白名单的效果

    当使用Azure Redis高级版时候,为了能更好的保护Redis的安全,启用了虚拟网路,把Redis集成在Azure中的虚拟网络,只能通过虚拟网络VENT中的资源进行访问,而公网是不可以访问的.但是 ...

  8. 理解Docker(6):若干企业生产环境中的容器网络方案

    本系列文章将介绍 Docker的相关知识: (1)Docker 安装及基本用法 (2)Docker 镜像 (3)Docker 容器的隔离性 - 使用 Linux namespace 隔离容器的运行环境 ...

  9. C#进阶系列——WebApi 路由机制剖析:你准备好了吗?

    前言:从MVC到WebApi,路由机制一直是伴随着这些技术的一个重要组成部分. 它可以很简单:如果你仅仅只需要会用一些简单的路由,如/Home/Index,那么你只需要配置一个默认路由就能简单搞定: ...

随机推荐

  1. SAP R/3系统的R和3分别代表什么含义,负载均衡的实现原理

    1972年,SAP诞生,推出了RF系统(实时财务会计系统), 后来命名为R1. R指Real time.3既指第三代系统,又代表3层架构. 三层架构分别为下图的Presentation server ...

  2. C语言编译器,写给萌新们看看。

    就我已经经历过的大学课程,仿佛每一门计算机的专业课程的开头,都是在介绍计算机发展的历史,和大名鼎鼎的冯诺依曼结构. 譬如C语言,比较水的计算机导论,c++,数据结构,计算机组成原理,甚至是Linux实 ...

  3. 关于Hibernate多对多关联关系的更新问题。

    一个账套类Reckoning和账套项目类 AccountItem.这两个类是双向多对多关联关系. Reckoning.hbm.xml文件的配置如下 <set name="account ...

  4. R在Centos下安装

    R语言是主要用于统计分析.绘图的语言和操作环境. 官方网站: http://www.r-project.org/ Windows下面有直接的安装包,直接下载安装很方便,但是对于刚出的CentOS6.0 ...

  5. 20165322 第七周 mybash 的实现

    mybash的实现 要求 使用fork,exec,wait实现mybash 写出伪代码,产品代码和测试代码 发表知识理解,实现过程和问题解决的博客 相关函数的作用 fork fork()函数通过系统调 ...

  6. Yii2 配置发送邮件

    'components' => [ 'mailer' => [ 'class' => 'yii\swiftmailer\Mailer', 'viewPath' => '@com ...

  7. Win32线程——优先权

    <Win32多线程程序设计>–Jim Beveridge & Robert Wiener Win32 优先权是以数值表现的,并以进程的“优先权类别(priority class)” ...

  8. 三十、详述使用 IntelliJ IDEA 解决 jar 包冲突的问题

    在实际的 Maven 项目开发中,由于项目引入的依赖过多,遇到 jar 冲突算是一个很常见的问题了.在本文中,我们就一起来看看,如何使用 IntelliJ IDEA 解决 jar 包冲突的问题!简单粗 ...

  9. Hello, GitHub!

    GitHub作为版本控制的软件,我决定重新系统学习这个东西,毕竟以前都是fork.clone... 1. 理解Git思维 首先呢,我一开始就被GitHub和Git两个东西搞昏了,所以有必要理解二者的关 ...

  10. Vue--- 一点车项目 6个小时实际看了10天(完结)

    一个项目 环境安装   使用了 cli 脚手架  Koa2  workpackage  其他小的不计 前端Vue组件搭建 数据的简单测试交互 数据库的设计 创建.连接接数据库 前台[表单/分类]   ...