亚马逊AWS官方博客

浅谈数据库连接池优化之 Amazon DocumentDB

本文主要介绍后端程序开发过程中,有关 Amazon DocumentDB 连接池优化的最佳实践,希望可以帮助大家设计一个高性能、高可用的 DocumentDB 连接池。它能够在 DocumentDB 维护期间完成个位数秒级自愈,保证后端应用依然近乎无感的对外提供服务。

Amazon DocumentDB(与 MongoDB 兼容)是一个完全托管的云原生 JSON 文档数据库,它可以轻松且经济高效地处理几乎任何规模的关键文档工作负载,并且无需管理基础设施。用户以往使用 MongoDB 的后端应用可以几乎不做修改的迁移到 DocumentDB 上。

开发人员在使用数据库的时候必须充分考虑一些计划内和计划外的事件,所谓计划内的事件就是由 Amazon Web Services 或者客户的运维人员人为产生的可提前预见的日常运维事件,比如客户的运维人员按照实际的业务发展情况按需变更 DocumentDB 集群的实例硬件配置和集群实例数量,Amazon Web Services 定期地向客户推送 bugfix、安全补丁、主版本/子版本软件更新等;计划外事件就是非人为原因产生的不可提前预见的运维事件,比如底层硬件故障产生后,Amazon Web Services 会立即进行计算实例的替换来保证整个集群快速恢复到正常状态。作为后端软件开发人员,我们需要在使用数据库的时候提前考虑到各种可能的情况,尽最大的技术可能保证线上业务的连续性,无论这些事件是那种类型。

我将对 DocumentDB 集群结构进行介绍并以 Spring Data MongoDB 为例进行连接池优化效果验证。

Amazon DocumentDB 集群结构

DocumentDB 集群分为 2 种,第一种是名为 Instance Based Cluster 的一写多读结构,基本可以对应为 MongoDB 的副本集群结构;第二种是名为 Elastic cluster 的多写多读的分片结构,基本可以对应为 MongoDB 的分片集群结构。

我们在创建 DocumentDB 的时候就会被询问需要创建哪种结构的集群:

下图是一写多读的 instance based cluster 结构:

Instance Based Cluster 结构中 primary 实例是读写实例,replica 实例是只读实例。数据只能从 primary 实例写入,但是可以从 primary 实例和 replica 实例中读出,从 replica 实例读取的数据会有延迟,延迟一般为个位数毫秒,具体取决于实际的线上业务压力情况而定。

这种集群结构提供 3 种可接入的 endpoint:

  1. 集群端点(cluster endpoint):集群端点是用于 DocumentDB 集群的端点,它永远指向该集群的当前 primary 实例。每个 DocumentDB 集群都具有一个集群端点和一个 primary 实例。在 failover 情况下,集群端点重新映射到新的 primary 实例,集群端点可以同时进行数据读写操作。
  2. 读取端点(reader endpoint):读取端点是 DocumentDB 集群的一个只读端点,它指向该集群 replica 实例。每个 DocumentDB 集群都具有一个读取端点。如果有多个 replica 实例,每次客户端建立新的数据库连接时,读取端点会通过 DNS 随机解析的方式将连接请求定向到其中的一个 replica 实例上,实现读请求的负载均衡;如果集群没有 replica 实例,它就指向该集群的 primary 实例。
  3. 实例端点(instance endpoint): 实例端点是连接到特定实例的端点。集群中的每个实例(不论其是 primary 实例还是 replica 实例)都有各自唯一的实例端点。最好不要在应用程序中直接使用实例端点。这是因为它们在发生失效转移时会更改角色,从而需要在应用程序中更改代码。

下图是多写多读的 elastic cluster 结构:

Elastic cluster 结构依然是计算和存储分离结构,不过它和 MongoDB 分片集群一样做了数据分片(Shard)。对整个数据库集群的数据使用基于哈希的分片跨分布式存储系统进行了数据分区,每个分片中的数据由独立计算容量(compute capacity)负责提供读写操作。Elastic cluster 相比 Instance Based Cluster 有更好的水平扩展性,支持每秒数百万次读/写和成千万亿字节存储容量的工作负载,可以做到多点写入多点读取,能够突破后者单点写入瓶颈的问题。

这种集群结构只提供 1 种可接入的 endpoint:

  • 集群端点(cluster endpoint):集群端点指向集群的 request router,request router 可以理解为独立的连接代理(proxy),它在客户端应用程序和分片集群之间提供统一的连接接入点,客户端直接连接到 request router 即可。request router 按照分片键在将读写请求路由到具体的计算容量(compute capacity)上。

由于有 request router 屏蔽计算容量(compute capacity)的底层计算资源的变化, 因此在运维操作时 Elastic cluster 有更短的可感知时间,应用程序一般不会明显的感知或感知不到集群的维护操作,不过也不能保证绝对感知不到,例如如果 shard 的 shard instance count 为 1 时进行维护可能会感知到抖动,因为这种结构对某个 shard 而言不是高可用结构。

建议 shard instance count 大于等于 2,如下图:

无论哪种结构,DocumentDB 都是计算和存储分离的结构。MongoDB 集群实例间的数据同步是通过在实例间传输 oplog 并在每个实例上进行操作回放来实现,集群各实例都需要消耗资源进行数据的落盘,数据是最终一致性的。但是 DocumentDB 完全不同,DocumentDB 数据存储在共享的集群卷中,该卷是一个由大量 SSD 共同组成的单个抽象的虚拟卷。卷中每个数据块都由六个数据副本组成,数据块可在单个 AWS Region 的多个可用区之间自动复制,因此不需要额外消耗计算实例的硬件资源完成数据同步,这是 DocumentDB 和 MongoDB 的显著不同点。存储层数据块复制有助于确保数据具有高持久性,减少数据丢失的可能性,因为每次写入都需要 6 个副本中的至少 4 个副本完成同步才会确认提交;这个设计让集群具有更高可用性和更短的恢复时间,因为多份数据副本已存在于其他可用区中,这些数据副本可以继续向 DocumentDB 集群中的实例提供数据请求服务。相比之下 MongoDB 的某台实例故障后,新加入集群的实例需要立即进行自动全量和增量数据同步。

Amazon Document 集群连接原理

根据了解 Amazon DocumentDB 集群容错能力这篇文档,我们以 Instance Based Cluster 为分析对象对集群维护操作进行时间维度的分析。在 DocumentDB 维护期间会发生 failover,集群中原先的 primary 实例和 replica 实例会发生角色转换,旧的 primary 实例会下线并重新加入集群成为 replica 实例,新的 primary 实例会在集群内部选举后从原先的 replica 实例中产生。这里值得强调的是,强烈建议将我们的集群设计成高可用结构,即一个 instance based cluster 最好是有 1 个 primary 实例和多个 replica 实例,一个 elastic cluster 中分片(shard)的计算容量(compute capacity)最好至少有 2 个实例,一个作为 primary 实例,其余的作为 replica 实例。

经典的 DocumentDB 重连流程

下图中我们可以大致了解整个 failover 对业务稳定性的影响,DocumentDB 在维护时先进行内部实例间的选举,这会导致集群拓扑关系变化,确定新的 primary 实例后集群重新恢复到稳定状态。然后集群更新 endpoint 的 DNS 解析,将新的 primary 实例和 replica 实例的 IP 地址提供给客户端,它的 DNS TTL 时间是 5S

虽然集群内部的 failover 通常可在不到 30 秒的时间内完成,但是传统的连接方式服务通常在 120 秒内(大多数在 60 秒内)还原。这是因为除了 DocumentDB 的集群维护需要时间外,维护成功后还需要通过 endpoint DNS 更新的方式对外通知最新的集群拓扑关系,考虑到 DNS TTL 是 5S 再加上某些数据库客户端有本地 DNS 缓存的情况,客户端并不一定马上得到最新的 DNS 状态,客户端只能连接重试来逐渐恢复整个连接池中的全部连接。整体来讲这是一个维护事件发生后的被动更新流程而且是串行发生的,各个环节的等待时间较长、效率比较低,连接池基本不可能做到个位数秒级的刷新。

改进的 DocumentDB 重连流程

既然知道 DocmentDB 的维护过程中实例间拓扑关系的变化会内部协商,那么如果我们让客户端以观察者的身份来近实时的获取到新集群的拓扑关系会怎么样?答案是可以不等待 DNS 对外发布和刷新,直接在集群拓扑关系确定后马上进行连接。

下图中,对集群而言:

  1. 表示集群从 primary 实例下线到整个集群需要发起重新选举协商的时间。
  2. 表示集群开始选举并产生最终选举结论的时间。
  3. 表示集群选举后从各实例间重新进行通信到必要的 metadata 同步完成的时间。
  4. 表示集群完全同步后,从集群内部完成必要的最终检查到确认可以对外发布新拓扑关系的时间。

对客户端而言:

A. 客户端发现集群拓扑关系变动,这时客户端开始等待集群选举完成,期间客户端会密切监视集群选举情况。

B. 客户端按照集群新拓扑关系进行连接池刷新。

我们可以看到如果实现了上述的改进,那么连接池的重新恢复时间将会大幅度减少。我们可以参考 MongoDB Cluster Monitoring 来进行连接池设计,当客户端连接到的实例或集群的状态发生变化时,驱动程序会创建拓扑结构事件,也称为服务器发现和监控(SDAM)事件,例如当客户端建立新连接或集群选出新的主实例时,驱动程序会创建一个事件。此外对于 DocumentDB 和 MongoDB 而言,每个实例里面都有集群的 metadata 信息供客户端了解集群当前的拓扑关系。综合这些信息足够让客户端及时做出连接池刷新操作。

例如 DocumentDB 集群里面可以通过 rs.status() 查看全部的实例状态。

Spring Data MongoDB 连接池优化和验证

上述的分析完成后,我们以 Spring Data MongoDB 连接进行深入优化,本次测试对象为 Instance Based Cluster,集群使用了 1 写 2 读共 3 个实例。

引入必要的连接池组件

这里我直接使用大家习惯的 spring-boot-starter-data-mongodb,并引入 zstd 包对传输的数据进行压缩。

参数设置

这里我列出了必要的设置项:

spring.data.mongodb.host AWS DocumentDB Replica’s cluster endpoint 地址,详情见:Connecting to Amazon DocumentDB as a replica set
spring.data.mongodb.additional-hosts AWS DocumentDB Replica’s reader endpoint 地址
spring.data.mongodb.port AWS DocumentDB 服务端口(默认 27017)
spring.data.mongodb.username AWS DocumentDB 用户名
spring.data.mongodb.password AWS DocumentDB 密码
spring.data.mongodb.database AWS DocumentDB 数据库名称
spring.data.mongodb.replica-set-name AWS DocumentDB replica set name(AWS DocumentDB Replica 默认 rs0)
spring.data.mongodb.ssl.enabled AWS DocumentDB 是否启用 SSL
spring.ssl.bundle.pem.aws-documentdb-pem.truststore.certificate AWS DocumentDB 启用 SSL 后,客户端侧需要指定 pem 文件,这里设置为 aws-documentdb-pem
spring.data.mongodb.ssl.bundle AWS DocumentDB 启用 SSL 后,直接引用上一行指定的 aws-documentdb-pem
options.connectTimeoutMS 客户端和 AWS DocumentDB 服务器建立 TCP 连接的超时时间,单位为毫秒,默认为 10000 毫秒。详情见:MongoDB 连接字符串选项
options.socketTimeoutMS 客户端通过已建立的 TCP 连接发送请求(SQL)后等待响应超时的时间,单位为毫秒,默认为永不超时
options.compressors 开启 client-server 间的传输压缩,可以减少传输带宽要求;可以指定多种压缩算法
options.maxPoolSize 连接池中最大的连接数量,默认为 100
options.minPoolSize 连接池中最小的连接数量,默认为 0
options.maxIdleTimeMS 连接池中某个连接在被删除和关闭之前可以在池中保持空闲的最长时间,单位为毫秒
options.w 写关注设置(AWS DocumentDB 和 MongoDB 略有不同),建议设置为 majority 保证多数副本确认,这样保证数据的高可用性,详情见:Transactions in Amazon DocumentDBMongoDB w 选项
options.wtimeoutMS 写关注操作超时,wtimeoutMS 指定写关注的时间限制(以毫秒为单位),详情见:Transactions in Amazon DocumentDB 和 MongoDB w 选项
options.journal 要求 AWS DocumentDB/MongoDB 确认写入操作已写入本地日志,保证意外情况发生后可以进行事务的恢复或回滚,详情见:MongoDB w 选项
options.readConcernLevel 读关注设置(AWS DocumentDB 和 MongoDB 略有不同),建议设置为 majority,这样查询会返回已被副本集多数成员确认的数据,详情见:MongoDB readConcern 选项
options.readPreference 读取偏好选项,非常适合用于读写分离的场景,详情见:MongoDB 读取偏好选项
options.maxStalenessSeconds 读取偏好选项设置后,实例的陈旧程度(以秒为单位)容忍时间
如果写实例-只读实例间同步延迟超过了容忍值,则将该只读实例移除出备选只读实例列表,后续读取查询不再在该只读实例上就进行,详情见:MongoDB 读取偏好选项
retryWrites 写重试,DocuemrntDB 目前不支持支持,详情见:Transactions in Amazon DocumentDB 和 MongoDB 其他配置
retryReads 读重试,详情见: MongoDB 其他配置

最后设置如下图:

备注:

  1. DocumenDB 的 SSL 链路层加密其实在 Spring Data 中非常容易设置,上面的样例里面直接把 pem 文件引入即可,其实使用时没有 AWS 官方文档里面加密传输中数据描述的这么复杂。
  2. DocumentDB 的读写分离也非常简单,如果容忍写入延迟就非常建议使用 secondaryPreferred,否则建议使用 primaryPreferred。
  3. DocumentDB 的事务读写关注和 MongoDB 由于数据同步机制不一样,所以支持的级别不一样,建议读关注和写关注为 majority 即可。
  4. 客户端和数据库间的传输压缩功能不是必须的,但是建议开启。只需要牺牲一点 CPU 计算资源就可以提升数据传输效率,减少网络带宽开销, 降低网络传输成本。
  5. Spring Data MongoDB 本身没有直接暴露很精细的连接池配置,但是我们可以按照上述的参数列表做精细化设置,并最后拼接出完整的连接池 URL。例如我配置中的 option 都是补充的精细化设置项,并最后达到了连接池精细化设置的效果。
  6. 如果我们为 Spring Data MongoDB 指定了合适的 host 和 additional-hosts,那么非常有助于客户端稳定的进行 DocumentDB 的集群拓扑感知。

测试

这是一个简单的测试流程,我用 jmeter 以多线程(并发 20 线程无限循环)的方式请求运行在 EC2 上的 Spring 应用,这个应用先进行一次写入操作然后马上读取写入的记录,并将记录的主键返回给 jmeter。DocumentDB 的集群为 3 台 t3.medium 组成的 Instance Based Cluster。在测试过程中,我们可以手动执行 failover、reboot 和实例配置变更,测试不同场景下的连接池恢复时间以及连接池恢复期间 Spring 应用具体有那些影响。

下图是测试示意图:

下图是典型的 failover 操作实际测试过程 gif 图:

首次启动,客户端会初始化查询集群整体的拓扑关系并启动额外的监视线程时刻关注 DocumentDB 的集群变化。

程序正常运行后如果我们进行数据库集群的维护,客户端及时的感知到数据库集群的变化并做出反应。

后端应用持续处于外部请求压力的情况下也会在数秒内完成自愈,期间可能会有少量错误发生,比如下面 2 张图是抖动从开始到结束的截图:

集群选举结束时,客户端马上会尝试对新的实例进行连接、自动重新刷新连接池并尽快恢复到了稳定连接状态,结果显示是 1s 完成了恢复(2024-09-21T17:52:31.410Z – 2024-09-21T17:52:31.615Z)。

经过多次测试,可以观察到客户端能及时的感知到 DocumentDB 集群的拓扑关系变化并进行连接池重建。一般情况下连接池会出现个位数秒内的抖动(典型为 1~2s),伴随少量的 info 和 warn 级别的日志产生,期间的抖动会导致 SQL 执行效率变慢。在多轮压力测试情况下偶尔出现极少量的 SQL 执行超时错误,错误次数不多,SQL 执行超时错误是因为连接池出现了写入请求等待,我设置的连接数量不多(最大 8 个),写请求多导致抖动期积压的数量超过了等待队列的长度或者等待超时时间。依然建议在业务低峰期进行数据库维护,也建议按照实际的情况调整连接池的最大数量。抖动期读取操作基本很难看到影响,因为我设置了 readPreference 为 primaryPreferred,如果 primary 实例不可用时,客户端可以降级去读 replica 实例。正式环境的 DocumentDB 集群建议使用非 T 系列的机型,性能更好的机型可以在大压力下更快的提交完现有事务并完成故障切换。如果开启了 TLS 链路层加密后,花费的时间会略多于不开启 TLS 加密。

集群层面的 reboot、failover 或者 primary 实例变更配置会导致 primary 实例切换,对写可能有短暂影响、对读影响基本不大;加 replica 实例对读写没有任何影响;减 replica 实例对写没有影响、对读会有抖动但是影响也非常小,客户端马上会选择其他可选的 replica 来完成读操作。

总结

如果没有做好设计,连接池的更新就变得低效,这也是以前很多开发者轻易不敢进行软硬件维护的原因。经过实际的分析论证和测试,连接池合理优化后典型的恢复时间基本可以保证在个位数秒内(典型值为 1~2s),这已经是非常短的连接池自愈时间了,相比以前的分钟级的连接抖动,这个优化可以极大地提升我们日常运维操作的信心,加快采纳补丁和软件更新的频次,保障线上业务的连续性。

值得一提的是对我们的互联网业务而言安全是第一优先级。为了确保客户用云的安全和合规性,Amazon Web Services 会定期地为 DocumentDB 提供 bugfix 或者安全补丁、持续地通过迭代 DocumentDB 软件版本来给用户带来最新的特性和性能提升,对这类计划内维护事件不建议客户无限期推迟。最佳实践是规划一个定期的维护计划来及时的采纳这些计划内的软件层面的更新,比如我们可以以月度、季度、半年度或年度等为时间单位主动进行软件更新,确保自己的软件版本不会因为过于老旧而处于不安全或不稳定的状态。

本文的参数优化思路实际上也适用于 MongDB 的副本集群和分片集群。

本文参考链接

本篇作者

罗新宇

亚马逊云科技解决方案架构师,在架构设计与开发领域有非常丰富的实践经验,目前致力于 Serverless 在云原生架构中的应用。