2. daemon详解——APIServer

APIServer

Server接收用户通过client发来的请求,按照路由规则分发,交给后端处理完毕后将结果返回至client

APIServer

之前说到start()中的NewDaemon()是初始化Daemon的核心逻辑,APIServer的创建在start()中。

  • 首先调用NewAPIServerConfig(cli),根据DaemonCli的配置生成APIServer的配置文件,比如log、Version、TLS,并将Cli.Config.Hosts的值置为1,然后将serverConfig返回。之后如果发现建立config失败,则输出错误信息,反之进入下个环节。
  • 第二步调用apiserver.New(serverConfig)新建server实例,server中包含了config、router、HTTPServer等,新建之后填充了config的部分,其他仍未初始化。
  • 第三步调用loadListeners(),在这个函数里根据DaemonCli里的HOSTS,对每一个可用的host,解析出protocol和address,调用listeners.Init()与Daemon建立socket连接,分配一个Daemon的端口监听socket。即与每个可用的Daemon建立连接(如果你尝试一个APIServer连接到多个daemon的话,是可行的)。
  • 到此为止,已经初始化了apiserver,之后会为APIServer初始化router。
func newAPIServerConfig(cli *DaemonCli) (*apiserver.Config, error) {
    serverConfig := &apiserver.Config{
        Logging:     true,
        SocketGroup: cli.Config.SocketGroup,
        Version:     dockerversion.Version,
        CorsHeaders: cli.Config.CorsHeaders,
    }

    if cli.Config.TLS {
        tlsOptions := tlsconfig.Options{
            CAFile:             cli.Config.CommonTLSOptions.CAFile,
            CertFile:           cli.Config.CommonTLSOptions.CertFile,
            KeyFile:            cli.Config.CommonTLSOptions.KeyFile,
            ExclusiveRootPools: true,
        }

        if cli.Config.TLSVerify {
            // server requires and verifies client's certificate
            tlsOptions.ClientAuth = tls.RequireAndVerifyClientCert
        }
        tlsConfig, err := tlsconfig.Server(tlsOptions)
        if err != nil {
            return nil, err
        }
        serverConfig.TLSConfig = tlsConfig
    }

    if len(cli.Config.Hosts) == 0 {
        cli.Config.Hosts = make([]string, 1)
    }

    return serverConfig, nil
}

func New(cfg *Config) *Server {
    return &Server{
        cfg: cfg,
    }
}

// Server contains instance details for the server
type Server struct {
    cfg           *Config
    servers       []*HTTPServer
    routers       []router.Router
    routerSwapper *routerSwapper
    middlewares   []middleware.Middleware
}

func loadListeners(cli *DaemonCli, serverConfig *apiserver.Config) ([]string, error) {
    var hosts []string
    for i := 0; i < len(cli.Config.Hosts); i++ {
        var err error
        if cli.Config.Hosts[i], err = dopts.ParseHost(cli.Config.TLS, cli.Config.Hosts[i]); err != nil {
            return nil, fmt.Errorf("error parsing -H %s : %v", cli.Config.Hosts[i], err)
        }

        protoAddr := cli.Config.Hosts[i]
        protoAddrParts := strings.SplitN(protoAddr, "://", 2)
        if len(protoAddrParts) != 2 {
            return nil, fmt.Errorf("bad format %s, expected PROTO://ADDR", protoAddr)
        }

        proto := protoAddrParts[0]
        addr := protoAddrParts[1]

        // It's a bad idea to bind to TCP without tlsverify.
        if proto == "tcp" && (serverConfig.TLSConfig == nil || serverConfig.TLSConfig.ClientAuth != tls.RequireAndVerifyClientCert) {
            logrus.Warn("[!] DON'T BIND ON ANY IP ADDRESS WITHOUT setting --tlsverify IF YOU DON'T KNOW WHAT YOU'RE DOING [!]")
        }
        ls, err := listeners.Init(proto, addr, serverConfig.SocketGroup, serverConfig.TLSConfig)
        if err != nil {
            return nil, err
        }
        ls = wrapListeners(proto, ls)
        // If we're binding to a TCP port, make sure that a container doesn't try to use it.
        if proto == "tcp" {
            if err := allocateDaemonPort(addr); err != nil {
                return nil, err
            }
        }
        logrus.Debugf("Listener created for HTTP on %s (%s)", proto, addr)
        hosts = append(hosts, protoAddrParts[1])
        cli.api.Accept(addr, ls...)
    }

    return hosts, nil
}

router

初始化router的过程同样在start()里。

  • 调用newRouterOptions()进行参数的设置
  • 调用initRouter。建立了decoder进行命令的解析,之后建立了多个router分别为不同的模块服务,比如checkpoint router, container router, image router, volume router, swarm router等等。接收消息后会根据消息的信令分配不同的router进行处理。对于router的初始化都调用了NewRouter函数。比如container.NewRouter()里调用了r.initRoutes(),其中定义了router.NewGetRoute("/containers/json", r.getContainersJSON)这样的配置,就为处理命令寻找到了相应的文件。
  • 把所有的router初始化好,调用InitRouter。吐槽一下命名,只是i大小写的区别都可以吗!很容易看不懂的啊!大写的比较牛逼,定义了API Server使用的main router,它通过调用CreateMux()中的makeHTTPHandler()来建立main router。这个函数的形参是responseWriter和Request,就非常好理解是干啥的了,它定义了context这个概念,这是一个全局的变量,然后通过handlerWithGlobalMiddlewares()定义了handlerFunc来处理request,通过WrapHandler()把回调函数注册进去了,之后通过mux.Vars()获取了request相关的router的信息,于是请求的处理过程真正是通过handlerFunc(ctx, w, r, vars)实现的。
func (s *Server) makeHTTPHandler(handler httputils.APIFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Define the context that we'll pass around to share info
        // like the docker-request-id.
        //
        // The 'context' will be used for global data that should
        // apply to all requests. Data that is specific to the
        // immediate function being called should still be passed
        // as 'args' on the function call.

        // use intermediate variable to prevent "should not use basic type
        // string as key in context.WithValue" golint errors
        var ki interface{} = dockerversion.UAStringKey
        ctx := context.WithValue(context.Background(), ki, r.Header.Get("User-Agent"))
        handlerFunc := s.handlerWithGlobalMiddlewares(handler)

        vars := mux.Vars(r)
        if vars == nil {
            vars = make(map[string]string)
        }

        if err := handlerFunc(ctx, w, r, vars); err != nil {
            statusCode := httputils.GetHTTPErrorStatusCode(err)
            if statusCode >= 500 {
                logrus.Errorf("Handler for %s %s returned error: %v", r.Method, r.URL.Path, err)
            }
            httputils.MakeErrorHandler(err)(w, r)
        }
    }
}

func initRouter(opts routerOptions) {
    decoder := runconfig.ContainerDecoder{}

    routers := []router.Router{
        // we need to add the checkpoint router before the container router or the DELETE gets masked
        checkpointrouter.NewRouter(opts.daemon, decoder),
        container.NewRouter(opts.daemon, decoder),
        image.NewRouter(opts.daemon.ImageService()),
        systemrouter.NewRouter(opts.daemon, opts.cluster, opts.buildCache, opts.buildkit),
        volume.NewRouter(opts.daemon.VolumesService()),
        build.NewRouter(opts.buildBackend, opts.daemon),
        sessionrouter.NewRouter(opts.sessionManager),
        swarmrouter.NewRouter(opts.cluster),
        pluginrouter.NewRouter(opts.daemon.PluginManager()),
        distributionrouter.NewRouter(opts.daemon.ImageService()),
    }

    if opts.daemon.NetworkControllerEnabled() {
        routers = append(routers, network.NewRouter(opts.daemon, opts.cluster))
    }

    if opts.daemon.HasExperimental() {
        for _, r := range routers {
            for _, route := range r.Routes() {
                if experimental, ok := route.(router.ExperimentalRoute); ok {
                    experimental.Enable()
                }
            }
        }
    }

    opts.api.InitRouter(routers...)
}

middleware

那么,还需要关注一下middleware。它的初始化在start中的initMiddlewares(),形参用到了cli.api、serverConfig、pluginStore。

pluginStore在上一行的plugin.NewStore()完成初始化,它返回一个Store对象,其中最重要的是定义了注册回调函数的接口

/* handlers are necessary for transition path of legacy plugins
* to the new model. Legacy plugins use Handle() for registering an
* activation callback.*/
handlers map[string][]func(string, *plugins.Client)

initMiddlewares定义了不同的中间件,包括:

  • NewExperimentalMiddleware: 返回一个experimental string(true或者false)标志实验性功能是否可用
  • NewVersionMiddleware: 返回version信息,包括server、default、minVersion
  • NewCORSMiddleware: 不清楚是什么
  • authzMiddleware: authorizationPlugin is an internal adapter to docker plugin system,有更具体的解释吗?
// TODO: remove this from cli and return the authzMiddleware
func (cli *DaemonCli) initMiddlewares(s *apiserver.Server, cfg *apiserver.Config, pluginStore plugingetter.PluginGetter) error {
    v := cfg.Version

    exp := middleware.NewExperimentalMiddleware(cli.Config.Experimental)
    s.UseMiddleware(exp)

    vm := middleware.NewVersionMiddleware(v, api.DefaultVersion, api.MinVersion)
    s.UseMiddleware(vm)

    if cfg.CorsHeaders != "" {
        c := middleware.NewCORSMiddleware(cfg.CorsHeaders)
        s.UseMiddleware(c)
    }

    cli.authzMiddleware = authorization.NewMiddleware(cli.Config.AuthorizationPlugins, pluginStore)
    cli.Config.AuthzMiddleware = cli.authzMiddleware
    s.UseMiddleware(cli.authzMiddleware)
    return nil
}

重新回到上面的handlerWithGlobalMiddlewares()函数,它内部有一个循环,遍历了使用的middlewares,对每个middleware调用WrapHandler()

Middleware的作用docker的解释是:

// Middleware is an interface to allow the use of ordinary functions as Docker API filters.
// Any struct that has the appropriate signature can be registered as a middleware.

WrapHandler对于不同的middleware有不同的实现版本,它的原型是

type Middleware interface {
    WrapHandler(func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error) func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error
}

开始提供服务

在创建好APIServer,连接上Daemon,创建、设置好Router之后,APIServer就可以向外提供服务了,它提供的是一个HTTP Server之上标准的RESTful-API,对用户非常友好。
APIServer的真正运行采用go routine的方式,go cli.api.wait()真正启动了APIServer。采用go routine的原因是:如果APIServer出错,daemon会随之退出,采用go routine可以在出错的时候尝试重启,保证docker的正常运行。在wait函数中调用了serveAPI(),由于之前可能和多个Daemon建立了连接,所以对每个HTTP server,调用Serve()函数。
Serve函数为每个client的连接建立goroutine的服务,服务会解析request,调用handler,返回结果。
该函数并不是docker开发者自己写的,而是调用了GO语言的net/http库。大概里面的函数有:readRequesthandler := sh.srv.HandlerServeHTTP(ResponseWriter, *Request)等。由于是系统库,所以先不看了。

// serveAPI loops through all initialized servers and spawns goroutine
// with Serve method for each. It sets createMux() as Handler also.
func (s *Server) serveAPI() error {
    var chErrors = make(chan error, len(s.servers))
    for _, srv := range s.servers {
        srv.srv.Handler = s.routerSwapper
        go func(srv *HTTPServer) {
            var err error
            logrus.Infof("API listen on %s", srv.l.Addr())
            if err = srv.Serve(); err != nil && strings.Contains(err.Error(), "use of closed network connection") {
                err = nil
            }
            chErrors <- err
        }(srv)
    }

    for range s.servers {
        err := <-chErrors
        if err != nil {
            return err
        }
    }
    return nil
}

总结

附上start中的高层流程:

func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
    serverConfig, err := newAPIServerConfig(cli)
    if err != nil {
        return fmt.Errorf("Failed to create API server: %v", err)
    }
    cli.api = apiserver.New(serverConfig)

    hosts, err := loadListeners(cli, serverConfig)
    if err != nil {
        return fmt.Errorf("Failed to load listeners: %v", err)
    }
...
    pluginStore := plugin.NewStore()
    if err := cli.initMiddlewares(cli.api, serverConfig, pluginStore); err != nil {
        logrus.Fatalf("Error creating middlewares: %v", err)
    }
...
    routerOptions, err := newRouterOptions(cli.Config, d)
    if err != nil {
        return err
    }
    routerOptions.api = cli.api
    routerOptions.cluster = c

    initRouter(routerOptions)
...
    serveAPIWait := make(chan error)
    go cli.api.Wait(serveAPIWait)
...
    errAPI := <-serveAPIWait

APIServer是daemon和docker client通信的接口,在daemon的初始化流程中优先级非常高。通过初始化apiserver实例、router实例、middleware实例,系统已经为APIServer提供了完整的运行环境。Middleware为在docker和用户之间做了一层隔离,为用户提供标准的接口,通过middleware,回调函数被注册进router中。
真正运行时,采用goroutine的方式保证了保证了daemon的稳定运行,当request到达APIServer中后,APIServer通过main router进行查找,调用相应的router,也就执行了相应的回调函数的调用,从而实现了对request的处理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,639评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,277评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,221评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,474评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,570评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,816评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,957评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,718评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,176评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,511评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,646评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,322评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,934评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,755评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,987评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,358评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,514评论 2 348

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • Refer to: www.threemeal.com/blog/12/ 中间件 中间件是一个钩子框架,它们可以介...
    兰山小亭阅读 16,474评论 9 165
  • 【公司】浙江康意洁具有限公司 【姓名】景桃桃 【组别】235期六项精进【乐观二组】 【日精进打卡第013天】 【知...
    景桃桃阅读 140评论 0 0
  • 荣辱兮祸福兮 幸得红木一般的亲人 亦得青瓷一般的友人 愿得白玉一般的恋人 与众生共此生
    后来居士阅读 265评论 0 1
  • 写在前面:最近参加了好好花钱的年终总结征文比赛,经过一个多月的角逐和评审,我最终获得了一等奖,开心~撒花~ 现在将...
    查苏bobo阅读 397评论 0 2