我正在努力优化(主要)由MongoDB支持的Django应用程序。它在负载测试下死亡。在当前有问题的页面上,New Relic显示了对pymongo.collection:Collection.find
的700多个调用。大部分代码都是由初级编码员编写的,通常我会寻找添加标记,进行更智能连接和删除循环以减少查询调用的地方,但这里不能选择连接。我做了什么(在添加基于EXPLAINs的指标之后)尝试通过进行一般查询然后在循环中过滤那个较小的集合来降低循环中的成本。虽然我已经从900个查询中得到了这个数字,但即使在页面上进行了大量的工作,700仍然看起来很疯狂。我想即使在过滤现有的查询集时也可能调用find
,但代码表明它始终是database query。
我已经添加了一些日志记录到mongoengine以查看查询的来源并查看EXPLAIN语句,但是我没有通过信息墙筛选大量的运气。 mongoengine本身似乎是性能问题的一部分:我切换到mongomallard作为测试,并在页面上获得了50%的性能提升。不幸的是,我在其他一些页面上出现错误(最好我可以告诉它,当过滤现有的查询集时,Mallard表现不佳;错误抱怨发生在发电机中的deepcopy
调用,你不能这样做 - 我在那里打了一堵砖墙。虽然Mallard对我们来说似乎不是一个可行的替代品,但它确实表明在mongoengine中将很多时间用于将对象转换为Python和从Python转换。
如何进一步减少通话费用?或者我是否专注于错误的事情,应该在其他地方攻击问题?
编辑:提供一些代码/模型
相关页面显示课程的教学大纲,显示课程中的所有模块,课程以及课程下的概念。对于每个概念,还显示了用户在概念中的进展。因此,有很多循环可以使层次结构变得清晰(并且它不会根据任何patterns the Mongo docs suggest存储)。
class CourseVersion(Document):
...
course_instances = ListField(ReferenceField('CourseInstance'))
courseware_containers = ListField(EmbeddedDocumentField('CoursewareContainer'))
class CoursewareContainer(EmbeddedDocument):
id = UUIDField(required=True, binary=False, default=uuid.uuid4)
....
courseware_containers = ListField(EmbeddedDocumentField('self'))
teaching_element_instances = ListField(StringField())
课程的模块,课程和概念存储在courseware_containers
;我们需要获取所有概念,以便我们可以在teaching_element_instances
中获取id列表,以找到用户为该概念工作的最新代码(如果有的话),然后查找他们的进度。
*为了清楚起见,我正在使用一个分析器,并且在时间和行为上查看正确的方法,而不仅仅是改变事物并希望最好。
答案 0 :(得分:9)
代码示例并不是很糟糕,但有许多方面需要考虑,可能有助于提高性能。
class CourseVersion(Document):
...
course_instances = ListField(ReferenceField('CourseInstance'))
courseware_containers = ListField(EmbeddedDocumentField('CoursewareContainer'))
class CoursewareContainer(EmbeddedDocument):
id = UUIDField(required=True, binary=False, default=uuid.uuid4)
....
courseware_containers = ListField(EmbeddedDocumentField('self'))
teaching_element_instances = ListField(StringField())
查看强>
无限名单。
course_instances
,courseware_containers
,teaching_element_instances
如果这些字段无限制并且不断增长,那么随着文档的增长,文档将在磁盘上移动,从而导致重负载系统上的磁盘争用。有两种模式可以帮助减少这种情况:
a)Turn on Power of two sizes。这将花费磁盘空间,但应该随着文档的增长而降低io churn的数量
b)初始填充 - 自定义填充插入的文档,以便将其放入更大的范围,然后删除填充。真的是反模式,但它可能会给你一些里程。
最后一个障碍是最大文档大小 - 16MB,您可以将数据增加到更大。
ReferenceFields列表 - course_instances
MongoDB没有联接,因此需要额外的查询才能查找ReferenceField
- 实质上它们是应用内加入。哪个不是很糟糕,但了解权衡很重要。默认情况下,mongoengine不会自动取消引用该字段只执行course_version.course_instances
它是否会执行另一个查询,然后填充整个引用列表。因此,您可能需要另外一个查询 - 如果您不需要数据,那么exclude()
来自查询,以阻止任何泄漏的查询。
EmbeddedFields
这些字段是文档的一部分,因此除了传输和加载数据的电汇成本之外,它们不需要任何费用。 **由于它们是文档的一部分,因此您不需要select_related
来获取此数据。
teaching_element_instances
这些是id的列表吗?它在上面的代码示例中说它是StringField
。无论哪种方式,如果您不需要取消引用整个列表,那么将_ids
存储为StringField
并且如果编码正确则手动解除引用可能更有效 - 特别是如果您只需要最新的(最后?)id。
模型复杂性
CoursewareContainer
很复杂。对于任何给定的CourseVersion
,您n
CoursewareContainers
自己有一个n
个容器列表,每个容器都有n
个容器,并且...
查找最近的实例
我们需要获取所有概念,以便我们可以获取ID列表
teaching_element_instances
找到用户拥有的最新版本 为这个概念工作(如果有的话),然后查看他们的进展。
我不确定您是否有一个实例或每个Container一个实例或每个课程一个实例。无论哪种方式 - 都应该检查查询数据的逻辑。如果它是你所追求的单个实例 - 则可以针对用户存储,以简化查找的逻辑。如果它的每个课程或容器然后提高性能确保您最小化查询的数量 - 如果可能的话收集所有ids
,然后最后发出一个$in
查询,而不是每个容器进行一次查询
Mongoengine费用
目前,将数据加载到Mongoengine类中会产生性能成本 - 如果您不需要这些类并且很乐意使用简单的词典,则可以发出原始pymongo查询或使用as_pymongo
架构设计
模式看起来足够合理,但是它适用于用例 - 本质上它是使用MongoDB的优势还是在文档数据库形状的孔中放置关系挂钩?我不能回答你,但我确实知道通往MongoDB的快乐道路的方法是基于其用例设计架构。对于关系数据库,模式设计从一开始就很简单 - 您可以使用文档数据库对数据的使用方式进行规范化,这是一个主要因素。
MongoDB最佳实践
还有许多其他最佳做法,mongodb有一个可能感兴趣的指南:MongoDB Operations Best Practices。
欢迎通过Mongoengine mailing list与我联系,进一步讨论,如果需要私下讨论。
罗斯