您将如何平衡Google App Engine的多个*动态*后端实例的负载?

时间:2011-11-20 00:31:39

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

我对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次/秒。显然它会达到数据存储区的并发写入限制。

我决定收集一小时的数据并保存,这就是avgPercent中有hitCountHitsStatsDO的原因。 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转移到数据存储区。

另外,我class LifetimeStatsMap<Integer, HitsStatsDO>保留在memcached中,其中map-key是目标ID。

同样在我的backend shutdown hook method我将未完成时间的统计信息转储到数据存储区。

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

  • 我可以不使用后端实例吗?
  • 如果一个实例不够怎么办?
  • 如何在多个动态后端实例之间拆分数据?这很难,因为我不知道我有多少,因为谷歌在负载增加时创建新的。
  • 我知道我可以启动确切数量的驻留后端实例。但有多少? 2,5,10?如果我一周没有负载怎么办?不断运行10个后端实例太贵了。
  • 当后端实例死机/重启时,如何处理来自客户端的数据?

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

非常感谢你的想法。

3 个答案:

答案 0 :(得分:1)

开发人员不应回避使用gae整合离线资源,Google网站和Google数据API。

您可以设置一个google网站,该网站会显示您的调查表单。

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

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

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

您需要熟悉OAuth,以及Google联合登录/ openid消费者。

您可以探索将受访者的登录信息与您的表单进行整合。

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

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

然后仅使用gae生成用户特定页面。

或者,如果你对gae有太大的了解,你可以用它来生成调查页面,然后使用数据api将结果存储在google docs中,但是使用你自己的离线资源来执行统计计算。

答案 1 :(得分:0)

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

答案 2 :(得分:0)

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

因此,我不是在单个后端实例的内存中收集数据一小时,而是决定将其收集到多个动态后端实例中,并且每隔10分钟从每个实例更新数据存储区中当前小时的分片。类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)每小时,这是不错的。此外,我可以在我的代码中预先创建密钥,并从Datastore批量获取。

同样,我更改了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真是一个非常棒的社区。