이전 글에서 namespace로 프로세스를 격리하는 방법에 대해서 알아봤다.
하지만 마지막에도 언급했듯이 격리된 프로세스만으로는 부족한 점이 몇가지 있다.
- CPU를 100% 점유하는 나쁜 녀석들
- 메모리를 무한정 먹어버리는 이상한 녀석들
그럼 착한 녀석들로 만들어주기 위해선 어떻게 해야될까?
이 문제를 해결하는 게 cgroup이다.
cgroup?
namespace가 '뭘 볼 수 있는가'를 제한한다면, cgroup은 '얼마나 쓸 수 있는가'를 제한한다.
cgroup은 Control Groups의 약자로, 프로세스 그룹의 리소스 사용량을 제한, 계량, 격리하는 리눅스 커널 기능이다.
cgroup이 제어하는 리소스
| 컨트롤러 | 설명 |
| cpu | CPU 시간 할당량 제한 -> 전체의 50%만 써 |
| cpuset | 특정 CPU 코어만 사용하도록 제한 -> 0번, 1번 코어만 써 |
| memory | 메모리 사용량 제한 -> 512MB까지만 |
| io (blkio) | 디스크 I/O 대역폭 제한 -> 초당 10MB까지만 읽어 |
| pids | 생성 가능한 프로세스 수 제한 -> fork bomb 방지 |
| devices | 접근 가능한 디바이스 제한 -> /dev/sda 접근 금지 |
| freezer | 프로세스 그룹을 일시 정지/재개 |
만약 이게 없다면?
| 컨트롤러 | 없다면? | 있다면? |
| cpu | 한 컨테이너가 CPU 독점 | CPU 시간 배분 |
| memory | 메모리 누수 -> OOM Killer가 다 죽임 | 해당 컨테이너만 죽임 |
| pids | fork bomb -> 시스템 마비 | 해당 cgroup만 제한 |
| io | 한 컨테이너가 디스크 독점 | I/O 대역폭 공정 배분 |
cgroup v1 / v2
cgroup에는 두 가지 버전이 있다.
v1 (레거시)
- 각 컨트롤러(cpu, memory 등)가 독립적인 계층 구조를 가짐
- /sys/fs/cgroup/cpu/, /sys/fs/cgroup/memory/ 등 별도 디렉토리
- 복잡하고, 컨트롤러 간 조율이 어려움
v2 (통합)
- 단일 계층 구조로 통합
- /sys/fs/cgroup/ 하나로 관리
- 더 일관되고 예측 가능한 동작
- 최신 Docker/Kubernetes는 v2 사용 권장

커널 내부 구조
namespace에서 task_struct -> nsproxy를 봤듯이, cgroup도 커널 구조체로 관리된다.
struct task_struct {
// ...
struct css_set *cgroups; // 이 프로세스가 속한 cgroup 정보
// ...
};
css_set은 Cgroup SubSystem Set의 약자로, 프로세스가 어떤 cgroup들에 속해있는지를 담고 있다.
cgroup 구조체
https://elixir.bootlin.com/linux/v6.18/source/include/linux/cgroup-defs.h#L472
cgroup-defs.h - include/linux/cgroup-defs.h - Linux source code v6.18 - Bootlin Elixir Cross Referencer
elixir.bootlin.com
cgroup 구조체는 아래와 같은 구조를 가진다.
struct cgroup {
struct cgroup_subsys_state self;
unsigned long flags;
int id;
int level; // 계층 구조에서의 깊이
struct cgroup *parent;
struct list_head children;
// 각 서브시스템(cpu, memory 등)의 상태
struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
// ...
};
각 컨트롤러(서브시스템)은 아래와 같은 구조를 가진다.
- memory 컨트롤러를 보면
struct mem_cgroup {
struct cgroup_subsys_state css;
struct page_counter memory; // 메모리 사용량 카운터
struct page_counter swap; // 스왑 사용량
unsigned long memory_high; // soft limit
unsigned long memory_max; // hard limit (이거 넘으면 OOM)
// ...
};직접 cgroup을 만들어보자.
지난번처럼 Docker 컨테이너에서 해본다.
docker run -it --privileged --name cgroup-test ubuntu:22.04 /bin/bash
1. cgroup 파일시스템을 확인해본다.(난 v2다)
root@396f2978646c:/# mount | grep cgroup
cgroup on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

2. memory 컨트롤러 활성화
- cgroup v2에서는 컨트롤러를 명시적으로 켜야 한다. 근데 root cgroup에 프로세스가 있으면 활성화가 안된다.
- 따라서 임시 cgroup을 만들어서 프로세스를 옮겨주고, 거기서 memory 컨트롤러를 활성화해야 한다.
# root cgroup에 프로세스 있는지 확인
cat /sys/fs/cgroup/cgroup.procs
# 임시 cgroup 만들어서 프로세스 옮기기
mkdir /sys/fs/cgroup/init-group
for pid in $(cat /sys/fs/cgroup/cgroup.procs); do
echo $pid > /sys/fs/cgroup/init-group/cgroup.procs 2>/dev/null
done
# 이제 memory 컨트롤러 활성화
echo "+memory" > /sys/fs/cgroup/cgroup.subtree_control
3. 그 후 새로운 cgroup을 만들어준다.
# my-limited-group이라는 cgroup 생성
mkdir /sys/fs/cgroup/my-limited-group
# memory.max 파일이 생겼는지 확인
ls /sys/fs/cgroup/my-limited-group/ | grep memory

4. 이제 메모리 제한을 설정해줄 수 있다.
# 메모리 50MB로 제한
echo "52428800" > /sys/fs/cgroup/my-limited-group/memory.max
# 확인
cat /sys/fs/cgroup/my-limited-group/memory.max
5. 프로세스를 cgroup에 넣기
# 현재 쉘의 PID 확인
echo $$
# 현재 쉘을 my-limited-group에 추가
echo $$ > /sys/fs/cgroup/my-limited-group/cgroup.procs
# 확인
cat /sys/fs/cgroup/my-limited-group/cgroup.procs
6. 메모리 제한 테스트를 위해 파이썬을 깔고, 메모리 폭탄용 코드를 돌려보자.
# python 설치
apt update && apt install -y python3
# 50MB 넘게 할당 시도
python3 -c "
data = []
for i in range(100):
data.append('X' * 1024 * 1024) # 1MB씩 추가
print(f'{i+1}MB allocated')
"
처음에 얘가 50MB 근처에서 죽지를 않았다.
알고보니 swap 제한도가 max여서 그랬다
cat /sys/fs/cgroup/my-limited-group/memory.swap.max
# swap도 0으로 제한
echo "0" > /sys/fs/cgroup/my-limited-group/memory.swap.max
# 확인
cat /sys/fs/cgroup/my-limited-group/memory.swap.max
메모리 뿐 아니라 CPU 사용량 제한 등.. 다양한 제한을 cgroup을 통해서 관리할 수 있다.
Container를 만들 때는
매번 /sys/fs/cgroup을 찾아 들어가서 echo로 숫자를 계산해서 넣지 않는다.
docker를 사용한다면 우린 주문만 하면 된다.
docker run -d --name my-web \
--memory="50m" \
--cpus="0.5" \
nginx
이렇게 명령어를 입력하면, Container Runtime이 내부적으로 다음과 같은 일을 수행한다.
- Container Runtime는 도커가 내부적으로 사용하는 자동화 기술로 생가하면 된다.
- 추후 자세히 다룰 예정이다.
- /sys/fs/cgroup/system.slice/docker-{ID}.scope 디렉토리를 생성한다.
- memory.max 파일에 52428800 (50MB)을 쓴다.
- cpu.max 파일에 CPU 주기에 맞는 값을 계산해서 쓴다.
- 생성된 프로세스의 PID를 cgroup.procs에 등록한다.
결국 도커의 리소스 제한 플래그(--memory, --cpus)는 cgroup 파일 시스템에 값을 쓰기 위해 만들어진 기능이다.
정리
이전 글의 namespace와 이번 글의 cgroup을 합쳐, 컨테이너를 설정해줄 수 있다.
리눅스 커널 입장에서 컨테이너는 조금 특별하게 설정된 프로세스일 뿐이다.
- namespace: nsproxy
- 격리 담당
- PID, Network, Mount 등을 분리해 다른 영역의 프로세스를 못 보게 함
- cgroup: css_set
- 제한 담당
- CPU, Memory 사용량을 통제하여 남의 것을 못 쓰게 함
이 두가지 기술로 Host 안에 수많은 OS가 독립적으로, 그리고 서로 방해받지 않으면서 돌아갈 수 있다.
'Infra > Docker' 카테고리의 다른 글
| Docker Image & Container 명령어 정리 (0) | 2025.12.12 |
|---|---|
| Container의 근반이 되는 기술에 대해 알아보자(완) UnionFS (0) | 2025.12.11 |
| Container의 근반이 되는 기술에 대해 알아보자(1) namespace (0) | 2025.12.11 |
| 도커(Docker)로 PHP / Laravel 개발 환경 구축하기 (0) | 2025.12.08 |
| Utility Docker에 대해 알아보자 (0) | 2025.12.07 |