Codis源码-server、proxy加入集群(Redis分布式解决方案2)

910 阅读6分钟

1.概述

上一篇文章介绍了codis-proxy启动以及处理请求的过程。本文主要说一下codis-proxy和codis-server在处理请求之前如何加入集群(上线),也会对sentinel集成进行简单分析。

2.核心点

codis-proxy的启动

  • 路由表的加载
  • 服务注册(客户端如何调用) codis-server启动
  • codis-proxy如何发现redis节点

3.源码解析

codis-proxy加入集群

上篇文章讲过,codis-proxy启动后进入wait状态,并不能直接处理请求,必须调用fillSlot方法填充路由表之后,才能online(处理请求)。那我们就来关注这个过程是什么时候触发的。

说这个之前我们需要了解codis的topom模块:topom模块主要是dashboard api的kernel。也就是说控制台的api请求都是由topom来处理(可以理解为java的一个web服务)。

其实这里设计是有一点缺陷的,比如topom单点。为什么单点,作者也回答过:因为topom是直接和zk去交互的,单个topom对zk压力已经不小,所以只能采取单点+进程cache的方式实现。

topom_proxy#CreateProxy

这个方法被topom_api的一个http接口调用。也就是在控制台new 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, err := proxy.NewApiClient(addr).Model()
    if err != nil {
        return errors.Errorf("proxy@%s fetch model failed, %s", addr, err)
    }
    c := s.newProxyClient(p)

    if err := c.XPing(); err != nil {
        return errors.Errorf("proxy@%s check xauth failed, %s", addr, err)
    }
    if ctx.proxy[p.Token] != nil {
        return errors.Errorf("proxy-[%s] already exists", p.Token)
    } else {
        p.Id = ctx.maxProxyId() + 1
    }
    defer s.dirtyProxyCache(p.Token)

    if err := s.storeCreateProxy(p); err != nil {
        return err
    } else {
        return s.reinitProxy(ctx, p, c)
    }
}

1.调用s.newContext方法,该方法初始化一个ctx,其实就是获取了一份当前集群元数据(路由信息、sentinel信息等)。用于后面逻辑处理,topom的大部分操作都会先调用该方法。 简单说一下这个方法:其实就是从内存中获取数据,如果发现数据为空,则会去zk拿。这也就是为啥是单点的原因。

2.调用http方法从proxy获取到model信息。model其实就是proxy启动的时候对应的节点信息。

3.对proxy进行ping,保证其alive

4.如果该proxy已经加入当前集群,则返回错误。用token进行唯一性区分。

5.将proxy数据存储到zk中。然后调用reinitProxy方法。

注:每个对集群元数据更新方法,都会通过defer以及hooks保证方法执行异常对数据的清理,避免脏数据。

topom_proxy#reinitProxy

func (s *Topom) reinitProxy(ctx *context, p *models.Proxy, c *proxy.ApiClient) error {
    log.Warnf("proxy-[%s] reinit:\n%s", p.Token, p.Encode())
    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)
    }
    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
}

这个方法主要就是调用proxy的http方法,意料之中,先调用c.FillSlots,再调用start。最后设置对应的sentinels实现集群高可用。

FillSlots

这个最终会调用router.go的fillSlot方法。 其实就是为路由表初始化solt数据。代码比较简单,我们重点关注下面几行。

if addr := m.BackendAddr; len(addr) != 0 {
    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
}

主要是初始化backend, migrate两个结构。因为处理请求以及数据迁移主要就是通过backend, migrate去和后端redis交互。

newSharedBackendConn方法

s.pool.primary.Retain主要会调用newSharedBackendConn去初始化连接

func newSharedBackendConn(addr string, pool *sharedBackendConnPool) *sharedBackendConn {
    host, port, err := net.SplitHostPort(addr)
    if err != nil {
        log.ErrorErrorf(err, "split host-port failed, address = %s", addr)
    }
    s := &sharedBackendConn{
        addr: addr,
        host: []byte(host), port: []byte(port),
    }
    s.owner = pool
    s.conns = make([][]*BackendConn, pool.config.BackendNumberDatabases)
    for database := range s.conns {
        parallel := make([]*BackendConn, pool.parallel)
        for i := range parallel {
            parallel[i] = NewBackendConn(addr, database, pool.config)
        }
        s.conns[database] = parallel
    }
    if pool.parallel == 1 {
        s.single = make([]*BackendConn, len(s.conns))
        for database := range s.conns {
            s.single[database] = s.conns[database][0]
        }
    }
    s.refcnt = 1
    return s
}

1.创建连接池sharedBackendConn

2.初始化连接二维数组,单点redis分为16个db。每个db可以创建多条连接,保证高并发。

3.在NewBackendConn中会启动协程,执行loopWrite以及loopReader方法。这个时候往BackendConn的input管道丢请求就可以自动处理了。

Start方法

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
    s.router.Start()
    if s.jodis != nil {
        s.jodis.Start()
    }
    return nil
}

1.主要是更改proxy中各状态,比如proxy和router的online字段。

2.s.jodis.Start主要就是将节点数据注册到zk。这样客户端就可以发现节点,并进行调用

SetSentinels方法

这个方法主要就是更新当前proxy的sentinel信息。用于监听sentinel对主节点的switch,后文细讲。

codis-server启动

GroupAddServer

c, err := redis.NewClient(addr, s.topom.Config().ProductAuth, time.Second)
if err != nil {
    log.WarnErrorf(err, "create redis client to %s failed", addr)
    return rpc.ApiResponseError(err)
}
defer c.Close()
if _, err := c.SlotsInfo(); err != nil {
    log.WarnErrorf(err, "redis %s check slots-info failed", addr)
    return rpc.ApiResponseError(err)
}
if err := s.topom.GroupAddServer(gid, dc, addr); err != nil {
    return rpc.ApiResponseError(err)
} else {
    return rpc.ApiResponseJson("OK")
}

加入server节点会调用该方法。

1.创建一个redis客户端

2.发送SLOTSINFO命令。主要判断是否能将该server加入到集群。这个命令redis本身不支持,是codis-server增加的命令。

3.调用Topom的GroupAddServer方法。

Topom#GroupAddServer

1.校验该server是否已经加入集群

for _, g := range ctx.group {
    for _, x := range g.Servers {
        if x.Addr == addr {
            return errors.Errorf("server-[%s] already exists", addr)
        }
    }
}

2.更新sentinel信息

3.将节点加入到对应的group

Group结构

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"`
}

GroupServer存储了当前group所有codis-server,数组0索引为主节点。

如果同一个group加入多个节点,那么需要调用SyncCreateAction去将主节点同步为从节点。具体操作参考后台。

Sentinle模块

topom启动的时候会调用rewatchSentinels方法。其实proxy也有这个方法,而且逻辑差不多。proxy的该方法在topom启动以及topom上线或创建新proxy的时候会被调用到。我们直接关注topom的rewatchSentinels方法即可。

   s.ha.monitor = redis.NewSentinel(s.config.ProductName, s.config.ProductAuth)
    s.ha.monitor.LogFunc = log.Warnf
    s.ha.monitor.ErrFunc = log.WarnErrorf
    go func(p *redis.Sentinel) {
        var trigger = make(chan struct{}, 1)
        delayUntil := func(deadline time.Time) {
            for !p.IsCanceled() {
                var d = deadline.Sub(time.Now())
                if d <= 0 {
                    return
                }
                time.Sleep(math2.MinDuration(d, time.Second))
            }
        }
        go func() {
            defer close(trigger)
            callback := func() {
                select {
                case trigger <- struct{}{}:
                default:
                }
            }
            for !p.IsCanceled() {
                timeout := time.Minute * 15
                retryAt := time.Now().Add(time.Second * 10)
                if !p.Subscribe(servers, timeout, callback) {
                    delayUntil(retryAt)
                } else {
                    callback()
                }
            }
        }()
        go func() {
            for range trigger {
                var success int
                for i := 0; i != 10 && !p.IsCanceled() && success != 2; i++ {
                    timeout := time.Second * 5
                    masters, err := p.Masters(servers, timeout)
                    if err != nil {
                        log.WarnErrorf(err, "fetch group masters failed")
                    } else {
                        if !p.IsCanceled() {
                            s.SwitchMasters(masters)
                        }
                        success += 1
                    }
                    delayUntil(time.Now().Add(time.Second * 5))
                }
            }
        }()
    }(s.ha.monitor)

1.创建一个trigger管道

2.启动协程调用p.Subscribe方法订阅sentinel事件。了解sentienl的都知道sentienl在主从切换的时候会调用业务方的回调函数。这里不细看,其实就是监听+switch-master事件。如果有+switch-master事件触发则会返回callback,也就是往trigger写入。

3.启动协程处理+switch-master逻辑。每当trigger可读,调用p.Masters。该方法使用SENTINEL masters命令从每个sentinel获取当前master信息。根据信息获取到每个group对应的master。

4.调用s.SwitchMasters执行切换master的逻辑。topom其实就是更新zk中master的信息。 proxy的该方法其实就是更新内存中路由表的信息

4.总结

本文简单描述了codis-proxy和server加入集群的流程。比较简单,后面会介绍slot相关逻辑。