天天看點

Hyper 源碼分析-----建立hyper daemon

一、概述

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