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