Codis源码解析——proxy添加到集群
来源:互联网 发布:拇指特效软件 编辑:程序博客网 时间:2024/05/22 08:16
前面我们说过,proxy启动之后,会默认处于 waiting 状态,以一秒一次的频率刷新状态,监听proxy_addr 地址(默认配置文件中的19000端口),但是不会 accept 连接,通过fe或者命令行添加到集群并完成集群状态的同步,才能改变状态为online。那么,将proxy添加到集群的过程中发生了什么?这一篇我们就来看看。
通过界面添加比较简单,直接输入proxy的地址即可
主要调用的方法在/pkg/topom/topom_proxy.go中
//传入的addr就是proxy的地址func (s *Topom) CreateProxy(addr string) error { s.mu.Lock() defer s.mu.Unlock() ctx, err := s.newContext() if err != nil { return err } //这里的p就是根据proxy地址取出models.proxy,也就是/codis3/codis-wujiang/proxy路径下面的那个proxy-token中的详细信息 p, err := proxy.NewApiClient(addr).Model() if err != nil { return errors.Errorf("proxy@%s fetch model failed, %s", addr, err) } //这个ApiClient中存储了proxy的地址,以及根据productName,productAuth(默认为空)以及token生成的auth c := s.newProxyClient(p) //之前我们说过,proxy启动的时候,在s.setup(config)这一步,会生成一个xauth,存储在Proxy的xauth属性中,这一步就是讲上面得到的xauth和启动proxy时的xauth作比较,来唯一确定需要的xauth if err := c.XPing(); err != nil { return errors.Errorf("proxy@%s check xauth failed, %s", addr, err) } //检查上下文中的proxy是否已经有token,如果有的话,说明这个proxy已经添加到集群了 if ctx.proxy[p.Token] != nil { return errors.Errorf("proxy-[%s] already exists", p.Token) } else { //上下文中所有proxy的最大id+1,赋给当前的proxy作为其id p.Id = ctx.maxProxyId() + 1 } defer s.dirtyProxyCache(p.Token) //到这一步,proxy已经添加成功,更新"/codis3/codis-wujiang/proxy/proxy-token"下面的proxy信息 if err := s.storeCreateProxy(p); err != nil { return err } else { return s.reinitProxy(ctx, p, c) }}
下面我们着重看一下reinitProxy方法,这个方法里面主要是包含三个模块,都是ApiClient的方法。前面我们已经说过,ApiClient中存储了proxy的地址和auth
func (s *Topom) reinitProxy(ctx *context, p *models.Proxy, c *proxy.ApiClient) error { log.Warnf("proxy-[%s] reinit:\n%s", p.Token, p.Encode()) //初始化1024个槽 if err := c.FillSlots(ctx.toSlotSlice(ctx.slots, p)...); err != nil { log.ErrorErrorf(err, "proxy-[%s] fillslots failed", p.Token) return errors.Errorf("proxy-[%s] fillslots failed", p.Token) } if err := c.Start(); err != nil { log.ErrorErrorf(err, "proxy-[%s] start failed", p.Token) return errors.Errorf("proxy-[%s] start failed", p.Token) } //由于此时sentinels还没有,传入的server队列为空,所以这个方法我们暂时可以不管 if err := c.SetSentinels(ctx.sentinel); err != nil { log.ErrorErrorf(err, "proxy-[%s] set sentinels failed", p.Token) return errors.Errorf("proxy-[%s] set sentinels failed", p.Token) } return nil}
ctx.toSlotSlice主要就是根据models.SlotMapping来创建1024个models.Slot
func (ctx *context) toSlot(m *models.SlotMapping, p *models.Proxy) *models.Slot { slot := &models.Slot{ Id: m.Id, Locked: ctx.isSlotLocked(m), ForwardMethod: ctx.method, } switch m.Action.State { case models.ActionNothing, models.ActionPending: slot.BackendAddr = ctx.getGroupMaster(m.GroupId) slot.BackendAddrGroupId = m.GroupId slot.ReplicaGroups = ctx.toReplicaGroups(m.GroupId, p) case models.ActionPreparing: slot.BackendAddr = ctx.getGroupMaster(m.GroupId) slot.BackendAddrGroupId = m.GroupId case models.ActionPrepared: fallthrough case models.ActionMigrating: slot.BackendAddr = ctx.getGroupMaster(m.Action.TargetId) slot.BackendAddrGroupId = m.Action.TargetId slot.MigrateFrom = ctx.getGroupMaster(m.GroupId) slot.MigrateFromGroupId = m.GroupId case models.ActionFinished: slot.BackendAddr = ctx.getGroupMaster(m.Action.TargetId) slot.BackendAddrGroupId = m.Action.TargetId default: log.Panicf("slot-[%d] action state is invalid:\n%s", m.Id, m.Encode()) } return slot}
Topom.FillSlots阶段,根据之前的1024个models.Slot,创建1024个/pkg/proxy/slots.go中的Slot,并且创建了SharedBackendConn。之前在Codis源码解析——proxy监听redis请求中我们提到过,redis请求是由sharedBackendConn中取出的一个BackendConn进行处理的。而sharedBackendConn就是在填充槽的过程中创建的,如下列代码所示。这个方法由Proxy.Router调用,第一个参数是1024个models.slot中的每一个,第二个参数是写死的false,第三个是FowardSemiAsync(这个是由配置文件dashboard.toml中的migration_method指定的)。
之前我们说过,Proxy.Router中存储了集群中所有sharedBackendConnPool和slot,用于将redis请求转发给相应的slot进行处理,而Router里面的sharedBackendConnPool和slot就是在这里进行填充的
func (s *Router) fillSlot(m *models.Slot, switched bool, method forwardMethod) { slot := &s.slots[m.Id] slot.blockAndWait() //清空models.Slot里面的backendConn slot.backend.bc.Release() slot.backend.bc = nil slot.backend.id = 0 slot.migrate.bc.Release() slot.migrate.bc = nil slot.migrate.id = 0 for i := range slot.replicaGroups { for _, bc := range slot.replicaGroups[i] { bc.Release() } } slot.replicaGroups = nil //false slot.switched = switched //初始阶段addr和from都是空字符串 if addr := m.BackendAddr; len(addr) != 0 { //从Router的primary sharedBackendConnPool中取出addr对应的sharedBackendConn,如果没有就新建并放入,也相当于初始化了 slot.backend.bc = s.pool.primary.Retain(addr) slot.backend.id = m.BackendAddrGroupId } if from := m.MigrateFrom; len(from) != 0 { slot.migrate.bc = s.pool.primary.Retain(from) slot.migrate.id = m.MigrateFromGroupId } if !s.config.BackendPrimaryOnly { for i := range m.ReplicaGroups { var group []*sharedBackendConn for _, addr := range m.ReplicaGroups[i] { group = append(group, s.pool.replica.Retain(addr)) } if len(group) == 0 { continue } slot.replicaGroups = append(slot.replicaGroups, group) } } if method != nil { slot.method = method } if !m.Locked { slot.unblock() } if !s.closed { if slot.migrate.bc != nil { if switched { log.Warnf("fill slot %04d, backend.addr = %s, migrate.from = %s, locked = %t, +switched", slot.id, slot.backend.bc.Addr(), slot.migrate.bc.Addr(), slot.lock.hold) } else { log.Warnf("fill slot %04d, backend.addr = %s, migrate.from = %s, locked = %t", slot.id, slot.backend.bc.Addr(), slot.migrate.bc.Addr(), slot.lock.hold) } } else { if switched { log.Warnf("fill slot %04d, backend.addr = %s, locked = %t, +switched", slot.id, slot.backend.bc.Addr(), slot.lock.hold) } else { log.Warnf("fill slot %04d, backend.addr = %s, locked = %t", slot.id, slot.backend.bc.Addr(), slot.lock.hold) } } }}
这个阶段主要是初始化槽。以id为0的槽为例,初始化之后,其结构如下图所示,每个slot都被分配了相应的backendConn,只不过此时每个backendConn都为空
接下来主要介绍上面的Topom.start方法,主要就是将Proxy自身,和它的router和jodis设为上线,在zk中创建临时节点/jodis/codis-productName/proxy-token,并监听该节点的变化
func (s *Proxy) Start() error { s.mu.Lock() defer s.mu.Unlock() if s.closed { return ErrClosedProxy } if s.online { return nil } s.online = true //router的online属性设为true s.router.Start() if s.jodis != nil { s.jodis.Start() } return nil}func (j *Jodis) Start() { j.mu.Lock() defer j.mu.Unlock() if j.online { return } //也是这个套路,先把online属性设为true j.online = true go func() { var delay = &DelayExp2{ Min: 1, Max: 30, Unit: time.Second, } for !j.IsClosed() { //这一步在zk中创建临时节点/jodis/codis-wujiang/proxy-token,并添加监听事件,监听该节点中内容的改变。最终返回的w是一个chan struct{}。具体实现方法在下面的watch中 w, err := j.Rewatch() if err != nil { log.WarnErrorf(err, "jodis watch node %s failed", j.path) delay.SleepWithCancel(j.IsClosed) } else { //从w中读出zk下的变化 <-w delay.Reset() } } }()}func (c *Client) watch(conn *zk.Conn, path string) (<-chan struct{}, error) { //GetW的核心方法就是下面的addWatcher,w就是addWatcher返回的ch _, _, w, err := conn.GetW(path) if err != nil { return nil, errors.Trace(err) } signal := make(chan struct{}) go func() { defer close(signal) <-w log.Debugf("zkclient watch node %s update", path) }() return signal, nil}//这个类在/github.com/samuel/go-zookeeper/zk/conn.go中,返回一个channel。当关注的路径下发生变更时,这个channel中就会读出值func (c *Conn) addWatcher(path string, watchType watchType) <-chan Event { c.watchersLock.Lock() defer c.watchersLock.Unlock() ch := make(chan Event, 1) wpt := watchPathType{path, watchType} c.watchers[wpt] = append(c.watchers[wpt], ch) return ch}
上面的Proxy中的jodis如下图所示
到这里,proxy已经成功添加到集群中。可以从三个方面看出来:
1 界面中显示成功
这里多说一句,界面上显示的total session,是历史记录总数,alive session才有意义。每次新建一个session,这两个数据都会加一,但是如果alive session加一之后超过了proxy.toml中设置的proxy_max_clients(默认为1000),就不会建立连接,并会把alive session减一。alive session减一的另外两种情况是,成功返回请求结果后,以及session过期后(默认为75秒)
2 zk中增加了/jodis/codis-wujiang/proxy-token路径
3 proxy的日志从waiting变为working,dashboard的日志打印创建了proxy-token
别忘了,在dashboard启动的时候,启动了goroutine来刷新proxy的状态,下面我们就来看看,当集群中新增了proxy之后,是如何刷新的
//上下文中存储了当前集群的slots、group、proxy、sentinels等信息type context struct { slots []*models.SlotMapping group map[int]*models.Group proxy map[string]*models.Proxy sentinel *models.Sentinel hosts struct { sync.Mutex m map[string]net.IP } method int}func (s *Topom) RefreshProxyStats(timeout time.Duration) (*sync2.Future, error) { s.mu.Lock() defer s.mu.Unlock() //在这个构造函数中将判断cache中的数据是否为空,为空的话就通过store从zk中取出填进cache ctx, err := s.newContext() if err != nil { return nil, err } var fut sync2.Future //由于我们刚才添加了proxy,这里ctx.proxy已经不为空了 for _, p := range ctx.proxy { //fut中的waitsGroup加1 fut.Add() go func(p *models.Proxy) { stats := s.newProxyStats(p, timeout) stats.UnixTime = time.Now().Unix() //在fut的vmap属性中,添加以proxy.Token为键,ProxyStats为值的map,并将waitsGroup减1 fut.Done(p.Token, stats) switch x := stats.Stats; { case x == nil: case x.Closed || x.Online: //如果一个proxy因为某种情况出现error,被运维重启之后,处于waiting状态,会调用OnlineProxy方法将proxy重新添加到集群中 default: if err := s.OnlineProxy(p.AdminAddr); err != nil { log.WarnErrorf(err, "auto online proxy-[%s] failed", p.Token) } } }(p) } ////当所有proxy.Token和ProxyStats的关系map建立好之后,存到Topom.stats.proxies中 go func() { stats := make(map[string]*ProxyStats) for k, v := range fut.Wait() { stats[k] = v.(*ProxyStats) } s.mu.Lock() defer s.mu.Unlock() //Topom的stats结构中的proxies属性,存储了完整的stats信息,回想我们之前介绍的,Topom存储着集群中的所有配置和节点信息 s.stats.proxies = stats }() return &fut, nil}
以上步骤执行完之后,我们来看看返回值fut
手动kill一个proxy的进程,发现集群上显示该proxy为红色error,但是其实这个proxy并没有被踢出集群ctx。可能有人会问,如果一个proxy挂了,codis是怎么知道不要把请求发到这个proxy的呢?其实,当下面几种情况,都会调用proxy.close方法,这个里面调用了Jodis的close方法,将zk上面的该proxy信息删除。jodis客户端是通过zk转发的,自然就不会把请求发到这个proxy上了
go func() { defer s.Close() c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM) sig := <-c log.Warnf("[%p] proxy receive signal = '%v'", s, sig)}()func (s *Proxy) serveAdmin() { if s.IsClosed() { return } defer s.Close() log.Warnf("[%p] admin start service on %s", s, s.ladmin.Addr()) eh := make(chan error, 1) go func(l net.Listener) { h := http.NewServeMux() h.Handle("/", newApiServer(s)) hs := &http.Server{Handler: h} eh <- hs.Serve(l) }(s.ladmin) select { case <-s.exit.C: log.Warnf("[%p] admin shutdown", s) case err := <-eh: log.ErrorErrorf(err, "[%p] admin exit on error", s) }}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) if err != nil { return err } NewSession(c, s.config).Start(s.router) } }(s.lproxy) if d := s.config.BackendPingPeriod.Duration(); d != 0 { go s.keepAlive(d) } select { case <-s.exit.C: log.Warnf("[%p] proxy shutdown", s) case err := <-eh: log.ErrorErrorf(err, "[%p] proxy exit on error", s) }}
总结一下,proxy启动之后,一直处于waiting的状态,直到将proxy添加到集群。首先对要添加的proxy做参数校验,根据proxy addr获取一个ApiClient,更新zk中有关于这个proxy的信息。接下来,从上下文中获取ctx.slots,也就是[]*models.SlotMapping,创建1024个models.Slot,再填充1024个pkg/proxy/slots.go中的Slot,此过程中Router为每个Slot都分配了对应的backendConn。
下一步,将Proxy自身,和它的router和jodis设为上线,在zk中创建临时节点/jodis/codis-productName/proxy-token,并监听该节点的变化,如果有变化从相应的channel中读出值。启动dashboard的时候,有一个goroutine专门负责刷新proxy状态,将每一个proxy.Token和ProxyStats的关系map对应起来,存入Topom.stats.proxies中。
说明:
如有转载,请注明出处
http://blog.csdn.net/antony9118/article/details/76339817
- Codis源码解析——proxy添加到集群
- Codis源码解析——codis-server添加到集群
- Codis源码解析——proxy的启动
- Codis源码解析——proxy监听redis请求
- Codis源码解析——sharedBackendConn
- Codis源码解析——Jodis
- Codis源码解析——dashboard的启动(1)
- Codis源码解析——dashboard的启动(2)
- Codis源码解析——fe的启动
- Codis源码解析——slot的分配
- Codis源码解析——处理slot操作(1)
- Codis源码解析——处理slot操作(2)
- CODIS源码剖析二(codis-proxy功能实现)
- Codis源码解析——sentinel的重同步(1)
- Codis源码解析——sentinel的重同步(2)
- 豌豆夹Redis解决方案Codis源码剖析:Proxy代理
- codis proxy 配制
- Redis 集群解决方案 Codis
- 学前段半个月的一些心得(三)
- 题目1048:判断三角形类型
- 如何确认同一网段
- CodeChef ISCC2017 HISTOSIM
- hdu4607—Park Visit(树的直径)
- Codis源码解析——proxy添加到集群
- 深入浅出Tensorflow(五):循环神经网络简介
- NKOJ 4040 (CQOI 2017) 小Q的表格(莫比乌斯反演+分块+递推+线性筛/欧拉函数+分块+线性筛)
- “玲珑杯”ACM比赛 Round #19 E.Expected value of the expression【Dp】
- 老生常谈之SQL Server (行转列,列转行)
- 环境变量的配置
- golang 实现一种环形队列,及周期任务
- 打造 高性能,轻量级的 webform框架---js直接调后台
- 设计模式基本原则