Codis源码解析——proxy的启动

来源:互联网 发布:淘宝无线端链接 编辑:程序博客网 时间:2024/05/22 09:38

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 intif 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是路由规则,最终的返回结果是由路由规则得到的handlerfunc 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