repl.info

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のリポジトリはここ

まず、起動の流れについて図示する。

2015-03-13_rancheros_init

以下のように、大きく3段階に分かれており、1つずつ見ていく。

  1.  initrdのブート・/initの実行
  2. 初期化・SystemDockerの実行
  3. システムコンテナの実行

1. /boot/initrdを使ってブート

ビルド時にinitrdを作るが、その際にBusyBoxの/initをRancherOS独自に実装したものに置き換えている。これはGoで書かれている。
Dockerの起動をしたりRancherOS用のコマンドを提供している。

置き換えは以下のような流れで行われる。

  1. script/buildの中でgo buildしてinitとなるバイナリ(bin/rancheros)を作る
  2. script/packageで作成したbin/rancherosをinitrd/initにコピーする
  3. 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の起動後の動きは見ていないのでそちらもチェックししたい。 本記事に加筆した。