cgroup v2 지원 추가 구현

개요

이 문서에서는 컨테이너 런타임에 cgroup v2 지원을 추가하는 방법을 설명합니다. 시스템의 cgroup 버전을 자동으로 감지하여 적절한 제어 방식을 적용합니다.

구현

cgroup 버전 확인

시스템에서 사용 중인 cgroup 버전은 다음 명령어로 확인할 수 있습니다:

stat -fc %T /sys/fs/cgroup/

출력 결과가 cgroup2fs이면 v2를, tmpfs이면 v1을 의미합니다.

Go 언어로 구현된 버전 확인 함수는 다음과 같습니다:

const (
    cgroupRootPath = "/sys/fs/cgroup"
)

var (
    checkOnce sync.Once
    isV2      bool
)

func IsCgroupV2() bool {
    checkOnce.Do(func() {
        var fsStat unix.Statfs_t
        err := unix.Statfs(cgroupRootPath, &fsStat)
        if err != nil && os.IsNotExist(err) {
            isV2 = false
            return
        }
        isV2 = fsStat.Type == unix.CGROUP2_SUPER_MAGIC
    })
    return isV2
}

cgroup v2 처리

cgroup v2의 동작 방식은 v1과 유사하지만 파일 구조와 인터페이스가 다릅니다.

하위 그룹 생성

v2에서는 루트 디렉토리 아래에 하위 디렉토리를 생성하는 방식으로 cgroup을 구성합니다:

const BasePath = "/sys/fs/cgroup"

func getGroupPath(groupName string, shouldCreate bool) (string, error) {
    fullPath := path.Join(BasePath, groupName)
    if !shouldCreate {
        return fullPath, nil
    }
    
    if _, err := os.Stat(fullPath); err != nil && os.IsNotExist(err) {
        err = os.Mkdir(fullPath, 0755)
        return fullPath, err
    }
    return fullPath, nil
}

리소스 제한 설정

CPU 제한은 cpu.max 파일을 통해 설정합니다:

echo "5000 10000" > cpu.max

이는 10000 마이크로초 주기 중 5000 마이크로초 동안 CPU를 사용하도록 제한하는 것을 의미합니다.

Go 구현 예시:

func (c *CPUSubsystem) Configure(groupPath string, config *ResourceLimits) error {
    if config.CPULimit == 0 {
        return nil
    }
    
    groupDir, err := getGroupPath(groupPath, true)
    if err != nil {
        return err
    }

    period := 100000  // 기본 주기 (마이크로초)
    quota := period * config.CPULimit / 100
    
    limitData := fmt.Sprintf("%d %d", quota, period)
    if err = os.WriteFile(path.Join(groupDir, "cpu.max"), []byte(limitData), 0644); err != nil {
        return fmt.Errorf("failed to set cpu limit: %v", err)
    }
    return nil
}

프로세스 할당

프로세스를 cgroup에 할당하려면 PID를 cgroup.procs 파일에 기록합니다:

echo 1033 > cgroup.procs

Go 구현:

func (c *CPUSubsystem) AssignProcess(groupPath string, pid int) error {
    return assignToGroup(pid, groupPath)
}

func assignToGroup(pid int, groupPath string) error {
    groupDir, err := getGroupPath(groupPath, true)
    if err != nil {
        return fmt.Errorf("failed to get group path: %v", err)
    }
    
    pidData := strconv.Itoa(pid)
    if err = os.WriteFile(path.Join(groupDir, "cgroup.procs"), []byte(pidData), 0644); err != nil {
        return fmt.Errorf("failed to assign process: %v", err)
    }
    return nil
}

그룹 제거

cgroup을 제거하려면 해당 디렉토리를 삭제합니다:

func (c *CPUSubsystem) RemoveGroup(groupPath string) error {
    groupDir, err := getGroupPath(groupPath, false)
    if err != nil {
        return err
    }
    return os.RemoveAll(groupDir)
}

v1/v2 호환성 처리

시스템의 cgroup 버전에 따라 적절한 관리자를 선택합니다:

func CreateGroupManager(path string) GroupManager {
    if IsCgroupV2() {
        log.Info("Using cgroup v2 mode")
        return NewV2Manager(path)
    }
    log.Info("Using cgroup v1 mode")
    return NewV1Manager(path)
}

테스트

cgroup v1 환경

v1 환경에서 메모리 및 CPU 제한 테스트:

./container run -mem 10m -cpu 10 -it container-name busybox /bin/sh

메모리 테스트:

yes > /dev/null
# Out of Memory로 종료됨

CPU 테스트:

while : ; do : ; done &
# top 명령어로 CPU 사용량이 10%로 제한되는지 확인

cgroup v2 환경

v2로 전환하려면 GRUB 설정을 수정해야 합니다:

# /etc/default/grub 파일 수정
GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1"

# 설정 적용
sudo update-grub
reboot

재부팅 후 버전 확인:

stat -fc %T /sys/fs/cgroup/
# cgroup2fs 출력 확인

v2 환경에서도 동일한 테스트를 수행하여 정상 동작을 검증합니다.

태그: cgroup cgroup-v2 docker container-runtime linux-kernel

7월 15일 16:00에 게시됨