前言:
客户端连接的 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就可以返回给客户端端响应了。
总结:
- codis-proxy会创建两个服务一个admin的api默认端口11080,另一个是proxy的服务默认端口19000。
- 请求中的key,会根据hash(hkey) % 1024取模,计算存放在哪个slot中。而slot存放在Router结构体中。
- 默认session最大数1000。
- runtime.GOMAXPROCS设置使用核数。
- r.Batch.Add(1)会发起loopWriter中r.Batch等待,得到执行完成,等待r.Batch.Done()后返回。