"docker源码阅读笔记四"

  "docker客户端与服务器端通信模块二"

Posted by Xu on April 21, 2017

docker源码阅读笔记四-docker客户端与服务器通信模块

在docker笔记三中,我们学习了客户端如何发送请求,服务器端的启动配置过程,大致了解了服务器端的提供api服务是如何实现的。这一节我们将会结合客户端和服务器端的通信接口的对接及服务器端接收到api请求后的方法路由分发到api的具体实现。

服务器端API请求路由实现

1.1 路由初始化入口

在服务器端启动的过程当中已经将docker daemon的路由信息配置好了,第17个步骤就是对路由器的初始化操作:
initRouter(api, d, c)
,这里我们将以该函数为入口,分析Docker服务器端对api路由器的初始化过程。

  func initRouter(s *apiserver.Server, d *daemon.Daemon, c *cluster.Cluster) {
	decoder := runconfig.ContainerDecoder{}//获取解码器
	routers := []router.Router{//添加路由数据项
		// we need to add the checkpoint router before the container router or the DELETE gets masked
		checkpointrouter.NewRouter(d, decoder),
		container.NewRouter(d, decoder),
		image.NewRouter(d, decoder),
		systemrouter.NewRouter(d, c),
		volume.NewRouter(d),
		build.NewRouter(dockerfile.NewBuildManager(d)),
		swarmrouter.NewRouter(c),
		pluginrouter.NewRouter(d.PluginManager()),
	}
	if d.NetworkControllerEnabled() {
		routers = append(routers, network.NewRouter(d, c))
	}
	if d.HasExperimental() {//如果是experimental模式将所有路由数据项中的experimental模式下的api路由功能激活
		for _, r := range routers {
			for _, route := range r.Routes() {
				if experimental, ok := route.(router.ExperimentalRoute); ok {
					experimental.Enable()
				}
			}
		}
	}
	s.InitRouter(debug.IsEnabled(), routers...)//根据设置好的路由表routers来初始化apiServer的路由器。
}

在路由初始化入口的实现代码中,分为三大步骤:
(1)向路由表routers中添加路由数据项router
(2)检验experimental模式,若为experimental模式则激活该模式下的api路由功能
(3)根据路由表初始化apiServer路由器

1.2 路由数据项添加实现

1.1中的分析指出路由器初始化入口的三大步骤,这一小节分析第一个步骤,路由表添加数据项的过程,即如何建立api请求json数据和该API方法实现handler的关联关系。所以从代码中看出该过程的实现是通过函数checkpointrouter.NewRouter(d, decoder)。并且我们采用的研究对象也正是和热迁移相关的checkpoint命令路由数据项:

 // NewRouter initializes a new checkpoint router,初始化了一个新的checkpoint路由器对象
func NewRouter(b Backend, decoder httputils.ContainerDecoder) router.Router {
	r := &checkpointRouter{
		backend: b,
		decoder: decoder,
	}
	r.initRoutes()
	return r
}
//结构体checkpointRouter中的backend结构体对象结构如下,checkpoint后端包括如下三个子命令接口
// Backend for Checkpoint
type Backend interface {
	CheckpointCreate(container string, config types.CheckpointCreateOptions) error
	CheckpointDelete(container string, config types.CheckpointDeleteOptions) error
	CheckpointList(container string, config types.CheckpointListOptions) ([]types.Checkpoint, error)
}
//这里我们选取CheckpointCreate(container string, config types.CheckpointCreateOptions) error来深入研究
该函数接口的实现定义在docker/daemon/checkpoint.go当中<br>
// CheckpointCreate checkpoints the process running in a container with CRIU
func (daemon *Daemon) CheckpointCreate(name string, config types.CheckpointCreateOptions) error {
}

该部分对checkpointCreate的实现内容我们将放到下一节进行描述,虽然该部分容器检查点的创建实现非常重要,但是本章的主要内容是api路由的创建和实现,所以在解释完checkpointRouter结构体中的内容后,接下来则是根据该checkpointRouter结构体对象来初始化checkpoint路由器的过程:
r.initRoutes()

initRounter实现代码如下,其中router.Experimental()函数用于将一个路由器标记为Experiameantal路由器,router.NewGetRoute(path string, handler httputils.APIFunc)则是建立一个Get方式的本地路由对象localRoute

   func (r *checkpointRouter) initRoutes() {
	r.routes = []router.Route{
		router.Experimental(router.NewGetRoute("/containers/{name:.*}/checkpoints", r.getContainerCheckpoints)),
		router.Experimental(router.NewPostRoute("/containers/{name:.*}/checkpoints", r.postContainerCheckpoint)),
		router.Experimental(router.NewDeleteRoute("/containers/{name}/checkpoints/{checkpoint}", r.deleteContainerCheckpoint)),
	}
}


本地路由对象localRoute结构体如下

// with the docker daemon. It implements Route.
type localRoute struct {
	method  string//该路由中方法名
	path    string//该路由中方法所在的路径
	handler httputils.APIFunc//该方法的handler
}


从router.Experimental(router.NewPostRoute(“/containers/{name:.*}/checkpoints”, r.postContainerCheckpoint)看出新建的本地路由的结构体对象中的handler(api方法实际处理程序)由r.postContainerCheckpoint提供,该函数的实现如下:

//接受请求,解析请求,和响应的实现函数
func (s *checkpointRouter) postContainerCheckpoint(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}
	var options types.CheckpointCreateOptions
	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&options); err != nil {
		return err
	}
	err := s.backend.CheckpointCreate(vars["name"], options)//这里就是调用了1.2小节我们所提到的容器检查点具体实现的方法,下一章将对该检查点的实现进行深入研究
	if err != nil {
		return err
	}
	w.WriteHeader(http.StatusCreated)//返回创建状态吗
	return nil
}

1.3 路由表创建完成

1.1小节中的路由表对象

    routers := []router.Router{//添加路由数据项
		// we need to add the checkpoint router before the container router or the DELETE gets masked
		checkpointrouter.NewRouter(d, decoder),
		container.NewRouter(d, decoder),
		image.NewRouter(d, decoder),
		systemrouter.NewRouter(d, c),
		volume.NewRouter(d),
		build.NewRouter(dockerfile.NewBuildManager(d)),
		swarmrouter.NewRouter(c),
		pluginrouter.NewRouter(d.PluginManager()),
	}

经过1.2步骤中的数据项的添加成功的对路由表进行配置得到了完整路由信息的路由对象routers
然后进入路由初始化入口的第二个步骤,检验experimental模式,若为该模式,则将所有在1.2步骤中标记为experimental的路由项进行激活操作:experimental.Enable()

1.4 初始化apiServer路由器

在路由表创建成功且经过experimental运行模式识别后,进入第三个步骤:利用该路由表routers信息初始化apiServer的路由器。
s.InitRouter(debug.IsEnabled(), routers…)
该函数代码如下:

   // InitRouter initializes the list of routers for the server.
// This method also enables the Go profiler if enableProfiler is true.
func (s *Server) InitRouter(enableProfiler bool, routers ...router.Router) {
	s.routers = append(s.routers, routers...)//将我们创建好的路由表信息追加到apiServer对象中的routers。
	m := s.createMux()//追加后再次初始化apiServer路由器进行更新,该初始化实现有具体解释
	if enableProfiler {
		profilerSetup(m)
	}
	s.routerSwapper = &routerSwapper{
		router: m,
	}//这里设置好了mux.Route之后,将该route设置到apiServer的路由交换器中去,至此所有deamon.start()的相关工作处理完毕
}


下面是对函数createMux创建mux.route路由器的过程分析:

// createMux initializes the main router the server uses.
func (s *Server) createMux() *mux.Router {
	m := mux.NewRouter()//mux位于vendor/github.com/gorilla/mux,该函数新建一个mux.go中的Route(路由数据项)对象并追加到mux
.Router结构体中的成语routes中去然后返回该路由器mux.Route m
	logrus.Debug("Registering routers")
	for _, apiRouter := range s.routers {//遍历所有apiserver中的api路由器如:checkpoint
		for _, r := range apiRouter.Routes() {//遍历每个apiRouter的子命令路由r如checkpoint create,list ,remove等
			f := s.makeHTTPHandler(r.Handler())//给每个r的路由handler包裹了一层中间件(这里还不是很清楚)
			logrus.Debugf("Registering %s, %s", r.Method(), r.Path())
			m.Path(versionMatcher + r.Path()).Methods(r.Method()).Handler(f)//在mux.Route路由结构中根据这个r.Path()路径设置一个适配器来匹配方法method和handler,当满足ersionMatcher + r.Path()路径的正则表达式要求就可以适配到相应的方法名及该handler
			m.Path(r.Path()).Methods(r.Method()).Handler(f)//同上
		}
	}
	err := errors.NewRequestNotFoundError(fmt.Errorf("page not found"))
	notFoundHandler := httputils.MakeErrorHandler(err)
	m.HandleFunc(versionMatcher+"/{path:.*}", notFoundHandler)
	m.NotFoundHandler = notFoundHandler//以上四行代码是设置mux.Route没有找到请求数据所对应的方法或函数handler时的处理办法
	return m
}

1.5 总结

终于是把Docker daemon启动过程中路由器初始化过程详细分析了一遍,确实相当复杂。我担心用不了几天,这整个路由器初始化的思路难以捋清,所以我把这一整个初始化的流程做一个容易理解和抽象性的描述:很明显,路由器的初始化工作是从Daemon.start()函数中的initRouter(api, d, c)步骤开始执行,这个初始化入口做了三件事:
(1)构建apiserver.routers结构体,并且添加所有api的路由数据项,添加过程中,首先是建立checkpintRoute路由结构体,配置好checkpointRouted的所有子命令的实现函数后,然后初始化该路由,设置请求路径与httpHandler的对应关系,httpHandler再调用checkpintRoute已经配置好的api实现函数即可。该checkpintRoute路由即是被添加的路由数据添加到apiServer.routers中去了。
(2)检测experimental模式,检测成功,激活被标记为experimental的路由项,这部分很简单,因为在路由数据的添加过程中,已将该标记为experimental的api都标记,激活即可
(3)路由表创建完成后,根据该路由表信息创建mux.route对象(这里与apiserver中的routers有区别),然后将得到的mux.route添加到apiserver.routerSwapper中去,路由初始化完成。构建mux.route的时候就是根据apiserver.routers中的数据项逐个添加适配器。