여러 대의 컨테이너 호스트를 가지고 유저에게 컨테이너를 할당해 주는 서비스를 하는 경우를 생각해 보자. 유저는 컨테이너 호스트의 자원을 어느 정도 할당받아 사용하게 되는데, 모든 유저가 100%의 자원을 사용하는 것이 아니므로, 서비스 자원은 컨테이너 호스트의 자원을 유저에게 오버커밋하여 할당할 수 있다. 잘 일어나지는 않지만 만약 여기서, 어쩌다 다수의 유저가 자신의 자원을 100% 사용하게 되는 경우 컨테이너 호스트의 자원이 고갈되는데 이를 해결하기 위해 자원 상태를 주시하고 있다가 해당 유저의 컨테이너를 다른 컨테이너 호스트로 옮길 수 있다. 유저의 프로세스 상태와 작업 내용을 그대로 유지한 채로.

CRIU

CRIU를 통해 프로세스의 상태를 프리징하여 파일 등으로 저장하고, 다시 이것을 복원하여 기존 상태로 되돌리는 기능을 하도록 하는 프로젝트가 있는데, 1.10 버전부터 도커의 실험 기능(Experimental)으로 포함되어 있다. 언제 정식 기능으로 올라갈 지는 모르겠지만 어쨌든 해당 기능을 활성화하면 docker cli상에서 checkpoint 기능을 사용할 수 있게 된다. checkpoint를 만들고 복원하는 것 자체는 Docker에 포함된 것은 아니며 CRIU를 사용해 해당 기능을 구현하므로, apt-get으로 따로 설치하여야 한다. 설치는 다음과 같다.

$ apt-get install criu
...

CRIU도 코드를 보면 꽤 도움이 될 것이다. 꼭 Docker가 아니라도 쓸 곳이 은근히 있을 것 같다.

Docker

Docker Experimental 활성화

도커의 실행 옵션에 --experimental을 주는 것으로 가능하다. 현재 최신 도커 버전의 경우 /etc/docker/daemon.json에 옵션을 주는 것이 가능하므로 문서를 참고하여 해당 기능을 활성화하자.

$ cat /etc/docker/daemon.json 
{
    ...
    "experimental": true
    ...
    ...
}

도커를 재시작하면 이제 docker cli에서 checkpoint 관련 작업이 추가된다.

$ docker checkpoint

Usage:  docker checkpoint COMMAND

Manage checkpoints

Options:
      --help   Print usage

Commands:
  create      Create a checkpoint from a running container
  ls          List checkpoints for a container
  rm          Remove a checkpoint

Run 'docker checkpoint COMMAND --help' for more information on a command.

체크포인트를 생성해 보자.

$ docker checkpoint create --help

Usage:    docker checkpoint create [OPTIONS] CONTAINER CHECKPOINT

Create a checkpoint from a running container

Options:
      --checkpoint-dir string   Use a custom checkpoint storage directory
      --help                    Print usage
      --leave-running           Leave the container running after checkpoint

$ docker ps
CONTAINER ID        IMAGE         COMMAND                  CREATED             STATUS              PORTS               NAMES
32e83844770c        supervisord   "tini -- /usr/bin/..."   5 days ago          Up 5 days                               cublr_test_0000001.cublr.com

$ docker checkpoint create --checkpoint-dir /tmp cublr_test_0000001.cublr.com checkpoint_test_001
checkpoint_test_001

$  docker ps -a
CONTAINER ID        IMAGE         COMMAND                  CREATED             STATUS                        PORTS               NAMES
32e83844770c        supervisord   "tini -- /usr/bin/..."   5 days ago          Exited (137) 12 seconds ago                       cublr_test_0000001.cublr.com

$ docker checkpoint ls --checkpoint-dir /tmp cublr_test_0000001.cublr.com
CHECKPOINT NAME
checkpoint_test_001

$ cd /tmp/32e83844770ca56402f6f3fdcbf32b1a4deed1d926787c0cb8de09ca558d7073/checkpoints/checkpoint_test_001

$ ls -la
total 19456
drwxr-xr-x 3 root root     4096 Sep 17 18:06 .
drwxr-xr-x 3 root root     4096 Sep 17 18:06 ..
-rw-r--r-- 1 root root     4892 Sep 17 18:06 cgroup.img
-rw-r--r-- 1 root root      157 Sep 17 18:06 config.json
-rw-r--r-- 1 root root      959 Sep 17 18:06 core-10.img
...

drwxr-xr-x 2 root root     4096 Sep 17 18:06 criu.work
-rw-r-xr-x 1 root root       46 Sep 17 18:06 descriptors.json
-rw-r--r-- 1 root root      188 Sep 17 18:06 eventfd.img
-rw-r--r-- 1 root root      510 Sep 17 18:06 eventpoll.img
-rw-r--r-- 1 root root       44 Sep 17 18:06 fdinfo-2.img
...

-rw-r--r-- 1 root root       18 Sep 17 18:06 fs-10.img
...

-rw-r--r-- 1 root root       34 Sep 17 18:06 ids-10.img
...

-rw-r--r-- 1 root root      282 Sep 17 18:06 inetsk.img
-rw-r--r-- 1 root root       42 Sep 17 18:06 inventory.img
-rw-r--r-- 1 root root      248 Sep 17 18:06 ip6tables-9.img
-rw-r--r-- 1 root root       82 Sep 17 18:06 ipcns-var-10.img
-rw-r--r-- 1 root root      455 Sep 17 18:06 iptables-9.img
-rw-r--r-- 1 root root     1171 Sep 17 18:06 mm-10.img
...

-rw-r--r-- 1 root root     2669 Sep 17 18:06 mountpoints-12.img
-rw-r--r-- 1 root root      204 Sep 17 18:06 pagemap-10.img
...

-rw-r--r-- 1 root root       22 Sep 17 18:06 pagemap-shmem-29070.img
...

-rw-r--r-- 1 root root     4096 Sep 17 18:06 pages-10.img
...

-rw-r--r-- 1 root root      106 Sep 17 18:06 pipes-data.img
-rw-r--r-- 1 root root      350 Sep 17 18:06 pipes.img
-rw-r--r-- 1 root root      130 Sep 17 18:06 pstree.img
-rw-r--r-- 1 root root     3374 Sep 17 18:06 reg-files.img
-rw-r--r-- 1 root root    61180 Sep 17 18:06 seccomp.img
-rw-r--r-- 1 root root      910 Sep 17 18:06 sigacts-10.img
...

-rw-r--r-- 1 root root      399 Sep 17 18:06 tmpfs-dev-49.tar.gz.img
...

-rw-r--r-- 1 root root      650 Sep 17 18:06 unixsk.img
-rw-r--r-- 1 root root       26 Sep 17 18:06 utsns-11.img

체크포인트 생성 옵션에 –checkpoint-dir을 같이 입력하였다. 체크포인트의 내용이 저장되는 경로는 다음과 같았다.

/tmp/CONTAINER_ID/checkpoints/CHECKPOINT_NAME

컨테이너에 대한 여러 정보를 파일로 만들어 저장한다. json을 제외한 나머지 파일들은 모두 바이너리 파일이다.

$ cat descriptors.json
["pipe:[134276]","pipe:[134277]","pipe:[134278]"]
$ cat config.json
{"created":"2017-09-17T18:06:27.894891643+09:00","name":"checkpoint_test_001","tcp":true,"unixSockets":true,"shell":false,"exit":true,"emptyNS":["network"]}

그러면 컨테이너를 한번 복원해 보자. docker start 명령에 checkpoint를 사용할 수 있다. 이미 이전에 컨테이너의 체크포인트를 만들 때 스탑되었던 컨테이너에 그대로 복원할 것이다.

$ docker start --help

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

Start one or more stopped containers

Options:
  -a, --attach                  Attach STDOUT/STDERR and forward signals
      --checkpoint string       Restore from this checkpoint
      --checkpoint-dir string   Use a custom checkpoint storage directory
      --detach-keys string      Override the key sequence for detaching a container
      --help                    Print usage
  -i, --interactive             Attach container's STDIN

$  docker ps -a
CONTAINER ID        IMAGE         COMMAND                  CREATED             STATUS                        PORTS               NAMES
32e83844770c        supervisord   "tini -- /usr/bin/..."   5 days ago          Exited (137) 22 minutes ago                       cublr_test_0000001.cublr.com

$ docker start --checkpoint-dir /tmp/32e83844770ca56402f6f3fdcbf32b1a4deed1d926787c0cb8de09ca558d7073/checkpoints --checkpoint checkpoint_test_001 cublr_test_0000001.cublr.com

$ docker ps
CONTAINER ID        IMAGE         COMMAND                  CREATED             STATUS              PORTS               NAMES
32e83844770c        supervisord   "tini -- /usr/bin/..."   5 days ago          Up 6 seconds                            cublr_test_0000001.cublr.com

$ docker exec cublr_test_0000001.cublr.com ps -ef
PID   USER     TIME   COMMAND
    1 root       0:00 tini -- /usr/bin/supervisord
    7 root       0:00 {supervisord} /usr/bin/python2 /usr/bin/supervisord
   10 root       0:00 nginx: master process /usr/sbin/nginx -g daemon off;
   11 root       0:00 /usr/bin/gotty -a 0.0.0.0 -p 80 --reconnect --reconnect-time 10 -w /bin/bash
   13 nginx      0:00 nginx: worker process
   15 nginx      0:00 nginx: worker process
   16 nginx      0:00 nginx: worker process
   17 nginx      0:00 nginx: worker process
   21 root       0:00 ps -ef

컨테이너가 다시 Running 상태로 전환되었고, 프로세스가 그대로 존재하고 있음을 알 수 있다.

제약사항

컨테이너 마이그레이션을 위한 몇 가지 테스트와 조사를 토대로 다음의 제약사항을 확인하였다.

프로세스 1번과 1번의 자식 프로세스들만 체크포인트로 복원할 수 있다.

정확히는 CMD/ENTRYPOINT만 다시 실행하여 복원하는 것으로 보인다. 따라서 docker exec 등으로 실행한 프로세스는 복원할 수 없다. 그리고 복원할 때마다 PID가 변경되는 것으로 보아 아예 그대로 덤프를 떠서 복원하는 것은 아니고, 해당 프로세스들을 다시 실행한 다음 정보를 채워넣는 것으로 보인다.

메모리는 덤프하나 추가된 파일은 가져가지 않는다.

예를 들어 A이미지에서 생성된 AA, AB컨테이너가 있다고 가정하자. AA에서 실행 어플리케이션 ping을 다운로드하고 AA를 덤프했을 경우 이를 컨테이너 AB에 복원하더라도 ping이 없으므로 실행되지 않는다. 따라서 컨테이너의 내용이 변경될 수 있는 경우 해당 컨테이너의 스토리지 또한 다른 컨테이너 호스트로 이동해야 할 것이다.

/dev/console이 존재하면 checkpoint를 만드는 데 실패한다.

즉 컨테이너 생성시에 tty: false를 주어야 한다.

컨테이너 실행시 –security-opt=seccomp:unconfined 필요

마지막 두 개는 이 곳의 설명을 확인하자.

이유없이 실패하는 경우가 발생

컨테이너 호스트 3대에서 순서대로 계속 마이그레이션을 진행할 경우 많은 횟수 시도 후에 마이그레이션이 실패하는 경우가 발생했다. 모든 컨테이너가 대략 5회 이상 시도 후에 마이그레이션이 실패했는데, 원인을 딱히 발견하기 어려웠다. 이를 서비스에 사용하기 위해서는 조금 더 안정적으로 기능이 업그레이드 되어야 할 것이다. 반면 대략 5회 이하일 경우 예외없이 모두 성공하였는데, 적어도 1~2번은 컨테이너를 마이그레이션 할 수 있다는 의미이기도 했다. 자세한 내용은 더 많은 실험 후에 결론내야 할 듯.

POC 구성과 테스트

간단하게 라이브 마이그레이션을 위한 POC를 구성해 보자.

poc_environment

  • 컨테이너 호스트 두 대가 준비되어 있다.
  • 이 두 대의 컨테이너 호스트는 NFS 스토리지를 마운트하고 있다.
  • 컨테이너 호스트 1에 있는 컨테이너의 checkpoint를 생성한다. 생성된 체크포인트는 NFS 스토리지에 저장한다.
  • NFS 스토리지에 저장된 checkpoint를 사용하여 컨테이너 호스트 2에 컨테이너를 복원한다.
  • DNS 서버에서 변경된 컨테이너의 IP를 재등록한다.

DNS 서버 없이 진행한다면 변경된 IP를 따로 확인하여 그 곳으로 접속해야 하기 때문에 IP를 직접 확인하는 번거로운 과정이 필요할 것이다.

컨테이너 호스트 #1

먼저 컨테이너 호스트 #1에 컨테이너를 생성한다.

$ docker ps -a
CONTAINER ID        IMAGE                  COMMAND               CREATED             STATUS              PORTS               NAMES
bfa5e73ed61d        cublr/ubuntu-ssh   "/usr/sbin/sshd -D"   39 seconds ago      Up 37 seconds                           realtest-001-pty.cublr.com

컨테이너에는 openssh-server, nginx(http)와 gotty(websocket)을 설치했으며 컨테이너에 ssh 접속하여 해당 프로그램을 실행하였다.

$ nginx &
$ gotty -a 0.0.0.0 -p 1025 --reconnect --reconnect-time 10 top &
$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 09:37 ?        00:00:00 /usr/sbin/sshd -D
root         7     1  0 09:38 ?        00:00:00 sshd: root@pts/0
root         9     7  0 09:38 pts/0    00:00:00 -bash
root        16     9  0 09:38 pts/0    00:00:00 nginx: master process nginx
www-data    17    16  0 09:38 pts/0    00:00:00 nginx: worker process
root        27     9  0 09:40 pts/0    00:00:00 gotty -a 0.0.0.0 -p 1025 --reconnect --reconnect-time 10 top
root        33    27  0 09:42 pts/1    00:00:00 top
root        34     9  0 09:42 pts/0    00:00:00 ps -ef

이제 컨테이너에서 빠져나와 컨테이너 호스트에서 다음의 명령으로 checkpoint를 생성한다. checkpoint-dir은 NFS 스토리지 경로를 주었다.

$ docker checkpoint create --checkpoint-dir /NFS realtest-001-pty.cublr.com check001
...

컨테이너의 체크포인트를 생성한 순간 컨테이너는 Exited상태로 전환된다. 즉, 이 시점에서 컨테이너가 다시 복원되기 전까지는 nginx/gotty 모두 사용이 불가능하다.

컨테이너 호스트 #2

컨테이너 호스트 #2에 같은 이름의 컨테이너를 Created 상태로 미리 준비하였다. 이 컨테이너에 체크포인트를 복원할 것이다.

$ docker ps -a
CONTAINER ID        IMAGE                  COMMAND               CREATED             STATUS              PORTS               NAMES
9b30548e0b8e        cublr/ubuntu-ssh   "/usr/sbin/sshd -D"   7 seconds ago       Created                                 realtest-001-pty.cublr.com

$ docker start --checkpoint check001 --checkpoint-dir /NFS/bfa5e73ed61da56402f6f3fdcbf32b1a4deed1d926787c0cb8de09ca558d7073/checkpoints realtest-001-pty.cublr.com

복원하여 start가 되는 순간 할당받는 컨테이너의 IP를 DNS서버에 재등록한다. 즉 외부에서는 똑같이 DNS쿼리를 통해 변경된 IP로 통신할 수 있다.

복원 후

복원된 컨테이너의 프로세스를 살펴보자.

$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 09:48 ?        00:00:00 /usr/sbin/sshd -D
root        16     1  0 09:48 ?        00:00:00 nginx: master process nginx
www-data    17    16  0 09:48 ?        00:00:00 nginx: worker process
root        27     1  0 09:48 ?        00:00:00 gotty -a 0.0.0.0 -p 1025 --reconnect --reconnect-time 10 top
root        39     1  0 09:50 ?        00:00:00 sshd: root@pts/1
root        41    39  0 09:50 pts/1    00:00:00 -bash
root        47    41  0 09:50 pts/1    00:00:00 ps -ef

이전 프로세스와 똑같이 복원되었음을 알 수 있으며, PID는 변경이 되었다는 것도 알 수 있다. 그렇다면 이제 복원 후 연결성을 살펴보자.

nginx(http)

웹 브라우저에서 새로 고침 후 다시 정상적으로 연결되었다.

gotty(websocket)

재연결이 자동으로 이루어지지 않았으며 새로 고침 후 정상적으로 연결되었다.

결론

Checkpoint/Restore를 통해 컨테이너의 마이그레이션이 가능하다. 이를 사용하면 서비스의 중단을 최소하하며 컨테이너를 우아하게 컨테이너 호스트에 재배치할 수 있을 것이다. 특히 컨테이너를 유저에게 할당하는 서비스의 경우, 이를 통해 자원 사용을 컨테이너 호스트에 균등하게 분배할 수도 있고, 한 곳에 몰아넣을 수도 있다.

외부에서 컨테이너에 접속하는 서비스의 경우, 마이그레이션 순간 유저의 연결이 끊어지는 것은 피할 수 없다. DNS가 변경되었으므로 클라이언트 쪽에서 이를 알아야 하는데, DNS 서버의 TTL 등을 조정하여 최대한 빨리 클라이언트가 알아챌 수 있도록 설정이 필요하며, 어플리케이션에서는 연결이 끊어졌을 시 재연결 프로세스를 확실하게 준비해 둔다면 유저의 입장에서는 순단 후 다시 정상적으로 컨테이너를 사용할 수 있을 것이다.