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

Jus*_*tin 9 amazon-web-services amazon-simpledb

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

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

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

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

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

Moc*_*cky 20

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

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

简单的方法

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

SELECT count(*) FROM domain WHERE key='myKey'
Run Code Online (Sandbox Code Playgroud)

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

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

SELECT count(*) FROM domain WHERE key='myKey' AND timestamp > '...'
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

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

困难的方式

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

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

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

put(key,value ="Ver016 Count090")

之前的值不会被删除,您最终会得到更新的审计跟踪,这些更新让人想起lamport时钟.

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

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

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

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

      11 --- 12
     /
10 --- 11
     \
       11
Run Code Online (Sandbox Code Playgroud)

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

它不应该是火箭科学

如果你想要的只是一个简单的计数器:这就是过度杀戮.制作简单的计数器不应该是火箭科学.这就是为什么SimpleDB可能不是制作简单计数器的最佳选择.

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

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

注意

但是如果所有这些实现细节都对你隐藏而你只需要调用increment(key),它就不会很复杂了.使用SimpleDB,客户端库是使复杂事物变得简单的关键.但目前还没有公开的库实现这一功能(据我所知).


Ste*_*thy 15

对于重新审视此问题的任何人,亚马逊刚刚添加了对条件推送的支持,这使得实现计数器变得更加容易.

现在,要实现一个计数器 - 只需调用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
Run Code Online (Sandbox Code Playgroud)