개요
이 문서에서는 컨테이너 런타임에 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 환경에서도 동일한 테스트를 수행하여 정상 동작을 검증합니다.