在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.youkuaiyun.com/antony9118/article/details/77334729