2

出于性能原因,我在我的 GAE 应用程序中使用了分片计数器 ( https://cloud.google.com/appengine/articles/sharding_counters ),但我在理解它为什么这么慢以及如何加快速度时遇到了一些麻烦.

背景
我有一个 API,它一次抓取一组 20 个对象,对于每个对象,它从计数器中获取总数以包含在响应中。

指标
打开 Appstats 并清除缓存后,我注意到通过 datastore_v3.Get 获取 20 个计数器的总数会产生 120 个 RPC,这需要 2500 毫秒。

想法
这似乎是相当多的 RPC 调用,而且仅读取 20 个计数器就需要相当多的时间。我认为这会更快,也许这就是我错的地方。它应该比这更快吗?

进一步检查
我深入研究了统计数据,查看了 get_count 方法中的这两行:

all_keys = GeneralCounterShardConfig.all_keys(name)
for counter in ndb.get_multi(all_keys):

如果我注释掉 get_multi 行,我会看到 datastore_v3 有 20 个 RPC 调用。Get 总计 185 毫秒。

正如预期的那样,这使得 get_multi 成为 datastore_v3 100 次 RPC 调用的罪魁祸首。获得超过 2500 毫秒的时间。我验证了这一点,但这就是我感到困惑的地方。为什么用 20 个键调用 get_multi 会导致 100 次 RPC 调用?

更新 #1
我在 GAE 控制台中检查了 Traces 并看到了一些附加信息。他们在那里也显示了 RPC 调用的细分 - 但在他们所说的“批量获取以减少远程过程调用的数量”的情况下。不知道如何在使用 get_multi 之外做到这一点。认为那完成了工作。这里有什么建议吗?

更新 #2
以下是一些屏幕截图,显示了我正在查看的统计数据。第一个是我的基线 - 没有任何计数器操作的功能。第二个是在为一个计数器调用 get_count 之后。这显示了 6 个 datastore_v3.Get RPC 的差异。

基线 在此处输入图像描述

在一个计数器上调用 get_count 之后 在此处输入图像描述

更新 #3
根据 Patrick 的请求,我添加了来自控制台跟踪工具的信息屏幕截图。 在此处输入图像描述

4

2 回答 2

1

尝试拆分遍历每个项目的 for 循环和实际的 get_multi 调用本身。所以像:

all_values = ndb.get_multi(all_keys)
for counter in all_values:
    # Insert amazeballs codes here

我有一种感觉,它是其中之一:

  1. 生成器模式(来自 for 循环)导致 get_multi 执行路径有些奇怪
  2. 也许您期望的项目数与实际结果数不匹配,这可能会揭示 GeneralCounterShardConfig.all_keys(name) 的问题
  3. 分片数量设置得太高。我已经意识到任何超过 10 个分片都会导致性能问题。
于 2016-01-07T14:49:33.213 回答
0

当我研究类似的问题时,我了解到的一件事是,这get_multi可能会导致从您的应用程序发送多个 RPC。看起来 SDK 中的默认设置为每次获取 1000 个密钥,但我在生产应用程序中观察到的批量大小要小得多:更像 10(从内存中取出)。

我怀疑它这样做的原因是,在某个批量大小下,使用多个 RPC 实际上会更好:您的应用程序有更多的 RPC 开销,但有更多的 Datastore 并行性。换句话说:这仍然可能是读取大量数据存储对象的最佳方式。

但是,如果您不需要读取绝对最新值,您可以尝试设置该db.EVENTUAL_CONSISTENCY选项,但这似乎只在较旧的db库中可用,而在ndb. (尽管它似乎也可以通过Cloud Datastore API获得)。

细节

如果您查看 App Engine SDK 中的 Python 代码,特别是文件google/appengine/datastore/datastore_rpc.py,您将看到以下几行:

max_count = (Configuration.max_get_keys(config, self.__config) or
             self.MAX_GET_KEYS)
...

if is_read_current and txn is None:
  max_egs_per_rpc = self.__get_max_entity_groups_per_rpc(config)
else:
  max_egs_per_rpc = None

...

pbsgen = self._generate_pb_lists(indexed_keys_by_entity_group,
                                 base_req.ByteSize(), max_count,
                                 max_egs_per_rpc, config)

rpcs = []
for pbs, indexes in pbsgen:
  rpcs.append(make_get_call(base_req, pbs,
                            self.__create_result_index_pairs(indexes)))

我对此的理解:

  • max_count从配置对象设置,或1000作为默认值
  • 如果请求必须读取当前值,max_gcs_per_rpc从配置中设置,或者10作为默认值
  • 将输入键拆分为单独的 RPC,同时使用max_countmax_gcs_per_rpc作为限制。

因此,这是由 Python 数据存储库完成的。

于 2016-12-12T18:11:50.173 回答