15 minute read

Virtualization

가상화는 컴퓨터의 자원을 여러 가상머신으로 나누어서 유휴자원의 이용률을 높이기 위한 전략이다.

Hypervisor 가상화방식

가상화기능이 탑재된 Hypervisor(type2)나 hypervisor OS(type1)을 통해 게스트 리소스를 생성하는 방식, 관리를 위한 컴퓨터나 콘솔이 필요하다.

전가상화(Full-Virtualization), IO 작업을 OS 차원의 하이퍼콜을 통해 성능을 높인 반가상화(Para-Virtualization) 방식이 존재한다.

최근엔 전가상화나 반가상화의 성능차이가 많지 않다고 한다.

Container 가상화

리눅스의 네임스페이스 기술, 유니온 마운트를 지원하는 파일시스템 (다른 OS는 별도의 가상머신 필요)을 사용하여 컴퓨터의 자원을 나누는기술이다.

컨테이너는 리눅스상의 다른 네임스페이스에 있는 프로세스일 뿐이라 성능이 좋다.

컨테이너는 새 인스턴스 (프로세스)를 실행하면 컨테이너가 사라지고 재시작 되는데, 생성된 신규 컨테이너는 이전 컨테이너와의 연결된 상태 정보가 없다. (무상태성)

🌙 🌙 🌙 🌙  컨테이너 기반 기술 🌙 🌙 🌙 🌙

cgroup (control group)

cgroup은 프로세스가 사용하는 자원을 제어하는 파일시스템으로, 리눅스 커널 모듈로서 cpu, memory, device 사용을 프로세스 그룹들에 대해 제한할수 있다.

서브시스템

특정 자원(예: CPU, 메모리)에 대한 cgroup 제어를 담당

계층구조

  • 상위 계층에서 설정한 규칙이 하위 계층에 상속된다. 이를 통해 상위 계층이 자원을 하위 계층에 분배하거나 제어하는 역활이 가능하다
  • 계층구조을 파일시스템의 디렉토리구조로 표현한다.
  • 여러 cgroup VFS 마운트 가능하다.

계층구조 예시

# /sys/fs/cgroup
├── cpu
│   ├── my_group
│   │   ├── tasks
│   │   ├── cpu.cfs_quota_us 
│   │   └── ...
│   ├── other_group
│   │   ├── tasks
│   │   ├── 
│   │   └── ...
│   └── ...
├── memory
│   ├── my_group
│   │   ├── tasks
│   │   ├── 
│   │   └── ...
│   ├── other_group
│   │   ├── tasks
│   │   ├── 
│   │   └── ...
│   └── ...
└── ...
  • /sys/fs/cgroup는 cgroup 파일 시스템을 마운트한 경로
  • cpu, memory 등은 각각의 서브시스템(subsystem)
  • my_group, other_group 등은 각 서브시스템 내에서의 계층
    • 현재 my_group의 cpu 사용제한은 cpu.cfs_quota_us 파일로 제어되고 있음
  • tasks 파일에는 각 그룹에 속한 프로세스들의 PID들이 담겨 있다

cgroup 사용예제

# my_group 디렉터리 생성 (이미 생성되어 있다면 생략)
mkdir /sys/fs/cgroup/cpu/my_group

# CPU 사용량 제한 설정 (예: 20%)
echo 20 > /sys/fs/cgroup/cpu/my_group/cpu.cfs_quota_us

# CPU 주기 설정 (예: 100000us = 100ms)
echo 100000 > /sys/fs/cgroup/cpu/my_group/cpu.cfs_period_us

# my_group에 프로세스 할당
echo <PID_of_process> > /sys/fs/cgroup/cpu/my_group/tasks

리눅스 네임스페이스

리눅스 네임스페이스(Linux namespaces)는 리눅스 커널에서 자체적으로 지원하는 가상화 기법이다.

네임스페이스는 프로세스 간에 리소스를 격리하고, 각각의 네임스페이스 내에서는 리소스가 격리된 것처럼 동작하도록 한다. 다양한 네임스페이스들이 있으며, 주로 다음과 같은 네임스페이스들이 사용된다.

  • PID 네임스페이스 (pid):
    • 각각의 프로세스에게 고유한 프로세스 ID(PID)를 부여하며, 다른 네임스페이스 내의 프로세스는 서로 다른 PID를 가진다. 따라서 프로세스 간에는 PID가 겹치지 않는다.
    • 디폴트로 리눅스에서는 1번 프로세스(init)에 할당되어있는 네임스페이스들을 자식 프로세스들이 모두 공유해서 사용하는 구조로 이루어져있다.
  • 마운트 네임스페이스 (mnt):
    • 파일 시스템을 격리하며, 각각의 마운트 네임스페이스는 독립적인 파일 시스템 계층 구조를 가진다. 따라서 서로 다른 마운트 네임스페이스 내의 프로세스는 서로 다른 파일 시스템을 볼 수 있다.
  • 네트워크 네임스페이스 (net):
    • 네트워크 인터페이스, IP 주소, 라우팅 테이블 등을 격리하며, 각각의 네트워크 네임스페이스는 독립적인 네트워크 스택을 가진다. 따라서 서로 다른 네트워크 네임스페이스 내의 프로세스는 서로 다른 네트워크 환경을 가진다.
  • IPC 네임스페이스 (ipc):
    • Inter-Process Communication(IPC) 리소스를 격리한다. 서로 다른 IPC 네임스페이스 내의 프로세스는 메시지 큐, 세마포어 등과 같은 IPC 메커니즘을 통해 통신할 수 있다.
  • UTS 네임스페이스 (uts):
    • 호스트 시스템의 호스트 이름 및 도메인 이름을 격리한다. 각각의 UTS 네임스페이스 내에서는 독립된 시스템 식별자를 가진다.
  • 유저 네임스페이스 (user):
    • 사용자 및 그룹 ID를 격리한다. 각각의 유저 네임스페이스 내에서는 서로 다른 사용자 및 그룹을 나타내는 ID가 겹치지 않는다.
    • 유저 네임스페이스는 중첩이 가능하다

네임스페이스 예제

# 1번 프로세스에서 사용하는 네임스페이스 고유 id
$ ls -al /proc/1/ns
total 0
dr-x--x--x 2 root root 0 Jan 31 03:47 .
dr-xr-xr-x 9 root root 0 Jan 24 14:46 ..
lrwxrwxrwx 1 root root 0 Jan 31 03:47 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Jan 31 03:47 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Jan 31 03:47 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 Jan 31 03:47 net -> 'net:[4026531993]'
lrwxrwxrwx 1 root root 0 Jan 31 03:47 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 31 03:47 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 31 03:47 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Jan 31 03:47 uts -> 'uts:[4026531838]'

# namespace를 만드는 예제
# 새로운 bash 쉘을 실행하면 새로운 네임스페이스들을 적용한다.
# If program is not given, then "${SHELL}" is run (default: /bin/sh).
#
# fork 옵션을 안주면 exec을 통해 unshare 프로세스가 bash로 대체되어 새로운 pid 네임스페이스의 1번 프로세스가 된다.
unshare --mount --uts --ipc --net --pid --fork bash

# unshare process와 새로운 쉘 프로세스가 bash로부터 fork되어 실행되었음을 확인한다.
# 새로운 쉘 프로세스 번호 확인후 변경된 네임스페이스 확인
[root@ip-172-31-34-61 ~]# ps
    PID TTY          TIME CMD
   2540 pts/0    00:00:00 sudo
   2542 pts/0    00:00:00 bash
   2565 pts/0    00:00:00 unshare
   2568 pts/0    00:00:00 bash
   2796 pts/0    00:00:00 ps

[root@ip-172-31-34-61 ~]# ls -al /proc/2568/ns
total 0
dr-x--x--x. 2 root root 0 Jan  7 03:33 .
dr-xr-xr-x. 9 root root 0 Jan  7 03:27 ..
lrwxrwxrwx. 1 root root 0 Jan  7 03:33 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx. 1 root root 0 Jan  7 03:33 ipc -> 'ipc:[4026532297]'
lrwxrwxrwx. 1 root root 0 Jan  7 03:33 mnt -> 'mnt:[4026532295]'
lrwxrwxrwx. 1 root root 0 Jan  7 03:33 net -> 'net:[4026532299]'
lrwxrwxrwx. 1 root root 0 Jan  7 03:33 pid -> 'pid:[4026532298]'
lrwxrwxrwx. 1 root root 0 Jan  7 03:33 pid_for_children -> 'pid:[4026532298]'
lrwxrwxrwx. 1 root root 0 Jan  7 03:33 time -> 'time:[4026531834]'
lrwxrwxrwx. 1 root root 0 Jan  7 03:33 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx. 1 root root 0 Jan  7 03:33 user -> 'user:[4026531837]'
lrwxrwxrwx. 1 root root 0 Jan  7 03:33 uts -> 'uts:[4026532296]'

# hostname 은 namespace 생성시 copy되므로 원하는값으로 변경한다.
# hostname 변경후 새로운 쉘로 exec를 통해 프로세스를 덮어쒸어버린다.
hostname tuna
exec bash

unshare —fork option deepdive

—fork 없이 unshare를 실행하면 exec을 통해 bash는 현재 “unshare” 프로세스와 동일한 pid를 갖게 된다.

unshare 프로세스는 공유 해제 시스템 호출을 호출하고 새 pid 네임스페이스를 생성하지만, 현재 unshare 프로세스는 새 pid 네임스페이스에 없다. 리눅스 커널에선 프로세스 A가 새 네임스페이스를 생성시 프로세스 A 자체는 새 네임스페이스에 넣지 않고 프로세스 A의 하위 프로세스만 새 네임스페이스에 넣는다.

unshare -p /bin/bash

즉 bash의 첫 번째 하위 프로세스가 새 네임스페이스의 PID 1이 되는데, 현재 하위프로세스가 없거나 실행되었다 종료되어 1번프로세스가 없는 상태인것이다.

PID 1 프로세스에는 모든 고아 프로세스의 부모 프로세스가 되는 특별한 기능이 있다. 루트 네임스페이스의 PID 1 프로세스가 종료되면 커널은 패닉 상태가 된다. 특정 네임스페이스의 PID 1 프로세스가 종료되면 리눅스 커널은 disable_pid_allocation 함수를 호출하여 해당 네임스페이스의 PIDNS_HASH_ADDING 플래그를 정리한다.

리눅스 커널이 새 프로세스를 생성할 때 커널은 네임스페이스에 PID를 할당하기 위해 alloc_pid 함수를 호출하고, PIDNS_HASH_ADDING 플래그가 설정되지 않은 경우 alloc_pid 함수는 -ENOMEM 오류를 반환한다. 이는 “Cannot allocate memory” 를 발생시킬수 있다.

unshare --fork -p /bin/bash

—fork를 통해 unshare 프로세스에서 /bin/bash를 exec 하는것이 아니라 fork 한다. fork 된 bash 프로세스가 세 네임스페이스의 1번 프로세스가 되는것이다.

스냅샷과 컨테이너

checkpoint을 지정해 해당 시점의 데이터를 보존, 복구를 돕는 기능이다.

실제로 Container 내부에서 Read, 새로운 파일에 대한 Write, 기존 파일에 대한 Write와 같은 작업이 일어나면 Storage Driver에 따라서 Copy-on-Write (CoW) 혹은 Redirect-on-Write (RoW) 방식의 스냅샷을 사용한다.

그리고 CoW와 RoW에서 사용되는 스냅샷들위한 전용공간 Snapshot Pool이 있다.

기본적으로 Snapshot은 Immutable한 속성으로 Read-Only이다. 반면Snapshot Pool은 원본데이터에 Write 시에 기존 데이터 보존을 위해 이용되는데, CoW와 RoW에 따라서 동작되는 과정의 차이가 있다.

컨테이너의 스토리지는 스냅샷(체크포인트)를 clone하는 방식으로 동작한다.

COW (Copy on Write) 방식과 스냅샷

COW 방식에서 파일을 복사하면 파일들 전체 복사하는것이 아니라 참조(래퍼런스) 만한다. 이후 복사된 파일에서 변경사항이 발생하면 그때서 새로운 파일을 생성하게 된다.

이는 스냅샷(이미지)을 만드는 과정과 스냅샷의 동작과정에서도 똑같이 동작한다.

스냅샷을 만든 이후 기존 파일에 대해 Write(수정) request를 받게 된다면, 원본으로 유지 중인 파일을 복사하여 Snapshot Pool에 먼저 보존한다.

이를 통해 스냅샷은 원본 데이터들을 그대로 포인팅할수 있고, snapshot pool에 보존이 끝난후 원본데이터을 최신 데이터들로 갱신한다.

COW는 Write 요청에 의한 수정 사항 반영 시 Write가 2번(기존 스냅샷에서 데이터 보존 + 신규 데이터 반영) 진행된다. 총 2회의 Write를 수행하기 때문에 이에 대한 Overhead가 있는 편이다.

CoW는 write 에서 손해를 보지만 Container의 생성(스냅샷을 복제하여 파일시스템으로 마운트)과 삭제 같은 작업은 빠르다고 볼 수 있다.

ROW (Reference on write)과 스냅샷

RoW는 Snapshot으로 유지하고 있는 파일에 대해 Write 요청을 받게 되었을 때, 원본으로 유지 중인 파일을 변경할 수 없도록 Freeze하고 Snapshot Pool에 새로운 블록을 할당 받아 변경 사항을 기록하게 된다.

Snapshot Pool에 유지하고 있는 블록에는 변경된 내역만 diff 파일로써 저장한다.

RoW는 1회의 Write 작업만 이뤄지고 수정시 CoW처럼 파일을 복사를 하는 것이 아니기 때문에, 성능면에서는 이득이 있을수 있다.

하지만 변경 사항에 대한 반영을 위해 해당 파일을 찾을때 추가적인 시간이 요구되므로 Container의 생성과 삭제 같은 작업이 느리다고 볼 수 있다.

스냅샷과 컨테이너

그리고 Snapshot이 Immutable이라는 점을 염두에 두고 위에서 CoW와 RoW에 대해서 보았듯, 원본 파일에 대한 Snapshot과 snapshot을 복제한 파일시스템이 각각 Image와 Container에 대응되는것으로 추측된다.

컨테이너와 inode limit

컨테이너가 많아질경우 파일에 대한 정보를 가지고 있는 inode의 갯수가 모자를 가능성이 있다. 이를 예방하기 위해 커널 파라미터를 수정하는것을 고려해야 한다.

유니온 파일시스템 컨셉

그렇게 스냅샷을 참조하여 컨테이너를 만들고 거기서 스냅샷을 또만들고 또만드는일이 반복될수 있다.

예를 들어 파이썬 환경의 이미지를 만든후, 파이썬 스냅샷을 기반으로 django 운영을 위한 이미지를 만들고, django 위에 우리가 개발한 시스템을 올리는등 말이다.

이렇게 여러 레이어가 merge되어서 하나의 파일시스템이 되는 컨셉을 유니온 파일시스템이라 하며 컨테이너의 파일시스템의 근간이다.

Union FS에서 레이어(스냅샷) 수가 많으면 생기는 문제

각각의 레이어는 부모 레이어를 참조하고 있으므로, 레이어가 많고 찾으려는 데이터가 루트에 가까울수록 참조해야하는 레이어수가 많아져서 성능 저하가 발생할수 있다.

주요 스토리지 드라이버

AUFS

AUFS는 기존에 설명했던 Image와 Container의 구조와 굉장히 유사하다. mount된 Container는 Union Mount Point에 존재하는 Image들을 읽기전용으로 사용하게 된다.

Container를 사용하면서 Read-Only로만 수행되는 파일에 대해서 변경을 해야할 때 AUFS는 CoW 방식으로 Write를 수행하기 때문에, 해당 파일 전체를 Container의 Layer로 복사하고 복사된 파일을 변경하는 식으로 Write를 수행한다.

파일에 대해서 변경할 때 원본 파일 전체를 Container의 Layer로 복사해야 하므로, 복사할 파일을 찾아내는 과정이 필요하다.

이 때 AUFS는 파일을 Image의 가장 위 쪽에 존재하는 Layer부터 아래 쪽으로 파일을 찾아낸다.

따라서 찾으려는 파일이 가장 아래 쪽 Layer에 존재한다면 Write에 걸리는 시간이 오래 걸릴 수 있으며, 더군다나 Write를 수행하려는 파일의 크기가 크다면 그만큼 복사하는데 시간이 더 걸릴 수 있다.

이에 대해선 파일을 변경하려고 할 때 최초 1회에 대해서만 파일을 복사하기 때문에, 해당 파일을 여러 차례 변경하여 Write할 때는 기존에 복사된 파일을 이용한다.

Overlay2

다른 Storage Driver들과 달리 단일화된 Image의 Layer로 구성된다. 이 때문에 AUFS 보다 조금 더 나은 성능을 보인다.

CoW 방식으로 Write를 수행하기 때문에 Container의 생성과 삭제에 대한 작업이 빨라 PaaS (Platform as a Service)에 적합한 Storage Driver이다.

Container의 Storage 제한에 대한 설정이 가능하다. 다만 방법이 까다로운데 간단히 요약하면, Storage Driver를 OverlayFS로 이용하면서 Docker에 대한 데이터가 xfs에 저장되어 있는 경우에 xfs의 project quota라는 기능을 통해 Container의 Storage 제한이 가능하다.

# ubuntu image layers

$ ls -l /var/lib/docker/overlay2

total 24
drwx------ 5 root root 4096 Jun 20 07:36 223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7
drwx------ 3 root root 4096 Jun 20 07:36 3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b
drwx------ 5 root root 4096 Jun 20 07:36 4e9fa83caff3e8f4cc83693fa407a4a9fac9573deaf481506c102d484dd1e6a1
drwx------ 5 root root 4096 Jun 20 07:36 e8876a226237217ec61c4baf238a32992291d059fdac95ed6303bdff3f59cff5
drwx------ 5 root root 4096 Jun 20 07:36 eca1e4e1694283e001f200a667bb3cb40853cf2d1b12c29feda7422fed78afed
drwx------ 2 root root 4096 Jun 20 07:36 l

$ ls -l /var/lib/docker/overlay2/l

total 20
lrwxrwxrwx 1 root root 72 Jun 20 07:36 6Y5IM2XC7TSNIJZZFLJCS6I4I4 -> ../3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/diff
lrwxrwxrwx 1 root root 72 Jun 20 07:36 B3WWEFKBG3PLLV737KZFIASSW7 -> ../4e9fa83caff3e8f4cc83693fa407a4a9fac9573deaf481506c102d484dd1e6a1/diff
lrwxrwxrwx 1 root root 72 Jun 20 07:36 JEYMODZYFCZFYSDABYXD5MF6YO -> ../eca1e4e1694283e001f200a667bb3cb40853cf2d1b12c29feda7422fed78afed/diff
lrwxrwxrwx 1 root root 72 Jun 20 07:36 NFYKDW6APBCCUCTOUSYDH4DXAT -> ../223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7/diff
lrwxrwxrwx 1 root root 72 Jun 20 07:36 UL2MW33MSE3Q5VYIKBRN4ZAGQP -> ../e8876a226237217ec61c4baf238a32992291d059fdac95ed6303bdff3f59cff5/diff
# lowest layer

$ ls /var/lib/docker/overlay2/3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/

diff  link

$ cat /var/lib/docker/overlay2/3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/link

6Y5IM2XC7TSNIJZZFLJCS6I4I4

$ ls  /var/lib/docker/overlay2/3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/diff

bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
# second lowest layer

$ ls /var/lib/docker/overlay2/223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7

diff  link  lower  merged  work

$ cat /var/lib/docker/overlay2/223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7/lower

l/6Y5IM2XC7TSNIJZZFLJCS6I4I4

$ ls /var/lib/docker/overlay2/223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7/diff/

etc  sbin  usr  var

Devicemapper

레드햇 계열의 Linux에서 사용하기 위해 만들어진 Storage Driver이다. RoW 방식으로 Write를 수행하기 때문에 성능으로 이득을 볼 수 있지만, Container의 생성과 삭제에 대한 작업이 느린 편이다.

CentOS와 같은 운영체제에서 주로 사용했지만, 성능이 좋지 않아 Deprecated된 Storage Driver이다. 따라서 Docker Engine 버전 1.13.0 이전으로는 Devicemapper를, 이후로는 OverlayFS를 기본 Storage Driver로 이용한다.

컨테이너 실습

파일시스템(zfs)를 이용하여 파티션(이미지) 생성

이미지(스냅샷)을 만들 zfs 파일시스템을 만들어준다.

# zfs 및 스트레스 테스트 도구 설치
# 우분투 기준
apt install zfsutils-linux
apt install stress

# 준비된 파티션으로 zpool 생성
zpool create zpools /dev/sda5

# 이미지를 위한 zfs 파일시스템 생성 & 마운트
zfs create zpools/images -o mountpoint=/zpools/images
zfs create zpools/images/myImage -o mountpoint=/zpools/images/myImage

# 테스트에 필요한 유틸들을 이미지가 될 파일시스템에 복사
# 환경마다 /bin, /lib을 다루는 방식이 다를수 있어 확인후 사용
mkdir /zpools/images/myImage/bin
mkdir /zpools/images/myImage/usr
mkdir /zpools/images/myImage/usr/bin

cd /zpools/images/myImage/bin

cp /bin/mount /bin/umount /bin/df /bin/lsblk .
cp /bin/ls /bin/mkdir /bin/rm .
cp /bin/ping /bin/ip .
cp /bin/ps .
cp /bin/stress.

# usr/bin을 찾아가는 경우도 있어서 미리 예방
cd /zpools/images/myImage
cp bin/* usr/bin*

# 테스트에 필요한 라이브러리를 이미지가 될 파일시스템에 복사
# 환경마다 /bin, /lib을 다루는 방식이 다를수 있어 확인후 사용
cp -r /lib /zpools/images/myImage/lib
cp -r /usr/lib /zpools/images/myImage/usr/lib
cp -r /usr/lib64 /zpools/images/myImage/usr/lib64

root@ip-172-31-42-199:~# ls /zpools/images/myImage/
ls  ping  ps

파일시스템으로 부터 이미지를 만든후 컨테이너를 위한 파일시스템을 생성한다.

# snapshot 생성 (분기를 만들기 위한 read-only 포인트 생성)
zfs snapshot zpools/images/myImage@v1.0

# clone 생성 (컨테이너 생성)
zfs create zpools/containers -o mountpoint=/zpools/containers
zfs clone zpools/images/myImage@v1.0 zpools/containers/myContainer

# 컨테이너안의 파일 확인
root@tuna:~# ls /zpools/containers/myContainer/
bin  lib  lib64  ls  usr

# 컨테이너에서 실제 USED된 용량은 62K (copy on write)
root@tuna:~# zfs list
NAME                            USED  AVAIL     REFER  MOUNTPOINT
zpools                          605M  3.03G       24K  /zpools
zpools/containers                86K  3.03G       24K  /zpools/containers
**zpools/containers/myContainer    62K  3.03G      605M  /zpools/containers/myContainer**
zpools/images                   605M  3.03G       24K  /zpools/images
zpools/images/myImage           605M  3.03G      605M  /zpools/images/myImage

컨테이너를 위한 네임스페이스 생성

# namespace를 만들어주어야 한다.
unshare --mount --uts --ipc --net --pid --cgroup --fork bash

# hostname 은 namespace 생성시 copy되므로 원하는값으로 변경
# exec을 통해 세션이 분리되어 영향 최소화
hostname tuna
exec bash

pivot root 와 proc 파일시스템 마운트

새로운 루트 파일 시스템과 이전 루트 파일 시스템 간의 분리한다.

pivot_root를 사용하면 현재의 루트 파일 시스템과 새로운 루트 파일 시스템을 교체한다.

oldroot 디렉터리를 만들어서 이전 루트 파일 시스템을 임시로 마운트하면, 이전 루트와 새로운 루트를 서로 구분할 수 있다.

이렇게 함으로써 새로운 루트로 전환되더라도 이전 루트 파일 시스템을 안전하게 유지하면서 필요에 따라 사용할 수 있다.

다만 컨테이너에선 oldroot가 필요없고, 보안상 문제도 되므로 바로 umount해버린다.

chroot 명령은 보안상 문제로 잘 사용하지 않는다.

# mount namespace 역시 기본적으로 copy되므로 다른 디렉토리에 접근이 가능
# 컨테이너 디렉토리를 root 디렉토리로 변경
# exec chroot . /bin/sh 는 보안상의 문제로 쓰지 않는다.
cd /zpools/containers/myContainer/
mkdir oldroot
pivot_root . oldroot

# ls
bin  dev  etc  home  lib  media  mnt  proc  root  
run  sbin  srv  sys  tmp  usr  var oldroot

# current namespace의 proc filesystem 을 새로 마운트 해준다.
# 새로운 pid 네임스페이스에 대한 proc 파일 시스템이 마운트 된다.
# 필요시 네임스페이스의 의해 격리된 cgroup 파일시스템도 마운트할수 있다.
mkdir proc
mount -t proc proc /proc

# 현재 쉘의 pid가 1번인것을 확인
root@tuna:/# ps
    PID TTY          TIME CMD
      1 ?        00:00:00 bash
     39 ?        00:00:00 ps
root@tuna:/# echo $$
1

# old root는 필요없으므로 unmount 시켜버린다.
umount -l /oldroot

# mount 정보 확인
root@tuna:/# df -h
Filesystem                     Size  Used Avail Use% Mounted on
zpools/containers/myContainer  3.7G  611M  3.1G  17% /

root@tuna:/# mount
zpools/containers/myContainer on / type zfs (rw,xattr,noacl)
proc on /proc type proc (rw,relatime)

컨테이너를 위한 네트워크 가상화

다른 SHELL을 실행하여 컨테이너를 위한 network interface를 만든다

# 다른 쉘을 실행하여 가상 네트워크 인터페이스를 추가한다.
# peer name 옵션을 주면 인터페이스가 하나 더생성된다.
# 두 인터페이스는 서로 연결되어 통신이 가능
# 하나는 호스트 네임스페이스, 하나는 컨테이너 네임스페이스에서 사용할 예정이다.
CPID=$(pidof unshare)
ip link add name h$CPID type veth peer name c$CPID

root@ip-172-31-42-199:~# ip addr
......
3: c1632@h1632: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether f6:57:fc:69:7b:ff brd ff:ff:ff:ff:ff:ff
4: h1632@c1632: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether b6:ba:dc:e5:12:2f brd ff:ff:ff:ff:ff:ff

# 둘중 하나의 인터페이스를 격리된 network 네임스페이스로 설정
ip link set c$CPID netns $CPID

# 컨테이너의 쉘로 돌아가 ip addr로 확인해보자!
root@tuna:/# ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3: c1632@if4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether f6:57:fc:69:7b:ff brd ff:ff:ff:ff:ff:ff link-netnsid 0

인터페이스들에 통신을 위한 사설주소 부여

# host용 인터페이스에 사설주소 부여
# 사설 대역은 172.17.0.1/16 를 사용한다.
# 브릿지 장치를 사용하면 하나의 브릿지로 여러 컨테이너의 네트워크 인터페이스를 구성하는것이 가능
ip addr add 172.17.0.1/16 dev h$CPID
ip link set dev h$CPID up

root@tuna:/# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3: c1632@if4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether f6:57:fc:69:7b:ff brd ff:ff:ff:ff:ff:ff link-netnsid 0

# 컨테이너의 터미널에서, 새로만든 인터페이스 활성화 작업이 필요하다.
# ip주소 할당, default route 설정, up 을 진행한다.
# c1632 는 엔터페이스의 이름인데, eth0 로 바꿔준다.
# 네트워크 대역은 같이 맞춰준다. (172.17.0.1/16)
ip link set dev lo up
ip link set c1632 name eth0 up
ip addr add 172.17.0.2/16 dev eth0
# ip route add default via 172.17.0.1

# 컨테이너에서 호스트로 ping 테스트
root@tuna:/# ping 172.17.0.1
PING 172.17.0.1 (172.17.0.1) 56(84) bytes of data.
64 bytes from 172.17.0.1: icmp_seq=1 ttl=64 time=0.053 ms
64 bytes from 172.17.0.1: icmp_seq=2 ttl=64 time=0.035 ms

(옵션) 컨테이너에 인터넷 통신 기능 부여하기

# 호스트의 터미널에서 172.17.0.1/16 대역에서 공인IP로의 nat 규칙 추가
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 172.17.0.1/16 -j MASQUERADE

# 컨테이너의 터미널에서, 디폴트 게이트웨이 추가
ip route add default via 172.17.0.1

# 구글 dns 공인 ip주소로 ping 해보기
root@tuna:/# ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=103 time=29.2 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=103 time=29.3 ms

cgroup을 통해 컨테이너의 자원 제한

호스트 시스템에서 cgroup 에 myContainer 그룹 생성하기

# cgroup 작성을 도와주는 툴
apt install -y cgroup-tools

root@ip-172-31-42-199:~# cgcreate -a root -g cpu:myContainer
root@ip-172-31-42-199:~# cgcreate -a root -g memory:myContainer

root@ip-172-31-42-199:~# ls /sys/fs/cgroup/myContainer/
cgroup.controllers      cpu.max.burst          io.prio.class        memory.reclaim
cgroup.events           cpu.pressure           io.stat              memory.stat
cgroup.freeze           cpu.stat               io.weight            memory.swap.current
cgroup.kill             cpu.uclamp.max         memory.current       memory.swap.events
cgroup.max.depth        cpu.uclamp.min         memory.events        memory.swap.high
cgroup.max.descendants  cpu.weight             memory.events.local  memory.swap.max
cgroup.pressure         cpu.weight.nice        memory.high          memory.zswap.current
cgroup.procs            cpuset.cpus            memory.low           memory.zswap.max
cgroup.stat             cpuset.cpus.effective  memory.max           pids.current
cgroup.subtree_control  cpuset.cpus.partition  memory.min           pids.events
cgroup.threads          cpuset.mems            memory.numa_stat     pids.max
cgroup.type             cpuset.mems.effective  memory.oom.group     pids.peak
cpu.idle                io.max                 memory.peak
cpu.max                 io.pressure            memory.pressure

# myContainer 그룹의 프로세스들은 cpu 20%, mem 200MB로 제한된다.
echo 20000 > /sys/fs/cgroup/myContainer/cpu.max
echo 209715200 > /sys/fs/cgroup/myContainer/memory.max

컨테이너의 호스트 기준 PID를 myContainer 그룹에 추가

# 컨테이너의 호스트 기준 pid조회
root@ip-172-31-42-199:~# ps -a
    PID TTY          TIME CMD
   1465 pts/1    00:00:00 bash
   1632 pts/1    00:00:00 unshare
   **1633 pts/1    00:00:00 bash**
   1814 pts/3    00:00:00 bash
   2707 pts/3    00:00:00 ps

# cgroup의 myContainer 그룹의 cpu, memory 서브시스템에 pid추가
# 호스트 기준 네임스페이스에서의 컨테이너 pid
sudo cgclassify -g cpu,memory:myContainer 1633

컨테이너 부하테스트

# 컨테이너 환경에서 cpu 부하 시작
stress -c 1

# host 환경에서 top으로 최대 20% cpu 부하 확인
top - 06:08:52 up  6:05,  4 users,  load average: 0.00, 0.00, 0.00
Tasks: 153 total,   2 running, 151 sleeping,   0 stopped,   0 zombie
%Cpu(s): 20.3 us,  0.0 sy,  0.0 ni, 79.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :    949.7 total,    190.9 free,    220.8 used,    538.0 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.    566.2 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   3028 root      20   0    3704    128    128 R  19.9   0.0   0:06.22 stress
      1 root      20   0  101864  12408   7928 S   0.0   1.3   0:04.17 systemd

# 컨테이너 환경에서 mem 200MB 초과시 OOM으로 인해 터지는거 확인
root@tuna:/# stress --vm 1 --vm-bytes 198M
stress: info: [128] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
Killed
root@tuna:/# stress --vm 1 --vm-bytes 199M
stress: info: [130] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
Killed
root@tuna:/# stress --vm 1 --vm-bytes 200M
stress: info: [132] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [132] (416) <-- worker 133 got signal 9
stress: WARN: [132] (418) now reaping child worker processes
stress: FAIL: [132] (452) failed run completed in 0s

참고자료

https://stackoverflow.com/questions/44666700/unshare-pid-bin-bash-fork-cannot-allocate-memory

When do you need a default gateway? - YouTube

[Docker] 12. 스토리지 드라이버(storage driver) (tistory.com)

https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/resource_management_guide/sec-memory

Leave a comment