代码之家  ›  专栏  ›  技术社区  ›  Justin

AmazonSimpleDB困难:实现计数器属性

  •  9
  • Justin  · 技术社区  · 15 年前

    长话短说,我正在重写一个系统的一部分,并正在寻找一种方法来将一些计数器存储在AWS SimpleDB中。

    对于不熟悉simpledb的人来说,存储计数器的主要问题是云传播延迟通常超过一秒钟。我们的应用程序当前每秒获得约1500次点击。并不是所有的点击都会映射到同一个键,但一个大概的数字可能是每秒5-10次更新到一个键。这意味着,如果我们使用传统的更新机制(读取、增量、存储),我们最终会无意中丢失大量的点击量。

    一个潜在的解决方案是将计数器保存在memcache中,并使用cron任务推送数据。这件事的最大问题是它不是“正确”的方法。memcache不应该真正用于持久存储…毕竟,它是一个缓存层。此外,当我们进行推送时,我们将以问题结束,确保删除了正确的元素,并希望在删除它们时不会出现对它们的争用(这很可能)。

    另一个可能的解决方案是保留一个本地SQL数据库并在那里写入计数器,每隔这么多请求就更新simpledb,或者运行cron任务来推送数据。这就解决了同步问题,因为我们可以包含时间戳,以便为simpledb push轻松设置边界。当然,还有其他问题,尽管这可能与相当数量的黑客攻击一起工作,但看起来并不是最优雅的解决方案。

    有没有人在他们的经历中遇到过类似的问题,或者有什么新颖的方法?任何建议或想法都会受到赞赏,即使它们没有被完全冲走。我已经考虑了一段时间了,可以用一些新的观点。

    6 回复  |  直到 6 年前
        1
  •  20
  •   Mocky    15 年前

    现有的simpledb api本身并不适合作为分布式计数器。但这当然可以做到。

    严格在simpledb中工作有两种方法可以使其工作。一种简单的方法,需要像cron作业这样的东西来清理。或者一种更复杂的清洁技术。

    简单的方法

    简单的方法是为每一个“点击”制作不同的项目。只有一个属性,这是键。使用计数快速轻松地抽取域。当您需要获取计数(更不常见地假定)时,必须发出一个查询

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

    当然,这将导致您的域无限增长,并且随着时间的推移,查询将需要越来越长的时间来执行。解决方案是一个摘要记录,您可以在其中汇总到目前为止为每个键收集的所有计数。它只是一个具有键summary='mykey'属性的项,以及一个粒度小于毫秒的“最后更新”时间戳。这还要求您将“timestamp”属性添加到“hit”项中。摘要记录不需要在同一个域中。实际上,根据您的设置,它们最好保存在单独的域中。无论采用哪种方法,都可以使用键作为项名称,并使用getattributes而不是执行select。

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

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

    您还需要一种定期更新摘要记录的方法。您可以按计划(每小时)或基于某些其他条件动态地执行此操作(例如,当查询返回多个页面时,在常规处理过程中执行此操作)。只要确保在更新摘要记录时,将其建立在一个足够远的时间基础上,这样您就可以通过最终的一致性窗口。一分钟比安全多了。

    此解决方案在面对并发更新时有效,因为即使同时写入了许多摘要记录,它们都是正确的,无论哪一个成功都将是正确的,因为计数和“上次更新”属性将彼此一致。

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

    这对缓存很好。如果缓存失败,您将拥有权威备份。

    当有人想要返回并编辑/删除/添加一个旧的“时间戳”值的记录时,时间就会到来。此时您必须更新您的摘要记录(针对该域),否则您的计数将关闭,直到您重新计算该摘要。

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

    艰难的道路

    另一种方法是执行正常的读取-增量-存储机制,但也编写一个包含版本号和值的复合值。如果使用的版本号大于正在更新的值的版本号,则为1。

    get(key)返回属性value=“ver015 count089”

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

    输入(key,value=“ver016 count090”)

    上一个值是 删除后,您将得到一个类似于lamport时钟的更新审计跟踪。

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

    1. 在每次执行GET时识别和解决冲突的能力
    2. 一个简单的版本号将不起作用,您需要包含一个时间戳,其分辨率至少为毫秒,也可能包括一个进程ID。
    3. 实际上,您希望您的值包括当前版本号和更新所基于的值的版本号,以便更容易地解决冲突。
    4. 您不能在一个项目中保留无限的审计跟踪,因此您需要在执行过程中为旧值发出删除。

    使用这种技术所得到的结果就像是一棵不同的更新树。您将拥有一个值,然后突然发生多个更新,您将拥有一系列基于相同旧值的更新,这些旧值彼此都不知道。

    当我说“在获取时解决冲突”时,我的意思是,如果您阅读了一个项目,并且该值如下所示:

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

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

    这不应该是火箭科学

    如果您只需要一个简单的计数器: 这太过分了 . 制造一个简单的计数器不应该是火箭科学。这就是为什么simpledb可能不是制作简单计数器的最佳选择。

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

    别误会我,我确实喜欢这个方法,因为没有锁,并且可以同时使用这个计数器的进程数的限制在100左右。(由于一个项目中属性的数量有限),您可以通过一些更改超过100个。

    注释

    但是,如果所有这些实现细节对您都是隐藏的,并且您只需要调用increment(key),那么它就一点也不复杂了。使用simpledb,客户机库是使复杂事物简单化的关键。但目前没有公开的库来实现这个功能(据我所知)。

        2
  •  15
  •   Stephen McCarthy    6 年前

    对于任何重新讨论这个问题的人,亚马逊只是增加了对 Conditional Puts, 这使得计数器的实现更加容易。

    现在,要实现计数器,只需调用getattributes,增加计数,然后调用puttributes,并正确设置期望值。如果亚马逊回应错误 ConditionalCheckFailed ,然后重试整个操作。

    注意,每个puttributes调用只能有一个预期值。因此,如果您希望在一行中有多个计数器,那么可以使用version属性。

    伪代码:

    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
    
        3
  •  2
  •   Scott McKenzie    15 年前

    我看你已经接受了一个答案,但这可能算是一种新颖的方法。

    如果您正在构建一个Web应用程序,那么您可以使用谷歌的分析产品来跟踪页面印象(如果页面到域的项目映射适合),然后使用分析API定期将数据推送到项目本身。

    我没有仔细考虑过,所以可能会有漏洞。鉴于您在该领域的经验,我对您对这种方法的反馈非常感兴趣。

    谢谢 斯科特

        4
  •  2
  •   Justin    15 年前

    对于任何对我最终如何处理这件事感兴趣的人…(略微Java专用)

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

    我做了一点负载测试,而且它的规模似乎很好。在本地,我能够在负载测试仪崩溃之前处理大约500次点击/秒(而不是servlets-hah),所以如果我认为在EC2上运行任何东西只会提高性能。

        5
  •  1
  •   Maxim Filippovich    10 年前

    费曼斯达德的回答:

    如果您想要存储大量的事件,我建议您使用分布式提交日志系统,例如 kafka aws kinesis . 它们允许消费便宜而简单的事件流(Kinesis的定价是每月25美元,每秒钟1千个事件)_瘮瘮瘮瘮瘮瘮瘮瘮瘮瘮瘮瘮瘮瘮瘮瘮瘮瘮瘮T

    只需使用nginx日志就可以记录事件,并使用 fluentd . 这是一个非常便宜,性能和简单的解决方案。

        6
  •  0
  •   feynmansbastard    10 年前

    也有相似的需求/挑战。

    我研究过使用谷歌分析和count.ly。后者似乎太贵,不值得(另外,它们对会话的定义有些混乱)。我本来很想用的,但我花了两天时间使用他们的库和一些第三方的库(gadotnet和另一个来自codeproject)。不幸的是,我只能在GA Realtime部分看到计数器的发布,而在正常的仪表板中却看不到计数器的发布,即使API报告成功。我们可能做错了什么,但我们超出了GA的时间预算。

    我们已经有了一个现有的simpledb计数器,它使用前面的注释中提到的条件更新进行更新。这很好地工作,但当存在争用和一致性时,计数会丢失(例如,与备份系统相比,我们最新的计数器在3个月内丢失了数百万计数)。

    我们实现了一个新的解决方案,它与这个问题的答案有些相似,只是简单得多。

    我们只是把柜台分开。当您创建计数器时,您指定碎片的数量,这是您期望的模拟更新数量的函数。这将创建许多子计数器,每个计数器都以它作为属性开始碎片计数:

    计数器(带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写道 知道碎片数后,随机挑选一块碎片,并有条件地写下来。如果由于争用而失败,请选择另一个碎片并重试。 如果你不知道碎片的数量,从存在的根碎片中得到它,不管有多少碎片。因为它支持每个计数器进行多次写入,所以它可以将争用问题减少到您需要的程度。

    锐利阅读 如果你知道碎片的数量,读下每一块碎片并求和。 如果你不知道碎片的数量,从根碎片中得到它,然后读取全部和。

    由于更新速度缓慢,您仍然可能会错过计数读数,但应该稍后再读取。这足以满足我们的需要,但是如果您希望对其进行更多的控制,您可以确保-在读取时-最后一个时间戳如您所期望的那样,然后重试。

    推荐文章