内容提要
文章对比四家Postgres托管服务在内存压力下的可靠性:递归UNION查询会生成不受work_mem限制的哈希表,可能耗尽内存。测试中,ClickHouse Managed Postgres通过内存上限使查询干净失败,集群保持存活;RDS触发OOM崩溃并进入恢复;Cloud SQL和PlanetScale则杀掉连接。结论是较低内存上限虽牺牲部分查询,但换取了数据库整体可用性。
延伸解读
内存上限的权衡:可用性优先
文章测试显示,ClickHouse Managed Postgres 通过设置较低的内存上限,在查询失控时主动报错,从而保持集群存活。这种策略牺牲了部分查询的成功率,但换来了数据库整体可用性。对于无法完全控制工作负载的场景,这种设计可以避免因单条糟糕查询导致整个实例崩溃,适合优先考虑稳定性的用户。
不同故障模式的对比
测试中,RDS 在内存耗尽时触发 OOM 崩溃并进入恢复,导致集群不可用;Cloud SQL 和 PlanetScale 则通过监控杀死连接,但连接会收到 FATAL 错误且可能不稳定。ClickHouse Managed Postgres 是唯一让查询干净失败(返回 ERROR)且连接保持可用的方案。读者可根据对故障恢复速度和连接稳定性的要求选择。
递归查询为何难以限制内存
文章指出,递归 CTE 中使用 UNION 去重时,哈希表需要保存所有已见节点,且该内存结构不支持 work_mem 溢出到磁盘。因此,即使调低 work_mem,这类查询仍可能耗尽内存。这提醒读者,某些查询模式无法通过常规参数控制内存,需要依赖数据库层面的硬性内存限制来防止崩溃。
Q&A
为什么 Postgres 的 work_mem 不能保证一条查询的内存使用上限?
work_mem 是每个操作节点的内存预算,而不是整条查询的上限。一个查询计划可能有多个需要内存的节点,每个节点都有自己的 work_mem 预算,并且哈希类节点还会乘以 hash_mem_multiplier(默认 2.0)。因此实际内存使用可能是 work_mem 的许多倍。
递归 UNION 查询为什么可能耗尽内存?
递归 UNION 需要去重,会在执行器中创建一个哈希表来记录所有已见节点。这个哈希表在整个查询生命周期内驻留内存,且不支持像 HashAggregate 那样溢出到磁盘,因为每次成员检查都需要读取整个表。因此,对于大图,哈希表可能持续增长直到内存耗尽。
在内存压力测试中,ClickHouse Managed Postgres 与其他托管服务相比表现如何?
ClickHouse Managed Postgres 通过内存上限使超限查询干净地失败(返回 SQL ERROR),但集群始终保持存活。RDS 在 19 个连接时触发 OOM 崩溃并进入恢复;Cloud SQL 和 PlanetScale 则会杀死连接,且 PlanetScale 在 11+ 连接时多数运行以崩溃告终。
Postgres 在内存不足时有哪些失败模式?
有三种失败模式:查询失败(返回 out_of_memory 错误,事务回滚,连接保持)、会话失败(连接被终止,如 Cloud SQL 和 PlanetScale 的 supervisor 杀死查询)、集群失败(OOM killer 杀死后端,Postgres 重启进入崩溃恢复,导致数分钟不可用,如 RDS)。
为什么 ClickHouse Managed Postgres 选择较低的内存上限?
较低的内存上限是一种有意的权衡,以换取数据库整体可用性。它让系统在失控查询时只失败单个查询,而不是让整个数据库崩溃。虽然会牺牲部分查询,但避免了所有客户端经历崩溃恢复。
并行查询如何影响 Postgres 的内存使用?
并行查询会启动多个工作进程,每个进程都会构建自己的内存节点副本(如哈希表、排序等),并且每个进程有自己的 work_mem 预算。因此,并行度增加会成倍增加总内存使用。例如,两个工作进程加领导者进程可能使内存使用增加约 3 倍。