Golang分布式应用中如何解决脑裂问题

Golang分布式应用中如何解决脑裂问题 我正在开发一个分布式应用程序,需要选举一个主节点来为集群调度一系列任务。Etcd 在这方面表现良好,但我该如何处理脑裂问题?

假设某个节点被选举为主节点,随后由于某种原因失去了租约,另一个节点接替了主节点身份。在 Etcd 中一切正常,它能准确知道新的主节点是谁。问题在于,当新主节点接管时,旧主节点会收到通知告知它不再是主节点,因此在短时间内两者都会同时工作,这可能导致应用程序出现异常行为。

以下是选举主节点的代码片段:

func (e *Election) Elect(ctx context.Context) context.Context {
	session, err := concurrency.NewSession(e.Client.base, concurrency.WithTTL(e.TTL))
	if err != nil {
		nctx, nctxCancel := context.WithCancel(context.Background())
		nctxCancel()
		return nctx
	}

	nctx, nctxCancel := context.WithCancel(ctx)
	election := concurrency.NewElection(session, "/election")
	if err = election.Campaign(nctx, e.NodeID); err != nil {
		nctxCancel()
		return nctx
	}

	go func() {
		for range election.Observe(nctx) {
		}
		nctxCancel()
	}()

	return ctx
}

有什么建议吗?


更多关于Golang分布式应用中如何解决脑裂问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

7 回复

更多关于Golang分布式应用中如何解决脑裂问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


diego.bernardes:

多么希望 Go 能像 Erlang 那样拥有监控树机制

虽然实现方式不同,而且在 Go 中无法完全复现,但我推荐使用 thejerf/suture 来实现类似功能。

刚刚发现ZooKeeper也无法保证这一点:

Piyg

ZooKeeper多领导者选举问题

标签: java, apache-zookeeper, distributed, distributed-computing

我正在使用 etcd,它的功能与 zookeeper 相同。可能我之前表述得不够清楚,问题在于当被选举出的主节点因某些原因(网络问题、系统负载等)未能及时更新租约而失去主节点身份,同时另一个节点被选举为新的主节点时。

在短暂但确实存在的时间段内,旧主节点仍然认为自己是主节点并继续以主节点身份运行,而新选举出的主节点也开始承担主节点职责并开始运行。在这段时间内,集群可能会出现异常行为。

目前在生产环境中尚未遇到此问题。通过查看代码,发现这种情况有可能发生。但由于我没有找到简单的解决方案,我打算先冒这个风险,等问题实际发生后再解决。

[@diego.bernardes](https://forum.golangbridge.org/u/diego.bernardes),这个可能你会感兴趣:

en.wikipedia.org

LampFlowchart

Raft(计算机科学)

Raft是一种共识算法,旨在作为Paxos的替代方案。它通过逻辑分离的方式比Paxos更易于理解,同时也被正式证明是安全的,并提供了一些额外功能。Raft提供了一种在计算系统集群中分布式状态机的通用方法,确保集群中的每个节点就相同的状态转换序列达成一致。它有许多开源参考实现,包括完整规范实现。

GitHub

GitHub Avatar

hashicorp/raft

raft - Raft共识协议的Golang实现

Consul应该会实现这个。

是的,这正是问题所在。我想到了一个解决方法,虽然不够优雅,但应该能行。应用程序有一个结构体用于生成和维护Etcd租约,租约设有TTL,我之前使用Lease.KeepAlive来保持租约活跃,但在循环中使用Lease.KeepAliveOnce会更合理。前者只需调用一次,租约会在Etcd Go客户端内部刷新,我无法过多控制;而后者我可以自主控制租约的刷新方式。

假设租约的TTL为60秒,我可以每30秒执行一次租约刷新。如果出现任何问题,应用程序会取消上下文并撤销租约,这样我就有30秒的容错时间。

以下是应用程序当前的工作方式:

.
└── registry              租约1
    ├── election          租约2
    │   └── scheduler     租约2
    └── runner            租约3
        └── runner-unit   租约3

基本上我使用了三个独立的租约来控制所有环节。如果租约1被撤销,应用程序会调用停止函数来终止子任务。对于租约2租约3,如果租约被撤销,子任务会立即停止而无需调用stop函数。通过这种方式,我能进一步降低同时出现多个主节点的概率。

附言:真希望Go能像Erlang那样有监督器机制,这样我的代码会更简洁/更完善。

在分布式系统中,脑裂问题确实是一个关键挑战。以下是针对您代码的具体改进方案,通过增强租约管理和状态同步来减少脑裂风险:

type Election struct {
    Client   *EtcdClient
    TTL      int
    NodeID   string
    isLeader atomic.Bool
    session  *concurrency.Session
}

func (e *Election) Elect(ctx context.Context) (context.Context, error) {
    session, err := concurrency.NewSession(e.Client.base, concurrency.WithTTL(e.TTL))
    if err != nil {
        return nil, fmt.Errorf("failed to create session: %w", err)
    }
    e.session = session

    election := concurrency.NewElection(session, "/election")
    
    // 使用带超时的Campaign
    campaignCtx, cancel := context.WithTimeout(ctx, time.Second*5)
    defer cancel()
    
    if err := election.Campaign(campaignCtx, e.NodeID); err != nil {
        return nil, fmt.Errorf("campaign failed: %w", err)
    }

    e.isLeader.Store(true)
    
    leaderCtx, leaderCancel := context.WithCancel(ctx)
    
    // 监控租约状态和选举变化
    go e.monitorLeadership(leaderCtx, election, leaderCancel)
    
    return leaderCtx, nil
}

func (e *Election) monitorLeadership(ctx context.Context, election *concurrency.Election, cancel context.CancelFunc) {
    defer cancel()
    
    // 监控会话是否过期
    sessionDone := e.session.Done()
    
    // 监控选举键变化
    obsCtx, obsCancel := context.WithCancel(ctx)
    defer obsCancel()
    
    obsChan := election.Observe(obsCtx)
    
    for {
        select {
        case <-sessionDone:
            // 会话过期,失去领导权
            e.isLeader.Store(false)
            return
            
        case resp, ok := <-obsChan:
            if !ok {
                return
            }
            
            // 检查当前节点是否仍是leader
            if string(resp.Kvs[0].Value) != e.NodeID {
                e.isLeader.Store(false)
                return
            }
            
        case <-ctx.Done():
            return
        }
    }
}

// 在执行关键操作前检查领导状态
func (e *Election) IsLeader() bool {
    return e.isLeader.Load()
}

// 安全的主节点操作示例
func (e *Election) PerformMasterTask(taskData string) error {
    if !e.IsLeader() {
        return fmt.Errorf("node is not leader, cannot perform master task")
    }
    
    // 双重检查租约状态
    select {
    case <-e.session.Done():
        e.isLeader.Store(false)
        return fmt.Errorf("session expired during task execution")
    default:
    }
    
    // 执行主节点任务
    fmt.Printf("Executing master task: %s\n", taskData)
    return nil
}

// 优雅放弃领导权
func (e *Election) Resign() error {
    if e.session != nil {
        election := concurrency.NewElection(e.session, "/election")
        if err := election.Resign(context.Background()); err != nil {
            return fmt.Errorf("failed to resign: %w", err)
        }
    }
    e.isLeader.Store(false)
    return nil
}

使用示例:

func main() {
    election := &Election{
        Client: etcdClient,
        TTL:    10,
        NodeID: "node-1",
    }
    
    leaderCtx, err := election.Elect(context.Background())
    if err != nil {
        log.Fatal("Election failed:", err)
    }
    
    // 定期执行主节点任务
    go func() {
        ticker := time.NewTicker(time.Second * 2)
        defer ticker.Stop()
        
        for {
            select {
            case <-ticker.C:
                if err := election.PerformMasterTask("scheduled task"); err != nil {
                    log.Println("Task failed:", err)
                }
            case <-leaderCtx.Done():
                log.Println("No longer leader, stopping tasks")
                return
            }
        }
    }()
    
    <-leaderCtx.Done()
    log.Println("Leadership lost")
}

这个实现通过以下机制减少脑裂影响:

  1. 使用原子操作维护领导状态
  2. 双重检查会话状态和选举结果
  3. 在关键操作前验证领导权
  4. 及时检测租约过期并放弃领导权

这些改进能够显著减少旧主节点在失去领导权后继续执行关键操作的时间窗口。

回到顶部