)
文章目录先把要解决的问题钉在墙上第一代原始单通道连路由都没有第二代哈希分配同源有序第三代轮询事务数绝对均衡第四代负载均衡盯着队列深度第五代文件映射把控制权交还给DBA第六代事务拆分加表亲和性专治跨表大事务第七代行哈希单表也能并行最后一道防线DDL安全和表级一致性兜底多通道挂了怎么办——位点的学问460万条数据实测表拆分对决行哈希光看实验室数据不够唠两个真实大场面调参这事儿我再啰嗦几句收尾把入库这件事想明白兼容是对前人努力的尊重是确保业务平稳过渡的基石然而这仅仅是故事的起点上篇把单通道内部那几个优化讲完了这篇来啃硬骨头——多通道并行。我前面说过单通道你优化到天上去本质还是一个线程、一个连接在写多核CPU大部分核心在摸鱼。真正让吞吐产生量级变化的是开多个通道同时往目标库里灌。但并行这扇门一推开数据顺序、事务完整性、DDL安全、故障恢复这些雷全冒出来了。KFS的路由策略不是一开始就完备的它是被一个又一个坑逼着从最原始的样子一路演进过来的。我扒的时候最大的乐趣就在这——你能清楚看到每一代策略解决了什么、又留下了什么新麻烦。这篇我就按这个演进的路子往下讲最后再说说多通道挂了怎么恢复以及我自己做的460万条数据的实测。先把要解决的问题钉在墙上多通道并行说白了就是上游的变更事件流进来经过Partitioner这么一个分流器被分配到不同通道每个通道一个线程、一个连接各写各的。上游事件流DML / DDL / 提交 │ ▼ [Partitioner 路由引擎] ← 每个事件该进哪个通道 │ ┌────┼────────────┐ ▼ ▼ ▼ Q0 Q1 ... QN 线程0 线程1 线程N 连接0 连接1 连接N │ │ │ └────┴────────────┘ ▼ 目标端数据库整个多通道设计翻来覆去就是在回答一个问题一个事件到底该分到哪个通道这个问题看着简单实际上是一连串互相打架的要求——你要并行度高就容易牺牲顺序你要保证同表有序就可能让某个通道忙死你要绝对均衡又可能把有依赖的事务拆开。后面这些策略每一个都是在这几个要求之间做不同的取舍。我先把结论撂这没有银弹。KFS提供了好几种路由策略让你按场景选还能包着组合用这才是它真正聪明的地方。第一代原始单通道连路由都没有最开始目标端就是单通道跑压根没有路由逻辑任务和通道一对一绑定就完事。Task 0 → 通道0 Task 1 → 通道1 不需要任何分配计算零开销它提供了最简单的多通道基础外部系统想自己决定怎么分也行自己没有任何计算成本。但问题明摆着——同一张表的INSERT可能进通道0UPDATE却进了通道1。这俩要是在不同通道并行UPDATE先执行了数据顺序直接乱掉一致性没法保证。而且分配权在外部KFS自己控制不了负载均衡。所以需要一个办法让同一个数据源的事件始终去同一个通道。哈希策略就这么来了。第二代哈希分配同源有序思路很直接对数据源标识shardId做哈希再对通道数取模shardId oracle → hashCode() → 42 → 42 % 4 → 通道2 公式abs(shardId.hashCode()) % 通道数 oracle → 42 → 通道2 kingbase → 17 → 通道1同一个shardId哈希值固定永远落到同一个通道。这么一来同一个数据源的所有操作都在一个通道里顺序执行顺序有了。上篇说的那个INSERT和UPDATE散到不同通道的问题在这一代就解决了——只要它们来自同一个数据源就一定在同一个通道通道内部又是严格有序的。但新坑紧跟着就来了。哈希只认shardId不关心每个数据源的数据量。万一某个数据源是热点——比如核心的订单库产生的变更占了大头——它对应的那个通道就长期满载其他通道却闲得发慌。忙的忙死闲的闲死整体吞吐还是被那个热点通道卡住。于是需要一种不关心数据源归属、只关心各通道事务数均不均衡的办法。轮询出场。第三代轮询事务数绝对均衡轮询更简单按事务序号对通道数取模transno % 通道数 Tx1 (订单库) → Q0 Tx2 (日志库) → Q1 Tx3 (订单库) → Q2 Tx4 (报表库) → Q0 Tx5 (订单库) → Q1 Tx6 (日志库) → Q2 四个通道每个通道分到的事务数一样多不管你是哪个数据源轮到哪个通道就是哪个事务数量上绝对均衡。热点数据源的事务被摊到各个通道热点通道的问题没了。听起来挺美但它又忽略了一个维度——事务大小。事务数均衡不等于实际负载均衡。一个事务可能包含几十万行变更另一个事务只有两三行。轮询只数事务个数那个几十万行的大事务整个砸进一个通道其他通道处理完小事务早早空转就等着它。我在真实项目里就碰到过凌晨批量跑一个超大事务单个通道被堵得死死的监控上看其他通道全空闲但整体吞吐就是上不去。所以得能实时感知每个通道实际积压了多少活儿动态选最空的那个。这就是负载均衡。第四代负载均衡盯着队列深度负载均衡的逻辑是每次分配前先看一圈各通道的队列谁队列里积压的行数最少就给谁argmin( queue[i].size ) Q0: 5000 行 Q1: 5 行 ← 选我 Q2: 3200 行 每次都挑当前最空的通道这样能实时把活儿往空闲通道送队列深度始终比较均衡大事务小事务的差异被动态消化了。但负载均衡也不是终点它留下了两个很要命的盲区我必须重点说。第一它只看队列深度不管同一张表的数据不能散到不同通道。这么分同一张表的事件完全可能进不同通道等于把第二代好不容易解决的同表无序问题又放回来了。第二没有DDL并发保护。这个更危险。DDL——比如ALTER TABLE加列、CREATE INDEX建索引——要是和同一张表的DML在不同通道同时执行一个在改表结构、一个在往表里插数据元数据直接冲突严重的会导致数据损坏。前面哈希、轮询、负载均衡没有一个考虑到DDL这个事。另外生产上还有个需求它满足不了——DBA有时候就想手动指定核心表走高速通道、日志表走慢通道、报表表走专用通道这种人为调度它做不到。第五代文件映射把控制权交还给DBA针对手动调度搞了个配置文件的方式读一个叫shard.list的映射表# shard.list 配置示例 db_order.* → 通道0 核心业务 db_log.* → 通道1 日志 db_report.* → 通道2 报表 * → 通道0 默认兜底哪张表去哪个通道DBA写得明明白白重启配置生效。核心表想独占通道、日志表往后放都能控制。但它只解决了手动控制路由前面说的DDL并发安全问题它照样没碰。这个DDL的雷一直挂到最后才被专门处理。第六代事务拆分加表亲和性专治跨表大事务前面轮询和负载均衡都栽在大事务上但大事务其实分两种得区别对待。一种是跨表大事务——一个事务里同时写订单表、库存表、流水表好几张表。以前这种事务整个进一个通道其他通道干瞪眼。KFS的做法是按表拆成子事务拆分前 大事务 T(seqno100) ├ 表1 ├ 表2 └ 表3 整个堆进通道0通道1/2/3全空闲 拆分后 子事务1 (fragno0) 表1 → 通道0 子事务2 (fragno1) 表2 → 通道1 子事务3 (fragno2) 表3 → 通道2 每张表一个子事务各自独立编号可以并行配合一个叫表亲和性的规则——同一张表的所有操作包括后续事务里的始终路由到同一个通道。这样既保证单表内部有序、外键关系安全又能让跨表的部分并行起来。这里我要插个批判性的观点。事务按表拆开并行听上去没问题但你得想清楚一件事源端这是一个事务具有原子性——要么全成功要么全回滚。你拆成几个子事务分到不同通道万一其中一个失败了其他几个已经提交了这个原子性怎么兜这是分布式事务的经典难题。我理解它在通道层面得有协调机制保证这些子事务最终要么都生效、要么都能补偿。用这个策略之前你最好搞清楚目标端这套一致性是怎么保证的别光看到并行快就开。而且它还有个天生缺陷——单表大事务。如果一个事务不是跨表而是往一张表猛插一百万行表亲和性规定这张表永远走同一个通道那这一百万行还是全堆一个通道退化成串行了。要破这个得继续往下。第七代行哈希单表也能并行行哈希把粒度从表细化到行用主键值来打散hashKey 模式名.表名 主键值 id1 → hash → 0 → 通道0 id2 → hash → 3 → 通道3 id3 → hash → 1 → 通道1 id4 → hash → 0 → 通道0 同一个主键同一行永远在同一个通道 → 行级有序 同一张表、不同主键的行 → 散到不同通道 → 单表也并行这一下单表大事务的瓶颈破了——一百万行按主键哈希摊到十几个通道同一行的操作始终在一个通道保证不乱序不同行各走各的热点表不再独占单通道。但注意它对有主键和没主键的表处理不一样这个区别很关键✅ 有主键表 hashKey 模式名.表名 主键值 同表不同主键行可以分散单表并行 ⚠️ 无主键表 hashKey 模式名.表名退化成表级 因为没有主键没法判断两行会不会冲突 整张表必须串行安全第一我上篇就念叨过补主键的事到这儿你就看明白为什么了——没主键行哈希直接退化成表亲和并行能力作废。所以迁移前给关键大表补上主键等于白捡的并行性能。最后一道防线DDL安全和表级一致性兜底DDL那个雷前面几代都没拆最后用一个安全包装层来解决。思路是这样用一个叫NoneCritical的包装器把内部分配策略包起来事件到达 │ ▼ 是 DDL 吗 ── 是 → 固定送到通道0 │ 否DML │ ▼ 查表→通道映射表 ├ 这张表已经在通道2了→ 必须去通道2表亲和 └ 是张新表→ 交给内部Partitioner分配然后记下映射并持久化 效果 ☑ DDL永远走通道0不会和DML在不同通道撞车 ☑ 同一张表的DML一定在同一个通道有序 ☑ 里面可以包任意一种内部分配策略DDL全部固定到通道0串行执行从根上杜绝了DDL和同表DML在不同通道并发导致元数据冲突的问题。DML则通过一张表到通道的映射表保证同表同通道。这层包装最妙的是它不替换你选的内部分配策略而是套在外面做安全兜底——你想用行哈希就用行哈希外面再包一层DDL保护。到这儿并行的几个雷才算基本拆完同源有序、负载均衡、手动调度、跨表并行、单表并行、DDL安全各有各的策略还能组合。这就是我前面说的没有银弹但给你一箱子零件让你拼出最合适的方案。多通道挂了怎么办——位点的学问并行还有个绕不开的难题故障恢复。单通道好办记一个位点挂了从那儿继续。多通道各跑各的进度还不一样怎么保证恢复后一条不丢、一条不重这个问题刚接触的时候我是真有点懵单通道记一个位点天经地义可你想象一下十六个通道有的处理到第100号事务、有的才到97号中间还有事务被拆成了好几个片段这状态乱成一锅粥凭一个数字根本描述不清楚。我一开始甚至怀疑这事儿能不能靠位点解决后来看完它那套复合位点的设计才服气人家是真把每个通道的进度和全局的事务完成情况都揉到一起记了。KFS目标端有张位点表每个通道一行task_id | seqno | fragno | last_frag | wait_seqnos ──────────────────────────────────────────────── 通道0 | 100 | 2 | true | 10 | 100-2 | 98-0,99-1 | 97-3 通道1 | 99 | 1 | true | 9 | 100-2 | 98-0,99-1 | 97-3 通道2 | 97 | 3 | true | 8 | 100-2 | 98-0,99-1 | 97-3wait_seqnos是个复合位点前面是个单调递增的全局提交计数后面跟着各通道待处理的位点。每个通道提交后把自己处理到哪了、全局还有哪些事务没完成都写进去崩了之后靠这个重建状态。恢复的时候有两个关键位点再配合三段区间判定通道进度通道0100通道199通道297 重发起点 min(100, 99, 97) 97 ← 从这里开始重发 上界 lastMaxPoint max 100 ← 重发到这儿为止 从 seqno97 重发逐条判定三段 ① 已提交97 ≤ seqno ≤ 100且不在待处理列表 → 跳过绝不重复写 ② 未提交在待处理列表里 → 重放到原来的通道 ③ 新事件seqno 100 → 正常处理恢复完成逻辑很严密——min决定从哪儿重发保证不丢最保守地从最慢通道的位置开始max决定重发到哪儿为止待处理列表决定具体哪些要补。已经提交的一律跳过防重复没提交的重放回原通道之后的新事件正常走。这么下来Exactly-Once——不多不少正好一次。说实话我自己恢复过一次进程意外退出重启后自动从位点接上数据没重没丢延迟接着往下追。这套机制平时看不见但它是你敢放心并行的底气。460万条数据实测表拆分对决行哈希光看演进不过瘾我做了个实测专门对比表拆分和行哈希这两种对付大事务的策略。数据构造58个事务总共460万条数据两张表a和b。前18个事务往表里插60万然后20个事务只往a表插、每个10万条纯INSERT共200万最后20个事务往a、b两表插、每个10万条且INSERT/UPDATE/DELETE都有又200万。通道数配16。场景一表拆分验证跨表事务不独占单通道 场景二行哈希验证单表海量数据能均衡结果对比很说明问题。表拆分方式因为我这数据核心就两张表按表拆完你看监控里真正在跑的通道就两个其他通道都是空的数据是0、seqno-1q-to-dbms Events41 ← 通道0在跑 q-to-dbms1 Events4 ← 通道1在跑 q-to-dbms2 Events0 ← 通道2空闲 q-to-dbms3 Events0 ← 空闲 ...一直到 q-to-dbms15 全是 0 两张表 → 只有两个通道在执行行哈希方式就完全不同虽然还是那两张表但数据按主键打散所有16个通道全在跑q-to-dbms Events1 q-to-dbms1 Events1 q-to-dbms2 Events1 ...一路到... q-to-dbms15 Events6 全部16个通道都有数据没有一个空闲这就直观印证了前面的结论表拆分对跨表大事务有效表越多并行度越高但如果事务集中在一两张表上并行度就被表的数量卡住行哈希直接按行打散哪怕只有一张表也能把所有通道全部用满。吞吐上多通道并行确实接近线性增长——通道数往上加吞吐跟着往上走当然到数据库自身IO、CPU的极限就到头了不能无限加。我实测的体会是开通道之前先摸清目标库的承载能力16个线程同时写连接数、CPU、IO都得兜得住不然通道开得越多数据库锁竞争和IO争抢越严重反而变慢。参数和数据库容量得一起规划。光看实验室数据不够唠两个真实大场面我自己的实测才几百万行数据说实话在有些客户面前不算什么。再给你们说两个资料里看到的大场面能更直观感受目标端入库扛到极限是什么样。一个是解放军总医院的医疗云数据汇聚。他们要把下属好多个医学中心和医疗区的所有医信系统数据全部归集到数据仓库里做分析。涉及的系统你们听听——HIS、LIS检验、电子病历、急诊、康复、护理、超声、心电全是关键业务一刻不能停。源库也是五花八门好几个版本的Oracle、SQL Server、MySQL、KingbaseES都有操作系统Windows、Linux、AIX、Solaris全齐。存量数据60TB往上每天还新增300多GB。他们的架构是每个医学中心部署KFS前置节点实时把业务数据采集过来清洗、转换之后合并入湖。这种多源汇聚的场景目标端要同时接几十个源的写入入库吞吐和数据冲突处理都是硬考验。最后做下来60TB全量加每天300多GB增量业务不停、性能无损采集转换的延迟控制在10秒以内。这个数据量目标端入库但凡差一点整个湖就别想实时更新了。另一个是某直辖市的市政交通一卡通清结算系统。这系统承载1.8亿用户存量12TB多早高峰三个小时5900万笔交易核心清结算系统绝对不允许停机。要从原来的Oracle切到国产读写分离集群。存量12TB、日增500多GB而且是金融级数据一致性要求极高一笔都不能错。他们靠KFS做零停机迁移之后还在业务中台的结算系统和数据分析平台之间持续同步。这种场景目标端入库压力有多大你们想想早高峰那三小时每秒涌进来的都是真金白银的交易记录写入稍一卡顿结算就积压。举这俩例子不是为了凑数字是想说明一个事——目标端入库这套并行机制不是实验室里的花架子60TB、12TB这种规模、还不能停机的核心业务真的是靠它硬扛下来的。规模会放大所有设计上的缺陷扛得住这种场面才说明这套入库架构是真扎实。调参这事儿我再啰嗦几句前面讲策略演进的时候没细说参数这里集中念叨下因为我踩过的坑大半跟参数有关。# 我实际调过的几类具体名字以你手上的版本为准 # 通道相关这是并行度的总开关 channels 16 # 单批大小多少条刷一次 applier.batch.size 2000 # 定时刷批毫秒防止数据一直捂着 # applier.flush.interval 500 # 语句缓存大小看命中率调 # statement.cache.size 500 # 是否开表拆分跨表大事务 # table.split.enable true # 是否开行哈希单表热点 # row.hash.enable true我调参的路子一般是这样先定通道数这个取决于目标库能扛多少并发连接和IO不是越多越好然后定批大小追数据阶段开大、稳态调小定时刷批务必配上缓存和路由策略根据数据分布选。每改一组就压一轮盯着三个东西——同步延迟、目标库CPU、目标库IO找到吞吐上去了但库还没被压垮的那个点。千万别一上来把所有参数都拉满那不是调优是赌博。一个变量一个变量来你才知道瓶颈到底在哪、每个参数起了什么作用。我早年图快通道、批次全开最大结果目标库IO直接打穿还查不出来是哪个参数惹的祸老老实实回退重来。这个教训送给你们。收尾把入库这件事想明白上下两篇写到这KFS目标端入库这套东西基本讲透了我简单收个尾。我的核心体会就一句数据同步的下半场拼的是目标端存得快不快。源端几百连接并行产生的变更目标端要靠一套精巧的机制才能追得上——单通道内部批量提交砍网络往返、小事务合并降prepare次数、语句缓存消重复解析、定量定时平衡吞吐与延迟这是把单次写入的效率榨干。跨通道并行从原始单通道到哈希、轮询、负载均衡、文件映射再到表拆分、行哈希、DDL安全包装一代代策略在并行度、顺序、均衡、安全这几个互相拉扯的目标之间做权衡让你能按场景组合出最合适的方案。这是把多核、多连接的并发能力彻底放开。再加上多通道复合位点挂了能精确恢复、保证一条不重不丢整套东西才算闭环。当然我也不把它吹成完美无缺。我这一路挑了不少批判性的刺——批量攒太大引入可见性延迟、小事务重排得守住行依赖的边界、语句缓存大小得看命中率调、事务拆分要想清楚原子性怎么兜、无主键表并行会退化、通道开太多数据库自己扛不住。这些都是用的时候要自己盯着的地方。工具抬高了下限但那些需要理解业务、需要权衡取舍的判断永远得人来做。如果你也在搞异构同步、被目标端写入卡着我建议的排查顺序就是这个思路先看单通道有没有把批处理和缓存这些低成本优化做到位再看通道数和路由策略是不是匹配你的数据分布——是跨表事务多还是单表热点决定了你该用表拆分还是行哈希最后别忘了DDL保护和恢复预案。这么一套下来至少割接那晚你不用眼睁睁看着增量死活追不平。入库快了心里才不慌。