cpusets & CPU Isolation

cpusets는 태스크를 지정 CPU와 메모리 노드에 고정해 성능 예측성을 높이는 핵심 제어 수단입니다. 이 문서는 cgroup v2의 cpuset.cpus/cpuset.mems 의미, isolcpus/nohz_full/rcu_nocbs 조합, IRQ affinity와 housekeeping 분리, NUMA 배치, HPC 및 실시간 워크로드의 지터 최소화 튜닝과 검증 절차를 상세히 다룹니다.

역할 분리 안내: cpuset은 cgroups의 하위 컨트롤러입니다. cgroup 트리 모델, delegation, 메모리/IO 정책은 cgroups 문서를 먼저 확인한 뒤 이 문서를 읽는 것을 권장합니다.
전제 조건: 프로세스 스케줄러프로세스 문서를 먼저 읽으세요. 실행 단위 관리 주제는 태스크 상태 전이와 큐 정책이 핵심이므로, 스케줄링 기준과 wakeup 경로를 먼저 이해해야 합니다.
일상 비유: 이 주제는 작업장 인력 배치와 호출 순서 관리와 비슷합니다. 긴급 작업, 대기열, 우선순위를 함께 조정하듯이 커널도 태스크 분류와 깨우기 정책이 전체 반응성을 결정합니다.

핵심 요약

  • cpuset.cpus — 작업이 실행 가능한 CPU 집합
  • cpuset.mems — 메모리 할당 가능한 NUMA 노드 집합
  • isolcpus — 부팅 시점의 정적 격리
  • nohz_full — 주기 tick 인터럽트를 줄여 지터 완화
  • rcu_nocbs — RCU 콜백을 격리 CPU 밖으로 오프로드

단계별 이해

  1. 대상 워크로드 선정
    지연 민감 태스크와 일반 태스크를 먼저 분류합니다.
  2. CPU/NUMA 매핑 설계
    코어와 메모리 노드를 짝지어 cpuset 경계를 정의합니다.
  3. 커널 파라미터 결합
    isolcpus, nohz_full, rcu_nocbs를 함께 조정해 간섭원을 제거합니다.
  4. 성능 측정 반복
    latency/throughput를 측정하며 cpuset 경계와 밸런싱 정책을 미세 조정합니다.
관련 문서: cgroups (리소스 제어), Process Scheduler (스케줄링 정책), Real-time (실시간 커널), CPU Topology (CPU 구조)

개요

CPU Isolation은 특정 CPU 코어를 일반 커널 작업으로부터 격리하여 중요한 워크로드가 방해받지 않도록 보장하는 기술입니다.

Boot Params isolcpus/nohz_full/rcu_nocbs (부팅 시 정적 설정) cpuset cgroup cpu mask 분배 (런타임 동적 설정) 격리 워크로드 지터 감소/예측성 향상 isolcpus=nohz_full로 격리된 CPU를 cpuset에서 다시 분배 ex: isolcpus=4-7 설정 시, cpuset.cpus="4-7"로 프로세스 배정

격리 이유

격리 방법

방법 설명 적용 시점
isolcpus 부팅 시 CPU를 스케줄러에서 제외 커널 파라미터 (정적)
cpusets cgroup으로 프로세스 CPU 제한 런타임 (동적)
nohz_full 틱리스 모드 (타이머 인터럽트 제거) 커널 파라미터 (정적)
rcu_nocbs RCU 콜백을 다른 CPU로 오프로드 커널 파라미터 (정적)
taskset 프로세스 CPU affinity 설정 런타임 (명령줄)

cpusets 서브시스템

개념

cpusets는 프로세스 그룹에 CPU와 메모리 노드를 할당하는 cgroup v1/v2 서브시스템입니다.

cpuset 제어 파일

파일 설명
cpuset.cpus 허용된 CPU 목록 (예: 0-3,8-11)
cpuset.mems 허용된 메모리 노드 (NUMA)
cpuset.cpu_exclusive 독점 모드 (1 = 다른 cpuset과 CPU 중복 불가)
cpuset.mem_exclusive 메모리 노드 독점
cpuset.sched_load_balance 스케줄러 로드 밸런싱 (0 = 비활성화)
cpuset.memory_migrate 메모리 마이그레이션 허용
cpuset.memory_pressure 메모리 압력 통계 (읽기 전용)

cpuset 생성 (cgroup v1)

# cpuset cgroup 마운트
$ sudo mount -t cgroup -o cpuset cpuset /sys/fs/cgroup/cpuset

# 격리된 cpuset 생성
$ sudo mkdir /sys/fs/cgroup/cpuset/isolated

# CPU 4-7 할당
$ echo 4-7 | sudo tee /sys/fs/cgroup/cpuset/isolated/cpuset.cpus

# NUMA 노드 0 할당
$ echo 0 | sudo tee /sys/fs/cgroup/cpuset/isolated/cpuset.mems

# 독점 모드 활성화
$ echo 1 | sudo tee /sys/fs/cgroup/cpuset/isolated/cpuset.cpu_exclusive

# 로드 밸런싱 비활성화 (지터 최소화)
$ echo 0 | sudo tee /sys/fs/cgroup/cpuset/isolated/cpuset.sched_load_balance

# 프로세스 추가
$ echo $$ | sudo tee /sys/fs/cgroup/cpuset/isolated/tasks
$ ./my_rt_app

cpuset in cgroup v2

cgroup v2 cpuset 설정 (CPU 격리) ① mkdir /sys/fs/cgroup/isolated 새 cgroup 디렉토리 생성 ② echo "+cpuset" → /sys/fs/cgroup/cgroup.subtree_control cpuset 컨트롤러 활성화 (cgroup v2 통합 방식) ③-a CPU 할당 echo "4-7" → .../isolated/cpuset.cpus ③-b 메모리 노드 할당 echo "0" → .../isolated/cpuset.mems ④ 프로세스 이동 echo $$ → .../isolated/cgroup.procs 결과: CPU 4-7에만 스케줄링 / NUMA 0에서만 메모리 할당 / 다른 프로세스와 완전 격리

isolcpus 커널 파라미터

개요

isolcpus는 부팅 시 지정된 CPU를 기본 스케줄러 도메인에서 제외합니다. 이로 인해 일반 프로세스는 해당 CPU에 스케줄링되지 않습니다.

문법

# GRUB 설정 (/etc/default/grub)
GRUB_CMDLINE_LINUX="isolcpus=4-7"

# 또는 도메인별 격리
GRUB_CMDLINE_LINUX="isolcpus=domain,4-7"

# 옵션
isolcpus=4-7           # CPU 4~7 격리 (기본)
isolcpus=domain,4-7    # 스케줄러 도메인에서 제외
isolcpus=managed_irq,4-7 # managed IRQ도 격리

# GRUB 업데이트
$ sudo update-grub
$ sudo reboot

격리 확인

# isolcpus 설정 확인
$ cat /proc/cmdline | grep isolcpus

# 스케줄러 도메인 확인
$ cat /proc/sched_debug | grep -A 10 "cpu#4"

# 프로세스 CPU affinity 확인
$ taskset -p $$
pid 1234's current affinity mask: f  # 0-3만 허용 (4-7 제외)

nohz_full (틱리스 모드)

개념

nohz_full은 지정된 CPU에서 주기적인 타이머 틱(timer tick)을 제거하여 실시간 워크로드의 지터를 최소화합니다.

요구사항

설정

# GRUB 설정
GRUB_CMDLINE_LINUX="nohz_full=4-7"

# isolcpus와 함께 사용 (권장)
GRUB_CMDLINE_LINUX="isolcpus=4-7 nohz_full=4-7"

# 확인
$ cat /sys/devices/system/cpu/nohz_full
4-7

$ cat /sys/devices/system/cpu/isolated
4-7

동작 방식

/* nohz_full CPU에서 */
- 단일 runnable 태스크만 있을 때 → 타이머 틱 중단
- 2개 이상 runnable 태스크 → 타이머 틱 재개 (스케줄링 필요)
- 시스템 콜/인터럽트 진입 시 → 일시적으로 틱 재개

rcu_nocbs (RCU 콜백 오프로드)

개념

RCU(Read-Copy-Update) 콜백 처리를 별도 CPU로 오프로드하여 격리된 CPU의 지터를 줄입니다.

설정

# GRUB 설정
GRUB_CMDLINE_LINUX="rcu_nocbs=4-7"

# 완전한 CPU 격리 설정
GRUB_CMDLINE_LINUX="isolcpus=domain,managed_irq,4-7 nohz_full=4-7 rcu_nocbs=4-7"

# 확인
$ cat /sys/devices/system/cpu/nohz_full
4-7

# RCU 스레드 확인
$ ps aux | grep rcuo
root       12  0.0  0.0      0     0 ?        S    12:00   0:00 [rcuo/4]
root       13  0.0  0.0      0     0 ?        S    12:00   0:00 [rcuo/5]
...

taskset (CPU Affinity)

사용법

# CPU 4에 프로세스 바인딩
$ taskset -c 4 ./my_app

# CPU 4-7에 바인딩
$ taskset -c 4-7 ./my_app

# 비트마스크 사용 (CPU 0,2,4,6)
$ taskset 0x55 ./my_app

# 실행 중인 프로세스 affinity 변경
$ taskset -cp 4 1234
pid 1234's current affinity list: 0-7
pid 1234's new affinity list: 4

# 현재 affinity 확인
$ taskset -p $$
pid 5678's current affinity mask: f0  # CPU 4-7

sched_setaffinity() 시스템 콜

#define _GNU_SOURCE
#include <sched.h>

int main(void)
{
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(4, &cpuset);  /* CPU 4 */

    if (sched_setaffinity(0, sizeof(cpuset), &cpuset) == -1) {
        perror("sched_setaffinity");
        return 1;
    }

    /* 이제 CPU 4에서만 실행됨 */
    while (1) {
        /* compute-intensive task */
    }
}

IRQ Affinity (인터럽트 격리)

인터럽트 관리

격리된 CPU에서 인터럽트도 제거해야 완전한 격리가 가능합니다.

# 모든 IRQ 확인
$ cat /proc/interrupts

# IRQ 0의 affinity 확인
$ cat /proc/irq/0/smp_affinity
ff  # 모든 CPU

# IRQ 0을 CPU 0-3으로 제한 (4-7 격리)
$ echo 0f | sudo tee /proc/irq/0/smp_affinity
0f

# 모든 IRQ를 CPU 0-3으로 이동 (스크립트)
$ for irq in $(ls /proc/irq/ | grep -E '^[0-9]+$'); do
    echo 0f | sudo tee /proc/irq/$irq/smp_affinity 2>/dev/null
done

irqbalance 비활성화

# irqbalance 데몬이 IRQ affinity를 자동 조정하므로 비활성화
$ sudo systemctl stop irqbalance
$ sudo systemctl disable irqbalance

# 또는 특정 CPU 제외
$ sudo irqbalance --banirq=4,5,6,7

실전 예제

HPC 워크로드 설정

#!/bin/bash
# HPC CPU 격리 스크립트

# 1. GRUB 설정 확인
grep -q "isolcpus" /proc/cmdline || {
    echo "Error: isolcpus not set"
    exit 1
}

# 2. cpuset 생성
sudo mkdir -p /sys/fs/cgroup/cpuset/hpc
echo 4-7 | sudo tee /sys/fs/cgroup/cpuset/hpc/cpuset.cpus
echo 0 | sudo tee /sys/fs/cgroup/cpuset/hpc/cpuset.mems
echo 1 | sudo tee /sys/fs/cgroup/cpuset/hpc/cpuset.cpu_exclusive
echo 0 | sudo tee /sys/fs/cgroup/cpuset/hpc/cpuset.sched_load_balance

# 3. IRQ affinity 설정 (CPU 0-3만)
for irq in $(ls /proc/irq/ | grep -E '^[0-9]+$'); do
    echo 0f | sudo tee /proc/irq/$irq/smp_affinity 2>/dev/null
done

# 4. HPC 애플리케이션 실행
echo $$ | sudo tee /sys/fs/cgroup/cpuset/hpc/tasks
exec taskset -c 4 chrt -f 99 ./hpc_simulation

실시간 애플리케이션

#define _GNU_SOURCE
#include <sched.h>
#include <pthread.h>
#include <stdio.h>

void setup_rt_thread(int cpu_id)
{
    struct sched_param param;
    cpu_set_t cpuset;

    /* CPU affinity 설정 */
    CPU_ZERO(&cpuset);
    CPU_SET(cpu_id, &cpuset);
    pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);

    /* 실시간 우선순위 설정 */
    param.sched_priority = 99;
    pthread_setschedparam(pthread_self(), SCHED_FIFO, ¶m);

    /* 메모리 락 (페이지 폴트 방지) */
    mlockall(MCL_CURRENT | MCL_FUTURE);

    printf("RT thread on CPU %d\\n", cpu_id);
}

void *rt_worker(void *arg)
{
    int cpu_id = *(int *)arg;
    setup_rt_thread(cpu_id);

    while (1) {
        /* Critical real-time work */
    }
}

모니터링

CPU 격리 모니터링

#!/bin/bash
# CPU 격리 상태 모니터링

echo "===== CPU Isolation Status ====="

echo "Kernel Parameters:"
grep -o 'isolcpus=[^ ]*' /proc/cmdline
grep -o 'nohz_full=[^ ]*' /proc/cmdline
grep -o 'rcu_nocbs=[^ ]*' /proc/cmdline

echo
echo "Isolated CPUs:"
cat /sys/devices/system/cpu/isolated

echo
echo "Tasks on Isolated CPUs:"
for cpu in 4 5 6 7; do
    echo "  CPU $cpu:"
    ps -eLo psr,comm | grep "^ *$cpu" | wc -l
done

echo
echo "IRQ Distribution:"
cat /proc/interrupts | head -1
cat /proc/interrupts | grep -E "^ *[0-9]+:" | head -5

커널 설정

CONFIG_CPUSETS=y                  # cpuset 서브시스템
CONFIG_PROC_PID_CPUSET=y          # /proc/[pid]/cpuset
CONFIG_NO_HZ_FULL=y               # Full tickless mode
CONFIG_RCU_NOCB_CPU=y             # RCU callback offload
CONFIG_IRQ_FORCED_THREADING=y     # IRQ 스레딩

# 실시간 커널 (선택)
CONFIG_PREEMPT_RT=y               # PREEMPT_RT 패치
CONFIG_HIGH_RES_TIMERS=y          # 고해상도 타이머

Best Practices

권장사항:
  • Housekeeping CPU 확보 — 최소 1~2개 CPU는 일반 작업용으로 남겨둘 것
  • NUMA 고려 — 격리된 CPU와 메모리 노드를 동일하게 설정
  • 하이퍼스레딩 비활성화 — SMT 비활성화로 캐시 경합 제거
  • 전원 관리 비활성화 — cpufreq governor를 performance로 설정
  • 투명 거대 페이지 비활성화 — THP가 지터를 유발할 수 있음

참고자료

다음 학습:

cgroup v1 vs v2 cpuset 계층 구조

cpuset 컨트롤러는 cgroup v1과 v2에서 근본적으로 다른 계층 모델을 사용합니다. v1은 전용 마운트 포인트(/sys/fs/cgroup/cpuset/)에 독립된 트리를 구성하는 반면, v2는 통합된 단일 계층에서 cgroup.subtree_control을 통해 컨트롤러를 활성화합니다. 마이그레이션 시 API 차이, 파일 이름 변경, 동작 의미 변화를 반드시 이해해야 합니다.

cgroup v1 vs v2 cpuset 계층 비교 cgroup v1 (레거시) mount -t cgroup -o cpuset cpuset /sys/fs/cgroup/cpuset /sys/fs/cgroup/cpuset/ (루트) cpuset.cpus = 0-3 cpuset.mems = 0 cpuset.cpus = 4-7 cpuset.mems = 1 housekeeping/ isolated/ v1 전용 파일 • cpuset.cpu_exclusive (독점) • cpuset.sched_load_balance • cpuset.memory_pressure (읽기 전용) v1 제한사항 • 컨트롤러마다 별도 마운트 필요 • 프로세스가 여러 계층에 동시 존재 • delegation 모델 불명확 • threaded 모드 미지원 cgroup v2 (통합) echo "+cpuset" > cgroup.subtree_control /sys/fs/cgroup/ (통합 루트) cpuset.cpus = 0-3 cpuset.mems = 0 cpuset.cpus = 4-7 cpuset.mems = 1 housekeeping/ isolated/ v2 신규/변경 파일 • cpuset.cpus.partition (root/member) • cpuset.cpus.effective (읽기 전용) • cpuset.mems.effective (읽기 전용) v2 장점 • 통합 계층 (모든 컨트롤러 단일 트리) • no-internal-process 규칙 명확 • threaded 모드 (스레드 수준 격리) • partition 기반 자동 sched domain

API 차이 비교

항목 cgroup v1 cgroup v2
마운트 mount -t cgroup -o cpuset mount -t cgroup2 (통합)
컨트롤러 활성화 마운트 옵션으로 자동 echo "+cpuset" > cgroup.subtree_control
프로세스 이동 echo PID > tasks echo PID > cgroup.procs
독점 CPU cpuset.cpu_exclusive=1 cpuset.cpus.partition=root
로드 밸런싱 제어 cpuset.sched_load_balance=0 partition이 자동 관리
유효 CPU 확인 cpuset.effective_cpus (일부 배포판) cpuset.cpus.effective
스레드 모드 미지원 cgroup.type=threaded
내부 프로세스 규칙 없음 (자유) no-internal-process (리프 노드만 태스크 보유)

v1 → v2 마이그레이션 절차

주의: v1과 v2는 동일 시스템에서 동시 사용이 가능하나(하이브리드 모드), 같은 컨트롤러는 하나의 계층에서만 활성화됩니다. 마이그레이션 전에 반드시 기존 v1 cpuset 설정을 백업하세요.
# 1. 현재 v1 cpuset 설정 확인
$ cat /proc/mounts | grep cpuset
cpuset /sys/fs/cgroup/cpuset cgroup rw,cpuset 0 0

# 2. v1 cpuset 계층 구조 백업
$ find /sys/fs/cgroup/cpuset -name "cpuset.cpus" -exec sh -c 'echo "$1: $(cat $1)"' _ {} \;

# 3. systemd가 cgroup v2를 사용하도록 GRUB 설정
$ sudo sed -i 's/GRUB_CMDLINE_LINUX="/GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1 /' /etc/default/grub
$ sudo update-grub

# 4. 재부팅 후 v2 통합 계층 확인
$ mount | grep cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

# 5. cpuset 컨트롤러 활성화
$ echo "+cpuset" | sudo tee /sys/fs/cgroup/cgroup.subtree_control

# 6. v2 방식으로 cpuset 생성
$ sudo mkdir /sys/fs/cgroup/isolated
$ echo "4-7" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus
$ echo "0" | sudo tee /sys/fs/cgroup/isolated/cpuset.mems

# 7. partition 모드로 CPU 독점 (v1의 cpu_exclusive 대체)
$ echo "root" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus.partition

v2 스레드 모드

cgroup v2는 스레드 수준의 cpuset 제어를 지원합니다. cgroup.type=threaded로 설정하면 개별 스레드를 서로 다른 CPU 집합에 배치할 수 있습니다.

# 스레드 모드 cgroup 생성
$ sudo mkdir /sys/fs/cgroup/app
$ echo "+cpuset" | sudo tee /sys/fs/cgroup/app/cgroup.subtree_control

# 하위 cgroup을 threaded 타입으로 전환
$ sudo mkdir /sys/fs/cgroup/app/worker-fast
$ echo "threaded" | sudo tee /sys/fs/cgroup/app/worker-fast/cgroup.type

$ sudo mkdir /sys/fs/cgroup/app/worker-slow
$ echo "threaded" | sudo tee /sys/fs/cgroup/app/worker-slow/cgroup.type

# 각 스레드 그룹에 다른 CPU 할당
$ echo "4-5" | sudo tee /sys/fs/cgroup/app/worker-fast/cpuset.cpus
$ echo "6-7" | sudo tee /sys/fs/cgroup/app/worker-slow/cpuset.cpus

# 스레드 이동 (TID 사용)
$ echo 12345 | sudo tee /sys/fs/cgroup/app/worker-fast/cgroup.threads

CPU 격리와 cpuset.cpus.partition

cgroup v2에서 cpuset.cpus.partition은 CPU 격리의 핵심 메커니즘입니다. root 파티션으로 설정하면 해당 cgroup의 CPU가 부모에서 분리되어 완전히 독립된 스케줄링 도메인을 형성합니다. 이는 v1의 cpuset.cpu_exclusive=1 + cpuset.sched_load_balance=0를 하나로 통합한 것입니다.

cpuset.cpus.partition 동작 구조 시스템 전체 CPU: 0 1 2 3 4 5 6 7 8코어 시스템 (2 NUMA 노드: 노드0=CPU0-3, 노드1=CPU4-7) / (루트 cgroup) — cpuset.cpus = 0-7, partition = root (기본) housekeeping/ (member) cpuset.cpus = 0-3 partition = member (기본값) isolated/ (root partition) cpuset.cpus = 4-7 partition = root → 독립 sched domain 일반 스케줄링 도메인 CPU0 CPU1 CPU2 CPU3 격리된 스케줄링 도메인 CPU4 CPU5 CPU6 CPU7 스케줄링 도메인 경계 cpuset.cpus.partition 상태 전이 member root isolated echo "root" echo "isolated" 부모 도메인에 포함 로드 밸런싱 활성 독립 sched domain 부모에서 CPU 분리 root + 로드 밸런싱 비활성 완전 격리 (최고 수준) isolcpus: 부팅 시 정적 격리 (변경 불가) → cpuset.cpus.partition=isolated: 런타임 동적 격리 (변경 가능) 권장: isolcpus 대신 cpuset.cpus.partition=isolated 사용 (커널 5.11+)

isolcpus 비권장 추세

비권장 추세: 커널 커뮤니티에서는 isolcpus를 점진적으로 cpuset partition 기반으로 대체하는 것을 권장합니다. isolcpus는 부팅 시 고정되어 런타임 변경이 불가능하며, 잘못 설정하면 재부팅 없이 복구할 수 없습니다.
# 기존 방식 (isolcpus — 재부팅 필요)
GRUB_CMDLINE_LINUX="isolcpus=domain,managed_irq,4-7 nohz_full=4-7 rcu_nocbs=4-7"

# 새로운 방식 (cpuset partition — 런타임 변경 가능, 커널 5.11+)
$ sudo mkdir /sys/fs/cgroup/isolated
$ echo "4-7" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus
$ echo "0-1" | sudo tee /sys/fs/cgroup/isolated/cpuset.mems
$ echo "isolated" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus.partition

# 격리 해제 (재부팅 없이!)
$ echo "member" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus.partition

# 현재 유효 CPU 확인
$ cat /sys/fs/cgroup/isolated/cpuset.cpus.effective
4-7

Housekeeping CPU 설계

격리 환경에서 housekeeping CPU는 커널 데몬, IRQ 처리, RCU 콜백, 워크큐 등 시스템 유지보수 작업을 전담합니다. 최소 1~2개의 CPU를 housekeeping 전용으로 확보해야 하며, NUMA 토폴로지를 고려하여 배치합니다.

CPU 역할 CPU 번호 (예) 담당 작업 커널 파라미터
Housekeeping 0-1 커널 데몬, IRQ, RCU, 워크큐, 타이머 (기본값, 격리 대상에서 제외)
관리 워크로드 2-3 모니터링, 로깅, 관리 프로세스 cpuset으로 제한
격리 워크로드 4-7 지연 민감 애플리케이션 nohz_full=4-7 rcu_nocbs=4-7
# housekeeping CPU 마스크 확인 (nohz_full 미지정 CPU = housekeeping)
$ cat /sys/devices/system/cpu/nohz_full
4-7

# housekeeping CPU에 커널 스레드 고정
$ for pid in $(pgrep -f "\[.*\]"); do
    taskset -cp 0-1 $pid 2>/dev/null
done

# irqbalance에서 격리 CPU 제외
$ echo 'IRQBALANCE_BANNED_CPULIST=4-7' | sudo tee -a /etc/default/irqbalance
$ sudo systemctl restart irqbalance

NOHZ_FULL 통합과 틱 오프로딩

NOHZ_FULL(전체 틱리스 모드)은 cpuset 격리와 결합하여 CPU 코어의 주기적 타이머 인터럽트를 완전히 제거합니다. 지연에 민감한 워크로드(금융 거래, 실시간 제어)에서 마이크로초 수준의 지터를 제거하는 핵심 기술입니다.

NOHZ_FULL 틱 오프로딩 과정 시간 흐름 → 일반 CPU (틱 활성) tick tick tick tick tick tick tick tick 태스크 실행 (매 tick마다 인터럽트 발생 → 지터) nohz_full CPU (틱 오프로드) 마지막 tick 틱 없는 구간 (인터럽트 없이 연속 실행) syscall 복귀 조건: CPU에 runnable 태스크 1개만 존재 → 틱 중단 / 2개 이상 → 틱 재개 (스케줄링 필요) 잔여 인터럽트 소스와 제거 방법 타이머 틱 nohz_full 제거 RCU 콜백 rcu_nocbs 오프로드 하드웨어 IRQ managed_irq 격리 워크큐 WQ_UNBOUND 사용 최적 격리 명령 조합 isolcpus=domain,managed_irq,4-7 nohz_full=4-7 rcu_nocbs=4-7 irqaffinity=0-3

NOHZ_FULL 검증 방법

# 1. nohz_full 활성 CPU 확인
$ cat /sys/devices/system/cpu/nohz_full
4-7

# 2. 실제 틱 중단 여부를 /proc/interrupts로 확인
# 격리 CPU의 LOC(Local timer) 카운트가 증가하지 않아야 함
$ watch -n 1 "cat /proc/interrupts | grep LOC"

# 3. ftrace로 tick_stop 이벤트 확인
$ echo 1 | sudo tee /sys/kernel/debug/tracing/events/timer/tick_stop/enable
$ cat /sys/kernel/debug/tracing/trace_pipe | head -20

# 4. perf로 인터럽트 빈도 측정
$ sudo perf stat -C 4 -e irq:irq_handler_entry -- sleep 10

지연 민감 워크로드 최적화

#define _GNU_SOURCE
#include <sched.h>
#include <time.h>
#include <stdio.h>
#include <sys/mman.h>

/* nohz_full CPU에서 최소 지터로 실행하기 위한 설정 */
void setup_nohz_full_task(int cpu)
{
    cpu_set_t mask;
    struct sched_param sp = { .sched_priority = 99 };

    /* 1. CPU affinity — 격리 CPU에 고정 */
    CPU_ZERO(&mask);
    CPU_SET(cpu, &mask);
    sched_setaffinity(0, sizeof(mask), &mask);

    /* 2. SCHED_FIFO 최고 우선순위 — 선점 방지 */
    sched_setscheduler(0, SCHED_FIFO, &sp);

    /* 3. 메모리 잠금 — 페이지 폴트 제거 */
    mlockall(MCL_CURRENT | MCL_FUTURE);

    /* 4. 타이머 슬랙 제거 (prctl) */
    prctl(PR_SET_TIMERSLACK, 1);
}

/* 지터 측정: 연속 clock_gettime 호출 간격 편차 */
void measure_jitter(int iterations)
{
    struct timespec ts1, ts2;
    long max_ns = 0, min_ns = 999999999;

    for (int i = 0; i < iterations; i++) {
        clock_gettime(CLOCK_MONOTONIC, &ts1);
        /* 워크로드 시뮬레이션 */
        asm volatile("pause");
        clock_gettime(CLOCK_MONOTONIC, &ts2);

        long diff = (ts2.tv_sec - ts1.tv_sec) * 1000000000L
                   + (ts2.tv_nsec - ts1.tv_nsec);
        if (diff > max_ns) max_ns = diff;
        if (diff < min_ns) min_ns = diff;
    }
    printf("지터: min=%ldns max=%ldns range=%ldns\n",
           min_ns, max_ns, max_ns - min_ns);
}
팁: nohz_full CPU에서 시스템 콜을 호출하면 일시적으로 틱이 재개됩니다. 최소 지터를 원하면 시스템 콜 호출을 최소화하고, vDSO를 활용하여 clock_gettime() 등을 사용자 공간에서 처리하세요.

메모리 노드 바인딩과 NUMA 친화성

cpuset.mems는 프로세스 그룹이 메모리를 할당할 수 있는 NUMA 노드를 제한합니다. CPU와 메모리 노드를 올바르게 짝지어야 원격 메모리 접근(Remote Memory Access)으로 인한 성능 저하를 방지할 수 있습니다. 잘못된 cpuset.mems 설정은 OOM(Out of Memory)을 유발할 수 있습니다.

cpuset.mems와 NUMA 노드 바인딩 NUMA 노드 0 CPU 0 CPU 1 CPU 2 CPU 3 로컬 메모리 (32GB) — 접근 지연: ~80ns cpuset A: cpus=0-3, mems=0 로컬 접근만 허용 (최적 성능) NUMA 노드 1 CPU 4 CPU 5 CPU 6 CPU 7 로컬 메모리 (32GB) — 접근 지연: ~80ns cpuset B: cpus=4-7, mems=1 로컬 접근만 허용 (최적 성능) QPI/UPI 잘못된 설정 예 cpuset C: cpus=4-7 (노드1), mems=0 (노드0) → 모든 메모리 접근이 원격 (Remote Access) 결과: ~160ns 접근 지연 (2배), 인터커넥트 대역폭 소모, 성능 30-50% 저하 OOM 시나리오 1. cpuset.mems=1 설정 → 노드 1에서만 메모리 할당 가능 2. 노드 1 메모리 소진 → 다른 노드 사용 불가 (cpuset 제한) 3. OOM killer 발동 → cpuset 내 프로세스 강제 종료 (시스템 전체가 아닌 cpuset 범위 내)

cpuset.mems 설정과 확인

# NUMA 토폴로지 확인
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3
node 0 size: 32768 MB
node 1 cpus: 4 5 6 7
node 1 size: 32768 MB

# CPU와 NUMA 노드 매핑 확인
$ lscpu | grep NUMA
NUMA node0 CPU(s): 0-3
NUMA node1 CPU(s): 4-7

# 올바른 cpuset 설정 (CPU와 메모리 노드 일치)
$ sudo mkdir /sys/fs/cgroup/numa-aligned
$ echo "4-7" | sudo tee /sys/fs/cgroup/numa-aligned/cpuset.cpus
$ echo "1" | sudo tee /sys/fs/cgroup/numa-aligned/cpuset.mems

# 유효 메모리 노드 확인
$ cat /sys/fs/cgroup/numa-aligned/cpuset.mems.effective
1

# 메모리 마이그레이션 활성화 (cpuset.mems 변경 시 기존 페이지도 이동)
$ echo 1 | sudo tee /sys/fs/cgroup/numa-aligned/cpuset.memory_migrate

NUMA OOM 처리와 대응

/* 커널 내부: cpuset OOM 처리 경로 */
/* mm/oom_kill.c */

static void select_bad_process(struct oom_control *oc)
{
    /*
     * cpuset이 설정된 경우, OOM killer는 해당 cpuset의
     * mems_allowed에 속한 태스크만 종료 대상으로 선택합니다.
     *
     * 이는 전체 시스템이 아닌 cpuset 범위 내에서만
     * OOM이 발생한다는 것을 의미합니다.
     */
    if (is_memcg_oom(oc))
        return;

    /* cpuset 제한 내에서 가장 큰 메모리 사용자 선택 */
    for_each_process(p) {
        if (!cpuset_mems_allowed_intersects(current, p))
            continue;  /* cpuset 외부 프로세스는 건너뜀 */
        /* ... oom_badness 점수 계산 ... */
    }
}
위험: cpuset.mems를 너무 제한적으로 설정하면 해당 NUMA 노드의 메모리가 부족할 때 다른 노드에서 빌려올 수 없어 OOM이 발생합니다. 워크로드의 최대 메모리 사용량을 사전에 파악하고, 여유분을 확보하세요.

스케줄러 도메인과 로드 밸런싱

cpuset은 스케줄러 도메인(Scheduling Domain) 구조를 직접 변경합니다. cpuset.cpus.partition=root를 설정하면 커널은 partition_sched_domains_locked()을 호출하여 스케줄러 도메인을 재구축합니다. 이 과정에서 로드 밸런싱 경계가 재설정되며, 격리된 CPU는 다른 CPU와 태스크를 교환하지 않습니다.

cpuset에 의한 스케줄러 도메인 재구축 기본 상태: 단일 스케줄링 도메인 (모든 CPU 포함) SD_NUMA (NUMA 도메인) — 노드 간 로드 밸런싱 SD_MC (노드0: CPU 0-3) SD_MC (노드1: CPU 4-7) 로드 밸런싱 활성 echo "root" > cpuset.cpus.partition 분리 후: 독립 스케줄링 도메인 도메인 A: CPU 0-3 (housekeeping) 로드 밸런싱 활성, 일반 태스크 실행 도메인 B: CPU 4-7 (isolated) 로드 밸런싱 비활성, 명시적 배치만 경계: 태스크 이동 차단 커널 함수 호출 흐름 cpuset_write_resmask() update_cpumasks_hier() rebuild_sched_domains_locked() cpu_attach_domain() kernel/cgroup/cpuset.c → kernel/sched/topology.c

sched_relax_domain_level 튜닝

sched_relax_domain_level은 cpuset 내에서 스케줄러가 로드 밸런싱을 수행하는 도메인 깊이를 제어합니다. 값이 클수록 더 넓은 범위에서 밸런싱하지만, 캐시 친화성이 저하됩니다.

도메인 수준 설명 사용 시나리오
-1 기본값 커널 기본 동작 일반 워크로드
0 없음 밸런싱 비활성화 완전 격리 (지연 민감)
1 SMT 하이퍼스레드 내에서만 SMT 쌍 활용
2 MC 같은 다이 내 코어 L2 캐시 공유 코어
3 NUMA 같은 NUMA 노드 메모리 지역성 유지
4 Cross-NUMA 전체 NUMA 노드 최대 처리량 (지역성 무시)
# cgroup v1에서 sched_relax_domain_level 설정
$ echo 0 | sudo tee /sys/fs/cgroup/cpuset/isolated/cpuset.sched_relax_domain_level

# 스케줄러 도메인 정보 확인
$ cat /proc/schedstat | head -20

# CPU별 스케줄러 도메인 구조 확인
$ find /proc/sys/kernel/sched_domain/ -name "name" -exec sh -c 'echo "$1: $(cat $1)"' _ {} \;
/proc/sys/kernel/sched_domain/cpu0/domain0/name: SMT
/proc/sys/kernel/sched_domain/cpu0/domain1/name: MC
/proc/sys/kernel/sched_domain/cpu0/domain2/name: NUMA

스케줄러 도메인 변경 확인

# CPU별 스케줄러 도메인 확인 (격리 전)
$ for cpu in 0 4; do
    echo "=== CPU $cpu ==="
    for dom in /proc/sys/kernel/sched_domain/cpu$cpu/domain*; do
        echo "  $(basename $dom): name=$(cat $dom/name) flags=$(cat $dom/flags)"
    done
done

# 격리 전 출력 예시:
# === CPU 0 ===
#   domain0: name=SMT flags=4143
#   domain1: name=MC flags=4655
#   domain2: name=NUMA flags=6199
# === CPU 4 ===
#   domain0: name=SMT flags=4143
#   domain1: name=MC flags=4655
#   domain2: name=NUMA flags=6199

# cpuset partition=isolated 설정 후:
# === CPU 0 ===
#   domain0: name=SMT flags=4143     (그대로)
#   domain1: name=MC flags=4655      (그대로)
# === CPU 4 ===
#   (도메인 없음 — 완전 격리, 로드 밸런싱 불가)

# sched_debug로 상세 정보 확인
$ cat /proc/sched_debug | grep -A 5 "cpu#4"

로드 밸런싱 비활성화의 영향

영향 분석: cpuset에서 로드 밸런싱을 비활성화하면 다음 사항이 변경됩니다:
  • migration 스레드 중지 — 격리 CPU의 migration 커널 스레드가 동작하지 않음
  • wake_affine 비활성 — 깨어나는 태스크가 격리 CPU로 이동하지 않음
  • IPI 감소 — 스케줄러 리밸런싱을 위한 IPI(Inter-Processor Interrupt)가 제거됨
  • 태스크 고정sched_setaffinity() 또는 cpuset 프로세스 이동으로만 배치 가능

컨테이너 환경에서의 cpuset 활용

Docker와 Kubernetes는 cpuset을 활용하여 컨테이너에 전용 CPU를 할당합니다. 특히 Kubernetes의 Guaranteed QoS 클래스에서는 CPU Manager가 자동으로 cpuset을 설정하여 정수 단위 CPU를 독점 할당합니다.

컨테이너 런타임과 cpuset cgroup 계층 Kubernetes Node (8 CPU, cpuset.cpus=0-7) kubelet (CPU Manager Policy: static) → cpuset.cpus 자동 설정 시스템 예약 cpuset.cpus = 0-1 kube 예약 cpuset.cpus = 0-1 (공유) 할당 가능 cpuset.cpus = 2-7 Pod 배치 (QoS 클래스별) Guaranteed QoS requests.cpu == limits.cpu (정수) Container A cpu: 2 (전용) cpuset.cpus=2-3 Container B cpu: 2 (전용) cpuset.cpus=4-5 Burstable QoS requests < limits (공유 풀) Container C, D, E cpu.max로 쓰로틀링 cpuset.cpus=6-7 (공유) cgroup 계층 (cgroup v2) /sys/fs/cgroup/ └─ kubepods.slice/ ├─ kubepods-guaranteed.slice/ ← cpuset.cpus=2-5 (전용) ├─ kubepods-burstable.slice/ ← cpuset.cpus=6-7 (공유) └─ kubepods-besteffort.slice/ ← cpuset.cpus=6-7 (공유)

컨테이너 cgroup 계층 구조

컨테이너 런타임(containerd, CRI-O)은 Pod/컨테이너마다 cgroup을 생성하고, cpuset 컨트롤러를 통해 CPU를 할당합니다. systemd 기반 cgroup 드라이버를 사용하는 경우, slice 계층으로 구성됩니다.

# 시스템 cgroup 계층 구조 확인
$ systemd-cgls --no-pager | head -30
Control group /:
├─init.scope
├─system.slice
│ ├─containerd.service
│ └─kubelet.service
└─kubepods.slice
  ├─kubepods-guaranteed.slice
  │ ├─kubepods-guaranteed-podxxxx.slice
  │ │ └─cri-containerd-xxxx.scope
  │ └─...
  ├─kubepods-burstable.slice
  └─kubepods-besteffort.slice

# 특정 컨테이너의 cpuset 확인
$ find /sys/fs/cgroup -name "cri-containerd-*" -exec sh -c '
    echo "Container: $(basename $1)"
    echo "  cpuset.cpus: $(cat $1/cpuset.cpus 2>/dev/null)"
    echo "  cpuset.cpus.effective: $(cat $1/cpuset.cpus.effective 2>/dev/null)"
    echo "  cpuset.mems: $(cat $1/cpuset.mems 2>/dev/null)"
' _ {} \;

# CRI 런타임의 cpuset 설정 흐름:
# kubelet → CRI gRPC → containerd → runc → cgroup 파일 쓰기
# Guaranteed Pod (정수 CPU) → cpuset.cpus에 특정 CPU 할당
# Burstable/BestEffort Pod → 공유 CPU 풀의 cpuset.cpus 사용

Docker cpuset 설정

# Docker에서 cpuset 지정
$ docker run -d --cpuset-cpus="4-7" --cpuset-mems="1" --name rt-app my-rt-image

# cpuset 확인
$ docker inspect rt-app --format '{{.HostConfig.CpusetCpus}}'
4-7

# 컨테이너의 cgroup cpuset 파일 직접 확인
$ cat /sys/fs/cgroup/system.slice/docker-$(docker inspect rt-app --format '{{.Id}}').scope/cpuset.cpus
4-7

# docker-compose에서 cpuset 설정
# docker-compose.yml
# services:
#   rt-service:
#     image: my-rt-image
#     cpuset: "4-7"
#     deploy:
#       resources:
#         limits:
#           cpus: '4'

Kubernetes CPU Manager

# kubelet CPU Manager 설정 (/var/lib/kubelet/config.yaml)
# cpuManagerPolicy: "static"
# cpuManagerReconcilePeriod: "10s"
# reservedSystemCPUs: "0-1"
# topologyManagerPolicy: "single-numa-node"

# Guaranteed QoS Pod 예시 (정수 CPU 요청 → cpuset 독점)
# apiVersion: v1
# kind: Pod
# metadata:
#   name: rt-workload
# spec:
#   containers:
#   - name: worker
#     image: my-rt-image
#     resources:
#       requests:
#         cpu: "4"            # 정수 → cpuset 독점
#         memory: "8Gi"
#       limits:
#         cpu: "4"            # requests == limits → Guaranteed
#         memory: "8Gi"

# CPU Manager 상태 확인
$ cat /var/lib/kubelet/cpu_manager_state
{"policyName":"static","defaultCpuSet":"0-1,6-7",
 "entries":{"pod-uid-1":{"container-A":"2-3"},
            "pod-uid-2":{"container-B":"4-5"}}}

# Pod 내에서 실제 cpuset 확인
$ kubectl exec rt-workload -- cat /sys/fs/cgroup/cpuset.cpus.effective
2-5
팁: Kubernetes에서 NUMA-aware 스케줄링을 위해 Topology Manager(topologyManagerPolicy: single-numa-node)와 CPU Manager(cpuManagerPolicy: static)를 함께 사용하세요. 이렇게 하면 Pod의 CPU와 메모리가 동일 NUMA 노드에 배치되어 최적 성능을 보장합니다.

cpuset 커널 내부 구현

cpuset 서브시스템의 핵심 구현은 kernel/cgroup/cpuset.c에 있습니다. 주요 함수인 cpuset_attach()update_cpumask()의 동작을 이해하면 cpuset이 태스크 마이그레이션과 스케줄러 도메인에 미치는 영향을 깊이 파악할 수 있습니다.

cpuset 커널 내부 구조 struct cpuset css (cgroup_subsys_state) cpus_allowed ← cpuset.cpus (사용자 입력) effective_cpus ← cpuset.cpus.effective (계산) mems_allowed ← cpuset.mems effective_mems ← cpuset.mems.effective partition_root_state ← member/root/isolated subparts_cpus ← 자식에 배포된 CPU flags ← CS_CPU_EXCLUSIVE 등 relax_domain_level ← 밸런싱 도메인 깊이 주요 커널 함수 cpuset_write_resmask() — cpuset.cpus 쓰기 update_cpumask() — CPU 마스크 갱신 update_cpumasks_hier() — 재귀 전파 cpuset_attach() — 태스크 연결 rebuild_sched_domains_locked() — 도메인 재구축 effective_cpus 계산 과정 (계층적 전파) 루트: cpus=0-7, effective=0-7 A: cpus=0-5 effective = {0-5} ∩ {0-7} = 0-5 B: cpus=4-7, partition=root effective = {4-7} (독립) C: cpus=2-6 → effective = {2-5} B가 4-7을 가져갔으므로 A의 effective에서 4-7 제외 가능

핵심 자료구조

/* kernel/cgroup/cpuset.c */

struct cpuset {
    struct cgroup_subsys_state css;

    /* 사용자가 설정한 CPU/메모리 마스크 */
    cpumask_var_t    cpus_allowed;       /* cpuset.cpus */
    nodemask_t       mems_allowed;       /* cpuset.mems */

    /* 실제 유효 마스크 (부모와의 교집합) */
    cpumask_var_t    effective_cpus;     /* cpuset.cpus.effective */
    nodemask_t       effective_mems;     /* cpuset.mems.effective */

    /* 하위 cpuset에 배포된 CPU (partition root용) */
    cpumask_var_t    subparts_cpus;

    /* 플래그 */
    int              flags;              /* CS_CPU_EXCLUSIVE 등 */
    int              pn;                 /* partition 번호 */

    /*
     * partition 상태:
     * PRS_MEMBER   — 부모 도메인에 포함 (기본)
     * PRS_ROOT     — 독립 스케줄링 도메인
     * PRS_ISOLATED — root + 로드 밸런싱 비활성
     */
    int              partition_root_state;

    /* 로드 밸런싱 제어 (v1) */
    int              relax_domain_level;
};

/* 주요 플래그 */
#define CS_CPU_EXCLUSIVE       0x01  /* CPU 독점 */
#define CS_MEM_EXCLUSIVE       0x02  /* 메모리 독점 */
#define CS_SCHED_LOAD_BALANCE  0x04  /* 로드 밸런싱 */
#define CS_SPREAD_PAGE         0x08  /* 페이지 분산 */
#define CS_SPREAD_SLAB         0x10  /* slab 분산 */

cpuset_attach() 태스크 이동 흐름

/* kernel/cgroup/cpuset.c — 태스크를 cpuset에 연결하는 핵심 함수 */

static void cpuset_attach(struct cgroup_taskset *tset)
{
    struct task_struct *task;
    struct cgroup_subsys_state *css;
    struct cpuset *cs = css_cs(css);
    struct cpuset *oldcs;

    /* 잠금 획득 — cpuset 변경은 직렬화 */
    percpu_down_write(&cpuset_rwsem);

    cgroup_taskset_for_each(task, css, tset) {
        oldcs = task_cs(task);

        /* 1. CPU affinity 갱신 */
        cpuset_change_task_nodemask(task, &cs->mems_allowed);

        /* 2. 태스크의 cpus_mask를 새 cpuset으로 갱신 */
        set_cpus_allowed_ptr(task, cs->effective_cpus);

        /*
         * 3. 태스크가 현재 허용되지 않는 CPU에서 실행 중이면
         *    즉시 마이그레이션 트리거
         */
        if (!cpumask_test_cpu(task_cpu(task), cs->effective_cpus))
            do_migrate_task(task);

        /* 4. NUMA 메모리 정책 갱신 */
        cpuset_update_task_spread_flags(cs, task);
    }

    /*
     * 5. 스케줄러 도메인 재구축이 필요한 경우
     *    (partition root 변경 등)
     */
    if (need_rebuild_sched_domains())
        rebuild_sched_domains_locked();

    percpu_up_write(&cpuset_rwsem);
}

update_cpumask() CPU 마스크 갱신

/* cpuset.cpus 파일 쓰기 시 호출되는 핵심 경로 */

static int update_cpumask(struct cpuset *cs,
                          struct cpuset *trialcs,
                          const char *buf)
{
    int retval;

    /* 1. 문자열 파싱 → cpumask 변환 */
    retval = cpulist_parse(buf, trialcs->cpus_allowed);
    if (retval < 0)
        return retval;

    /* 2. 유효성 검사: 온라인 CPU인지 확인 */
    cpumask_and(trialcs->cpus_allowed,
                trialcs->cpus_allowed,
                cpu_active_mask);

    /* 3. 독점 모드 검사: 다른 cpuset과 중복 불가 */
    if (is_cpu_exclusive(trialcs)) {
        retval = validate_change(cs, trialcs);
        if (retval)
            return retval;
    }

    /* 4. 계층적 유효 마스크 갱신 */
    update_cpumasks_hier(cs, &trialcs->cpus_allowed, 0);

    /* 5. 스케줄러 도메인 재구축 */
    rebuild_sched_domains_locked();

    return 0;
}

/*
 * update_cpumasks_hier() — 하위 cpuset에 재귀적으로
 * effective_cpus를 갱신합니다.
 *
 * effective_cpus = cpus_allowed ∩ parent->effective_cpus
 *
 * 부모의 CPU가 줄어들면 자식의 effective도 자동 축소됩니다.
 */
static void update_cpumasks_hier(struct cpuset *cs,
                                  struct cpumask *new_cpus,
                                  int update_partition)
{
    struct cpuset *cp;
    struct cgroup_subsys_state *pos_css;

    css_for_each_descendant_pre(pos_css, &cs->css) {
        cp = css_cs(pos_css);

        /* 부모와 교집합 → 유효 마스크 */
        cpumask_and(cp->effective_cpus,
                    cp->cpus_allowed,
                    parent_cs(cp)->effective_cpus);

        /* 이 cpuset에 속한 태스크의 affinity 갱신 */
        update_tasks_cpumask(cp);
    }
}

partition 상태 머신 구현

cpuset의 partition 상태는 update_parent_subparts_cpumask()에서 관리됩니다. partition root가 되려면 부모에서 충분한 CPU를 가져올 수 있어야 하며, 부모의 유효 CPU가 0이 되면 안 됩니다.

/* kernel/cgroup/cpuset.c — partition 상태 전이 로직 */

/*
 * partition 상태:
 * PRS_MEMBER   (0) — 부모 도메인 내 일반 멤버
 * PRS_ROOT     (1) — 독립 sched domain (CPU 분리)
 * PRS_ISOLATED (2) — root + 로드 밸런싱 비활성
 * PRS_INVALID  (-1) — 유효하지 않은 partition (에러 상태)
 */

static int update_parent_subparts_cpumask(
    struct cpuset *cs,
    int cmd,
    struct cpumask *newmask,
    struct tmpmasks *tmp)
{
    struct cpuset *parent = parent_cs(cs);
    int old_prs, new_prs;

    /*
     * 검증: partition root로 전환 시
     * 부모에 최소 1개 CPU가 남아야 함
     */
    if (cmd == partcmd_enable) {
        cpumask_andnot(tmp->new_cpus,
                       parent->effective_cpus,
                       cs->cpus_allowed);

        /* 부모의 남은 CPU가 없으면 실패 */
        if (cpumask_empty(tmp->new_cpus))
            return PERR_NOCPUS;
    }

    /*
     * 부모의 subparts_cpus에 이 cpuset의 CPU 추가
     * → 부모의 effective_cpus에서 해당 CPU 제거
     * → 이 cpuset이 독립 스케줄링 도메인 획득
     */
    cpumask_or(parent->subparts_cpus,
               parent->subparts_cpus,
               cs->effective_cpus);

    cpumask_andnot(parent->effective_cpus,
                   parent->effective_cpus,
                   cs->effective_cpus);

    /* 스케줄러 도메인 재구축 요청 */
    set_bit(CS_SCHED_LOAD_BALANCE, &cs->flags);
    notify_partition_change(cs, old_prs, new_prs);

    return 0;
}

cgroup 서브시스템 콜백

/* kernel/cgroup/cpuset.c — cpuset cgroup 서브시스템 등록 */

struct cgroup_subsys cpuset_cgrp_subsys = {
    .css_alloc      = cpuset_css_alloc,
    .css_online     = cpuset_css_online,
    .css_offline    = cpuset_css_offline,
    .css_free       = cpuset_css_free,
    .can_attach     = cpuset_can_attach,
    .cancel_attach  = cpuset_cancel_attach,
    .attach         = cpuset_attach,      /* 태스크 이동 */
    .post_attach    = cpuset_post_attach,
    .bind           = cpuset_bind,
    .fork           = cpuset_fork,        /* 새 태스크 생성 시 */
    .legacy_cftypes = cpuset1_files,     /* v1 인터페이스 */
    .dfl_cftypes    = dfl_files,         /* v2 인터페이스 */
    .early_init     = true,
    .threaded       = true,              /* 스레드 모드 지원 */
};

/*
 * can_attach(): 태스크를 이 cpuset으로 이동할 수 있는지 검증
 * - effective_cpus가 비어있으면 ENOSPC 반환
 * - 스레드 모드에서 TID 이동 검증
 */
static int cpuset_can_attach(struct cgroup_taskset *tset)
{
    struct cgroup_subsys_state *css;
    struct cpuset *cs = css_cs(css);
    struct task_struct *task;

    percpu_down_write(&cpuset_rwsem);

    /* cpuset에 유효 CPU가 없으면 태스크 이동 불가 */
    if (cpumask_empty(cs->effective_cpus)) {
        percpu_up_write(&cpuset_rwsem);
        return -ENOSPC;
    }

    cgroup_taskset_for_each(task, css, tset) {
        /* kthread는 특정 CPU에 바인딩되었을 수 있음 */
        if (task->flags & PF_NO_SETAFFINITY) {
            percpu_up_write(&cpuset_rwsem);
            return -EINVAL;
        }
    }

    percpu_up_write(&cpuset_rwsem);
    return 0;
}
소스 코드 참조: 전체 구현은 kernel/cgroup/cpuset.c에 있습니다. partition 관련 로직은 update_parent_subparts_cpumask()update_partition_sd_lb()를 참조하세요. 스케줄러 도메인 재구축은 kernel/sched/topology.cbuild_sched_domains()에서 처리합니다.

ftrace/bpftrace로 cpuset 동작 추적

cpuset 설정 변경이 실제로 어떤 커널 경로를 거치는지 확인하려면 ftrace와 bpftrace를 활용합니다. 특히 태스크 마이그레이션, 스케줄러 도메인 재구축 시점, CPU mask 변경을 실시간으로 관찰할 수 있습니다.

cpuset 추적 포인트와 도구 매핑 사용자 액션: cpuset 파일 쓰기 / 태스크 이동 커널 이벤트 (tracepoint) cgroup:cgroup_attach_task 태스크 cgroup 이동 sched:sched_migrate_task CPU 간 태스크 이동 timer:tick_stop nohz_full 틱 중단 irq:irq_handler_entry 인터럽트 핸들러 진입 sched:sched_switch 컨텍스트 스위치 kprobe:cpuset_attach cpuset 연결 함수 (동적) function_graph: cpuset_attach → set_cpus_allowed_ptr → rebuild_sched_domains_locked 호출 관계와 실행 시간을 트리 형태로 출력 추적 도구 선택 가이드 ftrace 커널 내장, 오버헤드 최소 function_graph, 이벤트 perf 통계 수집, 프로파일링 perf stat, perf record bpftrace 프로그래밍 가능, 유연 kprobe, 히스토그램, 집계 /proc, /sys 파일 실시간 상태 확인 interrupts, schedstat

ftrace를 사용한 cpuset 이벤트 추적

# 1. cpuset 관련 ftrace 이벤트 확인
$ ls /sys/kernel/debug/tracing/events/cgroup/
cgroup_attach_task  cgroup_destroy_root  cgroup_mkdir
cgroup_release      cgroup_remount       cgroup_rmdir
cgroup_setup_root   cgroup_transfer_tasks

# 2. cpuset 태스크 연결 이벤트 추적
$ echo 1 | sudo tee /sys/kernel/debug/tracing/events/cgroup/cgroup_attach_task/enable
$ cat /sys/kernel/debug/tracing/trace_pipe

# 출력 예시:
#   bash-1234 [002] .... 12345.678: cgroup_attach_task:
#     dst_root=1 dst_id=15 dst_level=2 dst_path=/isolated pid=5678 comm=my_app

# 3. sched_migrate_task 이벤트로 실제 마이그레이션 확인
$ echo 1 | sudo tee /sys/kernel/debug/tracing/events/sched/sched_migrate_task/enable

# 4. function_graph로 cpuset_attach 내부 호출 추적
$ echo "cpuset_attach" | sudo tee /sys/kernel/debug/tracing/set_graph_function
$ echo "function_graph" | sudo tee /sys/kernel/debug/tracing/current_tracer
$ cat /sys/kernel/debug/tracing/trace_pipe

# 출력 예시:
#  0)               |  cpuset_attach() {
#  0)   0.850 us    |    cpuset_change_task_nodemask();
#  0)               |    set_cpus_allowed_ptr() {
#  0)   1.200 us    |      __set_cpus_allowed_ptr_locked();
#  0)   1.500 us    |    }
#  0)               |    rebuild_sched_domains_locked() {
#  0)  45.000 us    |      build_sched_domains();
#  0)  48.000 us    |    }
#  0)  52.000 us    |  }

# 5. 추적 정리
$ echo 0 | sudo tee /sys/kernel/debug/tracing/events/cgroup/cgroup_attach_task/enable
$ echo "nop" | sudo tee /sys/kernel/debug/tracing/current_tracer

bpftrace를 사용한 고급 추적

# 1. cpuset CPU 마스크 변경 추적
$ sudo bpftrace -e '
kprobe:update_cpumask {
    printf("PID %d (%s) updating cpumask\n",
           pid, comm);
}

kprobe:rebuild_sched_domains_locked {
    printf("=== Sched domains rebuild at %llu ===\n",
           nsecs);
    print(kstack);
}
'

# 2. 태스크 마이그레이션 지연 측정
$ sudo bpftrace -e '
tracepoint:sched:sched_migrate_task {
    printf("Task %s (PID %d): CPU %d -> CPU %d\n",
           args->comm, args->pid,
           args->orig_cpu, args->dest_cpu);
}
'

# 3. cpuset_attach 실행 시간 측정
$ sudo bpftrace -e '
kprobe:cpuset_attach { @start[tid] = nsecs; }
kretprobe:cpuset_attach {
    $dur = nsecs - @start[tid];
    printf("cpuset_attach took %d us\n", $dur / 1000);
    @hist = hist($dur / 1000);
    delete(@start[tid]);
}
END { print(@hist); }
'

# 4. cpuset cgroup 파일 쓰기 추적
$ sudo bpftrace -e '
kprobe:cpuset_write_resmask {
    printf("cpuset write by %s (PID %d): %s\n",
           comm, pid, str(arg1));
}
'

# 5. 격리 CPU의 인터럽트 빈도 모니터링
$ sudo bpftrace -e '
tracepoint:irq:irq_handler_entry /cpu == 4/ {
    @[args->name] = count();
}
interval:s:10 { print(@); clear(@); }
'

bpftrace 고급 스크립트 예제

# cpuset 변경에 의한 sched domain 재구축 비용 분석
$ sudo bpftrace -e '
kprobe:rebuild_sched_domains_locked {
    @start[tid] = nsecs;
    @stack[tid] = kstack;
}
kretprobe:rebuild_sched_domains_locked {
    $dur = nsecs - @start[tid];
    printf("sched domain rebuild: %d us (by %s PID %d)\n",
           $dur / 1000, comm, pid);
    if ($dur > 100000) {
        printf("  WARNING: slow rebuild!\n");
        print(@stack[tid]);
    }
    @latency = hist($dur / 1000);
    delete(@start[tid]);
    delete(@stack[tid]);
}
END {
    printf("\n=== Rebuild latency histogram (us) ===\n");
    print(@latency);
}
'

# 격리 CPU의 잔여 간섭 소스 종합 분석
$ sudo bpftrace -e '
tracepoint:irq:irq_handler_entry /cpu >= 4 && cpu <= 7/ {
    @irq[cpu, args->name] = count();
}
tracepoint:irq:softirq_entry /cpu >= 4 && cpu <= 7/ {
    @softirq[cpu, args->vec] = count();
}
tracepoint:sched:sched_switch /cpu >= 4 && cpu <= 7/ {
    @switches[cpu] = count();
}
tracepoint:workqueue:workqueue_execute_start /cpu >= 4 && cpu <= 7/ {
    @wq[cpu] = count();
}
interval:s:30 {
    printf("\n=== 30초 간 격리 CPU 간섭 보고서 ===\n");
    printf("--- IRQ ---\n"); print(@irq);
    printf("--- SoftIRQ ---\n"); print(@softirq);
    printf("--- Context Switches ---\n"); print(@switches);
    printf("--- Workqueue ---\n"); print(@wq);
    clear(@irq); clear(@softirq);
    clear(@switches); clear(@wq);
}
'

perf를 사용한 cpuset 이벤트 기록

# cpuset 관련 이벤트를 perf로 기록
$ sudo perf record -e 'cgroup:*' -e 'sched:sched_migrate_task' \
    -a -- sleep 30

# 기록 분석
$ sudo perf script | grep -E "cgroup_attach|sched_migrate"

# 특정 CPU에서 실행된 태스크 프로파일링
$ sudo perf stat -C 4-7 -e context-switches,cpu-migrations,page-faults \
    -- sleep 10

 Performance counter stats for 'CPU(s) 4-7':
                 3      context-switches
                 0      cpu-migrations      # 격리 시 0이어야 함
                 0      page-faults

성능 비교: cpuset vs taskset vs cgroup cpu.max

CPU 제어에는 여러 메커니즘이 있으며, 각각 목적과 동작 방식이 다릅니다. 격리 수준에 따른 지터(jitter) 감소 효과를 이해하면 워크로드에 적합한 방식을 선택할 수 있습니다.

격리 수준별 최대 지터(Max Latency) 비교 지터 (us) 85 42 12 4 격리 수준 → 격리 없음 Max: 85us 스케줄러 노이즈 IRQ 간섭 taskset만 Max: 42us CPU 고정 밸런싱 간섭 cpuset Max: 12us 도메인 격리 partition=root 완전 격리 Max: 4us cpuset+isolcpus +nohz+rcu_nocbs
항목 cpuset (cgroup) taskset / sched_setaffinity cpu.max (cgroup v2)
제어 대상 프로세스 그룹 개별 프로세스/스레드 프로세스 그룹
제어 방식 CPU 집합 제한 (배치) CPU affinity 비트마스크 시간 할당량 쓰로틀링
CPU 독점 지원 (partition=root) 불가 (다른 프로세스도 실행 가능) 불가
NUMA 메모리 cpuset.mems로 제어 별도 numactl 필요 미지원
스케줄러 도메인 도메인 재구축 (격리) 변경 없음 변경 없음
로드 밸런싱 비활성화 가능 정상 동작 정상 동작
지터 감소 높음 (도메인 격리) 낮음 (밸런싱 간섭) 없음 (쓰로틀링만)
계층적 관리 지원 (cgroup 트리) 미지원 지원 (cgroup 트리)
런타임 변경 가능 (파일 쓰기) 가능 (시스템 콜) 가능 (파일 쓰기)
컨테이너 통합 Docker/K8s 기본 지원 수동 설정 필요 Docker/K8s 기본 지원
권장 사용 시나리오 HPC, 실시간, 컨테이너 격리 단일 프로세스 고정 멀티테넌트 공정 분배

성능 벤치마크 예제

#!/bin/bash
# CPU 제어 방식별 지터 측정 스크립트

ISOLATED_CPU=4
ITERATIONS=1000000

echo "===== 1. 기본 (격리 없음) ====="
$ cyclictest -m -n -p 99 -i 1000 -l $ITERATIONS 2>&1 | tail -1
# T: 0 ( 1234) P:99 I:1000 C:1000000 Min:  1 Act:  3 Avg:  4 Max: 85

echo "===== 2. taskset만 사용 ====="
$ taskset -c $ISOLATED_CPU cyclictest -m -n -p 99 -i 1000 -l $ITERATIONS 2>&1 | tail -1
# T: 0 ( 1235) P:99 I:1000 C:1000000 Min:  1 Act:  2 Avg:  3 Max: 42

echo "===== 3. cpuset 격리 (partition=root) ====="
sudo mkdir -p /sys/fs/cgroup/bench
echo "$ISOLATED_CPU" | sudo tee /sys/fs/cgroup/bench/cpuset.cpus
echo "0" | sudo tee /sys/fs/cgroup/bench/cpuset.mems
echo "root" | sudo tee /sys/fs/cgroup/bench/cpuset.cpus.partition
echo $$ | sudo tee /sys/fs/cgroup/bench/cgroup.procs
$ cyclictest -m -n -p 99 -i 1000 -l $ITERATIONS 2>&1 | tail -1
# T: 0 ( 1236) P:99 I:1000 C:1000000 Min:  1 Act:  1 Avg:  2 Max: 12

echo "===== 4. cpuset + isolcpus + nohz_full + rcu_nocbs ====="
# (부팅 파라미터: isolcpus=4 nohz_full=4 rcu_nocbs=4)
$ cyclictest -m -n -p 99 -a $ISOLATED_CPU -i 1000 -l $ITERATIONS 2>&1 | tail -1
# T: 0 ( 1237) P:99 I:1000 C:1000000 Min:  1 Act:  1 Avg:  1 Max:  4

벤치마크 결과 요약

격리 수준 Min (us) Avg (us) Max (us) 비고
격리 없음 1 4 85 스케줄러 노이즈, IRQ 간섭
taskset만 1 3 42 CPU 고정, 밸런싱 간섭 잔존
cpuset (partition=root) 1 2 12 도메인 격리, IRQ 일부 잔존
cpuset + isolcpus + nohz_full 1 1 4 완전 격리, 최소 지터
측정 도구: cyclictestrt-tests 패키지에 포함된 실시간 지연 측정 도구입니다. sudo apt install rt-tests로 설치할 수 있습니다. Max 값이 낮을수록 격리가 효과적입니다.

cpu.max (CFS 대역폭) vs cpuset 비교

cpu.max는 CFS 대역폭 제어(CFS Bandwidth Control)를 사용하여 시간 기반으로 CPU 사용을 제한합니다. cpuset과는 근본적으로 다른 접근 방식으로, 상호 보완적으로 사용할 수 있습니다.

# cpu.max 설정 (CFS 대역폭 제한)
# 형식: $MAX $PERIOD (마이크로초)
# 200000 100000 → 100ms 기간 중 200ms CPU 시간 = 2 CPU 상당
$ echo "200000 100000" | sudo tee /sys/fs/cgroup/app/cpu.max

# cpuset + cpu.max 조합 사용
# cpuset으로 CPU 4-7에 배치, cpu.max로 최대 3 CPU 사용 제한
$ echo "4-7" | sudo tee /sys/fs/cgroup/app/cpuset.cpus
$ echo "300000 100000" | sudo tee /sys/fs/cgroup/app/cpu.max

# 쓰로틀링 발생 여부 확인
$ cat /sys/fs/cgroup/app/cpu.stat
usage_usec 8765432
user_usec 7654321
system_usec 1111111
nr_periods 12345
nr_throttled 42       # 쓰로틀링 발생 횟수
throttled_usec 98765  # 쓰로틀링된 총 시간
특성 cpuset cpu.max (CFS BW)
제어 방식 CPU 배치 (어디서 실행) 시간 할당 (얼마나 실행)
격리 효과 높음 (물리 CPU 분리) 없음 (같은 CPU 공유)
유휴 CPU 낭비 다른 그룹이 사용 불가 다른 그룹이 사용 가능
쓰로틀링 없음 한도 초과 시 발생
지터 영향 감소 (독점) 증가 가능 (쓰로틀링)

실전 시나리오별 권장 설정

시나리오 격리 방식 설정 예
금융 거래 (초저지연) cpuset + isolcpus + nohz_full + rcu_nocbs Max 지터 < 5us
5G 기지국 제어 cpuset partition=isolated + PREEMPT_RT 결정적 지연 보장
게임 서버 cpuset (partition=root) 안정적 FPS 유지
웹 서버 (멀티테넌트) cpu.max + cpuset (공유 풀) 공정 분배 + 격리
CI/CD 빌드 cpu.max 자원 공정 분배
과학 시뮬레이션 (HPC) cpuset + NUMA 정렬 메모리 대역폭 최적화

자동화 격리 스크립트

#!/bin/bash
# cpuset_isolate.sh — 완전 CPU 격리 자동 설정 스크립트
# 사용법: sudo ./cpuset_isolate.sh <CPU_LIST> <NUMA_NODE> <CGROUP_NAME>

CPU_LIST="${1:-4-7}"
NUMA_NODE="${2:-1}"
CGROUP_NAME="${3:-isolated}"
CGROUP_PATH="/sys/fs/cgroup/${CGROUP_NAME}"

set -e

echo "[1/6] cgroup v2 확인..."
if ! mount | grep -q cgroup2; then
    echo "오류: cgroup v2가 마운트되지 않았습니다."
    exit 1
fi

echo "[2/6] cpuset 컨트롤러 활성화..."
echo "+cpuset" | tee /sys/fs/cgroup/cgroup.subtree_control

echo "[3/6] cgroup 생성: ${CGROUP_NAME}..."
mkdir -p "${CGROUP_PATH}"
echo "${CPU_LIST}" | tee "${CGROUP_PATH}/cpuset.cpus"
echo "${NUMA_NODE}" | tee "${CGROUP_PATH}/cpuset.mems"

echo "[4/6] partition 모드 설정..."
echo "isolated" | tee "${CGROUP_PATH}/cpuset.cpus.partition"

echo "[5/6] IRQ affinity 재설정..."
# 격리 CPU에서 IRQ 제거 (housekeeping CPU로 이동)
HOUSEKEEPING_MASK=$(python3 -c "
import os
isolated = set()
for part in '${CPU_LIST}'.split(','):
    if '-' in part:
        a, b = part.split('-')
        isolated.update(range(int(a), int(b)+1))
    else:
        isolated.add(int(part))
ncpus = os.cpu_count()
hk = set(range(ncpus)) - isolated
mask = sum(1 << c for c in hk)
print(f'{mask:x}')
")

for irq in $(ls /proc/irq/ | grep -E '^[0-9]+$'); do
    echo "${HOUSEKEEPING_MASK}" > /proc/irq/$irq/smp_affinity 2>/dev/null || true
done

echo "[6/6] 검증..."
echo "  cpuset.cpus.effective: $(cat ${CGROUP_PATH}/cpuset.cpus.effective)"
echo "  cpuset.cpus.partition: $(cat ${CGROUP_PATH}/cpuset.cpus.partition)"
echo "  cpuset.mems.effective: $(cat ${CGROUP_PATH}/cpuset.mems.effective)"
echo
echo "사용법: echo \$PID > ${CGROUP_PATH}/cgroup.procs"
echo "완료!"

cpuset vs numactl 비교

numactl은 프로세스 수준의 NUMA 메모리 정책을 설정하는 도구입니다. cpuset과 함께 사용할 때의 상호작용을 이해해야 합니다.

# numactl: 프로세스 수준 NUMA 제어
$ numactl --cpunodebind=1 --membind=1 ./my_app

# cpuset: cgroup 수준 NUMA 제어 (더 강력)
$ echo "4-7" | sudo tee /sys/fs/cgroup/app/cpuset.cpus
$ echo "1" | sudo tee /sys/fs/cgroup/app/cpuset.mems

# 주의: cpuset 제한이 numactl보다 우선
# cpuset.mems=0인 cgroup에서 numactl --membind=1을 실행하면
# cpuset 제한에 의해 노드 0에서만 할당됩니다.

# 조합 확인
$ numastat -p $(pgrep my_app)
Per-node process memory usage (in MBs) for PID 5678 (my_app)
                           Node 0          Node 1           Total
                  --------------- --------------- ---------------
Huge                         0.00            0.00            0.00
Heap                         0.00          128.50          128.50
Stack                        0.00            0.25            0.25

cpufreq/cpupower와의 통합

격리 CPU의 주파수를 고정하면 동적 주파수 스케일링(DVFS)에 의한 추가 지터를 방지할 수 있습니다.

# 격리 CPU의 governor를 performance로 설정
$ for cpu in 4 5 6 7; do
    echo "performance" | sudo tee /sys/devices/system/cpu/cpu$cpu/cpufreq/scaling_governor
done

# C-state 비활성화 (idle 진입 방지 → 깨어나는 지연 제거)
$ for cpu in 4 5 6 7; do
    echo 0 | sudo tee /sys/devices/system/cpu/cpu$cpu/cpuidle/state*/disable
    echo 1 | sudo tee /sys/devices/system/cpu/cpu$cpu/cpuidle/state[1-9]*/disable
done

# 또는 커널 파라미터로 전역 설정
# processor.max_cstate=1 intel_idle.max_cstate=0

# 터보 부스트 비활성화 (주파수 변동 제거)
$ echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo

# 현재 주파수 확인
$ cpupower -c 4-7 frequency-info | grep "current CPU frequency"
주의: performance governor와 C-state 비활성화는 전력 소비를 크게 증가시킵니다. 서버 환경에서만 적용하고, 열 관리(thermal throttling)에 주의하세요. CPUFreq, CPUIdle 문서를 참고하세요.

SMT(하이퍼스레딩) 고려사항

SMT(Simultaneous Multi-Threading) 환경에서는 하이퍼스레드 쌍이 캐시와 실행 유닛을 공유합니다. 격리 효과를 극대화하려면 SMT 쌍을 함께 격리하거나 SMT를 비활성화해야 합니다.

# SMT 토폴로지 확인
$ lscpu -e=CPU,CORE,SOCKET,NODE
CPU CORE SOCKET NODE
  0    0      0    0
  1    1      0    0
  2    2      0    0
  3    3      0    0
  4    0      0    0   # CPU4 = CPU0의 하이퍼스레드 쌍
  5    1      0    0   # CPU5 = CPU1의 하이퍼스레드 쌍
  6    2      0    0
  7    3      0    0

# 하이퍼스레드 쌍 확인
$ cat /sys/devices/system/cpu/cpu0/topology/thread_siblings_list
0,4

# 권장: 물리 코어 단위로 cpuset 구성
# 코어 2,3 격리 → CPU 2,3,6,7 (SMT 쌍 포함)
$ echo "2-3,6-7" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus

# 또는 SMT 비활성화 (최고 수준 격리)
$ echo off | sudo tee /sys/devices/system/cpu/smt/control

# 커널 파라미터로 영구 비활성화
# nosmt 또는 mitigations=auto,nosmt

문제 해결과 디버깅

cpuset 설정에서 자주 발생하는 문제와 해결 방법을 정리합니다. 잘못된 설정은 태스크 실행 불가, 성능 저하, OOM 등 심각한 결과를 초래할 수 있습니다.

cpuset 문제 진단 흐름도 cpuset 문제 발생 태스크가 실행되지 않는가? Yes No 원인 확인 cpuset.cpus 비어있음 성능이 예상보다 낮은가? Yes No 원인 확인 NUMA 불일치 or IRQ 잔존 partition 변경이 실패하는가? Yes No 원인 확인 부모 CPU 부족 or 중복 OOM이 발생하는가? Yes 원인 확인 cpuset.mems 노드 메모리 부족 주요 문제와 해결 방법 cpuset.cpus.effective 비어있음 → 부모 cpuset.cpus 확인 후 상위부터 설정 partition=root → invalid 전이 → 부모에 남은 CPU 부족, CPU 재분배 로드 불균형 (CPU 과부하) → sched_relax_domain_level 조정 격리 CPU에서 지터 잔존 → IRQ affinity + RCU + 워크큐 확인 cgroup.procs 쓰기 EINVAL → cpuset.cpus/mems 미설정, 먼저 할당

자주 발생하는 문제와 해결

# === 문제 1: 태스크를 cgroup에 추가할 수 없음 (EINVAL) ===
$ echo $$ > /sys/fs/cgroup/mygroup/cgroup.procs
bash: echo: write error: Invalid argument

# 원인: cpuset.cpus 또는 cpuset.mems가 비어있음
$ cat /sys/fs/cgroup/mygroup/cpuset.cpus
# (비어있음)

# 해결: CPU와 메모리 노드를 먼저 설정
$ echo "0-7" | sudo tee /sys/fs/cgroup/mygroup/cpuset.cpus
$ echo "0" | sudo tee /sys/fs/cgroup/mygroup/cpuset.mems
$ echo $$ > /sys/fs/cgroup/mygroup/cgroup.procs  # 성공

# === 문제 2: partition이 invalid로 전이됨 ===
$ cat /sys/fs/cgroup/isolated/cpuset.cpus.partition
root (invalid)

# 원인: 부모에서 해당 CPU를 더 이상 제공할 수 없음
$ cat /sys/fs/cgroup/cpuset.cpus.effective
0-7
# 다른 형제 cpuset이 같은 CPU를 이미 partition root로 가져감

# 해결: 형제 cpuset의 CPU 배분을 확인하고 겹치지 않게 조정
$ cat /sys/fs/cgroup/sibling1/cpuset.cpus
4-7  # 이미 같은 CPU 사용 중!

# === 문제 3: 격리 CPU에서 예상치 못한 인터럽트 ===
$ cat /proc/interrupts | awk '{print $1, $6}'  # CPU4 열 확인

# 원인: managed IRQ가 여전히 격리 CPU에 배정됨
# 해결: isolcpus에 managed_irq 플래그 추가
# isolcpus=domain,managed_irq,4-7

# === 문제 4: cpuset.cpus.effective가 설정값과 다름 ===
$ echo "0-7" | sudo tee /sys/fs/cgroup/child/cpuset.cpus
$ cat /sys/fs/cgroup/child/cpuset.cpus.effective
0-3  # 설정한 0-7이 아님!

# 원인: effective = cpus_allowed ∩ parent.effective_cpus
$ cat /sys/fs/cgroup/cpuset.cpus.effective
0-3  # 부모가 0-3만 가지고 있음

# 해결: 부모의 cpuset.cpus를 먼저 확장

로드 불균형 디버깅

# CPU별 실행 큐 길이 확인
$ cat /proc/schedstat | awk '/^cpu/ {print $1, "runqueue:", $3}'
cpu0 runqueue: 15423
cpu1 runqueue: 14892
cpu2 runqueue: 15001
cpu3 runqueue: 14756
cpu4 runqueue: 1       # 격리 CPU — 단일 태스크만
cpu5 runqueue: 0
cpu6 runqueue: 0
cpu7 runqueue: 0

# 각 CPU에서 실행 중인 태스크 확인
$ ps -eo psr,pid,comm --sort=psr | awk '$1 >= 4'
  4  5678 my_rt_app    # 의도된 태스크
  4  12   rcuo/4       # ← 이 스레드가 있으면 rcu_nocbs 미적용

# 스케줄러 도메인 플래그 확인
$ cat /proc/sys/kernel/sched_domain/cpu4/domain0/flags
0  # SD_LOAD_BALANCE 없으면 정상 격리

# 격리 CPU의 컨텍스트 스위치 카운트 (최소여야 함)
$ sar -w -P 4 1 5
12:00:01     CPU    cswch/s
12:00:02       4      0.00     # 컨텍스트 스위치 없음 → 정상 격리

문제 해결 체크리스트

cpuset 격리 검증 체크리스트:
  • cat /sys/fs/cgroup/<name>/cpuset.cpus.effective — 의도한 CPU가 유효한지 확인
  • cat /sys/fs/cgroup/<name>/cpuset.cpus.partition — partition 상태가 invalid가 아닌지 확인
  • cat /proc/interrupts — 격리 CPU의 IRQ 카운트가 증가하지 않는지 확인
  • ps -eo psr,pid,comm — 격리 CPU에 의도하지 않은 태스크가 없는지 확인
  • cat /proc/softirqs — 격리 CPU의 softirq 카운트 확인
  • perf stat -C <cpu> -e context-switches — 컨텍스트 스위치가 최소인지 확인
  • cat /proc/sys/kernel/sched_domain/cpu<N>/domain0/flags — 스케줄러 도메인 플래그 확인
  • numactl --hardware — CPU와 NUMA 메모리 노드가 올바르게 짝지어졌는지 확인

커널 로그 분석

cpuset 관련 문제는 dmesg와 커널 로그에서 진단할 수 있습니다. 특히 partition 상태 전이와 CPU 핫플러그 이벤트에 주의하세요.

# cpuset 관련 커널 메시지 확인
$ dmesg | grep -i "cpuset\|sched_domain\|isolcpus\|nohz_full"

# 일반적 메시지 예시:
# [    0.000000] Command line: ... isolcpus=domain,managed_irq,4-7 nohz_full=4-7 rcu_nocbs=4-7
# [    0.123456] sched_isolation: housekeeping mask: 0-3
# [    0.234567] NO_HZ: Full dynticks CPUs: 4-7
# [    0.345678] rcu:   Offloading RCU callbacks from CPUs: 4-7

# CPU 핫플러그 이벤트 (cpuset에 영향)
$ dmesg | grep -i "cpu.*offline\|cpu.*online"

# partition 상태 전이 실패 시 커널 경고
# [  100.123] cpuset: partition root ... has empty effective_cpus
# [  100.124] cpuset: ... forced to be invalid partition

# OOM 발생 시 cpuset 범위 확인
$ dmesg | grep -A 20 "Out of memory"
# ... cpuset=isolated mems_allowed=1 ...

에러 메시지 레퍼런스

에러 발생 상황 해결 방법
EINVAL (cgroup.procs 쓰기) cpuset.cpus 또는 cpuset.mems 미설정 먼저 cpuset.cpus와 cpuset.mems를 설정
ENOSPC (cgroup.procs 쓰기) cpuset.cpus.effective가 비어있음 부모 cpuset의 CPU 범위 확인
EINVAL (partition 쓰기) partition=root로 전환 시 부모 CPU 부족 형제 cpuset의 CPU 중복 확인
EBUSY (cpuset.cpus 쓰기) 독점 CPU가 다른 cpuset에서 사용 중 다른 cpuset의 cpu_exclusive 확인
EPERM (cgroup.procs 쓰기) 권한 부족 (CAP_SYS_ADMIN 필요) root 또는 delegation 설정 확인
partition → "root invalid" 부모에서 CPU를 제공할 수 없게 됨 CPU 재분배 후 partition 재설정
OOM within cpuset cpuset.mems 노드의 메모리 소진 cpuset.mems 확장 또는 메모리 제한 조정

systemd와 cpuset 통합

# systemd 서비스에서 cpuset 설정 (유닛 파일)
# /etc/systemd/system/my-rt-app.service
# [Service]
# Type=simple
# ExecStart=/opt/my-rt-app
# AllowedCPUs=4-7          # cpuset.cpus 설정 (systemd v244+)
# AllowedMemoryNodes=1     # cpuset.mems 설정
# CPUSchedulingPolicy=fifo
# CPUSchedulingPriority=99
# MemoryDenyWriteExecute=yes
# LockPersonality=yes

# systemd-run으로 즉시 cpuset 지정 실행
$ sudo systemd-run --scope \
    -p AllowedCPUs=4-7 \
    -p AllowedMemoryNodes=1 \
    ./my_app

# 현재 서비스의 cpuset 확인
$ systemctl show my-rt-app.service --property=AllowedCPUs
AllowedCPUs=4-7

# 런타임에 cpuset 변경
$ sudo systemctl set-property my-rt-app.service AllowedCPUs=4-5

CPU 핫플러그와 cpuset 상호작용

CPU가 오프라인되면 해당 CPU를 포함하는 cpuset의 effective_cpus가 자동으로 갱신됩니다. 모든 CPU가 오프라인되면 cpuset의 태스크가 실행 불가 상태가 될 수 있습니다.

# CPU 오프라인
$ echo 0 | sudo tee /sys/devices/system/cpu/cpu5/online

# cpuset effective에서 자동 제거됨
$ cat /sys/fs/cgroup/isolated/cpuset.cpus
4-7   # 설정값은 유지
$ cat /sys/fs/cgroup/isolated/cpuset.cpus.effective
4,6-7   # CPU5 제외됨

# CPU 다시 온라인
$ echo 1 | sudo tee /sys/devices/system/cpu/cpu5/online
$ cat /sys/fs/cgroup/isolated/cpuset.cpus.effective
4-7   # 자동 복구

# 위험: 모든 격리 CPU가 오프라인되면?
# → 태스크가 루트 cpuset의 CPU로 대피 (fallback)
# → 커널 로그에 경고 메시지 출력

완전 격리 레시피

최종 완전 격리 설정 (종합): 아래는 CPU 4-7을 완전히 격리하는 데 필요한 모든 단계를 순서대로 정리한 것입니다.
# ====== 부팅 시 설정 (GRUB) ======
# /etc/default/grub
GRUB_CMDLINE_LINUX="isolcpus=domain,managed_irq,4-7 \
    nohz_full=4-7 \
    rcu_nocbs=4-7 \
    irqaffinity=0-3 \
    processor.max_cstate=1 \
    intel_idle.max_cstate=0 \
    nosmt \
    transparent_hugepage=never \
    skew_tick=1"

# ====== 부팅 후 런타임 설정 ======

# 1. cpuset cgroup 설정
$ echo "+cpuset" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
$ sudo mkdir /sys/fs/cgroup/isolated
$ echo "4-7" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus
$ echo "1" | sudo tee /sys/fs/cgroup/isolated/cpuset.mems
$ echo "isolated" | sudo tee /sys/fs/cgroup/isolated/cpuset.cpus.partition

# 2. CPU governor 고정
$ for c in 4 5 6 7; do
    echo performance | sudo tee /sys/devices/system/cpu/cpu$c/cpufreq/scaling_governor
done

# 3. 터보 부스트 비활성화
$ echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo 2>/dev/null || \
  echo 0 | sudo tee /sys/devices/system/cpu/cpufreq/boost 2>/dev/null

# 4. IRQ affinity 재확인
$ for irq in $(ls /proc/irq/ | grep -E '^[0-9]+$'); do
    echo 0f | sudo tee /proc/irq/$irq/smp_affinity 2>/dev/null
done

# 5. irqbalance 격리 CPU 제외
$ sudo systemctl stop irqbalance
echo 'IRQBALANCE_BANNED_CPULIST=4-7' | sudo tee /etc/default/irqbalance
$ sudo systemctl start irqbalance

# 6. 커널 스레드 housekeeping CPU로 이동
$ for pid in $(pgrep -f "\[.*\]"); do
    taskset -cp 0-3 $pid 2>/dev/null || true
done

# 7. 워크로드 실행
$ echo $$ | sudo tee /sys/fs/cgroup/isolated/cgroup.procs
$ exec chrt -f 99 ./my_rt_application

# 8. 격리 상태 최종 검증
$ echo "=== Isolation Status ==="
$ echo "Isolated CPUs:    $(cat /sys/devices/system/cpu/isolated)"
$ echo "nohz_full CPUs:   $(cat /sys/devices/system/cpu/nohz_full)"
$ echo "Effective CPUs:   $(cat /sys/fs/cgroup/isolated/cpuset.cpus.effective)"
$ echo "Partition state:  $(cat /sys/fs/cgroup/isolated/cpuset.cpus.partition)"
$ sudo perf stat -C 4-7 -e context-switches,cpu-migrations -- sleep 5