Google app engine 从数据存储中查询大量ndb实体的最佳实践

Google app engine 从数据存储中查询大量ndb实体的最佳实践,google-app-engine,app-engine-ndb,google-cloud-datastore,Google App Engine,App Engine Ndb,Google Cloud Datastore,我在应用程序引擎数据存储中遇到了一个有趣的限制。我正在创建一个处理程序来帮助我们分析一个生产服务器上的一些使用数据。要执行分析,我需要查询和汇总从数据存储中提取的10000多个实体。计算并不困难,它只是通过使用情况样本特定过滤器的项目的直方图。我遇到的问题是,我无法以足够快的速度从数据存储中获取数据,以便在到达查询截止日期之前进行任何处理 我已经尽了我所能将查询分块到并行RPC调用中以提高性能,但根据appstats,我似乎无法让查询实际并行执行。无论我尝试什么方法(见下文),RPC总是会退回到

我在应用程序引擎数据存储中遇到了一个有趣的限制。我正在创建一个处理程序来帮助我们分析一个生产服务器上的一些使用数据。要执行分析,我需要查询和汇总从数据存储中提取的10000多个实体。计算并不困难,它只是通过使用情况样本特定过滤器的项目的直方图。我遇到的问题是,我无法以足够快的速度从数据存储中获取数据,以便在到达查询截止日期之前进行任何处理

我已经尽了我所能将查询分块到并行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\u x','premium\u account']。因此,这些标记形成了一组非规范化的标记,可用于查找感兴趣的样本

我试图进行的分析包括确定日期范围,并询问每个客户帐户(公司,而不是每个用户)每天(或每小时)使用一组功能(可能是所有功能)的功能有多少次

因此,处理程序的输入类似于:

  • 开始日期
  • 结束日期
  • 标签
产出将是:

[{
   'company_account': <string>,
   'counts': [
      {'timeperiod': <iso8601 date>, 'count': <int>}, ...
   ]
 }, ...
]
尝试的方法 我尝试了多种方法,试图尽快并行地从数据存储中提取数据。到目前为止,我尝试的方法包括:

A.单次迭代 与其他方法相比,这更像是一个简单的基本情况。我只是构建查询并迭代所有项,让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>=开始时间)
q=q.filter(Sample.timestamp=start\u时间)
q=q.filter(Sample.timestamp=cur\u start\u time,
Sample.timestamp
D.异步映射 我尝试了这种方法,因为文档使它听起来像ndb在使用Query.map\u异步方法时可以自动利用一些并行性

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()
q=q.filter(Sample.timestamp>=开始时间)

q=q.filter(Sample.timestamp应用程序引擎上的大数据操作最好使用某种mapreduce操作实现

这里有一段视频描述了这个过程,但包括BigQuery

听起来您不需要BigQuery,但您可能希望同时使用管道的Map和Reduce部分

您所做的与mapreduce情况的主要区别在于,您正在启动一个实例并迭代查询,在mapreduce上,每个查询都会有一个单独的并行实例。您将需要一个reduce操作来“汇总”所有数据,并将结果写在某个地方

另一个问题是应该使用游标进行迭代


如果迭代器使用的是查询偏移量,则效率会很低,因为偏移量会发出相同的查询,跳过许多结果,并给出下一个集合,而光标会直接跳到下一个集合。

这样的大处理不应该在用户请求中完成,因为用户请求有60秒的时间限制。相反,应该在cont中完成支持长时间运行请求的ext。支持长达10分钟的请求,并且(我相信)支持正常的内存限制(F1实例,默认值)。对于更高的限制(无请求超时,1GB+内存),请使用


这里有一些可以尝试的方法:设置一个URL,当被访问时,它会触发一个任务队列任务。它会返回一个网页,每隔5秒轮询一次另一个URL,如果任务队列任务已经完成,该URL会以真/假响应。任务队列处理数据,可能需要大约10秒的时间,并将结果保存到数据存储中,或者作为计算数据或呈现的网页。一旦初始页面检测到它已完成,用户将重定向到该页面,该页面将从数据存储中获取现在计算的结果。

新的实验功能(MapReduce的AppEngine API)看起来非常适合解决此问题。它会自动切分以执行多个并行工作进程。

我也有类似的问题,在与谷歌支持部门合作几周后,我可以确认至少到2017年12月为止没有神奇的解决方案

tl;dr:对于在B1实例上运行的标准SDK,可以预期吞吐量从220个实体/秒到在B8实例上运行的补丁SDK的900个实体/秒

该限制与CPU有关,更改实例类型直接影响性能。在B4和B4_1G实例上获得的类似结果证实了这一点

对于具有大约30个字段的Expando实体,我得到的最佳吞吐量是:

标准GAE SDK
  • B1实例:~220个实体/秒
  • B2实例:~250个实体/秒
  • B4实例:~560个实体/秒
  • B4_1G实例:~560个实体/秒
  • B8实例:~650个实体/秒
补丁GAE SDK
  • B1实例:~420个实体/秒
  • B8实例:~900
    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)
    
    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()