豌豆夹Redis解决方案Codis源码剖析:Proxy代理

1.预备知识

1.1 Codis

Codis就不详细说了,摘抄一下GitHub上的一些项目描述

Codis is a proxy based high performance Redis cluster solution written in Go/C, an alternative to Twemproxy. It supports multiple stateless proxy with multiple redis instances and is engineered to elastically scale, Easily add or remove redis or proxy instances on-demand/dynamicly.

  • Auto rebalance
  • Support both redis or rocksdb transparently
  • GUI dashboard & admin tools
  • Supports most of Redis commands, Fully compatible with twemproxy
  • Native Redis clients are supported
  • Safe and transparent data migration, Easily add or remove nodes on-demand
  • Command-line interface is also provided
  • RESTful APIs

安装步骤官方网站上也写的很清楚了:

  1. // Golang环境安装配置
  2. [root@vm root]$ tar -C /usr/local -zxf go1.4.2.linux-amd64.tar.gz
  3. [root@vm root]$ vim /etc/profile
  4. export GOROOT=/usr/local/go
  5. export PATH=$GOROOT/bin:$PATH
  6. export GOPATH=/home/user/go
  7. [root@vm root]$ source /etc/profile
  8. [root@vm root]$ go version
  9. // 下载Codis依赖,编译Codis
  10. [root@vm root]$ cd codis-1.92
  11. [root@vm root]$ ./bootstrap.sh

1.2 Golang

Codis的核心代码都是用Golang开发的,所以在一头扎进源代码里之前,先了解Golang的语法特性是必不可少的!好在Golang除了少数一些“古怪之处”外,还算容易上手。具体请参考笔者的另一篇文章《 Java程序员的Golang入门指南(上)》

1.3 Redis通信协议

Redis通信协议简称为RESP,在分析网络通信时需要这一部分的知识。RESP本身的设计非常简单,所以还是快速过一下吧。具体请参考笔者的另一篇文章《用Netty解析Redis网络协议》以及官网上的协议具体规范

1.4 Zookeeper

Codis以及现今很多后端中间件都使用Zookeeper来协调分布式通信,所以在阅读源码前我们至少要知道Zookeeper是干什么的,有哪些基本操作和监听器。具体请参考笔者的另一篇文章《Apache Curator入门实战》

2.Proxy源码剖析

Codis可以分为客户端Jodis、代理中间件Codis Proxy、Zookeeper协调、监控界面、Redis定制版Codis Server等组件。这里第一部分主要关注最核心的Proxy部分的源码。

2.1 程序入口main.go

codis-1.92/cmd/proxy/main.go是Proxy组件的main函数入口,完成的主要工作就是设置日志级别、解析命令行参数(CPU核数、绑定地址等)、加载配置文件、Golang环境(runtime.GOMAXPROCS并发数)、启动Socket监听等常规任务。顺藤摸瓜,我们要分析的关键应该就在router中。

  1. func main() {
  2. // 1.打印banner,设置日志级别
  3. fmt.Print(banner)
  4. log.SetLevelByString("info")
  5. // 2.解析命令行参数
  6. args, err := docopt.Parse(usage, nil, true, "codis proxy v0.1", true)
  7. if err != nil {
  8. log.Error(err)
  9. }
  10. if args["-c"] != nil {
  11. configFile = args["-c"].(string)
  12. }
  13. ...
  14. dumppath := utils.GetExecutorPath()
  15. log.Info("dump file path:", dumppath)
  16. log.CrashLog(path.Join(dumppath, "codis-proxy.dump"))
  17. // 3.设置Golang并发数等
  18. router.CheckUlimit(1024)
  19. runtime.GOMAXPROCS(cpus)
  20. // 4.启动Http监听
  21. http.HandleFunc("/setloglevel", handleSetLogLevel)
  22. go http.ListenAndServe(httpAddr, nil)
  23. log.Info("running on ", addr)
  24. conf, err := router.LoadConf(configFile)
  25. if err != nil {
  26. log.Fatal(err)
  27. }
  28. // 5.创建Server,启动Socket监听
  29. s := router.NewServer(addr, httpAddr, conf)
  30. s.Run()
  31. log.Warning("exit")
  32. }

2.2 核心类Server

打开codis-1.92/pkg/proxy/router/router.go,在分析请求接收和分发前,先来看一个最核心的类Server,它就是在main.go中调用router.NewServer()时创建的。说一下比较重要的几个字段:

  • reqCh:Pipeline请求的Channel。
  • pools:Slot与cachepool的map。
  • evtbus/top:处理Zookeeper消息,更新拓扑结构。
  • bufferedReq:Slot处于migrate期间被缓冲的请求。
  • pipeConns:Slot对应的taskrunner。

注意interface{},它表示空interface,按照Golang的Duck Type继承方式,任何类都是空接口的子类。所以interface{}有点像C语言中的void*/char*。

因为Codis是先启动监听再开始接收Socket请求,所以对go s.handleTopoEvent()的分析放到后面。在下一节我们先看一下Codis是如何启动对Socket端口监听并将接收到的请求放入到Server的reqCh管道中的。

  1. type Server struct {
  2. slots [models.DEFAULT_SLOT_NUM]*Slot
  3. top *topo.Topology
  4. evtbus chan interface{}
  5. reqCh chan *PipelineRequest
  6. lastActionSeq int
  7. pi models.ProxyInfo
  8. startAt time.Time
  9. addr string
  10. moper *MultiOperator
  11. pools *cachepool.CachePool
  12. counter *stats.Counters
  13. OnSuicide OnSuicideFun
  14. bufferedReq *list.List
  15. conf *Conf
  16. pipeConns map[string]*taskRunner //redis->taskrunner
  17. }
  18. func NewServer(addr string, debugVarAddr string, conf *Conf) *Server {
  19. log.Infof("start with configuration: %+v", conf)
  20. // 1.创建Server类
  21. s := &Server{
  22. conf: conf,
  23. evtbus: make(chan interface{}, 1000),
  24. top: topo.NewTopo(conf.productName, conf.zkAddr, conf.f, conf.provider),
  25. counter: stats.NewCounters("router"),
  26. lastActionSeq: -1,
  27. startAt: time.Now(),
  28. addr: addr,
  29. moper: NewMultiOperator(addr),
  30. reqCh: make(chan *PipelineRequest, 1000),
  31. pools: cachepool.NewCachePool(),
  32. pipeConns: make(map[string]*taskRunner),
  33. bufferedReq: list.New(),
  34. }
  35. ...
  36. // 2.启动Zookeeper监听器
  37. s.RegisterAndWait()
  38. _, err = s.top.WatchChildren(models.GetWatchActionPath(conf.productName), s.evtbus)
  39. if err != nil {
  40. log.Fatal(errors.ErrorStack(err))
  41. }
  42. // 3.初始化所有Slot的信息
  43. s.FillSlots()
  44. // 4.启动对reqCh和evtbus中事件的监听
  45. go s.handleTopoEvent()
  46. return s
  47. }

2.2.1 Zookeeper通信topology.go

NewServer()中调用的RegisterAndWait()和WatchChildren()都是处理Zookeeper的。一部分代码在codis-1.92/pkg/proxy/router/topology/topology.go中,一部分底层实现在codis-1.92/pkg/models包下。这里就不具体分析models包是如何与Zookeeper通信的了,以免偏离了主题。现阶段,我们只需知道Zookeeper中结点关系(Proxy拓扑结构)的变化都会反映到evtbus管道中就行了。

  1. func (s *Server) RegisterAndWait() {
  2. _, err := s.top.CreateProxyInfo(&s.pi)
  3. if err != nil {
  4. log.Fatal(errors.ErrorStack(err))
  5. }
  6. _, err = s.top.CreateProxyFenceNode(&s.pi)
  7. if err != nil {
  8. log.Warning(errors.ErrorStack(err))
  9. }
  10. s.registerSignal()
  11. s.waitOnline()
  12. }
  13. func (top *Topology) WatchChildren(path string, evtbus chan interface{}) ([]string, error) {
  14. content, _, evtch, err := top.zkConn.ChildrenW(path)
  15. if err != nil {
  16. return nil, errors.Trace(err)
  17. }
  18. // 启动监听器,监听Zookeeper事件
  19. go top.doWatch(evtch, evtbus)
  20. return content, nil
  21. }
  22. func (top *Topology) doWatch(evtch <-chan topo.Event, evtbus chan interface{}) {
  23. e := <-evtch
  24. if e.State == topo.StateExpired || e.Type == topo.EventNotWatching {
  25. log.Fatalf("session expired: %+v", e)
  26. }
  27. log.Warningf("topo event %+v", e)
  28. switch e.Type {
  29. //case topo.EventNodeCreated:
  30. //case topo.EventNodeDataChanged:
  31. case topo.EventNodeChildrenChanged: //only care children changed
  32. //todo:get changed node and decode event
  33. default:
  34. log.Warningf("%+v", e)
  35. }
  36. // 将Zookeeper结点变化的事件放入Server的evtbus管道中
  37. evtbus <- e
  38. }

2.2.2 初始化槽信息fillSlots()

Codis将Redis服务器按照Group划分,每个Group就是一个Master以及至少一个Slave。也就是说每个Group都对应哈希散列的一个Slot。fillSlots()从ZooKeeper中取出注册的Redis后端信息,初始化每个Slot(默认1024个):包括Slot状态、Group信息等。

  1. func (s *Server) FillSlots() {
  2. // 为所有默认1024个Slot初始化信息
  3. for i := 0; i < models.DEFAULT_SLOT_NUM; i++ {
  4. s.fillSlot(i, false)
  5. }
  6. }
  7. func (s *Server) fillSlot(i int, force bool) {
  8. s.clearSlot(i)
  9. // 1.获得当前Slot的信息和Group信息
  10. slotInfo, groupInfo, err := s.top.GetSlotByIndex(i)
  11. slot := &Slot{
  12. slotInfo: slotInfo,
  13. dst: group.NewGroup(*groupInfo),
  14. groupInfo: groupInfo,
  15. }
  16. // 2.创建Slot对应的cachepool
  17. s.pools.AddPool(slot.dst.Master())
  18. if slot.slotInfo.State.Status == models.SLOT_STATUS_MIGRATE {
  19. //get migrate src group and fill it
  20. from, err := s.top.GetGroup(slot.slotInfo.State.MigrateStatus.From)
  21. if err != nil { //todo: retry ?
  22. log.Fatal(err)
  23. }
  24. slot.migrateFrom = group.NewGroup(*from)
  25. s.pools.AddPool(slot.migrateFrom.Master())
  26. }
  27. s.slots[i] = slot
  28. s.counter.Add("FillSlot", 1)
  29. }

codis-1.92/pkg/proxy/cachepool/cachepool.go和codis-1.92/pkg/proxy/redispool/redispool.go中负责创建与Redis通信的连接池。

  1. type LivePool struct {
  2. pool redispool.IPool
  3. }
  4. type CachePool struct {
  5. mu sync.RWMutex
  6. pools map[string]*LivePool
  7. }
  8. func (cp *CachePool) AddPool(key string) error {
  9. // 1.锁住cachepool
  10. cp.mu.Lock()
  11. defer cp.mu.Unlock()
  12. // 2.查找当前Slot的连接池
  13. pool, ok := cp.pools[key]
  14. if ok {
  15. return nil
  16. }
  17. // 3.若不存在则新建LivePool
  18. pool = &LivePool{
  19. //pool: redispool.NewConnectionPool("redis conn pool", 50, 120*time.Second),
  20. pool: NewSimpleConnectionPool(),
  21. }
  22. // 4.打开连接
  23. pool.pool.Open(redispool.ConnectionCreator(key))
  24. // 5.保存新建好的连接池
  25. cp.pools[key] = pool
  26. return nil
  27. }

2.3 请求接收router.go(1)

下面继续跟踪主流程,main()方法在调用NewServer()创建出Server实例后,调用了其Run()方法。Run()是标准的服务端代码,首先net.Listen()绑定到端口上监听,然后进入死循环Accept(),每接收到一个连接就启动一个goroutine进行处理。

  1. func (s *Server) Run() {
  2. log.Infof("listening %s on %s", s.conf.proto, s.addr)
  3. listener, err := net.Listen(s.conf.proto, s.addr)
  4. ...
  5. for {
  6. conn, err := listener.Accept()
  7. if err != nil {
  8. log.Warning(errors.ErrorStack(err))
  9. continue
  10. }
  11. go s.handleConn(conn)
  12. }
  13. }

handleConn()接收到客户端的连接,完成三件事儿:

  • 创建session对象:保存当前客户端的Socket连接、读写缓冲区、响应Channel等。
  • 启动响应goroutine:client.WritingLoop()中处理backQ中的响应数据。
  • 建立Redis连接:server.redisTunnel()中打开连接,读取客户端请求并转发给Redis处理。
  1. func (s *Server) handleConn(c net.Conn) {
  2. log.Info("new connection", c.RemoteAddr())
  3. s.counter.Add("connections", 1)
  4. // 1.创建当前客户端的Session实例
  5. client := &session{
  6. Conn: c,
  7. r: bufio.NewReaderSize(c, 32*1024),
  8. w: bufio.NewWriterSize(c, 32*1024),
  9. CreateAt: time.Now(),
  10. backQ: make(chan *PipelineResponse, 1000),
  11. closeSignal: &sync.WaitGroup{},
  12. }
  13. client.closeSignal.Add(1)
  14. // 2.启动监视backQ写回响应的子routine
  15. go client.WritingLoop()
  16. ...
  17. // 3.循环读取该客户端的请求并处理
  18. for {
  19. err = s.redisTunnel(client)
  20. if err != nil {
  21. close(client.backQ)
  22. return
  23. }
  24. client.Ops++
  25. }
  26. }

redisTunnel可以说是Proxy服务端的“代码中枢”了,最核心的代码都是在这里共同协作完成任务的,它调用三个最为关键的函数:

  • getRespOpKeys()解析请求:在helper.go中,委托parser.go解析客户端请求。此处对多参数的请求例如hmset进行特殊处理,因为key可能对应多个后端Redis实例。如果是单参数,则可以Pipeline化发送给后端。
  • mapKey2Slot()哈希映射:在mapper.go中,计算key应该分配到哪台Redis服务器的Slot中。
  • PipelineRequest()创建Pipeline请求:根据前面得到的数据新建PipelineRequest,并发送到当前客户端Session中的Channel中。之后调用pr.wg.Wait(),当前go s.handleConn()创建的goroutine休眠等待响应
  1. func (s *Server) redisTunnel(c *session) error {
  2. resp, op, keys, err := getRespOpKeys(c)
  3. k := keys[0]
  4. ...
  5. if isMulOp(opstr) {
  6. if len(keys) > 1 { //can not send to redis directly
  7. var result []byte
  8. err := s.moper.handleMultiOp(opstr, keys, &result)
  9. if err != nil {
  10. return errors.Trace(err)
  11. }
  12. s.sendBack(c, op, keys, resp, result)
  13. return nil
  14. }
  15. }
  16. i := mapKey2Slot(k)
  17. //pipeline
  18. c.pipelineSeq++
  19. pr := &PipelineRequest{
  20. slotIdx: i,
  21. op: op,
  22. keys: keys,
  23. seq: c.pipelineSeq,
  24. backQ: c.backQ,
  25. req: resp,
  26. wg: &sync.WaitGroup{},
  27. }
  28. pr.wg.Add(1)
  29. s.reqCh <- pr
  30. pr.wg.Wait()
  31. return nil
  32. }

2.3.1 RESP协议解析parser.go

redisTunnel()调用了helper.go中的getRespOpKeys(),后者使用parser.go解析RESP协议请求,从Parse()函数的代码中能清晰地看到对RESP五种通信格式’-‘,’+’,’:’,’$’,’*’。因为要根据请求中的命令和key做路由,以及特殊处理(例如多参数命令),所以Codis不能简单地透传,而是解析协议获得所需的信息

注意parser.Parse()的用法,这里parser是包名不是一个对象实例,而Parse是parser包中的一个public函数。所以乍看之下有点困惑了,这也是Golang支持既像C一样面向过程编程,又有高级语言的面向对象甚至Duck Type的缘故。

Parse()读取网络流,并递归处理整个请求。例如”GET ab”命令:

  1. *2\r\n
  2. $3\r\n
  3. GET\r\n
  4. $2\r\n
  5. ab\r\n

最终Parse()返回时得到:

  1. Resp{
  2. Raw: "*2\r\n",
  3. Multi{
  4. Resp{ Raw: "$3\r\nGET\r\n" },
  5. Resp{ Raw: "$2\r\nab\r\n" }
  6. }
  7. }

如果细致分析的话,readLine()中使用readSlice()读取缓冲区的切片,节约了内存。这种设计上的小细节还是很值得关注和学习的,毕竟“天下大事,必作于细”。

  1. func getRespOpKeys(c *session) (*parser.Resp, []byte, [][]byte, error) {
  2. resp, err := parser.Parse(c.r) // read client request
  3. op, keys, err := resp.GetOpKeys()
  4. ...
  5. return resp, op, keys, nil
  6. }
  7. type Resp struct {
  8. Type int
  9. Raw []byte
  10. Multi []*Resp
  11. }
  12. func Parse(r *bufio.Reader) (*Resp, error) {
  13. line, err := readLine(r)
  14. if err != nil {
  15. return nil, errors.Trace(err)
  16. }
  17. resp := &Resp{}
  18. if line[0] == '$' || line[0] == '*' {
  19. resp.Raw = make([]byte, 0, len(line)+64)
  20. } else {
  21. resp.Raw = make([]byte, 0, len(line))
  22. }
  23. resp.Raw = append(resp.Raw, line...)
  24. switch line[0] {
  25. case '-':
  26. resp.Type = ErrorResp
  27. return resp, nil
  28. case '+':
  29. resp.Type = SimpleString
  30. return resp, nil
  31. case ':':
  32. resp.Type = IntegerResp
  33. return resp, nil
  34. case '$':
  35. resp.Type = BulkResp
  36. ...
  37. case '*':
  38. resp.Type = MultiResp
  39. ...
  40. }

2.3.2 哈希映射mapper.go

mapKey2Slot()处理HashTag,并使用CRC32计算哈希值。

  1. const (
  2. HASHTAG_START = '{'
  3. HASHTAG_END = '}'
  4. )
  5. func mapKey2Slot(key []byte) int {
  6. hashKey := key
  7. //hash tag support
  8. htagStart := bytes.IndexByte(key, HASHTAG_START)
  9. if htagStart >= 0 {
  10. htagEnd := bytes.IndexByte(key[htagStart:], HASHTAG_END)
  11. if htagEnd >= 0 {
  12. hashKey = key[htagStart+1 : htagStart+htagEnd]
  13. }
  14. }
  15. return int(crc32.ChecksumIEEE(hashKey) % models.DEFAULT_SLOT_NUM)
  16. }

2.4 请求分发router.go(2)

NewServer()中执行go s.handleTopoEvent()启动goroutine,对Server数据结构中的reqCh和evtbus两个Channel进行事件监听处理。这里重点看拿到reqCh的事件后是如何dispatch()的。reqCh的事件也就是PipelineRequest,会经dispath()函数放入对应Slot的taskrunner的in管道中。也就是说,reqCh中的请求会被分发到各个Slot自己的Channel中

另外注意:此处会检查PipelineRequest对应Slot的状态,如果正在migrate,则暂时将请求缓冲到Server类的bufferedReq链表中

  1. func (s *Server) handleTopoEvent() {
  2. for {
  3. select {
  4. // 1.处理Server.reqCh中事件
  5. case r := <-s.reqCh:
  6. // 1.1 如果正在migrate,则将请求r暂时缓冲起来
  7. if s.slots[r.slotIdx].slotInfo.State.Status == models.SLOT_STATUS_PRE_MIGRATE {
  8. s.bufferedReq.PushBack(r)
  9. continue
  10. }
  11. // 1.2 处理缓冲中的请求e
  12. for e := s.bufferedReq.Front(); e != nil; {
  13. next := e.Next()
  14. s.dispatch(e.Value.(*PipelineRequest))
  15. s.bufferedReq.Remove(e)
  16. e = next
  17. }
  18. // 1.3 处理当前请求r
  19. s.dispatch(r)
  20. // 2.处理Server.evtbus中请求
  21. case e := <-s.evtbus:
  22. switch e.(type) {
  23. case *killEvent:
  24. s.handleMarkOffline()
  25. e.(*killEvent).done <- nil
  26. default:
  27. evtPath := GetEventPath(e)
  28. ...
  29. s.processAction(e)
  30. }
  31. }
  32. }
  33. }
  34. func (s *Server) dispatch(r *PipelineRequest) {
  35. s.handleMigrateState(r.slotIdx, r.keys[0])
  36. // 1.查找Slot对应的taskrunner
  37. tr, ok := s.pipeConns[s.slots[r.slotIdx].dst.Master()]
  38. // 2.若没有,则新建一个taskrunner
  39. if !ok {
  40. // 2.1 新建tr时出错,则向r.backQ放入一个空响应
  41. if err := s.createTaskRunner(s.slots[r.slotIdx]); err != nil {
  42. r.backQ <- &PipelineResponse{ctx: r, resp: nil, err: err}
  43. return
  44. }
  45. // 2.2 拿到taskrunner
  46. tr = s.pipeConns[s.slots[r.slotIdx].dst.Master()]
  47. }
  48. // 3.将请求r放入in管道
  49. tr.in <- r
  50. }

taskrunner.go的createTaskRunner()调用NewTaskRunner()创建当前Slot对应的taskrunner。每个taskrunner都拥有一对in和out管道。之前的PipelineRequest就是放到in管道中。然后启动了两个goroutine,分别调用writeloop()和readloop()函数监听in和out管道,处理其中的请求。

  1. func (s *Server) createTaskRunner(slot *Slot) error {
  2. dst := slot.dst.Master()
  3. if _, ok := s.pipeConns[dst]; !ok {
  4. tr, err := NewTaskRunner(dst, s.conf.netTimeout)
  5. if err != nil {
  6. return errors.Errorf("create task runner failed, %v, %+v, %+v", err, slot.dst, slot.slotInfo)
  7. } else {
  8. s.pipeConns[dst] = tr
  9. }
  10. }
  11. return nil
  12. }
  13. func NewTaskRunner(addr string, netTimeout int) (*taskRunner, error) {
  14. // 1.创建TaskRunner实例
  15. tr := &taskRunner{
  16. in: make(chan interface{}, 1000),
  17. out: make(chan interface{}, 1000),
  18. redisAddr: addr,
  19. tasks: list.New(),
  20. netTimeout: netTimeout,
  21. }
  22. // 2.创建Redis连接,并绑定到tr
  23. c, err := redisconn.NewConnection(addr, netTimeout)
  24. tr.c = c
  25. // 3.开始监听读写管道in和out
  26. go tr.writeloop()
  27. go tr.readloop()
  28. return tr, nil
  29. }
  30. func (tr *taskRunner) writeloop() {
  31. var err error
  32. tick := time.Tick(2 * time.Second)
  33. for {
  34. ...
  35. select {
  36. // 1.处理in管道中来自客户端的请求
  37. case t := <-tr.in:
  38. tr.processTask(t)
  39. // 2.处理out管道中来自Redis的响应
  40. case resp := <-tr.out:
  41. err = tr.handleResponse(resp)
  42. // 设置select间隔
  43. case <-tick:
  44. if tr.tasks.Len() > 0 && int(time.Since(tr.latest).Seconds()) > tr.netTimeout {
  45. tr.c.Close()
  46. }
  47. }
  48. }
  49. }

2.5 请求发送taskrunner.go

终于到了请求的生命周期的最后一个环节了!writeloop()会不断调用processTask()处理in管道中的请求,通过dowrite()函数发送到Redis服务端。当in管道中没有其他请求时,会强制刷新一下缓冲区。

  1. func (tr *taskRunner) processTask(t interface{}) {
  2. var err error
  3. switch t.(type) {
  4. case *PipelineRequest:
  5. r := t.(*PipelineRequest)
  6. var flush bool
  7. if len(tr.in) == 0 { //force flush
  8. flush = true
  9. }
  10. err = tr.handleTask(r, flush)
  11. case *sync.WaitGroup: //close taskrunner
  12. err = tr.handleTask(nil, true) //flush
  13. ...
  14. }
  15. ...
  16. }
  17. func (tr *taskRunner) handleTask(r *PipelineRequest, flush bool) error {
  18. if r == nil && flush { //just flush
  19. return tr.c.Flush()
  20. }
  21. // 1.将请求保存到链表,接收到响应时再移除
  22. tr.tasks.PushBack(r)
  23. tr.latest = time.Now()
  24. // 2.发送请求到Redis
  25. return errors.Trace(tr.dowrite(r, flush))
  26. }
  27. type Resp struct {
  28. Type int
  29. Raw []byte
  30. Multi []*Resp
  31. }
  32. func (tr *taskRunner) dowrite(r *PipelineRequest, flush bool) error {
  33. // 1.通过Bytes()函数取出Resp中的原始字节Raw
  34. b, err := r.req.Bytes()
  35. ...
  36. // 2.将原始请求发送到Redis服务端
  37. _, err = tr.c.Write(b)
  38. ...
  39. // 3.如果需要,强制刷新缓冲区
  40. if flush {
  41. return errors.Trace(tr.c.Flush())
  42. }
  43. return nil
  44. }

Codis使用Golang的bufio库处理底层的IO流读写操作。在NewConnection()中,用net包创建到Redis的Socket连接,并分别创建大小为512K的读写缓冲流。

  1. //not thread-safe
  2. type Conn struct {
  3. addr string
  4. net.Conn
  5. closed bool
  6. r *bufio.Reader
  7. w *bufio.Writer
  8. netTimeout int //second
  9. }
  10. func NewConnection(addr string, netTimeout int) (*Conn, error) {
  11. // 1.打开到Redis服务端的TCP连接
  12. conn, err := net.DialTimeout("tcp", addr, time.Duration(netTimeout)*time.Second)
  13. ...
  14. // 2.创建Conn实例,及读写缓冲区
  15. return &Conn{
  16. addr: addr,
  17. Conn: conn,
  18. r: bufio.NewReaderSize(conn, 512*1024),
  19. w: bufio.NewWriterSize(deadline.NewDeadlineWriter(conn, time.Duration(netTimeout)*time.Second), 512*1024),
  20. netTimeout: netTimeout,
  21. }, nil
  22. }
  23. func (c *Conn) Flush() error {
  24. return c.w.Flush()
  25. }
  26. func (c *Conn) Write(p []byte) (int, error) {
  27. return c.w.Write(p)
  28. }

2.6 返回响应session.go

当writeloop()在“如火如荼”地向Redis发送请求时,readloop()也没有闲着。它不断地从Redis读取响应。发送每个请求时都不会等待Redis的响应,也就是说发送请求和读取响应完全是异步进行的,所以就充分利用了Pipeline的性能优势

  1. func (tr *taskRunner) readloop() {
  2. for {
  3. // 1.从Redis连接中读取响应
  4. resp, err := parser.Parse(tr.c.BufioReader())
  5. if err != nil {
  6. tr.out <- err
  7. return
  8. }
  9. // 2.将解析好的响应放入out管道中
  10. tr.out <- resp
  11. }
  12. }
  13. func (tr *taskRunner) handleResponse(e interface{}) error {
  14. switch e.(type) {
  15. ...
  16. case *parser.Resp:
  17. // 1.取到out管道中的PipelineResponse
  18. resp := e.(*parser.Resp)
  19. // 2.取出对应的PipelineRequest
  20. e := tr.tasks.Front()
  21. req := e.Value.(*PipelineRequest)
  22. // 3.将响应放入到backQ管道中(req.backQ也就是session中的backQ)
  23. req.backQ <- &PipelineResponse{ctx: req, resp: resp, err: nil}
  24. // 4.从任务列表中移除已拿到响应的请求
  25. tr.tasks.Remove(e)
  26. return nil
  27. }
  28. return nil
  29. }

因为writeloop()不仅监视in管道,也监视out管道。所以writeloop()会将readloop()放入的响应交给handleResponse()处理。最终PipelineResponse被放入Session对象的backQ管道中。还记得它吗?在最开始NewServer时为当前客户端创建的Session实例。最后,接收到的PipleResponse会转成RESP协议的字节序列,发送回客户端。

  1. func (s *session) WritingLoop() {
  2. s.lastUnsentResponseSeq = 1
  3. for {
  4. select {
  5. case resp, ok := <-s.backQ:
  6. if !ok {
  7. s.Close()
  8. s.closeSignal.Done()
  9. return
  10. }
  11. flush, err := s.handleResponse(resp)
  12. ...
  13. }
  14. }
  15. }
  16. func (s *session) handleResponse(resp *PipelineResponse) (flush bool, err error) {
  17. ...
  18. if !s.closed {
  19. if err := s.writeResp(resp); err != nil {
  20. return false, errors.Trace(err)
  21. }
  22. flush = true
  23. }
  24. return
  25. }
  26. func (s *session) writeResp(resp *PipelineResponse) error {
  27. // 1.取出Resp中的原始字节
  28. buf, err := resp.resp.Bytes()
  29. if err != nil {
  30. return errors.Trace(err)
  31. }
  32. // 2.写回到客户端
  33. _, err = s.Write(buf)
  34. return errors.Trace(err)
  35. }
  36. //write without bufio
  37. func (s *session) Write(p []byte) (int, error) {
  38. return s.w.Write(p)
  39. }

2.7 Proxy源码流程总结

最后以一张Proxy的流程图作结束。经过我们的分析能够看出,关于并发安全方面,Codis唯一需要并发控制的地方就是从reqCh分发到各个Slot的Channel,为了避免竞争,这一部分是由一个goroutine完成的。

豌豆夹Redis解决方案Codis源码剖析:Proxy代理的更多相关文章

  1. 豌豆夹Redis解决方案Codis源码剖析:Dashboard

    豌豆夹Redis解决方案Codis源码剖析:Dashboard 1.不只是Dashboard 虽然名字叫Dashboard,但它在Codis中的作用却不可小觑.它不仅仅是Dashboard管理页面,更 ...

  2. 豌豆夹Redis解决方案Codis安装使用

    豌豆夹Redis解决方案Codis安装使用 1.安装 1.1 Golang环境 Golang的安装非常简单,因为官网被墙,可以从国内镜像如studygolang.com下载. [root@vm roo ...

  3. 豌豆夹Redis解决方式Codis源代码剖析:Proxy代理

    豌豆夹Redis解决方式Codis源代码剖析:Proxy代理 1.预备知识 1.1 Codis Codis就不详细说了,摘抄一下GitHub上的一些项目描写叙述: Codis is a proxy b ...

  4. Redis源码剖析

    Redis源码剖析和注释(一)---链表结构 Redis源码剖析和注释(二)--- 简单动态字符串 Redis源码剖析和注释(三)--- Redis 字典结构 Redis源码剖析和注释(四)--- 跳 ...

  5. 玩转Android之Picasso使用详详详详详详解,从入门到源码剖析!!!!

    Picasso是Squareup公司出的一款图片加载框架,能够解决我们在Android开发中加载图片时遇到的诸多问题,比如OOM,图片错位等,问题主要集中在加载图片列表时,因为单张图片加载谁都会写.如 ...

  6. 老李推荐:第5章5节《MonkeyRunner源码剖析》Monkey原理分析-启动运行: 获取系统服务引用

    老李推荐:第5章5节<MonkeyRunner源码剖析>Monkey原理分析-启动运行: 获取系统服务引用   上一节我们描述了monkey的命令处理入口函数run是如何调用optionP ...

  7. 老李推荐:第4章3节《MonkeyRunner源码剖析》ADB协议及服务: ADB协议概览

    老李推荐:第4章3节<MonkeyRunner源码剖析>ADB协议及服务: ADB协议概览   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试, ...

  8. t-io 集群解决方案以及源码解析

    t-io 集群解决方案以及源码解析 0x01 概要说明 本博客是基于老谭t-io showcase中的tio-websocket-showcase 示例来实现集群.看showcase 入门还是挺容易的 ...

  9. Jedis cluster集群初始化源码剖析

    Jedis cluster集群初始化源码剖析 环境 jar版本: spring-data-redis-1.8.4-RELEASE.jar.jedis-2.9.0.jar 测试环境: Redis 3.2 ...

随机推荐

  1. zookeeper初探

    安装三台linux虚拟机,安装好java环境,并配置好网络以及host文件,分别改好hostname为node0.node1.node2 上传下载好的zookeeper文件到node0的/usr/lo ...

  2. FTP下载文件

    linux命令方式下载 step1: >>ftp ip port 根据提示输入用户名 根据提示输入用户密码 >>cd 目录(重要:一定要进入文件所在的目录) >>g ...

  3. myeclipse自动添加注释

    开发需要,新建类的时候,需要加自己的名字,每次都要自己写,嫌麻烦,修改一下myeclipse配置文件即可 打开window---preferences 选中 new Java files 点击edit ...

  4. svg param.js的大bug

    在svg文件里定义控件,带参数,然后引用. 如果是 text 且没有为其它添加默认值,那么会报错. 即, <svg width="200" height="200& ...

  5. HTML笔记05------AJAX

    AJAX初探01 AJAX概念 概念:即"Asynchronous JavaScript And XML" 通过在后台与服务器进行少量数据交换,AJAX可以使网页实现异步更新.这意 ...

  6. [LeetCode] Number of Distinct Islands 不同岛屿的个数

    Given a non-empty 2D array grid of 0's and 1's, an island is a group of 1's (representing land) conn ...

  7. PyCharm 2018 永久激活

    PyCharm是一种Python IDE,带有一整套可以帮助用户在使用Python语言开发时提高其效率的工具,比如调试.语法高亮.Project管理.代码跳转.智能提示.自动完成.单元测试.版本控制. ...

  8. Ubantu16.04系统优化

    系统清理篇 系统更新 安装完系统之后,需要更新一些补丁.Ctrl+Alt+T调出终端,执行一下代码: sudo apt-get update sudo apt-get upgrade 卸载libreO ...

  9. hdu 4812 DTree (点分治)

    D Tree Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 102400/102400 K (Java/Others)Total S ...

  10. hdu 5591 BestCoder Round #65(博弈)

    题意: 问题描述 ZYBZYB在远足中,和同学们玩了一个“数字炸弹”游戏:由主持人心里想一个在[1,N][1,N]中的数字XX,然后玩家们轮流猜一个数字,如果一个玩家恰好猜中XX则算负,否则主持人将告 ...