Codis源碼解析——proxy監聽redis請求

上一篇我們講到,pkg/proxy/proxy.go的構造函數中,傳入Config,返回Proxy。其中有一步是

//s是Proxy
go s.serveProxy()

每接到一個redis請求,就創建一個獨立的session進行處理(默認的每個session的tcp連接過期時間爲75秒,也就是每個請求最多處理75秒)。這裏的第一個參數是net.Conn,Conn是一個通用的面向流的網絡連接,多個goroutines可以同時調用Conn的方法。這裏的net.Conn就是我們之前Proxy的lproxy這個Listener監聽到的19000請求到來的時候返回的net.Conn。

func NewSession(sock net.Conn, config *Config) *Session {
    c := redis.NewConn(sock,
        config.SessionRecvBufsize.AsInt(),
        config.SessionSendBufsize.AsInt(),
    )
    c.ReaderTimeout = config.SessionRecvTimeout.Duration()
    c.WriterTimeout = config.SessionSendTimeout.Duration()
    c.SetKeepAlivePeriod(config.SessionKeepAlivePeriod.Duration())

    s := &Session{
        Conn: c, config: config,
        CreateUnix: time.Now().Unix(),
    }
    s.stats.opmap = make(map[string]*opStats, 16)
    log.Infof("session [%p] create: %s", s, s)
    return s
}

下一步將路由器傳入,調用start方法。在stats.go中,有一個sessions結構,裏面記錄了總的session數量和alive的session數量。

start方法首先檢查總的session數量是否超過上限(默認爲1000),以及Router是否在線。如果不符合就返回錯誤。注意下面的方法是被Session中的Once.Do包起來的,即使有多次調用,也只會執行一次,避免浪費不必要的性能

if int(incrSessions()) > s.config.ProxyMaxClients {
    go func() {
        s.Conn.Encode(redis.NewErrorf("ERR max number of clients reached"), true)
        s.CloseWithError(ErrTooManySessions)
    }()
    decrSessions()
    return
}

if !d.isOnline() {
    go func() {
        s.Conn.Encode(redis.NewErrorf("ERR router is not online"), true)
        s.CloseWithError(ErrRouterNotOnline)
    }()
    decrSessions()
    return
}

核心就是創建loopReader和loopWriter。loopReader負責讀取和分發請求到後端,loopWriter負責合併請求結果,然後返回給客戶端。

//給RequestChan的buff中賦1024位的數組,並返回一個RequestChan
tasks := NewRequestChanBuffer(1024)
go func() {
    s.loopWriter(tasks)
    //alive session減一
    decrSessions()
}()

go func() {
    s.loopReader(tasks, d)
    //所有請求取完或者proxy退出之後,上面的方法就會結束,關閉tasks這個requestChan
    tasks.Close()
}()

我們看一下RequestChan的結構,後面很多東西都是基於它的。每一個session都會有一個RequestChan

type RequestChan struct {
    lock sync.Mutex

    //sync.NewCond(&RequestChan.lock)
    //如果RequestChan爲空,就讓goroutinewait;如果向RequestChan放入了一個請求,並且有goroutine在等待,就喚醒一個
    cond *sync.Cond

    data []*Request
    buff []*Request

    waits  int
    closed bool
}

這一篇我們對這兩個方法進行詳細介紹。

loopReader讀取和分發請求到後端,關鍵代碼就是handleRequest函數,傳入的兩個參數分別是d *Router和r := &Request{},也就是把結果存到task裏面,後面loopWriter會用到

func (s *Session) loopReader(tasks *RequestChan, d *Router) (err error) {
    defer func() {
        s.CloseReaderWithError(err)
    }()

    var (
        breakOnFailure = s.config.SessionBreakOnFailure
        maxPipelineLen = s.config.SessionMaxPipeline
    )

    //session只要沒有退出,就一直從conn中取請求,直到請求取完就return,然後會關閉tasks這個requestChan
    for !s.quit {
        //從redis連接中取出請求參數
        multi, err := s.Conn.DecodeMultiBulk()
        if err != nil {
            return err
        }
        s.incrOpTotal()

        //檢測requestChan的data是否超過配置的每個pipeline最大請求長度,默認爲10000
        if tasks.Buffered() > maxPipelineLen {
            return ErrTooManyPipelinedRequests
        }

        start := time.Now()
        s.LastOpUnix = start.Unix()
        s.Ops++

        r := &Request{}
        //這個Multi非常重要,請求的參數就在裏面,是一個[]*redis.Resp切片
        r.Multi = multi
        //WaitGroup的作用是,阻塞主線程的執行,一直等到所有的goroutine執行完成。每創建一個goroutine
        //就把任務隊列中任務的數量+1,任務完成,將任務隊列中的任務數量-1。有點類似於java裏面的CountDownLatch
        //這個Batch用於檢測redis請求是否完成(完成的標誌是BackendConn調用了setRResponse)
        r.Batch = &sync.WaitGroup{}
        r.Database = s.database
        r.UnixNano = start.UnixNano()

        if err := s.handleRequest(r, d); err != nil {
            r.Resp = redis.NewErrorf("ERR handle request, %s", err)
            tasks.PushBack(r)
            if breakOnFailure {
                return err
            }
        } else {
            tasks.PushBack(r)
        }
    }
    return nil
}

先解釋一下上面的PushBack方法,這個方法比較簡單,就是把request(此時已經處理完畢,將resp設置爲request的一個參數)添加到當前Session中之前創建的RequestChan中。loopWriter後面再遍歷RequestChan取出所有請求及結果

func (c *RequestChan) lockedPushBack(r *Request) int {
    if c.closed {
        panic("send on closed chan")
    }
    //RequestChan的waits不爲0的時候(也就是在RequestChan上等待的request數量不爲0時),喚醒一個在cond上等待的goroutine
    //這裏的意思是,如果向requestChan中放入了請求,就將一個在cond上等待取出的goroutine喚醒
    if c.waits != 0 {
        c.cond.Signal()
    }
    //將request添加到RequestChan的data []*Request切片中,用於記錄處理過的請求。
    c.data = append(c.data, r)
    return len(c.data)
}

這個方法不是重點,重點在於handleRequest方法,就是將請求取出,然後根據不同的redis請求調用不同的方法,被調用的就是codis-server

func (s *Session) handleRequest(r *Request, d *Router) error {
    //解析請求。opstr取決於具體的命令,比如說"SET"
    opstr, flag, err := getOpInfo(r.Multi)
    if err != nil {
        return err
    }
    r.OpStr = opstr
    r.OpFlag = flag
    r.Broken = &s.broken

    //有些命令不支持,就會返回錯誤
    if flag.IsNotAllowed() {
        return fmt.Errorf("command '%s' is not allowed", opstr)
    }

    switch opstr {
    case "QUIT":
        return s.handleQuit(r)
    case "AUTH":
        return s.handleAuth(r)
    }

    if !s.authorized {
        if s.config.SessionAuth != "" {
            r.Resp = redis.NewErrorf("NOAUTH Authentication required")
            return nil
        }
        s.authorized = true
    }

    //根據不同的redis操作調用不同的方法
    switch opstr {
    case "SELECT":
        return s.handleSelect(r)
    case "PING":
        return s.handleRequestPing(r, d)
    case "INFO":
        return s.handleRequestInfo(r, d)
    case "MGET":
        return s.handleRequestMGet(r, d)
    default:
    return d.dispatch(r)
    .
    .
    }
}

其實仔細觀察上面的switch case語句,就能發現,除了SELECT和default之外,都多傳入了一個參數,也就是Router。聯想到路由本身的作用,就不難想到,handleRequestPing之類的方法,都是要對底層的redis服務器進行分發的;反之,handleSelect方法,則不需要分發。我們來對比一下。

首先是handleSelect方法,進去看一下。這個方法很簡單了,因爲只是選擇db。從請求參數中取出db號,做了一個號超出範圍的異常處理,如果db取得沒有問題的話,就返回ok,error爲nil

func (s *Session) handleSelect(r *Request) error {
    if len(r.Multi) != 2 {
        r.Resp = redis.NewErrorf("ERR wrong number of arguments for 'SELECT' command")
        return nil
    }
    //將字符串轉換爲十進制整數,從請求參數中取出是對哪個db進行操作
    switch db, err := strconv.Atoi(string(r.Multi[1].Value)); {
    case err != nil:
        r.Resp = redis.NewErrorf("ERR invalid DB index")
    case db < 0 || db >= int(s.config.BackendNumberDatabases):
        r.Resp = redis.NewErrorf("ERR invalid DB index, only accept DB [0,%d)", s.config.BackendNumberDatabases)
    default:
        r.Resp = RespOK
        s.database = int32(db)
    }
    return nil
}

其他傳入了Router參數的方法中,都調用了Router的dispatch方法,將請求分發給相應的槽進行處理,如果調用Mset,一次設置多個值的話,就還要多做一步,通過Coalesce來合併請求結果

func (s *Session) handleRequestMSet(r *Request, d *Router) error {
    var nblks = len(r.Multi) - 1
    switch {
    case nblks == 0 || nblks%2 != 0:
        r.Resp = redis.NewErrorf("ERR wrong number of arguments for 'MSET' command")
        return nil
    case nblks == 2:
        return d.dispatch(r)
    }

    //將一個Mset請求拆分成多個子set請求,分別dispatch
    var sub = r.MakeSubRequest(nblks / 2)
    for i := range sub {
        sub[i].Multi = []*redis.Resp{
            r.Multi[0],
            r.Multi[i*2+1],
            r.Multi[i*2+2],
        }
        if err := d.dispatch(&sub[i]); err != nil {
            return err
        }
    }
    r.Coalesce = func() error {
        for i := range sub {
            if err := sub[i].Err; err != nil {
                return err
            }
            switch resp := sub[i].Resp; {
            case resp == nil:
                return ErrRespIsRequired
            case resp.IsString():
                r.Resp = resp
            default:
                return fmt.Errorf("bad mset resp: %s value.len = %d", resp.Type, len(resp.Value))
            }
        }
        return nil
    }
    return nil
}

將某個request分發給各個具體的slot進行處理的方法主要有三個,都是由Router來完成:

//根據key進行轉發
func (s *Router) dispatch(r *Request) error {
    hkey := getHashKey(r.Multi, r.OpStr)
    var id = Hash(hkey) % MaxSlotNum
    slot := &s.slots[id]
    //將請求分發到相應的slot
    return slot.forward(r, hkey)
}
//將request發到指定slot
func (s *Router) dispatchSlot(r *Request, id int) error {
    if id < 0 || id >= MaxSlotNum {
        return ErrInvalidSlotId
    }
    slot := &s.slots[id]
    return slot.forward(r, nil)
}
//這個是指明瞭redis服務器地址的請求,如果找不到就返回false
func (s *Router) dispatchAddr(r *Request, addr string) bool {
    s.mu.RLock()
    defer s.mu.RUnlock()
    if bc := s.pool.primary.Get(addr).BackendConn(r.Database, r.Seed16(), false); bc != nil {
        bc.PushBack(r)
        return true
    }
    if bc := s.pool.replica.Get(addr).BackendConn(r.Database, r.Seed16(), false); bc != nil {
        bc.PushBack(r)
        return true
    }
    return false
}

我們看一下forward方法,這個方法的具體實現是在/pkg/proxy/foward.go中。每一個slot可能有兩種forwardMethod,分別是forwardSync或者forwardSemiSync,原理類似,都是將指定的slot、request、鍵的哈希值,經過process得到實際處理請求的BackendConn,然後把請求放入BackendConn的chan *Request中,等待處理。這兩個的區別就是,如果一個slot在遷移中,proxy分別會使用SLOTSMGRTTAGONE和SLOTSMGRT-EXEC-WRAPPER強制完成其遷移。BackendConn一般最重要的信息就是redis服務器的地址(例如10.0.2.15:6379)以及對應的database編號(0到15,一般就是0)。

//forwardSync的Forward和process
func (d *forwardSync) Forward(s *Slot, r *Request, hkey []byte) error {
    //加slot的讀鎖
    s.lock.RLock()
    bc, err := d.process(s, r, hkey)   
    s.lock.RUnlock()
    if err != nil {
        return err
    }
    //請求放入BackendConn等待處理
    bc.PushBack(r)                     
    return nil
}
func (d *forwardSync) process(s *Slot, r *Request, hkey []byte) (*BackendConn, error) {
    //檢查該slot的backend是否爲空
    if s.backend.bc == nil {
        log.Debugf("slot-%04d is not ready: hash key = '%s'",
            s.id, hkey)
        return nil, ErrSlotIsNotReady
    }
    //如果這個slot處在遷移過程中,那麼其migrate就不爲空(從何處遷移),由proxy的slot的fowardMethod強制對其完成遷移
    if s.migrate.bc != nil && len(hkey) != 0 {
        if err := d.slotsmgrt(s, hkey, r.Database, r.Seed16()); err != nil {
            log.Debugf("slot-%04d migrate from = %s to %s failed: hash key = '%s', database = %d, error = %s",
                s.id, s.migrate.bc.Addr(), s.backend.bc.Addr(), hkey, r.Database, err)
            return nil, err
        }
    }
    r.Group = &s.refs
    r.Group.Add(1)
    return d.forward2(s, r), nil
}
//forwardSemiAsync的Forward和process
func (d *forwardSemiAsync) Forward(s *Slot, r *Request, hkey []byte) error {
    var loop int
    for {
        s.lock.RLock()
        bc, retry, err := d.process(s, r, hkey)
        s.lock.RUnlock()

        switch {
        case err != nil:
            return err
        case !retry:
            if bc != nil {
                bc.PushBack(r)
            }
            return nil
        }

        var delay time.Duration
        switch {
        case loop < 5:
            delay = 0
        case loop < 20:
            delay = time.Millisecond * time.Duration(loop)
        default:
            delay = time.Millisecond * 20
        }
        time.Sleep(delay)

        if r.IsBroken() {
            return ErrRequestIsBroken
        }
        loop += 1
    }
}

無論是forwardSync還是forwardSemiAsync,在process的過程中,都要調用下面的forward2方法來從Slot獲取真正處理redis請求的BackendConn


//獲取真正處理redis請求的BackendConn 
func (d *forwardHelper) forward2(s *Slot, r *Request) *BackendConn {
    var database, seed = r.Database, r.Seed16()
    //如果是讀請求,就調用replicaGroup來處理
    if s.migrate.bc == nil && r.IsReadOnly() && len(s.replicaGroups) != 0 {
        for _, group := range s.replicaGroups {
            var i = seed
            for _ = range group {
                i = (i + 1) % uint(len(group))
                if bc := group[i].BackendConn(database, seed, false); bc != nil {
                    return bc
                }
            }
        }
    }
    //從sharedBackendConn中取出一個BackendConn(sharedBackendConn中儲存了BackendConn組成的二維切片)
    return s.backend.bc.BackendConn(database, seed, true)
}
func (s *sharedBackendConn) BackendConn(database int32, seed uint, must bool) *BackendConn {
    if s == nil {
        return nil
    }

    if s.single != nil {
        bc := s.single[database]
        if must || bc.IsConnected() {
            return bc
        }
        return nil
    }

    var parallel = s.conns[database]

    var i = seed
    for _ = range parallel {
        i = (i + 1) % uint(len(parallel))
        if bc := parallel[i]; bc.IsConnected() {
            return bc
        }
    }
    if !must {
        return nil
    }
    return parallel[0]
}

選出了bc之後,就把請求交給bc,等待處理

//請求放入BackendConn等待處理。如果request的sync.WaitGroup不爲空,就加一,然後判斷加一之後的值,如果加一之後couter爲0,那麼所有阻塞在counter上的goroutine都會得到釋放
//將請求直接存入到BackendConn的chan *Request中,等待後續被取出並進行處理。
func (bc *BackendConn) PushBack(r *Request) {
    if r.Batch != nil {
        //請求處理之後,setResponse的時候,r.Batch會減1,表明請求已經處理完。
        //session的loopWriter裏面收集請求結果的時候,會調用wait方法等一次的所有請求處理完成
        r.Batch.Add(1)
    }
    bc.input <- r
}

再回到LoopWriter,如果已經找不到文件位置的話,請轉向/pkg/proxy/session.go。LoopWriter的作用就是合併請求的處理結果並返回給客戶端。請求結果由BackendConn處理好之後,放在Request這個struct的*redis.Resp屬性中,這裏只需要把結果取出。可以看到,codis將請求與結果關聯起來的方式,就是把結果當做request的一個屬性

func (s *Session) loopWriter(tasks *RequestChan) (err error) {
    defer func() {
        s.CloseWithError(err)
        tasks.PopFrontAllVoid(func(r *Request) {
            s.incrOpFails(r, nil)
        })
        s.flushOpStats(true)
    }()

    var (
        breakOnFailure = s.config.SessionBreakOnFailure
        maxPipelineLen = s.config.SessionMaxPipeline
    )

    p := s.Conn.FlushEncoder()
    p.MaxInterval = time.Millisecond
    p.MaxBuffered = maxPipelineLen / 2

    //前面我們在tasks.PushBack(r)中,將請求放入了data []*Request切片,現在就是取出最早的請求及其處理結果
    //如果當前session的requestChan爲空,就調用cond.wait讓goroutine等待,直到調用pushback又放入請求爲止
    return tasks.PopFrontAll(func(r *Request) error {
        resp, err := s.handleResponse(r)
        if err != nil {
            resp = redis.NewErrorf("ERR handle response, %s", err)
            if breakOnFailure {
                s.Conn.Encode(resp, true)
                return s.incrOpFails(r, err)
            }
        }
        if err := p.Encode(resp); err != nil {
            return s.incrOpFails(r, err)
        }
        fflush := tasks.IsEmpty()
        if err := p.Flush(fflush); err != nil {
            return s.incrOpFails(r, err)
        } else {
            s.incrOpStats(r, resp.Type)
        }
        if fflush {
            s.flushOpStats(false)
        }
        return nil
    })
}
func (s *Session) handleResponse(r *Request) (*redis.Resp, error) {
    //goroutine都會一直阻塞到所有請求處理完之後,前面我們已經看到,向RequestChan中添加一個請求的時候
    //request.Batch會加一,後面BackendConn調用setResponse,也就是完成處理的時候,又會調用Done方法減一
    r.Batch.Wait()
    //如果是單個的請求,例如SET,這裏就爲空了
    if r.Coalesce != nil {
        //如果是MSET這種請求,就需要調用之前自定義的Coalesce方法,將請求合併之後再返回
        if err := r.Coalesce(); err != nil {
            return nil, err
        }
    }
    if err := r.Err; err != nil {
        return nil, err
    } else if r.Resp == nil {
        return nil, ErrRespIsRequired
    }
    return r.Resp, nil
}

至於sharedBackendConn是何時創建以及如何處理請求的,請看筆者的另一篇博客Codis源碼解析——sharedBackendConn

總結一下,在啓動proxy的時候,會有一個goroutine專門負責監聽發送到19000端口的redis請求。每次有redis請求過來,會新建一個session,並啓動兩個goroutine,loopReader和loopWriter。loopReader的作用是,Router根據請求指派到slot,並找到slot後面真正處理請求的BackendConn,再將請求放入BackendConn的RequestChan中,等待後續取出並進行處理,然後將請求寫入session的RequestChan。loopWriter則將請求的結果從session的RequestChan(Request的*redis.Resp屬性)中取出並返回,如果是MSET這種批處理的請求,要注意合併結果再返回

說明
如有轉載,請註明出處:
http://blog.csdn.net/antony9118/article/details/75293115

發佈了62 篇原創文章 · 獲贊 237 · 訪問量 58萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章