了解大规模实时查询
阅读本文档,了解如何将您的无服务器应用规模扩容到可支持每秒执行数千个操作,或是容纳数十万名并发用户。本文档包含一些高级主题,可帮助您深入了解该系统。如果您刚刚开始使用 Firestore,请参阅快速入门指南。
Firestore 和 Firebase 移动版/Web 版 SDK 提供了一个功能强大的模型,用以开发无服务器应用,其中客户端代码会直接访问数据库。这些 SDK 可让客户端实时监听数据更新。您可以利用实时更新构建响应迅速的应用,且不需要拥有服务器基础架构。虽然要启动并运行这样的应用是很容易的事,不过若能同时了解 Firestore 所涉及的一些系统的限制条件,会有助于您的无服务器应用在流量增加时相应扩容并保持良好性能。
如需有关应用扩缩方面的建议,请参阅以下部分。
选择靠近用户的数据库位置
下图展示了实时应用的架构:
当用户设备上运行的应用(移动版或 Web 版)与 Firestore 建立连接时,该连接会被路由到您的数据库所在区域中的 Firestore 前端服务器。例如,如果您的数据库位于 us-east1
,则该连接也会被路由到同样位于 us-east1
中的 Firestore 前端。这些连接会长期存在并保持打开状态,直到应用明确关闭它们为止。此前端会从底层 Firestore 存储系统读取数据。
用户的实际位置与 Firestore 数据库位置之间的距离会影响用户端的延迟表现。例如,印度的用户与位于 Google Cloud 北美区域的数据库通信时,通信延迟会较高并且应用的流畅度也会受影响;但如果数据库位于更近的位置(如印度境内或亚洲的其他国家/地区),这些问题都会有所改善。
实现确保可靠性的设计
以下部分介绍了可提高或会影响应用可靠性的因素:
启用离线模式
Firebase SDK 支持离线数据持久化。如果用户设备上的应用无法连接到 Firestore,用户仍可通过在本地缓存的数据使用该应用。这样可以确保即使用户网络连接不稳定,或者几小时或几天内完全没有网络连接,也仍然能够访问数据。如需详细了解离线模式,请参阅启用离线数据。
了解自动重试机制
Firebase SDK 提供了一个机制来重试操作和重新建立断开的连接。这有助于解决因重启服务器或因客户端与数据库之间的网络问题而导致的暂时性错误。
单区域位置和多区域位置
在选择是使用单区域位置还是多区域位置时,有一些需要权衡的因素。这两种方案的主要区别在于数据的复制方式;而数据的复制方式又决定了应用可用性方面的保证。多区域实例拥有更高的服务可靠性和数据耐用性,但弊端是费用更高。
了解实时查询系统
实时查询(也称为快照监听器)可让应用监听数据库中的更改,并在数据更改后立即收到低延迟通知。虽然应用可以通过定期轮询数据库更新来实现相同的效果,但定期轮询速度通常更慢、费用也更高,并且需要使用更多代码。有关如何设置和使用实时查询的示例,请参阅获取实时更新。以下各部分详细介绍了快照监听器的工作原理,并介绍了一些最佳实践来帮助您了解如何扩大实时查询规模并保持良好的应用性能。
假设有两位用户通过使用某个移动 SDK 构建的即时通讯应用连接到 Firestore。
客户端 A 向数据库写入数据,在名为 chatroom
的集合中添加和更新文档:
collection chatroom:
document message1:
from: 'Sparky'
message: 'Welcome to Firestore!'
document message2:
from: 'Santa'
message: 'Presents are coming'
客户端 B 使用快照监听器监听同一集合中的更新。每当有用户创建新消息时,客户端 B 都会立即收到通知。下图展示了快照监听器背后的架构:
当客户端 B 将快照监听器连接到数据库时,会发生以下一系列事件:
- 客户端 B 会打开一个与 Firestore 之间的连接,并通过 Firebase SDK 调用
onSnapshot(collection("chatroom"))
来注册监听器。此监听器可在数小时内保持活跃状态。 - Firestore 前端会查询底层存储系统以引导数据集。该过程会加载匹配文档的完整结果集。我们将此过程称为一次轮询式查询。之后,系统会评估数据库的 Firebase 安全规则,验证用户是否有权访问这些数据。如果用户拥有访问权限,数据库会将数据返回给用户。
- 接下来,客户端 B 的查询便会转为监听模式。该监听器会注册一个订阅处理程序并等待数据更新。
- 客户端 A 现在发送一个写入操作来修改文档。
- 数据库会将文档更改提交到其存储系统。
- 该系统会以事务方式向内部更新日志提交相同的更新内容。该更新日志则会按照更改发生的顺序确立严格的更改排序。
- 更新日志继而会将更新后的数据扇出到订阅处理程序池。
- 系统会执行一个反向查询匹配函数,以查看更新后的文档是否与任何当前注册的快照监听器相匹配。在此示例中,文档与客户端 B 的快照监听器相匹配。顾名思义,您可以将反向查询匹配函数视为反向执行的普通数据库查询。该过程不是在文档中搜索与查询匹配的那些文档,而是在查询中搜索与传入文档匹配的那些查询,这种方式更为高效。找到匹配项之后,系统会将相关文档转发给快照监听器。然后,系统会评估数据库的 Firebase 安全规则,确保只有拥有权限的用户才会收到数据。
- 系统会将文档更新转发到客户端 B 设备上的 SDK,这继而会触发
onSnapshot
回调。如果启用了本地持久化,SDK 还会将更新应用于本地缓存。
Firestore 可伸缩性的一个关键因素在于从更新日志到订阅处理程序及前端服务器的扇出过程。通过扇出,单项数据更改可以高效地传播到数百万实时查询和关联用户。通过在多个可用区(如果是多区域部署,则为多个区域)运行所有这些组件的多个副本,Firestore 可实现高可用性和可伸缩性。
值得注意的是,通过移动版和 Web 版 SDK 发出的所有读取操作都遵循上述模型;即都会先执行一个轮询式查询,然后进入监听模式,以实现一致性保证。该模型也适用于实时监听器、文档检索调用和一次性查询。您可以将单次文档检索和一次性查询视为短期有效的快照监听器,因此在性能方面也有着类似的限制。
运用最佳实践来扩缩实时查询
运用以下最佳实践来设计可扩缩的实时查询。
了解系统中的高写入流量
本部分将帮助您了解系统如何应对写入请求渐增的情况。
在 Firestore 中,实时查询是通过更新日志触发的,随着写入流量增加,更新日志会自动横向扩容。如果数据库的写入速率超出了单个服务器可以处理的上限,系统便会将更新日志拆分到多个服务器上进行处理,之后系统在处理查询时会利用多个订阅处理程序中的数据,而不是使用单一数据源。从客户端和 SDK 的角度来看,这一过程是透明的,在系统拆分更新日志时,应用端无需执行任何操作。下图演示了实时查询的扩缩方式:
利用自动扩缩,您的写入流量可以无限增加,但在流量渐增的过程中,系统可能需要一些时间才能做出响应。请遵循 5-5-5 规则中的建议操作,以避免产生写入热点。Key Visualizer 是一种用于分析写入热点的实用工具。
许多应用都有可预测的自然增长,对于此类增长,即使不准备任何预案,Firestore 也能应对自如。不过,一些批量工作负载(例如导入大型数据集)可能会导致写入激增。在设计应用时,请留意写入流量的来源。
了解写入和读取如何交互
您可以将实时查询系统视为连接写入操作与读取程序的流水线。每当创建、更新或删除文档时,相应更改都会从存储系统传播到当前注册的监听器。Firestore 的更新日志结构可保证强一致性,这意味着您的应用始终按数据库提交数据更改的顺序接收相应的更新通知。这可避免发生数据一致性相关的极端情况,从而简化了应用开发。
鉴于此流水线的连接作用,如果有写入操作引发了热点或锁争用问题,那么读取操作可能也会受到不良影响。当写入操作失败或出现节流限制时,读取操作可能亦会停止,以等待通过更新日志获取一致的数据。如果您的应用中发生这种情况,您可能会发现写入速度缓慢,并且相关查询的响应速度也较慢。避免热点是解决此问题的关键。
尽量减少文档数量和写入操作次数
如果您构建的应用包含快照监听器,您通常会希望用户能够快速了解数据的更改情况。为了实现这个目标,请尽量减小系统需处理的内容量。系统可以非常快地推送包含数十个字段的小型文档。对于包含数百个字段和大量数据的大型文档,则需要更长的时间进行处理。
同样,建议执行耗时较短的提交操作和写入操作,以确保低延迟。虽然从写入操作的角度来看,大批量写入可能会提高吞吐量,但实际上可能会减慢快照监听器发出通知的速度。这似乎不合常理,因为其他数据库系统通常是通过批处理来提高性能。
使用高效的监听器
随着数据库写入速率的提高,Firestore 会将数据拆分到多个服务器中进行处理。Firestore 的分片算法会尽量将来自同一集合或集合组的数据放置在同一个更新日志服务器上进行处理。此外,系统还会尽量提高写入吞吐量,同时尽可能减少处理查询所用的服务器数量。
但是,某些模式仍可能会导致快照监听器的表现不佳。例如,如果您的应用将大部分数据存储在一个大型集合中,则监听器可能需要连接到许多服务器才能接收所需的全部数据。即使您应用了查询过滤条件,情况也是如此。此外,连接到许多服务器也可能会导致响应速度变慢。
为避免响应速度变慢,您在构建架构和应用时,应避免将监听器设计为从许多不同的服务器获取数据。最好将数据拆分为较小的集合,这样写入速率也会较低。
考虑一个类似的情况,假定一个关系型数据库中有一些需要全表扫描的高性能查询。在关系型数据库中,一个需要全表扫描的查询就类似于一个需要监控高流失率集合的快照监听器。与数据库可以使用更具体的索引提供数据的查询相比,此查询的速度可能会较慢。 一个利用更具体的索引的查询就类似于一个监控更改频率较低的单个文档或集合的快照监听器。您应对应用进行负载测试,以充分了解自己用例的行为和需求。
确保轮询式查询快速高效
若要让实时查询能够快速响应,另一个关键因素是需确保用于引导数据的轮询式查询快速高效。一个新的快照监听器在首次连接时,必须加载整个结果集并将其发送到用户的设备。如果查询速度缓慢,应用的响应速度也会降低。反例包括尝试读取许多文档的查询或未使用适当索引的查询。
在某些情况下,监听器还可能会从监听状态回退至轮询状态。这个过程是自动进行的,并且从 SDK 和应用的角度来看,该过程是透明的。以下情况可能会触发轮询状态:
- 由于负载发生变化,系统重新分配更新日志的数据以实现负载均衡。
- 热点导致数据库写入失败或延迟。
- 瞬时服务器重启暂时影响监听器。
如果您的轮询式查询速度足够快,那么从应用用户的角度来看,轮询状态的阶段是透明的。
建议让监听器保持长期有效
在构建使用 Firestore 的应用时,通常最经济有效的方式是打开监听器后让它们在尽可能长的时间里保持有效状态。使用 Firestore 时,您需要为返回到您应用的文档支付相应费用,而不必为保持连接处于打开状态而付费。长期有效的快照监听器在其保持有效期间只会读取处理查询所需的数据。这包括初始轮询操作,以及后续在数据实际发生更改时发出通知。另一方面,一次性查询可能会重新读取自应用上次执行查询以来没有更改的数据。
如果您的应用必须以较高的速率来使用数据,快照监听器可能就不太合适。例如,如果您的用例通过一个长期有效的连接每秒推送大量文档,则最好选择使用以较低频率运行的一次性查询。