Linux cgroups를 활용한 리소스 격리 기술 분석

cgroups 기반의 자원 제한 메커니즘

컨테이너 기술에서 핵심적인 역할을 하는 Control Groups (cgroups)는 리눅스 커널이 제공하는 자원 관리 시스템입니다. Docker는 이 기술을 기반으로 각 컨테이너에 대해 메모리, 프로세스, CPU 등 다양한 자원을 세밀하게 제어합니다.

기본 동작 원리

컨테이너 생성 시, Docker는 내부적으로 해당 컨테이너의 고유 ID를 이름으로 하는 하위 cgroup 디렉터리를 생성합니다. 예를 들어, 다음 명령어로 컨테이너를 시작하면:

docker run -itd -m 128m nginx

이 경우 커널은 /sys/fs/cgroup/memory/docker/ 경로 아래에 da82f9ebd384730dda7f831b4331c9e55893c100c83c0c9b0ce112436aa93416라는 이름의 디렉터리를 생성하고, 그 안에 메모리 제한 설정을 반영합니다.

해당 디렉터리 내부의 memory.limit_in_bytes 파일을 확인하면, 실제 설정된 값이 134217728 바이트임을 확인할 수 있으며, 이는 약 128메가바이트에 해당합니다.

자원 회수 처리

컨테이너를 종료하면, 해당 컨테이너와 연결된 cgroup 디렉터리도 자동으로 삭제됩니다. 다시 시작할 때는 동일한 이름의 디렉터리가 재생성되며, 이는 자원 할당과 회수의 일관성을 보장합니다.

자원 제어를 위한 cgroups 명령어

계층 구조 관리

cgroups 계층은 마운트된 가상 파일시스템으로 구성됩니다. 새로운 계층을 생성하려면 다음과 같이 수행합니다:

mkdir cg1
mount -t cgroup -o cpuset cg1 ./cg1

이렇게 하면 ./cg1 디렉터리가 새로운 cgroup 계층의 루트가 됩니다.

정리 작업: release_agent 사용

계층이 더 이상 사용되지 않을 때 자동 정리를 위해 notify_on_releaserelease_agent를 설정할 수 있습니다.

  • notify_on_release=1로 설정하면, 마지막 프로세스가 종료되고 하위 그룹이 없을 때 자동 실행됩니다.
  • release_agent 파일에 스크립트 경로를 기록하면, 해당 스크립트가 호출됩니다.

예시:

echo 1 > notify_on_release
echo /home/user/cleanup.sh > release_agent

그룹 생성 및 관리

새로운 cgroup는 단순히 디렉터리 생성으로 가능합니다:

mkdir cpu_group
cd cpu_group
mkdir sub_group

삭제 시에는 하위 그룹부터 삭제해야 합니다:

rmdir sub_group
rmdir cpu_group

Go 언어로 cgroups 직접 조작하기

다음은 Go 코드를 통해 컨테이너의 메모리 제한을 설정하는 예제입니다. 이 코드는 자체적으로 CLONE_NEWPID, CLONE_NEWNS, CLONE_NEWUTS를 사용하여 네임스페이스를 분리하고, 자식 프로세스를 새로운 cgroup에 배치합니다.

func createCgroupLimit() {
    if os.Args[0] == "/proc/self/exe" {
        // 자식 프로세스: 메모리 소비 테스트 실행
        cmd := exec.Command("sh", "-c", "stress --vm-bytes 100m --vm-keep -m 1")
        cmd.SysProcAttr = &syscall.SysProcAttr{}
        cmd.Stdin = os.Stdin
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr
        cmd.Run()
        return
    }

    // 부모 프로세스: 자식 프로세스 생성 및 cgroup 적용
    child := exec.Command("/proc/self/exe")
    child.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWNS | syscall.CLONE_NEWPID,
    }
    child.Stdin = os.Stdin
    child.Stdout = os.Stdout
    child.Stderr = os.Stderr

    if err := child.Start(); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("생성된 프로세스 PID: %d\n", child.Process.Pid)

    // cgroup 경로 생성
    cgroupPath := filepath.Join("/sys/fs/cgroup/memory", "test_limit")
    os.Mkdir(cgroupPath, 0755)

    // 프로세스 추가
    pidStr := strconv.Itoa(child.Process.Pid)
    ioutil.WriteFile(filepath.Join(cgroupPath, "cgroup.procs"), []byte(pidStr), 0644)

    // 메모리 제한 설정
    ioutil.WriteFile(filepath.Join(cgroupPath, "memory.limit_in_bytes"), []byte("100m"), 0644)

    child.Wait()
}

실행 후 top 명령으로 확인하면, 프로세스의 실제 메모리 사용량이 100메가바이트를 초과하지 않음을 확인할 수 있습니다. 이는 cgroup가 효과적으로 작동했음을 의미합니다.

결론

이 글에서는 cgroups가 어떻게 컨테이너의 자원을 격리하고 제어하는지, 그리고 이를 직접적으로 조작하는 방법을 설명했습니다. 특히, Go 언어를 활용해 컨테이너 환경을 구현하는 기초를 다졌으며, 이후의 Docker 클론 개발에 바로 적용될 수 있는 실용적 기반을 마련했습니다.

태그: cgroups linux memory limit container go

6월 14일 00:59에 게시됨