退職

新卒で入社したアリエル・ネットワークを退職します。
3月13日が最終出社日で、3月末までは有給消化期間です。
入社したのが2012年の4月なので、ちょうど在籍3年ということになります。
会社の休憩室で「ちょうど3年かー」みたいな話をしてたら本棚に「若者はなぜ3年で辞めるのか? 」って本がありました。
3年で辞める若者らしいです。とはいえ別に3年と辞めるのが偶然このタイミングだったというだけなので3年とかはどうでもいいです。

振り返ってみると、ほぼJavaScriptとRuby、Goを書いてました。Javaは10行くらいしか書きませんでした。
それと、開発だけじゃなくてお客さんのとこに行って話したりサポートしたり、結構いろんな事をやりました。
入社した時は開発だけしたいなーと思っていたけど、やってみるとそこから得られるものもあったのでよかったかなと。
後、やってよかったのは社内サービスを作ったことで、自分が欲しいものを隙間時間でちょこちょこ作ったのが他の人に使われるようになって楽しかった。

4月からは別の会社で働く予定です。相変わらず都内にいます。
CTOが次どこ行くのか聞いて回ってるって噂を聞いておもしろかった。

Facebookに投稿したらこんなことを言われたので作りました。こちらです。

2015-03-15-retired_1

まぁ作ったといっても適当に登録しまくってたのを整理しただけなんだけど。

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の起動後の動きは見ていないのでそちらもチェックししたい。 本記事に加筆した。