버퍼링된 쓰기(write())는 대부분 페이지 캐시에만 반영되고 바로 리턴되지만, dirty 페이지가 일정 기준을 넘으면 커널이 그 프로세스의 write() 호출 자체를 블로킹시켜 디스크가 따라잡을 때까지 강제로 기다리게 한다. 영상 인코더나 로깅 스레드처럼 일정한 주기로 write를 반복하는 프로세스라면 이 순간이 곧 프레임 드롭이나 지연 스파이크로 나타난다. 이 글에서는 dirty 임계값을 바꿔가며 동일한 쓰기 패턴에서 지연시간이 어떻게 달라지는지 실측한다.
dirty 관련 sysctl
$ sysctl vm.dirty_ratio vm.dirty_background_ratio
vm.dirty_ratio = 20
vm.dirty_background_ratio = 10
| 파라미터 | 동작 |
|---|---|
dirty_background_ratio | 전체 메모리의 이 비율을 dirty 페이지가 넘으면 커널 flusher 스레드가 백그라운드로 조용히 쓰기 시작(프로세스는 안 막힘) |
dirty_ratio | 이 비율을 넘으면 write()를 호출한 프로세스 자체가 디스크가 따라잡을 때까지 블로킹됨(balance_dirty_pages) |
7.8GB RAM 기준 기본값은 각각 약 1.56GB(dirty_ratio 20%), 780MB(background_ratio 10%)에 해당한다. _bytes 접미사가 붙은 dirty_bytes/dirty_background_bytes로 절대 용량 기준 설정도 가능하다(둘 중 하나만 활성화되며 설정 시 나머지 방식은 0으로 표시된다).
기본값에서 쓰기 지연 재현
50MB씩 buffered write를 40회(총 2GB) 반복하며 각 write 호출의 소요시간을 측정했다.
chunk 0~20: 대부분 90~110ms
chunk 23: 189.6ms
chunk 24: 287.5ms
chunk 25: 336.9ms
chunk 26: 382.8ms
chunk 27~39: 다시 90~110ms대로 복귀
같은 구간에서 /proc/meminfo의 Dirty 값을 0.5초 간격으로 찍어보면 원인이 보인다.
Dirty: 315036 kB Writeback: 0 kB
Dirty: 583324 kB Writeback: 0 kB
Dirty: 804700 kB Writeback: 16384 kB ← background_ratio(780MB) 돌파, flusher 가동
Dirty: 993116 kB Writeback: 98308 kB
Dirty: 1087328 kB Writeback: 53248 kB ← 여기서부터 write() 지연 급증 구간
Dirty: 302940 kB Writeback: 0 kB ← flusher가 한꺼번에 밀어내며 dirty 급감
dirty가 background_ratio를 넘어 계속 쌓이자 flusher가 대량으로 몰아서 디스크에 쓰기 시작했고, 그 디스크 대역폭을 우리 프로세스의 write()와 나눠 쓰느라 4개 청크(189~382ms)가 평소 대비 2~4배 느려졌다. dirty_ratio(1.56GB)까지는 도달하지 않았지만 background 단계의 경합만으로도 이 정도 스파이크가 발생했다.
임계값을 낮춰 재비교
dirty_background_bytes=50MB, dirty_bytes=100MB로 훨씬 촘촘하게 잡고 같은 쓰기 패턴을 다시 실행했다.
$ echo 52428800 | sudo tee /proc/sys/vm/dirty_background_bytes
$ echo 104857600 | sudo tee /proc/sys/vm/dirty_bytes
min 10.8ms max 113.1ms avg 48.8ms
[88.7, 87.7, 102.8, 100.2, 110.2, 91.4, 65.7, 46.6, 36.2, 113.1,
25.2, 31.2, 25.1, 42.5, 41.4, 35.5, 58.9, 29.8, 34.2, 27.6,
32.1, 10.8, 32.4, 41.6, 25.4, 16.9, 32.3, 22.2, 29.0, 27.1]
최댓값이 382ms → 113ms로 줄었다. 기본값에서는 대부분 빠르다가 임계값을 넘는 순간 한 번에 크게 밀리는 반면, 임계값을 낮추면 처음부터 자주 조금씩 막혀 평균 지연(48.8ms)은 오히려 늘지만 최악의 스파이크는 억제된다.
실제 프레임 예산으로 드롭 횟수 확인
지금까지는 write() 소요시간(ms) 자체만 봤다. 제목의 “프레임 드롭”을 실제로 증명하려면 몇 ms가 걸렸느냐가 아니라 정해진 예산을 넘겼느냐가 기준이어야 한다. 30fps 기준 프레임 예산을 33ms로 잡고, 인코더가 프레임 하나(2MB)를 쓸 때마다 write() 호출이 이 예산을 넘기면 “드롭”으로 센다. 총 1000프레임(2GB)을 연속으로 쓰면서 기본값과 촘촘한 임계값 각각 3회씩 실제 드롭 횟수를 셌다.
import os, sys, time
FRAME_BUDGET_MS = 33.0 # 30fps
FRAME_SIZE = 2 * 1024 * 1024 # 2MB per encoded frame
NUM_FRAMES = 1000 # 1000 * 2MB = 2GB total
path = sys.argv[1]
data = os.urandom(FRAME_SIZE)
f = open(path, "wb")
drops = 0
for i in range(NUM_FRAMES):
t0 = time.time()
f.write(data)
ms = (time.time() - t0) * 1000
if ms > FRAME_BUDGET_MS:
drops += 1
f.close()
print(f"dropped={drops} ({drops/NUM_FRAMES*100:.1f}%)")기본값 (dirty_ratio=20 / dirty_background_ratio=10), 3회
dropped=7 (0.7%)
dropped=8 (0.8%)
dropped=22 (2.2%) → 평균 1.2%
촘촘한 임계값 (dirty_background_bytes=50MB / dirty_bytes=100MB), 3회
dropped=69 (6.9%)
dropped=53 (5.3%)
dropped=39 (3.9%) → 평균 5.4%
드롭 개수 기준으로는 결과가 뒤집힌다 — 임계값을 촘촘하게 낮췄더니 오히려 평균 5.4%로, 기본값(1.2%)보다 4배 넘게 더 많은 프레임이 예산을 넘겼다. 앞서 본 “최댓값이 줄어든다”는 결론과 모순되는 게 아니라, 기준이 다른 것이다. 임계값을 낮추면 개별 스톨의 최대 크기는 확실히 줄지만, 예산을 살짝이라도 넘기는 경미한 지연 자체는 훨씬 자주 일어난다. 하드 리얼타임처럼 “예산을 단 한 번이라도 넘기면 드롭”으로 취급되는 워크로드라면, 임계값을 낮추는 튜닝이 오히려 드롭 횟수를 늘릴 수 있다는 뜻이다.
주의사항
| 항목 | 내용 |
|---|---|
| “최댓값 감소”와 “드롭 횟수 감소”는 다른 지표다 | 임계값을 낮추면 최대 스파이크는 줄지만, 예산을 살짝 넘기는 경미한 지연이 잦아져 하드 데드라인 기준 드롭 횟수는 오히려 늘 수 있다(실측 4배). 무엇을 최소화하고 싶은지(최악값 vs 드롭 횟수)부터 정하고 튜닝할 것 |
| bytes와 ratio는 상호 배타적 | dirty_bytes를 설정하면 dirty_ratio는 화면상 0으로 바뀐다(비활성화된 것이지 실제 0%가 아님). 원복하려면 dirty_ratio/dirty_background_ratio를 다시 써줘야 bytes 값도 0으로 정리된다 |
| 메모리 용량에 따라 절대값이 달라진다 | ratio 기준은 메모리가 클수록 dirty 허용량도 커져, 대용량 메모리 서버에서 한 번에 몰리는 writeback 양이 오히려 더 커질 수 있다. 메모리가 큰 서버일수록 _bytes 방식으로 절대 상한을 두는 걸 권장 |
| 재부팅 시 초기화 | 영구 적용은 /etc/sysctl.conf에 기록해야 한다 |
마무리
dirty 임계값이 크면 평소엔 빠르지만 한 번 몰릴 때 큰 스톨이 온다. 작게 잡으면 항상 약간씩 느리지만 스톨의 크기가 예측 가능한 범위로 줄어든다. 다만 실제 프레임 예산 기준으로 드롭 횟수를 세어보면, 임계값을 낮추는 쪽이 항상 유리한 건 아니었다 — 최악의 스톨은 줄어도 예산을 살짝 넘기는 경미한 지연이 잦아져 드롭 횟수 자체는 오히려 늘었다. “프레임 드롭을 줄이고 싶다”는 목표라면 최댓값이 아니라 실제 드롭 횟수를 직접 재보고 튜닝 방향을 정해야 한다.