一个生产环境的 PostgreSQL 部署崩溃了,起因是一名开发人员使用 CREATE OR REPLACE FUNCTION 为现有函数添加了一个可选参数。这一改动导致出现了两个同名函数,使得数据库返回“function is not unique”(函数不唯一)错误,并导致 API 抛出 400 错误。这起事件表明,一个单一的迁移错误是如何在无声无息中损坏在线模式(schema)的,以及为什么仅靠代码层面的检查是不够的。

问题出在哪里

团队需要通过增加一个额外的可选参数来扩展一个存储过程。他们运行了 CREATE OR REPLACE FUNCTION …,以为这会覆盖旧的定义。然而,只有当完整的参数列表完全匹配时,PostgreSQL 才会替换函数。更改函数签名会创建一个全新的函数条目,而原始函数则保持不变。

由于新参数带有默认值,那些提供旧参数数量的调用者可能会匹配到这两个定义中的任何一个。PostgreSQL 无法决定调用哪一个,于是抛出了“function is not unique”错误,这在 API 端表现为 400 响应。

代码仓库中只显示了一个定义,而扫描源码树的自定义脚本也没有报告任何重复项。重复项仅存在于数据库中,是在服务器上重新执行旧的迁移文件时引入的。

为什么迁移未能拦截此问题

添加可选参数的迁移仅仅运行了 CREATE OR REPLACE FUNCTION。当迁移第二次运行时——可能是在回滚之后或在重复部署期间——数据库将该命令视为“添加一个新的重载”,而不是“替换现有的一个”。迁移没有验证最终的状态,因此重复项在无人察觉的情况下持续存在。

检查代码仓库的脚本检查的是源文件,而不是在线模式(live schema)。它在盯着正门,而漏洞却从后门溜了进来。

风险所在

一个模糊的函数可能会导致任何依赖它的服务崩溃。修复方案包括:如果函数计数错误,则回滚事务,并通知模式缓存(schema cache)重新加载。

如何保障迁移安全

团队通过显式检查重构了迁移,将其变成了一个具有自我断言能力的(self-asserting)操作:

  • 启动事务,以便任何失败都能回滚整个变更。
  • 在创建新版本之前显式删除旧函数,以确保只存在一个定义。
  • 使用所需的签名创建新函数
  • pg_catalog统计具有给定名称的函数数量,并验证其数量正好为 1。
  • 如果计数不符,则回滚事务,防止重复项持续存在。
  • 通知模式缓存重新加载,确保后续查询能看到更新后的定义。

通过询问数据库“存在哪些函数?”而不是假设代码是正确的,迁移在面对重复运行、部分部署或手动编辑时变得更加可靠。

反方观点:便利性 vs. 安全性

CREATE OR REPLACE FUNCTION 非常吸引人,因为它允许开发人员快速迭代,而无需编写单独的删除语句。在迁移仅运行一次且永不重复运行的环境中,这种快捷方式运行良好。但当迁移被重新执行时,风险就会显现——无论是由于重置测试数据库的 CI 流水线、自动回滚,还是在生产环境中的手动重新应用。

总结

使用 CREATE OR REPLACE 更改函数签名并不能保证替换——如果参数列表不同,PostgreSQL 会在不提示的情况下创建一个重载。依赖迁移的生产环境必须验证最终生成的模式(schema),而不仅仅是源代码。通过嵌入显式的删除操作、事务检查和迁移后断言,可以将一个便捷的快捷方式转变为一个可靠、可重复的过程。