【问题标题】:How would you balance load on multiple *dynamic* backend Instances of Google App Engine?您将如何平衡 Google App Engine 的多个*动态*后端实例的负载?
【发布时间】:2011-11-20 00:31:39
【问题描述】:

我对 StackOverflow 比较陌生,不确定是否适合提出设计问题。网站给了我一个提示“您提出的问题似乎很主观,很可能已关闭”。也许应该在programmers.stackexchange.com 上询问。请告诉我。

无论如何.. 我正在进行的项目之一是在线调查引擎。这是我在GAE 上的第一个大型商业项目。

我需要您就如何收集统计数据并有效地将其记录在 DataStore 中而不会让我破产的建议。初始要求是:

  • 用户完成调查后,客户端发送对列表 [ID (int) + PercentHit (double)]。此列表显示此用户的答案与参考回答者的预定义答案(由 ID 标识)的匹配程度。我称它们为“目标 ID”。
  • 调查的创建者希望查看给定 ID 在过去一小时、特定时间范围内或从调查开始时的汇总百分比。
  • 有些调查可能有成千上万的目标/参考回答者。

所以我创建了实体

public class HitsStatsDO implements Serializable
{
    @Id
    transient private Long id;
    transient private Long version = (long) 0;

    transient private Long startDate;

    @Parent transient private Key parent;   // fake parent which contains target id
    @Transient int targetId;

    private double avgPercent;
    private long hitCount;
}

但是为每个用户的每个目标编写 HitsStatsDO 会提供大量数据。例如,我进行了一项针对 3000 个目标的调查,一周内约有 400 万人回答了该调查,第一天有 30 万人参加了调查。即使我们假设他们在 24 小时内均匀地回答它,它也会给我们大约 1040 次写入/秒。显然它达到了 Datastore 的并发写入限制。

我决定收集一小时的数据并将其保存,这就是为什么HitsStatsDO 中有avgPercenthitCount。 GAE 实例是无状态的,所以我不得不使用dynamic backend instance

我有这样的东西:

// Contains stats for one hour
private class Shard
{
    ReadWriteLock lock = new ReentrantReadWriteLock();
    Map<Integer, HitsStatsDO> map = new HashMap<Integer, HitsStatsDO>(); // Key is target ID

    public void saveToDatastore();
    public void updateStats(Long startDate, Map<Integer, Double> hits);
}

并用分片映射当前小时和前一小时(不会在这里停留很长时间)

private HashMap<Long, Shard> shards = new HashMap<Long, Shard>();   // Key is HitsStatsDO.startDate

所以我每小时一次将前一小时的 Shard 转储到 Datastore。

另外,我有 class LifetimeStatsMap&lt;Integer, HitsStatsDO&gt; 保存在 memcached 中,其中 map-key 是目标 ID。

还在我的backend shutdown hook method 中,我将未完成小时的统计信息转储到数据存储区。

这里只有一个主要问题 - 我只有一个后端实例 :) 它提出了以下问题,我想听听您的意见:

  • 我可以在不使用后端实例的情况下执行此操作吗?
  • 如果一个实例不够用怎么办?
  • 如何在多个动态后端实例之间拆分数据?这很难,因为我不知道我有多少,因为随着负载的增加,Google 会创建新的。
  • 我知道我可以启动确切数量的驻留后端实例。但是有多少? 2、5、10 ?如果我一周没有任何负担怎么办。持续运行 10 个后端实例太昂贵了。
  • 当后端实例死机/重新启动时,我该如何处理来自客户端的数据?

需要注意的一点是,我不能改变太多客户。目前它是嵌入到客户网页中的 JavaScript。我可以以某种方式更改 RPC,但在架构上我不能用 Google Docs 表单替换客户端。

非常感谢您的想法。

【问题讨论】:

    标签: google-app-engine architecture scalability backend software-design


    【解决方案1】:

    开发人员不应回避将离线资源、谷歌网站和谷歌数据 api 与 gae 集成。

    您可以建立一个 google 网站来引导您的调查表。

    目标受访者会将他们的答案输入您的表单,Google 网站会将其收集到一个 Google 文档电子表格中。

    然后您使用离线系统(不是 gae)定期/每小时通过谷歌数据 api 访问“电子表格”以下载数据。

    Google 文档会为您提供数据输入的时间,而您的表单设计应该能够允许受访者编制索引。这样,您将只能下载“电子表格”的部分内容。

    您需要熟悉 OAuth,也许还需要熟悉 google 联合登录/openid 消费者。

    您可以探索将受访者的登录信息与您的表单集成。

    事实上,你甚至可能不必使用 gae。

    您应该能够使用 google sites api 更新您的页面,更新要显示的统计信息,将表单切换到新的电子表格。

    然后只使用 gae 来生成用户特定的页面。

    或者,如果你对 gae 的亲和力太大,你可以用它来生成调查页面,然后使用 data api 将结果存储在 google docs 中,但使用你自己的离线资源进行统计计算。

    【讨论】:

    • 感谢您的反馈!不幸的是,我不能那么彻底地改变客户。我应该在原帖中提到它。我更新了它。再次感谢!
    • “文档表单不会扩展到一百万受访者”——这就是 Google 所说的吗?就像一个谷歌博客页面不会扩展到一百万读者?请记住,匿名未能打倒亚马逊。一百万用户会关闭一个谷歌页面吗?
    • 我添加了对这个问题的回答。
    【解决方案2】:

    您在描述的模型上遇到并发写入限制的唯一原因是因为您将所有实例都设为同一父级的子实体。仅当您出于交易目的需要同一实体组中的实体时才需要这样做,此处不是这种情况。删除父属性并将所有实体存储为顶级实体,您将不再需要担心更新率。

    【讨论】:

    • 让我粗略计算一下成本。所以我想每个实体有 6 个写操作(实体 2 个 + targetId 索引 2 个 + 时间戳索引 2 个)。然后用 3k 个目标和 400 万用户进行调查将为我提供 12'000'000'000 个新的 HitsStatsDO 实体。那是 72B 写入 ($0.1/100K) + 12B smalls ($0.01/100K),分别是 $72K + $1200。对于一项受欢迎的调查来说,73.2 万美元的价格是相当高的 :(
    • @ruslan 在这种情况下什么是“3k 目标”?
    • 这些是与用户的答案进行比较的参考答案。例如,它可能是关于政治/社会/经济偏好的调查。 3000 名政治家参加了这项调查。然后公众也接受调查。现在你可以看到哪个候选人更接近多数人的观点。更简单的例子是 Sunglasses 在线商店。你不知道你想买什么。您回答 10 个问题,它会告诉您哪种太阳镜最适合您。类似的东西:)
    • @ruslan 那么为什么需要为用户响应和“目标”的每个组合存储数据呢?只需存储用户的响应,最后计算您需要的任何分析/报告。
    • 比存储 O(n*m) 记录更昂贵?这似乎不太可能。 :)
    【解决方案3】:

    我的服务上线了,我想分享一下我是如何实现它的。

    因此,我决定在多个动态后端实例中收集数据,而不是在单个后端实例的内存中收集一个小时的数据,并从每个实例每 10 分钟更新一次 Datastore 中当前小时的分片。 Shard 类保持不变,saveToDatastore() 除外,我现在在事务循环中更新 HitsStatsDOs 以确保即使另一个后端实例此时更改了分片,它也会更新。

    为了真正快速地获取 HitsStatsDO,我决定将目标 ID 放入假的父键和时间戳中,如果这样很难主 ID 的话

    public class HitsStatsDO implements Serializable
    {
        @Id
        transient private Long id;  // always equals to "startDate"
        @Unindexed
        transient private Long version = (long) 0;
    
        transient private Long startDate;
    
        @Parent
        transient private Key targetIdKey;   // fake parent which contains target id
    
        @Unindexed
        private double avgPercent;
        @Unindexed
        private long hitCount;
    
        public Key<HitsStatsDO> createKey()
        {
            return new Key<HitsStatsDO>(targetIdKey, HitsStatsDO.class, startDate);
        }
    
        public HitsStatsDO(Long startDate, long targetId)
        {
            this.id = this.startDate = startDate;
            this.targetIdKey = new Key(Long.class, targetId);
        }
    }
    

    这个实体只需要 2 次写入来存储。每小时写入量永远不会超过([后端实例数量] * 2 * 6),这还不错。我也可以在我的代码中预先创建密钥并从数据存储中批量获取。

    同样,我更改了HitsStatsTotalDO,其中包含调查开始时的统计数据。看起来是这样的

    public class HitsStatsTotalDO implements Serializable
    {
        @Id
        private Long targetId;
        @Unindexed
        transient private Long version = (long) 0;
    
        @Unindexed
        private double avgPercent;
        @Unindexed
        private long hitCount;
    }
    

    同样的事情 - 2 次写入存储/更新。

    服务于 3 天前上线。迄今为止的最大负载为 230 QPS。我正在使用动态 B1 类型的实例。在配置中,我现在最多设置 4 个实例,但令我高兴的是,GAE 从未实例化超过一个。令人惊讶的是,我还没有并发异常。

    如果您有任何问题或认为我遗漏了什么,请告诉我。

    感谢大家的帮助。 StackOverflow 是一个非常棒的社区。​​p>

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-07-15
      • 2016-09-23
      • 2018-11-25
      • 2019-08-15
      • 1970-01-01
      • 2014-08-31
      • 2012-08-06
      相关资源
      最近更新 更多