KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
03 · 连接池、读写分离与高并发写 — keel 龙骨
这一章回答:并发上来之后,最先崩的往往是连接池——怎么配、怎么测、怎么救。
这一章回答:并发上来之后,最先崩的往往是连接池——怎么配、怎么测、怎么救。
应用层的并发上限、数据库的连接数、磁盘 IO 三者之间存在木桶效应。连接池配置不当的典型表现是:数据库还很闲,应用已经在报错了,或者反过来:应用把数据库连接打满,导致所有服务一起不可用。
一、连接池到底在限制什么
一个请求进来要经过:取连接 → 执行 SQL → 归还连接。连接池的容量决定了同时能有多少个 SQL 在执行,取不到连接的请求会排队或直接失败。
关键参数(以 SQLAlchemy 异步引擎为例):
engine = create_async_engine(
url,
pool_size=20, # 常驻连接数
max_overflow=10, # 高峰时可临时额外创建的连接数(超出部分用后即弃)
pool_timeout=30, # 取连接的最长等待时间,超时抛错而不是无限等
pool_recycle=3600, # 定期回收连接,避免被中间设备或数据库主动断开
pool_pre_ping=True, # 每次取用时先探活,牺牲一点性能换稳定性
)
配置要点:
| 参数 | 常见误区 | 建议 |
|---|---|---|
pool_size + max_overflow |
设得越大越好 | ❌ 它必须小于数据库的 max_connections,且多个实例要乘上实例数计算 |
pool_timeout |
用默认值不管 | 应配合接口超时,让排队在可接受时间内失败而不是堆到超时连锁反应 |
pool_pre_ping |
不知道有这个 | 有负载均衡/长空闲的生产环境建议开启,能消除"连接已被断开"类报错 |
pool_recycle |
从不设置 | 小于数据库的 wait_timeout |
容量估算公式(粗估):总连接数 = 实例数 × (pool_size + max_overflow),这个值要显著小于数据库的 max_connections(留出给管理连接、迁移任务、其他服务)。
二、异步环境下的额外注意
异步不是万能的:连接池上限不会因为用了 async 就变大。数据库侧的连接是有限资源,协程只是让等待不占用线程。
所以:
AsyncSession必须显式关闭/用上下文管理器,否则连接不归还,几轮压测就耗尽;- 一个请求里不要开多个不相关的会话/事务,串行执行会放大持连接时间;
- 长事务持有连接不放,等于变相降低连接池容量(这与上一章呼应)。
三、读写分离
什么时候引入:读压力明显大于写、且主从延迟可接受时。
写 → 主库
读取(容忍延迟)→ 只读实例
要求读到自己刚写的数据 → 主库(或强制一致性读)
三个必须处理的坑:
- 主从延迟:写完立刻读可能读不到。做法是关键路径读主库,或提供"写后读走主库"的开关;
- 大查询拖垮从库:报表、导出类查询要走独立的分析实例,不能和在线查询混用;
- 延迟监控:把主从延迟做成指标,超过阈值时自动把读流量切回主库。
四、高并发写的四条策略
| 策略 | 做法 | 适用 |
|---|---|---|
| 批量合并 | 多条 INSERT 合并为一条 INSERT INTO ... VALUES (...),(...),更新走批量 UPDATE |
日志、流水类写入 |
| 异步落库 | 写请求先进 MQ/Redis,消费者批量落库 | 允许最终一致的场景 |
| 削峰 | Redis 预扣/计数 + 队列平滑 | 秒杀、抢购 |
| 拆分表 | 单表超过千万级行数考虑分库分表 | 数据量本身成为瓶颈之后 |
顺序建议:先做批量与SQL优化,再考虑异步化,最后才上分库分表。分库分表会引入分布式主键、跨片查询、迁移运维三重复杂度,不要提前透支。
五、必须有的三条保护
- 连接耗尽时的降级:明确返回"系统繁忙"而不是让请求无限堆积;
- 不同用途用不同连接池:在线业务、后台任务、数据迁移各自独立,互不挤占;
- 慢查询保护:设置
max_execution_time(或等价机制),防止一条 SQL 拖死整个实例。
动手:可观察结果
| 产出 | 判断标准 |
|---|---|
| 一份连接池配置单据 | 写出计算过程:实例数 × (pool_size + max_overflow) < max_connections |
| 一次压测 | 打到连接池上限,观察是快速失败还是超时崩溃;记录 P99 与错误率 |
| 连接泄漏检查 | 长时间压测后连接数能回落到基线(附会话泄漏检测的排查记录) |
| 主从延迟监控 | 延迟超阈值自动切主库的演练记录 |
完成标志:给定 QPS 目标,你能算出需要的连接池容量,并说明超出时的降级行为。
故障注入
| 注入方式 | 观察 |
|---|---|
忘记关闭 AsyncSession |
压测几分钟后是否出现连接耗尽 |
pool_size 设得远大于数据库上限 |
是否出现数据库拒绝连接、影响其他服务 |
关闭 pool_pre_ping 并长时间空闲后再请求 |
是否出现"连接已断开"类错误 |
| 从库人为制造 30 秒延迟 | 写后立即读是否读到旧数据 |
| 跑一个大查询打到同一从库 | 在线查询延迟是否被拉高 |
自测题
- 连接池容量应该怎么算?多实例部署时最容易犯的错误是什么?
pool_pre_ping解决了什么问题,代价是什么?- 用了异步 ORM 之后,数据库连接瓶颈消失了吗?为什么?
- 主从延迟导致的"写后读不一致"怎么处理?
- 高并发写的四条策略,你的采用顺序是什么?为什么最后才考虑分库分表?