CRIUを使ってDockerコンテナのCR (一時停止/再開)をしてみる (コンテナのマイグレーションもあり)

# 自己紹介 & 募集
[CircleCI](https://circleci.com/)のKimです。CircleCIではDockerをサポートしていて、その関係で、今自分の中で一番熱いCRIUのことを[ブログ](http://blog.circleci.com/checkpoint-and-restore-docker-container-with-criu/)に書いたので日本語に訳してみました。

現在CircleCIでは日本からリモートで働ける[サポートエンジニア](https://jobs.lever.co/circleci/65cfaf2f-91fc-46dc-816d-a4d258cba8f1)を鋭意募集中です。完全フレックス制・コアタイムなし、無制限休暇、Clojureが使えるなど色々いいことがあるので、興味があればぜひ応募してみてください。質問などがあれば、yangkookkim@gmail.comまで連絡していただければ何でもお答えします。

# TL;DR: DockerでコンテナをCRするデモ

## コンテナをスタート
`$ export cid=$(docker run -d busybox tail -f /dev/null)` 

## コンテナを一時停止
`$ docker checkpoint $cid
7cc692f22c11` 

## コンテナが停止された
`$ docker ps --quiet`
<何も表示されない>

## コンテナを再開
`$ docker restore $cid
7cc692f22c11` 

## 再開した！!
`$ docker ps --quiet
7cc692f22c11`

# CRって何？CRIUって何？
CR (checkpoint and restart)はプロセスのメモリ状態をある状態でディスクに保存して、あとでそこからプロセスの状態を再開する技術です。

そして、[CRIU](https://github.com/xemul/criu)は元々はLXCコンテナをCRするために始まったプロジェクトです。DockerはLXCをコンテナのバックエンドとして使うことができるので、DockerコンテナをCRすることもできそうな気がしますよね？以前、実験した結果を[ここ](http://qiita.com/kimh/items/5af591dc5b22a7442ad0)で報告させてもらいましたが、残念ながらその時はDockerコンテナのCRはできませんでした。

それから、早１年。CRIUチームの多大な努力の甲斐あって、今回はDockerコンテナの再開に成功したので情報を共有しようと思います。

# CRIU Vagrant box
CRIUはまだDockerには完全にはマージされていないので、DockerコンテナのCRをするにはExperimental Featureを有効にしたDockerを使わないといけないといけません。また、特別のカーネルの設定が必要なのでカーネルの再構築をしないといけません。正直かなりめんどいです。

でも心配ありません！すぐに使えるVagrant boxを用意したので、それをダウンロードすればCRIUを試すことができます。

### Vagrant boxをスタートする
ダウンロードするVagrant boxをここでは**vg-1**と呼ぶことにします。以下のコマンドを実行してください。

```
vagrant box add https://atlas.hashicorp.com/kimh/boxes/criu
mkdir <path to vg-1>
cd <path to vg-1>
vagrant init kimh/criu
vagrant up
vagrant ssh
```

これで準備完了です。


### docker restore/checkpoint コマンド

今vagrant upした**vg-1**で動いているDockerはExperimental Featureが有効になっているDockerです。これを有効にすると普通のDockerはない**checkout**と**restore**というコマンドがあります。

```
$ docker checkpoint --help

Usage:  docker checkpoint [OPTIONS] CONTAINER [CONTAINER...]

Checkpoint one or more running containers
....
```

```
$ docker restore --help

Usage:  docker restore [OPTIONS] CONTAINER [CONTAINER...]

Restore one or more checkpointed containers
....
```

**vg-1**にはCRIUのコマンドもインストールされていて、`checkpoint` と `restore`はこのコマンドを使ってCRを実現します。

```
$ criu --help

Usage:
  criu dump|pre-dump -t PID [<options>]
  criu restore [<options>]
  criu check [--ms]
  criu exec -p PID <syscall-string>
  criu page-server
  criu service [<options>]
  criu dedup

....
```

さっそくDockerコンテナをCRしてみます。

### コンテナをスタートする

```
docker run \
  --name np \
  --rm \
  busybox:latest \
  /bin/sh -c \
  'i=0; while true; do echo $i; i=$(expr $i + 1); sleep 1; done'
```

上記のコマンドを実行するとbusyboxをダウンロードして数字を表示し続ける**number-printer**コンテナをスタートします。

### コンテナを一時停止する
コンテナを停止するには`checkpoint`コマンドを使います。number-printerコンテナはフォアグラウンドで実行されているので、別のターミナルを開いて以下のコマンドを実行してください。

```
docker checkpoint np
```

コンテナを停止すると、数字の出力が止まるはずです。`docker ps`にも表示されません。

### コンテナを再開する
今度は停止したコンテナを再開します。

`docker restore np`

再開が成功したらnumber-printerコンテナが数字の出力を再開するはずです。

# pause/unpaseとcheckpoint/restoreコマンドの違い
*"これってすでにdocker pause/unpaseコマンドでできるよね？"*って思った人がいるかもしれません。確かにさっきの例だと同じ事を`pause/unpause`コマンドでも実現することができます。`pause/unpase`も`checkpoint/restore`もコンテナを一時停止して再開することができるという点では似ています。二つのコマンドの違いは、`pause/unpase`はただ単にコンテナプロセスをメモリ上に保持したまま一時停止するのに対し、`checkpoint/restore`はコンテナのメモリ状態をディスクに保存するということです。

`checkpoint/restore`はメモリ状態をディスクに保存するということを活かせば色々面白いことがDockerでできるようになります。ここからは、Docker CRの具体的な使い道を紹介します。

# Docker CRの使い道 

### (1) 長時間起動するコンテナの途中復帰
コンテナは一般的には使い捨て/短命ですが、時には一つのコンテナを長時間走らせたいということもあると思います。例えば、円周率を１兆桁まで計算するプログラムをコンテナ上で走らせたかったとします。このような場合、不意なアクシデントでコンテナを停止しないようにしないといけません。なぜなら停止すると今までの計算結果が失われてしまうからです。もちろん、この問題を解決する方法は色々ありますが、CRIUを使えば定期的にコンテナの状態をディスクに保存して仮に停止されたとしても、途中からコンテナを復帰すれば最初から円周率を計算する必要はありません。

### (2) コンテナの高速起動
起動が遅いアプリケーションをコンテナで起動する場合、アプリケーションの起動速度に引っ張られるのでコンテナの利点が失われてしまいます。CRを使えば、アプリケーションが起動した状態を保存しておいて、今後はそこから再開することで高速にコンテナを起動することができます。

この使い道を実際にやってみます。説明のためにredisの起動にとても時間がかかるとします。（本当はredisはとても早い子ですよ！）

まずは普通にredisコンテナを起動します。

`cid=$(docker run -d redis)`

この記事の中ではredisは起動するまでに２０秒かかるとします。redisコンテナを起動して２０秒待った後、コンテナをチェックポイントします。

`docker checkpoint --image-dir=/tmp/redis $cid`

この保存したコンテナから再開するのですが、今回の例では保存したコンテナから複数のコンテナを再開するので**--force=true**オプションを使用して空のコンテナIDを渡します。

```
docker create --name=redis-0 redis
docker restore --force=true --image-dir=/tmp/redis redis-0
```

こうやってスタートした`redis-0`はすでにredisが起動した状態からのスタートなので２０秒待つ必要がありません。この方法のいいところは一気に複数のコンテナをスタートできるところです。

```
for i in 1 2 3 4 5; do
  docker create --name=redis-$i redis
  docker restore --force=true --image-dir=/tmp/redis redis-$i
done
```

本来であれば５つのredisコンテナを再開するには１００秒（２０秒 x ５）待たないといけないですが、この方法だと一瞬で全てのコンテナを起動することができます。

### (3) コンテナのマイグレーション
CRIUを使えばDockerコンテナのマイグレーションもできるようになります。マイグレーションを実際にやってみるには二つのVagrant VMが必要です。すででに**vg-1**は起動しているはずなので新たに**vg-2**を作成しましょう。

```
mkdir <path to vg-2>
cd <path to vg-2>
vagrant init kimh/criu
vagrant up`
```

**vg-2**が起動したら以下のコマンドを実行してください。これは、CRIUにバグがあって最低一つコンテナを起動しておかないとそれ以降のマイグレーションで失敗してしまうからです。

```
vagrant ssh -- 'docker run --name=foo -d busybox tail -f /dev/null && docker rm -f foo'
```

上記のコマンドを実行したら、**vg-1**上で**number-printer**コンテナを起動します。

```
docker run \
  -d \
  --name np busybox:latest \
  /bin/sh -c \
  'i=0; while true; do echo $i; i=$(expr $i + 1); sleep 1; done'
```

今回はバックグランドで起動したので出力を見るにはこうします。

```
$ docker logs -f np
1
2
3
4
5
....
```

それではこのコンテナをマイグレーションをしてみます。[マイグレーションのスクリプト]( https://gist.github.com/kimh/79f7bcb195466acea39a)を作成したのでまずはローカルマシンにダウンロードしてください。

```
curl -L -o docker-migrate.sh https://gist.githubusercontent.com/kimh/79f7bcb195466acea39a/raw/ca0965d90c850dcbe54654a6002678fff333d408/docker-migrate.sh
chmod +x docker-migrate.sh
```

このスクリプトは３つの引数を取ります。第一引数にはマイグレーションするコンテナの名前を指定します。今回だと**np**です。第二、三引数にはマイグレーション元と先のVagrant VMがあるディレクトリを指定します。

```
docker-migrate.sh np <path to vg-1> <path to vg-2>

Ex. /tmp/docker-migrate.sh np /Users/kimh/vagrant/vg1 /Users/kimh/vagrant/vg2
```

ちゃんと実行できましたか？実行できたら**vg-2**にログインして`docker logs -f np`してみてください。マイグレーションに成功していれば、**vg-1**で停止されたところから数字の出力が再開されているはずです。

![alt](http://blog.circleci.com/wp-content/uploads/2015/09/Migration_Succeeded.jpg)

Dockerコンテナのマイグレーションに成功しました！！

# まとめ

ざっとですが、CRIUを使ったDockerコンテナのCheckpoint/Restartを紹介しました。この記事を読んで、他にも面白い使い方を考えてもらえればと思います。

もっとCRIUやDocker CRについて知りたければ以下のサイトが役に立つのでぜひ見てみてください。

[CRIUのメインサイト](http://criu.org/Docker)
[CRIUをサポートしたDockerのフォーク (今回インストールするしたやつ)](https://github.com/boucher/docker/blob/cr-combined/experimental/checkpoint_restore.md)
[Kubernetesのブログ](http://blog.kubernetes.io/2015/07/how-did-quake-demo-from-dockercon-work.html)