分布式系统选主场景分析及实现

一:需要选主的场景
1:服务有多台机器,取其中一台去执行任务。多台机器同时执行会出问题,如将数据库中状态为失败的记录取出来重新执行,如果多台机器同时执行,会导致一个失败的任务被多台机器同时执行。
2:服务有多台机器,选其中一台作为主,主负责任务的分发,大家一起消费并处理任务。还是将数据库中状态为失败的记录取出来重新执行,由于一台机器可能处理不过来,需要多台机器协同处理。这个时候主机器负责将失败的记录从数据库中查出来,写入消息队列,其他机器一同消费队列中的任务,并处理失败的记录
 
二:进行选主
根据上面的选主场景,我们其实可以从多台机器中随机取一台,比raft这种选主算法简单得多。我们甚至可以在配置文件中指定一台机器,只有这台机器才执行相关功能,其他机器则不执行。如果是固定的几台机器,且一台机器也能完成我们的需求,这样搞其实也可以。如果机器不固定,而且单台处理不过来时,用配置文件的方式就不适合。
可采用竞争选主的方式,谁先抢到谁就是主。
 
1:方案一
采用redis方案实现。如果指定的key不存在就将机器信息写入这个key,成功写入的那台机器就是主,设置过期时间,防止机器异常挂掉的情况,所有的机器都需要定时去抢redis锁。SETNX这个命令就满足我们的需求,写redis成功的就是主,写失败的就是从。
优点:
  • 1:实现简单,比配置文件的方式好一点,支持机器动态
缺点:
  • 1:需要定时去抢锁
  • 2:主可能经常变化,而且要保证主在切换的过程中业务逻辑的正确性
  • 3:有些时间片可能没有主,就是主挂掉了,而其他机器还没到抢锁的时间,这个时间片就没有主
 
2:方案二
采用etcd方案实现。etcd支持事务能做到不存在就写入,达到redis SETNX一样的效果,而且通过etcd的租赁机制保证在主挂掉的情况下通知所有机器,这时大家自动开始新一轮的选主,还是那句话第一个抢到的就是主。
优点:
  • 满足我们的需求,没有设计上的缺陷
  • 只有主挂掉的情况,才会重新选主,不用担心主在切换的过程中对业务逻辑的影响
缺点:
  • 实现起来相对复杂,那我就来试试吧 

golang源码实现如下:

  1 package etcdDemo
  2 
  3 import (
  4     "context"
  5     "fmt"
  6     "github.com/coreos/etcd/clientv3"
  7     "github.com/google/uuid"
  8     "time"
  9 )
 10 
 11 type Callback func(isMaster bool)
 12 
 13 type SelectMaster struct {
 14     endPoints []string
 15     key       string
 16     cli       *clientv3.Client
 17     lease     *clientv3.LeaseGrantResponse
 18     chClose   chan int
 19     callback  Callback
 20     token     string
 21     isMaster  bool
 22 }
 23 
 24 func NewSelectMaster(endPoints []string, key string) (*SelectMaster, error) {
 25     sm := &SelectMaster{
 26         endPoints: endPoints,
 27         key:       key,
 28         chClose:   make(chan int, 0),
 29         token:     uuid.New().String(),
 30     }
 31 
 32     cli, err := clientv3.New(clientv3.Config{
 33         Endpoints:   endPoints,
 34         DialTimeout: 3 * time.Second,
 35     })
 36     if err != nil {
 37         return sm, err
 38     }
 39     sm.cli = cli
 40     go sm.ioLoop()
 41     return sm, nil
 42 }
 43 
 44 func (sm *SelectMaster) ioLoop() {
 45     fmt.Println("SelectMaster.ioLoop start")
 46     ticker := time.NewTicker(time.Second * 3)
 47     defer ticker.Stop()
 48     chWatch := sm.cli.Watch(context.TODO(), sm.key)
 49     for {
 50         select {
 51         case <-ticker.C:
 52             if sm.lease == nil {
 53                 leaseResp, err := sm.cli.Grant(context.Background(), 4)
 54                 if err != nil {
 55                     fmt.Println("cli.Grant error=", err.Error())
 56                 } else {
 57                     sm.lease = leaseResp
 58                 }
 59             }
 60             if sm.lease != nil {
 61                 _, err := sm.cli.KeepAliveOnce(context.Background(), sm.lease.ID)
 62                 if err != nil {
 63                     fmt.Println("cli.KeepAliveOnce error=", err.Error())
 64                     break
 65                 }
 66             }
 67         case c := <-chWatch:
 68             for _, e := range c.Events {
 69                 if e == nil || e.Kv == nil {
 70                     continue
 71                 }
 72                 token := string(e.Kv.Value)
 73                 sm.isMaster = sm.token == token
 74                 if sm.callback == nil {
 75                     fmt.Println("SelectMaster.callback is nil")
 76                 } else {
 77                     sm.callback(sm.isMaster)
 78                     fmt.Println("SelectMaster.isLoop token=", token)
 79                     if token == "" { //主挂了,开始竞选
 80                         sm.election()
 81                     }
 82                 }
 83             }
 84         case <-sm.chClose:
 85             goto stop
 86         }
 87     }
 88 stop:
 89     fmt.Println("SelectMaster.ioLoop end")
 90 }
 91 
 92 func (sm *SelectMaster) IsMaster() bool {
 93     return sm.isMaster
 94 }
 95 
 96 func (sm *SelectMaster) Close() {
 97     sm.chClose <- 1
 98 }
 99 
100 func (sm *SelectMaster) Election(callback Callback) (bool, error) {
101     sm.callback = callback
102     return sm.election()
103 }
104 
105 func (sm *SelectMaster) election() (bool, error) {
106     ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
107     defer cancel()
108     leaseResp, err := sm.cli.Grant(ctx, 10)
109     if err != nil {
110         return false, err
111     }
112     sm.lease = leaseResp
113     txn := clientv3.NewKV(sm.cli).Txn(context.TODO())
114     txn.If(clientv3.Compare(clientv3.CreateRevision(sm.key), "=", 0)).
115         Then(clientv3.OpPut(sm.key, sm.token, clientv3.WithLease(leaseResp.ID))).Else()
116     txnResp, err := txn.Commit()
117     if err != nil {
118         return false, err
119     }
120     return txnResp.Succeeded, nil
121 }
122 
123 func testSelectMaster() *SelectMaster {
124     endPoints := []string{"172.25.20.248:2379"}
125     sm, err := NewSelectMaster(endPoints, "/test/lock")
126     if err != nil {
127         fmt.Println(err.Error())
128         return nil
129     }
130     callback := func(isMaster bool) {
131         fmt.Println(sm.token, "callback=", isMaster)
132     }
133     isSuccess, err := sm.Election(callback)
134     if err != nil {
135         fmt.Println(sm.token, "Election=", err.Error())
136     } else {
137         fmt.Println(sm.token, "Election=", isSuccess)
138     }
139     return sm
140 }
141 
142 func TestSelectMaster() {
143     var master *SelectMaster
144     for i := 0; i < 3; i++ {
145         sm := testSelectMaster()
146         if sm.IsMaster() {
147             master = sm
148         }
149     }
150     if master != nil {
151         master.Close()
152     }
153     time.Sleep(time.Second*10)
154 }

 

posted @ 2020-09-09 10:31  啊汉  阅读(1258)  评论(0编辑  收藏  举报