查询App Engine中随时间变化的百分比

时间:2013-04-19 16:26:32

标签: python google-app-engine app-engine-ndb

我的网站对来自连续更新的Feed的时间序列数据进行索引。网站的用户应该能够配置当数据中特定属性的值在特定时间段内改变某个百分比时触发的警报。

示例:假设我们正在跟踪用户拥有的Twitter粉丝数量。这就是(简化的)数据Feed的样子:

日期,粉丝

  • 10:00,1
  • 10:01,2
  • 10:02,2
  • 10:03,15
  • ...

通知:

  • 如果'粉丝'在过去1小时内增加了15%,请通知我。
  • 如果“粉丝”在过去40分钟内减少了10%,请通知我。

只有一个简单的数据Feed。 (希望)将定义数以千计的警报。其中许多警报可能类似,但很难估计会有多少独特警报。

编辑:忘记提及此事,但是关注者的数量经常变化(每分钟)。

使用数据存储区和其他App Engine设施实现此类机制的最佳方式是什么?警报应该相对实时触发(+/-几分钟)。

谢谢!

4 个答案:

答案 0 :(得分:1)

覆盖put意味着每次写入都会进行计算,这可能效率很低。如果您允许用户设置这些警报,您可能最终会得到代表警报的数据存储对象,这意味着每次评估警报时都会有获取或查询。

一个选项是任务:当数据Feed发生变化时,启动任务以评估警报。至少,这将允许初始数据馈送写入请求更快地完成。但是,如果数据Feed快速变化,您可能会执行许多任务,而且大多数任务都会因最近的数据更改而变得不必要。

也许最好的选择是cron任务,每隔几分钟运行一次。如果需要,您可以根据负载更改cron作业的时间,如果您有许多用户/警报,则以高度并行的方式进行处理会更加可行。

答案 1 :(得分:0)

我将尝试对模型进行非规范化,并在性能和冗余,写操作和读操作之间找到平衡。

例如:

  1. 由于服务侧重于实时更改,因此每个特定属性的乘法数据可以一起存储在一个数据存储中。例如,大型实体在五天内存储同一用户的所有更改。因此,随时间的变化不需要额外的查询来计算。 这也是谷歌主持他们的代码堵塞应用程序引擎的方式。可以在数据存储区中应用树结构来提供一些额外的功能。

  2. 对于警报,一种常见的方法是写下直接在数据模型本身上观察数据更改的人。

  3. 由于非规范化确实需要澄清用例是什么,因此该设计仅基于我的假设。

    class Watcher(ndb.Model):
        # define the rule such as "Notify me if 'followers' has increased by 15% in the past 1 hour."
        pass
    
    
    class Attribute(ndb.Model):
        name = ndb.StringProperty() # the name of this attribute such as "twitter_user_1:followers"
        data = ndb.PickleProperty() # a tree store all changes of the specify attribute
    
        watch_list = ndb.LocalStructureProperty(repeated=True, kind=Watcher) # who want to received the notification
    

    因此,该服务可以在一个地方收集所有必要的信息。

答案 2 :(得分:0)

有时候,当你需要保持变量的移动平均值时,你能做的最好的事情就是回到那个需求的来源,看看它是否可以用指数衰减的加权平均值代替权重(阅读wikipedia article解释它)。它不像移动平均线那么简单,但维护和存储更简单,特别是如果你想在线和实时计算它。

例如,假设您没有查看所提供系列的移动平均线,而是查看具有衰减权重且平均半衰期为1分钟的平均值。

  • 10:00,1(平均为1)
  • 10:01,2(旧平均值为1,权重为0.5,新数据为2,新加权平均值为(1 * 0.5 + 2 * 1)/(0.5 + 1)= 1.667
  • 10:02,2(旧平均值为1.667,权重为0.75,新数据为2,新加权平均值为(1.667 * 0.75 + 2 * 1)/(0.75 + 1)= 1.85
  • 10:03,15(旧平均值为1.85,重量为0.875,新数据为15,新加权平均值为(1.85 * 0.875 + 15 * 1)/(0.875 + 1)= 8.8667
  • ...

它可能看起来很复杂,但实际上很简单。当然,你需要将你看到的半衰期调整到适合你需要的东西(这与选择移动平均线的窗口有点不同)。

使用衰减权重平均值比移动平均值有两大优势:

  1. 您无需记录离散值来计算平均值;您只需要存储当前值和采样时间。
  2. 您只需要在数据更改时重新计算平均值。当它没有变化时,平均值的权重会衰减,但其值仍然存在。因此,您可以在收到新数据时计算它,而不是在cron或其他某种类型的单独任务中计算。
  3. PS,稍微玩一下方程,你可以找到一些你可以用它做的更有用的东西,比如存储你可以索引的那个值的e ^ X,因为它保持了不同值之间的序数关系。您随时间监控的指标。

答案 3 :(得分:0)

如果您的数据不需要每位用户每分钟更新一次:

  1. 在用户的LocalStructuredProperty中设置“提醒”。
  2. 当从Feed中“放入”传入数据点时,请使用预置挂钩预先计算值:

    • 在pre-put挂钩中抓取用户实体。 (如果使用NDB而你已经抓住了用户,它应该来自本地内存)
    • 抓取该用户的所有“提醒”并异步处理它们(tasklets)
    • 将每个人的警报数据存储在自己的实体中,使用特殊的键名来快速查询(例如,设置他们的键名为<user>_<alert_type>_<time_in_seconds>_<percentage>,这样您就可以get代替query在此对象中,存储所有进入并且在指定时间范围内的数据点。对于每分钟一次更新,您可以存储1000多个数据点作为元组(<timestamp>, <value>)的列表。根据定义的配置发出警报并存储新值。
  3. 示例(tbh。这是一个粗略的例子。如果你想要保证数据,应该使用交易):

    class AlertConfiguration(ndb.Model):
      timespan_in_seconds = ndb.IntegerProperty('tis', indexed=False)
      percent_change = ndb.FloatProperty('pc', indexed=False)
    
    class User(ndb.Model):
      alerts = LocalStructuredProperty(AlertConfiguration, repeated=True, name='a')
      ...
    
    class DataPoint(ndb.Model):
       timestamp = ndb.DateTimeProperty('ts', auto_now_add=True)
       value = ndb.FloatProperty('v')
       user = ndb.KeyProperty(name='u', kind=User)
    
       def _pre_put_hook(self):
         alerts = self.user.get().alerts
         futures = []
         for alert in alerts:
           futures.append(process_alert(alert, self))
         yield futures
    
    class AlertProcessor(ndb.Model):
      previous_data_points = ndb.JsonProperty(name='pdp', compressed=True)
    
    @ndb.tasklet
    def process_alert(alert_config, data_point):
      key_name = '{user}_{timespan}_{percentage}'.format(user=data_point.user.id(), timespan=alert_config.timespan_in_seconds, percentage=alert_config.percent_change)
      processor = yield AlertProcessor.get_or_insert_async(key_name)
      new_points = []
      found = False
      for point in processor.previous_data_points:
         delta = data_point.timestamp - datetime.strptime(point[0], '%c')
         seconds_diff = (86400 * delta.days) + delta.seconds
         if seconds_diff < alert_config.timespan_in_seconds:
           new_points.add(point)
           if not found:
             found = True
             if (data_point.value - point[1]) / data_point.value >= alert_config.percent_change:
                #E-mail alert here?
      new_points.append((data_point.timestamp.strftime('%c'), data_point.value))
      processor.previous_data_points = new_points
      yield processor.put_async()