【问题标题】:Performance issues with App Engine memcache / ndb.get_multiApp Engine 内存缓存 / ndb.get_multi 的性能问题
【发布时间】:2014-07-24 17:09:28
【问题描述】:

在 App Engine (Python) 中使用 ndb.get_multi() 从 Memcache 获取多个键时,我发现性能非常差。

我正在获取大约 500 个小对象,所有这些对象都在内存缓存中。如果我使用ndb.get_multi(keys) 执行此操作,则需要 1500 毫秒或更长时间。以下是 App Stats 的典型输出:

如您所见,所有数据均来自 memcache。大多数时间被报告为在 RPC 调用之外。但是,我的代码尽可能少,所以如果时间花在 CPU 上,它必须在 ndb 内部的某个地方:

# Get set of keys for items. This runs very quickly.
item_keys = memcache.get(items_memcache_key)
# Get ~500 small items from memcache. This is very slow (~1500ms).
items = ndb.get_multi(item_keys)

您在 App Stats 中看到的第一个 memcache.get 是获取一组键的单次提取。第二个 memcache.get 是 ndb.get_multi 调用。

我正在获取的项目非常简单:

class Item(ndb.Model):
    name = ndb.StringProperty(indexed=False)
    image_url = ndb.StringProperty(indexed=False)
    image_width = ndb.IntegerProperty(indexed=False)
    image_height = ndb.IntegerProperty(indexed=False)

这是某种已知的 ndb 性能问题吗?与反序列化成本有关吗?还是内存缓存问题?

我发现如果我不是获取 500 个对象,而是将所有数据聚合到一个 blob 中,我的函数运行时间为 20 毫秒而不是 > 1500 毫秒:

# Get set of keys for items. This runs very quickly.
item_keys = memcache.get(items_memcache_key)
# Get individual item data.
# If we get all the data from memcache as a single blob it is very fast (~20ms).
item_data = memcache.get(items_data_key)
if not item_data:
    items = ndb.get_multi(item_keys)
    flat_data = json.dumps([{'name': item.name} for item in items])
    memcache.add(items_data_key, flat_data)

这很有趣,但对我来说并不是真正的解决方案,因为我需要获取的项目集不是静态的。

我看到的表现是典型的/预期的吗?所有这些测量都基于默认的 App Engine 生产配置(F1 实例、共享内存缓存)。是反序列化成本吗?或者可能是由于从 memcache 中获取多个键? 我认为问题不在于实例加速时间。我使用 time.clock() 调用逐行分析代码,我看到大致相似的数字(比我在 AppStats 中看到的快 3 倍,但仍然非常慢)。这是一个典型的配置文件:

# Fetch keys: 20 ms
# ndb.get_multi: 500 ms
# Number of keys is 521, fetch time per key is 0.96 ms

更新:出于兴趣,我还分析了这一点,所有应用引擎性能设置都增加到最大值(F4 实例、2400Mhz、专用内存缓存)。表现也好不了多少。在更快的实例上,App Stats 计时现在与我的 time.clock() 配置文件相匹配(因此获取 500 个小对象需要 500 毫秒而不是 1500 毫秒)。但是,它似乎非常缓慢。

【问题讨论】:

  • 你是在测试生产环境还是SDK。
  • 显示的所有测试均来自生产。
  • 500 是很多对象。每个都需要反序列化和重组,这需要时间。单人获得那么糟糕,您是否可能在项目上获得非常好的缓存命中率。当您在 memcache 中没有全部 500 个但只有一半/季度时会发生什么。如果不是一组静态键,你如何识别这组 500 个键?
  • 我相信这些对象是使用 protobufs 序列化/反序列化的,它们并不是特别快(至少以前是这样)。
  • 多想想发生了什么。 ndb.get_multi 将尝试从潜在的实例级缓存 memcache 中获取每个键,然后失败并转到数据存储区。这不仅仅是用这个键列表获取一堆项目。对于 500 个密钥,需要进行大量工作才能保证您获得 500 个列表中的所有密钥

标签: python performance google-app-engine memcached


【解决方案1】:

我对此进行了详细调查,问题在于 ndb 和 Python,而不是 memcache。事情如此缓慢的原因部分是反序列化(解释了大约 30% 的时间),其余部分似乎是 ndb 的任务队列实现的开销。

这意味着,如果你真的想要,你可以避免使用 ndb,而是直接从 memcache 中获取和反序列化。在我的包含 500 个小实体的测试用例中,这提供了 2.5 倍的巨大加速(在生产中的 F1 实例上为 650 毫秒对 1600 毫秒,或者在 F4 实例上为 200 毫秒对 500 毫秒)。 这个要点展示了如何做到这一点: https://gist.github.com/mcummins/600fa8852b4741fb2bb1

这里是手动 memcache 获取和反序列化的 appstats 输出:

现在将此与使用 ndb.get_multi(keys) 获取完全相同的实体进行比较:

几乎是 3 倍的差异!!

分析每个步骤如下所示。请注意,时间与 appstats 不匹配,因为它们在 F1 实例上运行,因此实时是 3 倍时钟时间。

手动版:

# memcache.get_multi: 50.0 ms
# Deserialization:  140.0 ms
# Number of keys is 521, fetch time per key is 0.364683301344 ms

vs ndb 版本:

# ndb.get_multi: 500 ms
# Number of keys is 521, fetch time per key is 0.96 ms

所以 ndb 需要 1 毫秒的时间来获取每个实体,即使实体只有一个属性并且在内存缓存中。那是在 F4 实例上。在 F1 实例上需要 3 毫秒。这是一个严重的实际限制:如果您想保持合理的延迟,则在处理 F1 实例上的用户请求时,您不能获取超过 100 个任何类型的实体。

显然 ndb 正在做一些非常昂贵且(至少在这种情况下)不必要的事情。我认为它与它的任务队列和它设置的所有期货有关。是否值得绕过 ndb 并手动执行操作取决于您的应用程序。如果您有一些 memcache 未命中,那么您将不得不进行数据存储提取。因此,您基本上最终会部分重新实现 ndb。然而,由于 ndb 似乎有如此巨大的开销,这可能是值得的。至少根据我对小对象的大量 get_multi 调用的用例来看,似乎是这样,并且预期的内存缓存命中率很高。

这似乎还表明,如果 Google 将 ndb 和/或反序列化的一些关键位实现为 C 模块,Python App Engine 可能会大大加快。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-23
    • 2013-11-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-28
    相关资源
    最近更新 更多