亚马逊AWS官方博客

把 RDS MySQL 数据入仓 Amazon Redshift 的三种方式 · 首推 Zero-ETL

摘要:从”手工搬 CSV”到”零代码近实时同步”,围绕同一个 RDS for MySQL → Redshift Serverless 场景,本文完整走通三条主流路径,对比它们的架构、成本、运维与延迟,并给出选型建议。


一、写在前面:一个典型的私有网络场景

本文假设一个常见的生产形态:RDS for MySQL、跳板 EC2、Amazon Redshift Serverless 全部部署在同一个 VPC 的私有子网中,均不对公网开放。这决定了三种方案的网络前提,也是很多”坑”的根源。

二、方式一:S3 + COPY 批量加载 [批量 · 手工]

最经典、门槛最低的做法:把源数据导成 CSV 放到 Amazon S3,再用 Redshift 的 COPY 命令并行加载。因为源库在私有子网、本地连不上,可借一台同 VPC 的 EC2(用 SSM)执行 SQL 并导出 CSV。

[图 1]

2.1 关键命令

步骤 要点
建表 id 用普通 BIGINT(不要 IDENTITY),与 CSV 列一一对应
加载 COPY user_test (id,name,email,age,created_at) FROM 's3://...' IAM_ROLE '...' CSV IGNOREHEADER 1 REGION 'us-east-1';
入口 Query Editor v2 里选 Load from S3 bucket(数据已在 S3 时最省事)

2.2 Step by step

1. 准备一个可读 S3 的 IAM 角色,并关联到 Redshift

COPY 是 Redshift 自己去读 S3,所以权限挂在 Redshift 端的角色上,而不是你的登录身份。

    • 建角色:受信实体选 Redshift,策略给目标桶前缀的 s3:GetObject / s3:ListBucket
    • 关联:Redshift 控制台 → Namespace → Security and encryption → Associate IAM role。

2. 准备源数据(若源库已有数据可跳过)

CREATE TABLE user_test (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  name       VARCHAR(50)  NOT NULL,
  email      VARCHAR(100) NOT NULL,
  age        INT          NOT NULL,
  created_at DATETIME     NOT NULL
);

INSERT INTO user_test (name, email, age, created_at)
WITH RECURSIVE seq AS (
  SELECT 1 AS n UNION ALL SELECT n+1 FROM seq WHERE n < 100
)
SELECT CONCAT('user_', LPAD(n,3,'0')),
       CONCAT('user_', LPAD(n,3,'0'), '@example.com'),
       FLOOR(18 + RAND()*42),
       NOW() - INTERVAL FLOOR(RAND()*365) DAY
FROM seq;

源库在私有子网、本地连不上时,用同 VPC 的一台 EC2 经 Systems Manager Session Manager 执行,无需开放任何公网入站。

3. 从 MySQL 导出为分隔文件

# --batch 输出制表符分隔,且第一行为表头
mysql -h <rds-endpoint> -u <user> -p <db> --batch --raw \
  -e "SELECT id,name,email,age,created_at FROM user_test" \
  > user_test.tsv

head -2 user_test.tsv    # 确认表头 + 首行数据

若字段内可能含制表符、换行或引号,别用文本拼装,改用真正的 CSV 导出(例如 MySQL Shell 的 util.exportTable),再用 COPY … CSV 加载。

4. 上传到 S3

aws s3 cp user_test.tsv s3://<your-bucket>/ingest/user_test.tsv --region us-east-1
aws s3 ls s3://<your-bucket>/ingest/

5. 在 Redshift 建目标表

关键:id 用普通整型而不是 IDENTITY——否则 COPY 会跳过该列导致整体列错位。

DROP TABLE IF EXISTS public.user_test;
CREATE TABLE public.user_test (
    id         BIGINT,
    name       VARCHAR(50),
    email      VARCHAR(100),
    age        SMALLINT,
    created_at TIMESTAMP
)
DISTSTYLE AUTO
SORTKEY (created_at);

6. 执行 COPY 加载

两条铁律:显式写列清单 + IGNOREHEADER 1

COPY public.user_test (id, name, email, age, created_at)
FROM 's3://<your-bucket>/ingest/user_test.tsv'
IAM_ROLE '<redshift-copy-role-arn>'
DELIMITER '\t'
IGNOREHEADER 1
REGION 'us-east-1';

-- 若是标准 CSV,把 DELIMITER 换成 CSV:
-- CSV IGNOREHEADER 1

7. 验证与排错

SELECT COUNT(*) FROM public.user_test;          -- 应等于源表行数
SELECT * FROM public.user_test ORDER BY id LIMIT 5;

-- 加载失败时看逐行错误原因
SELECT * FROM sys_load_error_detail
ORDER BY start_time DESC LIMIT 10;

2.3 ✅ 优点

  • 概念简单,任何数据源导出 CSV 即可
  • COPY 并行加载,大批量吞吐高
  • 不依赖额外托管组件

2.4 ⚠️ 代价 / 坑

  • 全手工、无增量,靠脚本反复搬
  • 列错位:不写列清单 + 遇到 IDENTITY 列 → Invalid digit, Value 'u'
  • 本地文件加载需暂存桶:A staging S3 bucket is required
  • QEv2 默认并发会话上限 3:current limit of 3 sessions
网络提醒:若 Workgroup 开了 Enhanced VPC Routing 而 VPC 内没有 S3 Gateway Endpoint,COPY 读 S3 的流量会绕 NAT 出去。生产建议补一个 S3 Gateway VPC Endpoint:更安全、省 NAT 流量费、避免大批量加载时 NAT 成为瓶颈。

三、方式二:AWS Glue ETL [微批 · 分钟级]

用 Glue(Spark)把 MySQL 读出来写进 Redshift,配合 Job Bookmark 做增量、定时触发器逼近”近实时”(分钟级)。Glue Job 会在你指定连接的子网里创建 ENI 跑 Spark,写 Redshift 时先落 S3 临时目录(TempDir)再 COPY

[图 2]

最重要的一课——网络:连接一定要放私有子网 + NAT,放到公有子网(只有 IGW)会失败:Failed to assume the customers role. Verify that your VPC has access to STS.(Glue ENI 无公网 IP,走不了 IGW,连不上 STS)。或者用 JDBC 连接直接填 SECRET_ID、不走 ROLE_ARN,就不需要 STS。

3.1 Step by step

1. 前置准备

    • S3 临时目录(TempDir):写 Redshift 时 Glue 用它做 COPY 中转,必须有。
    • IAM 角色给 Glue Job:托管策略 AWSGlueServiceRole(含在 VPC 建 ENI 的权限)+ TempDir 桶的读写 + 两个数据库 Secret 的 secretsmanager:GetSecretValue。生产按最小权限收敛。
    • 数据库凭证放 Secrets Manager,不要硬编码在脚本或连接里。

2. 先在 Redshift 建好目标表

让 Glue 自动建表会拿不准类型和长度,先手工建更可控:

CREATE TABLE IF NOT EXISTS public.address_glue (
    id         INTEGER NOT NULL,
    street     VARCHAR(255),
    city       VARCHAR(100),
    state      VARCHAR(100),
    zip_code   VARCHAR(20),
    country    VARCHAR(100),
    created_at TIMESTAMP,
    PRIMARY KEY (id)
) DISTSTYLE AUTO SORTKEY (id);

源表名若带连字符(如 user-test),Redshift 侧建议改成下划线,否则每次都要写双引号。

3. 创建 MySQL 连接(JDBC)

直接给 SECRET_ID、不走 ROLE_ARN,就不需要访问 STS,最省心:

aws glue create-connection --region us-east-1 --connection-input '{
  "Name": "glue-mysql-conn",
  "ConnectionType": "JDBC",
  "ConnectionProperties": {
    "JDBC_CONNECTION_URL": "jdbc:mysql://<rds-endpoint>:3306/<db>",
    "JDBC_ENFORCE_SSL": "true",
    "SECRET_ID": "<mysql-secret-arn>"
  },
  "PhysicalConnectionRequirements": {
    "SubnetId": "<private-subnet-id>",
    "SecurityGroupIdList": ["<glue-sg-id>"],
    "AvailabilityZone": "us-east-1b"
  }
}'

 ⚠️ SubnetId 必须是私有子网(0.0.0.0/0 → NAT)。

4. 创建 Redshift 原生连接(v2)

aws glue create-connection --region us-east-1 --connection-input '{
  "Name": "glue-redshift-native-conn",
  "ConnectionType": "REDSHIFT",
  "ConnectionProperties": {
    "HOST": "<workgroup>.<account>.us-east-1.redshift-serverless.amazonaws.com",
    "PORT": "5439",
    "DATABASE": "dev",
    "ROLE_ARN": "<glue-job-role-arn>"
  },
  "AuthenticationConfiguration": {
    "AuthenticationType": "BASIC",
    "SecretArn": "<redshift-secret-arn>"
  },
  "PhysicalConnectionRequirements": {
    "SubnetId": "<private-subnet-id>",
    "SecurityGroupIdList": ["<glue-sg-id>"],
    "AvailabilityZone": "us-east-1b"
  },
  "ValidateForComputeEnvironments": ["SPARK"]
}'
# 创建返回 IN_PROGRESS,几十秒后应变 READY,READY 才能用
aws glue get-connection --region us-east-1 \
  --name glue-redshift-native-conn --query 'Connection.Status'

两个要点:ValidateForComputeEnvironments 对 v2 连接是必填;Redshift 的安全组要放行 Glue 安全组的 5439

5. (可选)用 Crawler 探查源表结构

aws glue create-database --region us-east-1 \
  --database-input '{"Name":"mysql_src"}'
aws glue create-crawler --region us-east-1 \
  --name src-mysql-crawler --role <glue-job-role> --database-name mysql_src \
  --targets '{"JdbcTargets":[{"ConnectionName":"glue-mysql-conn","Path":"<db>/%"}]}'
aws glue start-crawler --region us-east-1 --name src-mysql-crawler

6. 编写并上传 Glue 脚本

import sys
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job
args = getResolvedOptions(sys.argv, ["JOB_NAME"])
sc = SparkContext()
glueContext = GlueContext(sc)
job = Job(glueContext)
job.init(args["JOB_NAME"], args)
# 读 MySQL 源
src = glueContext.create_dynamic_frame.from_options(
    connection_type="mysql",
    connection_options={
        "useConnectionProperties": "true",
        "connectionName": "glue-mysql-conn",
        "dbtable": "address",
    },
    transformation_ctx="src",          # 开书签必须有 ctx
)
# 写 Redshift 目标
glueContext.write_dynamic_frame.from_options(
    frame=src,
    connection_type="redshift",
    connection_options={
        "useConnectionProperties": "true",
        "connectionName": "glue-redshift-native-conn",
        "database": "dev",
        "dbtable": "public.address_glue",
        "redshiftTmpDir": "s3://<your-bucket>/temporary/",
    },
    transformation_ctx="dst",
)
job.commit()
aws s3 cp mysql-to-redshift.py s3://<your-bucket>/scripts/mysql-to-redshift.py

7. 创建 Job(先全量 APPEND 跑通)

aws glue create-job --region us-east-1 \
  --name mysql-to-redshift-sync \
  --role <glue-job-role-arn> \
  --command '{"Name":"glueetl","ScriptLocation":"s3://<your-bucket>/scripts/mysql-to-redshift.py","PythonVersion":"3"}' \
  --connections '{"Connections":["glue-mysql-conn","glue-redshift-native-conn"]}' \
  --default-arguments '{
      "--job-language":"python",
      "--TempDir":"s3://<your-bucket>/temporary/",
      "--enable-metrics":"true",
      "--enable-continuous-cloudwatch-log":"true",
      "--job-bookmark-option":"job-bookmark-disable"
  }' \
  --glue-version "4.0" \
  --worker-type G.1X --number-of-workers 2 \
  --max-retries 0 --timeout 20

调试期务必先 job-bookmark-disable + APPEND,别一上来就配自定义 MERGE。

8. 运行、盯状态、验证

RUN_ID=$(aws glue start-job-run --region us-east-1 \
          --job-name mysql-to-redshift-sync --query JobRunId --output text)
# 期望 RUNNING → SUCCEEDED;失败时 ErrorMessage 里有根因
aws glue get-job-run --region us-east-1 \
  --job-name mysql-to-redshift-sync --run-id "$RUN_ID" \
  --query 'JobRun.[JobRunState,ErrorMessage]'

详细日志在 CloudWatch 日志组 /aws-glue/jobs/output 与 /aws-glue/jobs/error。跑通后在 Redshift SELECT COUNT(*) 对齐源表行数,再进入下一节做增量。

3.2 增量与调度

  • Job Bookmark:--job-bookmark-option job-bookmark-enable,用递增列(id/created_at)做书签键,只读新增行
  • MERGE upsert:目标节点选 Merge data into target table,Matching keys = id,可更新已存在行。
  • 定时:cron(0/5 * * * ? *) 每 5 分钟触发一次,配合书签即近实时。

3.3 ✅ 优点

  • 能做复杂转换 / 清洗 / 多源 join
  • Bookmark + MERGE 支持增量 upsert
  • 命令行可复现、可版本化

3.4 ⚠️ 代价 / 坑

  • 组件多、要维护连接 / 脚本 / 触发器 / IAM
  • 只能到分钟级,非秒级 CDC
  • 捕获不到源库物理 DELETE
  • 源节点 schema 未推断 → no output schema;源表/目标/MERGE 三者不一致会报错

四、方式三:Zero-ETL 集成 [近实时 · 零代码] ★ 推荐

Amazon RDS for MySQL 与 Amazon Redshift 的 Zero-ETL 集成是完全托管的近实时复制:底层读 MySQL binlog,无需你写任何 ETL 代码、不维护任何计算资源。建好集成后,源库的插入/更新/删除会在约 1 分钟内同步到 Redshift。

[图 3]

4.1 前置条件速览

前置 设置位置
源库启用自动备份 RDS → Modify → Backup retention > 0
binlog_format=ROWbinlog_row_image=full RDS 自定义参数组(需重启)
被同步的表必须有主键 建表时定义 PK,否则该表不同步
Redshift 大小写敏感 Workgroup 参数 enable_case_sensitive_identifier=true
命名空间资源策略授权 Namespace → Resource policy(授权主体 + 集成源 ARN)
预期管理最重要的一条:集成创建(Creating → Active)通常需要一段基础设施编排时间,与数据量无关,是一次性开销。演示 / 汇报请提前预留。但注意——”创建慢” ≠ “同步慢”,建成后的数据同步是近实时的(约 1 分钟内)。

4.2 Step by step

下面 1–5 步是把前置条件逐项做实;6–8 步是创建集成并验证。走 RDS 控制台的 Zero-ETL 向导时,1、2、4、5 步大多能被”Fix it for me”自动完成——建议先跳到第 6 步,让向导告诉你缺什么。

1. 源库启用自动备份

RDS → Databases → 选实例 → Modify → Backup retention period 从 0 改为 1 天或更大 → Apply immediately。等实例回到 Available。

对应报错:Automated backups must be enabled on the source database…

2. 配置 binlog 参数组并重启

    • RDS → Parameter groups → Create parameter group(family 选 mysql8.0)。
    • 编辑三个参数:binlog_format = ROWbinlog_row_image = full,并确认 binlog_row_value_options 不是 PARTIAL_JSON
    • 回到实例 → Modify → 挂上该参数组 → Apply immediately。
    • 这些是静态参数,必须 Actions → Reboot 重启才生效。

3. 确认要同步的表都有主键

没有主键的表会被静默跳过——这是”某张表就是不同步”最常见的原因。先查一遍:

SELECT t.table_name
FROM information_schema.tables t
LEFT JOIN information_schema.table_constraints c
       ON c.table_schema = t.table_schema
      AND c.table_name   = t.table_name
      AND c.constraint_type = 'PRIMARY KEY'
WHERE t.table_schema = '<db>'
  AND t.table_type   = 'BASE TABLE'
  AND c.constraint_name IS NULL;   -- 输出的表缺主键,需补

4. 目标端开启大小写敏感

Redshift → Redshift Serverless → Workgroup configuration → 选 workgroup → 数据库参数区域 Edit → 把 enable_case_sensitive_identifier 设为 true → Save。

预置集群则在参数组里改同名参数并重启集群。

5. 配置命名空间资源策略授权

Redshift → Namespace configuration → 选命名空间 → Resource policy 标签页:

    • Add authorized principal:填账号 ID 或指定 IAM 角色/用户 ARN。
    • Add authorized integration source:填源库 ARN arn:aws:rds:us-east-1:<account>:db:<db-instance>,建议明确添加。

6. 创建 Zero-ETL 集成

RDS 控制台 → Zero-ETL integrations → Create zero-ETL integration:

    • Integration identifier:起个名字。
    • Source:选源 RDS 实例。若提示备份/参数组/binlog 不满足 → 点”Fix it for me”让向导自动处理。
    • Target:选 Amazon Redshift 的目标命名空间。若提示大小写敏感/资源策略不满足 → 同样一键修复。
    • Review and create → Create。

7. 等状态变 Active,然后从集成创建数据库

状态 Creating → Active 需要一段一次性编排时间。只要没有红色报错就是正常的,不要反复重建。

Active 后二选一:

    • 控制台一键:Redshift → Zero-ETL integrations → 打开集成 → Create database from integration,输入库名。
    • Query Editor v2:用 Admin 用户连接(普通 IAM 身份没有建库权限),MySQL 源不要带 DATABASE 子句:
CREATE DATABASE zeroetl_db FROM INTEGRATION '<集成ID>';
-- 集成 ID = 集成 ARN 的最后一段

-- permission denied to create database  → 换 Admin 用户
-- Unknown arn ... pair                  → 删掉多写的 DATABASE 子句

8. 验证初始加载与近实时增量

用三段式 集成库.源库名.表名 查询(表名带连字符或大小写时加双引号):

-- 初始加载:应与源库行数一致
SELECT COUNT(*) FROM zeroetl_db.<db>.address;
SELECT * FROM zeroetl_db.<db>.address ORDER BY id LIMIT 5;

-- 看每张表的同步状态(哪张没同步、为什么)
SELECT * FROM SVV_INTEGRATION_TABLE_STATE;

增量验证:在源库 MySQL 插入 1 行,等约 1 分钟后在 Redshift 重跑 COUNT(*),数字应 +1。同样试一次 UPDATE 和 DELETE——三种变更都会被同步,这正是它和 Glue 微批的关键差别。

4.3 ✅ 优点

  • 零代码、零 ETL 运维,无计算资源可管
  • 近实时(约 1 分钟),含 INSERT/UPDATE/DELETE
  • 整表/整库自动同步,schema 变更自动跟随
  • 成本模型简单,无 Glue DPU / 无脚本维护

4.4 ⚠️ 注意点

  • 初次创建有一次性编排耗时
  • 源表必须有主键
  • 是”同步复制”,不做复杂转换(转换放到 Redshift 里做)
  • 建库要用 Admin,去掉多余 DATABASE 子句

五、三种方式横向对比

维度 ① S3 + COPY ② Glue ETL ③ Zero-ETL ★推荐
延迟 手工批量(小时/按需) 分钟级(微批) 近实时 ≈ 1 分钟
增量 / CDC 无,全靠重搬 Bookmark 增量,不捕获 DELETE 完整 CDC(含 DELETE)
开发工作量 脚本 + 手工操作 连接/脚本/触发器/IAM 零代码,向导点选
运维负担 高(人肉) 中(管 Job 与调度) 极低(全托管)
数据转换能力 强(Spark) 无(转换后置到 Redshift)
初始上手成本 一次性创建编排
适用场景 一次性 / 冷数据导入 需重清洗、多源整合 持续近实时分析同步

六、选型建议:默认选 Zero-ETL

如果你的目标是”让 Redshift 里的数据持续贴近 MySQL 现状、用于近实时分析”,Zero-ETL 是首选:它把最麻烦的 CDC、增量、DELETE 捕获、调度、扩缩容全部托管掉,你几乎不写代码、也没有计算资源要运维,延迟还做到约 1 分钟。

另外两种作为补充:

  • 需要重度转换 / 多源整合 / 落库前清洗 → 用 Glue ETL(或 Zero-ETL 同步原始表 + 在 Redshift 里做转换)。
  • 一次性 / 冷数据 / 外部 CSV 导入 → 用 S3 + COPY,最直接。
常见组合拳:Zero-ETL 负责把源表近实时同步进来,Redshift 里用物化视图 / auto_mv 做聚合与建模,历史/外部数据用 S3 COPY 一次性回填。三者并不互斥。

七、参考资料

7.1 官方博客

7.2 文档 · 总览

7.3 文档 · 操作步骤

7.4 文档 · 排查

示例基于 Amazon RDS for MySQL → Amazon Redshift Serverless。生产环境请遵循最小权限,妥善托管数据库凭证到 Secrets Manager,并按需补齐 S3 Gateway Endpoint 与多 AZ NAT。

➡️ 下一步行动:

相关产品:

相关文章:

*前述特定亚马逊云科技生成式人工智能相关的服务目前在亚马逊云科技海外区域可用。亚马逊云科技中国区域相关云服务由西云数据和光环新网运营,具体信息以中国区域官网为准。

本篇作者

张振华

亚马逊云科技解决方案架构师,负责基于亚马逊云科技的云计算方案的架构和设计,在 Edge、Serverless 、容器化,微服务架构,云原生 DevOps 等方向具有丰富的实践经验。自加入亚马逊云科技后,专注于游戏行业,以及 GenAI 在游戏行业的应用。


AWS 架构师中心:云端创新的引领者

探索 AWS 架构师中心,获取经实战验证的最佳实践与架构指南,助您高效构建安全、可靠的云上应用