# JNPF MySQL → PostgreSQL 迁移工具包操作手册 > ## 🏁 迁移已完成,本工具包大部分已退役(2026-07-25 标注) > > MySQL→PG 迁移于 **2026-07-21 全部完成**(数据落在腾讯云 CVM 共享 PG `YOUR_DB_HOST:15432`, > 开发与测试共用一库)。本目录内脚本的现状分三类: > > | 脚本 | 现状 | > |---|---| > | `backup.sh` | ✅ **仍是主力**——CVM 库每日 03:00 四库备份即由它执行 | > | `create-app-roles.sh` | ✅ **仍需要**——新环境(含将来的正式环境)restore 前必须先建角色,否则 owner 落不对 | > | `install.sh` / `assemble-*.sh` | ⚠️ **仅"全新空库"备用**——上线已改为整库 dump/restore(见 `docs/release-checklist.md` §1),不再现场重建 schema | > | `migrate-data.sh` / `migrate-nacos-config.sh` / `schema-drift-check.sh` | ⛔ **已退役,勿再执行**——它们的前提是"腾讯云 MySQL 是事实源",该前提已消失(MySQL 数据停在 07-16,仅作冷备回退底牌保留) | > > **误跑 `migrate-data.sh` 的后果是真实的数据事故**:它会用 MySQL 旧快照 truncate 重灌目标库。 > 脚本已加执行护栏(启动即拒绝,须显式设 `I_KNOW_MIGRATION_IS_RETIRED=1` 才放行)。 > > 当前拓扑与连接纪律:`docs/db-connection-pool-guide.md`。 > **定位(2026-07-17,Backlog B 方案 (c) 合流后)**:本目录是**一次性 MySQL→PG 数据迁移工具包**, > 只覆盖「schema 初始化 + 停机窗口迁数据 + 备份」;现场割接完成后即退役。 > **PG 容器的部署统一走仓库根 `docker-compose.yml`**(Backlog A 加固基线:端口收敛 + `${VAR:?}` > 硬约束凭据),旧 `docker-compose.prod.yml` 单机部署面已删除。 > 整栈离线交付见 `docs/docker-compose-guide.md` §14(`make images-offline`);本目录会被收入 > 交付包的 `migration/` 子目录。方案背景见 `docs/archive/pg-migration-plan.md`,踩坑见 `tasks/lessons.md`。 --- ## 0. 工具包内容 | 文件 | 说明 | |---|---| | `install.sh` | schema 初始化(全新空库):建四库(jnpf_init/jnpf_flow/jnpf_xxljob/nacos_config)+ 建表 | | `migrate-data.sh` | 停机窗口一次性数据迁移(现场 MySQL → PG) | | `backup.sh` | pg_dump 定时备份 + 保留策略(配 cron),默认含 nacos_config | | `create-app-roles.sh` + `create_app_roles.sql` | 建业务专用连库角色(三库三角色,读 .env,幂等,L030) | | `schema-drift-check.sh` | 表级+列级漂移检查(远程 MySQL 事实源 vs 本地 PG);合并 main 或数据刷新**之前**先跑,阻断项直接给类型明细(SOP 见 docs/docker-compose-guide.md §18,L035) | | `assemble-package.sh` | **构建机专用**,把 repo 里的 SQL 组装进 `sql/`(现场不用) | | `sql/` | 全部初始化/迁移 SQL(构建机 assemble 生成,不进 git) | ### 构建机组装(交付前,在开发机跑一次) ```bash cd docker/postgres/migration ./assemble-package.sh # 把 sql/ 填满(含官方 init + 自建表 DDL + pgloader 模板) ``` 组装结果随 `make images-offline` 一并进整栈离线交付包,不再单独打 tar。 --- ## 1. 现场前置条件 - Linux x86_64,已装 **Docker + docker compose 插件**(`docker compose version` 可用)。 - 数据盘:`/data` 分区建议 ≥ 20GB(PG 数据 + 备份 + 临时 dump)。 - 与现场 MySQL(源库)网络可达。 - PG 容器已按根 compose 拉起(见 §2);离线环境镜像已 `docker load`(含 `mysql:8.0` 与 `dimitri/pgloader` 迁移辅助镜像,在交付包 `images/jnpf-migration-tools-*.tar.gz`)。 --- ## 2. 起 PG 容器与建 schema(可提前做,不需停机) PG 由根 compose(仓库根或交付包根)拉起——凭据经 `.env` 注入、基线不对外发布端口: ```bash # 在仓库根 / 交付包根: cp .env.example .env && vi .env # 全部密码改现场值(角色名/用户名保持默认,SQL 已硬编码) docker volume create postgres_pgdata # 全新机器 docker compose up -d cx-postgres # 只起 PG(healthcheck 过了即可) ``` > **混合形态**(JNPF 服务暂留宿主进程、仅 PG 容器化):基线不发布 PG 端口,需临时 override—— > ```yaml > # docker-compose.override.yml(现场临时,切全容器后删除) > services: > cx-postgres: > ports: ["127.0.0.1:5432:5432"] > ``` > 全容器形态无需此步(业务经 compose 网络服务名 `cx-postgres:5432` 直连,见 release-checklist §0.10)。 然后建 schema(**只用于全新空库**;库里已有表会拒绝执行,防止 init 里的 DROP TABLE 清数据): ```bash cd migration/ # 交付包内;仓库内则 cd docker/postgres/migration ./install.sh # 建四库 → 官方 init → 自建表 → 数据接口修正 → Nacos 存储库 schema ``` 结束会打印表数核对,应为: ``` jnpf_init BASE TABLE = 366 (官方293 + 自建73) jnpf_flow BASE TABLE = 70 jnpf_xxljob BASE TABLE = 11 nacos_config BASE TABLE = 12 ``` > 此刻库是**空 schema**,业务数据在下一步停机窗口迁入。 > schema 就绪后按 `docs/release-checklist.md` §0.3 跑 `./create-app-roles.sh` 建业务角色 > (顺序铁律 L030:先定稿 .env → 跑脚本 → 同步 Nacos datasource.yaml → 重启消费方)。 --- ## 3. 停机窗口:数据迁移 > 迁移期间需**停止对源 MySQL 的写入**(停 JNPF 业务服务),保证数据一致。 ### 3.1 时序 ``` ① 停 JNPF 业务服务(停止对源 MySQL 写入) ② cd migration/ && SRC_MYSQL_HOST=... SRC_MYSQL_PWD='***' ./migrate-data.sh all ③ 核对 migrate-data.sh 末尾抽样校验 + 跑全量 rowcount 校验(§3.3) ④ 切 Nacos datasource.yaml 到 PG(§4) ⑤ 重启 JNPF 服务,冒烟验证 ⑥ 清理临时资源:./migrate-data.sh cleanup ``` ### 3.2 `migrate-data.sh all` 做了什么 1. **dump**:从现场 MySQL `mysqldump`(`--single-transaction --hex-blob`,跳过 `base_sys_log` 大日志表)导出 `jnpf_init` / `jnpf_flow`。 2. **mirror**:起临时 `mysql:8.0` 中转容器(`mysql_native_password`),灌入 dump。 —— 为何中转:pgloader 的 qmynd 库不支持 MySQL8 握手,必须经 native_password 的 mirror。 3. **load**: - `pgloader` 灌 `jnpf_init`(排除 `base_sys_log`、`blade_visual_map`)与 `jnpf_flow`(大写 ACT/FLW 表)。 - `blade_visual_map`(巨行表,pgloader 会堆爆)走 `mysqldump --tab` → PG `COPY`(NULL=`\N` 天然兼容)。 - 重放数据接口修正 `data_interface_pg_fixes.sql`(4 条 UPDATE)。 - `lims_biz_log.log_seq` 序列 setval 到当前最大值。 - 抽样行数校验(mirror vs PG)。 源库地址与密码**必须显式传入**(不设软默认,codex review 2026-07-17);目标 PG 密码默认读 仓库根/交付包根 `.env` 的 `JNPF_PG_PASSWORD`: ```bash SRC_MYSQL_HOST=x.x.x.x SRC_MYSQL_PORT=3306 SRC_MYSQL_USER=root SRC_MYSQL_PWD='***' \ ./migrate-data.sh all ``` > 迁移产生的 `dump/` 目录含完整业务数据,验收后务必 `./migrate-data.sh cleanup` 清除。 ### 3.3 全量逐表行数校验(强烈建议) ```bash # mirror 仍在时,逐表对比 MySQL 源 vs PG(应输出「不一致 0」) MYSQL_HOST=cx-mysql-mirror MYSQL_PORT=3306 MYSQL_USER=root MYSQL_PWD_V=mirror_2026 \ MYSQL_DOCKER_NET=$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}' cx-postgres) \ PG_CONTAINER=cx-postgres \ bash sql/rowcount_check.sh jnpf_init # jnpf_flow 同理: ... bash sql/rowcount_check.sh jnpf_flow ``` --- ## 4. 切换 Nacos 数据源 `datasource.yaml` 改为 PG(示例值,现场按实际填): ```yaml jnpf: datasource: db-type: PostgreSQL host: 127.0.0.1 # PG 容器宿主 port: 5432 db-name: jnpf_init username: postgres password: 改成强密码 # 与 install 时 PG_PASSWORD 一致 ``` 改法二选一: - **控制台**:Nacos → 配置管理 → `datasource.yaml`(group=DEFAULT_GROUP、对应命名空间)→ 编辑发布。 - **API**: ```bash TOKEN=$(curl -s -X POST 'http://:8848/nacos/v1/auth/login' \ -d 'username=nacos&password=nacos' | sed 's/.*"accessToken":"\([^"]*\)".*/\1/') curl -X POST "http://:8848/nacos/v1/cs/configs?accessToken=$TOKEN" \ --data-urlencode 'dataId=datasource.yaml' \ --data-urlencode 'group=DEFAULT_GROUP' \ --data-urlencode 'tenant=<命名空间id>' \ --data-urlencode 'type=yaml' \ --data-urlencode 'content@datasource.yaml' ``` > ⚠️ **`ConfigValueUtil` 没有 `@RefreshScope`**:改完 Nacos **必须重启 JNPF 服务**才生效,不会热更新。 重启后冒烟:登录 → 菜单 → 打开若干 LIMS 台账列表(数据/中文正常)→ 文件上传预览(注意 L015:Nacos `config.ApiDomain` 要指现场网关域名)。 --- ## 5. 定时备份 ```bash # 手动跑一次确认可用(产物落 volume 外) BACKUP_DIR=/data/pg-backup ./backup.sh # crontab -e,每天 02:00 备份,保留 14 天(路径按现场实际解包位置调整) 0 2 * * * BACKUP_DIR=/data/pg-backup RETENTION_DAYS=14 /opt/jnpf/offline/migration/backup.sh >> /var/log/jnpf-pg-backup.log 2>&1 ``` 还原(custom 格式 `-Fc`,可选择性还原): ```bash docker exec -i cx-postgres pg_restore -U postgres -d jnpf_init --clean --if-exists < /data/pg-backup/jnpf_init_YYYYMMDD_HHMMSS.dump ``` --- ## 5.5 附加:Nacos 与 xxl-job-admin 存储库也切 PG(可选,建议同窗口做) > 本地已验证(2026-07-16):JNPF 定制 Nacos 与 xxl-job-admin 均原生支持 PG,切换后 MySQL 可完全下线。 ### Nacos 存储库 1. PG 建库跑 schema:`install.sh` 已把 `nacos_config` 作为第四库一并建好(含 nacos/nacos 登录种子—— 官方 mysql-schema.sql 无 INSERT,漏这步空库无法登录)。老版本单独建库的命令保留备查: ```bash docker exec cx-postgres psql -U postgres -c "CREATE DATABASE nacos_config ENCODING 'UTF8'" docker exec -i cx-postgres psql -U postgres -d nacos_config -v ON_ERROR_STOP=1 -f - < sql/nacos_config_pg.sql ``` 2. (已有 Nacos 迁移时)**先经旧 Nacos API 导出**全部 namespace + 配置(逐条 GET content 落盘)。 3. 改 `nacos/conf/application.properties`(先备份;文件里自带 Postgre 注释示例段,照抄改连接串): ```properties spring.datasource.convert=postgre # platform 保持 mysql 不动 db.url.0=jdbc:postgresql://:5432/nacos_config?currentSchema=public db.user=postgres db.password= ``` 4. 重启 Nacos,`logs/start.out` 出现 `SwitchDatabase:postgre` + `use external storage` 即成功;登录(nacos/nacos,尽快改密)→ 同 id 重建 namespace(`customNamespaceId` 参数)→ 重发配置 → 逐条 GET 回来与导出内容 diff。 5. **切换顺序(重要,实测教训)**:`建库跑 schema → 导出旧配置 → 停全部 JNPF 业务服务 → 改配置重启 Nacos → 导入配置并 diff → 按序拉起业务服务`。**不要让业务服务带着旧连接硬扛 Nacos 重启**——nacos-client 在服务端不可达窗口会泄漏 grpc 重连线程(实测 1.5h 后打满每进程 4096 上限,服务假活、注册表逐个掉线,见 tasks/lessons.md L022)。 6. 回滚:恢复 application.properties 备份 + 重启。 ### xxl-job-admin 1. `application-dev.yml` 里注释 MySQL 段、启用自带的 PostgreSQL 注释段: ```yaml datasource: url: jdbc:postgresql://:5432/jnpf_xxljob username: postgres password: driver-class-name: org.postgresql.Driver # 驱动 jar 已内置 postgresql-42.7.8 ``` 2. `server.sh restart`,登录 `/xxl-job-admin`(admin/123456 种子)验证列表/日志页正常。 3. ⚠️ **JNPF 商业 license**:admin 启动要求 `jnpf.license.licensePath`(默认 `/data/jnpfsoft/license.lic`)存在且有效(绑定硬件),缺失即退出——本地开发机无 license 未验启动,**此步必须在现场有 license 的机器上做**。 4. jnpf-scheduletask(30009) 服务本身走主 `datasource.yaml`(已是 PG),`datasource-scheduletask.yaml` 里只有 admin 地址无 DB 连接,无需改。 ## 6. 回滚方案 PG 侧数据是**新写入**,源 MySQL 在停机窗口内**未被改动** → 回滚零数据损失: 1. 把 Nacos `datasource.yaml` 切回 **MySQL 原连接**: ```yaml db-type: MySQL host: <现场源 MySQL 地址> port: 3306 db-name: jnpf_init username: <原用户> password: <原密码> ``` 2. **重启 JNPF 服务**(同样因无 `@RefreshScope`)。 3. 业务恢复到 MySQL。PG 容器可保留待排查,不影响回滚。 > 回滚判据:切 PG 后核心链路冒烟失败、或 §3.3 校验发现数据缺失且无法快速修复。 --- ## 7. 常见问题(对应 lessons.md) - **服务报 `database "jnpf_init" does not exist` 但 `docker exec psql` 能看到表**(L016):宿主机另有 PG 占了 5432,服务连错实例。`lsof -iTCP:5432 -sTCP:LISTEN` 查占用;现场应确保只有容器监听 5432。 - **脚本化登录报"不支持此登录方式"**(L017):JNPF 登录是 `form-urlencoded + AES(MD5(密码))`,不能用 JSON 裸调。 - **某在线模型 List 返回空**(L018):先查 `base_visual_release.f_web_type`,webType=1 纯表单无列表属正常,非数据丢失。 - **文件上传成功但预览/下载 502**(L015):Nacos `system-config.yaml` 的 `config.ApiDomain` 要指现场网关;改后重启 `jnpf-file`。 --- ## 8. 命令速查 | 目的 | 命令 | |---|---| | 起 PG 容器(根 compose) | `docker volume create postgres_pgdata && docker compose up -d cx-postgres` | | 建 schema(全新空库) | `./install.sh` | | 建业务角色(.env 定稿后) | `./create-app-roles.sh` | | 停机窗口迁数据 | `SRC_MYSQL_HOST=... SRC_MYSQL_PWD='***' ./migrate-data.sh all` | | 只跑 PG 侧(mirror 已就绪) | `./migrate-data.sh load` | | 全量行数校验 | 见 §3.3 | | 清理临时 mirror/dump | `./migrate-data.sh cleanup` | | 备份 | `BACKUP_DIR=/data/pg-backup ./backup.sh` | | 进 PG 命令行 | `docker exec -it cx-postgres psql -U postgres -d jnpf_init` |