前言:
接着上一章讲,上一章讲了loopReader。这一块会延续上一章节继续往下讲。
golang版本: go1.13.5 darwin/amd64
codis版本:codis 3.2
调试工具:dlv
codis源码:
https://github.com/CodisLabs/codis/
(一)proxy处理流程
上一章中讲proxy流程时只讲了session连接部分,这一章把完整的流程串一串。
如图中所示,codis-proxy会分为两大部分:
1)一部分是客户端交互部分,session的两个方法loopWriter和loopReader。
loopWriter负责返回客户端信息,loopReader负责分发slots部分。
2)另一部分是服务交互部分,backendConn的两个方法loopWriter和loopReader。
loopWriter负责写入服务端数据,loopReader部分负责处理返回客户端等待。
(二)源码解析
2.1 RequestChan结构与Request结构
type RequestChan struct {
lock sync.Mutex //互斥锁
cond *sync.Cond //条件锁
data []*Request
buff []*Request
waits int //等待数
closed bool //是否关闭
}
每一个session都有一个RequestChan结构。
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 //精确的unix时间
*redis.Resp //保存响应数据,也是redis的resp协议类型
Err error
Coalesce func() error //聚合函数,适用于mget/mset等需要聚合响应的操作命令
}
上一章讲到session中的loopReader将请求放入RequestChan的data字段中,并且将请求放入bc.input 管道中。在Batch处加1。而bc.input管道中存放的则是一个Request结构。如下图:
下图中为Request交互时原型:
2.2 BackendConn的loopWriter处理
图中为dlv调试,BackendConn的loopWriter会开启一堆协程去轮询。
在bc.input 管道设置后,会在loopWriter的for r := range bc.input 轮询取出数据并且处理。
tasks也是一个管道,是BackendConn中 loopWriter和loopReader交流信息。
loopWriter源码
func (bc *BackendConn) loopWriter(round int) (err error) {
// 。。。 省略
c, tasks, err := bc.newBackendReader(round, bc.config) //调用newBackendReader函数。
if err != nil {
return err
}
//。。。省略
for r := range bc.input {
if r.IsReadOnly() && r.IsBroken() {
bc.setResponse(r, nil, ErrRequestIsBroken)
continue
}
if err := p.EncodeMultiBulk(r.Multi); err != nil { //将请求编码发送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 //将Request请求放入tasks管道中
}
}
return nil
}
bc.newBackendReader的tasks也是一个存放*Request的channel,用来BackendConn中的loopWriter和loopReader交流信息。p.EncodeMultiBulk函数是将请求编码发送到server,这里的server是codis server。codis server是改版后的redis服务。
2.3 BackendConn的loopReader处理
func (bc *BackendConn) newBackendReader(round int, config *Config) (*redis.Conn, chan<- *Request, error) {
//...省略
if err := bc.verifyAuth(c, config.ProductAuth); err != nil { //验证权限
c.Close()
return nil, nil, err
}
if err := bc.selectDatabase(c, bc.database); err != nil { //选择数据库
c.Close()
return nil, nil, err
}
tasks := make(chan *Request, config.BackendMaxPipeline) ///创建task这个管道并且返回给loopWriter
go bc.loopReader(tasks, c, round) //启动loopReader协程
return c, tasks, nil
}
newBackendReader函数创建了tasks返回给loopWriter,并且启动loopReader协程。
loopReader函数:
func (bc *BackendConn) loopReader(tasks <-chan *Request, c *redis.Conn, round int) (err error) {
//...省略
for r := range tasks { //从tasks取出Request
resp, err := c.Decode()
if err != nil {
return bc.setResponse(r, nil, fmt.Errorf("backend conn failure, %s", err))
}
//...省略
bc.setResponse(r, resp, nil) //响应
}
return nil
}
从tasks中取出响应,设置到bc.setResponse函数中。
图中为设置到bc.setResponse函数的dlv调试结果。
setResponse函数:
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() //r.Batch.Add(1)返回session中loopWriter的r.Batch.Wait等待
}
return err
}
图中为dlv调试结果,r.Batch设置时触发r.Batch.Done() 返回。
图中为dlv调试结果,r.Batch.Done() 触发session中loopWriter的r.Batch.Wait等待返回。
总结:
1.codis-proxy会分为两大部分:一部分是客户端交互部分,session的两个方法loopWriter和loopReader。
loopWriter负责返回客户端信息,loopReader负责分发slots部分。另一部分是服务交互部分,backendConn的两个方法loopWriter和loopReader。loopWriter负责写入服务端数据,loopReader部分负责处理返回客户端等待。
2.bc.input是连接session中loopReader与backendConn中loopWriter交流信息。
3.tasks是backendConn中loopWriter与loopReader的交流信息。
4.BackendConn中loopReader的r.Batch.Done() 触发返回session中loopWriter的r.Batch.Wait。