Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

linux-container-from-scratch

컨테이너 격리 메커니즘, 프로세스 격리를 중심으로 구현

도커 안에서 컨테이너가 어떻게 동작하는지 이해하기 위해 Docker, containerd, runc 없이 Linux 커널 기능만으로 컨테이너를 직접 구현한 프로젝트입니다.
Prometheus + Grafana + Loki 기반 모니터링 스택을 함께 구성해 격리 상태를 실시간으로 관찰합니다.


아키텍처

컨테이너는 격리된 프로세스입니다. 리눅스의 세 가지 기능으로 프로세스를 제어합니다.

  ├── Linux Namespace          프로세스가 무엇을 "보는지"  (격리)
  │     ├── PID namespace      독립적인 프로세스 트리
  │     ├── Mount namespace    파일시스템 뷰 분리
  │     └── UTS namespace      hostname 분리
  │
  ├── cgroups v2               프로세스가 얼마나 "쓰는지"  (리소스 제한)
  │     
  │
  └── seccomp                  프로세스가 무엇을 "할 수 있는지"  (syscall 필터링)
        reboot, ptrace, mount/umount2 등 차단
        chmod 계열은 SCMP_ACT_LOG → Loki 수집

[monitoring/]
  ├── custom_exporter.py  컨테이너 상태 → Prometheus 메트릭 (포트 8000)
  ├── Prometheus          메트릭 수집
  ├── Grafana             대시보드 시각화
  └── Loki + Promtail     seccomp 차단 로그 수집

구현 기술

프로세스 격리 — Linux Namespaces

unshare --pid --fork --mount --uts 로 세 가지 namespace를 동시에 분리합니다.

  • PID namespace: 컨테이너 내부 프로세스는 PID 1부터 시작. 호스트에서는 다른 PID로 보임
  • Mount namespace: 컨테이너 내부 마운트 조작이 호스트에 영향을 주지 않음
  • UTS namespace: hostname mini-container 설정이 호스트 hostname과 독립적으로 동작

namespace unshare 코드

파일시스템 격리 — OverlayFS

lower/   읽기전용 베이스 레이어 (busybox 바이너리 + 기본 디렉토리)
upper/   컨테이너 내 변경사항만 기록되는 레이어
merged/  컨테이너가 실제로 보는 합성 뷰

컨테이너 종료 후 upper/ 디렉토리만 확인하면 컨테이너 내부에서 발생한 변경사항을 diff처럼 볼 수 있습니다.

리소스 제한 — cgroups v2

항목 제한
CPU 50% (cpu.max = 50000 100000)
메모리 256MB
PID 최대 32개
cpuset 전체 코어의 절반만 허용
hugetlb 비활성화

cgroups 설정 코드

시스템 콜 필터링 — seccomp

libseccomp을 Python ctypes로 직접 바인딩해 seccomp.json 프로파일을 로드합니다.
차단 대상: reboot, kexec_load, ptrace, mount/umount2, swapon/swapoff
chmod 계열은 SCMP_ACT_LOG로 설정해 Loki로 로그를 수집합니다.


디렉토리 구조

demo/
├── container/
│   ├── minicontainer.sh   컨테이너 실행 (rootfs → OverlayFS → cgroups → namespace → seccomp)
│   ├── cleanup.sh         마운트 해제, cgroup 제거, 임시 디렉토리 삭제
│   ├── seccomp_helper.py  libseccomp ctypes 바인딩 + chroot 진입
│   └── seccomp.json       syscall 허용/차단 프로파일
└── monitoring/
    ├── custom_exporter.py  Prometheus 메트릭 서버 (포트 8000)
    ├── docker-compose.yml  Prometheus + Grafana + Loki + Promtail
    ├── prometheus.yml
    ├── promtail.yml
    └── grafana/

실행 방법

의존성 설치

apt install busybox-static libseccomp2 libseccomp-dev auditd

컨테이너 실행

sudo ./container/minicontainer.sh

모니터링 (별도 터미널)

# 메트릭 서버
sudo python3 monitoring/custom_exporter.py

# Prometheus + Grafana + Loki
cd monitoring && docker compose up -d
# Grafana: http://localhost:3000

정리

sudo ./container/cleanup.sh

모니터링 (Grafana 대시보드)

custom_exporter.py/proc, /sys/fs/cgroup을 직접 읽어 Prometheus 메트릭으로 노출합니다.

패널 메트릭 설명
컨테이너 상태 container_running 실행 중 / 중지
CPU 사용량 cgroup_cpu_usage_usec cgroup CPU 누적 사용 시간
CPU 제한선 cgroup_cpu_quota_percent 설정된 CPU 쿼터 (50%)
메모리 사용량 cgroup_memory_current_bytes 현재 메모리 사용량
메모리 제한선 cgroup_memory_limit_bytes 설정된 메모리 한도 (256MB)
seccomp 로그 Loki type=1326 차단된 syscall 감사 로그

Grafana 대시보드


트러블슈팅

1. Namespace 내부 PID와 호스트 PID 불일치

모니터링을 위해 컨테이너 프로세스의 호스트 PID가 필요했습니다.
그런데 unshare --pid로 생성된 namespace 내부에서는 자신의 PID가 1로 보입니다.

/proc를 새로 마운트하기 전에 readlink /proc/self를 읽으면 호스트 proc을 통해 실제 PID를 얻을 수 있습니다.

# unshare 내부, proc 마운트 전에 실행
readlink /proc/self > $PID_FILE   # 호스트 PID 기록

# 이후 proc 마운트
mount -t proc proc $MERGED/proc

2. cgroups v2 컨트롤러 활성화 순서

cpu.max, memory.max를 child cgroup에 쓰면 No such file or directory 오류가 발생했습니다.
cgroups v2는 부모 cgroup의 cgroup.subtree_control에 컨트롤러를 먼저 활성화해야 child cgroup에 해당 파일이 생성됩니다.

# 수정 전: child cgroup에 바로 쓰기 시도 → 파일 없음
echo "50000 100000" > /sys/fs/cgroup/minicontainer/cpu.max

# 수정 후: 부모에서 컨트롤러 활성화 → child에 파일 생성됨
echo "+cpu"    > /sys/fs/cgroup/cgroup.subtree_control
echo "+memory" > /sys/fs/cgroup/cgroup.subtree_control
echo "50000 100000" > /sys/fs/cgroup/minicontainer/cpu.max

3. OverlayFS 언마운트 실패

cleanup.sh 실행 시 OverlayFS 언마운트가 device is busy로 실패하는 경우가 있었습니다.
merged/ 하위에 마운트된 proc, sys, dev를 먼저 언마운트해야 상위 OverlayFS를 해제할 수 있습니다.

# 순서가 중요
umount merged/proc
umount merged/sys
umount merged/dev
umount merged/        # OverlayFS

추가로 진행하고 싶은 부분

Kubernetes 환경에서 cpu.max의 한계와 해결 방향

이 프로젝트에서 구현한 cpu.max는 CFS quota 방식입니다.
Kubernetes에서 limits.cpu를 설정하면 내부적으로 동일하게 동작합니다.

문제:

100ms 주기마다 50ms만 CPU 사용 허용
→ 50ms를 다 쓰면 남은 50ms는 다른 컨테이너가 놀고 있어도 throttle
→ 실제 운영에서 P99 latency 급등으로 이어짐

해결 방향:

1단계: cpu.weight 도입
       cpu.max (하드 캡) → cpu.weight (상대적 우선순위)
       여유 CPU가 있으면 더 사용하고, 경합 시 가중치대로 분배
       → Kubernetes requests가 내부적으로 하는 일

2단계: CFS period 조정
       기본 100ms period를 줄여 throttle 주기를 단축
       → latency spike 완화

3단계: CPU Pinning (latency-critical 워크로드)
       cpuset exclusive로 특정 컨테이너에 코어 전용 할당
       → Kubernetes CPU Manager static policy와 동일한 원리

관찰 수단 고도화:

현재 /proc 폴링으로는 nr_throttled 숫자만 확인할 수 있습니다.
eBPF와 PSI를 도입하면 스케줄러 동작을 직접 관찰할 수 있습니다.

PSI  → cpu.pressure 수치로 실제 CPU 압박 정도 측정
eBPF → 어떤 프로세스가 언제 얼마나 throttle 당했는지 실시간 트레이싱

About

도커 핵심 메커니즘 구현

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages