首页 文章

Amazon SimpleDB问题:实现计数器属性

提问于
浏览
9

简而言之,我正在重写一个系统,我正在寻找一种在AWS SimpleDB中存储一些命中计数器的方法 .

对于那些不熟悉SimpleDB的人来说,存储计数器的(主要)问题是 Cloud 传播延迟通常超过一秒 . 我们的应用目前每秒达到约1,500次点击 . 并非所有这些命中都会映射到相同的键,但是每秒钟的大概数字可能会大约5-10次更新 . 这意味着如果我们使用传统的更新机制(读取,增量,存储),我们最终会无意中丢弃大量的命中 .

一种可能的解决方案是将计数器保留在memcache中,并使用cron任务来推送数据 . 这个问题的一大问题是它不是“正确”的方式 . Memcache不应该真正用于持久存储......毕竟,它是一个缓存层 . 另外,当我们进行推送时,我们最终会遇到问题,确保我们删除正确的元素,并希望它们没有争用,因为我们正在删除它们(很可能) .

另一个可能的解决方案是保留本地SQL数据库并在那里写入计数器,每隔很多请求更新我们的SimpleDB带外或运行cron任务来推送数据 . 这解决了同步问题,因为我们可以包含时间戳来轻松设置SimpleDB推送的边界 . 当然,还有其他问题,虽然这可能适用于大量的黑客攻击,但它似乎不是最优雅的解决方案 .

有没有人在他们的经历中遇到类似的问题,或有任何新颖的方法?任何建议或想法都会受到赞赏,即使它们没有完全被冲洗掉 . 我一直在考虑这个问题,并且可以使用一些新的观点 .

6 回答

  • 2

    我看到你已经接受了答案,但这可能算作一种新颖的方法 .

    如果您正在构建网络应用,那么您可以使用Google的Google Analytics产品跟踪网页展示次数(如果页面到域项目映射适合),然后使用Analytics API定期将这些数据推送到项目本身 .

    我没有详细考虑过,所以可能会有漏洞 . 考虑到您在该领域的经验,我实际上对您对此方法的反馈非常感兴趣 .

    谢谢斯科特

  • 15

    现有的SimpleDB API自然不适合作为分布式计数器 . 但它当然可以做到 .

    严格地在SimpleDB中工作有两种方法可以使它工作 . 一种简单的方法,需要像cron作业一样清理 . 或者是一种更复杂的技术,可以随时清理 .

    简单的方法

    简单的方法是为每个“命中”制作一个不同的项目 . 使用单个属性是关键 . 快速轻松地为域名注入数量 . 当您需要获取计数(通常不太可能)时,您必须发出查询

    SELECT count(*) FROM domain WHERE key='myKey'
    

    当然,这将导致您的域无限增长,并且查询将花费更长时间来执行 . 解决方案是一个摘要记录,您可以汇总到目前为止为每个密钥收集的所有计数 . 它只是一个项目,其中包含键的属性和一个“Last-Updated”时间戳,其粒度低至毫秒 . 这还要求您为“点击”项添加“timestamp”属性 . 摘要记录不需要位于同一个域中 . 实际上,根据您的设置,最好将它们保存在单独的域中 . 无论哪种方式,您都可以使用key作为itemName并使用GetAttributes而不是SELECT .

    现在计算是一个两步的过程 . 您必须提取摘要记录并查询“时间戳”,其严格大于摘要记录中的“上次更新”时间,并将两个计数加在一起 .

    SELECT count(*) FROM domain WHERE key='myKey' AND timestamp > '...'
    

    您还需要一种定期更新摘要记录的方法 . 您可以按计划(每小时)执行此操作,也可以根据其他一些条件动态执行此操作(例如,在查询返回多个页面时,在常规处理期间执行此操作) . 只需确保在更新摘要记录时,将其基于过去远远超过最终一致性窗口的时间 . 1分钟比安全更好 .

    这个解决方案适用于并发更新,因为即使许多摘要记录同时写入,它们都是正确的,无论哪一个胜利仍然是正确的,因为计数和'Last-Updated'属性将与每个其他 .

    即使您将摘要记录与命中记录保持在一起,这也适用于多个域,您可以同时从所有域中提取摘要记录,然后并行地向所有域发出查询 . 这样做的原因是,如果您需要更高的密钥吞吐量,而不是从一个域获得的密钥 .

    这适用于缓存 . 如果缓存失败,您将拥有权威备份 .

    时间会来到有人想要返回并编辑/删除/添加具有旧“Timestamp”值的记录 . 您必须在那时更新您的摘要记录(针对该域),否则您的计数将被取消,直到您重新计算该摘要为止 .

    这将为您提供与一致性窗口中当前可查看的数据同步的计数 . 这不会给你一个精确到毫秒的计数 .

    艰难的道路

    另一种方法是执行正常的读取 - 增量 - 存储机制,但也写入包含版本号和值的复合值 . 您使用的版本号比您要更新的值的版本号大1 .

    get(key)返回属性value =“Ver015 Count089”

    在这里,您检索存储为版本15的89计数 . 当您进行更新时,您可以编写如下值:

    put(key,value =“Ver016 Count090”)

    删除了之前的值 not ,最终得到的更新审计跟踪令人联想到lamport时钟 .

    这需要你做一些额外的事情 .

    • 能够在您进行GET时识别和解决冲突

    • 一个简单的版本号isn 't going to work you' ll想要包含一个时间戳,其分辨率至少为毫秒,也可能是一个进程ID .

    • 实际上,您希望您的值包含当前版本号和更新所基于的值的版本号,以便更轻松地解决冲突 .

    • 你可以't keep an infinite audit trail in one item so you' ll需要为你去的旧值发出删除 .

    你用这种技术得到的就像一棵不同的更新树 . 你将拥有一个值,然后突然发生多次更新,你将获得一堆基于相同旧值的更新,这些更新都不知道彼此 .

    当我说在GET时解决冲突时,我的意思是如果你读了一个项目并且值看起来像这样:

    11 --- 12
         /
    10 --- 11
         \
           11
    

    您必须能够确定实际值是14.如果为每个新值包含要更新的值的版本,则可以执行此操作 .

    它不应该是火箭科学

    如果你想要的只是一个简单的计数器: this is way over-kill . 制作简单的计数器不应该是火箭科学 . 这就是为什么SimpleDB可能不是制作简单计数器的最佳选择 .

    这不是唯一的方法,但如果您实现SimpleDB解决方案而不是实际拥有锁,那么大多数事情都需要完成 .

    不要误解我的意思,我实际上非常喜欢这种方法,因为没有锁定,并且可以同时使用此计数器的进程数量的界限大约为100.(因为项目中属性数量的限制)你可以通过一些改变超过100 .

    注意

    But if all these implementation details were hidden from you and you just had to call increment(key), it wouldn't be complex at all. With SimpleDB the client library is the key to making the complex things simple. But currently there are no publicly available libraries that implement this functionality (to my knowledge).

  • 0

    对于重新审视此问题的任何人,亚马逊刚刚添加了对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
    
  • 1

    对于任何对我最终处理这个问题感兴趣的人...(略微特定于Java)

    我最终在每个servlet实例上使用了EhCache . 我使用UUID作为键,使用Java AtomicInteger作为值 . 线程周期性地遍历缓存并将行推送到simpledb temp stats域,并将带有密钥的行写入失效域(如果密钥已存在,则无效地失败) . 该线程还使用前一个值递减计数器,确保我们在更新时不会错过任何匹配 . 一个单独的线程ping simpledb失效域,并在临时域中汇总统计信息(有每个键有多行,因为我们使用ec2实例),将其推送到实际的统计域 .

    我做了一点负载测试,似乎可以很好地扩展 . 本地我能够在负载测试器损坏之前处理大约500次点击(不是servlet - hah),所以如果我认为在ec2上运行应该只能提高性能 .

  • 2

    回答feynmansbastard:

    如果您想存储大量事件,我建议您使用分布式提交日志系统,例如kafkaaws kinesis . 它们允许廉价和简单地消耗事件流(kinesis的定价为每秒1K事件每月25美元) - 你只需要实现消费者(使用任何语言),批量读取前一个检查点的所有事件,聚合内存中的计数器然后将数据刷新到永久存储(dynamodb或mysql)并提交检查点 .

    可以使用nginx日志简单记录事件,并使用fluentd转换为kafka / kinesis . 这是一种非常便宜,高效且简单的解决方案 .

  • 20

    也有类似的需求/挑战 .

    我查看了使用谷歌分析和count.ly . 后者似乎太昂贵而不值得(加上他们对会话有一些混乱的定义) . GA我会喜欢使用,但我花了两天时间使用他们的库和一些第三方库(gadotnet和另外一个来自codeproject) . 不幸的是,我只能在GA实时部分看到计数器发布,即使api报告成功,也从未在正常的仪表板中发布 . 我们可能做错了但是我们超过了ga的时间预算 .

    我们已经有一个现有的simpledb计数器,使用前一个评论员提到的条件更新进行更新 . 这种方法效果很好,但是在存在争用和结合的情况下会受到影响(例如,与备份系统相比,我们最新的计数器在3个月内损失了数百万计数) .

    我们实现了一个更新的解决方案,这个解决方案有点类似于这个问题的答案,除了更简单 .

    我们只是对计数器进行分片/分区 . 创建计数器时,您可以指定分片数量,这是您期望的多少次同步更新的函数 . 这会创建一些子计数器,每个计数器都有一个以它作为属性开始的碎片计数:

    COUNTER(w / 5shards)创建:shard0 (仅供参考)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读取如果您知道碎片计数,读取每个碎片并求它们 . 如果您不知道分片计数,请从根分片中获取它,然后读取all和sum .

    由于更新传播缓慢,您仍然可以错过阅读中的计数,但它们应该在以后获取 . 这足以满足我们的需求,尽管如果您想要对此进行更多控制,您可以确保在阅读时 - 最后一个时间戳符合您的预期并重试 .

相关问题