一、概述
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去執行具體的函數,完成指令的具體實作。