跳到主要内容

升级与数据库迁移

schema 迁移在启动时怎么跑,以及多实例部署该按什么顺序升级。

换二进制的命令在升级里。这页讲运行侧的另一半: 进程启动时会对你的库做什么,以及从第一个实例停掉到最后一个实例回来的这段时间里, 多实例部署是什么样子。

迁移在进程启动时跑,没有单独的命令​

没有迁移命令,也没有迁移状态表。n9e 连上元数据库之后——在连 Redis 之前、 在建内存缓存之前、在监听端口之前——就用 GORM 的 AutoMigrate 把编译进二进制的那份模型列表 过一遍。新版本需要的表和列要么这时出现,要么就没有。

由此带来两条性质,都很重要:

  • 它只增不减。 缺的表、列、索引会被建出来,但从不删任何东西,也不记录改之前的样子。 所以不存在反向迁移——见回滚。
  • schema 的来源是模型,不是仓库里的 SQL 文件。 docker/sqlite.sql 和 docker/initsql/a-n9e.sql 是全新安装用的,而且落后于代码:sqlite.sql 里 根本没有 notify_rule、notify_channel_config、message_template、event_pipeline、 ai_llm_config 这些表。它们在跑着的 v9 里存在,纯粹是 AutoMigrate 建出来的。 别拿线上的表结构去和这两个文件比对,来判断升级有没有成功。

启动顺序,按「哪一步会失败」排列:

步骤失败了会怎样
连接元数据库进程直接退出
跑 schema 迁移记一行日志,继续启动
建 root 用户、JWT 签名密钥记一行日志,继续启动
连接 Redis进程直接退出
把配置加载进内存缓存、启动告警引擎规则不判定
监听 17000端口被占,进程退出

分两批:小表挡启动,大表不挡​

批次覆盖是否挡启动
同步除事件表以外的所有表——规则、用户、数据源、通知配置挡
异步 goroutinealert_cur_event、alert_his_event,外加三个索引不挡

异步这一批还要建三个索引,落在两张会无限长大的表上——而且建法不一样:

  • notification_record 上那两个是刻意不挡写的:MySQL 上显式带 ALGORITHM=INPLACE, LOCK=NONE, PostgreSQL 上走 CREATE INDEX CONCURRENTLY。MySQL 上在线加不了索引时它宁可报错, 也不会静默退化成阻塞写入的 DDL;
  • alert_his_event 上的 idx_group_last_eval_time 是普通的 CREATE INDEX,没有这些子句。 在 PostgreSQL 上它整个构建期间会持有该表的共享锁,把写入挡住——而 alert_his_event 通常就是你库里最大的那张表。PostgreSQL 上数据量大的部署,要预期历史事件在这段时间写不进去。

也就是说,库里最大的那张表还在改结构的时候,进程已经在回 /ping、在提供界面、 在判定规则了。alert_his_event 上了几个 GB 的话,这事要跑几十分钟。 不要为了「把它弄活」去重启:重启只会让同样的活儿从头再来一遍。

有一条警告是预期内的:如果 notification_record 还没有 notify_rule_id 列, 依赖这个列的那个索引会被跳过,日志写 will retry on next start,另一个索引照常建。 别管它。

迁移失败不会让进程停下来​

每一条迁移错误都是记日志然后吞掉,迁移里的 panic 也会被 recover 掉。 这是有意为之——另一个实例或者下次重启会把 schema 补齐——但带来的后果很容易漏掉: 一个实例可以正常启动、正常服务、正常上报心跳,同时缺着它需要的某一列。 你不会看到启动失败,你会在好几天以后看到某一个功能在运行时报 Unknown column。

所以升级之后要做的检查是 grep 日志,不是看健康检查:

grep -iE "failed to migrate table|failed to create index" /opt/n9e/logs/*.log

预期结果:没有输出。有输出的话几乎都是权限问题——[DB] DSN 里那个账号能读能写, 但没有 CREATE / ALTER 权限。补上授权重启,或者按 v8 升级 v9 里的说法,把 docker/migratesql/migrate.sql 里对应版本的段落交给 DBA 执行。

先让一个实例把迁移跑完,再滚其余的​

迁移代码里针对「多实例同时启动」写了两处显式防御,这说明这两件事是真会发生的:

  • 两个实例并发对同一张表发 ALTER,会让 MySQL 驱动查列类型时解引用到空值。 panic 会被 recover,但这次启动里那个迁移入口就等于放弃了;
  • 大表 alert_his_event 上重复执行的 CREATE INDEX 会卡在元数据锁上,把启动挂住—— 不是卡几秒,是一直卡着。

两种都不会弄坏数据,但都会白白搭进去一次启动,还得回头排查。避开这两件事的顺序是:

  1. 停一个实例,换二进制,起来。迁移由它来做。
  2. 等它的日志安静下来:没有 failed to migrate,索引也不再有动静。
  3. 然后才动下一个,之后一个一个来。
告警引擎页面告警引擎页面

升级里要求一个一个滚,是为了让告警不中断。 迁移竞争是同一条规则的第二个、独立的理由——也是「第一个实例升完要停一停再动第二个」 而不是「隔固定时间动下一个」的原因。

升级窗口里到底损失了什么​

以单个实例为单位,从 systemctl stop 到恢复稳态:

功能这个实例重启期间
规则判定它的规则 30 秒内被其他实例接管,之后照常跑
规则判定(重启的这个实例上)启动后还要再等 30 秒才开始([Alert] EngineDelay)
指标写入写到这个实例 /prometheus/v1/write 的请求会失败;前面挂负载均衡,或者接受这段缺口
界面和 API只有这个实例上不可用
通知跟着判定走——谁判定谁发
内置时序库整个重启期间既读不了也写不了,何况集群里本来就不该开它
边缘机房n9e-edge 继续在本地判定和发通知,见边缘断网行为

所有实例一起重启,会把上面第一行变成「升级多久,就多久没有任何规则在判定」—— 这正是要滚动升级的全部理由。

相关​