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相关逻辑。