在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