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

前面我们说过,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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值