왜 스케줄러 튜닝이 필요한가
한 대의 서버에서 배치 잡, 로그 수집, API 워커가 CPU를 두고 경쟁하면 지연이 튀는 구간이 생긴다. 리눅스 기본 스케줄러인 CFS(Completely Fair Scheduler)는 모든 태스크에 가상 실행시간(vruntime)을 부여하고, 가장 적게 실행된 태스크를 먼저 깨우는 방식으로 공정성을 맞춘다. 문제는 "공정"이 곧 "우리 서비스에 유리"를 뜻하지는 않는다는 점이다. 백그라운드 배치가 지연에 민감한 API 스레드와 동일한 몫을 가져가면, 사용자 응답 p99가 흔들린다. 이때 nice 값과 cgroup weight로 CPU 배분 비율을 명시적으로 조정한다.
vruntime과 nice의 관계
nice는 -20(최우선)부터 19(최하위)까지의 값이며, CFS에서는 이 값이 vruntime 누적 속도를 결정하는 가중치(weight)로 변환된다. nice가 1 낮아질 때마다 CPU 몫이 약 1.25배 늘어난다. 즉 nice 0과 nice 5의 차이는 산술적 5가 아니라 대략 3배의 CPU 시간 차이다. 이 지수적 특성 때문에 "조금만 우선순위를 올렸는데 다른 프로세스가 굶는" 상황이 발생한다.
| nice 값 | 상대 weight | CPU 몫(동일 조건 2개 경쟁) |
|---|---|---|
| 0 | 1024 | 기준 |
| -5 | 3121 | 약 3배 |
| 5 | 335 | 약 1/3 |
| 19 | 15 | 거의 굶음 |
nice로 배치 잡 격하하기
지연에 둔감한 배치는 nice를 올려 API 워커에 양보시킨다. 실행 시점에 nice, 이미 뜬 프로세스는 renice를 쓴다.
# 실행하면서 우선순위 격하 (nice 10)
nice -n 10 ./nightly_batch.sh
# 이미 실행 중인 PID의 우선순위 조정
renice -n 10 -p 20481
# 특정 프로세스가 쓰는 CPU와 nice 확인 (NI 컬럼)
ps -o pid,ni,pcpu,comm -p 20481
cgroup v2 weight로 그룹 단위 배분
프로세스가 수십 개로 늘어나면 개별 nice 관리는 무너진다. cgroup v2의 cpu.weight(1~10000, 기본 100)로 그룹 전체의 몫을 지정하는 편이 견고하다. systemd 서비스라면 CPUWeight가 그대로 매핑된다.
# /etc/systemd/system/batch.service
[Service]
ExecStart=/usr/local/bin/batch
CPUWeight=20 # 기본 100 대비 낮은 몫
# 절대 상한이 필요하면 (CPU 0.5개 = 50%)
CPUQuota=50%
# api.service 는 반대로 높게
# CPUWeight=400
weight는 경쟁이 있을 때만 작동하는 상대 비율이다. CPU가 남으면 weight 20짜리도 100%까지 쓸 수 있다. 상한을 강제하려면 CPUQuota(cpu.max)를 함께 건다.
지연 민감 워크로드의 검증
튜닝 후에는 반드시 실측한다. 아래는 nice 차이가 실제 CPU 점유로 이어지는지 간단히 확인하는 스크립트다.
import os, time, multiprocessing as mp
def burn(nice):
os.nice(nice) # 자기 우선순위 조정
end = time.time() + 10
n = 0
while time.time() < end:
n += 1 # 순수 CPU 소모
print(f"nice={nice:>3} iterations={n}")
if __name__ == "__main__":
# 코어 1개에 묶어 경쟁 유발
os.sched_setaffinity(0, {0})
for ni in (0, 10):
mp.Process(target=burn, args=(ni,)).start()
단일 코어에 고정하면 nice 0 프로세스의 반복 횟수가 nice 10보다 확연히 많게 나온다. 이 격차가 기대와 다르면 affinity, cgroup 소속, 다른 스케줄링 클래스를 의심한다.
주의점과 함정
- nice는 CFS(SCHED_OTHER) 안에서만 유효하다. 실시간 클래스(SCHED_FIFO/RR)로 뜬 프로세스에는 영향이 없다. 반대로 실시간 우선순위를 잘못 주면 커널 스레드까지 굶겨 시스템이 멈춘다.
- I/O 지연은 CPU nice로 풀리지 않는다. 디스크 경쟁이라면
ionice와 blkio cgroup을 봐야 한다. - weight와 quota를 혼동하지 말 것. weight는 상대 배분, quota는 절대 상한이다. 버스트가 필요한 워크로드에 quota를 낮게 걸면 오히려 스로틀링으로 지연이 악화된다.
- 커널 6.6부터 기본 스케줄러가 EEVDF로 교체됐다. nice·cpu.weight 인터페이스와 지수적 가중치 개념은 그대로 유지되지만, 지연 배분 로직이 달라 tunable(
sched_latency등) 이름과 동작이 바뀐다. 커널 버전을 먼저 확인하라.
정리
스케줄러 튜닝의 핵심은 "누가 양보해야 하는가"를 명시하는 것이다. 소수 프로세스는 nice/renice로 충분하고, 서비스 단위로 커지면 systemd의 CPUWeight로 관리한다. 무엇을 바꾸든 단일 코어 경쟁 상황에서 실측으로 검증하고, CPU가 아닌 I/O 병목은 별도 도구로 접근하는 원칙만 지키면 대부분의 지연 스파이크는 통제 가능하다.