Codis源码解析——proxy添加到集群

来源:互联网 发布:拇指特效软件 编辑:程序博客网 时间:2024/05/22 08:16

前面我们说过,proxy启动之后,会默认处于 waiting 状态,以一秒一次的频率刷新状态,监听proxy_addr 地址(默认配置文件中的19000端口),但是不会 accept 连接,通过fe或者命令行添加到集群并完成集群状态的同步,才能改变状态为online。那么,将proxy添加到集群的过程中发生了什么?这一篇我们就来看看。

通过界面添加比较简单,直接输入proxy的地址即可

这里写图片描述

主要调用的方法在/pkg/topom/topom_proxy.go中

//传入的addr就是proxy的地址func (s *Topom) CreateProxy(addr string) error {    s.mu.Lock()    defer s.mu.Unlock()    ctx, err := s.newContext()    if err != nil {        return err    }    //这里的p就是根据proxy地址取出models.proxy,也就是/codis3/codis-wujiang/proxy路径下面的那个proxy-token中的详细信息    p, err := proxy.NewApiClient(addr).Model()    if err != nil {        return errors.Errorf("proxy@%s fetch model failed, %s", addr, err)    }    //这个ApiClient中存储了proxy的地址,以及根据productName,productAuth(默认为空)以及token生成的auth    c := s.newProxyClient(p)    //之前我们说过,proxy启动的时候,在s.setup(config)这一步,会生成一个xauth,存储在Proxy的xauth属性中,这一步就是讲上面得到的xauth和启动proxy时的xauth作比较,来唯一确定需要的xauth    if err := c.XPing(); err != nil {        return errors.Errorf("proxy@%s check xauth failed, %s", addr, err)    }    //检查上下文中的proxy是否已经有token,如果有的话,说明这个proxy已经添加到集群了    if ctx.proxy[p.Token] != nil {        return errors.Errorf("proxy-[%s] already exists", p.Token)    } else {        //上下文中所有proxy的最大id+1,赋给当前的proxy作为其id        p.Id = ctx.maxProxyId() + 1    }    defer s.dirtyProxyCache(p.Token)    //到这一步,proxy已经添加成功,更新"/codis3/codis-wujiang/proxy/proxy-token"下面的proxy信息    if err := s.storeCreateProxy(p); err != nil {        return err    } else {        return s.reinitProxy(ctx, p, c)    }}

下面我们着重看一下reinitProxy方法,这个方法里面主要是包含三个模块,都是ApiClient的方法。前面我们已经说过,ApiClient中存储了proxy的地址和auth

func (s *Topom) reinitProxy(ctx *context, p *models.Proxy, c *proxy.ApiClient) error {    log.Warnf("proxy-[%s] reinit:\n%s", p.Token, p.Encode())    //初始化1024个槽    if err := c.FillSlots(ctx.toSlotSlice(ctx.slots, p)...); err != nil {        log.ErrorErrorf(err, "proxy-[%s] fillslots failed", p.Token)        return errors.Errorf("proxy-[%s] fillslots failed", p.Token)    }    if err := c.Start(); err != nil {        log.ErrorErrorf(err, "proxy-[%s] start failed", p.Token)        return errors.Errorf("proxy-[%s] start failed", p.Token)    }    //由于此时sentinels还没有,传入的server队列为空,所以这个方法我们暂时可以不管    if err := c.SetSentinels(ctx.sentinel); err != nil {        log.ErrorErrorf(err, "proxy-[%s] set sentinels failed", p.Token)        return errors.Errorf("proxy-[%s] set sentinels failed", p.Token)    }    return nil}

ctx.toSlotSlice主要就是根据models.SlotMapping来创建1024个models.Slot

func (ctx *context) toSlot(m *models.SlotMapping, p *models.Proxy) *models.Slot {    slot := &models.Slot{        Id:     m.Id,        Locked: ctx.isSlotLocked(m),        ForwardMethod: ctx.method,    }    switch m.Action.State {    case models.ActionNothing, models.ActionPending:        slot.BackendAddr = ctx.getGroupMaster(m.GroupId)        slot.BackendAddrGroupId = m.GroupId        slot.ReplicaGroups = ctx.toReplicaGroups(m.GroupId, p)    case models.ActionPreparing:        slot.BackendAddr = ctx.getGroupMaster(m.GroupId)        slot.BackendAddrGroupId = m.GroupId    case models.ActionPrepared:        fallthrough    case models.ActionMigrating:        slot.BackendAddr = ctx.getGroupMaster(m.Action.TargetId)        slot.BackendAddrGroupId = m.Action.TargetId        slot.MigrateFrom = ctx.getGroupMaster(m.GroupId)        slot.MigrateFromGroupId = m.GroupId    case models.ActionFinished:        slot.BackendAddr = ctx.getGroupMaster(m.Action.TargetId)        slot.BackendAddrGroupId = m.Action.TargetId    default:        log.Panicf("slot-[%d] action state is invalid:\n%s", m.Id, m.Encode())    }    return slot}

Topom.FillSlots阶段,根据之前的1024个models.Slot,创建1024个/pkg/proxy/slots.go中的Slot,并且创建了SharedBackendConn。之前在Codis源码解析——proxy监听redis请求中我们提到过,redis请求是由sharedBackendConn中取出的一个BackendConn进行处理的。而sharedBackendConn就是在填充槽的过程中创建的,如下列代码所示。这个方法由Proxy.Router调用,第一个参数是1024个models.slot中的每一个,第二个参数是写死的false,第三个是FowardSemiAsync(这个是由配置文件dashboard.toml中的migration_method指定的)。

之前我们说过,Proxy.Router中存储了集群中所有sharedBackendConnPool和slot,用于将redis请求转发给相应的slot进行处理,而Router里面的sharedBackendConnPool和slot就是在这里进行填充的

func (s *Router) fillSlot(m *models.Slot, switched bool, method forwardMethod) {    slot := &s.slots[m.Id]    slot.blockAndWait()    //清空models.Slot里面的backendConn    slot.backend.bc.Release()    slot.backend.bc = nil    slot.backend.id = 0    slot.migrate.bc.Release()    slot.migrate.bc = nil    slot.migrate.id = 0    for i := range slot.replicaGroups {        for _, bc := range slot.replicaGroups[i] {            bc.Release()        }    }    slot.replicaGroups = nil    //false    slot.switched = switched    //初始阶段addr和from都是空字符串    if addr := m.BackendAddr; len(addr) != 0 {        //从Router的primary sharedBackendConnPool中取出addr对应的sharedBackendConn,如果没有就新建并放入,也相当于初始化了        slot.backend.bc = s.pool.primary.Retain(addr)        slot.backend.id = m.BackendAddrGroupId    }    if from := m.MigrateFrom; len(from) != 0 {        slot.migrate.bc = s.pool.primary.Retain(from)        slot.migrate.id = m.MigrateFromGroupId    }    if !s.config.BackendPrimaryOnly {        for i := range m.ReplicaGroups {            var group []*sharedBackendConn            for _, addr := range m.ReplicaGroups[i] {                group = append(group, s.pool.replica.Retain(addr))            }            if len(group) == 0 {                continue            }            slot.replicaGroups = append(slot.replicaGroups, group)        }    }    if method != nil {        slot.method = method    }    if !m.Locked {        slot.unblock()    }    if !s.closed {        if slot.migrate.bc != nil {            if switched {                log.Warnf("fill slot %04d, backend.addr = %s, migrate.from = %s, locked = %t, +switched",                    slot.id, slot.backend.bc.Addr(), slot.migrate.bc.Addr(), slot.lock.hold)            } else {                log.Warnf("fill slot %04d, backend.addr = %s, migrate.from = %s, locked = %t",                    slot.id, slot.backend.bc.Addr(), slot.migrate.bc.Addr(), slot.lock.hold)            }        } else {            if switched {                log.Warnf("fill slot %04d, backend.addr = %s, locked = %t, +switched",                    slot.id, slot.backend.bc.Addr(), slot.lock.hold)            } else {                log.Warnf("fill slot %04d, backend.addr = %s, locked = %t",                    slot.id, slot.backend.bc.Addr(), slot.lock.hold)            }        }    }}

这个阶段主要是初始化槽。以id为0的槽为例,初始化之后,其结构如下图所示,每个slot都被分配了相应的backendConn,只不过此时每个backendConn都为空

这里写图片描述

这里写图片描述

接下来主要介绍上面的Topom.start方法,主要就是将Proxy自身,和它的router和jodis设为上线,在zk中创建临时节点/jodis/codis-productName/proxy-token,并监听该节点的变化

func (s *Proxy) Start() error {    s.mu.Lock()    defer s.mu.Unlock()    if s.closed {        return ErrClosedProxy    }    if s.online {        return nil    }    s.online = true    //router的online属性设为true    s.router.Start()    if s.jodis != nil {        s.jodis.Start()    }    return nil}func (j *Jodis) Start() {    j.mu.Lock()    defer j.mu.Unlock()    if j.online {        return    }    //也是这个套路,先把online属性设为true    j.online = true    go func() {        var delay = &DelayExp2{            Min: 1, Max: 30,            Unit: time.Second,        }        for !j.IsClosed() {            //这一步在zk中创建临时节点/jodis/codis-wujiang/proxy-token,并添加监听事件,监听该节点中内容的改变。最终返回的w是一个chan struct{}。具体实现方法在下面的watch中            w, err := j.Rewatch()            if err != nil {                log.WarnErrorf(err, "jodis watch node %s failed", j.path)                delay.SleepWithCancel(j.IsClosed)            } else {                //从w中读出zk下的变化                <-w                delay.Reset()            }        }    }()}func (c *Client) watch(conn *zk.Conn, path string) (<-chan struct{}, error) {    //GetW的核心方法就是下面的addWatcher,w就是addWatcher返回的ch    _, _, w, err := conn.GetW(path)    if err != nil {        return nil, errors.Trace(err)    }    signal := make(chan struct{})    go func() {        defer close(signal)        <-w        log.Debugf("zkclient watch node %s update", path)    }()    return signal, nil}//这个类在/github.com/samuel/go-zookeeper/zk/conn.go中,返回一个channel。当关注的路径下发生变更时,这个channel中就会读出值func (c *Conn) addWatcher(path string, watchType watchType) <-chan Event {    c.watchersLock.Lock()    defer c.watchersLock.Unlock()    ch := make(chan Event, 1)    wpt := watchPathType{path, watchType}    c.watchers[wpt] = append(c.watchers[wpt], ch)    return ch}

上面的Proxy中的jodis如下图所示

这里写图片描述

到这里,proxy已经成功添加到集群中。可以从三个方面看出来:
1 界面中显示成功

这里写图片描述

这里多说一句,界面上显示的total session,是历史记录总数,alive session才有意义。每次新建一个session,这两个数据都会加一,但是如果alive session加一之后超过了proxy.toml中设置的proxy_max_clients(默认为1000),就不会建立连接,并会把alive session减一。alive session减一的另外两种情况是,成功返回请求结果后,以及session过期后(默认为75秒)

2 zk中增加了/jodis/codis-wujiang/proxy-token路径

这里写图片描述

3 proxy的日志从waiting变为working,dashboard的日志打印创建了proxy-token

这里写图片描述

这里写图片描述

别忘了,在dashboard启动的时候,启动了goroutine来刷新proxy的状态,下面我们就来看看,当集群中新增了proxy之后,是如何刷新的

//上下文中存储了当前集群的slots、group、proxy、sentinels等信息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}func (s *Topom) RefreshProxyStats(timeout time.Duration) (*sync2.Future, error) {    s.mu.Lock()    defer s.mu.Unlock()    //在这个构造函数中将判断cache中的数据是否为空,为空的话就通过store从zk中取出填进cache    ctx, err := s.newContext()     if err != nil {        return nil, err    }    var fut sync2.Future    //由于我们刚才添加了proxy,这里ctx.proxy已经不为空了    for _, p := range ctx.proxy {        //fut中的waitsGroup加1        fut.Add()        go func(p *models.Proxy) {            stats := s.newProxyStats(p, timeout)            stats.UnixTime = time.Now().Unix()            //在fut的vmap属性中,添加以proxy.Token为键,ProxyStats为值的map,并将waitsGroup减1            fut.Done(p.Token, stats)            switch x := stats.Stats; {            case x == nil:            case x.Closed || x.Online:            //如果一个proxy因为某种情况出现error,被运维重启之后,处于waiting状态,会调用OnlineProxy方法将proxy重新添加到集群中            default:                if err := s.OnlineProxy(p.AdminAddr); err != nil {                    log.WarnErrorf(err, "auto online proxy-[%s] failed", p.Token)                }            }        }(p)    }    ////当所有proxy.Token和ProxyStats的关系map建立好之后,存到Topom.stats.proxies中    go func() {        stats := make(map[string]*ProxyStats)        for k, v := range fut.Wait() {            stats[k] = v.(*ProxyStats)        }        s.mu.Lock()        defer s.mu.Unlock()        //Topom的stats结构中的proxies属性,存储了完整的stats信息,回想我们之前介绍的,Topom存储着集群中的所有配置和节点信息        s.stats.proxies = stats    }()    return &fut, nil}

以上步骤执行完之后,我们来看看返回值fut

这里写图片描述

手动kill一个proxy的进程,发现集群上显示该proxy为红色error,但是其实这个proxy并没有被踢出集群ctx。可能有人会问,如果一个proxy挂了,codis是怎么知道不要把请求发到这个proxy的呢?其实,当下面几种情况,都会调用proxy.close方法,这个里面调用了Jodis的close方法,将zk上面的该proxy信息删除。jodis客户端是通过zk转发的,自然就不会把请求发到这个proxy上了

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] proxy receive signal = '%v'", s, sig)}()func (s *Proxy) serveAdmin() {    if s.IsClosed() {        return    }    defer s.Close()    log.Warnf("[%p] admin start service on %s", 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("[%p] admin shutdown", s)    case err := <-eh:        log.ErrorErrorf(err, "[%p] admin exit on error", s)    }}func (s *Proxy) serveProxy() {    if s.IsClosed() {        return    }    defer s.Close()    log.Warnf("[%p] proxy start service on %s", s, s.lproxy.Addr())    eh := make(chan error, 1)    go func(l net.Listener) (err error) {        defer func() {            eh <- err        }()        for {            c, err := s.acceptConn(l)            if err != nil {                return err            }            NewSession(c, s.config).Start(s.router)        }    }(s.lproxy)    if d := s.config.BackendPingPeriod.Duration(); d != 0 {        go s.keepAlive(d)    }    select {    case <-s.exit.C:        log.Warnf("[%p] proxy shutdown", s)    case err := <-eh:        log.ErrorErrorf(err, "[%p] proxy exit on error", s)    }}

总结一下,proxy启动之后,一直处于waiting的状态,直到将proxy添加到集群。首先对要添加的proxy做参数校验,根据proxy addr获取一个ApiClient,更新zk中有关于这个proxy的信息。接下来,从上下文中获取ctx.slots,也就是[]*models.SlotMapping,创建1024个models.Slot,再填充1024个pkg/proxy/slots.go中的Slot,此过程中Router为每个Slot都分配了对应的backendConn。

下一步,将Proxy自身,和它的router和jodis设为上线,在zk中创建临时节点/jodis/codis-productName/proxy-token,并监听该节点的变化,如果有变化从相应的channel中读出值。启动dashboard的时候,有一个goroutine专门负责刷新proxy状态,将每一个proxy.Token和ProxyStats的关系map对应起来,存入Topom.stats.proxies中。

说明:
如有转载,请注明出处
http://blog.csdn.net/antony9118/article/details/76339817

原创粉丝点击