本系列文章将围绕东南亚头部科技集团 GoTerra 的真实迁移历程展开,逐步拆解 BigQuery 迁移至 MaxCompute 过程中的关键挑战与技术创新。本篇为第10篇,MMS如何助力GoTerra实现BigQuery到MaxCompute 50PB数据迁移
注:客户背景为东南亚头部科技集团,文中用 GoTerra 表示。
2025年6月,随着 GoTerra 集团最后一个GCP BigQuery分区 19.5 GB 的数据迁移到阿里云MaxCompute,历时 6 个月、总量高达 50PB 的数据迁移项目终于圆满收官。
这是一个跨越大洲(从美国到印尼)、涉及10 多个公司实体、数十个project、数十万张table的大规模迁移工程,目标是在仅 6 个月内完成迁移——任务艰巨,挑战空前。
这不是一次传统的全量同构迁移,还包含增量异构迁移,对迁移工具、数据处理能力、网络传输、协同机制都提出了极高要求。
而这一切,都由为大数据迁移而生的 MMS(MaxCompute Migration Service) 轻松承载。
1、项目背景与挑战
GoTerra集团把大数据业务从BigQuery迁移到MaxCompute,项目涉及非常广泛:
- 数据规模:按BigQuery logical size统计,大约50PB
- 数据对象:大约70个Project、10万张table
- 业务范围:10+个Account,每个Account背后是一个子公司实体
- 项目节奏:2025年1月启动迁移,6月底完成
除此之外,还有技术上的挑战:
- DataType复杂: 客户大量使用Array、JSON和Struct类型,其中存在多层Struct类型嵌套的情形,还使用到Decimal、timestamp、datetime等类型
- 分区策略多样: BigQuery支持非常灵活的分区策略,比如Time-unit column partitioning和Ingestion time partitioning
- 迁移速度要求高: 6个月迁移50PB,不出任何差错的情况下,至少每周迁移2PB。因此每周3PB才有较大容错空间
- 迁移灵活调度: 需要满足不同业务方多样化优先级需求
- API first:所有迁移功能,必须以API的形式提供给GoTerra客户
2、对象迁移方案
2.1 数据对象映射
BigQuery采用三层对象模型:Project -- Dataset -- Table
因此迁移到MaxCompute,继续保持三层模型:Project -- Schema -- Table
| 源对象 | 目标对象 |
|---|---|
| BigQuery Project | MaxCompute Project |
| BigQuery Dataset | MaxCompute Schema |
| BigQuery Table | MaxCompute Table |
2.2 数据类型映射
| BigQuery 数据类型 | MaxCompute数据类型 | 备注 |
|---|---|---|
| Boolean | Boolean | |
| Bytes(L) | Binary(L) | |
| Date | Date | |
| Datetime | Timestamp_NTZ | 由于BigQuery Datetime 是non-timezone, 且精度到10-6秒 MaxCompute Timestamp_NTZ也是一种non-time zone的数据类型,精度到精度到10-9秒 |
| JSON | JSON | JSON |
| INT64 SQL aliases: INT, SMALLINT, INTEGER, BIGINT, TINYINT, BYTEINT | bigint | |
| NUMERIC SQL aliases: DECIMAL | Decimal(38, 9) | BigQuery NUMERIC 默认精度: + precision=38, + scale=9 |
| DECIMAL(p[,s]) | DECIMAL(p[,s]) | BigQuery精度 : Maximum scale range: 0 ≤ S ≤ 9 Maximum precision range: max(1, S) ≤ P ≤ S + 29 MaxCompute精度 : Maximum scale range: 0 ≤ S ≤ 30 Maximum precision range: max(1, S) ≤ P ≤ 38 因此可以MaxCompute DECIMAL可以完全兼容BigQuery DECIMAL类型 |
| BIGNUMERIC SQL aliases: BIGDECIMAL | Decimal(p,s) | BigQuery BIGNUMERIC默认精度: + precision=76, + scale=38 由于MaxCompute 没有BIGDECIMAL类型, 同时以上默认精度也超出了MaxCompute DECIMAL类型的值域, 因此对于BigQuery BIGNUMERIC类型,需要由客户根据自己的数据特征,来指定目标精度 |
| BIGDECIMAL(p[,s]) | DECIMAL(p[,s]) | 同上,需要客户保证数据特征,指定目标精度 |
| FLOAT64 | Double | Double |
| Range | String | MaxCompute没有Range类型,因此转为String |
| String | String | String |
| Struct | Struct | Struct |
| Time | Bigint | 计算出 00:00:00.000000的offset 整数值value,以微秒为单位 |
| Timestamp | Timestamp_NTZ | 由于前期沟通的失误,迁移团队把BigQuery timestamp映射到MaxCompute Timestamp 其实合理的映射应该是Timestamp_NTZ |
| Geography | String | MaxCompute没有Geography类型,因此转为String |
| Interval | String | INTERVAL_YEAR_MONTH / INTERVAL_DAY_TIME |
| Array | Array | MaxCompute不支持 T=JSON 的情况, MMS把它转成Array,且它的element value以 jsonString形式存在 |
2.3 分区策略映射
BigQuery提供非常丰富的分区策略
2.3.1 Integer range partitioning
- 按指定integer column的range 分区
- 由于客户没用到该分区策略,MaxCompute没有支持
2.3.2 Time-unit column partitioning
- MaxCompute通过 auto partitioned 机制 生成一个伪列来等价实现BigQuery Time-unit 分区效果
- DDL demo如下:
| Time-Unit | BigQuery DDL | MaxCompute DDL |
|---|---|---|
| daily | create table test_table (id int64, d date) partition by d; | create table test_table (id bigint, d date) AUTO PARTITIONED BY (trunc_time(d, 'day') as _partition_value); |
| monthly | create table test_table (id int64, d date) partition by date_trunc(d, MONTH); | create table test_table (id bigint, d date) AUTO PARTITIONED BY (trunc_time(d, 'month') as _partition_value); |
2.3.3 Ingestion time partitioning
- BigQuery在处理Ingestion time partitioning时,通过在insert record时,自动记录insert timestamp,并存储在 伪列_PARTITIONTIME上
- MaxCompute通过auto partitioned 来实现BigQuery的Ingestion time分区效果
- DDL demo 如下:
| Time-Unit | BigQuery DDL | MaxCompute DDL |
|---|---|---|
| hourly | create table test_table (a string) PARTITION BY TIMESTAMP_TRUNC(_PARTITIONTIME, HOUR); | create table test_table(a string, _partitiontime timestamp_ntz) auto partitioned by (trunc_time(_partitiontime, 'hour') as _partition_value); |
| daily | create table test_table (a string) PARTITION BY _PARTITIONDATE; | create table test_table(a string, _partitiontime timestamp_ntz) auto partitioned by (trunc_time(_partitiontime, 'day') as _partition_value); |
3、迁移系统架构
迁移系统在技术选型上
3.1 方案一

- 先把BigQuery table数据dump到GCS
- 再通过传输工具把GCS文件传到OSS
- 最后使用MaxCompute external table技术把OSS文件导入
3.2 方案二

基于BigQuery Read API,通过MaxCompute Spark直接读BigQuery数据,直接写入MaxCompute
3.3 方案选型
综合考虑了易用性、成本与架构简洁性,MMS选择方案二。
3.4 网络专线方案

4、迁移关键技术
提供对象映射能力只是“万里长征第一步”,在此之上还要解决迁移速度、迁移原子性、优先调度、Column-Reorder-Fillback、双跑追增量等关键问题。
4.1 迁移速度优化
为了做到每周3PB的迁移速度,MMS主要从以下三方面着手:
- 网络带宽层面,需要专线保障,规划50Pbs
- 迁移系统读BigQuery数据时,启用压缩
- 为Spark迁移任务预备了数千CU计算资源,随时待命
经过以上优化后,迁移专线带宽常态化接近用满的状态

每周3PB的目标也很轻松达到,顶峰时期,单天跑出1.92PB的极限速度。

4.2 迁移原子性
客户有一天在群里问我们,有个partition明明有数据,为什么有时查不到数据。
经过分析发现,MMS在迁移的时候,采用的做法是
drop if exists partition
create if not exits partition
insert into ... select ...
以上流程明显有问题,在drop和commit 之间,从数据使用者角度 这个partition就是没数据的。
假如这个时候迁移系统因为网络或pod资源问题,可能会造成更大时间跨度的不一致。
因此,一种具备原子性的迁移方案呼之欲出:
insert overwrite ... select ...
由于MaxCompute只支持 statement 级别的原子性,insert overwrite 语句的效果,相当于DBMS
begin;
drop if exists partition
create if not exits partition
insert into ... select ...
commit;
4.3 优先调度
GoTerra客户内部有上百个业务团队,他们对不同table、partition迁移紧迫性是不一样的。
- 有的部门说,我今天就要在MaxCompute用到这些数据,越快越好
- 有的部门说,这批数据,我这几天还不需要的,但是我下周一定要用到
- 有的部门说,这批数据,量非常大,我几个月后才需要
由于上述第1类场景的存在,GoTerra客户负责数据迁移的平台团队,一开始每天向内部收集迁移需求,然后向MMS提交一批迁移任务。要确保这批任务全部完成后,他们才会启动下一批迁移任务。
有时客户内部有紧急迁移需求,还不得不暂停正在迁移的任务,让MMS优先处理紧急的。
等到这批紧急的迁移完成后,再人工重启刚刚暂停的任务。
沟通成本非常高!
这样运行了一周后,MMS团队被客户搞崩溃了,于是设计了基于优先级的调度功能。
这里的优先级priority不是传统的自然数 ( 比如 0~9 ) ,毕竟确定priority 每个value的语义,也不是容易的事。
MMS团队借用了平时排需求的**ETA(Estimated Time of Arrival)**概念,作为实际的优先级定义,无论是客户,还是MMS团队,都很容易理解哪天需要完成这个迁移任务。
有了基于ETA优先调度能力后,客户就可以更好的统筹迁移任务,并且MMS可以做到7*24全天候迁移。
4.4 Column-Reorder-Fillback
为什么会出现column reorder
GoTerra客户存在一批实时写入的streaming表,数据架构大致如下:

Kafka系统的message 是带 record schema的
客户自建了一个X-Flink系统(类似Flink的流计算系统),根据上游kafka的message,按需执行add column。
对于这一类streaming表,靠迁移BigQuery端的数据,是永远无法把X-Flink系统迁移到MaxCompute的
因此迁移过程,源端streaming表与目标端streaming表将在一段时间同时被写入的情况,即双跑局面:

- 1 实时系统 推送消息到kafka
- 2 GCP侧 X-Flink 拉取 kafka数据
- 3 GCP侧 X-Flink 实时写入GCP侧Streaming表-A
- 4 阿里云侧 X-Flink 拉取 kafka数据
- 5 阿里云侧 X-Flink 实时写入阿里云Streaming表-B
- 6 GCP侧Streaming表-A Fillback 数据到阿里云Streaming表-B
在双跑期间,由于各种各样的原因,阿里云Streaming表与GCP侧Streaming表数据将存在差异
因此需要步骤6 Fillback 来修正两边Streaming表的差异。
同时,由于步骤3与步骤5 执行add column的时机不同,就导致表A与表B的字段顺序不同。
如何处理Column reorder Fillback
- 对于基本类型的字段,column reorder fillback 是比较简单的
- 对于Struct嵌套类型的字段,需要逐级对sub field做旋转操作
以struct 类型为例,
create table a(
c1 struct<c1_1:String,c1_2:struct<c1_2_1:String,c1_2_2:String>,c1_3:String>
);
create table b(
c1 struct<c1_3:String,c1_2:struct<c1_2_2:String,c1_2_1:String>,c1_1:String>
);
以下是column reorder的fillback 过程如下:

4.5 双跑期间增量数据迁移优化
双跑期间,GoTerra客户几乎所有的ETL都要在BigQuery 与 MaxCompute两边同时运行。
为了能在MaxCompute侧运行当日ETL,需要提前准备好对应的上游数据分区。
客户要求MMS在2小时内把这些分区的数据从BigQuery迁移到MaxCompute。
为此,MMS系统对任务调度算法进一步升级。
为了方便描述,下文把SLA=2小时的迁移任务,称为紧急迁移任务。
原有的调度算法
原来调度 priority(ETA)只在 Project内生效,即不同的Project的迁移任务完全隔离。
如下图,MMS 会从每个Project中挑选 ETA最小的task执行

如上图,Project A有4个紧急迁移任务,ETA都在2024年5月份,
Project B有4个普通迁移任务,ETA都在2025年5月份。
此时 task1与task5同时被调度执行。
客户双跑第一天,紧急迁移任务平均完成时间超过6小时,远低于客户要求。
改进后的调度算法
只要把Prority改为全局调度就解决了客户问题

改进后,紧急任务的平均完成时间在30分钟以内,顺利保障了客户双跑期间对上游分区的紧急同步需求。
5、未来规划
在完成 GoTerra客户迁移后,MaxCompute与MMS产品依然在演进:
- 未来将全面对齐BigQuery数据类型,尤其是Geography与BigDecimal
- MMS将支持 View的迁移
- 支持更智能的调度算法,动态感知带宽并调整迁移任务资源分配