一、概述
hyper daemon是hyper的后台守护进程,我们必须要通过输入hyperd命令先打开hyper daemon,然后才能执行hyper run,hyper stop等一系列对于hyper虚拟机的操作命令。与hyper client不同的是,hyper daemon承担了整个hyper程序的大部分任务且主要由三部分组成:server,engine和job。
其中server用于接收来自hyper client端的请求,然后将请求转发到engine中。engine则主要是路由的作用,通过一个handler对象将具体的请求分发到相应的job去执行。至于job则用于将任务转发的具体的函数去执行。其实从上面的叙述我们可以发现,这些任务的转发的确显得有点啰嗦,所以在docker中engjine和job这个两个概念已经被移除了。希望hyper在以后的代码重构时也能对这部分代码进行精简。下面我们就通过源码来观察hyper daemon的整个启动过程。
二、flag 参数解析
当你输入在shell中输入hyperd命令时,我们就进入hyperd.go文件中的main函数开始创建hyper daemon了!首先和hyper client中类似,我们需要对命令行参数进行解析。main函数中先是定义了三个flag参数flConfig,flHost,flHelp分别表示hyper daemon的配置信息,host的IP地址和端口号,以及是否输出帮助信息。当然,这些我们都没有,所以直接采用默认设置。接下来执行语句
mainDaemon(*flConfig, *flHost)正式进入hyper daemon的创建。
三、mainDaemon函数分析
因为我们并没有显式指定配置文件的地址,所以首先要做的就是指定默认的配置文件地址为/etc/hyper/config,其实里面指定的无非是hyper kernel,hyper initrd等等用于启动虚拟机的组件的地址而已。如果你想用自己的kernel启动虚拟机,可以试着修改里面的内容。
接下来是两条非常关键的语句,分别用于初始化engine和daemon 。
eng := engine.New(config)
d, err := daemon.NewDaemon(eng)
因为它们都非常重要,所以在下面的小节中再进行详细的叙述。在初始化engine和daemon之后,通过err := d.Install(eng)函数将具体的方法注册到engine的handler中,其实做的不过就是初始化一个映射表map[string]engine.Handler而已。例如通过server传递给engine的是“podRun”请求,则将其映射得到daemon.CmdPodRun方法。由此完成了从server请求到具体执行函数的映射。此时,hyper daemon就已经完全准备好了,接下来就可以接收来自hyper client的请求。通过如下代码:
if err := eng.Job("acceptconnections").Run(); err != nil {
glog.Error("the acceptconnections job run failed!\n")
return
}
…....
job := eng.Job("serveapi", defaultHost...)
…....
go func() {
if err := job.Run(); err != nil {
glog.Errorf("ServeAPI error: %v\n", err)
serveAPIWait <- err
return
}
serveAPIWait <- nil
}()
先是创建了一个叫”acceptconnections”的job用于通知hyper daemon可以接受来自hyper client的请求了。紧接着创建了一个名为”serveapi”的job,然后创立了一个独立协程(goroutine)专门接受请求,也就是初始化前面所说的server,本质上来说,server也就是一个job。这样,在mainDaemon函数内,hyper daemon的各个组件都创建完备,接下来我们进入具体的函数,观察engine的启动,daemon数据结构的填充以及最后server的建立的详细过程。
四、engine初始化
在engine.New(config)函数中首先做的就是填充Engine结构体,初始化了一个engine:
eng := &Engine{
handlers: make(map[string]Handler),
id: "test-1024",
Config: config,
Stdout: os.Stdout,
Stderr: os.Stderr,
Stdin: os.Stdin,
Logging: true,
}
显然,其中最重要的就是handlers字段了,因为这相当与就是一个路由表,能够将相应的请求字符串映射到具体的处理函数。在这之后,因为handlers的注册主要是集中在d.Install(eng)中做的,在eng.New(config)中仅仅只是注册了键值为“commands”的handler用于输出engine中已经注册的命令。最后,将Engine对象返回,eng.New(eng)的任务也就完成了。从以上的论述可以发现,engine实质上就是一个任务分发器,在整个hyper daemon中起到的作用非常有限。但是需要说明的是,以这种注册的方式添加对于不同请求的操作,的确具有非常好的扩展性,只要扩充一下server的接收请求,然后将相应的请求注册到engine的分发表中,即可增加一个新的操作。
五、NewDaemon的创建
从上文中我们了解到通过daemon.NewDaemon(eng)我们就创建了一个新的daemon。实际的情况是,NewDaemon函数仅仅只是一个封装,里面只有一步操作,那就是调用daemon, err := NewDaemonFromDirectory(eng),这个函数才真正完成了daemon的初始化。
首先,在NewDaemonFromDirectory函数中做的就是对主机环境的检查,其中运行的操作系统必须是Linux。其实hyper本身对操作系统并没有依赖,但是由于它需要调用docker为其创建容器,而docker目前是只能在Linux环境下运行的,所以hyper需要检查主机是否为Linux。之后要做的就是添加对daemon的配置,也就是填充daemon这个数据结构。其中Daemon这个数据结构的定义如下所示:
type Daemon struct {
ID string
db *leveldb.DB
eng *engine.Engine
dockerCli *docker.DockerCli
containerList []*Container
podList map[string]*Pod
vmList map[string]*Vm
qemuChan map[string]interface{}
qemuClientChan map[string]interface{}
subQemuClientChan map[string]interface{}
kernel string
initrd string
bios string
cbfs string
BridgeIface string
BridgeIP string
Host string
Storage *Storage
}
可以发现Daemon中存在着大量的map[string]interface{}类型的变量。至于为什么会这样呢?其实仔细想一下我们就能知道,假设我现在通过hyper client要对一个虚拟机进行操作,那我怎么找到它呢?其实就是通过daemon里面的这些podList,vmList映射表。例如podList[“podID”]就能得到该podID对应的Pod数据结构。而pod,vm,container之间往往是相互关联的,因此就能非常容易得找到对方,而这些同时也体现了hyper daemon的调度作用。
还有就是dockerCli这个字段看起来感觉非常奇怪,为什么hyper中会出现docker的client?等到我们分析hyper启动虚拟机的时候就可以发现,hyper本身是不创建容器的,它做的不过是调用docker client给docker daemon发送一个create container的请求,让docker创建一个容器的镜像,然后将其挂载到虚拟机中去。明白了这一点之后,我们再来看看Storage这个字段,它其实是一个指向Storage类型的指针。而Storage保存的是容器镜像存储方面的内容,我们知道docker的镜像是支持多种文件系统的,例如:aufs,devicemapper,vfs等等。因此,这时hyper就需要获得docker daemon方面的信息,根据docker daemon使用的镜像的存储方式来确认自身的存储类别配置。而这个工作是通过body, _, err := dockerCli.SendCmdInfo()这个函数完成的。然后根据从docker daemon得到的配置信息对hyper daemon的storage字段进行配置。因为容器镜像的存储方式是个非常重要的内容,完全值得再开一篇专题进行论述,因此这里就不再详细分析了。到这里,hyper daemon的主体也就创建完成了,最后剩下的就是对http server的创建。
六、server的创建
从前面对mainDaemon的分析我们已经知道,最后会创建一个叫做”serveapi”的job用于接受来自hyper client的请求,其实也就是做了一个http server。然后,这个叫”serveapi”的job最终会被分发到server.go文件的ServeApi(job *engine.Job)函数中。第一步,我们要做的就是根据hyper daemon支持的不同协议,创建出不同的http server。这里,我们遍历Job中的Args字段,分离出不同的协议类型以及对应的通信地址,最后通过srv, err := NewServer(protoAddrParts[0], protoAddrParts[1], job)函数创建出相应的http 服务器。其实hyper 原生支持的是tcp和unix domain两种形式协议的 。但由于我们默认使用的unix domain协议并且为了叙述方便,我们就以基于unix domain的http server进行介绍。因此在NewSever()函数中自然是选择setupUnixHttp(addr, job)函数进行执行。在setupUnixHttp(addr, job)中首先执行r := createRouter(job.Eng, job.GetenvBool("Logging"), job.GetenvBool("EnableCors"), job.Getenv("CorsHeaders"), job.Getenv("Version"))创建一个路由分发表。通过分发表,将不同的http请求,分发到相应的handler进行执行。然后通过l, err := newListener("unix", addr, job.GetenvBool("BufferRequests")设立监听模块,其实整个函数也只是一个封装,内部实现就只是调用Go语言内置net库的net.Listen(proto, addr)而已。当请求路由和监听模块都配置完整了之后,就可以填充并返回http server相应的数据结构&HttpServer{&http.Server{Addr: addr, Handler: r}, l},回到前面的所说的serveApi函数之后再调用srv.Server()函数就架起了一个基于unix domain协议的http服务器了,可以用于接收来自hyper client的请求。
这里有必要再详细说明一下r := createRouter(...)这个函数,它的主要作用是创建并配置一个路由分发表。开始的时候,它先定义了一个变量r := mux.NewRouter(),其中NewRouter是gorilla/mux包内置的一个函数,用于创建一个Router类型的对象。然后,函数中又定义了变量m:
m := map[string]map[string]HttpApiFunc{
"GET": {
"/info": getInfo,
"/pod/info": getPodInfo,
"/version": getVersion,
"/list": getList,
"/checkpoint": getCheckpoint,
},
"POST": {
"/container/create": postContainerCreate,
"/image/create": postImageCreate,
"/pod/create": postPodCreate,
"/pod/start": postPodStart,
"/pod/remove": postPodRemove,
"/pod/run": postPodRun,
"/pod/stop": postStop,
"/vm/create": postVmCreate,
"/vm/kill": postVmKill,
"/exec": postExec,
"/attach": postAttach,
"/tty/resize": postTtyResize,
},
"DELETE": {},
"OPTIONS": {
"": optionsHandler,
},
}
显然,从m的定义我们就可以看出,这就是一张完整的路由分发表。例如我们在需要启动虚拟机的时候,hyper client就会向server发送一个POST /pod/run的请求,这时候,通过整个路由表我们就能找到posPodRun函数进行执行,然后在postPodRun中再调用engine的相关操作,server的功能也就达到了。接下来我们要做的就是将路由表m添加到之前定义的router中就行了,所以就有了一下的一系列语句:
for method, routes := range m {
for route, fct := range routes {
glog.V(0).Infof("Registering %s, %s\n", method, route)
// NOTE: scope issue, make sure the variables are local and won‘t be changed
localRoute := route
localFct := fct
localMethod := method
// build the handler function
f := makeHttpHandler(eng, logging, localMethod, localRoute, localFct, corsHeaders, version.Version(dockerVersion))
// add the new route
if localRoute == "" {
r.Methods(localMethod).HandlerFunc(f)
} else {
r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f)
r.Path(localRoute).Methods(localMethod).HandlerFunc(f)
}
}
}
显然,上述代码在做的工作就是遍历m中的各个表项,获取对应的http method ,执行路径,以及相应的http handler。而具体的http.HandlerFunc函数指针则是通过makeHttpHandler获得的,最后,将其注册到路由表r中。Http Server的路由转发模块完成。
七、总结
其实,由上文的分析我们可以看出hyper daemon不过就是两个路由转发表,先由http server接收来自http client的请求,通过http server的路由表转发到相应的函数执行。接着再相应函数中再用engine启动相应的job又完成了一次转发。最后又通过job去执行具体的函数,完成命令的具体实现。
版权声明:本文为博主原创文章,未经博主允许不得转载。
原文地址:http://blog.csdn.net/u011915301/article/details/48010757