我在App Engine数据存储区遇到了一个有趣的限制。我正在创建一个处理程序来帮助我们分析一个生产服务器上的一些使用数据。为了执行分析,我需要查询和汇总从数据存储中提取的10,000多个实体。计算并不困难,它只是通过使用样本的特定过滤器的项目的直方图。我遇到的问题是,在达到查询截止日期之前,我无法足够快地从数据存储中获取数据以进行任何处理。
我已经尝试了我能想到的所有内容,将查询分成并行RPC调用以提高性能,但根据appstats,我似乎无法让查询实际并行执行。无论我尝试什么方法(见下文),似乎RPC似乎都会回到连续下一次查询的瀑布。
注意:查询和分析代码确实有效,它只是运行缓慢,因为我无法从数据存储中快速获取数据。
我没有可以分享的实时版本,但这里是我所谈论的系统部分的基本模型:
class Session(ndb.Model):
""" A tracked user session. (customer account (company), version, OS, etc) """
data = ndb.JsonProperty(required = False, indexed = False)
class Sample(ndb.Model):
name = ndb.StringProperty (required = True, indexed = True)
session = ndb.KeyProperty (required = True, kind = Session)
timestamp = ndb.DateTimeProperty(required = True, indexed = True)
tags = ndb.StringProperty (repeated = True, indexed = True)
您可以将样本视为用户使用给定名称功能的时间。 (例如:' systemA.feature_x')。标签基于客户详细信息,系统信息和功能。例如:[' winxp',' 2.5.1',' systemA',' feature_x',' premium_account'] )。因此,标签形成一组非规范化的标记,可用于查找感兴趣的样本。
我想要做的分析包括获取一个日期范围,并询问每个客户帐户每天(或每小时)使用的功能集(可能是所有功能)的功能次数(公司,而不是每个用户) )。
因此处理程序的输入类似于:
输出将是:
[{
'company_account': <string>,
'counts': [
{'timeperiod': <iso8601 date>, 'count': <int>}, ...
]
}, ...
]
以下是所有查询的一些共同代码。处理程序的一般结构是使用webapp2的简单get处理程序,它设置查询参数,运行查询,处理结果,创建要返回的数据。
# -- Build Query Object --- #
query_opts = {}
query_opts['batch_size'] = 500 # Bring in large groups of entities
q = Sample.query()
q = q.order(Sample.timestamp)
# Tags
tag_args = [(Sample.tags == t) for t in tags]
q = q.filter(ndb.query.AND(*tag_args))
def handle_sample(sample):
session_obj = sample.session.get() # Usually found in local or memcache thanks to ndb
count_key = session_obj.data['customer']
addCountForPeriod(count_key, sample.timestamp)
我尝试过各种方法尝试尽快并行地从数据存储中提取数据。到目前为止我尝试过的方法包括:
这是一个与其他方法进行比较的简单基本案例。我只是构建查询并迭代所有项目,让ndb做它所做的事情,一个接一个地拉它们。
q = q.filter(Sample.timestamp >= start_time)
q = q.filter(Sample.timestamp <= end_time)
q_iter = q.iter(**query_opts)
for sample in q_iter:
handle_sample(sample)
这里的想法是看我是否能做一次非常大的提取。
q = q.filter(Sample.timestamp >= start_time)
q = q.filter(Sample.timestamp <= end_time)
samples = q.fetch(20000, **query_opts)
for sample in samples:
handle_sample(sample)
这里的想法是认识到样本在时间上是相当好的间隔所以我可以创建一组独立的查询,将整个时间区域分成块并尝试使用异步并行运行每个:
# split up timestamp space into 20 equal parts and async query each of them
ts_delta = (end_time - start_time) / 20
cur_start_time = start_time
q_futures = []
for x in range(ts_intervals):
cur_end_time = (cur_start_time + ts_delta)
if x == (ts_intervals-1): # Last one has to cover full range
cur_end_time = end_time
f = q.filter(Sample.timestamp >= cur_start_time,
Sample.timestamp < cur_end_time).fetch_async(limit=None, **query_opts)
q_futures.append(f)
cur_start_time = cur_end_time
# Now loop through and collect results
for f in q_futures:
samples = f.get_result()
for sample in samples:
handle_sample(sample)
我试过这个方法,因为文档听起来像ndb在使用Query.map_async方法时可能会自动利用一些并行性。
q = q.filter(Sample.timestamp >= start_time)
q = q.filter(Sample.timestamp <= end_time)
@ndb.tasklet
def process_sample(sample):
period_ts = getPeriodTimestamp(sample.timestamp)
session_obj = yield sample.session.get_async() # Lookup the session object from cache
count_key = session_obj.data['customer']
addCountForPeriod(count_key, sample.timestamp)
raise ndb.Return(None)
q_future = q.map_async(process_sample, **query_opts)
res = q_future.get_result()
我测试了一个示例查询来收集总体响应时间和appstats跟踪。结果是:
真实:15.645s
这个按顺序逐个获取批次,然后从memcache中检索每个会话。
真实:12.12s
与选项A有效,但由于某种原因有点快。
真实:15.251s
似乎在开始时提供更多并行性,但似乎在结果迭代期间通过一系列调用减慢速度。似乎也无法将会话内存缓存查找与待处理查询重叠。
真实:13.752s
这是我最难理解的。看起来它有很多重叠,但是所有东西似乎都在瀑布而不是平行延伸。
基于这一切,我错过了什么?我只是在App Engine上达到了限制,还是有更好的方法可以同时降低大量实体?
我不知道下一步该尝试什么。我想重写客户端以同时向app引擎发出多个请求,但这似乎是相当暴力的。我真的希望应用程序引擎应该能够处理这个用例,所以我猜我有些东西缺失。
最后我发现选项C对我来说是最好的。我能够在6.1秒内完成优化。仍然不完美,但更好。
在得到几个人的建议后,我发现以下项目是理解和记住的关键:
所以我做了更快的事情:
我仍然没有达到我期望或喜欢的性能,但它现在可行。我只是希望他们能够在处理程序中快速将大量顺序实体拉入内存。
答案 0 :(得分:8)
这样的大型处理不应该在具有60秒时间限制的用户请求中完成。相反,它应该在支持长时间运行的请求的上下文中完成。 task queue支持最多10分钟的请求,并且(我相信)正常的内存限制(F1实例,默认情况下为128MB of memory)。对于更高的限制(无请求超时,1GB +内存),请使用backends。
这里有一些尝试:设置一个URL,当访问该URL时,将触发任务队列任务。如果任务队列任务尚未完成,它将返回一个网页,该网页每隔约5秒轮询另一个响应true / false的URL。任务队列处理数据(可能需要大约10秒),并将结果作为计算数据或呈现的网页保存到数据存储区。初始页面检测到它已完成后,用户将被重定向到该页面,该页面将从数据存储区中获取现在计算的结果。
答案 1 :(得分:2)
新的实验Data Processing功能(MapReduce的AppEngine API)看起来非常适合解决此问题。它执行自动分片以执行多个并行工作进程。
答案 2 :(得分:1)
我遇到了类似的问题,在与Google支持人员合作几周后,我可以确认至少截至2017年12月没有神奇的解决方案。
tl; dr:对于B1实例上运行的标准SDK,可以预期 220 实体/秒的吞吐量高达 900 实体/秒对于在B8实例上运行的已修补SDK。
该限制与CPU相关,更改实例类型会直接影响性能。这通过在B4和B4_1G实例上获得的类似结果得到证实
我为拥有约30个字段的Expando实体获得的最佳吞吐量是:
对于标准GAE SDK,我尝试了各种方法,包括多线程,但最好证明fetch_async
和wait_any
。当前的NDB库在使用异步和未来方面做得非常出色,因此任何使用线程推送它的尝试都会使它变得更糟。
我找到了两种有趣的方法来优化它:
Matt Faus很好地解释了这个问题:
GAE SDK提供了一个API,用于读取和编写派生自的对象 您的类到数据存储区。这为您节省了无聊的工作 验证从数据存储区返回的原始数据并重新打包 成为一个易于使用的对象。特别是,GAE使用协议缓冲区 将原始数据从商店传输到需要的前端机器 它。然后,SDK负责解码此格式并返回 代码的干净对象。这个实用程序很棒,但有时它 比你想做的工作多一点。 [...]使用我们的分析 工具,我发现完全有50%的时间用于获取这些 实体是在protobuf-to-python-object解码阶段。这个 意味着前端服务器上的CPU是这些中的瓶颈 数据存储区读取!
这两种方法都试图通过减少解码字段的数量来减少将protobuf用于Python解码所花费的时间。
我尝试了两种方法,但我只能用Matt成功。自Evan发布他的解决方案以来,SDK内部发生了变化。我不得不改变Matt here发布的代码,但这很简单 - 如果有兴趣我可以发布最终代码。
对于具有约30个字段的常规Expando实体,我使用Matt的解决方案仅解码几个字段并获得显着改进。
总之,需要做出相应的规划,并且不要期望能够在实时&#34;实时&#34;中处理超过数百个实体。 GAE请求。
答案 3 :(得分:0)
App Engine上的大型数据操作最好使用某种mapreduce操作实现。
这是一个描述该过程的视频,但包括BigQuery https://developers.google.com/events/io/sessions/gooio2012/307/
听起来你不需要BigQuery,但你可能想要同时使用管道的Map和Reduce部分。
你正在做的事情和mapreduce情况之间的主要区别在于你正在启动一个实例并迭代查询,在mapreduce上,你将有一个单独的实例并行运行每个查询。您将需要一个reduce操作来“总结”所有数据,并将结果写在某处。
您遇到的另一个问题是您应该使用游标进行迭代。 https://developers.google.com/appengine/docs/java/datastore/queries#Query_Cursors
如果迭代器正在使用查询偏移量,那么它将是低效的,因为偏移量会发出相同的查询,跳过大量结果,并为您提供下一组,而光标会直接跳到下一组。