【每日笔记】【Go学习笔记】2019-01-10 codis proxy处理流程

张仕华

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操作

小结

一图胜千言,图片版权归李老师,如下

clipboard.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值