張仕華
proxy啓動
cmd/proxy/main.go文件
解析配置文件之後重點是proxy.New(config)函數
該函數中,首先會創建一個Proxy結構體,如下:
type Proxy struct {
mu sync.Mutex
...
config *Config
router *Router //Router中比較重要的是連接池和slots
...
lproxy net.Listener //19000端口的Listener
ladmin net.Listener //11080端口的Listener
...
}
然後起兩個協程,分別處理11080和19000端口的請求
go s.serveAdmin()
go s.serveProxy()
我們重點看s.serveProxy()的處理流程,即redis client連接19000端口後proxy如何分發到codis server並且將結果返回到客戶端
Proxy處理
s.serverProxy也啓動了兩個協程,一個協程對router中連接池中的連接進行連接可用性檢測,另一個協程是一個死循環,accept lproxy端口的連接,並且啓動一個新的Session進行處理,代碼流程如下:
go func(l net.Listener) (err error) {
defer func() {
eh <- err
}()
for {
c, err := s.acceptConn(l)//accept連接
if err != nil {
return err
}
NewSession(c, s.config).Start(s.router)//啓動一個新的session進行處理
}
}(s.lproxy)//s爲proxy,s.lproxy即19000端口的監聽
首先介紹一下Request結構體,該結構體會貫穿整個流程
type Request struct {
Multi []*redis.Resp //保存請求命令,按redis的resp協議類型將請求保存到Multi字段中
Batch *sync.WaitGroup //返回響應時,會在Batch處等待,r.Batch.Wait(),所以可以做到當請求執行完成後纔會執行返回函數
Group *sync.WaitGroup
Broken *atomic2.Bool
OpStr string
OpFlag
Database int32
UnixNano int64
*redis.Resp //保存響應數據,也是redis的resp協議類型
Err error
Coalesce func() error //聚合函數,適用於mget/mset等需要聚合響應的操作命令
}
Start函數處理流程如下:
tasks := NewRequestChanBuffer(1024)//tasks是一個指向RequestChan的指針,RequestChan結構體中有一個data字段,data字段是個數組,保存1024個指向Request的指針
go func() {
s.loopWriter(tasks)//從RequestChan的data中取出請求並且返回給客戶端,如果是mget/mset這種需要聚合相應的請求,則會等待所有拆分的子請求執行完畢後執行聚合函數,然後將結果返回給客戶端
decrSessions()
}()
go func() {
s.loopReader(tasks, d)//首先根據key計算該key分配到哪個slot.在此步驟中只會將slot對應的連接取出,然後將請求放到連接的input字段中。
tasks.Close()
}()
可以看到,s.loopWriter只是從RequestChan的data字段中取出請求並且返回給客戶端,通過上文Request結構體的介紹,可以看到,通過在request的Batch執行wait操作,只有請求處理完成後loopWriter纔會執行
下邊我們看loopReader的執行流程
...
r := &Request{} //新建一個Request結構體,該結構體會貫穿請求的始終,請求字段,響應字段都放在Request中
r.Multi = multi
r.Batch = &sync.WaitGroup{}
r.Database = s.database
r.UnixNano = start.UnixNano()
if err := s.handleRequest(r, d); err != nil { //執行handleRequest函數,處理請求
r.Resp = redis.NewErrorf("ERR handle request, %s", err)
tasks.PushBack(r)
if breakOnFailure {
return err
}
} else {
tasks.PushBack(r) //如果handleRequest執行成功,將請求r放入tasks(即上文的RequestChan)的data字段中。loopWriter會從該字段中獲取請求並且返回給客戶端
}
...
看handleRequest函數如何處理請求,重點是router的dispatch函數
func (s *Router) dispatch(r *Request) error {
hkey := getHashKey(r.Multi, r.OpStr)//hkey爲請求的key
var id = Hash(hkey) % MaxSlotNum //hash請求的key之後對1024取模,獲取該key分配到哪個slot
slot := &s.slots[id] //slot都保存在router的slots數組中,獲取對應的slot
return slot.forward(r, hkey)//執行slot的forward函數
}
forward函數調用process函數,返回一個BackendConn結構,然後調用其PushBack函數將請求放入bc.input中
func (d *forwardSync) Forward(s *Slot, r *Request, hkey []byte) error {
s.lock.RLock()
bc, err := d.process(s, r, hkey) //返回一個連接,並且將請求放入BackendConn的input中
s.lock.RUnlock()
if err != nil {
return err
}
bc.PushBack(r)
return nil
}
bc.PushBack(r)函數如下:
func (bc *BackendConn) PushBack(r *Request) {
if r.Batch != nil {
r.Batch.Add(1) //將請求的Batch執行add 1的操作,注意前文中的loopWriter會在Batch處等待
}
bc.input <- r //將請求放入bc.input channel
}
至此可以看到,Proxy的處理流程
loopWriter->RuquestChan的data字段中讀取請求並且返回。在Batch處等待
loopReader->將請求放入RequestChan的data字段中,並且將請求放入bc.input channel中。在Batch處加1
很明顯,Proxy並沒有真正處理請求,肯定會有goroutine從bc.input中讀取請求並且處理完成後在Batch處減1,這樣當請求執行完成後,loopWriter就可以返回給客戶端端響應了。
BackendConn的處理流程
從上文得知,proxy結構體中有一個router字段,類型爲Router,結構體類型如下:
type Router struct {
mu sync.RWMutex
pool struct {
primary *sharedBackendConnPool //連接池
replica *sharedBackendConnPool
}
slots [MaxSlotNum]Slot //slot
...
}
Router的pool中管理連接池,執行fillSlot時會真正生成連接,放入Slot結構體的backend字段的bc字段中,Slot結構體如下:
type Slot struct {
id int
...
backend, migrate struct {
id int
bc *sharedBackendConn
}
...
method forwardMethod
}
我們看一下bc字段的結構體sharedBackendConn:
type sharedBackendConn struct {
addr string //codis server的地址
host []byte //codis server主機名
port []byte //codis server的端口
owner *sharedBackendConnPool //屬於哪個連接池
conns [][]*BackendConn //二維數組,一般codis server會有16個db,第一個維度爲0-15的數組,每個db可以有多個BackendConn連接
single []*BackendConn //如果每個db只有一個BackendConn連接,則直接放入single中。當每個db有多個連接時會從conns中選一個返回,而每個db只有一個連接時,直接從single中返回
refcnt int
}
每個BackendConn中有一個 input chan *Request字段,是一個channel,channel中的內容爲Request指針。也就是第二章節loopReader選取一個BackendConn後,會將請求放入input中。
下邊我們看看處理BackendConn input字段中數據的協程是如何啓動並處理數據的。代碼路徑爲pkg/proxy/backend.go的newBackendConn函數
func NewBackendConn(addr string, database int, config *Config) *BackendConn {
bc := &BackendConn{
addr: addr, config: config, database: database,
}
//1024長度的管道,存放1024個*Request
bc.input = make(chan *Request, 1024)
bc.retry.delay = &DelayExp2{
Min: 50, Max: 5000,
Unit: time.Millisecond,
}
go bc.run()
return bc
}
可以看到,在此處創建的BackendConn結構,並且初始化bc.input字段。連接池的建立是在proxy初始化啓動的時候就會建立好。繼續看bc.run()函數的處理流程
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 { //執行loopWriter函數,此處的loopWriter和第二章節的loopWriter只是名稱相同,是兩個不同的處理函數
bc.delayBeforeRetry()
}
}
log.Warnf("backend conn [%p] to %s, db-%d stop and exit",
bc, bc.addr, bc.database)
}
func (bc *BackendConn) loopWriter(round int) (err error) {
...
c, tasks, err := bc.newBackendReader(round, bc.config) //調用newBackendReader函數。注意此處的tasks也是一個存放*Request的channel,用來此處的loopWriter和loopReader交流信息
if err != nil {
return err
}
...
for r := range bc.input { //可以看到,此處的loopWriter會從bc.input中取出數據並且處理
...
if err := p.EncodeMultiBulk(r.Multi); err != nil { //將請求編碼並且發送到codis server
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 <- r //將請求放入tasks這個channel中
}
}
return nil
}
注意此處的loopWriter會從bc.input中取出數據發送到codis server,bc.newBackendReader會起一個loopReader,從codis server中讀取數據並且寫到request結構體中,此處的loopReader和loopWriter通過tasks這個channel通信。
func (bc *BackendConn) newBackendReader(round int, config *Config) (*redis.Conn, chan<- *Request, error) {
...
tasks := make(chan *Request, config.BackendMaxPipeline)//創建task這個channel並且返回給loopWriter
go bc.loopReader(tasks, c, round)//啓動loopReader
return c, tasks, nil
}
func (bc *BackendConn) loopReader(tasks <-chan *Request, c *redis.Conn, round int) (err error) {
...
for r := range tasks { //從tasks中取出響應
resp, err := c.Decode()
if err != nil {
return bc.setResponse(r, nil, fmt.Errorf("backend conn failure, %s", err))
}
...
bc.setResponse(r, resp, nil)//設置響應數據到request結構體中
}
return nil
}
func (bc *BackendConn) setResponse(r *Request, resp *redis.Resp, err error) error {
r.Resp, r.Err = resp, err //Request的Resp字段設置爲響應值
if r.Group != nil {
r.Group.Done()
}
if r.Batch != nil {
r.Batch.Done() //注意此處會對Batch執行減1操作,這樣proxy中的loopWriter可以聚合響應並返回
}
return err
}
總結一下,BackendConn中的函數功能如下
loopWriter->從bc.input中取出請求並且發給codis server,並且將請求放到tasks channel中
loopReader->從tasks中取出請求,設置codis server的響應字段到Request的Resp字段中,並且將Batch執行減1操作
小結
一圖勝千言,圖片版權歸李老師,如下