- 云原生
- 后端
- 微服务
【免费下载链接】classicswarm
Swarm Classic: a container clustering system. Not to be confused with Docker Swarm which is at https://github.com/docker/swarmkit
Docker Classic Swarm(即本仓库 classicswarm)在提供多管理节点高可用(multi-manager HA)能力时,依赖一个独立的基础库 —— 位于仓库 vendor/github.com/docker/leadership 的leadership。它是一套构建在分布式 KV 存储之上的集群领导者选举(Leader Election)库,与docker/libkv配合,可同时支持 Consul、etcd 与 Zookeeper 三类后端。阅读本文后,你将掌握 Candidate(候选人)与 Follower(跟随者)两套 API 的完整用法、其容错与重试模型,以及它如何支撑 Swarm 中 Primary/Replica 管理节点的自动选主与故障切换。
什么是 leadership:面向集群环境的分布式选主库
leadership是用于集群领导者选举的 Go 库,其核心思路非常朴素:在分布式 KV 存储上创建一个带 TTL 的锁键(lock key),谁抢到锁,谁就是当前领导者;锁持有者不断续租,一旦持有者宕机或主动放弃,锁在 TTL 过期后被释放,其他候选人重新竞争。
该库基于docker/libkv构建,因此不绑定任何特定存储。libkv通过统一的 store.Store 接口 抽象出Put、Get、Watch、NewLock、AtomicPut等原子操作,而leadership只依赖其中NewLock(创建锁)与Watch(监听键变化)两个能力。在 libkv.go 中,libkv.NewStore会根据传入的后端字符串(consul、etcd、zk、boltdb)动态选择对应的客户端实现,从而让上层选举代码与具体存储完全解耦。
你可以通过store.NewStore("consul", ...)、store.NewStore("etcd", ...)或store.NewStore("zk", ...)接入相应后端。需要注意的是,虽然在store.go中注册了BOLTDB后端,但leadership文档明确说明其目标后端是 Consul、etcd 与 Zookeeper,实践时请以这三类为准。
核心角色:Candidate 与 Follower
leadership提供了两个互补的抽象:
- Candidate(候选人):参与选举、争夺领导权的角色。启动后异步运行选举算法,并在获得/失去领导权时通过 channel 推送状态变化。
- Follower(跟随者):不参与竞争,只实时监听“谁是领导者”的变化,适用于始终把请求转发给当前领导者的场景。
在 candidate.go 中,Candidate结构体持有客户端(store.Store)、选举键(key)、节点标识(node)、锁 TTL(lockTTL)以及一组用于同步的内部 channel(electedCh、stopCh、resignCh、errCh);而 follower.go 中的Follower则持有同样的客户端与选举键,外加当前领导者记录(leader string)和通知 channel(leaderCh)。
快速上手:运行一次选举(Candidate)
leadership的 README 给出了一个完整可运行的入门示例。首先创建一个基于libkv的存储客户端:
// Create a store using pkg/store. client, err := store.NewStore("consul", []string{"127.0.0.1:8500"}, &store.Config{}) if err != nil { panic(err) } underwood := leadership.NewCandidate(client, "service/swarm/leader", "underwood", 15*time.Second) electedCh, _ := underwood.RunForElection() for isElected := range electedCh { // This loop will run every time there is a change in our leadership // status. if isElected { // We won the election - we are now the leader. // Let's do leader stuff, for example, sleep for a while. log.Printf("I won the election! I'm now the leader") time.Sleep(10 * time.Second) // Tired of being a leader? You can resign anytime. candidate.Resign() } else { // We lost the election but are still running for leadership. // `elected == false` is the default state and is the first event // we'll receive from the channel. After a successful election, // this event can get triggered if someone else steals the // leadership or if we resign. log.Printf("Lost the election, let's try another time") } }这个例子中的三个关键参数值得逐一说明:
key(如"service/swarm/leader"):选举所依托的 KV 键,所有候选人都针对同一键竞争。由于锁键带有 TTL,它同时也是领导权持有状态的“心跳载体”。node(如"underwood"):候选人自身的唯一标识,赢得选举后被写入锁键的值(Value)。Follower 通过读取该值获知当前领导者是谁。ttl(如15*time.Second):锁的过期时间,即候选人在未续租情况下维持领导权的上限。参考 candidate.go 中的initLock实现,只有当你显式传入与defaultLockTTL(20 秒)不同的值时,TTL 才会被写入store.LockOptions,否则沿用底层默认值。
关于electedCh的语义,源码中的update方法与campaign循环(candidate.go)明确规定了三点:
- 首个事件必然是
false:候选人启动时先以“跟随者”身份运行(c.update(false)),表示“尚未获得领导权”; true表示抢锁成功:lock.Lock(nil)返回后调用c.update(true),此时该节点正式成为领导者;- 后续每次
false都代表领导权变更:可能是主动Resign()、被动被其他节点抢占(lostCh触发),也可能是收到Stop()退出信号。
值得注意的是,Resign()并不会让候选人退出竞争:它只是释放当前锁并立刻回到campaign循环重新争抢(candidate.go 注释明确写道 “Candidate will retry immediately to acquire the leadership. If no-one else took it, then the Candidate will end up being a leader again.”)。如果希望彻底退出,需要调用Stop()。
实时观察领导权变化(Follower)
跟随者模式用于在选举进行中实时感知领导者的变更:
follower := leadership.NewFollower(client, "service/swarm/leader") leaderCh, _ := follower.FollowElection() for leader := range leaderCh { // Leader is a string containing the value passed to `NewCandidate`. log.Printf("%s is now the leader", leader) } log.Fatal("Cannot follow the election, store is probably down") // Recovery code or exitleaderCh中推送的是字符串,即获胜候选人在NewCandidate时传入的node值。从 follower.go 的实现看,follow()调用client.Watch(f.key, f.stopCh)对选举键建立长连接监听,每次收到新的KVPair时,先与本地缓存的f.leader比较,只有值发生变化时才向leaderCh推送,从而避免重复通知。当Watch返回的 channel 被关闭时,Follower会向错误 channel 写入"Leader Election: watch leader channel closed, the store may be unavailable...",提示存储可能不可用。
一个典型的应用场景是:集群中的客户端始终把请求发送给当前领导者。在 Classic Swarm 中,Follower正是被 replica 管理节点用来跟踪 primary 的地址(见下文)。
容错设计:错误 channel 与自动重试
Candidate和Follower都同时返回一个错误 channel,用于在存储不可用等故障场景下实现弹性恢复。README 给出了一个带重试循环的容错写法:
func participate() { // Create a store using pkg/store. client, err := store.NewStore("consul", []string{"127.0.0.1:8500"}, &store.Config{}) if err != nil { panic(err) } waitTime := 10 * time.Second underwood := leadership.NewCandidate(client, "service/swarm/leader", "underwood", 15*time.Second) go func() { for { run(underwood) time.Sleep(waitTime) // retry } }() } func run(candidate *leadership.Candidate) { electedCh, errCh := candidate.RunForElection() for { select { case isElected := <-electedCh: if isElected { // Do something } else { // Do something else } case err := <-errCh: log.Error(err) return } } }这种“循环调用 +time.Sleep后重试”的模式是leadership官方推荐的容错姿势:当监听领导者键的 watch 因存储不可用而失败时,程序从run返回,等待waitTime后重新创建选举流程,从而在存储恢复后自动回归选举。这一点与campaign循环的设计相互印证:initLock在每次重试时都会关闭上一轮的stopRenewchannel(注释原文为 “Give up on the lock session if we recovered from a store failure”),避免旧会话残留。
源码剖析:campaign 状态机与锁的续租机制
把 README 的 API 说明与 candidate.go 的实现对照,可以看到整个选举过程是一个清晰的循环状态机:
- 进入跟随状态:
c.update(false)通知外部“当前不是领导者”; - 初始化锁:
initLock()构造store.LockOptions,将节点标识写入Value,将传入的 TTL 写入TTL,并创建一个RenewLock停止信号 channel(lockOpts.RenewLock = make(chan struct{})),随后调用client.NewLock(c.key, lockOpts); - 阻塞抢锁:
lock.Lock(nil)在 KV 存储上尝试获取互斥锁,抢到后返回lostCh(监听“锁丢失”的 channel); - 成为领导者:
c.update(true)广播领导权已获得; - 等待三类事件:
<-c.resignCh:收到Resign()请求,lock.Unlock()释放锁后回到第 1 步继续竞选;<-c.stopCh:收到Stop()请求,若当前是领导者则先释放锁,然后彻底退出(channel 被关闭);<-lostCh:锁被他人抢走或会话过期,静默回到第 1 步重新竞选。
选举所依赖的锁能力来自libkv的store.Locker接口(store.go):Lock(stopChan chan struct{}) (<-chan struct{}, error)与Unlock() error,以及store.LockOptions中的三个可选字段 ——Value(锁关联的值)、TTL(锁过期时间)、RenewLock(控制会话续租的 channel)。
以 Consul 后端为例,consul.go 中的NewLock实现展示了 TTL 与续租的具体语义:
- 它创建一个 ConsulSession,并将
Behavior设为SessionBehaviorRelease(会话过期即释放锁),LockDelay设为 1ms(几乎禁用锁延迟); - 由于 Consul 的 Session TTL 会按 2 倍计算,传给 Session 的 TTL 是
(ttl / 2); renewLockSession会启动一个后台协程,每隔ttl/2调用一次Session.Renew续租;即使续租失败,也会持续重试直到会话被显式销毁或 TTL 自然超时 —— 注释强调这保证了“锁不会被无限期持有,从而在存储不可用时优先保证活性(liveness)而非安全性(safety)”。
这套“锁 + TTL + 会话续租”的底层机制,正是选举库能在节点崩溃后自动让出领导权的根本原因:崩溃的领导人不再续租,TTL 到期后锁被释放,其他候选人即可通过抢锁完成接任。
在 Classic Swarm 中的真实应用:Primary/Replica 高可用
leadership并不是仓库里“沉睡”的 vendor 代码,而是 Classic Swarm 多管理节点高可用特性的核心引擎,其调用链集中在 cli/manage.go。
选举键与候选人的构造
在manage流程中,当开启--replication时,Swarm 会调用getCandidateAndFollower(cli/manage.go):
- 选举键路径为
path.Join(kvDiscovery.Prefix(), "docker/swarm/leader"),常量leaderElectionPath = "docker/swarm/leader"; - 候选人标识为管理节点的
--advertise地址(形如ip:port),候选人 TTL 来自--replication-ttl; - 值得注意的是,该函数要求 discovery 必须是基于 KV 的(
*kvdiscovery.Discovery),否则会直接log.Fatal报错:“Leader election is only supported with consul, etcd and zookeeper discovery.” 这与 docs/discovery.md 中的提示一致:某些 discovery 方式与 replica 复制不兼容,需要复制能力时应使用 KV 型 discovery。
选主与请求代理的联动
setupReplication(cli/manage.go)启动了三个协同工作的部分:
- 候选循环:
run()中调用candidate.RunForElection(),一旦electedCh收到true(“Cluster leadership acquired”),就在本地创建cluster.NewWatchdog并把 HTTP handler 切换为api.NewPrimary(primary 拥有管理集群、复制日志与事件的权限);收到false(“Cluster leadership lost”)时则切回api.NewReplica。 - 跟随循环:
follow()中调用follower.FollowElection(),每当leaderCh出现新地址,就调用replica.SetPrimary(leader)更新 primary 指向。 - 故障重试:两个循环都在退出后
time.Sleep(defaultRecoverTime)(默认 10 秒)再重新进入,与上一节的容错模式完全对应。
replica 的请求代理实现在 api/replica.go:ServeHTTP先判断路径是否属于本地路由(/_ping、/info、/debug),否则通过getPrimary获取当前 primary 地址并反向代理请求;如果getPrimary时 primary 尚未被选出,会借助sync.Cond阻塞等待,直到SetPrimary广播唤醒(若请求上下文被取消则返回 “No elected primary cluster manager” 错误)。若 replica 发现自己就是 primary(primary == p.addr),则直接本地处理,避免代理回环。
状态展示与故障切换验证
statusHandler.Status()(cli/manage.go)利用candidate.IsLeader()和follower.Leader()输出节点的角色信息:非领导者显示Role: replica与Primary: <leader地址>,领导者显示Role: primary。这正是docker info输出中Role与Primary两行的来源。
完整的多管理节点部署与故障切换演练见 docs/multi-manager-setup.md:通过swarm manage -H :4000 --replication --advertise <ip>:4000 consul://<consul>:8500/nodes依次创建 primary 与 replicas;当 primary 宕机(Ctrl-C或kill)后,某个 replica 会在日志中先后打出New leader elected: <新地址>与Cluster leadership acquired,自动接任 primary,期间无需任何人工干预。对应命令行参数定义在 cli/flags.go:--replication(启用复制)与--replication-ttl(默认"20s",含义为“Leader lock release time on failure”,即故障时锁释放的等待时间)。该文档同时提醒:--advertise地址必须以ip:port或hostname:port的形式给出(cli/manage.go 中的checkAddrFormat校验),且--replication-ttl必须为正数。
使用注意事项与适用前提
结合仓库实现,使用leadership时有几点需要留意:
- 后端限制:选举能力完全依赖
libkv的锁与 watch 能力,目前官方支持的组合为 Consul、etcd 与 Zookeeper;Classic Swarm 中则进一步要求 discovery 本身是 KV 型的(getCandidateAndFollower的强校验)。 - TTL 是故障恢复的关键参数:TTL 越短,领导者宕机后其他候选人的接管越快,但过短的 TTL 也可能在存储抖动或网络延迟下造成频繁的“假退位”;
--replication-ttl默认 20 秒即是这种权衡的默认值。 - 选举不是强一致共识协议:
leadership依赖 KV 存储自身的原子锁语义(如 Consul Session)而非 Paxos/Raft 类算法。它适用于 Classic Swarm 这类“单主写入、多副本代理”的场景,其故障恢复保证的是“活跃时快速接管”,而不是多副本间严格的强一致复制 —— 这一点从 docs/plan-for-production.md 对复制/HA 技术的描述中也可以得到佐证。 - Resign 与 Stop 语义不同:
Resign()只是让出当前领导权并立刻重新竞争;Stop()才是彻底退出选举并关闭 channel。按 README 的容错写法,electedCh被关闭后应视作流程结束并进入重试逻辑。
许可证说明
leadership采用 Apache License 2.0 许可,完整许可文本见仓库内的 vendor/github.com/docker/leadership/LICENSE。当你在自己的 Go 项目中引用或扩展该库时,请遵循相应的许可证义务。
- 云原生
- 后端
- 微服务
【免费下载链接】classicswarm
Swarm Classic: a container clustering system. Not to be confused with Docker Swarm which is at https://github.com/docker/swarmkit
相关推荐
WebGAL入门指南:如何用可视化编辑器制作你的第一个视觉小说
WebGAL入门指南:如何用可视化编辑器制作你的第一个视觉小说 WebGAL是一款全新的网页端视觉小说引擎,无需复杂编程知识,就能让你轻松创作属于自己的互动故事
游戏开发前端Docker Classic Swarm 中的 etcd 发现服务:分布式键值存储接入实战与原理
Docker Classic Swarm 中的 etcd 发现服务:分布式键值存储接入实战与原理 导读 本文以当前仓库 classicswarm https:/
云原生后端微服务Docker Classic Swarm 调度策略深度解析:spread、binpack 与 random 的原理、选型与实战
Docker Classic Swarm 调度策略深度解析:spread、binpack 与 random 的原理、选型与实战 Docker Classic S
云原生后端微服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考