最佳做法

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

常规

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

API 调用

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

实体

  • 不要在一次提交中多次包含相同的实体(按照键判断)。在一次提交中多次包含相同的实体会影响延迟时间。

  • 请参阅有关实体更新的部分。

  • 如果在创建实体时未提供键名,则系统会自动生成键名。系统在分配键名时,会使其均匀分布在键空间中。
  • 对于使用自定义名称的键,请始终采用 UTF-8 字符,但不得包含正斜线 (/)。非 UTF-8 字符会影响多个流程,例如将 Datastore 模式导出文件导入到 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(),而且无需发出查询命令来查找实体。

索引

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

属性

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

查询

  • 如果只需要访问查询结果中的键,请使用仅限于键的查询。仅限于键的查询返回结果所用的成本比检索整个实体要低,且延迟时间更短。
  • 如果只需要访问实体中的特定属性,请使用投影查询。投影查询返回结果所用的成本比检索整个实体要低,且延迟时间更短。
  • 同样,如果只需要访问查询过滤条件中包含的属性(例如 order by 子句中列出的属性),请使用投影查询
  • 请勿使用偏移。应改为使用cursors。使用偏移仅会避免将跳过的实体返回到应用,但仍会在内部检索这些实体。跳过的实体会影响查询的延迟时间,并且由于检索实体需要读取操作,您的应用会因此而产生费用。

规模化设计

以下最佳实践介绍了如何避免产生争用问题的情况。

实体的更新

在设计应用时,请考虑应用更新单个实体的速度。表征工作负载性能的最佳方式是执行负载测试。应用更新单个实体的最大速率在很大程度上取决于工作负载。相关因素包括写入速率、请求之间的争用情况以及受影响索引的数量。

实体写入操作会更新该实体以及任何关联的索引,并且 Datastore 模式 Firestore 会将写入操作同步应用于一定数量的副本。当写入速率足够高时,数据库将开始遇到争用、更长的延迟时间或其他错误。

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

避免字典顺序上相近的文档具有较高的读取或写入速率,否则您的应用将会发生争用错误。此问题称为热点问题。如果您的应用执行以下任何操作,它就可能会出现热点问题:

  • 以极高速率创建新实体,并且分配自己的单调递增 ID。

    Datastore 模式使用分散算法分配键。如果使用自动实体 ID 分配创建新实体,则不会在写入时遇到热点问题。

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

  • 以高速率为拥有极少实体的种类创建新实体。

  • 以极高速率创建包含已编入索引且单调递增的属性值(如时间戳)的新实体。

  • 以高速率删除种类中的实体。

  • 以极高速率对数据库执行写入操作,而不是逐渐增加流量。

如果小范围键的写入速率突然增加,则由于热点,写入速度可能会变慢。Datastore 模式最终将分割键空间以支持高负载。

除非从单个键以高速率读取数据,否则读取限制通常远高于写入限制。

热点问题可发生在实体键和索引的键范围。

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

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

同样,如果需要通过排序或过滤对一个单调递增(或递减)属性执行查询,您可以在该属性的单调值前加上一个特点如下的值:在数据集中具有高基数,但对于您要执行的查询范围内的所有实体却很常见。然后,改为将该新属性编入索引。例如,如果想要通过时间戳查询条目,但每次只需要返回单个用户的结果,您可以在时间戳前加上用户 ID,然后改为将该新属性编入索引。这仍然允许返回该用户的查询和排序结果,但用户 ID 的存在将确保该索引本身得到合理分片。

循序渐进地增加流量

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

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

如果您通过更改代码来停止使用种类 A,改为使用种类 B,此渐增模式尤为重要。如需处理此迁移,一种简单的方式是更改代码以读取种类 B,如果种类 B 不存在,则读取种类 A。但是,这可能会导致只有非常小一部分键空间的新种类流量突然增加。

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

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

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

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

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

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

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

删除

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

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

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

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

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

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

分片和复制

使用分片或复制来处理热点问题。

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

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

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

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

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

后续步骤