Codis源码解析——proxy的启动

proxy启动的时候,首先检查输入的命令行,一般情况下,启动proxy的命令如下:

nohup ./bin/codis-proxy --ncpu=2 --config=./conf/proxy.conf --log=./logs/proxy.log --log-level=WARN &

程序会解析这行命令参数,下面举个例子(实例代码是cmd/proxy/main.go),有关于go的并行,这里要特别说明一下,如果不显示的指明GOMAXPROCS的话,goroutine都是运行在同一个CPU核心上,一个goroutine得到时间片的时候,其他的goroutine都在等待。所以还是要根据输入手动指定一下的。

d, err := docopt.Parse(usage, nil, true, "", false)
var ncpu int
if n, ok := utils.ArgumentInteger(d, "--ncpu"); ok {
    ncpu = n
} else {
    ncpu = 4
}
runtime.GOMAXPROCS(ncpu)

config制定了配置文件的目录,程序会读取配置文件中的配置项,填充到pkg/proxy/config.go中的config struct中。

当所有config属性填充完毕之后,调用pkg/proxy/proxy.go中的构造方法,得到一个Proxy,首先我们看看proxy这个struct的结构

type Proxy struct {
    mu sync.Mutex

    xauth string
    //不要搞混了,这个是/pkg/models/proxy.go下面的Proxy struct,也就是proxy成功创建之后,
    //在zookeeper的/codis3/codis-wujiang/proxy/proxy-token中展示的信息
    model *models.Proxy

    //接收退出信息的channel
    exit struct {
        C chan struct{}
    }
    online bool
    closed bool

    config *Config
    //Router中存储了集群中所有sharedBackendConnPool和slot,用于将redis请求转发给相应的slot进行处理
    router *Router
    ignore []byte

    //监听proxy的19000端口的Listener,也就是proxy实际工作的端口
    lproxy net.Listener
    //监听proxy_admin的11080端口的Listener,也就是codis集群和proxy进行交互的端口
    ladmin net.Listener

    ha struct {
        //上帝视角sentinel,并不是真正的物理服务器
        //s := &Sentinel{Product: product, Auth: auth}
        //s.Context, s.Cancel = context.WithCancel(context.Background())
        monitor *redis.Sentinel
        //int为groupId,标示每个group的主服务器
        masters map[int]string
        //当前集群中的所有物理sentinel
        servers []string
    }

    //java客户端Jodis与codis集群交互,就是通过下面的struct,里面存储了zkClient以及"/jodis/codis-wujiang/proxy-token"这个路径
    jodis *Jodis
}

当然,重点还是得到proxy的这个方法。这个方法是proxy启动过程中最重要的一步,内容也很多,初次看会比较头疼

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

    //新建一个Proxy,后面就是它填充属性
    s := &Proxy{}
    s.config = config
    s.exit.C = make(chan struct{})

    //通过config new一个Router,这一步只是初始化了Router中的两个sharedBackendConnPool的结构,
    //也就是map[string]*sharedBackendConn
    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()
    //返回路径的字符串
    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

    //将config中的参数设置到models.proxy里,主要设置的参数是admin_addr,proxy_addr,设置之后调用net.Listen进行监听,成功的话将Listener存入proxy,生成Token和xauth,以及proxy.Jodis。这里也设置了zk最终的树形路径

    //如果配置文件中的jodis_compatible设置的是false,就采用codis3的zk路径,jodis/codis-demo/proxy-token。如果兼容这里设置的true,就还采用codis2时期的/zk/codis/db_productName
    if err := s.setup(config); err != nil {
        s.Close()
        return nil, err
    }

    //到这一步,其实proxy已经新建完毕,控制台打印出新建的proxy信息
    log.Warnf("[%p] create new proxy:\n%s", s, s.model.Encode())

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

    //新建一个路由表,对发送到11080端口的请求做处理
    go s.serveAdmin()
    //启动goroutine来监听发送到19000端口的redis请求
    go s.serveProxy()

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

    return s, nil
}

models.Proxy的详细信息会挂载在zk目录”/codis3/codis-wujiang/proxy/proxy-token”下

{
    "id": 1,
    "token": "0317b8f67921f8c7a2d19d372cc9511b",
    "start_time": "2017-07-28 14:44:36.462306337 +0800 CST",
    "admin_addr": "*.*.*.*:11080",
    "proto_type": "tcp4",
    "proxy_addr": "*.*.*.*:19000",
    "jodis_path": "/jodis/codis-wujiang/proxy-0317b8f67921f8c7a2d19d372cc9511b",
    "product_name": "codis-wujiang",
    "pid": 45092,
    "pwd": "/app/codis",
    "sys": "Linux cnsz22vla888.novalocal 2.6.32-504.el6.x86_64 #1 SMP Wed Oct 15 04:27:16 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux",
    "hostname": "cnsz22vla888.novalocal",
    "datacenter": ""
}

路由表那里,着重介绍一下。路由表本质上其实就是一个map,key是路径 ==> “/”,value是该路径所对应的处理函数 ==> 在我们这里就是newApiServer(s)出来的处理函数。codis-proxy启动之后,这一步是启动http服务,当集群配置命令请求向prxoy_admin的11080端口发过来之后,转发做相应处理。newApiServer方法使用了这里使用了martini框架。有关于martini框架的详细信息,可以参见http://www.oschina.net/p/martini/

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()
        //这里表示newApiServer用来处理所有"/"路径的请求
        h.Handle("/", newApiServer(s))
        hs := &http.Server{Handler: h}
        //对每个net.Listener的连接,新建goroutine读请求,并调用srv.Handler进行处理
        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)
    }
}

//在/pkg/proxy/proxy_api.go中,请求转发到不同的路径,r是路由规则,最终的返回结果是由路由规则得到的handler
func newApiServer(p *Proxy) http.Handler {
    m := martini.New()
    m.Use(martini.Recovery())
    m.Use(render.Renderer())
    m.Use(func(w http.ResponseWriter, req *http.Request, c martini.Context) {
        path := req.URL.Path
        if req.Method != "GET" && strings.HasPrefix(path, "/api/") {
            var remoteAddr = req.RemoteAddr
            var headerAddr string
            for _, key := range []string{"X-Real-IP", "X-Forwarded-For"} {
                if val := req.Header.Get(key); val != "" {
                    headerAddr = val
                    break
                }
            }
            log.Warnf("[%p] API call %s from %s [%s]", p, path, remoteAddr, headerAddr)
        }
        c.Next()
    })
    m.Use(gzip.All())
    m.Use(func(c martini.Context, w http.ResponseWriter) {
        w.Header().Set("Content-Type", "application/json; charset=utf-8")
    })

    api := &apiServer{proxy: p}

    r := martini.NewRouter()
    r.Get("/", func(r render.Render) {
        r.Redirect("/proxy")
    })
    r.Any("/debug/**", func(w http.ResponseWriter, req *http.Request) {
        http.DefaultServeMux.ServeHTTP(w, req)
    })

    r.Group("/proxy", func(r martini.Router) {
        r.Get("", api.Overview)
        r.Get("/model", api.Model)
        r.Get("/stats", api.StatsNoXAuth)
        r.Get("/slots", api.SlotsNoXAuth)
    })
    r.Group("/api/proxy", func(r martini.Router) {
        r.Get("/model", api.Model)
        r.Get("/xping/:xauth", api.XPing)
        r.Get("/stats/:xauth", api.Stats)
        r.Get("/stats/:xauth/:flags", api.Stats)
        r.Get("/slots/:xauth", api.Slots)
        r.Put("/start/:xauth", api.Start)
        r.Put("/stats/reset/:xauth", api.ResetStats)
        r.Put("/forcegc/:xauth", api.ForceGC)
        r.Put("/shutdown/:xauth", api.Shutdown)
        r.Put("/loglevel/:xauth/:value", api.LogLevel)
        r.Put("/fillslots/:xauth", binding.Json([]*models.Slot{}), api.FillSlots)
        r.Put("/sentinels/:xauth", binding.Json(models.Sentinel{}), api.SetSentinels)
        r.Put("/sentinels/:xauth/rewatch", api.RewatchSentinels)
    })

    m.MapTo(r, (*martini.Routes)(nil))
    m.Action(r.Handle)
    return m
}

后面的serveProxy,s.acceptConn(l)是启动goroutine来监听redis请求,启动的时候肯定是没有请求过来的,所以我们看到这里为止, 下一节Codis源码解析——proxy监听redis请求会详细绍NewSession(c, s.config).Start(s.router)这里具体做了什么。

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 {
            //启动goroutine监听19000端口请求,有请求到来的时候,返回net.Conn
            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)
    }
}

最后面的三个方法,由于通常启动的时候,配置文件中的MetircsReporterServer,MetircsInfluxdbServer和MetircsStatsdPeriod三个字段都为空字符串,实际上什么都没做。

再回到主main函数,看看最后的一段代码

//启动时dashboard、coordinator.name和slots这三个参数都为空,所以此时这三个goroutine都没有执行
switch {
case dashboard != "":
    go AutoOnlineWithDashboard(s, dashboard)
case coordinator.name != "":
    go AutoOnlineWithCoordinator(s, coordinator.name, coordinator.addr)
case slots != nil:
    go AutoOnlineWithFillSlots(s, slots)
}
//未关闭,但也不在线的时候,控制台每秒输出日志
for !s.IsClosed() && !s.IsOnline() {
    log.Warnf("[%p] proxy waiting online ...", s)
    time.Sleep(time.Second)
}

到这里,整个proxy启动过程结束。启动之后,Proxy会默认处于 waiting 状态,以一秒一次的频率刷新状态,监听proxy_addr 地址(默认配置文件中的19000端口),但是不会 accept 连接,通过fe或者命令行添加到集群并完成集群状态的同步,才能改变状态为online。添加proxy到集群的过程,详见个人另一篇博客Codis源码解析——proxy添加到集群

这里写图片描述

到这里,我们总结一下,proxy启动过程中的流程:
读取配置文件,获取Config对象。根据Config新建Proxy,填充Proxy的各个属性,这里面比较重要的是填充models.Proxy(详细信息可以在zk中查看),并且与zk连接、注册相关路径。启动goroutine监听11080端口的codis集群发过来的请求并进行转发,以及监听发到19000端口的redis请求并进行相关处理。

说明
如有转载,请注明出处:
http://blog.csdn.net/antony9118/article/details/75268358

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值