RancherOSの起動時の仕組み
※2015-03-17にシステムコンテナの起動を追加、他にも加筆・修正。
Tiny Docker Operating Systems | Docker Blogを見たので触ってみた。
RancherOSはDockerコンテナを動かすためのミニマムなLinuxディストリビューションで、以下のような特徴を持つ。
- PID 1がDockerで、カーネルが起動する時の最初のプロセスである
- システム用のDockerとユーザ用のDockerがいる
- Goで書かれた独自のinitを持つ
- 非常にコンパクトである
PID1のDockerはシステム用のDocker(System Docker)と呼ばれていて、udevやdhcpd、ユーザ用のDocker(User Docker)を動かす。
要はsysvinitやsystemdの代わりにDockerを使っている、という感じだろうか。
RancherOSのサイトでもsystemdのような複雑なinitシステムを排除する、と言っている。
また、異なるディストリビューションのコンソールを簡単に使うこともできるらしい。
サイズも非常に小さく(20M)、起動も速い。
RancherOSの概要についてはAmazon EC2でRancherOSを触ってみたやREADME.mdを見れば分かりやすい。
また、Vagrantで簡単に試すことができる。
今回は、どのようにしてDockerデーモンをPID1のプロセスにしているのか、またシステムコンテナの起動部分を追ってみる。なお、RahcnerOSのリポジトリはここ。
まず、起動の流れについて図示する。
以下のように、大きく3段階に分かれており、1つずつ見ていく。
- initrdのブート・/initの実行
- 初期化・SystemDockerの実行
- システムコンテナの実行
1. /boot/initrdを使ってブート
ビルド時にinitrdを作るが、その際にBusyBoxの/initをRancherOS独自に実装したものに置き換えている。これはGoで書かれている。
Dockerの起動をしたりRancherOS用のコマンドを提供している。
置き換えは以下のような流れで行われる。
- script/buildの中でgo buildしてinitとなるバイナリ(bin/rancheros)を作る
- script/packageで作成したbin/rancherosをinitrd/initにコピーする
- artifact/initrd以下をcpio形式でアーカイブし、さらにlzmaで圧縮し、initrdファイルを作成する
isolinux.cfgは以下のようになっている。/boot/initrdのinitは/bin/rancherosなので、起動時にmain.goのmain関数が実行される。
[text]
default rancheros
label rancheros
kernel /boot/vmlinuz
initrd /boot/initrd
append quiet rancher.password=rancher rancher.modules=[mptspi]
[/text]
2. init/init.go:MainInit関数の実行
main.goのmain関数では、RancherOS用のコマンドを登録する。
コマンドのパスがキーで、実行する関数が値だ。
[go]
func main() {
registerCmd("/init", osInit.MainInit)
registerCmd(osInit.SYSINIT, sysinit.Main)
registerCmd("/usr/bin/system-docker", systemdocker.Main)
registerCmd("/sbin/poweroff", power.PowerOff)
registerCmd("/sbin/reboot", power.Reboot)
registerCmd("/sbin/halt", power.Halt)
registerCmd("/sbin/shutdown", power.Main)
registerCmd("/usr/bin/respawn", respawn.Main)
registerCmd("/usr/sbin/rancherctl", control.Main)
registerCmd("/usr/bin/cloud-init", cloudinit.Main)
registerCmd("/usr/sbin/netconf", network.Main)
if !reexec.Init() {
log.Fatalf(“Failed to find an entry point for %s”, os.Args[0])
}
}
[/go]
main.goのregisterCmd。cmdが「/usr/bin/system-docker」の場合、「/usr/bin/system-docker」と「system-docker」、「./system-docker」の3種類のキーで関数を登録する。
[go]
func registerCmd(cmd string, mainFunc func()) {
log.Debugf(“Registering main %s”, cmd)
reexec.Register(cmd, mainFunc)
parts := strings.Split(cmd, “/”)
if len(parts) == 0 {
return
}
last := parts[len(parts)-1]
log.Debugf(“Registering main %s”, last)
reexec.Register(last, mainFunc)
log.Debugf(“Registering main %s”, “./"+last)
reexec.Register(”./"+last, mainFunc)
}
[/go]
実際に登録しているのはpkg/reexec/reexec.goのRegister関数。
registeredInitializersに名前と関数を登録する。
[go]
var registeredInitializers = make(map[string]func())
// Register adds an initialization func under the specified name
func Register(name string, initializer func()) {
if _, exists := registeredInitializers[name]; exists {
panic(fmt.Sprintf(“reexec func already registred under name %q”, name))
}
registeredInitializers[name] = initializer
}
[/go]
main.goのmain関数に戻る。
registerCmdを全て実行したらreexec.Init()を実行。
[go]
func main() {
registerCmd("/init", osInit.MainInit)
registerCmd(osInit.SYSINIT, sysinit.Main)
registerCmd("/usr/bin/system-docker", systemdocker.Main)
registerCmd("/sbin/poweroff", power.PowerOff)
registerCmd("/sbin/reboot", power.Reboot)
registerCmd("/sbin/halt", power.Halt)
registerCmd("/sbin/shutdown", power.Main)
registerCmd("/usr/bin/respawn", respawn.Main)
registerCmd("/usr/sbin/rancherctl", control.Main)
registerCmd("/usr/bin/cloud-init", cloudinit.Main)
registerCmd("/usr/sbin/netconf", network.Main)
if !reexec.Init() {
log.Fatalf(“Failed to find an entry point for %s”, os.Args[0])
}
}
[/go]
reexec.goのinit関数では実行するコマンドに対応する関数を実行する。
os.Args[0]は実行するコマンド自身。
[go]
func Init() bool {
initializer, exists := registeredInitializers[os.Args[0]]
if exists {
initializer()
return true
}
return false
}
[/go]
つまり、起動時にinitが実行されると、それに対応する関数であるosInit.MainInitが実行される。
osInit.MainInitはosInit.RunInitを呼び出していて、これが行っているのはinitFuncsとDockerの実行である。
InitFuncは以下のような処理を行っている。
- ディレクトリの作成
- マウント
- resolve.confの設定
- シンボリックリンクの作成
- /sbin/rancheros-sysinit -> /init
- カーネルモジュールの有効化
- ReadOnlyで/を再度マウント
- sysInitの実行
- /sbin/rancheros-sysinitを外部コマンドとして実行
[go]
func RunInit() error {
var cfg config.Config
os.Setenv(“PATH”, “/sbin:/usr/bin”)
os.Setenv(“DOCKER_RAMDISK”, “true”)
initFuncs := []config.InitFunc{
func(cfg *config.Config) error {
return createDirs(dirs…)
},
func(cfg *config.Config) error {
log.Info(“Setting up mounts”)
return createMounts(mounts…)
},
func(cfg *config.Config) error {
newCfg, err := config.LoadConfig()
if err == nil {
newCfg, err = config.LoadConfig()
}
if err == nil {
*cfg = *newCfg
}
if cfg.Debug {
cfgString, _ := cfg.Dump()
if cfgString != "" {
log.Debugf(“Config: %s”, cfgString)
}
}
return err
},
mountCgroups,
setResolvConf,
createSymlinks,
extractModules,
loadModules,
mountState,
func(cfg *config.Config) error {
return createDirs(postDirs…)
},
func(cfg *config.Config) error {
return createMounts(postMounts…)
},
func(cfg *config.Config) error {
return cfg.Reload()
},
remountRo,
sysInit,
}
if err := config.RunInitFuncs(&cfg, initFuncs); err != nil {
return err
}
return execDocker(&cfg)
}
[/go]
createSymlinksでは/init、つまり自分自身へのリンクを/sbin/rancheros-sysinitとして作成し、sysInit関数で外部コマンドとしてそれを実行している。/initはrancheros-sysinitとして実行された場合、cmd/sysinit/sysinit.goのMain関数を実行する。
また、コマンドの実行終了は待たずに即リターンしている。
[go]
func sysInit(cfg *config.Config) error {
args := append([]string{SYSINIT}, os.Args[1:]…)
var cmd *exec.Cmd
if util.IsRunningInTty() {
cmd = exec.Command(args[0], args[1:]…)
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
} else {
cmd = exec.Command(args[0], args[1:]…)
}
if err := cmd.Start(); err != nil {
return err
}
return os.Stdin.Close()
}
[/go]
その後、init.goのexecDockerではsyscall.Execを使ってDockerを起動する。これがPID 1のプロセス、つまりSystem Dockerになる。
[go]
func execDocker(cfg *config.Config) error {
log.Info(“Launching System Docker”)
if !cfg.Debug {
output, err := os.Create("/var/log/system-docker.log")
if err != nil {
return err
}
syscall.Dup2(int(output.Fd()), int(os.Stdout.Fd()))
syscall.Dup2(int(output.Fd()), int(os.Stderr.Fd()))
}
os.Stdin.Close()
return syscall.Exec(DOCKER, cfg.SystemDockerArgs, os.Environ())
}
[/go]
3. cmd/sysinit/sysinit.go:Main関数の実行
SystemDockerを起動する前に外部コマンドとして/sbin/rancheros-sysinitを実行したが、このコマンドはシステムコンテナの実行を行っている。
標準で実行するのは以下のコンテナである。
- system-volumes
- /var/runや/var/log等をマウント
- command-volumes
- rahcnerctlやsystemdocker、cloud-init等を/initとしてマウント
- user-volumes
- /homeと/optをマウント
- udev
- network
- cloud-init
- デーモンとして実行
- ntp
- syslog
- userdocker
- console
最後に実行するconsoleコンテナはsshdやttyの実行も行っていて、ログインするとconsoleコンテナの中に入ることになる。
consoleコンテナはcommand-columesやuser-volumes、system-volumesをマウントしているのでrancherctlのようなコマンドを使うことができる。
まとめ
Goで書いた独自のinitを持ち、PID1番のプロセスがDockerであるRancherOSの起動時の挙動について見てみた。
Dockerに依存していて今後どうなるかわからないけれど、おもしろいと思う。
また、今回はSystem Dockerの起動後の動きは見ていないのでそちらもチェックししたい。 本記事に加筆した。