【Codis源码】codis-proxy处理流程上(二)

15 篇文章 2 订阅

前言:

客户端连接的 Redis 代理服务, 实现了 Redis 协议。代理部分极为关键,它负责转发请求,以及汇总数据结果。
codis-proxy的处理流程比较长,本章节会先叙述一部分,比如说loopReader。剩下的下一个章节继续叙述。

golang版本: go1.13.5 darwin/amd64
codis版本:codis 3.2

参考资料:
RESP协议:
https://redis.io/topics/protocol

(一)完整处理流程

在这里插入图片描述

(二)源码解析

2.1 main方法主体

proxy入口方法,位于文件cmd/proxy/main.go

func main() {
   //...省略
   
   d, err := docopt.Parse(usage, nil, true, "", false) //解析命令行参数
   if err != nil {
      log.PanicError(err, "parse arguments failed")
   }
   
   //...省略

   var ncpu int
   if n, ok := utils.ArgumentInteger(d, "--ncpu"); ok {
      ncpu = n
   } else {
      ncpu = 4
   }
   runtime.GOMAXPROCS(ncpu) //设置执行使用的核数
   
   //...省略

   config := proxy.NewDefaultConfig() //创建配置信息
   if s, ok := utils.Argument(d, "--config"); ok {
      if err := config.LoadFromFile(s); err != nil { //如果设置了配置信息,从文件加载
         log.PanicErrorf(err, "load config %s failed", s)
      }
   }
   //...省略
   s, err := proxy.New(config) // 注册proxy服务和admin接口
   if err != nil {
      log.PanicErrorf(err, "create proxy with config file failed\n%s", config)
   }
   defer s.Close()
   //...省略
}   

cmd/proxy/main.go的main方法中通过docopt.Parse解析参数、然后通过runtime.GOMAXPROCS设置执行的使用的核数。这个配置比较关键,goroutine设置多核的话可以并行执行。并行比较适合cpu计算密集型。如果IO密集型使用多核反而会增加cpu切换的成本。proxy.NewDefaultConfig创建配置。如果命令中设置–config,则会根据–config的文件调用config.LoadFromFile函数加载配置文件。proxy.New函数用于注册proxy服务和admin接口。

2.2 New方法

New 方法,位于文件pkg/proxy/proxy.go

func New(config *Config) (*Proxy, error) {
   if err := config.Validate(); err != nil { .   //校验配置
      return nil, errors.Trace(err)
   }
   if err := models.ValidateProduct(config.ProductName); err != nil { 
      return nil, errors.Trace(err)
   }

   s := &Proxy{}
   s.config = config
   s.exit.C = make(chan struct{})
   s.router = NewRouter(config)  //创建路由连接池子
   s.ignore = make([]byte, config.ProxyHeapPlaceholder.Int64())

   s.model = &models.Proxy{
      StartTime: time.Now().String(),
   }
   s.model.ProductName = config.ProductName      //名称
   s.model.DataCenter = config.ProxyDataCenter   //代理中心
   s.model.Pid = os.Getpid()                     //获得当前进程pid
   s.model.Pwd, _ = os.Getwd()                   //获得当前目录
   if b, err := exec.Command("uname", "-a").Output(); err != nil { //获得系统
      log.WarnErrorf(err, "run command uname failed")
   } else {
      s.model.Sys = strings.TrimSpace(string(b))
   }
   s.model.Hostname = utils.Hostname  //计算机名称

   if err := s.setup(config); err != nil {
      s.Close()
      return nil, err
   }

   log.Warnf("[%p] create new proxy:\n%s", s, s.model.Encode())

   unsafe2.SetMaxOffheapBytes(config.ProxyMaxOffheapBytes.Int64())

   go s.serveAdmin()   //创建后台接口服务
   go s.serveProxy()   //创建proxy服务

   s.startMetricsJson()
   s.startMetricsInfluxdb()
   s.startMetricsStatsd()

   return s, nil
}

通过NewRouter创建连接池已经slots。然后通过go s.serveAdmin调用goroutine创建后台接口服务监听端口默认11080。通过go s.serveProxy调用goroutine创建proxy服务监听默认端口19000。proxy服务是提供给redis-cli或者jodis做连接使用。

2.3 Proxy结构体解析

Proxy结构体,位于文件pkg/proxy/proxy.go

type Proxy struct {
   mu sync.Mutex             //互斥锁

   xauth string              //账号授权信息
   model *models.Proxy       //proxy的基本设置

   exit struct {             //用于关闭
      C chan struct{}
   }
   online bool                //是否在线
   closed bool                //是否关闭

   config *Config             //配置参数
   router *Router             //Router中比较重要的是连接池和slots
   ignore []byte              //设置堆占位符以减少GC频率。

   lproxy net.Listener        //proxy的监听,默认端口19000
   ladmin net.Listener        //admin的监听,默认端口11080

   ha struct {                //高可用相关、哨兵监听 
      monitor *redis.Sentinel 
      masters map[int]string  
      servers []string
   }
   jodis *Jodis               //jodis连接
}

Proxy结构体是一个比较关键的结构体,结构体内包含了proxy关键的一些信息。比如说配置参数,proxy和admin监听等等。

2.3 serveProxy方法解析

proxy服务方法,位于文件pkg/proxy/proxy.go

func (s *Proxy) serveProxy() {
   if s.IsClosed() {
      return
   }
   defer s.Close()

   log.Warnf("[%p] proxy start service on %s", s, s.lproxy.Addr())

   eh := make(chan error, 1)
   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)

   if d := s.config.BackendPingPeriod.Duration(); d != 0 {
      go s.keepAlive(d) //Router可用检测
   }

   select {
   case <-s.exit.C:
      log.Warnf("[%p] proxy shutdown", s)
   case err := <-eh:
      log.ErrorErrorf(err, "[%p] proxy exit on error", s)
   }
}

serveProxy中开启了两个goroutine,一个是用于router可用检测。另一个是接收accpet连接后,启动session处理。

2.3 session解析

创建session函数,位于文件pkg/proxy/session.go

func NewSession(sock net.Conn, config *Config) *Session {
   c := redis.NewConn(sock,                //创建连接
      config.SessionRecvBufsize.AsInt(),
      config.SessionSendBufsize.AsInt(),
   )
   c.ReaderTimeout = config.SessionRecvTimeout.Duration()         //回复缓冲区超时时间。默认30m
   c.WriterTimeout = config.SessionSendTimeout.Duration()         //发送缓冲区超时时间,默认30s
   c.SetKeepAlivePeriod(config.SessionKeepAlivePeriod.Duration()) //连接超时,默认75s

   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
}

创建redis的session连接操作,并设置一些超时时间。

Start函数、位于文件pkg/proxy/session.go

func (s *Session) Start(d *Router) {
   s.start.Do(func() {
      if int(incrSessions()) > s.config.ProxyMaxClients {  //默认最大session数1000
         go func() {
            s.Conn.Encode(redis.NewErrorf("ERR max number of clients reached"), true)
            s.CloseWithError(ErrTooManySessions)
            s.incrOpFails(nil, nil)
            s.flushOpStats(true)
         }()
         decrSessions()
         return
      }

      if !d.isOnline() {  //判断是否在线
         go func() {  //不在线情况处理
            s.Conn.Encode(redis.NewErrorf("ERR router is not online"), true)
            s.CloseWithError(ErrRouterNotOnline)
            s.incrOpFails(nil, nil)
            s.flushOpStats(true)
         }()
         decrSessions()
         return
      }
      //tasks是一个指向RequestChan的指针,RequestChan结构体中有一个data字段,
      //data字段是个数组,保存1024个指向Request的指针
      tasks := NewRequestChanBuffer(1024)

      go func() {
         s.loopWriter(tasks)  //合并请求结果
         decrSessions()
      }()

      go func() {
         
         s.loopReader(tasks, d) //读取分发请求
         tasks.Close()
      }()
   })
}

NewRequestChanBuffer函数保存1024个指向Request的指针、tasks是只想一个RequestChan的指针。s.loopWriter是合并请求结果,针对mset/mget参数会有合并请求操作。s.loopReader读取分发请求,首先根据key计算该key分配到哪个slot.在此步骤中只会将slot对应的连接取出,然后将请求放到连接的input字段中。

2.4 loopReader函数解析

读取分发请求,位于文件pkg/proxy/session.go

func (s *Session) loopReader(tasks *RequestChan, d *Router) (err error) {
  
    //。。。省略
   for !s.quit {
      //。。。省略
      
      r := &Request{} //创建一个Request结构体,该结构体会贯穿请求的始终,请求字段,响应字段都放在Request中
      r.Multi = multi
      r.Batch = &sync.WaitGroup{}
      r.Database = s.database         //选择数据库,选择范围0 ~ 15。
      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)  //如果handleRequest执行成功,将请求r放入tasks
         if breakOnFailure {
            return err
         }
      } else {
         tasks.PushBack(r)
      }
   }
   return nil
}

handleRequest函数中的r放入tasks,即上文的RequestChan的data字段中。loopWriter会从该字段中获取请求并且返回给客户端。

handleRequest函数,位于文件pkg/proxy/session.go

func (s *Session) handleRequest(r *Request, d *Router) error {
  //..。省略

   switch opstr {
    //。。。省略
   default:
      return d.dispatch(r)    //分发slots函数
   }
}

d.dispatch最为关键,其他的命令处理后都会调用dispatch函数。

在这里插入图片描述
图中为连接proxy服务端,接收一个set命令的dlv调试。

dispatch函数,位于文件pkg/proxy/router.go

func (s *Router) dispatch(r *Request) error {
   hkey := getHashKey(r.Multi, r.OpStr) //r.OpStr为命令,hKey为key
   var id = Hash(hkey) % MaxSlotNum     //hash计算key取模1024,计算放在哪个slots中
   slot := &s.slots[id]                 //slot都放在Router的slots中
   return slot.forward(r, hkey)         //执行slot.forward
}

该函数内根据key值hash后,取模1024.计算放在哪个slots中,而slots其实是存在Router结构体中。

dispatch调用forward最终是调用forward中Forward,位于文件pkg/proxy/forward.go

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)      //使session中的loopWriter会在Batch处等待,处理后返回。
   return nil
}

PushBack方法,位于文件pkg/proxy/backend.go

func (bc *BackendConn) PushBack(r *Request) {
   if r.Batch != nil {
      r.Batch.Add(1)  //将请求的Batch执行add 1
   }
   bc.input <- r //将请求放入BackendConn的input channel
}

将请求的Batch执行add 1,session中的loopWriter会在Batch处等待。很明显,Proxy并没有真正处理请求,肯定会有goroutine从bc.input中读取请求并且处理完成后调用r.Batch.Done()处减1,这样当请求执行完成后,session中的loopWriter就可以返回给客户端端响应了。

总结:

  1. codis-proxy会创建两个服务一个admin的api默认端口11080,另一个是proxy的服务默认端口19000。
  2. 请求中的key,会根据hash(hkey) % 1024取模,计算存放在哪个slot中。而slot存放在Router结构体中。
  3. 默认session最大数1000。
  4. runtime.GOMAXPROCS设置使用核数。
  5. r.Batch.Add(1)会发起loopWriter中r.Batch等待,得到执行完成,等待r.Batch.Done()后返回。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值