Serverless 数据库最怕什么?不是没连接,而是连接“太多了”
大家好,我是 Echo_Wish。
这几年 Serverless 越来越火。
以前我们部署一个 Web 服务,习惯是:
买几台服务器 → 部署应用 → 配连接池 → 连数据库 → 慢慢跑。
现在变成:
请求来了 → 函数启动 → 执行业务 → 函数结束。
看起来非常爽。
不用管服务器,不用操心扩容,流量来了自动加实例。
但问题也来了:
应用可以无限弹性,数据库连接却没有无限弹性。
这恰恰是很多 Serverless 项目上线之后才发现的“坑”。
尤其是数据库连接管理。
很多人第一次做 Serverless,会很自然地写出这样的代码:
def handler(event, context):
conn = pymysql.connect(
host="db.example.com",
user="root",
password="123456",
database="order"
)
cursor = conn.cursor()
cursor.execute("SELECT * FROM orders LIMIT 10")
result = cursor.fetchall()
conn.close()
return result
本地测试:
没问题。
测试环境:
没问题。
上线:
数据库连接数爆了。
然后数据库开始疯狂报错:
Too many connections
这时候你可能会很委屈:
“我明明每次请求结束都 close 了,为什么还是连接爆炸?”
问题就出在 Serverless 的运行模型上。
一、传统连接池,在 Serverless 里可能变成“放大器”
我们先理解传统应用为什么喜欢连接池。
假设一台服务器启动一个 Java 服务。
它创建一个连接池:
Application
│
├── Connection 1
├── Connection 2
├── Connection 3
├── Connection 4
└── Connection 5
│
▼
Database
比如配置:
maximumPoolSize: 20
minimumIdle: 5
通常问题不大。
因为:
一台服务器 = 一个连接池。
但是 Serverless 不一样。
假设你的函数突然扩容成 100 个实例。
每个实例都创建:
Pool Size = 20
那么数据库理论上可能面对:
100 × 20 = 2000
个连接。
这还只是数学题。
现实中还可能出现:
实例数 × 最大连接池连接数
再加上:
- 数据库管理连接
- 其他微服务连接
- 定时任务
- 后台任务
- 开发人员工具
- 慢连接
- 连接泄漏
数据库很容易直接跪下。
所以我一直觉得:
Serverless 最大的数据库连接问题,不是连接池太小,而是连接池被复制了太多份。
二、Serverless 为什么特别容易把数据库打爆?
关键就在于它的“弹性”。
假设平时只有 5 个实例。
每个实例:
Pool = 10
数据库连接:
5 × 10 = 50
看起来很健康。
突然来了促销活动。
Serverless 从:
5 instances
扩展到:
200 instances
那么:
200 × 10 = 2000 connections
数据库可能根本扛不住。
更麻烦的是,Serverless 还存在冷启动。
新实例启动:
请求
↓
创建实例
↓
初始化数据库连接池
↓
创建连接
↓
执行 SQL
流量一上来,就可能出现一波非常明显的连接创建峰值。
这就是所谓的:
Connection Storm,连接风暴。
它特别像什么?
像高速公路收费站。
平时 5 个车道够用。
突然来了 200 辆车。
你不是把汽车扩容了就完事了。
因为收费站还是那几个。
三、所以 Serverless 最重要的组件之一:数据库 Proxy
这时候,数据库 Proxy 就开始发挥作用了。
简单理解:
不要让 Serverless 函数直接怼数据库,让 Proxy 夹在中间。
架构从:
Function
│
▼
Database
变成:
Function
│
▼
Database Proxy
│
▼
Database
Proxy 干什么?
其中一个非常重要的工作就是:
帮你管理数据库连接。
例如:
Function A ─┐
Function B ─┤
Function C ─┤
Function D ─┤
Function E ─┤
▼
┌───────────┐
│ Proxy │
│ Connection│
│ Pool │
└───────────┘
│
┌─────┴─────┐
▼ ▼
Database Database
函数实例可以很多。
但 Proxy 到数据库的连接可以被控制。
比如:
1000 个函数实例
↓
Proxy
↓
100 个数据库连接
↓
MySQL
当然,具体连接数量还是要根据数据库规格、SQL 类型和业务负载压测出来。
但思路非常重要:
把“连接管理”从函数实例手里拿回来。
四、Proxy 并不是“万能盾牌”
这里特别容易产生一个误区。
有的人一听:
“用了数据库 Proxy,就不会连接爆炸了。”
这其实是不对的。
Proxy 能解决的是:
数据库连接管理和复用问题。
但是它不能解决:
- SQL 写得烂
- 查询特别慢
- 单个事务持续几十秒
- 数据库 CPU 已经 100%
- 大量全表扫描
- 索引设计错误
- 请求突然暴涨
- 函数疯狂重试
比如:
SELECT *
FROM orders
WHERE user_name LIKE '%abc%';
你前面加一个 Proxy。
后面还是:
1000 个请求
↓
1000 个慢 SQL
↓
Proxy
↓
Database CPU 100%
Proxy 也只能干瞪眼。
所以:
Proxy 解决的是“连接问题”,不是“数据库性能问题”。
这个边界一定要搞清楚。
五、那连接池到底要不要?
要。
但是不能再用传统服务器思维来配置。
传统应用可能喜欢:
minimumIdle = 10
maximumPoolSize = 50
Serverless 就要谨慎很多。
尤其是:
minimumIdle
这个参数。
为什么?
因为 Serverless 实例可能大量存在,又可能突然消失。
假设:
100 个实例
minimumIdle = 10
那么理论上就可能有:
100 × 10 = 1000
个空闲连接。
这些连接可能根本没有业务请求。
但它们依然占着数据库资源。
所以 Serverless 里我更倾向于:
让连接池足够小,让连接复用交给更合适的中间层。
例如:
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
connectionLimit: 2,
waitForConnections: true,
queueLimit: 10
});
这里的思路不是:
“连接池越大越好。”
而是:
每一个 Serverless 实例都要对自己的连接预算负责。
六、真正需要关注的是“连接预算”
我特别建议做 Serverless 的时候,别只看:
QPS
还要算:
数据库最大连接数
比如:
数据库最大连接数:
max_connections = 1000
不要直接认为:
1000
都可以给 Serverless 用。
你可能还需要预留:
100 管理连接
100 其他应用
100 运维工具
那么真正给 Serverless 的预算可能只有:
700
假设:
最大实例数 = 200
那么平均下来:
700 / 200 = 3.5
也就是说:
每个实例连接池最好不要轻易超过 3 个左右。
当然,这只是帮助理解的计算模型。
实际设计还要考虑:
- 并发请求数
- SQL 执行时间
- 事务时间
- Proxy 行为
- 数据库 CPU
- 读写比例
- 连接复用率
但至少你开始有了一个非常重要的概念:
连接预算。
七、还有一个经常被忽略的问题:数据库密码
很多 Serverless 项目一开始特别简单。
环境变量:
DB_HOST=xxx
DB_USER=root
DB_PASSWORD=123456
然后直接部署。
方便是方便。
但安全性呢?
函数环境变量一旦管理不严谨,数据库凭据就可能随着:
- 日志
- 配置文件
- CI/CD
- 镜像
- 调试信息
到处传播。
我的建议是:
数据库密码不要硬编码。
例如:
import os
db_user = os.getenv("DB_USER")
db_password = os.getenv("DB_PASSWORD")
进一步可以使用云平台的 Secret 管理能力。
架构变成:
Function
│
│ 获取 Secret
▼
Secret Manager
│
▼
Database Credential
这样做最大的好处不是“密码绝对不会泄露”。
而是:
你终于可以集中管理密码,而不是让密码散落在项目的各个角落。
八、数据库账号也别直接给 root
这一点看起来很基础,但现实项目中非常常见。
比如业务只需要:
SELECT
INSERT
UPDATE
结果直接给:
root
这就相当于:
你只是想让员工开门,结果把整个公司的保险柜钥匙都给他了。
应该遵循:
最小权限原则。
例如:
CREATE USER 'app_user'@'%'
IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE
ON order_db.*
TO 'app_user'@'%';
甚至可以进一步拆分:
Read User
↓
只读
Write User
↓
读 + 写
Admin User
↓
数据库管理
Serverless 函数使用的账号,最好不要拥有:
DROP DATABASE
CREATE USER
GRANT
ALTER SYSTEM
之类的高危权限。
九、网络安全也不能只靠用户名和密码
如果你的架构是:
Internet
↓
Function
↓
Database
那我会非常警惕。
更推荐:
Internet
↓
API Gateway
↓
Serverless Function
↓
Private Network
↓
Database Proxy
↓
Database
数据库尽量不要直接暴露公网。
同时可以配合:
- Security Group
- VPC
- 私有子网
- 白名单
- TLS
- 数据库身份认证
- Secret Manager
- IAM 权限
形成多层防护。
因为真正靠谱的安全策略从来不是:
“我密码够复杂,所以数据库很安全。”
而是:
就算密码泄露,攻击者也很难直接碰到数据库。
十、别忘了 Serverless 的“重试”
还有一个非常容易被低估的问题:
重试机制。
假设你的函数执行失败。
平台可能自动重试:
Request
↓
Function
↓
失败
↓
Retry
↓
Function
↓
失败
↓
Retry
如果一次请求会创建一个数据库连接。
那么一次业务请求可能变成:
1 Request
↓
3 Function Attempts
↓
3 Database Connections
如果同时有:
1000 Requests
问题就来了。
因此 Serverless 数据库架构一定要考虑:
重试 × 并发 × 连接数
甚至可以简单建立一个估算模型:
最大连接压力
≈
最大并发实例数
×
单实例最大连接数
×
重试放大系数
这不是严格的数据库容量公式。
但作为架构设计阶段的“报警器”,非常有用。
十一、事务时间越长,Serverless 越危险
还有一个问题:
BEGIN;
UPDATE orders
SET status = 'PAID'
WHERE id = 10001;
-- 中间干了一堆事情
COMMIT;
如果这个函数因为某个外部 API 卡住:
Database Transaction
↓
等待 5 秒
↓
等待 10 秒
↓
等待 30 秒
那么连接就一直被占用。
如果大量函数都这么干:
100 functions
×
1 long transaction
=
100 occupied connections
所以 Serverless 特别需要注意:
数据库事务里面尽量只做数据库相关的事情。
不要:
BEGIN
↓
UPDATE DB
↓
调用第三方支付
↓
请求 AI
↓
调用库存系统
↓
等待网络
↓
COMMIT
更合理的是:
数据库事务
↓
快速完成
↓
提交
然后
↓
异步处理其他事情
十二、我认为 Serverless 数据库真正靠谱的架构
如果让我自己设计一个生产环境,我更倾向于:
Internet
│
▼
API Gateway
│
▼
Serverless Function
│
┌─────────┴─────────┐
│ │
▼ ▼
Secret Manager Cache/Queue
│
▼
DB Proxy
│
▼
Database
这里每一层解决的问题其实不一样。
| 组件 | 主要解决问题 |
|---|---|
| API Gateway | 流量入口、认证、限流 |
| Serverless | 弹性计算 |
| Secret Manager | 凭据管理 |
| DB Proxy | 数据库连接复用 |
| Cache | 减少数据库查询 |
| Queue | 削峰填谷、异步处理 |
| Database | 最终数据存储 |
千万不要把所有压力都扔给数据库。
尤其是查询型业务:
用户
↓
Function
↓
Redis
↓
命中?
├── 是 → 返回
└── 否
↓
DB Proxy
↓
Database
数据库压力一下就能降很多。
十三、最后总结一下
Serverless 最大的魅力是什么?
弹性。
Serverless 最大的坑是什么?
也是弹性。
因为:
计算资源
可以无限扩
但是:
数据库
不能无限扩
所以传统架构里一句:
“给连接池调大一点。”
到了 Serverless 世界,可能直接变成:
“恭喜你,把数据库连接数调爆了。”
我个人认为,Serverless 数据库连接管理应该建立三个意识。
第一,连接不是越多越好
连接本身就是资源。
Function
↓
Connection Pool
↓
Proxy
↓
Database
每一层都应该控制连接规模。
第二,不要让数据库裸奔
数据库应该尽可能处于:
Private Network
+
Proxy
+
TLS
+
Secret
+
Least Privilege
的保护体系里面。
第三,数据库永远是瓶颈候选人
Serverless 可以一分钟扩容 100 倍。
数据库可不会因为你喊一句:
“流量来了,给我扩!”
就立刻扩 100 倍。
所以做 Serverless 架构时,真正应该问的不是:
“我的函数能扛多少 QPS?”
而应该问:
“我的数据库能承受多少连接、多少事务、多少 SQL,以及这些压力会不会随着函数扩容一起被放大?”
这才是 Serverless 数据库架构真正应该考虑的问题。
Serverless 不是“没有服务器”,而是把服务器的复杂度转移了。
而数据库连接管理,就是这个复杂度里非常容易被忽略、却又非常致命的一块。
我是 Echo_Wish。
如果你也正在折腾 Serverless,记住一句特别朴素的话:
函数可以疯狂扩,数据库连接千万别跟着疯。