mongoengine中queryset触发网络访问机制剖析
背景
最近新上线的一个服务,偶尔会有超时告警,其主要逻辑仅仅只是简单的读/写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触发网络访问机制剖析的更多相关文章
- mongoengine中collection名称自动生成机制浅探
项目碰到要使用mongodb的场景,以前只听过这一强大的文档数据库,但一直没有真正使用过,参考一下项目中已有的使用代码,是通过import mongoengine这一模块实现python服务对db中c ...
- Java网络编程和NIO详解1:JAVA 中原生的 socket 通信机制
Java网络编程和NIO详解1:JAVA 中原生的 socket 通信机制 JAVA 中原生的 socket 通信机制 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.co ...
- 已禁用对分布式事务管理器(MSDTC)的网络访问。请使用组件服务管理工具启用 DTC 以便在 MSDTC 安全配置中进行网络访问。
今天写ASP.NET程序,在网页后台的c#代码里写了个事务,事务内部对一张表进行批量插入,对另外一张表进行查询与批量插入. 结果第二张表查询后foreach迭代操作时报错:已禁用对分布式事务管理器(M ...
- Android中使用http协议访问网络
HTTP协议的工作原理:客户端向服务器端发送http请求,服务器端收到请求后返回一下数据给客户端,客户端接受消息并进行解析. 在Android中发送http请求的方式有两种,第一种是通过HttpURL ...
- 【转】非教育网中IPv4网络访问IPv6资源
1. 背景知识 随着个人电脑.移动终端.乃至物联网的不断发展,有很大的IP地址需求.由于IPv4协议设计时没有料到日后网络会如此发达,IPv4网络中的IP数量相对今天的需求来说,显得捉襟见肘.加上IP ...
- 游戏中的网络同步机制——Lockstep(帧同步)
本文来自: https://bindog.github.io/blog/2015/03/10/synchronization-in-multiplayer-networked-game-lockste ...
- 【Azure Redis 缓存 Azure Cache For Redis】在创建高级层Redis(P1)集成虚拟网络(VNET)后,如何测试VNET中资源如何成功访问及配置白名单的效果
当使用Azure Redis高级版时候,为了能更好的保护Redis的安全,启用了虚拟网路,把Redis集成在Azure中的虚拟网络,只能通过虚拟网络VENT中的资源进行访问,而公网是不可以访问的.但是 ...
- 理解Docker(6):若干企业生产环境中的容器网络方案
本系列文章将介绍 Docker的相关知识: (1)Docker 安装及基本用法 (2)Docker 镜像 (3)Docker 容器的隔离性 - 使用 Linux namespace 隔离容器的运行环境 ...
- C#进阶系列——WebApi 路由机制剖析:你准备好了吗?
前言:从MVC到WebApi,路由机制一直是伴随着这些技术的一个重要组成部分. 它可以很简单:如果你仅仅只需要会用一些简单的路由,如/Home/Index,那么你只需要配置一个默认路由就能简单搞定: ...
随机推荐
- web服务器、app(应用)服务器、DB后端性能瓶颈和分析
性能测试day07_性能瓶颈和分析 https://www.cnblogs.com/leixiaobai/p/9463748.html 其实如果之前都做的很到位的话,那么再加上APM工具(dynaTr ...
- postgresql+postgis+pgrouting实现最短路径查询(2)---openlayers+geoserver实现最短路径
自己的最短路径实现基本上是按照参考博文的1.2和3进行的,实现的时候也是问题不断,只能是一个一个解决. 问题1:自己发布的geoserver服务无法和OSM底图叠加到一起. 解决:参考博文2提到发布服 ...
- UVa 580 - Critical Mass(递推)
链接: https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem& ...
- 用SQL实现的BASE64加密及解密函数(SQL2005以上有效)
CREATE FUNCTION [dbo].[f_base64_encode] (@bin varbinary(max)) returns varchar(max) as begin return c ...
- 【[JLOI2011]飞行路线】
据说这是分层图最短路的板子题 但其实就是一个\(dij\)多带了一维状态 我们看到\(k\)很小所以显然我们可以设计一个这样的状态 \(d[v][k]\)表示从起点到点\(v\)免费走了\(k\)条路 ...
- jQuery中append()、prepend()与after()、before()的区别
转载 未曾见海 https://www.cnblogs.com/afei-qwerty/p/6682963.html 在jQuery中,添加元素有append(),prepend() 和 after ...
- maven编译package慢
mvn package编译出现连接不上mvn库的问题: [root@localhost nnnnn]# mvn package[INFO] Scanning for projects...Downlo ...
- IOS9中使用NSURLConection发送异步网络请求
IOS9中使用NSURLConection发送异步网络请求 在ios9中,NSURLConection的sendSync..和sendAsync已经过时.被NSURLSession代替. 以下蓝色部分 ...
- ARM v7-A 系列CPU的MMU隐射分析
ARM v7-A 系列CPU的MMU隐射分析 摘要:ARM v7-A系列的CPU加入了很多扩展,如多核处理器扩展.大物理地址扩展.TrustZone扩展.虚拟化扩展.若支持大的物理地址,则必须支持多核 ...
- javaScript 工作必知(十一) 数组常用方法实现
大纲 Array join reverse反转 sort排序 concat 拼接 slice splice 数组 //定义数组 var a = []; //使用Array定义一个数组, var a1 ...