Cloud Datastore 最佳做法

此处所列的最佳做法可作为您在构建使用 Datastore 的应用时的快速参考。如果您刚开始使用 Datastore,建议您不要从本页面开始学习,因为其中并未介绍有关如何使用 Datastore 的基础知识。如果您是新用户,我们建议您从 Datastore 使用入门开始学习。

常规

  • 对于命名空间名称、种类名称、属性名称和自定义键名,请始终使用 UTF-8 字符。如果这些名称中使用了非 UTF-8 字符,则 Datastore 功能可能受到影响。例如,属性名称中的非 UTF-8 字符可能会阻止创建使用该属性的索引。
  • 请勿在种类名称或自定义键名中使用正斜线 (/)。这些名称中的正斜线可能会影响未来的功能。
  • 避免在 Cloud 项目 ID 中存储敏感信息。Cloud 项目 ID 的保留期可能比项目的生命周期更长。
  • 根据数据合规性最佳实践,我们建议不要在 Datastore 实体名称或实体属性名称中存储敏感信息。

API 调用

  • 请对读取、写入和删除操作使用批量操作,而不要使用单个操作。批量操作效率更高,因为它们执行多个操作的开销与单个操作相同。
  • 如果事务失败,请务必尝试回滚事务。如果一个事务中存在不同请求争用相同资源的情况,回滚可以最大限度地减少重试的延迟时间。请注意,回滚本身可能会失败,因此回滚应仅作为尽力而为的一次尝试。
  • 如果可以,请使用异步调用,而非同步调用。 异步调用可将延迟时间的影响降到最低。例如,假设某个应用需要同步 lookup() 的结果以及某个查询的结果才能提供响应,如果 lookup() 和查询没有数据依赖关系,则启动查询前无需同步等待 lookup() 完成。

实体

  • 请将实体组中高度相关的数据划分为一组。实体组支持祖先查询,此类查询可返回强一致的结果。由于实体组中的各实体存储在 Datastore 服务器上物理邻近的位置,因此祖先查询还能以最小的 I/O 快速扫描实体组。
  • 避免写入实体组的速率超过每秒一次。如果以持续高于该限制的速率写入,会更加延后最终一致性的读取,导致高度一致性读取超时,并降低应用的整体性能。在此限制下,向实体组的一次批量写入或事务写入仅计为单次写入。
  • 不要在一次提交中多次包含相同的实体(按照键判断)。在同一次提交中多次包含相同的实体会影响 Datastore 延迟时间。

  • 如果在创建实体时未提供键名,则系统会自动生成键名。系统在分配键名时,会使其均匀分布在键空间中。
  • 对于使用自定义名称的键,请始终采用 UTF-8 字符,但不得包含正斜线 (/)。非 UTF-8 字符会影响多个流程,例如将 Datastore 备份导入到 Google BigQuery 的流程。正斜线可能会影响未来的功能。
  • 对于使用数字 ID 的键:
    • 请勿使用负数作为 ID。负数 ID 可能会影响排序。
    • 请勿使用值 0(零)作为 ID。如果使用该值,您将获得一个自动分配的 ID。
    • 如果您希望手动将自己的数字 ID 分配给您创建的实体,请通过 allocateIds() 方法让应用获取 ID 块。这样可以阻止 Datastore 将您的一个手动数字 ID 分配给另一个实体。
  • 如果您将自己的手动数字 ID 或自定义名称分配给您创建的实体,请勿使用单调递增的值,例如:

    1, 2, 3, ,
    "Customer1", "Customer2", "Customer3", .
    "Product 1", "Product 2", "Product 3", .
    

    如果应用生成大量流量,这类顺序编号可能会造成热点,进而影响 Datastore 的延迟时间。为避免顺序数字 ID 出现这一问题,请通过 allocateIds() 方法获取数字 ID。allocateIds() 方法会生成一系列恰当分布的数字 ID。

  • 通过指定键或存储生成的名称,您可以在之后对该实体执行一致的 lookup(),而且无需发出查询命令来查找实体。

索引

  • 如果查询永远都不需要某个属性,请将该属性从索引中排除。将不必要的属性编入索引可能会导致实现一致性的延迟时间增加,并增加索引条目的存储费用
  • 避免使用过多复合索引。过度使用复合索引可能会导致实现一致性的延迟时间增加,并增加索引条目的存储费用。如果您在无预先定义的索引的情况下,需要对大数据集执行临时查询,请使用 Google BigQuery
  • 请勿将具有单调递增值(例如 NOW() 时间戳)的属性编入索引。维护此类索引可能会造成热点,对于读写速率较高的应用会影响 Datastore 的延迟时间。如需获取有关处理单调属性的详细指南,请参阅下文中窄键范围的高读取/写入速率

属性

  • 对于字符串类型的属性,请始终使用 UTF-8 字符。字符串类型的属性中的非 UTF-8 字符可能会干扰查询。如果您需要保存含有非 UTF-8 字符的数据,请使用字节字符串
  • 请勿在属性名称中使用点号。属性名称中的点号会影响将嵌入式实体属性编入索引。

查询

  • 如果只需要访问查询结果中的键,请使用仅限于键的查询。仅限于键的查询返回结果所用的成本比检索整个实体要低,且延迟时间更短。
  • 如果只需要访问实体中的特定属性,请使用投影查询。投影查询返回结果所用的成本比检索整个实体要低,且延迟时间更短。
  • 同样,如果只需要访问查询过滤条件中包含的属性(例如 order by 子句中列出的属性),请使用投影查询
  • 请勿使用偏移。应改为使用游标。使用偏移仅会避免将跳过的实体返回到应用,但仍会在内部检索这些实体。跳过的实体会影响查询的延迟时间,并且由于检索实体需要读取操作,您的应用会因此而产生费用。
  • 如果需要查询具有强一致性,请使用祖先查询。(如需使用祖先查询,首先需要设计数据结构以获得强一致性)。祖先查询会返回强一致的结果。请注意,后跟 lookup()仅限于键的非祖先查询不会返回强一致的结果,因为仅限于键的非祖先查询可能会从查询时不一致的索引中获取结果。

可扩缩式设计

单个实体组的更新

Datastore 中的单个实体组的更新速率不应过快。

如果您使用的是 Datastore,Google 建议您在设计应用时,确保应用无需以超过每秒一次的速率更新实体组。请记住,不含父级和子级实体的实体自身就是实体组。如果实体组更新速率过快,则 Datastore 写入操作会出现更长的延迟时间、超时和其他类型的错误。这称为“争用”。

由于 Datastore 向单个实体组写入的速率有时可能会超过每秒一次的限制,所以负载测试可能不会显示此问题。

窄键范围的高读取/写入速率

避免字典顺序上相近的 Datastore 键具有较高的读取或写入速率。

Datastore 以 Google 的 NoSQL 数据库 Bigtable 为基础构建,受 Bigtable 的性能特征影响。Bigtable 通过将行分割到不同的片上实现缩放,并且这些行按键的字典顺序排列。

使用 Datastore 时,如果小范围键的写入速率突然增加,超过了单片服务器的容量,则写入速率可能会因为热片而变得很慢。Bigtable 最终将分割键空间以支持高负载。

除非从单个键以高速率读取数据,否则读取限制通常远高于写入限制。Bigtable 无法将单个键拆分到多个片上。

热片可应用于实体键和索引所使用的键范围。

在某些情况下,比起阻止读取或写入小范围键,Datastore 热点问题对应用产生的影响可能更广。例如,实例启动期间可能读取或写入热键,从而导致加载请求失败。

默认情况下,Datastore 使用分散算法分配键。因此,如果您使用默认 ID 分配政策以高写入速率创建新实体,则 Datastore 写入通常不会出现热点问题。在以下极端情况下可能会遇到此问题:

  • 如果使用旧版顺序 ID 分配政策以非常高的速率创建新实体。

  • 如果以非常高的速率创建新实体,并且要分配自定义单调递增 ID。

  • 如果以非常高的速率为之前现有实体极少的种类创建新实体。Bigtable 将先把所有实体创建在同一个片服务器上,然后在一段时间内将键的范围拆分到分别的片服务器上。

  • 如果以高速率创建包含单调递增的索引属性(例如时间戳)的新实体,也会遇到此问题,因为这些属性是 Bigtable 中索引表中各行的键。

  • Datastore 将命名空间和根实体组的种类前置到 Bigtable 行键。如果在未逐渐增加流量的情况下对新命名空间或种类开始执行写入操作,则可能会遇到热点问题。

如果您拥有单调递增的键或索引属性,则可以前置一个随机哈希,确保将键拆分到多个片中。

同样,如果您需要查询单调递增(或递减)的 属性,则可以改为将新属性编入索引, 请在单调值前面加上一个在数据集中具有高基数但 与您要执行的查询范围内的所有实体共有。 例如,如果您想按时间戳查询条目,但只需要 一次只能返回单个用户的结果,则可以在时间戳前添加 用户 ID,并将该新属性编入索引。虽然这仍然允许返回该用户的查询和排序结果,但用户 ID 的存在将确保该索引本身得到合理分片。

如需了解该问题的详细说明,请参阅 Ikai Lan 发表的有关在 Datastore 中保存单调递增值的博文

循序渐进地增加流量

采用循序渐进的方式来增加新的 Datastore 种类或键空间部分的流量。

您应该循序渐进地增加新 Datastore 种类的流量,以确保 Bigtable 在流量增加时有足够的时间拆分片。对于一个新的 Datastore 种类,我们建议最初每秒最多执行 500 次操作,然后每 5 分钟增加 50% 的流量。从理论上讲,使用这种渐增方案,90 分钟后可增加到每秒 74 万次操作。请确保写入操作在整个键范围内分布相对均匀。我们的 SRE 将这种做法称为“500/50/5”规则。

如果您通过更改代码来停止使用种类 A,改为使用种类 B,此渐增模式尤为重要。如需处理此迁移,一种简单的方式是更改代码以读取种类 B,如果种类 B 不存在,则读取种类 A。但是,这可能会导致具有极小部分键空间的新种类流量突增。如果键空间稀疏,Bigtable 可能无法高效地拆分片。

如果您迁移实体以使用同一种类中其他范围的键,也会出现相同的问题。

用于将实体迁移到新种类或键的策略取决于数据模型。以下是一个名为“并行读取”的示例策略。您需要确定此策略对您的数据是否有效。一个重要的考虑因素是迁移过程中并行操作的成本影响。

先从旧实体或键中读取。如果不存在,则可以从新实体或键中读取。以高速率读取不存在的实体可能导致热点问题,因此请务必循序渐进地增加负载。一种较佳的策略是将旧实体复制到新实体,然后删除旧实体。循序渐进地增加并行读取操作可确保新的键空间得到恰当拆分。

循序渐进地增加新种类的读取或写入操作的一种可行策略是,使用用户 ID 的确定性哈希获取写入新实体的用户的随机百分比。请确保用户 ID 哈希的结果不会因随机函数或用户行为而有所偏差。

同时,运行一个 Dataflow 作业,将您的所有数据从旧实体或键复制到新实体或键中。为防止 Bigtable 出现热点问题,您的批量作业应避免对顺序键执行写入操作。批量作业完成后,您只能从新位置读取。

可以将此策略改进为一次迁移一小批用户。向用户实体添加一个字段,用于跟踪该用户的迁移状态。您可以根据用户 ID 的哈希选择一批用户进行迁移。Mapreduce 或 Dataflow 作业将为该批用户迁移键。正在进行迁移的用户将使用并行读取。

请注意,除非在迁移阶段对旧实体和新实体进行双重写入,否则无法轻松回滚。这会增加 Datastore 的费用。

删除

避免在小范围键中删除大量 Datastore 实体。

为了提高读写操作的效率,Bigtable 会定期重写其表,以便移除已删除的条目并重新组织数据。这个过程称为压缩。

如果您在小范围键中删除了大量 Datastore 实体,则在该索引部分进行的查询将会变慢,直到压缩完成。在极端情况下,查询可能会在返回结果前超时。

对索引字段使用时间戳值来表示实体的过期时间是一种反模式。为了检索过期实体,您需要针对此索引字段进行查询。此索引字段可能位于键空间与最近所删除实体的索引条目的重叠部分。

您可以使用“分片查询”提高性能,这会在过期时间戳之前添加固定长度的字符串。索引根据完整的字符串排序,因此时间戳相同的实体将位于该索引的整个键范围内。您可以并行运行多个查询,从每个分片获取结果。

对于过期时间戳问题,一个更完整的解决方案是使用“世代号”,这是一种定期更新的全局计数器。系统会在过期时间戳之前添加世代号,以便依次按世代号、分片和时间戳对查询排序。删除旧实体的操作发生在上一世代。未删除的任何实体的世代号应增加。删除完成后,即进入到下一世代。压缩完成之前,针对旧世代的查询性能会很差。为降低因最终一致性而导致结果缺失的风险,您可能需要等待到若干世代完成后,方可查询该索引以获取要删除的实体列表。

分片和复制

对 Datastore 热键使用分片或复制。

如果需要以高于 Bigtable 允许的速率读取部分键范围,您可以使用复制。使用这种策略,可以为同一个实体存储 N 个副本,将单个实体支持的读取速率提高 N 倍。

如果需要以高于 Bigtable 允许的速率写入到部分键范围,则可以使用分片。分片将实体分解为更小的片段。

分片时的一些常见错误包括:

  • 使用时间前缀进行分片。当时间滚动到下一个前缀时,新的未拆分部分会出现热点问题。您应该将写入分多个部分逐渐滚动到新的前缀。

  • 仅对最热的实体进行分片。如果对所有实体的一小部分进行分片,则热实体之间可能没有足够行数来确保它们位于不同的拆分中。

后续步骤