前面我们说过,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