Codis源码解析——sharedBackendConn

Codis源码解析——proxy监听redis请求一篇中,我们介绍过,SharedBackendConn负责实际对redis请求进行处理。

上一篇,在fillslot的过程中通过codis-server地址获取SharedBackendConn是这样用的

slot.backend.bc = s.pool.primary.Retain(addr)

为了弄清这个方法的实现,首先我们要搞清楚,基本原理是,从proxy中获取Router,然后Router的pool属性中取出属性名为primary 的sharedBackendConnPool,而这个sharedBackendConnPool又有一个map,键为codis-server的addr,值为sharedBackendConn。这个过程中涉及到的struct如下所示,它们处在不同的类中。

type Router struct {
    mu sync.RWMutex

    pool struct {
        primary *sharedBackendConnPool
        replica *sharedBackendConnPool
    }
    slots [MaxSlotNum]Slot

    config *Config
    online bool
    closed bool
}
type sharedBackendConnPool struct {
    //从启动配置文件参数封装的config
    config   *Config
    parallel int

    pool map[string]*sharedBackendConn
}
type sharedBackendConn struct {
    addr string
    host []byte
    port []byte

    //所属的池
    owner *sharedBackendConnPool
    conns [][]*BackendConn
    single []*BackendConn

    //当前sharedBackendConn的引用计数,非正数的时候表明关闭。每多一个引用就加一
    refcnt int
}
type BackendConn struct {
    stop sync.Once
    addr string

    //buffer为1024的channel
    input chan *Request
    retry struct {
        fails int
        delay Delay
    }
    state atomic2.Int64

    closed atomic2.Bool
    config *Config

    database int
}

好,搞清上面的结构之后,我们来看Retain的具体实现方法。这个方法在/pkg/proxy/backend.go中。我们以id为0的slot为例,现在从offline状态迁移到group1中。addr是group1的master的地址,即”10.0.2.15:6379”

func (p *sharedBackendConnPool) Retain(addr string) *sharedBackendConn {
    //首先从pool中直接取,取的到的话,引用计数加一
    if bc := p.pool[addr]; bc != nil {
        return bc.Retain()
    } else {
        //取不到就新建,然后放到pool里面
        bc = newSharedBackendConn(addr, p)
        p.pool[addr] = bc
        return bc
    }
}
func (s *sharedBackendConn) Retain() *sharedBackendConn {
    if s == nil {
        return nil
    }
    if s.refcnt <= 0 {
        log.Panicf("shared backend conn has been closed")
    } else {
        s.refcnt++
    }
    return s
}

如果没有从Router.pool.primary中取到,就调用newSharedBackendConn新建,然后放到primary中

func newSharedBackendConn(addr string, pool *sharedBackendConnPool) *sharedBackendConn {
    //拆分ip和端口号
    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),
    }
    //确认新建的sharedBackendConn所属于的pool
    s.owner = pool
    //len和cap都默认为16的二维切片
    s.conns = make([][]*BackendConn, pool.config.BackendNumberDatabases)
    //range用一个参数遍历二维切片,datebase是0到15
    for database := range s.conns {
        //len和cap都默认为1的一维切片
        parallel := make([]*BackendConn, pool.parallel)
        //只有parallel[0]
        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]
        }
    }
    //新建之后,这个SharedBackendConn的引用次数就置为1
    s.refcnt = 1
    return s
}
func NewBackendConn(addr string, database int, config *Config) *BackendConn {
    bc := &BackendConn{
        addr: addr, config: config, database: database,
    }
    bc.input = make(chan *Request, 1024)
    bc.retry.delay = &DelayExp2{
        Min: 50, Max: 5000,
        Unit: time.Millisecond,
    }

    go bc.run()

    return bc
}

到这里,sharedBackendConn新建完成,结构如下所示,其中owner的config就是从启动配置文件中独出的config

这里写图片描述

conns结构如下,database属性从0到15不等

这里写图片描述

single结构如下,config是从启动配置文件中读出的配置,database也是从0到15。是conns这个二维切片每一列的第一个。

这里写图片描述

还有注意上面在NewBackendConn的时候启动了一个goroutine。下面我们重点看看这个goroutine做了什么事。

func (bc *BackendConn) run() {
    log.Warnf("backend conn [%p] to %s, db-%d start service",
        bc, bc.addr, bc.database)
    for round := 0; bc.closed.IsFalse(); round++ {
        log.Warnf("backend conn [%p] to %s, db-%d round-[%d]",
            bc, bc.addr, bc.database, round)
        if err := bc.loopWriter(round); err != nil {
            bc.delayBeforeRetry()
        }
    }
    log.Warnf("backend conn [%p] to %s, db-%d stop and exit",
        bc, bc.addr, bc.database)
}

在执行这个goroutine的过程中,控制台会循环打印两三次某个db开始服务。

backend.go:258: [WARN] backend conn [0xc42005ef60] to 127.0.0.1:6379, db-0 start service
backend.go:261: [WARN] backend conn [0xc42005ef60] to 127.0.0.1:6379, db-0 round-[0]
backend.go:334: [WARN] backend conn [0xc42005ef60] to 127.0.0.1:6379, db-0 writer-[0] exit
backend.go:267: [WARN] backend conn [0xc42005ef60] to 127.0.0.1:6379, db-0 stop and exit
backend.go:282: [WARN] backend conn [0xc42005ef60] to 127.0.0.1:6379, db-0 reader-[0] exit
backend.go:258: [WARN] backend conn [0xc42015cf60] to 127.0.0.1:6379, db-0 start service
backend.go:261: [WARN] backend conn [0xc42015cf60] to 127.0.0.1:6379, db-0 round-[0]
//16个db,每个db如此循环两三次
.
.
.

这个loopWriter方法是新建sharedBackendConn的核心方法,里面不止创建了loopWriter,也创建了loopReader。LoopWriter负责将redis请求取出并进行处理。

可能有读者会问,redis请求是什么时候写进来的?可以参照Codis源码解析——proxy监听redis请求一文,在启动proxy的时候,启动了一个goroutine监听发送到19000端口的请求,在proxy的loopReader中会将请求写入BackendConn.input这个channel

func (bc *BackendConn) loopWriter(round int) (err error) {
    //如果因为某种原因退出,还有input没来得及处理,就返回错误
    defer func() {
        for i := len(bc.input); i != 0; i-- {
            r := <-bc.input
            bc.setResponse(r, nil, ErrBackendConnReset)
        }
        log.WarnErrorf(err, "backend conn [%p] to %s, db-%d writer-[%d] exit",
            bc, bc.addr, bc.database, round)
    }()
    //这个方法内启动了loopReader
    c, tasks, err := bc.newBackendReader(round, bc.config)
    if err != nil {
        return err
    }
    defer close(tasks)

    defer bc.state.Set(0)

    bc.state.Set(stateConnected)
    bc.retry.fails = 0
    bc.retry.delay.Reset()

    p := c.FlushEncoder()
    p.MaxInterval = time.Millisecond
    p.MaxBuffered = cap(tasks) / 2

    //循环从BackendConn的input这个channel取redis请求
    for r := range bc.input {
        if r.IsReadOnly() && r.IsBroken() {
            bc.setResponse(r, nil, ErrRequestIsBroken)
            continue
        }
        //将请求取出并发送给codis-server
        if err := p.EncodeMultiBulk(r.Multi); err != nil {
            return bc.setResponse(r, nil, fmt.Errorf("backend conn failure, %s", err))
        }
        if err := p.Flush(len(bc.input) == 0); err != nil {
            return bc.setResponse(r, nil, fmt.Errorf("backend conn failure, %s", err))
        } else {
            //所有请求写入tasks这个channel
            tasks <- r
        }
    }
    return nil
}
func (bc *BackendConn) newBackendReader(round int, config *Config) (*redis.Conn, chan<- *Request, error) {
    //创建与Redis的连接Conn
    c, err := redis.DialTimeout(bc.addr, time.Second*5,
        config.BackendRecvBufsize.AsInt(),
        config.BackendSendBufsize.AsInt())    
    if err != nil {
        return nil, nil, err
    }
    c.ReaderTimeout = config.BackendRecvTimeout.Duration()
    c.WriterTimeout = config.BackendSendTimeout.Duration()
    c.SetKeepAlivePeriod(config.BackendKeepAlivePeriod.Duration())

    if err := bc.verifyAuth(c, config.ProductAuth); err != nil { 
        c.Close()
        return nil, nil, err
    }
    //选择redis库
    if err := bc.selectDatabase(c, bc.database); err != nil {   
        c.Close()
        return nil, nil, err
    }

    tasks := make(chan *Request, config.BackendMaxPipeline)    
    //读取task中的请求,并将处理结果与之对应关联
    go bc.loopReader(tasks, c, round)                                           

    return c, tasks, nil
}
func (bc *BackendConn) loopReader(tasks <-chan *Request, c *redis.Conn, round int) (err error) {
    //从连接中取完所有请求并setResponse之后,连接就会关闭
    defer func() {
        c.Close()
        for r := range tasks {
            bc.setResponse(r, nil, ErrBackendConnReset)
        }
        log.WarnErrorf(err, "backend conn [%p] to %s, db-%d reader-[%d] exit",
            bc, bc.addr, bc.database, round)
    }()
    //遍历tasks,此时的r是所有的请求
    for r := range tasks {
        //从redis.Conn中解码得到处理结果                                  
        resp, err := c.Decode()                             
        if err != nil {
            return bc.setResponse(r, nil, fmt.Errorf("backend conn failure, %s", err))
        }
        if resp != nil && resp.IsError() {
            switch {
            case bytes.HasPrefix(resp.Value, errMasterDown):
                if bc.state.CompareAndSwap(stateConnected, stateDataStale) {
                    log.Warnf("backend conn [%p] to %s, db-%d state = DataStale, caused by 'MASTERDOWN'",
                        bc, bc.addr, bc.database)
                }
            }
        }
        //请求结果设置为请求的属性
        bc.setResponse(r, resp, nil)                      
    }
    return nil
}
func (bc *BackendConn) setResponse(r *Request, resp *redis.Resp, err error) error {
    r.Resp, r.Err = resp, err
    if r.Group != nil {
        r.Group.Done()
    }
    if r.Batch != nil {
        r.Batch.Done()
    }
    return err
}

控制台也输出了

这里写图片描述

这个过程中对于channel的使用方式是值得读者学习的。分为如下几个步骤:proxy的router负责将收到的请求写到bc.input;newBackendReader创建一个名为task的channel,启动一个goroutine loopReader循环读出task的内容作处理,newBackendReader立即返回创建好的task给主线程loopwriter。主线程loopwriter中bc.input循环读出内容写入task。并且loopwriter和loopReader都做了range channel过程中因为异常退出的处理。

总结一下,backendConn负责实际对redis请求进行处理。在fillSlot的时候,主要目的就是给slot填充backend.bc(实际上是sharedBackendConn)。从models.slot得到BackendAddr和MigrateFrom的地址addr,根据这个addr,首先从proxy.Router的primary sharedBackendConnPool中取sharedBackendConn,如果没有获取到,就新建sharedBackendConn再放回sharedBackendConnPool。创建sharedBackendConn的过程中启动了两个goroutine,分别是loopWriter和loopReader,loopWriter负责从backendConn.input中取出请求并发送,loopReader负责遍历所有请求,从redis.Conn中解码得到resp并设置为相关的请求的属性,这样每一个请求及其结果就关联起来了。

另外补充一下,sharedBackendConn与codis-server连接的属性主要是conns和single这两个BackendConn,这两个BackendConn又是如何新建的呢?在调用fillslot的时候,会关闭每个slot之前的backend.bc,migrate.bc,replica.bc(关闭sharedBackendConn的时候,会逐个关闭它的conns,parallel这些backendConn),在一个sharedBackendConn被关闭之前,每个BackendConn都会调用loopWriter,loopWriter中调用newBackendReader来新建与codis-server的连接,每个连接有效期默认75秒。

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

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值