出于计费目的,ndb 缓存读取操作是否仍算作数据存储读取操作?

Are ndb cached read ops still counted as datastore read ops for billing purposes?

来自NDB Caching

NDB manages caches for you. There are two caching levels: an in-context cache and a gateway to App Engine's standard caching service, memcache. Both caches are enabled by default for all entity types, but can be configured to suit advanced needs.

我的应用程序没有进行任何 ndb 缓存配置更改,因此它必须使用默认设置 - 两个缓存级别都已启用。

我正在 运行 在我的暂存环境(一个单独的专用 GAE 项目)上进行一些测试,我可以在其中完全隔离 activity 序列与任何虚假的外部请求。

activity 的每个序列都包含相互触发的级联推送任务,创建数百个可变实体,修改其中一些实体的次数可变,读取所有实体的次数可变次,最后全部删除。

只有少数其他已经存在的实体在序列中被访问,所有这些实体在序列结束后仍然存在。对持久实体的访问次数应明显低于对易失实体的访问次数。

绝大多数读取操作是通过键或 ID 进行的实体查找,从 keys_only 查询或其他相关实体中获得。

我没有使用实体祖先。大多数这些任务执行跨组事务,我确实经常看到事务 failures/retries 由于一些 "hot" 实体的数据争用(我估计在这个 运行,在 Stackdriver 日志页面中很难计数)。

在每日配额重置后重新执行一个这样的 ~20 分钟序列后,应用程序的仪表板显示云数据存储读取操作(3 万次)是云数据存储实体写入操作(1 万次)的 3 倍。不稳定实体的数量由 Cloud Datastore 实体删除(0.00089 万)表示,如果重要的话。

memcache 命中率为 81%,但我不知道这是仅针对我的应用程序的显式 memcache 使用还是包括 ndb 的 memcache 使用。

一些较早的类似测量但不是在干净的环境中产生了类似的结果,我做了这个干净的作为验证。

这些观察 出现 表明实体从缓存中读取仍然算作数据存储读取。但在这里我假设:

我在文档中没有找到任何关于此的内容,所以想知道是否有人 知道 缓存的 ndb 读取是否确实算作数据存储读取或者可以指出我的缺陷有关该主题的解释或一些官方文档。

memcache 命中率包括 ndb 写入的实体的命中率。它不包括 ndb 的上下文缓存命中数。

每次写入数据存储都会使缓存失效,因此下一次读取不会缓存在内存缓存或上下文缓存中。

另一件事是 ndb 为每个事务创建一个新的上下文缓存,因此上下文缓存在面对事务时不是很有效。

快速回答是数据存储实体的内存缓存命中不作为数据存储读取收费。

根据吉姆的回答,我做了进一步的挖掘,在这里分享我的发现,因为其他人可能会觉得它们有用。

我祝贺自己从第一天起就决定为我的所有数据存储实体构建和使用通用包装器 class,并由每个实体种类的特定 classes 进一步继承。这不是使用直接继承 ndb.Model.

的模型 classes 进行操作

我已经在这个通用 class 中有一个 db_data @property 用于按需从数据存储中读取实体,很容易插入我自己的基于内存缓存的实体缓存方案和跟踪代码,以确定读取是来自 memcache 还是来自 ndb,以及它们是否在事务内部完成。这是它的样子:

@property
def db_data(self):
    if not hasattr(self, self.attr_db_data):
        db_data = None
        if self.readonly or not ndb.in_transaction():
            # noinspection PyTypeChecker
            db_data = memcache.get(self.memcache_key)
        if db_data:
            if not isinstance(db_data, self.db_model):
                logging.error('mismatched cached db_data kind, expected %s got type %s %s' %
                              (self.kind, type(db_data), db_data))
                db_data = None
            else:
                if self.trace:
                    logging.debug('%s from memcache' % self.lid)
                self.cached = True
        if not db_data:
            if hasattr(self, self.attr_db_key):
                db_data = self.db_key.get()
            elif hasattr(self, self.attr_key_id):
                db_data = self.db_model.get_by_id(id=self.key_id)
            else:
                raise RuntimeError('missing db_data, db_key and key_id')
            if db_data:
                # use ndb model names as strings in the list below. TODO: don't leave them on!
                show_trace = self.kind in ['']
                if self.trace or show_trace:
                    if show_trace:
                        self.print_stack(4)
                    logging.debug('%s from datastore' % self.lid)
                self.cached = False
                # noinspection PyTypeChecker
                memcache.set(self.memcache_key, db_data)
        setattr(self, self.attr_db_data, db_data)
    return getattr(self, self.attr_db_data)

有了这个我发现了:

  • 来自 ndb 的大量不必要的读取,最值得注意的是在整个序列中根本没有改变并且被访问了数百次的序列配置实体
  • 在同一请求中重复读取相同实体的某些内容
  • 最重要的:我的许多事务因 TransactionFailedError(The transaction could not be committed. Please try again.)TransactionFailedError: too much contention on these datastore entities. please try again 而失败的详细信息,这是由并行执行的许多事务中的所有这些不必要的读取造成的.我知道(或者更确切地说是怀疑)根本原因 - 请参阅相关内容 - 但有了这些详细信息,我现在可以真正努力减少它们。

对于写入端,我向通用 class 添加了一个 db_data_put() 方法(具有许多检查和跟踪支持)并替换了散布在整个应用程序中的所有 .db_data.put() 调用用它编码。这是它的样子:

def db_data_put(self, force=False):
    #assert ndb.in_transaction()  TODO: re-enable after clearing all violations

    self.db_key = self.db_data.put()

    show_trace = False
    if not ndb.in_transaction():
        logging.error('%s: db_data_put called outside transaction, readonly=%s' % (self.lid, self.readonly))
        show_trace = True
    if self.readonly:
        logging.error('%s: db_data_put called with readonly set' % self.lid)
        show_trace = True
    if force:
        if self.trace:
            logging.warn('%s: db_data_put called with force arg set' % self.lid)
    if self.cached:
        logging.error('%s: db_data_put called with cached data' % self.lid)
        show_trace = True
    if self.put_count:
        logging.warn('%s: db_data_put already called %d time(s)' % (self.lid, self.put_count))
        show_trace = True
    self.put_count += 1

    if  self.update_needed:
        self.update_needed = False
    elif not force:
        if self.trace:
            logging.warn('%s: db_data_put called without update_needed set' % self.lid)
            show_trace = True

    # noinspection PyTypeChecker
    memcache.set(self.memcache_key, self.db_data)

    # use ndb model names as strings in the list below. TODO: don't leave them on!
    show_kind = self.kind in ['']

    if show_trace or show_kind:
        self.print_stack(4)

    if self.trace or show_trace or show_kind:
        logging.debug('%s: db_data_put %s' % (self.lid, self.kind))

有了它,我发现了一些改进的空间和一些潜在的错误:

  • 在事务之外写入的实体(数据损坏的空间)
  • 响应同一请求多次写入实体(因此违反了最大 1 write/second/entity 组数据存储限制)
  • 几个 hot 请求偶尔聚集在一起非常接近彼此,从而导致争用问题,并且在编写相同的实体时,也违反了 1 write/sec限制。

在将 self.readonly 标志设置为所有序列配置对象和一些更经常不必要地读取的对象(即使在事务内部也启用缓存)后,序列化 hot 写入请求并修复了最发现严重错误我重新尝试了清洁测量测试:

  • 整个序列的执行时间下降到大约 13 分钟,这可能是因为序列化了 hot 个事务,这有助于减少序列的 "noise" 以及逻辑状态转换和推送队列任务的总数
  • 与数据争用相关的事务失败的数量下降了约 60% - 我将这大部分归因于我的实体的私有缓存,预计在事务期间不会更改(ndb 不知道实体是否在事务中访问最终将被写入或不写入)和序列化 hot 个事务
  • 云数据存储实体写入下降到 0.055 万次操作 - 可能是由于上面提到的序列状态转换较少
  • Cloud Datastore 读取操作下降到 010 万次操作 - 我的缓存在交易期间提供帮助

写入到读取的速率实际上增加了,这推翻了我认为它们应该具有可比性的假设——在这种情况下,大多数序列 activity 发生在事务中这一事实很重要。我想我的假设只在非事务上下文中有效。