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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章