唤醒数据库时不再遇到失效连接
Mythex 上已发布的应用及其数据库,在无人使用时都会进入休眠。应用唤醒时,第一个请求可能会被卡住 10–12 秒,因为它尝试复用在一切休眠期间已经失效的数据库连接。现在,我们为 Spring Boot 应用配置了在休眠后依然安全的数据库设置,并要求智能体以同样的方式为任何技术栈构建应用。在托管平台上,唤醒后的第一个请求从 12.7 秒降到了 2.3 秒。
为什么应用和数据库会休眠
已发布的 Mythex 应用运行在一台空闲几分钟后就会挂起的机器上,而它的数据库是无服务器的 Postgres,空闲五分钟后休眠,并在下一次请求时唤醒。这正是让闲置应用保持低成本的原因:数据库按其实际使用量计费,而休眠的应用不消耗计算资源。我们在按实际使用量计费的数据库一文中介绍过计费方面的内容。
问题在于,两者各自休眠,而它们对「连接」的理解并不一致。
症状:第一次访问很慢
大多数服务器应用会保持一个连接池:一小批已打开、随时待用的数据库连接,这样每个请求就不必承担新建连接的开销。这对于一直在线的应用来说是正确的设计。
休眠时会发生以下情况:
- 应用正在运行,连接池中有几个空闲连接。
- 无人访问。应用所在的机器挂起——冻结,连接池保持原样。
- 数据库进入休眠,并断开它那一端的所有连接。
- 访客到来。应用原样唤醒,但仍然持有数据库已不再认可的连接。
- 连接池分配出其中一个连接。它必须先尝试并等待,才能发现它已经失效,然后放弃它。
在 Spring Boot 应用中,验证这些失效连接会把第一个请求阻塞约 10 秒,最后以「Connection is not available」失败。对访客来说,网站就像卡住了一样,然后可能显示一个错误。
为什么 Spring 应用受影响最大
Spring Boot 是一个流行的 Java 框架,默认使用名为 HikariCP 的连接池。Spring 可以通过设置来配置该连接池,包括保留多少个空闲连接。
但 Mythex 为每个应用提供数据库时,使用的是一个 DATABASE_URL 环境变量,格式为 postgresql://user:pass@host/db。Spring 无法直接读取这种格式。因此,在 Mythex 上构建的 Spring 应用都是通过解析这个 URL,在代码中自行构建连接池。而手动构建的连接池会忽略 Spring 所有的 spring.datasource.* 设置。即使有人知道正确的连接池设置,它们也无处生效。
修复第一部分:Spring 可以使用的设置
现在,只要项目有数据库,我们还会提供 Spring 原生读取的环境变量:
SPRING_DATASOURCE_URL、SPRING_DATASOURCE_USERNAME和SPRING_DATASOURCE_PASSWORD,由DATABASE_URL转换而来(除非 URL 另有说明,否则要求 SSL)。- 能在休眠后保持有效的连接池设置:
| 设置 | 值 | 原因 |
|---|---|---|
| 最小空闲连接数 | 0 | 请求之间不保留任何已打开的连接,因此没有可能失效的连接 |
| 空闲超时 | 30 秒 | 尽快关闭未使用的连接 |
| 最大生命周期 | 2 分钟 | 在任何连接变旧之前将其淘汰 |
| 连接超时 | 5 秒 | 如果数据库响应缓慢,不让请求长时间挂起 |
由于不保留空闲连接,唤醒后的应用只需打开一个全新的连接,而不必在失效连接中逐个尝试。
存在这些变量时,Spring 会自行配置连接池,并应用这些设置。项目自己设置的值始终优先;我们只补充缺失的部分。非 Spring 应用会忽略这些变量。
几乎让它失效的细节
Spring 将环境变量映射到其设置的规则很容易搞错。minimum-idle 这一设置必须写成 SPRING_DATASOURCE_HIKARI_MINIMUMIDLE——去掉连字符,单词连在一起。如果写成看起来更自然的 SPRING_DATASOURCE_HIKARI_MINIMUM_IDLE,它会映射到一个不同的、并不存在的设置(minimum.idle),并且会被悄悄忽略。
这类错误能通过每一次代码审查,却在生产环境中什么也不做。因此,我们编写了一个测试,既检查这些值是否已设置,也检查没有任何变量使用下划线分隔的形式。
修复第二部分:所有其他技术栈
Spring 并不是唯一带连接池的框架,而我们也无法为每种语言中的每个库预先配置。因此,Mythex 智能体——负责编写你的应用的 AI——现在针对任何技术栈都有一条常驻指令:已发布的应用及其数据库在空闲时会休眠,因此一个保持打开的连接,到下一次请求时可能已经失效。它应当:
- 不保持任何空闲的数据库连接,或在使用前逐个检查;
- 在连接错误时重试一次;以及
- 使用平台在环境变量中提供的数据库设置,而不是通过解析
DATABASE_URL自行构建连接池。
最后一点与第一部分形成闭环:使用平台提供的设置构建的应用,会自动获得能够抵御休眠的配置。
结果
在我们运行已发布应用的托管平台上测量,唤醒后的第一个请求从 12.7 秒降到了 2.3 秒。
我们的收获
- 每个会休眠的层都需要就「什么能保留下来」达成一致。 冻结的应用和休眠的数据库各自做出了合理的选择。合在一起,应用醒来时持有的状态已经不再是真实的。
- 为常驻服务器设计的默认值,不适合会缩容到零的应用。 保持连接温热是个好主意,但前提是它周围的一切都没有变冷。
- 以框架自己的格式提供数据。 对我们来说,一个 URL 很简单,却迫使每个 Spring 应用都要手工构建一个忽略自身配置的连接池。
- 测试设置的拼写是否与框架的读取方式一致。 名称错误的正确值什么也不做,而且没有任何提示。
如果你刚开始了解 Mythex 上的数据库工作方式,请参阅数据库文档,以及我们关于什么是 Postgres的解释文章。