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

dashboard是codis的集羣管理工具,支持proxy和server的添加、刪除、數據遷移,所有對集羣的操作必須通過dashboard。dashboard的啓動過程和proxy類似。dashboard的啓動只是初始化一些必要的數據結構,複雜的在於對集羣的操作,這個日後的文章會有詳細的描述,本文先不管這些。

啓動的時候,首先讀取配置文件,填充config信息。根據coordinator的信息,如果是zookeeper而不是etcd的話,就創建一個zk客戶端。然後根據client和config創建一個topom。首先來看看topom中有哪些信息。這個類在/pkg/topom/topom.go中。Topom非常重要,這個結構裏面存儲了集羣中某一時刻所有的節點信息,在深入codis的過程中我們會逐步看到

type Topom struct {
    mu sync.Mutex

    //初始化之後,這個屬性中的信息可以在zk中看到,就像models.Proxy一樣
    //路徑是/codis3/codis-wujiang/topom
    model *models.Topom

    //存儲着zkClient以及product-name,Topom與zk交互都是通過這個store
    store *models.Store

    //緩存結構,如果緩存爲空就通過store從zk中取出slot的信息並填充cache
    //不是隻有第一次啓動的時候cache會爲空,如果集羣中的元素(server,slot等等)發生變化,都會調用dirtyCache,將cache中的信息置爲nil
    //這樣下一次調用s.newContext()獲取上下文信息獲取上下文信息的時候,就會通過Topom.store從zk中重新拉取
    cache struct {
        hooks list.List
        slots []*models.SlotMapping
        group map[int]*models.Group
        proxy map[string]*models.Proxy

        sentinel *models.Sentinel
    }

    exit struct {
        C chan struct{}
    }
    //與dashboard相關的所有配置信息
    config *Config
    online bool
    closed bool

    ladmin net.Listener

    //槽進行遷移的時候使用
    action struct {
        //這個pool,其實就是map[string]*list.List,用於保存redis的結構,裏面有addr,auth和Timeout。相當於緩存,需要的時候從這裏取,否則就新建然後put進來
        //鍵爲redis服務器的地址,值爲與這臺服務器建立的連接,過期的連接會被刪除
        //timeout爲配置文件dashboard.toml中的migration_timeout選項所配
        redisp *redis.Pool

        interval atomic2.Int64
        disabled atomic2.Bool

        progress struct {
            status atomic.Value
        }
        //一個計數器,有一個slot等待遷移,就加一;執行一個slot的遷移,就減一
        executor atomic2.Int64
    }

    //存儲集羣中redis和proxy詳細信息,goroutine每次刷新redis和proxy之後,都會將結果存在這裏
    stats struct {
        //timeout爲5秒
        redisp *redis.Pool

        servers map[string]*RedisStats
        proxies map[string]*ProxyStats
    }

    //這個在使用哨兵的時候會用到,存儲在fe中配置的哨兵以及哨兵所監控的redis主服務器
    ha struct {
        //timeout爲5秒
        redisp *redis.Pool

        monitor *redis.Sentinel
        masters map[int]string
    }
}

創建topom的方法如下所示,這裏傳入的client,是根據coordinator創建的zkClient

func New(client models.Client, config *Config) (*Topom, error) {
    //配置文件校驗
    if err := config.Validate(); err != nil {
        return nil, errors.Trace(err)
    }
    if err := models.ValidateProduct(config.ProductName); err != nil {
        return nil, errors.Trace(err)
    }


    s := &Topom{}
    s.config = config
    s.exit.C = make(chan struct{})
    //新建redis pool
    s.action.redisp = redis.NewPool(config.ProductAuth, config.MigrationTimeout.Duration())
    s.action.progress.status.Store("")

    s.ha.redisp = redis.NewPool("", time.Second*5)

    s.model = &models.Topom{
        StartTime: time.Now().String(),
    }
    s.model.ProductName = config.ProductName
    s.model.Pid = os.Getpid()
    s.model.Pwd, _ = os.Getwd()
    if b, err := exec.Command("uname", "-a").Output(); err != nil {
        log.WarnErrorf(err, "run command uname failed")
    } else {
        s.model.Sys = strings.TrimSpace(string(b))
    }
    s.store = models.NewStore(client, config.ProductName)

    s.stats.redisp = redis.NewPool(config.ProductAuth, time.Second*5)
    s.stats.servers = make(map[string]*RedisStats)
    s.stats.proxies = make(map[string]*ProxyStats)

    if err := s.setup(config); err != nil {
        s.Close()
        return nil, err
    }

    log.Warnf("create new topom:\n%s", s.model.Encode())

    go s.serveAdmin()

    return s, nil
}

新建redis pool的方法在/pkg/utils/redis/client.go中。auth如果在配置文件中沒有設置,就是一個空字符串。timeout時間的單位是納秒,配置文件中默認是30s,也就是3乘以10的10次方納秒。

如果連接池收到退出的消息,就直接return,並且每隔一分鐘清理連接池中的數據。清理規則是,從當前Pool的pool中,遍歷取出每個pool屬性。前面已經說過,這個pool屬性其實就是map[string]*list.List,從每個list中取出頭一個元素,轉爲Client類型,判斷是否還是可再利用的,如果是可再利用的,就重新將該Client放回到隊列的尾部。可再利用的規則是,如果Pool的timeout爲0,或者該Client上次距離最近一次被引用到現在的時間小於Pool的timeout,就是可再利用的。

func NewPool(auth string, timeout time.Duration) *Pool {
    p := &Pool{
        auth: auth, timeout: timeout,
        pool: make(map[string]*list.List),
    }
    p.exit.C = make(chan struct{})

    if timeout != 0 {
        go func() {
            var ticker = time.NewTicker(time.Minute)
            defer ticker.Stop()
            for {
                select {
                case <-p.exit.C:
                    return
                case <-ticker.C:
                    //每隔一分鐘清理Pool中無效的Client
                    p.Cleanup()
                }
            }
        }()
    }

    return p
}
//RedisClient結構,對於每臺redis服務器,都會有多個連接,過期的連接將會被清除
type Client struct {
    conn redigo.Conn
    Addr string
    Auth string

    Database int

    //上次使用時間,用於看某個client是否應該被回收
    LastUse time.Time
    Timeout time.Duration
}

細心的讀者可能已經發現,上一步初始化的redis pool是Topom.action.Pool,在Topom中實際上還有另外兩個池,分別在stats和ha中。可以看一下redis pool此時的結構,在dashboard的初始化過程中,這三個池都只是把基礎的數據結構建好。

這裏寫圖片描述

與之前初始化Proxy的方式類似,現在我們正在初始化的結構是/pkg/topom/topom.go中的結構,而/pkg/models/topom.go中存儲了系統的相關信息。接下來幾步,在/pkg/models/topom.go中填充相關信息,初始化完成之後可以在zk中看到。

創建Topom的最後兩步,監聽、並得到路由handler

//Topom的ladmin監聽配置文件中的admin_addr,生成Token和Xauth
if err := s.setup(config); err != nil {
    s.Close()
    return nil, err
}

log.Warnf("create new topom:\n%s", s.model.Encode())
//採用martini框架,得到路由,並從路由得到handler。這一步的原理和proxy的類似,就不再贅述
go s.serveAdmin()

以上兩步的處理方式和proxy的啓動中類似,監聽18080端口(dashboard與codis集羣交互的默認接口),並採用martini框架對發送過來的請求進行轉發


func (s *Topom) setup(config *Config) error {
    if l, err := net.Listen("tcp", config.AdminAddr); err != nil {
        return errors.Trace(err)
    } else {
        s.ladmin = l

        x, err := utils.ReplaceUnspecifiedIP("tcp", l.Addr().String(), s.config.HostAdmin)
        if err != nil {
            return err
        }
        s.model.AdminAddr = x
    }

    s.model.Token = rpc.NewToken(
        config.ProductName,
        s.ladmin.Addr().String(),
    )
    s.xauth = rpc.NewXAuth(config.ProductName)

    return nil
}


func (s *Topom) serveAdmin() {
    if s.IsClosed() {
        return
    }
    defer s.Close()

    log.Warnf("admin start service on %s", s.ladmin.Addr())

    eh := make(chan error, 1)
    go func(l net.Listener) {
        h := http.NewServeMux()
        h.Handle("/", newApiServer(s))
        hs := &http.Server{Handler: h}
        eh <- hs.Serve(l)
    }(s.ladmin)

    select {
    case <-s.exit.C:
        log.Warnf("admin shutdown")
    case err := <-eh:
        log.ErrorErrorf(err, "admin exit on error")
    }
}
func newApiServer(t *Topom) http.Handler {
    m := martini.New()
    m.Use(martini.Recovery())
    m.Use(render.Renderer())
    m.Use(func(w http.ResponseWriter, req *http.Request, c martini.Context) {
        path := req.URL.Path
        if req.Method != "GET" && strings.HasPrefix(path, "/api/") {
            var remoteAddr = req.RemoteAddr
            var headerAddr string
            for _, key := range []string{"X-Real-IP", "X-Forwarded-For"} {
                if val := req.Header.Get(key); val != "" {
                    headerAddr = val
                    break
                }
            }
            log.Warnf("[%p] API call %s from %s [%s]", t, path, remoteAddr, headerAddr)
        }
        c.Next()
    })
    m.Use(gzip.All())
    m.Use(func(c martini.Context, w http.ResponseWriter) {
        w.Header().Set("Content-Type", "application/json; charset=utf-8")
    })

    api := &apiServer{topom: t}

    r := martini.NewRouter()

    r.Get("/", func(r render.Render) {
        r.Redirect("/topom")
    })
    r.Any("/debug/**", func(w http.ResponseWriter, req *http.Request) {
        http.DefaultServeMux.ServeHTTP(w, req)
    })

    r.Group("/topom", func(r martini.Router) {
        r.Get("", api.Overview)
        r.Get("/model", api.Model)
        r.Get("/stats", api.StatsNoXAuth)
        r.Get("/slots", api.SlotsNoXAuth)
    })
    r.Group("/api/topom", func(r martini.Router) {
        r.Get("/model", api.Model)
        r.Get("/xping/:xauth", api.XPing)
        r.Get("/stats/:xauth", api.Stats)
        r.Get("/slots/:xauth", api.Slots)
        r.Put("/reload/:xauth", api.Reload)
        r.Put("/shutdown/:xauth", api.Shutdown)
        r.Put("/loglevel/:xauth/:value", api.LogLevel)
        r.Group("/proxy", func(r martini.Router) {
            r.Put("/create/:xauth/:addr", api.CreateProxy)
            r.Put("/online/:xauth/:addr", api.OnlineProxy)
            r.Put("/reinit/:xauth/:token", api.ReinitProxy)
            r.Put("/remove/:xauth/:token/:force", api.RemoveProxy)
        })
        r.Group("/group", func(r martini.Router) {
            r.Put("/create/:xauth/:gid", api.CreateGroup)
            r.Put("/remove/:xauth/:gid", api.RemoveGroup)
            r.Put("/resync/:xauth/:gid", api.ResyncGroup)
            r.Put("/resync-all/:xauth", api.ResyncGroupAll)
            r.Put("/add/:xauth/:gid/:addr", api.GroupAddServer)
            r.Put("/add/:xauth/:gid/:addr/:datacenter", api.GroupAddServer)
            r.Put("/del/:xauth/:gid/:addr", api.GroupDelServer)
            r.Put("/promote/:xauth/:gid/:addr", api.GroupPromoteServer)
            r.Put("/replica-groups/:xauth/:gid/:addr/:value", api.EnableReplicaGroups)
            r.Put("/replica-groups-all/:xauth/:value", api.EnableReplicaGroupsAll)
            r.Group("/action", func(r martini.Router) {
                r.Put("/create/:xauth/:addr", api.SyncCreateAction)
                r.Put("/remove/:xauth/:addr", api.SyncRemoveAction)
            })
            r.Get("/info/:addr", api.InfoServer)
        })
        r.Group("/slots", func(r martini.Router) {
            r.Group("/action", func(r martini.Router) {
                r.Put("/create/:xauth/:sid/:gid", api.SlotCreateAction)
                r.Put("/create-some/:xauth/:src/:dst/:num", api.SlotCreateActionSome)
                r.Put("/create-range/:xauth/:beg/:end/:gid", api.SlotCreateActionRange)
                r.Put("/remove/:xauth/:sid", api.SlotRemoveAction)
                r.Put("/interval/:xauth/:value", api.SetSlotActionInterval)
                r.Put("/disabled/:xauth/:value", api.SetSlotActionDisabled)
            })
            r.Put("/assign/:xauth", binding.Json([]*models.SlotMapping{}), api.SlotsAssignGroup)
            r.Put("/assign/:xauth/offline", binding.Json([]*models.SlotMapping{}), api.SlotsAssignOffline)
            r.Put("/rebalance/:xauth/:confirm", api.SlotsRebalance)
        })
        r.Group("/sentinels", func(r martini.Router) {
            r.Put("/add/:xauth/:addr", api.AddSentinel)
            r.Put("/del/:xauth/:addr/:force", api.DelSentinel)
            r.Put("/resync-all/:xauth", api.ResyncSentinels)
            r.Get("/info/:addr", api.InfoSentinel)
            r.Get("/info/:addr/monitored", api.InfoSentinelMonitored)
        })
    })

    m.MapTo(r, (*martini.Routes)(nil))
    m.Action(r.Handle)
    return m
}

topom初始化成功之後,看一下控制檯上打印的日誌。

這裏寫圖片描述

創建Topom之後,下一步就是創建一個channel,專門用來接收系統signal。這個signal在隨不同的系統而變化。着重說一下signal.Notify方法。第一個參數是channel,後面的可變參數是往channel中寫入的信號。如果沒有制定任何信號參數,就默認所有收到的信號都會寫入channel。

go func() {
    defer s.Close()
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM)

    //將收到的系統信號讀取並打印在日誌
    sig := <-c
    log.Warnf("[%p] dashboard receive signal = '%v'", s, sig)
}()

這樣做的目的是什麼呢?
比方說,當你強行停止掉dashboard進程,console上就會出現日誌

Process finished with exit code 137 (interrupted by signal 9: SIGKILL)

然後調用defer s.Close()來刪除dashboard在zk的註冊路徑,下次啓動dashboard就不會報acquire lock of codis-demo failed的錯。

到這裏,Topom創建結束,下一步就是傳入固定參數true,調用Topom的啓動方法。

很多人啓動dashboard的時候會報錯acquire lock of codis-demo failed,就是這裏報的錯。意思是說,創建路徑filepath.Join(CodisDir, product, “topom”)錯誤,報了node already exits。通常解決這個問題的方式就是遞歸刪除codis3文件夾的內容,然後重新創建

cd /app/zookeeper-3.4.6/bin/
./zkCli.sh
rmr /codis3
func (s *Topom) Start(routines bool) error {
 s.mu.Lock()
 defer s.mu.Unlock()
 if s.closed {
  return ErrClosedTopom
 }
 if s.online {
  return nil
 } else {
  //創建zk路徑
  if err := s.store.Acquire(s.model); err != nil {
   log.ErrorErrorf(err, "store: acquire lock of %s failed", s.config.ProductName)
   return errors.Errorf("store: acquire lock of %s failed", s.config.ProductName)
  }
  s.online = true
 }

 if !routines {
  return nil
 }

 go func() {
  for !s.IsClosed() {
   if s.IsOnline() {
    //刷新redis狀態
    w, _ := s.RefreshRedisStats(time.Second)
    if w != nil {
     w.Wait()
    }
   }
   time.Sleep(time.Second)
  }
 }()

 go func() {
  for !s.IsClosed() {
   if s.IsOnline() {
    //刷新proxy狀態
    w, _ := s.RefreshProxyStats(time.Second)
    if w != nil {
     w.Wait()
    }
   }
   time.Sleep(time.Second)
  }
 }()

 go func() {
  for !s.IsClosed() {
   if s.IsOnline() {
    //處理slot操作
    if err := s.ProcessSlotAction(); err != nil {
     log.WarnErrorf(err, "process slot action failed")
     time.Sleep(time.Second * 5)
    }
   }
   time.Sleep(time.Second)
  }
 }()

 go func() {
  for !s.IsClosed() {
   if s.IsOnline() {
    //處理同步操作
    if err := s.ProcessSyncAction(); err != nil {
     log.WarnErrorf(err, "process sync action failed")
     time.Sleep(time.Second * 5)
    }
   }
   time.Sleep(time.Second)
  }
 }()

 return nil
}

之前我們已經說過,整個集羣的操作和管理都要經過dashboard,因此dashboard中必須存有集羣狀態。codis如何處理狀態緩存的有效性和過期問題呢?沒錯,就是上面代碼中看起來很像的四個Goroutine。前兩個方法的具體實現在/pkg/topom/topom_stats.go中,後兩個方法的具體實現則是在/pkg/topom/topom_action.go。在下一節 Codis源碼解析——dashboard的啓動(2)我們具體講這四個方法的實現

總結一下,啓動dashboard的過程中,需要連接zk,創建Topom這個struct,通過18080這個端口與集羣進項交互,並將該端口收到的信息進行轉發。最重要的是啓動了四個goroutine,刷新集羣中的redis和proxy的狀態,以及處理slot和同步操作。

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

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