长话短说,我正在重写一个系统,正在寻找一种在AWS SimpleDB中存储一些命中计数器的方法。
对于那些不熟悉SimpleDB的人来说,存储计数器的(主要)问题是云传播延迟通常超过一秒。我们的应用目前每秒达到约1,500次点击。并非所有这些命中都会映射到相同的键,但是每秒钟的大概数字可能会大约5-10次更新。这意味着如果我们使用传统的更新机制(读取,增量,存储),我们最终会无意中丢弃大量的点击。
一个可能的解决方案是将计数器保留在memcache中,并使用cron任务来推送数据。这个问题的一大问题是它不是“正确”的方式。 Memcache不应该真正用于持久存储......毕竟,它是一个缓存层。另外,当我们进行推送时,我们最终会遇到问题,确保我们删除正确的元素,并希望它们没有争用,因为我们正在删除它们(很可能)。
另一个可能的解决方案是保留本地SQL数据库并在那里写入计数器,每隔很多请求更新我们的SimpleDB带外或运行cron任务来推送数据。这解决了同步问题,因为我们可以包含时间戳来轻松设置SimpleDB推送的边界。当然,还有其他问题,虽然这可能适用于大量的黑客攻击,但它似乎不是最优雅的解决方案。
有没有人在他们的经历中遇到类似的问题,或者有任何新颖的方法?任何建议或想法都会受到赞赏,即使它们没有完全被冲洗掉。我一直在考虑这个问题,并且可以使用一些新观点。
答案 0 :(得分:20)
答案 1 :(得分:15)
对于重新审视此问题的任何人,亚马逊刚刚添加了对Conditional Puts,的支持,这使得实施计数器变得更加容易。
现在,要实现一个计数器 - 只需调用GetAttributes,递增计数,然后调用PutAttributes,并正确设置Expected Value。如果Amazon响应错误ConditionalCheckFailed
,则重试整个操作。
请注意,每个PutAttributes调用只能有一个预期值。因此,如果您想在一行中有多个计数器,那么请使用版本属性。
伪码:
begin
attributes = SimpleDB.GetAttributes
initial_version = attributes[:version]
attributes[:counter1] += 3
attributes[:counter2] += 7
attributes[:version] += 1
SimpleDB.PutAttributes(attributes, :expected => {:version => initial_version})
rescue ConditionalCheckFailed
retry
end
答案 2 :(得分:2)
我看到你已经接受了答案,但这可能算作一种新颖的方法。
如果您正在构建网络应用,那么您可以使用Google的Google Analytics产品跟踪网页展示次数(如果网页到域项目映射适合),然后使用Analytics API定期将这些数据推送到项目本身
我没有详细考虑过,所以可能会有漏洞。考虑到您在该领域的经验,我实际上对您对此方法的反馈非常感兴趣。
由于 斯科特
答案 3 :(得分:2)
对于任何对我最终处理这个问题感兴趣的人...(略微特定于Java)
我最终在每个servlet实例上使用了EhCache。我使用UUID作为键,使用Java AtomicInteger作为值。线程周期性地遍历缓存并将行推送到simpledb temp stats域,并将带有密钥的行写入失效域(如果密钥已存在,则无效地失败)。该线程还使用前一个值递减计数器,确保我们在更新时不会错过任何匹配。一个单独的线程ping simpledb失效域,并在临时域中汇总统计信息(每个键有多行,因为我们使用的是ec2实例),将其推送到实际的统计域。
我做了一点负载测试,似乎可以很好地扩展。在负载测试器损坏之前,我能够在本地处理大约500次点击(不是servlet - hah),所以如果我认为在ec2上运行的任何东西应该只能提高性能。
答案 4 :(得分:1)
对feynmansbastard的回答:
如果您想存储大量事件,我建议您使用分布式提交日志系统,例如kafka或aws kinesis。它们允许廉价和简单地消耗事件流(kinesis的定价为每秒1K事件每月25美元) - 你只需要实现消费者(使用任何语言),批量读取前一个检查点的所有事件,聚合内存中的计数器然后将数据刷新到永久存储(dynamodb或mysql)并提交检查点。
可以使用nginx日志简单记录事件,并使用fluentd转移到kafka / kinesis。这是一种非常便宜,高效且简单的解决方案。
答案 5 :(得分:0)
也有类似的需求/挑战。
我查看了使用谷歌分析和count.ly。后者似乎太昂贵而不值得(加上他们对会话有一些混乱的定义)。 GA我本来喜欢使用,但是我花了两天时间使用他们的库和一些第三方库(gadotnet和另外一个来自codeproject)。不幸的是,我只能在GA实时部分看到计数器发布,即使api报告成功,也从未在正常的仪表板中发布。我们可能做错了,但我们超过了ga的时间预算。
我们已经有一个现有的simpledb计数器,它使用前一个评论员提到的条件更新进行更新。这种方法效果很好,但是在存在争用和结合的情况下会受到影响(例如,与备份系统相比,我们最新的计数器在3个月内损失了数百万计数)。
我们实施了一个更新的解决方案,这个解决方案有点类似于这个问题的答案,除了更简单。
我们只是对计数器进行分片/分区。创建计数器时,您可以指定分片数量,这是您期望的多少次同步更新的函数。这会创建一些子计数器,每个计数器都有一个以它作为属性开始的碎片计数:
COUNTER(w / 5shards)创建: shard0 {numshards = 5}(仅供参考) shard1 {count = 0,numshards = 5,timestamp = 0} shard2 {count = 0,numshards = 5,timestamp = 0} shard3 {count = 0,numshards = 5,timestamp = 0} shard4 {count = 0,numshards = 5,timestamp = 0} shard5 {count = 0,numshards = 5,timestamp = 0}
Sharded Writes 知道了碎片计数,只需随机选择一个碎片并尝试有条件地写入碎片。如果由于争用而失败,请选择另一个分片并重试。 如果您不知道分片计数,请从存在的根分片中获取它,而不管存在多少分片。因为它支持每个计数器多次写入,所以它可以根据您的需要减少争用问题。
Sharded Reads 如果您知道碎片计数,请读取每个碎片并将它们相加。 如果您不知道分片计数,请从根分片中获取它,然后读取all并求和。
由于更新传播速度缓慢,您仍然可以错过阅读中的计数,但是它们应该会在以后被提取。这足以满足我们的需求,但如果您想要更多地控制它,您可以确保 - 在阅读时 - 最后一个时间戳符合您的预期并重试。