BigTable慢还是我笨?

29

我基本上有一个经典的多对多模型。一个用户,一个奖项,以及连接用户和奖项之间的“多对多”表。

每个用户大约有400个奖项,每个奖项都授予了大约一半的用户。

我想遍历所有用户的奖项并将它们的点数加起来。在 SQL 中,这将是一个多对多的表连接,然后遍历每一行。在拥有 MySQL 实例的良好机器上,400行应该不成问题。

在应用程序引擎上,我看到需要大约10秒钟才能完成求和。大部分时间都花在Google的数据存储中。以下是cProfile的前几行:

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      462    6.291    0.014    6.868    0.015 {google3.apphosting.runtime._apphosting_runtime___python__apiproxy.Wait}
      913    0.148    0.000    1.437    0.002 datastore.py:524(_FromPb)
     8212    0.130    0.000    0.502    0.000 datastore_types.py:1345(FromPropertyPb)
      462    0.120    0.000    0.458    0.001 {google3.net.proto._net_proto___parse__python.MergeFromString}

我的数据模型有问题吗?我查询数据库的方式有误吗?这是一个必须通过缓存和批量更新来解决的缺陷(这将非常麻烦)吗?


10
+1 LOL。我喜欢这个问题的标题! - Elijah
1
Google的BigTable不就是一个哈希表吗? - balpha
那个顶部的函数是一个等待函数… 你为什么要花6秒钟在等待上? - workmad3
@balpha:不,它没有实现为哈希表。@workmad3:这是应用程序进程等待数据存储进程(可能在同一节点上运行,我不确定)。 - Steve Jessop
5个回答

20

可能是两者都有的问题;-)

如果对Awards表进行了400次查询,每个查询返回一个Mapping表中的结果,则我会认为这将是一种痛苦的体验。查询结果的1000个限制是因为BigTable认为返回1000个结果已经接近其在合理时间内操作的极限。基于该体系结构,我期望执行400次查询比执行一次返回400个结果的查询要慢得多(400logN与(logM)+ 400)。

好消息是,在GAE上,使用memcache缓存包含所有奖项及其分数值的单个哈希表非常简单(当我一段时间前查看了memcache文档时,它看起来非常简单。但我还没有需要这样做)。

此外,如果您还不知道,for result in query.fetch(1000)for result in query更快,并且无论如何您都受到1000个结果的限制。后者的优点是(1)如果您提前退出,则可能更快,(2)如果Google将限制增加到1000以上,则可以获得好处而无需更改代码。

当您删除用户(或奖项)时,您可能也会遇到问题。在一个测试中,我发现我可以在时间限制内删除300个对象。这些对象比您的映射对象更复杂,具有3个属性和5个索引(包括隐式索引),而您的映射表可能仅具有2个属性和2个(隐式)索引。[编辑:刚刚意识到在我知道db.delete()可以接受列表之前,我进行了这个测试,使用列表可能更快]。

BigTable不一定做关系数据库设计得很好的事情。相反,它将数据分布在许多节点上。但几乎所有网站都可以在单个数据库服务器上运行良好,因此并不严格需要BigTable所做的事情。

还有一件事:如果您在单个HTTP请求上执行400个数据存储查询,那么您会发现在达到请求配额之前,您会触及数据存储的固定配额。 当然,如果您远未达到配额,或者如果您先遇到其他问题,则这可能与您的应用程序无关。但是两个配额之间的比例大约为8:1,我认为这是Google期望我的数据模型看起来像什么的提示。


4
很好的建议。听起来我应该转移到普通的Django模型并将所有内容存储在MySQL上,直到我遇到扩展问题。 - Paul Tarjan
2
如果你的数据在MySQL中比BigTable更好,我认为你必须问问自己为什么要使用应用引擎。如果有一个很好的理由(例如“免费托管”),那么我想是的,但对我来说,这看起来有点像一个hack。BigTable(以及通常分布在Google云上)可能是GAE和任何旧的LAMP堆栈之间唯一有趣的技术差异。 - Steve Jessop
4
或者您可以重新考虑您的模型。使用AppEngine数据存储时,您不希望在请求期间迭代行,而是快速地提取一行。一种方法是在写入时保持总计/小计/聚合值的最新状态,而不是在读取时更新。另一种方法是运行后台进程(使用cron或remote_api),异步更新总计/小计/聚合值。 - dar
1
@dar: 是的。实际上,我要说的是,目前使用GAE的最好理由之一就是你应该学会在其限制内工作,而不是绕过它们。这是获得其最有趣特点——分布式/可扩展性的唯一途径。 - Steve Jessop

19
我的数据模型有问题吗?我的查询方式有问题吗? 很不幸,是的。
就您的数据模型而言,迄今为止处理这种情况最好的方法是针对用户记录存储总和,并在用户获得/失去奖项时进行更新。在绝大多数情况下,每次计算他们的分数并没有什么意义,因为它将保持不变。如果您将“UserAward”实体类型作为“User”的子实体类型,则可以在单个原子事务中更新分数并插入或删除UserAward条目,确保您的计数始终准确。
onebyone指出您可以将奖项表存储在memcache中。这是一个好主意,但鉴于数据量有限,将其存储在本地内存中会更好。全局成员会在HTTP请求之间持久存在,并且由于我假设您不经常更新奖项表,因此您不必担心缓存失效。只需在第一次请求时加载它(甚至硬编码到源代码中)。如果您要更改奖项列表,则部署新的小更新将重置所有实例,导致它们重新加载。
对于查询,需要记住进行数据存储操作的实质代价是往返时间。获取(get())操作按ID查找1个或多个记录(您可以批量!)需要大约20-40毫秒。但是,查询需要大约160-200毫秒。因此,冗余设计是非常有用的。

谢谢。为了这个问题的简化,我确实简化了我的问题。我经常更新奖项和获奖者。而且要返回“用户奖项”,我需要更多的信息而不仅仅是积分。我想要奖项的图标,还有可能是标题。你的批处理get()是基于引用的吗?当我有400个UserAward行并开始遍历以获取userAward.award时,它会按ID和批处理获取吗?那可能就是救星了。 - Paul Tarjan
无法以“自然”的方式批量引用属性解析。您可以调用myent.properties()['propname'].get_value_for_datastore(myent)来检索Key,从而允许您批量处理。即使您经常更新奖项,我仍建议将它们全部存储在本地内存或memcache中,并使用某种方式来使缓存失效。您的另一个选择是在每个实体上使用奖项的ListProperty(Reference)。如果您不需要通过奖项查找用户,则可以设置indexed=False以减少开销。 - Nick Johnson

2
一个重要的应用引擎习语是存储便宜,时间永远不足。在应用引擎中实现多对多关系的最佳方法似乎是简单地在两侧存储信息。例如,一个用户有一个奖项列表,每个奖项都有一个用户列表。要查找用户拥有的所有奖项,只需查询某个用户的奖项表即可。
这个想法在这里得到了很好的演示:构建可扩展的复杂应用

0

即使您提到了BigTable,我认为您正在云SQL上实现关系数据库。

您的模型很好,这是做类似事情的正确方式。我没有看到将聚合数据反规范化到用户表中的充分理由。

您是否为快速表连接创建了索引?这非常简单。您可能需要为涉及表连接的所有字段创建BTree索引。无需为聚合字段(您取SUM的字段)创建索引。基本上,N:N表的两个外键都应该被索引。如果这些外键引用其他两个表的主键,那就足够了。

对于超过100行的数据,外键上的简单BTree索引可以显著提高吞吐量。

我正在CloudSQL上运行一个数据库,其中一些边缘表具有超过200万条记录。只有在超过250万条记录后,我才考虑一些反规范化,而且还要添加一些额外的索引,并仍然进行SUM聚合。否则,每当添加新记录时,我都会进行不必要的SUM字段更新。

只有当表中的记录超过100万条时,我们才必须考虑使用读取副本。那时,我们可以区分仅读取某些表而不写入的进程。

如果您正在使用Django,请注意根据其文档实现LIMIT时要小心,因为它非常具有误导性。当您在记录集上进行[:100](拼接)时,实际发送到SQL服务器的SQL并不是您所期望的。我花了很长时间才弄清楚这一点。当您计划执行会扩展到非常大规模的操作时,Django不是一个好选择。但是对于1000条记录的订单,它是可以接受的。

0

Google BigTable 运行在 Google 分布式文件系统上。

数据是分布式的。也许对于只有 400 行的 MySQL 数据库来说,MySQL 更快一些,但对于更大的数据,Google BigTable 可能更快。

我认为这就是为什么他们鼓励我们使用 memcache 来加速的原因。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接