最佳做法
在构建使用 Firestore 的应用时,您可以快速参考本页面中列出的最佳做法。
数据库位置
创建数据库实例时,请选择距离您的用户和计算资源最近的数据库位置。 远距离网络跃点更容易出错,并会增加查询延迟时间。
如需最大限度地提高应用的可用性和耐用性,请选择一个多区域位置,并将关键计算资源安排在至少两个区域中。
如果您的应用对延迟时间比较敏感,或者您希望与其他 GCP 资源托管在同一位置,请选择一个单区域位置,以便降低费用并缩短写入延迟。
文档 ID
- 不要使用
.
和..
作为文档 ID。 - 避免在文档 ID 中使用正斜杠
/
。 不要使用单调递增的文档 ID,例如:
Customer1
,Customer2
,Customer3
, ...Product 1
、Product 2
、Product 3
…
此类序列 ID 可能会产生热点问题 (hotspot),导致增加延迟时间。
字段名称
避免在字段名称中使用以下字符,因为它们需要额外的转义:
- 句点
.
- 左中括号
[
- 右中括号
]
- 星号
*
- 反引号
`
- 句点
索引
缩短写入延迟时间
导致写入延迟的主要因素是索引扇出。减少索引扇出的最佳实践如下:
索引例外项
对于大多数应用,您可以依靠自动编入索引以及错误消息链接来管理索引。但是,在以下情况下,您可能需要添加单字段例外项:
场景 | 说明 |
---|---|
大型字符串字段 | 如果您的字符串字段通常包含不用于查询的长字符串值,您可以选择不将该字段编入索引来降低存储费用。 |
向所含文档具有依序值的集合进行高速率写入 | 如果您将在某个集合中的各文档之间依序递增或递减的字段(如时间戳)编入索引,则向该集合写入数据的最大速率为每秒 500 次写入。如果您不根据具有序列值的字段进行查询,可以选择不将该字段编入索引来绕过此限制。 例如,在具有高写入速率的 IoT 使用场景中,一个所含文档具有时间戳字段的集合可能会达到每秒 500 次的写入限制。 |
TTL 字段 |
请注意,如果您使用 TTL(存留时间)政策,则 TTL 字段必须是一个时间戳。默认情况下,系统会将 TTL 字段编入索引,这可能会在流量传输速率较高时影响性能。最佳做法是为 TTL 字段添加单字段例外项。 |
大型数组或映射字段 | 大型数组或映射字段可能会达到每个文档 40,000 个索引条目的限制。如果您的查询不是基于大型数组或映射字段,建议您不要将该数组或字段编入索引。 |
读写操作
应用更新单个文档的最大速率在很大程度上取决于工作负载。如需了解详情,请参阅单个文档的更新。
如果可以,请使用异步调用,而非同步调用。 异步调用可将延迟时间的影响降到最低。例如,假设某个应用需要文档查找的结果以及某个查询的结果才能给出响应,而该查找和该查询没有数据依赖关系,则启动查询前无需同步等待该查找完成。
请勿使用偏移。应改为使用游标。使用偏移仅会避免将跳过的文档返回到应用,但仍会在内部检索这些文档。跳过的文档会影响查询的延迟时间,并且由于检索文档需要执行读取操作,您的应用会因此而产生费用。
事务重试机制
Firestore SDK 和客户端库会自动重试失败的事务来处理暂时性错误。如果您的应用直接通过 REST 或 RPC API(而非通过 SDK)访问 Firestore,则您的应用应实施事务重试机制,以便提高可靠性。
实时更新
如需了解与实时更新相关的最佳实践,请参阅了解大规模实时查询。
规模化设计
以下最佳实践介绍了如何避免产生争用问题的情况。
单个文档的更新
在设计应用时,请考虑应用更新单个文档的速度。表征工作负载性能的最佳方式是执行负载测试。应用更新单个文档的最大速率在很大程度上取决于工作负载。涉及的因素包括写入速率、请求之间的争用情况以及受影响索引的数量。
文档写入操作会更新文档以及任何关联的索引,并且 Firestore 会将写入操作同步应用于大批数量的副本。当写入速率足够高时,数据库将开始遇到争用、更长的延迟时间或其他错误。
窄文档范围的高读取、写入和删除速率
避免字典顺序上相近的文档具有较高的读取或写入速率,否则您的应用将会发生争用错误。此问题称为热点问题。如果您的应用执行以下任何操作,就可能会出现热点问题:
以极高速率创建新文档,并且分配各自的单调递增 ID。
Firestore 使用分散算法分配文档 ID。如果使用自动文档 ID 创建新文档,则不会在写入时遇到热点问题。
在包含极少文档的集合中以高速率创建新文档。
以极高速率创建包含单调递增的字段(如时间戳)的新文档。
以高速率删除集合中的文档。
以极高速率对数据库执行写入操作,而不是逐渐增加流量。
避免跳过已删除的数据
避免运行跳过最近删除的数据的查询。如果初步查询结果最近被删除,则查询可能必须跳过大量索引条目。
一个可能需要跳过大量已删除数据的工作负载的例子,是尝试查找最早加入队列的工作项的查询。查询可能如下所示:
docs = db.collection('WorkItems').order_by('created').limit(100)
delete_batch = db.batch()
for doc in docs.stream():
finish_work(doc)
delete_batch.delete(doc.reference)
delete_batch.commit()
每当此查询运行时,都会扫描索引条目以在最近删除的文档中查找 created
字段。这会减慢查询速度。
为了提升性能,请使用 start_at
方法找到最佳起点。例如:
completed_items = db.collection('CompletionStats').document('all stats').get()
docs = db.collection('WorkItems').start_at(
{'created': completed_items.get('last_completed')}).order_by(
'created').limit(100)
delete_batch = db.batch()
last_completed = None
for doc in docs.stream():
finish_work(doc)
delete_batch.delete(doc.reference)
last_completed = doc.get('created')
if last_completed:
delete_batch.update(completed_items.reference,
{'last_completed': last_completed})
delete_batch.commit()
注意:上面的示例使用单调递增的字段,该字段是高写入速率的反模式。
增加流量
您应该循序渐进地增加新的集合或字典顺序上相近的文档的流量,以确保 Firestore 在流量增加时有足够的时间准备文档。对于一个新的集合,我们建议最开始每秒最多执行 500 次操作,然后每 5 分钟增加 50% 的流量。同样地,您可以循序渐进地增加写入流量,但请牢记 Firestore 标准限额。请确保操作在整个键范围内分布相对均匀。这就是所谓的“500/50/5”规则。
将流量迁移到新集合
如果您在集合之间迁移应用流量,则渐增模式尤为重要。处理此迁移的一种简单方法是从旧集合中读取;如果文档不存在,则从新集合中读取。但是,这可能会导致新集合中字典顺序上相近的文档的流量突然增加。在流量增加时,Firestore 可能无法高效地准备新集合,尤其是在新集合中包含很少文档的情况下。
如果您更改同一集合中的许多文档的文档 ID,就可能会出现类似的问题。
将流量迁移到新集合的最佳策略取决于数据模型。以下是一个名为“并行读取”的示例策略。您需要确定此策略对您的数据是否有效,并且一个重要的考虑因素是迁移过程中并行操作的费用影响。
并行读取
若要在将流量迁移到新集合时实现并行读取,请首先从旧集合中读取。如果文档不存在,再从新集合中读取。以高速率读取不存在的文档可能会导致热点问题,因此请务必循序渐进地增加新集合的负载。一种较佳的策略是将旧文档复制到新集合,然后删除旧文档。循序渐进地增加并行读取操作,以确保 Firestore 可以处理新集合的流量。
循序渐进地增加对新集合的读取或写入操作的一种可行策略是,使用用户 ID 的确定性哈希来选择尝试写入新文档的用户的随机百分比。请确保用户 ID 哈希的结果不会因函数或用户行为而有所偏差。
同时,运行一个批量作业,将所有数据从旧文档复制到新集合中。为防止出现热点问题,您的批量作业应避免对顺序文档 ID 执行写入操作。批量作业完成后,您可以只从新集合中读取。
可以将此策略改进为一次迁移一小批用户。 向用户文档添加一个字段,用于跟踪该用户的迁移状态。 您可以根据用户 ID 的哈希选择一批用户进行迁移。使用批量作业为该批用户迁移文档,并对处于迁移过程中的用户使用并行读取。
请注意,除非在迁移阶段对旧实体和新实体进行双重写入,否则无法轻松回滚。这会增加 Firestore 产生的费用。
隐私权
- 避免在 Cloud 项目 ID 中存储敏感信息。Cloud 项目 ID 的保留期可能比项目的生命周期更长。
- 根据数据合规性最佳实践,我们建议不要在文档名称和文档字段名称中存储敏感信息。