Codis源碼解析——dashboard的啓動(2)

1 刷新redis狀態

首先認識兩個重要的struct

type Future struct {
    sync.Mutex
    wait sync.WaitGroup
    vmap map[string]interface{}
}
type RedisStats struct {
    //儲存了集羣中Redis服務器的各種信息和統計數值,詳見redis的info命令
    Stats map[string]string `json:"stats,omitempty"`
    Error *rpc.RemoteError  `json:"error,omitempty"`

    Sentinel map[string]*redis.SentinelGroup `json:"sentinel,omitempty"`

    UnixTime int64 `json:"unixtime"`
    Timeout  bool  `json:"timeout,omitempty"`
}

接下來看看dashboard如何刷新redis狀態

func (s *Topom) RefreshRedisStats(timeout time.Duration) (*sync2.Future, error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    //從緩存中讀出slots,group,proxy,sentinel等信息封裝在context struct中
    ctx, err := s.newContext()
    if err != nil {
        return nil, err
    }
    var fut sync2.Future
    goStats := func(addr string, do func(addr string) (*RedisStats, error)) {
        fut.Add()
        go func() {
            stats := s.newRedisStats(addr, timeout, do)
            stats.UnixTime = time.Now().Unix()
            //vmap中添加鍵爲addr,值爲RedisStats的map
            fut.Done(addr, stats)
        }()
    }

    //遍歷ctx中的group,再遍歷每個group中的Server。如果對group和Server結構不清楚的,可以看看/pkg/models/group.go文件
    //每個Group除了id,還有一個屬性就是GroupServer。每個GroupServer有自己的地址、數據中心、action等等
    for _, g := range ctx.group {
        for _, x := range g.Servers {
            goStats(x.Addr, func(addr string) (*RedisStats, error) {
                //前面我們已經說過,Topom中有三個redis pool,分別是action,stats,ha。pool本質上就是map[String]*list.List。
                //這個是從stats的pool中根據Server的地址從pool中取redis client,如果沒有client,就創建
                //然後加入到pool裏面,並通過Info命令獲取詳細信息。整個流程和下面的sentinel類似,這裏就不放具體的方法實現了
                m, err := s.stats.redisp.InfoFull(addr)
                if err != nil {
                    return nil, err
                }
                return &RedisStats{Stats: m}, nil
            })
        }
    }

    //通過sentinel維護codis集羣中每一組的主備關係
    for _, server := range ctx.sentinel.Servers {
        goStats(server, func(addr string) (*RedisStats, error) {
            c, err := s.ha.redisp.GetClient(addr)
            if err != nil {
                return nil, err
            }
            //實際上就是將client加入到Pool的pool屬性裏面去,pool本質上就是map[String]*list.List
            //鍵是client的addr,值是client本身
            //如果client不存在,就新建一個空的list
            defer s.ha.redisp.PutClient(c)
            m, err := c.Info()
            if err != nil {
                return nil, err
            }
            sentinel := redis.NewSentinel(s.config.ProductName, s.config.ProductAuth)
            //獲得map[string]*SentinelGroup,鍵是每一組的master的名字,SentinelGroup則是主從對
            p, err := sentinel.MastersAndSlavesClient(c)
            if err != nil {
                return nil, err
            }
            return &RedisStats{Stats: m, Sentinel: p}, nil
        })
    }
    //前面的所有gostats執行完之後,遍歷Future的vmap,將值賦給Topom.stats.servers
    go func() {
        stats := make(map[string]*RedisStats)
        for k, v := range fut.Wait() {
            stats[k] = v.(*RedisStats)
        }
        s.mu.Lock()
        defer s.mu.Unlock()
        s.stats.servers = stats
    }()
    return &fut, nil
}
func (p *Pool) GetClient(addr string) (*Client, error) {
    c, err := p.getClientFromCache(addr)
    if err != nil || c != nil {
        return c, err
    }
    return NewClient(addr, p.auth, p.timeout)
}
func (p *Pool) getClientFromCache(addr string) (*Client, error) {
    p.mu.Lock()
    defer p.mu.Unlock()
    if p.closed {
        return nil, ErrClosedPool
    }
    if list := p.pool[addr]; list != nil {
        for i := list.Len(); i != 0; i-- {
            c := list.Remove(list.Front()).(*Client)
            //一個client可被回收的條件是,Pool的timeout爲0,或者這個client上一次使用距離現在小於Pool.timeout
            //ha和stats裏面的Pool的timeout爲5秒,action的則根據配置文件dashboard.toml中的migration_timeout一項來決定
            if p.isRecyclable(c) {
                return c, nil
            } else {
                c.Close()
            }
        }
    }
    return nil, nil
}
type Client struct {
    conn redigo.Conn
    Addr string
    Auth string

    Database int

    LastUse time.Time
    Timeout time.Duration
}

RedisStats中的sentinel如下所示,有幾組主備,就有幾組SentinelGroup,鍵是product-name與group-id拼起來的

這裏寫圖片描述

newContext一步主要就是調用refillCache,重載了四個緩存,分別是refillCacheSlots,refillCacheGroup,refillCacheProxy和refillCacheSentinel。這四個方法基本一致,以refillCacheSlots爲例。方法傳入的是Topom.cache.slots

type context struct {
    slots []*models.SlotMapping
    group map[int]*models.Group
    proxy map[string]*models.Proxy

    sentinel *models.Sentinel

    hosts struct {
        sync.Mutex
        m map[string]net.IP
    }
    method int
}
//重新填充topom.cache中的數據,並賦給context結構
func (s *Topom) newContext() (*context, error) {
    if s.closed {
        return nil, ErrClosedTopom
    }
    if s.online {
        if err := s.refillCache(); err != nil {
            return nil, err
        } else {
            ctx := &context{}
            ctx.slots = s.cache.slots
            ctx.group = s.cache.group
            ctx.proxy = s.cache.proxy
            ctx.sentinel = s.cache.sentinel
            ctx.hosts.m = make(map[string]net.IP)
            ctx.method, _ = models.ParseForwardMethod(s.config.MigrationMethod)
            return ctx, nil
        }
    } else {
        return nil, ErrNotOnline
    }
}
func (s *Topom) refillCacheSlots(slots []*models.SlotMapping) ([]*models.SlotMapping, error) {
    //如果cache中的slots爲空,就直接返回store裏面的slots
    if slots == nil {
        return s.store.SlotMappings()
    }
    for i, _ := range slots {
        //如果cache中的slots[i]不爲空,直接進入下一個循環
        if slots[i] != nil {
            continue
        }
        //如果slots[i]爲空,就從store中取出對應的SlotMapping並賦值給cache中的這個slot
        m, err := s.store.LoadSlotMapping(i, false)
        if err != nil {
            return nil, err
        }
        if m != nil {
            slots[i] = m
        } else {
            //如果store中取出的對應的SlotMapping也爲空,就新建一個SlotMapping賦值給當前slot
            slots[i] = &models.SlotMapping{Id: i}
        }
    }
    return slots, nil
}
func (s *Store) LoadSlotMapping(sid int, must bool) (*SlotMapping, error) {
    //返回值b是zkClient根據路徑轉化成的byte數組
    b, err := s.client.Read(s.SlotPath(sid), must)
    if err != nil || b == nil {
        return nil, err
    }
    m := &SlotMapping{}
    //將byte數組封裝在實體類SlotMapping實體類中
    if err := jsonDecode(m, b); err != nil {
        return nil, err
    }
    return m, nil
}
func (s *Store) SlotPath(sid int) string {
    return SlotPath(s.product, sid)
}
//這裏的codisDir是/codis3
func SlotPath(product string, sid int) string {
    return filepath.Join(CodisDir, product, "slots", fmt.Sprintf("slot-%04d", sid))
}
type SlotMapping struct {
    Id      int `json:"id"`
    GroupId int `json:"group_id"`

    Action struct {
        Index    int    `json:"index,omitempty"`
        State    string `json:"state,omitempty"`
        TargetId int    `json:"target_id,omitempty"`
    } `json:"action"`
}

總結一下,刷新redis的過程中,首先創建上下文,從cache中讀取slots,group,proxy,sentinel等信息,如果讀不到就通過store從zk上獲取,如果zk中也爲空就創建。遍歷集羣中的redis服務器以及主從關係,創建RedisStats並與addr關聯形成map,存儲在future的vmap中。全部存儲完後,再把vmap寫入Topom.stats.servers

我們可以在控制檯上打印出Topom.stats.redisp的相關信息。因爲goroutine中設置了每個一秒休眠,所以集羣的redisp實際上是每秒刷新一次

stats redisp: &{{0 0}  map[*.*.*.*:6379:0xc4206933e0 *.*.*.*:6380:0xc420693890 127.0.0.1:6379:0xc4206be540] 5000000000 {0xc420320000} false}

2 刷新proxy狀態

刷新proxy狀態的代碼和刷新redis的類似,就不贅述了。可以參照Codis源碼解析——proxy添加到集羣
最後的步驟

3 處理同步操作

首先要明白,同步操作,指的就是一個group中的主從codis-server服務器之間進行數據的同步,GroupServer是Group的一個屬性,標明瞭當前group中的所有codis-server的地址和action等等信息

type Group struct {
    Id      int            `json:"id"`
    Servers []*GroupServer `json:"servers"`

    Promoting struct {
        Index int    `json:"index,omitempty"`
        State string `json:"state,omitempty"`
    } `json:"promoting"`

    OutOfSync bool `json:"out_of_sync"`
}

type GroupServer struct {
    Addr       string `json:"server"`
    DataCenter string `json:"datacenter"`

    Action struct {
        Index int    `json:"index,omitempty"`
        State string `json:"state,omitempty"`
    } `json:"action"`

    ReplicaGroup bool `json:"replica_group"`
}

我們直接看ProcessSyncAction,在/pkg/topom/topom_action.go文件中

func (s *Topom) ProcessSyncAction() error {
    //同步操作之前的準備工作
    addr, err := s.SyncActionPrepare()
    if err != nil || addr == "" {
        return err
    }
    log.Warnf("sync-[%s] process action", addr)

    //執行同步操作
    exec, err := s.newSyncActionExecutor(addr)
    if err != nil || exec == nil {
        return err
    }
    return s.SyncActionComplete(addr, exec() != nil)
}

同步操作之前的準備工作是,使用s.newContext()獲取上下文,從上下文中,遍歷每個group中的每個codis-server,從Action.State爲pending的codis-server中,選出Action.Index最小的那臺服務器,並獲取其所在的group,如果這個group的Promoting.State爲nothing,這臺服務器就可以從主服務器同步數據。將這個codis-server的Action.Index設爲0,Action.State設爲syncing,更新zk中存儲的信息,並將cache中關於這臺服務器的信息設爲nil,這樣下次就會從store中重新載入數據到cache。

下一步,檢查當前server在group中的index,如果index不爲0,就表示這臺server不是group中的主服務器(codis是將group中index爲0的那臺server作爲主的),下一步就是當前server從主服務同步數據,通過redigo發送同步命令

return func() error {
    c, err := redis.NewClient(addr, s.config.ProductAuth, time.Minute*30)
    if err != nil {
        log.WarnErrorf(err, "create redis client to %s failed", addr)
        return err
    }
    defer c.Close()
    if err := c.SetMaster(master); err != nil {
        log.WarnErrorf(err, "redis %s set master to %s failed", addr, master)
        return err
    }
    return nil
}, nil
func NewClient(addr string, auth string, timeout time.Duration) (*Client, error) {
    c, err := redigo.Dial("tcp", addr, []redigo.DialOption{
        redigo.DialConnectTimeout(math2.MinDuration(time.Second, timeout)),
        redigo.DialPassword(auth),
        redigo.DialReadTimeout(timeout), redigo.DialWriteTimeout(timeout),
    }...)
    if err != nil {
        return nil, errors.Trace(err)
    }
    return &Client{
        conn: c, Addr: addr, Auth: auth,
        LastUse: time.Now(), Timeout: timeout,
    }, nil
}
func (c *Client) SetMaster(master string) error {
    host, port, err := net.SplitHostPort(master)
    if err != nil {
        return errors.Trace(err)
    }
    c.conn.Send("MULTI")
    c.conn.Send("CONFIG", "SET", "masterauth", c.Auth)
    c.conn.Send("SLAVEOF", host, port)
    c.conn.Send("CONFIG", "REWRITE")
    c.conn.Send("CLIENT", "KILL", "TYPE", "normal")
    values, err := redigo.Values(c.Do("EXEC"))
    if err != nil {
        return errors.Trace(err)
    }
    for _, r := range values {
        if err, ok := r.(redigo.Error); ok {
            return errors.Trace(err)
        }
    }
    return nil
}

同步之後,會將這臺codis-server的Action.State設置爲”synced”或者”synced_failed”,並在zk中更新相關信息,抹除cache。

注意,儘管整個過程中,都用了鎖,每次還是會檢查group的Promoting.State是否nothing,codis-server的Action.Index是否爲0,Action.State是否爲syncing,只有全部符合才進行同步

4 處理slot操作

對槽的操作是很複雜的,因爲有五種狀態,掛起、準備中、準備完成、遷移中、遷移完成,這個詳見另外兩篇博客Codis源碼解析——處理slot操作(1) 以及 Codis源碼解析——處理slot操作(2)

到這裏,dashboard的啓動工作已經完成,可以看到,dashboard啓動過程中,實際上啓動了很多goroutine來對後續操作進行處理,這些我們都會在後面的文章的具體章節中做分析,這一節只需要關注到dashboard啓動過程中做了什麼即可。

說明
如有轉載,請註明出處
http://blog.csdn.net/antony9118/article/details/76037488

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章