图解 Fluss(四):分布式协调 ------ 选举、副本状态机与 Rebalance
阅读本文你将了解: 三个 Coordinator 如何选出唯一 Leader 且不脑裂、一个 Tablet 副本的状态迁移路径、节点宕机时数据为什么不会丢、以及扩容时 Fluss 如何在不中断服务的前提下搬移数据。
配套图表:
seq-04-startup-leader-election、state-01-replica、state-02-coordinator、seq-05-rebalance难度:⭐⭐⭐⭐ | 适合人群:负责集群稳定性与容量规划的 SRE / 架构师
一、场景:大促前的三个运维动作
大促前一周,你要对 Fluss 集群做三件事:
动作一:冷启动整个集群
机房搬迁,所有节点关机再开机。开机顺序是什么?三个 Coordinator 同时启动,谁当 Leader?会不会出现两个 Leader?
动作二:应对节点宕机
大促期间一台 TabletServer 宕机了。它上面有 2000 个 Tablet,其中 300 个是 Leader。这些 Tablet 会发生什么?数据会丢吗?业务会中断多久?
动作三:紧急扩容
流量比预期高 3 倍,需要临时加 10 台 TabletServer。加了之后,数据会自动均衡过去吗?会不会因为大量数据迁移把网络打满,反而影响线上业务?
这三个动作,对应的正是本篇的四张图。
二、图 1:启动与 Leader 选举时序图

图里有两个 Coordinator 实例 A 和 B,完整演示了选举过程。
2.1 阶段 1:Standby 模式启动
A -> A : initCoordinatorStandby() 启动 RPC/元数据/调度器
A -> ZK : registerCoordinatorServer() (临时节点)
ZK --> A : 注册成功
第 1 篇讲过,Standby 阶段只起基础设施。关键点是 registerCoordinatorServer() 创建的是临时节点(ephemeral):
/fluss/coordinator/servers/
├── server_0000000001 ← A 注册,ephemeral
├── server_0000000002 ← B 注册,ephemeral
└── server_0000000003 ← C 注册,ephemeral
为什么必须是临时节点? 因为 ZK 的临时节点在会话断开时会自动删除。Coordinator A 宕机 → 会话超时 → 节点自动消失 → 集群立刻感知到"A 不在了"。如果用持久节点,还需要额外的心跳检测机制。
2.2 阶段 2:Leader 选举
这是整张图最精妙的部分。
A -> ZK : createEphemeralSequential(/coordinator_election/candidate_)
ZK --> A : 节点路径 candidate_001
B -> ZK : createEphemeralSequential(/coordinator_election/candidate_)
ZK --> B : 节点路径 candidate_002
A -> ZK : getChildren(选举路径)
ZK --> A : [candidate_001, candidate_002]
A -> A : 序号 001 最小 → 自己是 Leader
B -> ZK : getChildren(选举路径)
ZK --> B : [candidate_001, candidate_002]
B -> B : 序号 002 非最小 → Watch candidate_001
三个关键设计:
① ephemeral_sequential(临时顺序节点)
ZK 会自动在节点名后追加一个单调递增的序号:
/coordinator_election/
├── candidate_0000000001 ← A
├── candidate_0000000002 ← B
└── candidate_0000000003 ← C
"序号最小者为 Leader" 是一个无需协调的确定性规则------每个节点拿到 getChildren() 的结果后,自己就能判断出谁是 Leader,不需要额外的投票轮次。
同时因为是 ephemeral 节点,Leader 宕机后它的候选节点自动消失,下一个序号的节点自然成为新的最小者。
② Watch 前一个节点(避免羊群效应)
注意图里 B 的行为:
B -> B : 序号 002 非最小 → Watch candidate_001
B watch 的是 candidate_001(它的前一个节点),而不是所有节点,也不是 Leader 节点。
这个细节叫"链式 watch",它解决的是羊群效应(Herd Effect):
错误的做法:所有非 Leader 节点都 watch Leader 节点
→ Leader 宕机时,ZK 要向 N-1 个节点同时发送通知
→ N-1 个节点同时被唤醒,同时发起 getChildren() 查询
→ 瞬间产生 N-1 倍的 ZK 读请求 → ZK 被打爆
正确的做法(Fluss 采用):每个节点只 watch 它前面那一个
→ Leader (001) 宕机时,ZK 只需要通知 002
→ 002 成为新 Leader
→ 如果 002 也宕机,通知 003,以此类推
→ 通知复杂度 O(1),而不是 O(N)
这个模式和 Zookeeper 官方的分布式锁实现(Curator 的 LeaderLatch)是一致的。
③ ZK Fence(防脑裂)
A -> ZK : fenceBecomeCoordinatorLeader() (epoch +1, 防脑裂)
ZK --> A : ZkEpoch(newEpoch)
只有成为 Leader 之后,才会执行 fenceBecomeCoordinatorLeader(),把 ZK 上记录的 epoch 加一。
图下方的 note 总结了这三点:
ZooKeeper 选举关键设计:
1. ephemeral_sequential 节点自动处理崩溃退出
2. Watch 前一个节点 (非所有节点) → 避免羊群效应
3. fenceBecomeCoordinatorLeader 用 epoch 防脑裂
2.3 阶段 3:Leader 初始化
A -> A : initCoordinatorLeader() 创建 EventProcessor/AutoPartition/ChannelManager
A -> ZK : registerCoordinatorLeader() (临时节点)
A -> ZK : createDefaultDatabase("fluss")
A -> TS : 开始下发 Tablet 分配指令
注意 registerCoordinatorLeader() 创建的是另一个临时节点:
/fluss/coordinator/leader → {serverId: "server_1", epoch: 42, address: "..."}
这个节点的作用是让TabletServer 和客户端能找到 Leader。TabletServer 启动时 watch 这个节点,Leader 变更时立刻感知并重新注册。
2.4 回答"动作一"
机房搬迁后冷启动:
1. 先启动 ZooKeeper 集群(3 或 5 节点) ← 必须最先
2. 再启动 3 个 CoordinatorServer ← 自动选主,谁先注册谁序号小
3. 最后启动 N 个 TabletServer ← 向 Coordinator 注册,等待分配
会不会出现两个 Leader? 不会,因为:
- 顺序节点的序号由 ZK 保证全局唯一且单调
- "序号最小者为 Leader"是确定性规则
- 即使发生网络分区,旧 Leader 的 epoch 已过期,它下发的指令会被 TabletServer 拒绝
三、图 2:Coordinator 状态机

时序图看的是"一次成功的选举过程",状态图看的是"所有可能的生命周期路径"。
3.1 四个状态
[*] --> Standby : 进程启动
Standby --> Electing : startElectLeaderAsync()
Electing --> Leader : 选举成功 (序号最小 + ZK Fence)
Electing --> Standby : 选举失败 (等待前节点删除)
Leader --> Standby : 失去 Leader (会话超时/主动释放)
Standby --> Down : closeAsync()
Leader --> Down : closeAsync()
Down --> [*] : 资源清理完成
注意 Electing 是一个显式的中间状态。这一点很重要------很多人的直觉是"启动 → 竞选 → 成功就 Leader",但实际上竞选可能失败,失败后要回到 Standby 继续等待。
3.2 每个状态持有什么资源
图里两个 note 精确列出了差异:
Standby 保留(所有节点都有):
- ZooKeeper 连接
- RpcServer (健康检查)
- MetadataManager (只读)
- DynamicConfigManager (监听配置)
Leader 特有(只有一个节点有):
- CoordinatorEventProcessor (单线程事件循环)
- CoordinatorChannelManager (连接 TabletServer)
- AutoPartitionManager (自动分区)
- RpcClient (主动连接)
这个资源划分是第 1 篇"两阶段启动"的直接体现 :initCoordinatorStandby() 建前者,initCoordinatorLeader() 建后者。
3.3 状态迁移时的资源操作
| 迁移 | 操作 |
|---|---|
Standby → Electing |
创建顺序节点、getChildren、判断序号 |
Electing → Leader |
fence 递增 epoch → initCoordinatorLeader() 建 Leader 资源 → 注册 leader 节点 |
Electing → Standby |
watch 前一个节点,进入等待 |
Leader → Standby |
cleanupCoordinatorLeader() 按逆序清理 Leader 资源 → 重新参与选举 |
图里的 note 特别强调:
失去 Leader 时按逆序清理这些资源
为什么要"逆序"? 因为资源之间有依赖:
创建顺序:RpcClient → ChannelManager → AutoPartitionManager → EventProcessor
销毁顺序:EventProcessor → AutoPartitionManager → ChannelManager → RpcClient
如果先关掉 RpcClient,正在处理事件的 EventProcessor 就会因为连接断开而抛异常。逆序销毁保证上层消费者先停,下层依赖后关。
3.4 代码骨架
java
private void electCoordinatorLeaderAsync() {
coordinatorLeaderElection.startElectLeaderAsync(
// 成为 Leader 的回调
() -> {
try {
ZkEpoch epoch = zkClient.fenceBecomeCoordinatorLeader();
if (epoch == null) {
// fence 失败,说明有更新的 epoch,放弃
return;
}
initCoordinatorLeader();
zkClient.registerCoordinatorLeader();
zkClient.createDefaultDatabase(DEFAULT_DATABASE);
} catch (Exception e) {
// 初始化失败,主动释放 Leader 身份,重新选举
cleanupCoordinatorLeader();
}
},
// 失去 Leader 的回调
() -> cleanupCoordinatorLeader()
);
}
四、图 3:Replica 副本状态机

Coordinator 管元数据,真正的数据可靠性由 Replica 状态机保证。
4.1 五个状态
[*] --> NewReplica : Tablet 创建
NewReplica : 刚分配,尚未加载;LogTablet/KvTablet 未打开
OnlineReplica : 已加载数据,可服务;等待 Leader/Follower 角色
LeaderReplica : 处理读写请求;管理 ISR 列表;推进 High Watermark
FollowerReplica : 从 Leader 拉取日志;上报同步进度;等待晋升为 Leader
OfflineReplica : 暂时不可用;可能因故障/网络分区
4.2 迁移路径
NewReplica --> OnlineReplica : 加载完成 (becomeOnline)
OnlineReplica --> LeaderReplica : becomeLeader (Leader 选举成功)
OnlineReplica --> FollowerReplica : becomeFollower (跟随 Leader)
LeaderReplica --> FollowerReplica : 失去 Leader (被新的 Leader 取代)
FollowerReplica --> LeaderReplica : 晋升 (原 Leader 故障)
LeaderReplica --> OfflineReplica : 故障 / 网络分区
FollowerReplica --> OfflineReplica : 故障 / 同步超时
OfflineReplica --> OnlineReplica : 恢复 (重新加载)
OnlineReplica --> [*] : Tablet 删除
注意 NewReplica 和 OnlineReplica 的分离 :副本分配(Coordinator 决定)和副本加载(TabletServer 执行)是异步的两步。Tablet 被分配到某台机器,不代表它立刻能服务------要等 LogTablet 打开、索引 mmap、RocksDB 打开完成。
4.3 Leader 与 Follower 的职责
图里的两个 note:
Leader 职责:
- 接收客户端写入 (append)
- 管理 ISR (加入/移除 Follower)
- 推进 HW = min(所有 ISR 的 LEO)
Follower 职责:
- ReplicaFetcher 拉取日志
- 验证 Leader Epoch
- 落后太多时通过快照追平
"验证 Leader Epoch"这一条值得展开。
这是防止"日志分叉(log divergence)"的关键机制。考虑这个场景:
1. Leader A (epoch=5) 写入 offset 100-110,还没同步给 Follower
2. A 宕机
3. Follower B 晋升为新 Leader (epoch=6),它的日志只到 offset 100
4. B 从 offset 100 开始写入新数据:offset 100-105(内容不同!)
5. A 重启,它的日志有 offset 100-110
6. A 作为 Follower 向 B 拉取数据 ------ 如果不检查,A 会保留自己的 100-110,
导致同一个 offset 在两个副本上内容不同 → 日志分叉
解决办法是 Leader Epoch 校验:
A 作为 Follower 向 B 请求:我要从 offset 100 开始拉,我的 epoch 是 5
B 查询 epoch 5 对应的最后 offset 是 100,返回 OK
A 发现自己 offset 100 之后的数据(101-110)不在有效范围内 → 截断
A 从 B 重新拉取 epoch 6 的数据
这个机制和 Kafka 的 LeaderEpoch 完全一致(KIP-101 / KIP-279)。
4.4 ISR 的动态调整
Leader 会持续监控每个 Follower 的同步进度:
java
// 伪代码:Leader 侧的 ISR 管理
void maybeShrinkIsr() {
long leaderLagMs = ...; // 配置:replica.lag.time.max.ms(默认 30s)
for (Replica follower : assignedReplicas) {
if (now - follower.lastCaughtUpTimeMs > leaderLagMs) {
// Follower 落后超过 30 秒 → 移出 ISR
isr.remove(follower);
}
}
}
void maybeExpandIsr() {
for (Replica follower : assignedReplicas) {
if (follower.logEndOffset >= highWatermark && !isr.contains(follower)) {
// Follower 追上 HW → 重新加入 ISR
isr.add(follower);
}
}
}
配置 replica.lag.time.max.ms 的权衡:
| 设小(如 10s) | 设大(如 60s) |
|---|---|
慢副本快速被剔除,acks=all 不受拖累 |
副本容忍抖动,不会频繁进出 ISR |
| 风险:网络偶发抖动导致 ISR 频繁收缩 | 风险:真的慢副本拖慢 acks=all 的响应 |
生产建议:默认 30s,网络环境差的机房可放宽到 60s。
4.5 回答"动作二"
一台 TabletServer 宕机,上面 2000 个 Tablet(300 个 Leader):
T+0s ZK 会话超时(默认 18s),节点被摘除
↓
T+18s Coordinator 感知,标记这些 Tablet 的副本为 OfflineReplica
↓
T+18s 对每个失去 Leader 的 Tablet:
从 ISR 中选一个 Follower 晋升为 LeaderReplica
(优先选 LEO 最大的,保证数据最新)
↓
T+19s 新 Leader 开始接收读写
ISR 收缩(少了宕机那台的副本)
↓
T+?? 如果宕机节点恢复:
→ NewReplica → OnlineReplica → FollowerReplica
→ 追平 LEO → 重新加入 ISR
数据会丢吗? 只要写入时用的是 acks=all 且 min.insync.replicas >= 2,就不会丢------因为 acks=all 保证数据至少写进了所有 ISR 副本,而 ISR 里的副本即使不是 Leader,也持有完整数据。
业务中断多久? 主要取决于 ZK 会话超时时间:
properties
# 缩短故障发现时间(但会增加误判风险)
zookeeper.session-timeout: 10s
replica.lag.time.max.ms: 30s
调优提醒 :把
zookeeper.session-timeout设得太小(如 3s),一次 Full GC 就可能导致误判节点宕机,触发不必要的 Leader 切换。生产环境建议不低于 10s。
五、图 4:Rebalance 渐进式迁移

5.1 触发阶段
Coord -> Rebal : checkAndTriggerRebalance()
Rebal -> Rebal : 检测负载不均衡 (标准差 > 阈值) 或新节点加入
Rebal -> Rebal : generateRebalancePlan() 计算最优 Tablet 分布
Rebal -> Rebal : 生成迁移计划 (最小迁移成本)
Rebal --> Coord : RebalancePlan
两种触发条件:
- 新节点加入:集群扩容时自动触发
- 负载不均衡:各 TabletServer 的 Tablet 数量标准差超过阈值
properties
# 触发阈值配置
tablet-server.rebalance.trigger.threshold: 0.1 # 标准差比例超过 10% 触发
tablet-server.rebalance.check.interval: 5min
"最小迁移成本"是什么? Rebalance 规划是一个带约束的优化问题:
目标:minimize(迁移的 Tablet 数 × 每个 Tablet 的数据量)
约束:1. 每个 Tablet 的副本不能集中在同一台机器
2. 迁移后各节点的负载标准差 < 阈值
3. 单个节点同时进行的迁移数 < 上限
5.2 执行阶段:渐进式,分批
图里最关键的结构是这个 loop:
loop 每批迁移 (20% Tablet)
...
end
为什么是渐进式而不是一次性全搬?
一次性迁移的问题:
假设要迁移 10000 个 Tablet,总计 10TB 数据:
- 网络带宽被打满 → 线上业务的读写延迟飙升
- 磁盘 IO 被打满 → 正常读写受影响
- 万一中途出错,回滚困难
渐进式的好处:
每批只迁移 20%:
- 网络和磁盘压力可控
- 每批完成后检查集群稳定性,异常就停止
- 支持中途回滚(只回滚当前批次)
5.3 单个 Tablet 迁移的三步
图中每个 loop 内部有三个子阶段:
① 副本同步
Src -> Dst : 复制 Tablet-X 日志 (作为 Follower)
Dst -> Src : 追平 Leader (LEO 追上)
Dst -> ZK : 请求加入 ISR
ZK --> Dst : ISR 更新成功
注意:迁移的第一步是"在目标机器上增加一个 Follower 副本",而不是"直接搬数据"。 这个顺序保证了任何时刻都有完整的副本可用。
② Leader 切换
Coord -> ZK : 更新 Tablet-X Leader = Server-3
ZK --> Coord : Leader 元数据更新
Coord -> Dst : 晋升为 Leader
Dst --> Coord : 晋升完成
因为 Dst 已经追平了 LEO 并在 ISR 里,这次切换是"干净"的------不丢数据,业务侧只会感受到毫秒级的抖动(和上面"节点宕机"的切换是同一个机制,但更快,因为是主动切换)。
③ 清理旧副本
Coord -> Src : 删除 Tablet-X 旧副本
Src --> Coord : 删除完成
注意清理发生在最后,这保证:如果第 ② 步失败,旧副本还在,可以回退。
5.4 图下方 note 的总结
渐进式迁移三步:
1. Follower 同步 → 追上 Leader
2. 加入 ISR → Leader 切换
3. 删除旧副本
分批执行避免集群压力, 支持中途回滚
5.5 回答"动作三"
加 10 台 TabletServer 后:
T+0min 新节点注册到 Coordinator
T+5min checkAndTriggerRebalance 检测到负载不均衡
生成迁移计划(从老节点搬一部分 Tablet 到新节点)
T+5min 开始第 1 批(20%)
- 每个被迁的 Tablet 先在新节点建 Follower
- 追平后切 Leader
- 删老副本
T+?? 第 1 批完成,检查集群稳定性
继续第 2 批 ...
T+?? 全部完成,集群负载均衡
会打满网络吗? 可以通过限流控制:
properties
# 限制单个 TabletServer 用于副本同步的带宽
tablet-server.replica.fetch.max.bytes: 10MB
tablet-server.rebalance.max.concurrent.migrations: 5 # 单节点并发迁移数
tablet-server.rebalance.batch.ratio: 0.2 # 每批比例
六、动手验证
6.1 观察选举过程
bash
# 启动 Coordinator,观察日志
tail -f logs/coordinator-server.log | grep -E "Standby|Elect|Leader"
# 预期看到
# [main] initCoordinatorStandby - Starting coordinator server in standby mode
# [main] CoordinatorLeaderElection - Created candidate node: candidate_0000000001
# [main] CoordinatorServer - Became coordinator leader with epoch 1
# [main] initCoordinatorLeader - Starting coordinator leader services
6.2 手动触发 Leader 切换
bash
# 找到当前 Leader
zkCli.sh get /fluss/coordinator/leader
# kill 掉 Leader 进程
kill -9 <leader_pid>
# 观察谁接任(应该在 10-20 秒内完成)
zkCli.sh get /fluss/coordinator/leader
6.3 观察 Rebalance
bash
# 查看当前 Tablet 分布
curl http://coordinator:9124/metrics | grep tablet_distribution
# 手动触发 Rebalance(如果有管理接口)
curl -X POST http://coordinator:9124/rebalance
# 观察迁移进度
curl http://coordinator:9124/metrics | grep -E "rebalance|migrat"
6.4 观察 ISR 变化
bash
curl http://tablet-server:9125/metrics | grep isr
关注指标:
fluss_replica_isr_count # 每个 Tablet 的 ISR 大小
fluss_replica_under_replicated # 副本数不足的 Tablet 数(应该为 0)
fluss_replica_leader_count # 每台机器上的 Leader 数(应该均衡)
七、生产实践要点
7.1 部署建议
| 项目 | 建议 | 原因 |
|---|---|---|
| Coordinator 数量 | 3 或 5(奇数) | ZK 选举需要多数派 |
| ZK 集群 | 独立部署,3 或 5 节点 | 混部会因 IO 竞争导致误判 |
| ZK session timeout | 10 - 18s | 太小易误判,太大故障恢复慢 |
| TabletServer 数量 | ≥ 3 | 保证副本能分散 |
| 副本数 | 3 | 2 副本在滚动重启时会降级 |
7.2 关键配置
properties
# coordinator-server.yaml
zookeeper.session-timeout: 15s
zookeeper.connection-timeout: 10s
coordinator.rebalance.check-interval: 5min
coordinator.rebalance.trigger-threshold: 0.1
# tablet-server.yaml
replica.lag.time.max.ms: 30000
replica.fetch.max.bytes: 10MB
replica.fetch.min.bytes: 1KB
replica.fetch.wait.max.ms: 500
# 客户端(重要!)
client.request.acks: all
tablet-server.min.insync.replicas: 2
7.3 滚动重启的正确顺序
升级集群时,按这个顺序能避免不必要的 Leader 切换:
1. 先滚动重启 TabletServer(一次一台)
- 等该节点的 Tablet 全部回到 OnlineReplica 且 ISR 恢复
- 再重启下一台
2. 最后重启 Coordinator
- 先重启 Standby 节点
- 最后重启 Leader(会触发一次 Leader 切换)
千万不要先重启 Coordinator Leader------这会导致所有 TabletServer 重新注册,产生大量元数据变更事件。
7.4 容量规划中的副本开销
有效容量 = 裸容量 / 副本数 / 空间放大系数
举例:10 台机器,每台 2TB SSD
裸容量 = 20TB
副本数 = 3
RocksDB 空间放大 ≈ 1.5(PK 表)
有效容量 ≈ 20 / 3 / 1.5 ≈ 4.4TB(PK 表)
≈ 20 / 3 ≈ 6.7TB(Log 表)
规划时还要留出 30% 余量给 compaction 和 Rebalance 的临时空间。
八、排障手册
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 集群一直无 Leader | ZK 不可达或选举路径被污染 | zkCli.sh ls /fluss/coordinator/election;检查是否有残留的 candidate_ 节点 |
| Leader 频繁切换 | GC 停顿 / 网络抖动 / ZK 压力 | 查 GC 日志;检查 ZK 的 avg latency;适当调大 session timeout |
| 出现两个 Leader(epoch 相同) | ZK 脑裂 | 检查 ZK 集群健康度(是否过半数存活) |
under_replicated 指标持续 > 0 |
有副本卡在 Offline | 检查对应节点的磁盘和 GC;看 replica.lag.time.max.ms 是否太小 |
| Rebalance 一直不触发 | 标准差未超阈值 | 调小 trigger-threshold;或手动触发 |
| Rebalance 卡住不推进 | 目标节点磁盘满 / 网络限流 | 检查目标节点磁盘水位;检查 rebalance.max.concurrent.migrations |
| 迁移期间业务延迟飙升 | 迁移限速不当 | 调低并发迁移数和 replica.fetch.max.bytes |
| 节点恢复后一直追不上 | 落后太多需要快照同步 | 看日志有没有 "snapshot" 相关;检查网络带宽 |
九、小结
四张图串起 Fluss 的分布式协调机制:
Leader 选举(图 1 + 图 2)
ephemeral_sequential节点 + "序号最小者为 Leader" → 无需投票轮次- 每个节点只 watch 前一个节点 → 避免羊群效应,通知复杂度 O(1)
fenceBecomeCoordinatorLeader()递增 epoch → 防止旧 Leader 诈尸- 两阶段启动:Standby 建基础设施,Leader 才建协调资源,销毁时逆序
副本状态机(图 3)
- 五态:New → Online → Leader/Follower → Offline
HW = min(ISR 的 LEO)是"已提交"的边界- Leader Epoch 校验防止日志分叉
- ISR 动态调整,滞后超时(默认 30s)则剔除
Rebalance(图 4)
- 渐进式分批(每批 20%),避免网络和磁盘被打满
- 单个 Tablet 迁移三步:先加 Follower 追平 → 再切 Leader → 最后删旧副本
- 清理在最后,保证任何一步失败都能回退
三句话记住:
- 选举:顺序节点定序 + 链式 watch + epoch fence,三个设计缺一不可。
- 副本 :
acks=all+min.insync.replicas=2是不丢数据的底线,HW 是已提交边界。 - 迁移:先加副本再切主最后删旧,任何时候都有完整副本,这是"不中断服务"的关键。
下一篇是最后一篇,我们把视角拉回到数据怎么进来、怎么演进、怎么归档------Flink Connector、表生命周期与冷热分层。