Menu

문서정보

목차

Docker 이미지와 컨테이너

도커 스토리지를 다루기 전에 도커 이미지와 컨테이너의 관계를 살펴봐야 할 것 같다. 도커는 하나의 이미지로 부터 하나 이상의 컨테이너가 실행 될 수 있다. 하나의 프로그램 이미지(프로그램 파일)로 부터 하나 이상의 프로세스가 실행되는 것과 같은 이치다. 아래는 프로세스의 실행을 묘사한 그림이다.

 프로세스

프로그램에서 프로세스가 실행되지만 원본 프로그램은 변경이 되지 않는다. 원본 프로그램이 변경되면, 프로그램의 동작이 달라지기 때문에 당연히 변경되면 안된다. 변경되는 데이터들은 원본 프로그램과 독립된 다른 파일의 형태로 저장이 된다.

이미지와 컨테이너의 관계도 마찬가지다. 이미지의 내용이 변경된다면, 컨테이너의 동작이 달라져 버린다. 따라서 이미지의 내용은 그대로이고 변경되는 내용만 따로 저장이 되야 할 것이다. 프로세스 처럼 작동하 될 것 같지만, 프로세스 보다는 까다롭다. 컨테이너는 보통 실행 파일과 데이터 파일이 함께 패키징 되기 때문이다. 예를 들어 mysql 컨테이너가 있다면, 데이터도 호스트 파일 시스템이 아닌 컨테이너에 저장이 된다 .

 컨테이너

Mysql 도커 이미지로 부터 mysql 컨테이너가 실행된다. mysql 컨테이너가 실행된다라고 해서 컨테이너라는 프로그램이 실행 되는 것이라고 생각 할 수 있지만 그렇지 않다. 그냥 mysql 프로세스가 만들어진다. 호스트 운영체제로 부터 격리된 프로세스라는 차이만 있다. 이제 mysql이 데이터를 써야하는데, 읽기 전용의 이미지에 데이터를 쓸 수는 없다. 도커는 이미지 파일을 수정하는 대신 이미지의 원본 파일과 달라진 데이터를 diff 형식으로 컨테이너 파일시스템에 저장을 한다.

이제 이미지원본 파일과 diff 파일을 merge 하면, 데이터를 복원 할 수 있을 것이다. 읽기 전용의 파일과 읽기/쓰기 파일을 분리하는 이런 파일 시스템을 유니온 파일 시스템(Union File system)이라고 부른다. 도커는 유니온 파일 시스템을 이용해서 이미지와 컨테이너를 관리 한다.

Union File System

앞서 도커는 읽기전용의 공통 파일과 읽기/쓰기 전용의 개인(private) 파일을 둔다고 했다. 하나의 이미지 파일로 부터 개인화된 컨테이너를 실행하기 위함 인데, 리눅스의 일반 파일시스템(ext3, ext4 같은)은 이러한 기능을 지원하지 않는다. 그래서 union mount의 구현물을 사용한다. 하나의 파일로 부터 파생되는 여러 파일들을 관리 할 수 있는 기술이라고 보면 된다. 이러한 파일 시스템을 Union File system이라고 하며, 도커는 리눅스의 일반 파일 시스템위에 union file system을 올려서 사용한다.

리눅스에서 주로 사용하는 union file system으로는 AUFS와 OverlayFS가 있다. AUFS는 2006년, OverlayFS는 2009년경에 만들어졌다. OverlayFS는 리눅스 커널 소스코드에 추가되서 직접 지원하고 있다. 도커는 AUFS와 OverlayFS를 모두 지원하고 있다.

AUFS

AUFS와 OverlayFS의 기본 개념은 같다. 도커 스토리지가 어떻게 작동하는지 이해하기 위해서 AUFS를 간단히 살펴보려한다. 우분투 리눅스 기준이다. 먼저 aufs-tools를 설치하자.
# apt-get install aufs-tools
몇 개 디렉토리를 만들어서 테스트를 했다.
# mkdir /tmp/app1-diff
# mkdir /tmp/aufs-root
# mkdir /home/data
# touch /home/data/orignal.txt
# mount -t aufs -o br=/tmp/app1-diff:/home/data none /tmp/aufs-root/
/tmp/aufs-root 디렉토리를 보면 orignal.txt 파일이 보일 것이다. /tmp/app1-diff 디렉토리는 아래와 같은 파일들이 보일 것이다.
# ls -al /tmp/app1-diff/
합계 16
drwxr-xr-x  4 root root 4096 10월 19 00:21 .
drwxrwxrwt 38 root root 4096 10월 19 00:22 ..
-r--r--r--  1 root root    0 10월 19 00:21 .wh..wh.aufs
drwx------  2 root root 4096 10월 19 00:21 .wh..wh.orph
drwx------  2 root root 4096 10월 19 00:21 .wh..wh.plnk
읽기전용 브랜치인 /home/data에 대한 변경사항이 여기에 기록이 된다. 변경 사항들은 .wh로 시작하는 디렉토리리와 파일들에 저장이 된다. 이제 /tmp/aufs-root/orignal.txt 파일의 내용을 수정 한후 /tmp/app1-diff의 내용을 보자.
# ls -al /tmp/app1-diff/
합계 20
drwxr-xr-x  4 root root 4096 10월 19 00:22 .
drwxrwxrwt 38 root root 4096 10월 19 00:22 ..
-r--r--r--  1 root root    0 10월 19 00:21 .wh..wh.aufs
drwx------  2 root root 4096 10월 19 00:21 .wh..wh.orph
drwx------  2 root root 4096 10월 19 00:21 .wh..wh.plnk
-rw-r--r--  1 root root   12 10월 19 00:22 orignal.txt
/home/data/orignal.txt 는 변경이 없는 것을 확인 할 수있을 것이다.

/tmp/app2-diff와 /tmp/aufs-root2 디렉토리를 만들어서 마운트를 해보자.
# mount -t aufs -o br=/tmp/app2-diff:/home/data none /tmp/aufs-root2/
/tmp/aufs-root2에는 /home/data/ 의 원본 내용을 확인 할 수 있을거다. 여기에서 변경된 내용은 /tmp/app2-diff에 쌓일 것이다.

이해하기 어려울 수도 있는데, /tmp/app1-diff와 /tmp/aufs-root1 은 application1이 사용하고, /tmp/app2-diff와 /tmp/aufs-root2 는 application2이 사용한다고 보면 이해가 쉬울 것이다. application1과 application2는 모두 /home/data에 있는 동일한 데이터로 부터 시작하지만, 각자 자신의 데이터를 따로 유지 할 수가 있다.

COW 파일 시스템

AUFS가 어떤 식으로 하나의 원본 파일로 부터 여러 애플리케이션을 위한 파일을 관리 할 수 있는지 개념을 잡았을 것이다. 정리하자면 아래와 같다.
  1. 원본파일은 그대로 두고 여기에 변경된 diff 정보만 따로 쌓는다.
  2. 원본파일 + diff 정보 연산을 해서 애플리케이션 파일을 복원한다.
위의 유니온 파일 시스템 외에도 COW(Copy on Write) 파일 시스템으로도 동일한 구현이 가능하다. 이 방식을 이해하기 위해서 COW 파일 시스템을 간단히 살펴보도록 하겠다.

지금 우리에게 "파일 시스템을 백업"하라는 미션이 떨어졌다. 어떻게 해야 할까. ext4 파일 시스템이라면 전체 파일 시스템을 복사하는 수 밖에 없을 것이다. 간단하다고 ? 그런데, 6시간 마다 한번씩 백업을 해야 한다면 이야기가 달라질 것이다. 6시간 마다 파일 시스템 전체를 백업하고 그걸 보관해야 한다고 가정해 보자. 암담할 것이다.

ZFS나 Btrfs와 같은 파일 시스템, QCOW와 같은 가상 블럭 이미지들은 COW를 이용해서 이 문제를 해결한다. 아래 그림은 COW 파일 시스템의 작동방식을 묘사하고 있다.

 COW 파일 시스템 - 1

현재 1, 2, 3, 4 의 데이터가 있다. 여기에 5, 6의 데이터가 추가된다면 기존 데이터에 대한 수정 없이추가 할 수 있다. 1,2,3,4를 원본으로 하고 추가 되는 데이터만 따로 보관하면 되므로 AUFS 와 같은 방식으로 사용 할 수 있을 것이다.

그러나 이 방식은 데이터 수정에 대한 고려가 없기 때문에 문제가 있다. 예컨데 2번 데이터를 포함한 파일을 수정할 경우 원본이 깨져버리게 될 것이다. COW 파일 시스템의 경우 변경된 데이터를 다른 위치에 복사(Copy)하고 수정된 파일을 새로운 블럭에 쓰는(Write)방식으로 이 문제를 해결 한다. 아래 그림을 보자.

 COW Update

  1. 2번 블럭 데이터가 수정이 됐다.
  2. 2번 블럭을 다른 위치로 복사(Copy)한다.
  3. 링크를 1->2->3->4에서 1->2`->3->4로 바꾼다.
  4. 해당 블럭에 수정된 데이터를 쓴다(Write).
결과적으로 1에서 4까지의 원본을 그대로 유지하면서, 변경된 파일은 따로 저장되므로 유니온 파일 시스템 처럼 사용 할 수 있다 . 이런 원리로 도커는 Btrfs와 ZFS같은 COW 파일 시스템을 도커 스토리지로 사용 할 수 있다.

데이터 볼륨

유니온 파일 시스템을 이용해서 하나의 원본 이미지로 부터, 여러 컨테이너를 만들 수 있게 됐다. 하지만 이 방식은 데이터의 양이 많아질 수록 느려진다는 단점이 있다. 변경된 정보가 따로 쌓이고, 원본과 변경된 정보를 더해서(merge)해서 데이터를 복원하기 때문이다.

이 문제를 해결해 보자. 컨테이너는 근본적으로 프로그램과 차이가 없다고 했으니, 프로그램이 어떻게 데이터를 처리하는지를 살펴보고 이를 응용하면 된다. 간단하다. 프로그램은 프로그램 원본 파일과 데이터 파일을 완전히 분리한다. 읽기와 쓰기가 자유로운 데이터 파일을 분리하는 것 처럼, 도커 역시 그렇게 분리해서 구성하면 된다. mysql 컨테이너를 운용해야 한다고 가정해 보자.
  1. mysql 프로그램 파일과 여러가지 유틸리티 파일들은 수정 할 필요가 없다. 따라서 이들은 유니온 파일 시스템에 유지하면 된다.
  2. 설정파일과 데이터베이스 파일들은 유니온 파일 시스템이 아닌 ext4 같은 리눅스 네이티브 파일 시스템에 저장을 한다.
이렇게 컨테이너의 유니온 파일 시스템이 아닌 외부 볼륨을 데이터 볼륨이라고 부른다. mysql 컨테이너에서 데이터를 쌓는 위치가 /opt/mysql/data 로 가정하면 아래와 같이 사용 할 수 있을 것이다.

 데이터 볼륨

변경되는 데이터들은 ext4 파일 시스템에 쌓이기 때문에 성능이슈에서 자유롭다. 아래는 데이터 뷸륨을 사용하는 예제다.
# docker run -it -v /home/mysql/user01:/opt/mysql ubuntu-mysql
# docker run -it -v /home/mysql/user02:/opt/mysql ubuntu-mysql

스토리지 백앤드별 특성

도커에서 지원하는 주요 스토리지 백앤드별 특징을 정리한다. 테스트에 사용한 도커 버전은 1.9.1 이다.

사용 중인 스토리지 백앤드 확인 하기

docker info명령으로 현재 사용 중인 스토리지 백앤드를 확인 할 수 있다.
# docker info
Containers: 2
 Running: 1
 Paused: 0
 Stopped: 1
Images: 3
Server Version: 1.11.2
Storage Driver: aufs
 Root Dir: /var/lib/docker/aufs
 Backing Filesystem: extfs
 Dirs: 46
 Dirperm1 Supported: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins: 
 Volume: local
 Network: host bridge ovs null
Kernel Version: 4.2.0-16-generic
Operating System: Ubuntu 15.10
OSType: linux
Architecture: x86_64
CPUs: 1
Total Memory: 992.9 MiB
Name: ubuntu
ID: VTAZ:6TEW:VV2G:QF37:TFGW:XCD6:IR3N:PMYR:6LKJ:2KLI:CID7:I4TO
Docker Root Dir: /var/lib/docker
Debug mode (client): false
Debug mode (server): false
Registry: https://index.docker.io/v1/
WARNING: No swap limit support
스토리지 드라이버와 루트 디렉토리(Root Dir)을 확인 할 수 있다. 유저가 컨테이너를 만들면, 루트 디렉토리 밑에 변경된 데이터들이 저장된다.

AUFS

유니온 파일 시스템이다. 우분투 계열에 설치된 도커가 기본적으로 사용하는 스토리지 백앤드다. 컨테이너를 만들어서 스토리지를 어떻게 관리하는 지 확인해 보기로 했다. 컨테이너를 만든 다음 테스트를 위해서 yundream.txt 파일을 하나 만들었다.
# docker run -it --name=aufs_test ubuntu /bin/bash
root@8832e395ed50:/# echo "Hello World" >> yundream.txt 
id가 8832e395ed501c131bda436183eeb221cafa445f5df2302fae68029963ca6566인 컨테이너가 만들어졌다. 컨테이너의 파일 시스템은 /var/lib/docker/aufs에 저장이 된다. 1.9.x 이하 버전에서는 aufs/컨테이너이름의 디렉토리 밑에 저장이 돼서 찾기가 쉬웠으나, 이후 버전 부터는 다른 이름으로 저장이 돼서 찾기가 좀 힘들어졌다.

컨테이너 파일 시스템의 위치는 /var/lib/docker/images/aufs/layerdb/mounts/<컨테이너ID>/mount-id 파일에 기록 된다.
# cat mount-id 
b499be87f3baaa258c101b660bf3b8338d0620dc23b3b84a3e76e27387be08fc
컨테이너 파일 시스템의 이름을 찾았다. 이제 /var/lib/docker/aufs/diff/b499be87f3baaa258c101b660bf3b8338d0620dc23b3b84a3e76e27387be08fc로 이동해서 파일 목록을 확인해보자.
# ls -al
total 24
drwxr-xr-x  4 root root  4096 Oct 24 00:42 .
drwx------ 48 root root 12288 Oct 24 00:41 ..
-r--r--r--  1 root root     0 Oct 24 00:41 .wh..wh.aufs
drwx------  2 root root  4096 Oct 24 00:41 .wh..wh.orph
drwx------  2 root root  4096 Oct 24 00:41 .wh..wh.plnk
-rw-r--r--  1 root root     12 Oct 24 00:42 yundream.txt
원본 이미지로부터 수정된 데이터가 diff 형태로 저장된 걸 확인 할 수 있다. 굳이 diff 파일 시스템의 위치를 찾아본 이유는 원격 파일 시스템을 구성하기 위한 가장 중요한 요소이기 때문이다. 컨테이너의 파일 시스템은 원본 이미지와 diff 파일의 조합으로 이루어진다. 그렇다면 diff 파일들만 원격에 두는 것으로 원격 스토리지를 만들 수 있을 것이다. 프로토콜로는 NFS, SMB, iSCSI 등을 이용하면 된다.

OverlayFS

비교적 최근에 나온 유니온 파일 시스템으로 AUFS와 비슷한 특성을 가지고 있다. AUFS와 비교해서 아래와 같은 차이가 있다. 최근에 나온 파일 시스템인 만큼 AUFS보다 더 좋은 특성을 가지고 있다. 최근 OverlayFS의 인기가 올라가는 중이지만, 오래된 역사를 자랑하는 AUFS 보다는 안정성이 떨어지는 것 아니냐는 평가를 듣고 있다. OverlayFS의 구조는 아래와 같다.

 overlayfs 구성

lowerdir는 읽기 전용의 이미지 레이어에 해당한다. 컨테이너는 이미지 레이어로 부터 만들어지며, 변경되는 정보는 컨테이너 레이어에 저장된다. 원본과 변경되는 정보는 컨테이너 마운트 레이어에서 merge해서 복원된다. AUFS를 사용하든 OverlayFS를 사용하든 별 상관은 없다.

커널이 OverlayFS를 지원하는지 확인해 보자.
# grep overlay /proc/filesystems 
nodev	overlayfs
nodev	overlay
도커를 overlayFs 모드로 실행했다.
# docker daemon --storage-driver=overlay
INFO[0000] Firewalld running: false                     
INFO[0000] Default bridge (docker0) is assigned with an IP address 172.17.0.1/16. Daemon option --bip can be used to set a preferred IP address 
....
docker info 명령으로 파일시스템을 확인 할 수 있다.
root@ubuntu:~# docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 1.11.2
Storage Driver: overlay
....
이제 컨테이너를 만들어보자.
# docker run -it --name overlay_test ubuntu /bin/bash
컨테이너 파일 시스템은 /var/lib/docker/overlay에 만들어진다. 정확한 파일 시스템의 이름은 /var/lib/docker/images/overlay/layerdb/mounts/<container-id>/mount-id 파일에서 찾을 수 있다. 컨테이너 파일 시스템을 보면 3개의 디렉토리를 확인 할 수 있다.
# ls -al
total 24
drwx------ 5 root root 4096 Oct 28 16:22 .
drwx------ 9 root root 4096 Oct 28 16:22 ..
-rw-r--r-- 1 root root   64 Oct 28 16:22 lower-id
drwxr-xr-x 1 root root 4096 Oct 28 16:26 merged
drwxr-xr-x 4 root root 4096 Oct 28 16:26 upper
drwx------ 3 root root 4096 Oct 28 16:22 work

ZFS

ZFS는 2005년 솔라리스에서 개발한 파일 시스템이다. 미러링, RAID-5, RAID-6와 유사한 레벨의 RAIDZ, COW를 기반으로 하는 증분스냅샷, 분산 페리티, NFS, SMB 익스포트, iSCSI, 파일 시스템에 대한 쿼터 설정, hot spare 등의 기능을 포함하고 있다. COW를 기반으로 하고 있기 때문에, 유니온 파일 시스템을 대신해서 백앤드 스토리지로의 적용이 가능하다. 네이티브 파일 시스템인 만큼 유니온 파일 시스템에 비해서 뛰어난 성능과 확장성, 운용성을 보장한다. ZFS 설치와 기본 운영에 대한 내용은 리눅스 native zfs 모듈 문서를 참고하자. 설치를 끝냈다고 가정해고 진행을 한다.

테스트를 위해서 3개의 디스크를 radiz1 으로 묶었다. 파일 시스템의 이름은 volumes다.
# zfs list
NAME      USED  AVAIL  REFER  MOUNTPOINT
volumes  67.9K  1.91G  24.0K  /volumes
여기에 도커를 위해서 "docker-root"라는 이름의 파일 시스템을 만들어서 /var/lib/docker로 마운트 했다. 이제 도커는 /var/lib/docker 파일 시스템을 사용하게 될 것이다.
# zfs create volumes/docker-root
# zfs list
volumes               122K  1.91G  24.0K  /volumes
volumes/docker-root  24.0K  1.91G  24.0K  /var/lib/docker
도커를 실행한다. --storage-driver 옵션을 이용해서 zfs를 백앤드 스토리지로 설정하면 된다.
# docker daemon --storage-driver=zfs
# docker info
Containers: 0
Images: 0
Server Version: 1.9.1
Storage Driver: zfs
 Zpool: volumes
 Zpool Health: ONLINE
 Parent Dataset: volumes/docker-root
 Space Used By Parent: 24552
 Space Available: 2045744250
이제 컨테이너를 만들면, 각 컨테이너별로 zfs 파일 시스템이 만들어진다.
# zfs list
NAME                                                                                        USED  AVAIL  REFER  MOUNTPOINT
volumes                                                                                     223M  1.69G  24.0K  /volumes
volumes/docker-root                                                                         223M  1.69G   946K  /var/lib/docker
volumes/docker-root/001d664e2dd48c995aa08d92bf7423129fcd20fd382cd7b0c8017f05faf61151       38.6K  1.69G   200M  legacy
volumes/docker-root/4976a0f2dc0324fb779f4d2f3e97cf5c9708d08dae8687ea0ce97c9b7376b93e        385K  1.69G   200M  legacy
volumes/docker-root/5c117067c385337de439f29ec469c985aeceefa224a343acfa851820332c1a8d       55.3K  1.69G   200M  legacy
volumes/docker-root/c45e542e5722d52b490f1a44654a881393397a10ea9d6639c08eae3bf63db97c       21.6M  1.69G   199M  legacy
volumes/docker-root/c45e542e5722d52b490f1a44654a881393397a10ea9d6639c08eae3bf63db97c-init  74.6K  1.69G   200M  legacy
volumes/docker-root/ebe73f29e6e12c86af7b8691fa34249926fe105cb808662d7aa26bcac79afe4f        200M  1.69G   200M  legacy
독립된 zfs 파일 시스템이기 때문에, 쿼터 설정, 스냅샷, 백업, 복원 등의 작업을 각 컨테이너 단위로 수행 할 수 있다.

원격에서의 볼륨 관리

원격 볼륨 관리의 필요성

COW 기반의 파일 시스템을 사용하건 유니온 파일 시스템을 사용하건, 컨테이너 기반의 클라우드 인프라를 만들려고 하면 결국 원격에서 파일시스템 혹은 볼륨을 관리해야 한다. 클라우드는 하드웨어를 추상화 한다. 컨테이너가 떠 있는 컴퓨터노드에 문제가 생긴다면, 이 컨테이너는 다른 컴퓨터노드에서 실행될 수 있어야 한다. 컨테이너 볼륨이 로컬 컴퓨터에 저장이 된다면, 로컬 컴퓨터에 문제가 생겼을 때 파일 시스템에 접근 할 수 없기 때문에 컨테이너를 실행 할 수 없다. 컨테이너의 가용성을 확보하기 위해서는 볼륨을 원격에 보관해야 한다.

가정

원격 볼륨 관리 방법을 고민하기에 앞서 데이터 볼륨만을 원격 볼륨에 저장한다고 제한을 둘 것이다. 컨테이너 볼륨에 있는 데이터를 수정 할 경우diff 파일이 만들어지는데, 이 것까지를 관리하게 되면, 원격 볼륨 관리가 쓸데없이 복잡해 진다. 이 경우 컨테이너를 시작하기 전에 대략 아래와 같은 과정을 거쳐야 한다.
  1. 컨테이너를 만든다.
  2. diff를 삭제하고, 원격에 있는 diff를 mount 한다.
  3. 컨테이너를 start 한다.
그리 복잡하지 않다고 생각 할 수도 있겠으나, 볼륨 관리를 위한 별도의 애플리케이션을 개발해야 한다. 애초에 컨테이너를 만드는 이미지라는게 읽기 전용의 프로그램 파일과 같은 개념이므로 아예 이미지 데이터는 건드리지 않도록 만들면 된다. 굳이 별 필요도 없는 소프트웨어를 만들어서 복잡도를 높일 필요는 없다.

클라우드는 복잡한 시스템이다. 쓸데 없는 기능은 가능한 제거하고, 있어야 하는 기능도 최대한 단순하게 해서 전체 시스템의 복잡도를 낮추려는 노력이 필요하다.

NFS 기반의 원격 스토리지 구성

NFS는 가장 무난한 선택이 될 것이다. 구성은 아래와 같을 것이다.

 NFS를 이용한 container Volume 구성

도커이미지는 각 노드에 공통으로 복사해서 사용한다. 예를들어 Mysql 컨테이너를 서비스 한다면, 모든 컨테이너 노드에 하둡 이미지를 복사해 놓는다. 이미지 관리를 위한 이미지 레포지토리는 나중에 따로 다룬다. 각 컨테이너 노드들은 스토리지 노드를 마운트해서 사용한다.

별다른 기술이 필요없는 단순한 구성이긴 하지만 귀찮음을 무릅쓰고 NFS 서버 & 클라이언트 환경을 만들어서 테스트를 해보기로 했다. VirtualBox를 이용해서 아래와 같은 테스트 환경을 만들었다.

 Container NFS 스토리지 테스트 환경

NFS 환경을 구축하자. 스토리지 노드에 NFS 서버를 설치한다.
# apt-get install nfs-kernel-server 
export할 디렉토리를 설정한다. export 디렉토리의 이름은 /containers/volumes 로 결정했다.
# cat /etc/exports
/containers/volumes 192.168.56.0/24(rw,sync,no_root_squash,no_subtree_check)
# service nfs-kernel-server restart
# exportfs
/containers/volumes
		192.168.56.0/24

컨테이너 노드에 nfs-common 클라이언트 모듈을 설치하고 마운트 했다.
# apt-get install nfs-common
# mount -t nfs 192.168.56.57:/containers/volumes /mnt/containers/
# mount | grep containers
192.168.56.57:/containers/volumes on /mnt/containers type nfs4
(rw,relatime,vers=4.0,rsize=131072,wsize=131072,namlen=255,hard,proto=tcp,
port=0,timeo=600,retrans=2,sec=sys,clientaddr=192.168.56.55,local_lock=none,addr=192.168.56.57)

이제 컨테이너 볼륨을 마운트된 디렉토리에 배치하면 된다. 컨테이너 노드 - 1에 이름이 yundream인 컨테이너를 만든 다음 컨테이너 노드 - 2에 올리는 방식으로 테스트를 진행했다. 데이터 볼륨을 위한 디렉토리의 이름은 컨테이너의 이름과 일치시켰다. 컨테이너의 이름은 yundream, 데이터 볼륨 디렉토리는 /mnt/containers/yundream 이다.
node-01# mkdir /mnt/containers/yundream
node-01# docker run -it --name=yundream -v /mnt/containers/yundream:/opt/data ubuntu /bin/bash
root@f88d7075c558:/# cd /opt/data
root@f88d7075c558:/opt/data# echo "Hello world" > README.md 
컨테이너 노드 - 2에서 컨테이너를 실행했다.
node-02# mkdir /mnt/containers/yundream
node-02# docker run -it --name=yundream -v /mnt/containers/yundream:/opt/data ubuntu /bin/bash
root@ac4712bcc3b8:/# cat /opt/data/README.md 
Hello world
스토리지 노드를 분리하는 것으로 컨테이너가 자유롭게 떠 다닐 수 있도록 구성했다.

이 테스트는 개념 검증 수준이다. 실제 서비스를 구성하기 위해서는 컨테이너 이름과 볼륨이름을 묶기 위한 컨테이너 오케스트레이션툴을 운용해야 할 것이다. 도커 swarm, docker kubernetes, MesOS 등등의 툴을 그대로 혹은 수정해서 사용 해야 한다.

블럭 디바이스 기반의 원격 스토리지 구성

iSCSI 혹은 NBD를 이용해서 컨테이너에 블럭스토리지를 제공하는 방법이 있다. 프로세스에 블럭스토리지를 제공하는 것은 좀 생소 할 수 있긴하다. 대신 아래의 장점이 있다. 특히 관리하기 쉽다는 것은 매우 중요한 장점이다. NFS를 이용해도 파일시스템을 격리하거나 쿼터를 설정하는 등의 일을 할 수는(ZFS On Linux 참고) 있다. 하지만 NFS를 이용한 관리는 매우 복잡하다는 것을 알게 될 거다. 파일 시스템이 늘어날 경우 커널 모듈이 제대로 마운트 자원을 해제하지 못해서 device busy가 뜨는 문제는 드물지 않게 발생한다. 메모리도 상당히 먹는다. ZFS의 경우 파일 시스템이 1000개가 늘어나면 파일 시스템 정보를 유지하는데 600M 이상의 메모리가 필요하다.

블럭 디바이스는 "하나의 파일"이기 때문에 이러한 모든 문제에서 자유롭다. NBD(Network Block Device)로 테스트를 했다. 블럭디바이스 파일타입으로 qcow2를 사용하기로 했다. qcow2를 이용하기 위해서 qemu 패키지를 설치했다.
# apt-get install qemu
qemu-img로 5G 크기의 qcow2 이미지를 만들었다.
# cd /containers/nbd
# qemu-img create -f qcow2 yundream.img 2G
Formatting 'yundream.img', fmt=qcow2 size=2147483648 encryption=off cluster_size=65536 lazy_refcounts=off refcount_bits=16
5G라고 해서 크기를 걱정할 필요는 없다. qcow2는 sparse file을 지원한다. 즉 실제 크기가 아닌 쓴 만큼만 공간을 차지한다. 파일 크기를 확인해 보자.
# ls -al yundream.img 
-rw-r--r-- 1 root root 197120 Nov  2 23:51 yundream.img
# file yundream.img 
yundream.img: QEMU QCOW Image (v3), 5368709120 bytes

이제 qemu-nbd를 이용해서 yundream.img 파일을 외부 네트워크에 export 하면 된다. 다만 여기에서 고려해야 할 점이 하나 있다. 리눅스는 network block device를 위해서 nbd-server와 nbd-client 패키지를 제공한다. qemu-nbd는 nbd-server에 대응하는 프로그램으로 nbd-server 대신 실행 된다. 그런데, nbd-client와 프로토콜에 있어서 약간의 차이가 있다. nbd-client 3.10 버전 부터는 새로운 스타일(newstyle)의 프로토콜을 사용하는데, qemu 패키지로 설치된 qemu-nbd는 오래된 스타일(oldstyle)의 프로토콜을 사용한다. 그래서 newstyle nbd 프로토콜을 지원하는 최신의 qemu를 git clone해서 직접 빌드했다. git 주소는 git://git.qemu-project.org/qemu.git 이다. 컴파일 하면 qemu-nbd 실행파일이 만들어진다. 앞서 만든 yundream.img 블럭 디바이스를 4000 포트로 export 했다.
# qemu-nbd --bind=0.0.0.0 --port=4000 yundream.img -x yundream

이제 컨테이너 노드에서 nbd-client로 연결해서 사용하면 된다. 컨테이너 노드로 이동해서 nbd-client를 설치한다.
# apt-get install nbd-client
root@ubuntu:~# modinfo nbd
filename:       /lib/modules/4.2.0-16-generic/kernel/drivers/block/nbd.ko
license:        GPL
description:    Network Block Device
......
nbd-client 프로그램을 이용해서 원격 블럭디바이스(yundream.img)를 연결한다.
# nbd-client -N yundream 192.168.56.57 4000 /dev/nbd0
Negotiation: ..size = 2048MB
bs=1024, sz=2147483648 bytes
fdisk로 파티션을 만든다. fdisk를 이용한 파티셔닝 방법은 Partitioning with fdisk를 참고한다. 이제 ext4 파일 시스템을 만들고, 마운트를 했다.
# mkfs.ext4 /dev/nbd0
# mount /dev/nbd0 /containers/volumes/yundream 
이렇게 만들어진 볼륨을 컨테이너 볼륨으로 실행하면 된다.
# docker run --name=yundream -it -v /containers/volumes/yundream:/opt/mysql ubuntu /bin/bash
root@db6879a07416:/# df -hT
Filesystem     Type   Size  Used Avail Use% Mounted on
none           aufs   6.5G  1.5G  4.7G  24% /
tmpfs          tmpfs  497M     0  497M   0% /dev
tmpfs          tmpfs  497M     0  497M   0% /sys/fs/cgroup
/dev/nbd0      ext4   2.0G  3.0M  1.8G   1% /opt/mysql
/dev/dm-0      ext4   6.5G  1.5G  4.7G  24% /etc/hosts
shm            tmpfs   64M     0   64M   0% /dev/shm
/dev/nbd0이 /opt/mysql로 마운트된 걸 확인 할 수 있다.

컨테이너 가상화 운영 환경에서는 파티셔닝까지 모두 끝난 qcow2를 만들어 두고, 새로운 컨테이너가 생길 때마다 복사(copy)해서 사용하면 된다. 이때 단순 복사 보다는 qcow2의 linked clone를 사용하면 디스크 공간을 효과적으로 사용 할 수 있을 것이다. 원본은 그대로 두고, 변경되는 데이터만 쌓는 방식으로 virtualbox를 비롯한 대부분의 가상화 툴들이 지원하고 있다.

 Virtualbox에서의 linked clone

정리

  1. 컨테이너 볼륨을 도커 이미지로 부터 완전히 분리한다. 즉 도커 이미지에 있는 파일들에 대한 어떤 수정도 없도록 만들자. 도커 이미지가 프로그램 파일이라는 관점에서 보면 자연스러운 접근이다.
  2. 도커 이미지에 있는 파일들에 대한 수정이 없다면, diff 파일도 만들지 않을 거다. 따라서 AUFS나 OverLayFS 어떤 것을 사용하더라도 상관 없다.
  3. 스토리지는 NFS나 블럭디바이스로 분리 한다. 특히 다이나믹한 서비스를 목표로 한다면, 반드시 분리해야 한다.