ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Greenplum数据迁移实战:gpcopy工具详解与使用指南

Greenplum数据迁移实战:gpcopy工具详解与使用指南

1. 为什么数据搬运场景我最终选了 gpcopy

1.1 pg_dump 不是不能用,而是大集群里不好用

我第一次正儿八经做 Greenplum 集群搬迁,用的是 pg_dump 加 psql 的组合拳。数据库看着不大,也就两百多张表,但里面有几张事实表单表过亿行。导出的 SQL 文件占了一百多 GB,导入的时候还经常因为某个触发器、某个类型转换或者列权限问题中途报错,一旦报错就得从头再来,或者手工跳过一长串对象,人直接守在终端前不敢走开。

后来我意识到,Greenplum 这种 MPP 分布式架构,本质上不适合用传统的逻辑导出工具做全量搬运。pg_dump 是单线程方式把数据读出来再写成 SQL,数据量到了几十 GB 以上,导出本身还能忍,导入阶段才是真正的灾难:目标端只有一个会话在干活,几百个 segment 在旁边干等,单表几亿行数据导入可能要跑一整晚,中途还不敢做任何 DDL 操作。更麻烦的是,Greenplum 的不少对象属性是 pg_dump 顾不过来的,比如分布策略、分区表的边界定义,导来导去容易走样。

所以遇到跨集群迁移、新旧集群切换、测试环境数据抽取这类需求,我现在的第一反应已经不是“打开 pg_dump 手册”,而是看有没有现成的集群级复制工具。gpcopy 就是在这个背景下进入我日常工具箱的。

1.2 gpcopy 适合哪些场景,和 gpbackup 有什么区别

gpcopy 是 Greenplum 体系里专门做“实例到实例”数据复制的工具。它不是先把数据落盘成文件,再让你去目标端导入,而是直接起一个复制进程,通过源端和目标端的数据库连接把数据传输过去。你可以理解为它把“导出”和“导入”合并成了一个动作,中间少了一步落盘,也少了很多临时文件管理的麻烦。

我们团队现在用 gpcopy 主要覆盖这几类场景:

  • 集群版本升级,需要把旧集群的数据搬到新集群;
  • 机房迁移,把整套 Greenplum 从旧机器搬到新机器;
  • 从生产集群抽取部分数据库或部分表到测试集群;
  • 在两张结构几乎一样的表之间同步数据,比如换表结构后的回填;
  • 只需要复制表结构,不搬数据,用于预演或者建开发环境。

有人会问,gpbackup 不也是官方工具吗?为什么不用它?我的看法是,gpbackup 和 gpcopy 的定位确实不同。gpbackup 是标准的备份恢复工具,适合把数据备份到本地磁盘或者存储,再恢复到同一套或另一套集群,过程是“备份”和“恢复”两个独立阶段。gpcopy 则是直接面向复制场景,可以指定源端和目标端,支持选择库、schema、表,还支持只复制元数据,做增量同步也比 gpbackup 自然得多。

能力点gpcopypg_dump + psqlgpbackup + gprestore
跨实例直接复制支持需要先落盘再导入需要备份文件中转
并行复制能力支持多 job 并发基本单线程支持多线程恢复
按库、schema、表选择性复制支持支持但恢复时较繁琐支持过滤,但面向备份恢复设计
只复制表结构支持支持支持
目标表存在时处理策略可指定 drop/truncate基本靠手工可指定还原方式
日常增量同步更顺手不适合不适合

所以我的结论很直接:如果是“备份到盘里留档”,选 gpbackup;如果是“把这个集群的数据搬到那个集群去”,优先考虑 gpcopy。

2. 装 gpcopy 之前,建议先确认这四件事

2.1 版本和环境要匹配

gpcopy 一般会跟 Greenplum 安装包一起提供,不同大版本之间的命令行为、参数细节会有些差异。你在旧集群上看到的 gpcopy 用法,不一定能照搬到新集群,特别是如果源端和目标端是不同大版本,建议以两端中都较新的那个版本来准备工具,先把两端能连通的底层能力确认好。

这里说的“环境匹配”并不单指软件版本一致,而是指两端集群的角色、段主机配置不用完全一致,但你要知道自己集群的拓扑。比如源端 8 个 segment,目标端 4 个 segment,gpcopy 能不能搬?答案是能,但它做的事情不是简单把数据按原样灌进去,而是根据目标端的分布策略重新分布数据,这个过程需要额外的时间和容量。如果你事前不知道两端拓扑差异,执行到一半发现某个表的数据量总是卡住不动,就很容易误判成网络问题。

2.2 权限链路要提前打通

gpcopy 不是只在数据库层面工作,它还要在操作系统层面做一些判断和连接。常规情况下,我们建议使用两端集群的 gpadmin 用户来执行复制,因为超级用户才能读取到完整的对象定义、权限信息,以及在其他用户 schema 下建表。至少你要确认执行用户满足下面几个条件:

  • 在源端和目标端都拥有超级用户权限;
  • 能通过 psql 正常连接两端的 master 节点;
  • 如果是从跳板机执行,跳板机到两端主机的 SSH 免密登录要提前配好。

很多第一次用的人会漏掉最后一条,以为只要数据库端口能连通就万事大吉。实际上 gpcopy 在复制过程中会做一些主机层面的探测和并发调度,普通用户环境下跑起来总是怪怪的,不是少这个环境变量就是缺那个权限。我的建议是:执行之前先手动 ssh 到两端集群的 master 主机上,用 gpadmin 身份执行两次psql -c "select version();",确认链路没问题了再跑 gpcopy。

2.3 网络和端口要放通

gpcopy 的数据流不是只走一条连接。它会根据复制任务和并发参数,从执行机向两端发起多个连接,同时源端和目标端各自的 master、segment 之间也要能正常通信。所以你在防火墙或者云安全组里,不能只放通执行机到数据库 master 的 5432 端口,还要考虑两端集群内部节点之间的通信。

我遇到过一种情况:源端在 A 机房,目标端在 B 机房,安全组只允许了 A 机房某台机器到 B 机房 master 的 5432,结果复制一启动就大量报错,日志里全是连接超时。后来才发现 gpcopy 的并发连接会落到不同的 segment 主机上,而源端和目标端 segment 之间的端口没有放通。这个事在不同的网络环境里表现不一样,但排查思路是一致的:先看连接失败的主机和端口,再去核对安全策略。

2.4 目标端容量要预判

这块容易被忽略。gpcopy 做数据复制时,目标端的表会按自己的分布策略重新分布数据,不是简单把文件搬过去,所以实际占用的空间大小不能只看源端数据文件大小,还要考虑目标端压缩参数、表存储方式、分布键选择的不同。

以前有一次从一套压缩表集群搬到另一套非压缩表集群,源端数据看起来只有 2TB,结果目标端空间直接翻了快三倍。虽然 gpcopy 本身不会报错,但跑到一半目标端磁盘满了,后续所有复制任务全部失败。所以执行前建议先统计一下目标端可用的磁盘空间,尤其要留出 WAL 日志、临时文件和重建表的余量,宁可多预留 30%,也别卡在半路再腾空间。

3. gpcopy 安装与可用性验证

3.1 在现有 Greenplum 环境中找现成工具

如果你已经有一套能正常使用的 Greenplum 集群,第一步先别急着去网上下任何东西,直接在 master 节点上确认一下工具是否已经存在。命令很简单:

source /usr/local/greenplum-db/greenplum_path.sh which gpcopy ls -l $GPHOME/bin/gpcopy

正常情况下,$GPHOME/bin/gpcopy会在你初始化集群的时候就一起部署好。如果echo $GPHOME为空,说明当前 shell 没有加载 greenplum 的环境变量文件,先 source 一下再试。

我见过一些比较老的环境,GPHOME 路径是自定义的,不一定是默认路径。这时候可以用gpconfig -s gp_segment_configuration这类命令或者查看 gpadmin 用户的家目录配置来反推安装路径。只要确认了 GPHOME,再去 bin 目录下面找 gpcopy 就很容易了。

如果确实找不到,也不用慌,说明当前安装包可能精简过,或者被误删了。这时候再去获取与当前 Greenplum 版本匹配的安装介质,把工具解压出来用,而不是随便拿一个其他版本的 gpcopy 硬塞过来。

3.2 把工具放到跳板机上

并不是每台机器上都有现成的 gpcopy,实际工作中我更推荐把 gpcopy 放在一台专门的跳板机上执行。这台跳板机不用安装完整的 Greenplum 数据库软件,只要能同时访问源端集群和目标端集群的网络端口即可。

拿到匹配的安装介质后,我通常这样操作:

# 在跳板机上创建独立目录,避免污染系统路径 mkdir -p /data/tools/gpcopy # 解压安装包到该目录,包名根据实际介质调整 tar -xzf greenplum-*.tar.gz -C /data/tools/gpcopy # 找到工具位置并验证 find /data/tools/gpcopy -name gpcopy -type f # 添加执行权限 chmod +x /data/tools/gpcopy/bin/gpcopy # 把工具路径加入当前 shell 的 PATH export PATH=/data/tools/gpcopy/bin:$PATH

如果你只有集群内部机器能用,那直接放在任意一台可以被 SSH 访问的主机上也可以,只要能同时连两端数据库就行。但千万不要把 gpcopy 当成“目标端专用工具”只放在目标集群上,因为源端很多环境信息它也得去读取。

3.3 用 --help 验证环境基本可用

安装到位后,先跑一下帮助信息,这一步非常重要。它不只是让你看参数,还能顺便验证工具依赖的动态库、Python 运行环境是否正常。

gpcopy --help

如果命令能正常输出一长串参数说明,说明基础依赖没问题。如果报错提示缺少某个库或者 Python 模块,多半是当前机器的运行环境不完整,这时候优先检查 PATH 和 LD_LIBRARY_PATH 环境变量,把 Greenplum 的环境变量文件 source 进来再试:

source /usr/local/greenplum-db/greenplum_path.sh

有些版本还会提供gpcopy --version或gpcopy2 --version,不同版本命令名会有一点点差异。发现当前版本没有某个参数时,别硬记网上的旧文档,一切以实际 help 输出为准。

我在这个环节还习惯做一次“空跑”,也就是不指定实际库,只检查参数解析是否正常:

gpcopy --source-host 192.168.10.10 --source-port 5432 \ --source-user gpadmin --dest-host 192.168.10.20 \ --dest-port 5432 --dest-user gpadmin --dry-run

如果工具本身没问题,这里会提示缺少必要的数据库参数,或者直接进入预检查流程。能看到这类提示,说明安装和基础环境已经没问题,可以进入下一步配置了。

4. 核心命令参数拆解:先看懂这几个再说跑数据

4.1 基础选项:告诉 gpcopy 连哪两端

gpcopy 的核心思路就是“源端到目标端”,所以最基础的参数一定是围绕这两端展开。

gpcopy \ --source-host 192.168.10.10 \ --source-port 5432 \ --source-user gpadmin \ --dest-host 192.168.10.20 \ --dest-port 5432 \ --dest-user gpadmin \ --db sales

这些参数的含义很简单:

  • --source-host和--source-port:源端集群 master 的地址和端口;
  • --source-user:连接源端使用的数据库用户;
  • --dest-host和--dest-port:目标端集群 master 的地址和端口;
  • --dest-user:连接目标端使用的数据库用户;
  • --db或--database:要复制的数据库名称,可以指定多个。

如果源端和目标端使用同一个数据库名,直接用--db就行。如果目标端要换成另一个库名,部分版本支持--dest-db来单独指定目标端库名。这个在跨环境迁移时很实用,比如源端叫sales,目标端想叫sales_test,省得你先去目标端手工建库再改配置。

4.2 复制范围:库、schema、表是怎么圈定的

你要复制什么,决定了 gpcopy 的动作范围。最常用的是按数据库复制:

gpcopy --source-host A --source-port 5432 --source-user gpadmin \ --dest-host B --dest-port 5432 --dest-user gpadmin \ --db sales --db finance

如果数据库太大,只想搬某个 schema 下的对象,可以加--schema参数:

gpcopy --source-host A --source-port 5432 --source-user gpadmin \ --dest-host B --dest-port 5432 --dest-user gpadmin \ --db sales --schema public

如果只想搬一两张大表,加上--table指定完整的表名:

gpcopy --source-host A --source-port 5432 --source-user gpadmin \ --dest-host B --dest-port 5432 --dest-user gpadmin \ --db sales --table sales.orders --table sales.order_detail

这里有个很容易踩的细节:表名如果包含大写字母或者特殊字符,不同版本对标识符的处理逻辑不一致。我建议在执行前先确认一下你的源表命名风格。如果表名都是小写,那基本不用管;如果表里有大写字母,要么在参数里按数据库实际存储的名字写,要么提前在目标端建好完全一致的表,然后用只复制数据的方式绕过对象定义解析。

另外,--schema和--table是可以配合使用的。指定--schema以后,再限定--table,语义就是“只搬这个 schema 下的这几张表”,避免把整个 schema 都搬过去。

4.3 目标端怎么处理已有表

目标端如果已经有同名表,gpcopy 默认不会直接清掉,它需要你通过显式参数告诉它怎么处理。这是很多人第一次执行时困惑的地方:“为什么报表已存在?是不是工具出 Bug 了?”

其实不是工具出问题,而是 gpcopy 为了保证数据安全,把“是否覆盖”的决定权交给了你。常见策略有三种:

  • --create-table:目标表不存在时自动创建,已存在时按后续参数决定是否覆盖;
  • --truncate-table:先清空目标表数据,再复制新数据进去,保留表定义和相关权限;
  • --drop-table:先删除目标表,再按源端定义重建。

我在做全量迁移时,如果源端和目标端表结构完全一致,通常直接加--drop-table --create-table,让工具按源端结构重建。如果只是刷数据、表结构不能动,那就用--truncate-table。如果目标端表还存在其他依赖视图或外部引用,建议提前处理,不要随手--drop-table。

这里还要注意,--create-table只是允许建表,不等于一定重建。具体行为还要结合--drop-table和--truncate-table来看。实际使用时建议先用--dry-run把计划跑一遍,确认它会对哪些表执行什么动作,心里有底再正式执行。

4.4 并发和校验参数

复制大表时,并发数直接影响速度。gpcopy 支持多 job 并行复制,效果类似于同时开多个复制线程,每个 job 负责不同的对象或表。

gpcopy --source-host A --source-port 5432 --source-user gpadmin \ --dest-host B --dest-port 5432 --dest-user gpadmin \ --db sales --jobs 8

--jobs不是越大越好。它取决于执行机到两端集群的网络带宽、两端 master 的 CPU 处理能力、目标端磁盘写入速度。我自己的习惯是从 4 开始试,观察一段时间日志里每个表的复制速率,如果 CPU 和网络都没打满,再逐步往上加。我见过有人直接把 jobs 调到 32,结果目标端 master 的 CPU 先被打满了,复制速度反而比 jobs=8 更慢。

--dry-run是我建议每次正式执行前必加的参数。它不会真正传输数据,只会把要复制的对象清单、目标端动作、潜在冲突都列出来,相当于预演一遍。做复杂迁移时先跑一次 dry-run,能省去很多后知后觉的麻烦。

还有一类参数是控制对象范围的高级选项,比如只复制表结构、只复制数据:

  • --metadata-only:只复制建表、索引、视图、函数等定义,不复制实际数据;
  • --data-only:只复制数据,前提是目标端表已经存在,且字段结构兼容;
  • 两个都不加:默认同时复制结构和数据。

这套组合在分批迁移时特别有用。比如你先用--metadata-only把结构全部建好,让开发同学可以在新环境提前看表结构,再挑业务低峰期用--data-only去搬数据,整个过程拆得清清楚楚。

5. 一次完整迁移演练:从 dry-run 到增量同步

5.1 场景假设和前置检查

假设现在要把一套旧集群的数据搬到新集群,只搬sales库,包含public和dw两个 schema。新集群的 segment 数比旧集群少一半。业务方允许停机一个晚上,但希望数据最终保持一致。

我先在跳板机上把环境变量和 PATH 准备好,然后检查两端连通性:

psql -h 192.168.10.10 -p 5432 -U gpadmin -d postgres -c "select 1;" psql -h 192.168.10.20 -p 5432 -U gpadmin -d postgres -c "select 1;"

两步都能通,再去看目标端是否已经有sales库。如果还没有,可以用createdb在目标端先建一个空库,或者让 gpcopy 在建表时连带处理。我习惯先把空库建好,因为这样后面执行--data-only时会省掉很多权限相关的报错。

5.2 第一阶段:只搬结构

我的习惯是先搬结构,不搬数据。这样做有两个好处:一是结构问题越早暴露越好,二是可以提前核对目标端的表分区、分布键是否符合预期。

gpcopy --source-host 192.168.10.10 --source-port 5432 --source-user gpadmin \ --dest-host 192.168.10.20 --dest-port 5432 --dest-user gpadmin \ --db sales --schema public --schema dw \ --metadata-only --jobs 4 --dry-run

先加--dry-run看计划,确认没有理解错参数后,把--dry-run去掉正式执行。这个阶段通常很快,因为不传数据,只是建对象。日志里如果出现某个对象无法创建,先记下来,别急着忽略,后面数据复制时往往会再次遇到。

结构搬完后,在目标端跑一次:

psql -h 192.168.10.20 -p 5432 -U gpadmin -d sales -c "\dt"

看看表数量是否和源端一致,重点看大表和分区表的分布键,确认结构没有偏差。

5.3 第二阶段:搬迁数据

结构确认没问题后,再跑数据。目标端表已经存在,结构也和源端一致,所以我采用--data-only加--truncate-table的方式,防止重复执行时把数据叠加。

gpcopy --source-host 192.168.10.10 --source-port 5432 --source-user gpadmin \ --dest-host 192.168.10.20 --dest-port 5432 --dest-user gpadmin \ --db sales --schema public --schema dw \ --data-only --truncate-table --jobs 8

执行过程中我会另外开一个窗口,观察目标端 master 的负载和磁盘空间:

top -u gpadmin df -h /data

如果某张表特别大,复制时间明显过长,我可以随时中断整个任务,但它不会把所有工作都丢掉。等网络或者资源调整好以后,重新执行同样的命令,配合--truncate-table再跑一轮,已经完成的小表会快速跳过,没完成的表会重新复制。

这里想多说一句:gpcopy 的“增量同步”思路不能简单理解成每天都自动只搬差异数据,它更多是通过“全量复制+清空重建”来保证目标端和源端最终一致。如果你的场景是每天都在生产库上高频写入,又想持续同步到另一套集群,建议先评估源端表的写入模式和批次,再决定能不能把同步窗口压缩到足够短。分批按表复制、错峰执行,往往是更稳的方案。

5.4 第三阶段:校验与收尾

数据复制完,不代表万事大吉。我至少会做两层校验。

第一层是行数校验。挑几张大表,在两端分别执行:

select count(*) from sales.orders; select count(*) from sales.orders_detail;

如果两端 count 一致,说明大方向没错。

第二层是抽样校验。我会用md5或者sum聚合的方式比对关键表的字段指纹,避免出现行数一致但数据内容错位的情况。

select md5(string_agg(t.id || '|' || t.amount, ',' order by id)) from sales.orders t;

这个脚本只是示例,实际字段要根据表结构调整。如果两端指纹一致,这次迁移的数据质量基本就有保证了。

最后再做业务侧验证:让应用连目标库跑一遍常用查询,确认没有锁冲突、权限缺失或者视图引用错误。等业务验证通过后,再把源端集群停写或者下线,整个迁移才算真正结束。

6. 实操中踩过的坑和排查方法

6.1 “表不存在”的引号坑

有一次复制一张中间表,表名是类似ods_Order_2024这种混合大小写的名字。我在命令行里按直觉写了完整表名,结果 gpcopy 一直报找不到这个表。

原因是数据库在存储表名时,未加引号的标识符会统一转成小写。如果源表当初是用加引号的 DDL 创建的,它在系统表里存的名字就是混合大小写。这时候你在参数里写普通名字,工具会把它转成小写再去查,自然查不到。

解决办法很直接:查一下系统表里实际存储的对象名,严格按那个名字传参,或者提前在目标端把表建好,改为--data-only绕过建表逻辑。以后遇到含特殊字符的表名,我都建议先把对象清单导出来,确认系统表里的relname是什么,再写进 gpcopy 参数。

6.2 段数不一致导致的异常

旧集群 16 个 segment,新集群 8 个 segment,复制普通表没感觉,但复制一张分布键设计不合理的大表时,某些 segment 上的数据量明显不均,部分节点写入特别慢,整体进度被拖住。

一开始我以为是网络带宽问题,检查了两端网卡流量都很健康,后来才发现是表在目标端的分布策略导致数据倾斜。gpcopy 本身能正常完成数据搬迁,但它不会帮你优化分布键。遇到这种情况,正确的做法是在数据复制前,先在目标端重新设计分布键,或者用--dest-map这一类的映射参数来适配两端拓扑差异。

所以我现在做跨集群迁移前,都会先拉一份所有大表的分布键清单,逐张评估在目标端是否会产生倾斜。如果倾斜严重,就在结构复制阶段手工调整建表脚本,而不是等数据进去以后再来救。

6.3 连接被拒和认证失败

gpcopy 报错里最常见的是连接类错误,比如could not connect to server和password authentication failed。前者多半是网络或端口问题,后者多半是授权文件没配好。

我现在排查这类问题会按这个顺序走:

  1. 先在执行机上用psql直接连两端数据库,看能不能通;
  2. 如果不能通,检查源端和目标端的pg_hba.conf,确认执行机 IP 是否在允许列表里;
  3. 确认数据库用户密码或者 SSH 免密配置是否对当前执行用户生效;
  4. 确认两端数据库当前都能接受新的连接,没有因为连接数满而拒绝。

gpcopy 作为一个调度型工具,对连接的稳定性要求比普通 psql 更高,所以它失败时,日志里的连接报错往往只是一个表象,真正的原因可能是目标端并发连接数把 master 打满了。我在大并发复制前会调大max_connections或者降低--jobs值,很多莫名其妙的连接失败问题都能解决。

6.4 大表复制中断后的恢复思路

复制过程中如果网络抖动,大的表可能复制到一半就断掉。我刚用 gpcopy 时遇到这种情况会特别慌,怕数据混乱,实际上工具的处理方式比我想象中稳。

它通常会记录已经完成的表,失败的表会保留在工作队列里。重新执行相同的复制命令时,已经完成的表会跳过,未完成的表从零开始重来。如果你用了--truncate-table,那已经写进目标端的半截数据会被清掉再重灌,不会出现新旧数据混在一起的情况。

如果不想全部重来,也可以把已经成功的表从命令里去掉,只保留失败的表,缩小任务范围。这个思路在数据量大、失败面小时特别管用。不过要注意,去掉表名时别把 schema 搞混了,否则很容易复制错对象。

最后补充一个我的个人习惯:每次执行 gpcopy 前,先把当次执行的完整命令行和预期目标写在一个文本文件里,放在执行目录下,命名为 migration_note。等迁移结束或者问题排查时,翻出来对照日志,一眼就能看出当时选了哪些参数、为什么这么选。工具虽然好用,但真正让我少踩坑的是这套“事前确认、事后复盘”的流程,而不是某个单一命令的魔力。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进