首页 / 资讯中心 / 文章详情

MySQL数据迁移到ClickHouse:工具选型与全量/增量同步实战

MySQL数据迁移到ClickHouse:工具选型与全量/增量同步实战 ★ FEATURED ARTICLE
把MySQL里的数据弄到ClickHouse这件事我做过的项目没有十个也有八个了。最早的时候大家都是写脚本从MySQL一行一行查再拼insert语句插到ClickHouse里。数据量一上来这种方案基本就废了跑一次几个小时还经常断。后来我开始系统梳理各种迁移工具慢慢摸清了什么场景适合什么方案。这篇文章就围绕“方便”这两个字展开把市面上真正能用的思路、工具和坑一次性说清楚。1. 为什么折腾MySQL到ClickHouse的迁移需求从哪里来1.1 MySQL做分析查询为什么越来越吃力先别急着选工具得先搞清楚我们为什么要把数据搬走。MySQL本身是典型的行式存储引擎设计目标就是OLTP也就是支撑用户注册、下单、支付这类高频小事务。它的索引结构是B树特别擅长根据主键或唯一键快速定位某一行一次查几条、改几条性能非常优秀。但是一旦你要做的是“统计过去30天每个城市的订单金额”情况就完全不同了。这类分析型查询往往要扫描几百万甚至上千万行MySQL的处理方式是逐行读取数据做分组、聚合、排序就算加了索引面对多维度组合查询也经常力不从心。数据量一旦过上亿行MySQL跑一个报表查询可能要几十秒甚至几分钟业务方等不起DBA也受不了。1.2 ClickHouse到底合适做什么ClickHouse是列式存储数据库和MySQL是完全相反的设计思路。它的核心优势体现在三块数据按列存储查询只读取需要的字段I/O量大幅减少高压缩比像日志、订单这类文本型数据压缩后只有原来的三分之一到十分之一向量化执行引擎能用SIMD指令批处理数据聚合计算效率极高我举个直观的例子。某张日志表有2亿行字段有十来个。MySQL里做一次简单的按天分组统计往往要5到10秒同样的数据量、同样的查询在ClickHouse里常见情况是200毫秒以内。这不是个例而是量级上的常规差距。所以这个“迁移”的需求本质上就是把分析型负载从OLTP数据库里剥离出来交给专门的OLAP引擎去扛。搞清楚了这个大前提后面选工具和方案就有了判断基准数据不搬家报表跑不动搬过来的数据你要的是查询快、压缩高、吞吐大。2. 迁移工具盘点哪些方案配得上“非常方便”2.1 选择工具的四个考量维度我判断一个迁移工具是否“方便”从来不看它宣传得有多炫只看四个硬指标配置成本要不要装一堆环境、写一堆代码最好一个配置文件、一条命令就能跑。性能表现全量数据迁移速度够不够快增量同步延迟能压到多低。类型映射MySQL那些字段类型在ClickHouse里会不会被搞坏比如 datetime 的精度、varchar 的长度。容错能力中途断了能不能断点续传半路报错能不能跳过垃圾数据。这四个维度缺一不可。只追求操作简单但性能拉跨数据量一上来照样返工只看性能但配置繁琐新手上手成本又太高。2.2 主流方案横向对比我把目前常见的迁移路线做了个梳理大致分四类方案类型代表工具/方式适用场景上手难度同步模式原生表函数方案ClickHouse的MySQL表引擎小表、一次性迁移极低全量离线ETL工具DataX、SeaTunnel大批量全量迁移中低全量/周期实时CDC方案Canal/SeaTunnel Kafka业务系统在线迁移较高增量文件导入方案导出CSV再用clickhouse-client导入数据量极大、追求稳定中全量这里我先说一个很多人不知道的结论如果你只是要搬几张小表或者做一次性的历史数据迁移根本不需要额外部署任何工具。ClickHouse的MySQL表引擎就是最简单的“免费搬运工”。你只需要在ClickHouse里建一张和源表对应的表指定MySQL的连接信息就能用标准的SELECT把数据查出来配合INSERT INTO SELECT语句直接落进本地表。整个过程不写一行Python、不装任何中间件。如果数据量到了千万行以上、需要频繁反复迁移这时候就轮到DataX这类离线ETL工具登场了。它做的就是“源头读取-通道传输-目标写入”这套标准化流程你只需要写一份JSON配置里面写明数据源、目标表、字段映射和并发度执行一条命令剩下的交给框架。2.3 我的推荐组合在大多数项目里我的选择逻辑是这样的100万行以内优先用ClickHouse的MySQL表引擎简单直接跑完即删。100万到几千万行、全量模式用DataX配置清晰、并发可控、报错信息完整。持续有新数据写入、需要实时同步到ClickHouse做分析的用基于binlog的CDC方案由SeaTunnel或者Canal解析MySQL日志把变更数据发到消息队列再由消费者批量写入ClickHouse。这套组合的好处是每种工具只做自己最擅长的事不会出现“杀鸡用牛刀”或者“牛刀切不了鸡”的尴尬。3. 实操最简单的一条路用ClickHouse原能力全量搬家3.1 在ClickHouse里建MySQL引擎表我先把最直接、老手新手都容易忽略的方案完整演示一遍。假设MySQL里有一张用户行为表叫user_event结构大概是这样的CREATE TABLE user_event ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, event_name VARCHAR(64) NOT NULL, occur_time DATETIME NOT NULL, extra_data TEXT NULL ) ENGINEInnoDB;想在ClickHouse里直接读这张表只需要执行一段CREATE语句CREATE TABLE mysql_user_event ( id UInt64, user_id UInt64, event_name String, occur_time DateTime, extra_data Nullable(String) ) ENGINE MySQL( localhost:3306, analytics_db, user_event, root, your_password );这里有几个字段映射的核心点MySQL的BIGINT UNSIGNED对应ClickHouse的UInt64范围才不会缩水DATETIME直接对应DateTime精确到秒没问题TEXT这种可空类型在ClickHouse里建议用Nullable(String)建好之后你就能像查询本地表一样去查MySQL里的数据了SELECT * FROM mysql_user_event LIMIT 10;3.2 创建本地MergeTree表并完成搬迁查询通了下一步就是把目标表建在ClickHouse本地存储上。选MergeTree表引擎并认真设计排序键CREATE TABLE user_event_local ( id UInt64, user_id UInt64, event_name String, occur_time DateTime, extra_data Nullable(String) ) ENGINE MergeTree() PARTITION BY toYYYYMM(occur_time) ORDER BY (occur_time, user_id);这里我多说一句排序键的设计。ClickHouse的ORDER BY决定了数据在磁盘上的物理排序也直接决定了哪些查询走稀疏索引、哪些查询要全表扫描。像用户行为分析最常见的查询条件就是按时间范围过滤再按用户聚合所以把occur_time放第一位是很稳妥的。如果你们业务更常按user_id圈人就改成(user_id, occur_time)。建好目标表迁移就是一条SQL的事INSERT INTO user_event_local SELECT * FROM mysql_user_event;就这么简单。数据量不大时通常在几十秒内就能跑完。3.3 数据校验与收尾搬完数据千万别急着走。我的习惯是至少做三件事行数对比SELECT COUNT(*)两边分别跑一下确保对得上。抽样抽查从源表和目标表里各取几条ID相同的记录逐字段对比关键值。业务查询实测把你最核心的那条分析SQL在ClickHouse上跑一遍看一眼耗时确认没有明显的类型映射错误。确认无误后别忘了删除MySQL引擎表避免每次SELECT都远程连MySQLDROP TABLE mysql_user_event;这个方案的“方便”体现在哪零额外依赖、一条SQL完成迁移、出错能立刻用标准SQL排查。它是一个被很多人无视但实际上极其可靠的全量迁移路径。唯一的限制是数据量很大时通过MySQL引擎表逐行读取的效率会变低因为每次查询ClickHouse都需要跟MySQL服务端交互。这种情况就该换下面的工具方案了。4. 进阶用DataX/SeaTunnel做大批量与增量同步4.1 DataX配置与执行全流程DataX是开源社区里非常经典的离线数据同步工具核心思路是每个数据源对应一个Reader或Writer插件你只需要用JSON描述“从哪里读、怎么写、并发几路”。我在这里给你一套可以直接抄作业的配置。假设还是上面那张user_event表同步到ClickHouse{ job: { content: [ { reader: { name: mysqlreader, parameter: { username: root, password: your_password, column: [id, user_id, event_name, occur_time, extra_data], splitPk: id, connection: [ { table: [user_event], jdbcUrl: [jdbc:mysql://localhost:3306/analytics_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai] } ] } }, writer: { name: clickhousewriter, parameter: { username: default, password: , column: [id, user_id, event_name, occur_time, extra_data], preSql: [TRUNCATE TABLE user_event_local], connection: [ { table: [user_event_local], jdbcUrl: jdbc:clickhouse://localhost:8123/default } ] } } } ], setting: { speed: { channel: 4 }, errorLimit: { record: 100, percentage: 0.02 } } } }几个关键参数我展开讲一下splitPk是并发切分的依据。选了主键id之后DataX会把id范围拆成多段每个channel拉一段数据互不干扰。如果没有合理的切分键它就只能单线程读性能会差很多。注意splitPk字段最好确实是数字类型且分布相对均匀。channel是并发数最直接的性能调节旋钮。不是越大越好一般从4开始逐步往上加同时盯着MySQL的CPU和ClickHouse的写入情况。一个容易踩的坑是并发太高时MySQL的IO被打满反而拖慢整体速度。errorLimit是容错阈值。设置成最多允许100条或2%的错误数据质量有点小问题时不至于让整个任务白跑。执行命令很简单python datax.py job/user_event_to_clickhouse.json跑完之后日志里会明确显示读出多少条、写入多少条、平均速度是多少。我见过很多几百万行的表用默认配置就能跑到每秒两万到五万行的速度非常稳。4.2 用SeaTunnel配置增量同步如果说DataX是离线批量的标杆那实时增量同步的“方便”程度我投SeaTunnel一票。它比纯手写CanalKafka再自己消费要省事太多因为它的Source端直接集成了MySQL CDC能力能解析binlogSink端也早就封装好了ClickHouse写入。一份最基本的SeaTunnel配置长这样source { Jdbc { url jdbc:mysql://localhost:3306/analytics_db user root password your_password table_name user_event result_table_name mysql_user_event query SELECT id, user_id, event_name, occur_time, extra_data FROM user_event WHERE id :offset } } transform { Filter { result_table_name mysql_user_event } } sink { Clickhouse { host localhost:8123 database default table user_event_local fields [id, user_id, event_name, occur_time, extra_data] bulk_size 10000 } }海豚的CDC模式比这个还要自动它会自动识别binlog位置不需要你手动管理offset。不过有一点必须提醒ClickHouse本身不擅长高频小批量写入。你如果每秒往里面插几十条单行数据它的MergeTree会不断生成小parts后台后台合并压力巨大查询性能也会被拖累。所以无论用什么CDC工具收尾写入ClickHouse的那一步一定要开攒批比如攒到5000条或10000条统一flush一次。上面配置里的bulk_size10000就是这个意思。4.3 什么时候用什么方案很多人会纠结到底用DataX还是SeaTunnel其实不需要纠结全量历史数据优先DataX它快、直接、报错清晰。增量实时数据用SeaTunnel的CDC它把binlog解析和写入封装好了省心。全量增量混合通常是先用DataX把历史数据刷过去再启动SeaTunnel的增量同步两边接力。这个组合我在这几年的项目里反复验证过是稳定性最高、运维成本最低的一套。5. 常见问题与排查技巧实录5.1 数据类型映射最容易翻车的细节MySQL和ClickHouse的类型体系差异很大很多迁移问题表面上是“跑不通”实际上是“类型不对”。我的速查表大概这样MySQL类型ClickHouse类型注意事项TINYINTInt8无符号记得用UInt8INT / INTEGERInt32BIGINTInt64UNSIGNED时用UInt64否则数值被截断VARCHAR / TEXTStringString没有长度限制但排序键别放太长字段DATETIMEDateTime精确到秒毫秒必须用DateTime64TIMESTAMPDateTimejdbcUrl里务必指定serverTimezoneDECIMALDecimal(精度, 小数位)精度要按原定义设置否则金额出错BIT(1)UInt80/1要自行转换JSONStringClickHouse原生JSON类型不适用先转字符串一个特别典型的坑MySQL的TIMESTAMP是有时区的写入和读取都取决于会话时区设置。如果DataX的jdbcUrl里没有加serverTimezoneAsia/Shanghai默认用UTC来解析时间到了ClickHouse就会整体偏移8小时。这个问题排查起来相当隐蔽因为数据量、行数、字段类型全都没问题只有时间整整差了一个时区。5.2 写入慢、并发上不去怎么办如果你发现DataX跑得很慢每秒只有两三千行通常从三个方向排查MySQL侧确认splitPk有没有生效。如果它在跑任务时不切分通道日志里只会有一个channel此时应检查主键类型或者临时加一个可切分的字段。ClickHouse侧大批量写入时ClickHouse的merge线程本身会消耗资源。你可以在系统表里看后台merge队列如果积压严重适当调低channel数反而更快。网络和驱动ClickHouse的JDBC驱动性能差异很大优先使用官方驱动和HTTP接口避免走老旧的TCP协议封装。另外一个通用手段是分批写入。比如把几百万行拆成多个批次每批10万行用preSql把目标表锁住或者提前清理分区会让整个过程稳定很多。5.3 数据校验发现两边行数对不上这是几乎每个人都会遇到一次的场景源表COUNT是200万ClickHouse里只有199.5万少了五千行。我的排查顺序固定如下第一步看任务日志里的错误记录数。DataX会在errorLimit范围内丢弃脏数据如果你没看日志会以为全部搬完了。第二步查是否有重复值。源表的id如果存在重复splitPk按id切分时会出问题部分数据可能被覆盖或者读取遗漏。第三步检查写入时的去重逻辑。ClickHouse的MergeTree不天然保证主键唯一没配置去重引擎时重复INSERT数据会产生重复行配置了ReplacingMergeTree又可能出现“同样的数据被当作新旧版本替换”的问题。5.4 实时同步场景的幂等与乱序做CDC增量同步时最棘手的问题不是延迟而是数据的一致性和顺序。MySQL的binlog是严格按照事务顺序记录的但经过消息队列异步消费之后到达ClickHouse的顺序可能发生变化。如果一张表既做全量又做增量还可能在边界处漏数据或者重复插数据。我的建议是两条场景允许的话在ClickHouse目标表上使用ReplacingMergeTree用业务主键做版本号去重这样即使重复写入也能按版本保留最新值。绝对不能把事务边界忽略。如果你的MySQL源表有跨行事务消费端也要对应的按事务批次提交否则中间状态的半截数据会被查出来。因为ClickHouse默认不支持行级UPDATE和DELETE所以设计目标表时就要想清楚这张表的更新逻辑到底是什么。是用CollapsingMergeTree做增删抵消还是用ReplacingMergeTree按主键刷新需要提前定好而不是等上线后出了问题再改表结构。6. 使用过程中的几句真心话迁移数据库表面上是个技术活本质上是个“取舍活”。每次有人问我“推荐哪个工具”我第一句反问永远是你的数据量级是多少后续还有没有持续同步的需求。没有这两个前提任何工具推荐都是耍流氓。我个人这几年用得最顺手的是这套组合小表用原生MySQL引擎直接SELECT大批量全量用DataX的JSON配置跑通道实时同步交给CDC工具攒批写入。三个工具各管一段几乎没有互相打架的时候。而在每次迁完之后我都会在ClickHouse里把业务核心查询跑一遍顺手看一眼system.query_log里的耗时——这个数据能直观告诉你规划的表引擎、排序键、分区键到底合不合理。另外一个容易忽略的技巧是正式迁移前先拉一小批数据做“预迁移”比如取最近一天的数据先同步过去验证类型映射和时区问题。这一步能帮你挡掉超过半数的迁移事故是真的值得的投入。工具选得顺手迁移就是一条命令的事选得别扭够你调试一整周。希望这篇整理能让你少走一段弯路。
阅读完成 · 觉得有帮助?
咨询建站