3. 分布式调度核心:分布式协调、Leader选举与任务分片
好,咱们进入第三章节。前面聊了调度模型和状态管理,那都是单机视角。一旦把调度系统放到集群里,问题就来了——谁说了算?任务怎么分?节点挂了怎么办?
说白了,分布式调度最怕的就是「脑裂」。两个调度器都觉得自己是老大,同时下发指令,那任务就乱套了。我早期参与过一个实时数仓项目,就因为协调层没做好,两个节点同时调度同一个数据源,结果数据重复计算,对账对了一星期……嗯,从那以后我对分布式协调这块就特别敏感。
3.1 分布式协调:选 ZooKeeper 还是 Etcd?
先聊聊协调层选型。目前主流就两个:ZooKeeper 和 Etcd。很多人问我哪个好,我的回答是——看场景。
| 对比维度 | ZooKeeper | Etcd |
|---|---|---|
| 一致性算法 | ZAB (ZooKeeper Atomic Broadcast) | Raft |
| 数据模型 | 树形 ZNode,支持临时节点 | KV 存储,支持目录结构 |
| Watch 机制 | 一次性触发,需重新注册 | 持续 Watch,支持历史版本 |
| 性能 | 读性能好,写性能一般 | 读写均衡,延迟更低 |
| 运维复杂度 | 依赖 Java,配置较多 | Go 编写,部署简单 |
| 典型场景 | Hadoop/Spark 生态、传统调度 | Kubernetes、云原生调度 |
3.2 Leader 选举:谁当老大?
分布式调度必须有个 Leader。为什么?因为多个调度器同时决策,任务分配会冲突。Leader 选举就是解决这个问题的。
选举算法有很多,但生产环境最常用的是 Bully 算法 和 Raft 选举。我简单说下核心逻辑:
- 节点启动时:向协调层注册,创建临时节点(ZooKeeper)或租约(Etcd)。
- 竞选阶段:所有节点尝试创建同一个锁节点,谁成功谁就是 Leader。
- 心跳维持:Leader 定期续约,如果挂了,临时节点自动消失。
- 重新选举:其他节点监听到 Leader 消失,重新发起竞选。
你想想看,这个过程其实很像「抢凳子」游戏。谁先抢到锁,谁就坐上去。但有个坑——脑裂。如果网络抖动,Leader 的心跳没及时续约,其他节点以为它挂了,选出了新 Leader。结果旧 Leader 又活过来了……两个 Leader 同时存在,这就是灾难。
下面是一个基于 Etcd 的 Leader 选举伪代码,我习惯用 Go 写调度组件:
// 基于 Etcd 的 Leader 选举
func electLeader(client *etcd.Client, key string) {
lease := client.NewLease(10) // 10秒租约
lease.Grant()
// 尝试创建锁
txn := client.Txn().
If(notExist(key)).
Then(put(key, nodeID, lease)).
Else(get(key))
resp := txn.Commit()
if resp.Succeeded {
// 成为 Leader
startScheduler()
// 定期续约
go keepAlive(lease)
} else {
// 成为 Follower,监听 Leader 变化
watch(key)
}
}
3.3 任务分片:把大任务切成小任务
Leader 选出来了,接下来就是分任务。任务分片的核心思想是:把一个大任务拆成多个小片,分给不同的 Worker 执行。
分片策略有很多种,我列几个常用的:
- Hash 分片:按数据 Key 的 Hash 值取模,分配到不同分片。简单,但数据倾斜时负载不均。
- 范围分片:按数据范围划分,比如日期、ID 区间。适合有序数据,但热点问题明显。
- 轮询分片:轮流分配,负载最均衡,但数据局部性差。
- 动态分片:根据 Worker 的实时负载动态调整分片大小。复杂,但效果好。
我在流批一体调度里,最常用的是 动态分片 + 一致性 Hash。为什么?因为流任务的数据量波动大,静态分片容易导致某些 Worker 撑爆,某些 Worker 闲着。
举个例子,假设我们有 100 个数据分区,4 个 Worker。一致性 Hash 会把分区映射到一个环上,每个 Worker 负责环上的一段。当 Worker 增减时,只影响相邻的分区,不需要全量重分。
3.4 负载均衡:让每个 Worker 都不闲着
分片是静态的,负载均衡是动态的。即使分片策略再好,运行过程中也会出现负载倾斜——某个 Worker 处理的数据突然变多,或者某个 Worker 变慢了。
负载均衡的核心是 重新分配。调度器需要定期检查每个 Worker 的负载情况,如果发现某个 Worker 过载,就把它的一部分分片迁移到其他 Worker。
我常用的负载均衡策略:
- 最小连接数:优先把新分片分配给当前任务数最少的 Worker。
- 加权轮询:根据 Worker 的 CPU、内存、网络等资源权重分配。
- 自适应均衡:根据历史处理速度动态调整权重。
下面是一个简单的负载均衡流程图:
负载均衡不是越频繁越好。我见过有人每秒都做均衡,结果分片来回迁移,系统开销比收益还大。我的经验是:每 30-60 秒检查一次,连续 3 次过载才触发迁移。这样可以避免抖动。
3.5 总结一下
分布式协调、Leader 选举、任务分片和负载均衡,这四个东西是分布式调度的骨架。没有它们,调度系统就是一堆散沙。
我个人习惯把协调层看作「神经系统」,Leader 是「大脑」,分片是「肌肉」,负载均衡是「反射调节」。四者配合好了,系统才能稳定高效地跑起来。
嗯,这一章就到这里。记住一句话:分布式调度最难的不是技术,而是应对不确定性。网络会抖,节点会挂,数据会倾斜——你的系统必须能优雅地处理这些「意外」。