왜 fsync가 리더를 흔드는가
etcd는 Raft 기반 분산 KV 스토어다. 리더는 클라이언트 쓰기를 받으면 로그 엔트리를 디스크(WAL)에 fsync로 내구화한 뒤에야 팔로워에게 복제하고, 과반이 커밋되면 응답한다. 즉 모든 쓰기 경로에 디스크 fsync가 직렬로 끼어 있다.
문제는 fsync가 느려질 때다. 디스크가 100ms fsync를 하면 쓰기 레이턴시가 그대로 100ms로 튄다. 더 심각한 건 하트비트다. 리더는 주기적으로 팔로워에 하트비트를 보내는데, 디스크 I/O가 리더의 이벤트 루프를 붙잡으면 하트비트가 지연되고, 팔로워는 리더가 죽었다고 판단해 선거를 시작한다. 결과적으로 디스크 지연이 리더 교체(leader election)로 번진다.
증상: 로그와 지표로 확인
먼저 etcd 로그에서 아래 경고가 반복되는지 본다. 이게 보이면 디스크 fsync가 실질 임계치를 넘고 있다는 신호다.
journalctl -u etcd --since "1 hour ago" | grep -E "apply entries took|slow fdatasync|leader changed|took too long"
# 예시 출력
etcdserver: failed to send out heartbeat on time (exceeded the 100ms timeout for 245.3ms)
wal: sync duration of 312.4ms, expected less than 1s
etcdserver: leader changed from a1 to b2
정량 지표는 Prometheus 메트릭이 정확하다. WAL fsync와 백엔드 커밋의 p99를 본다.
# WAL fsync p99 (초)
histogram_quantile(0.99,
rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m]))
# 백엔드 commit p99 (초)
histogram_quantile(0.99,
rate(etcd_disk_backend_commit_duration_seconds_bucket[5m]))
# 리더 교체 누적 횟수 — 증가하면 불안정
etcd_server_leader_changes_seen_total
임계치 기준
SSD 환경에서 건강한 값과 위험 신호를 구분하는 대략적 기준은 다음과 같다.
| 지표 | 건강 | 주의 | 위험 |
|---|---|---|---|
| WAL fsync p99 | < 10ms | 10~25ms | > 50ms |
| backend commit p99 | < 25ms | 25~120ms | > 120ms |
| heartbeat 초과 | 없음 | 가끔 | 지속 발생 |
근본 원인 분류
fsync 지연의 원인은 대개 세 갈래다. 첫째는 디스크 자체가 느린 경우(HDD, 네트워크 스토리지, 버스트 크레딧이 소진된 클라우드 볼륨). 둘째는 디스크 경합 — 같은 볼륨을 로그 수집기나 다른 프로세스가 공유해 fsync가 뒤로 밀리는 경우. 셋째는 DB 크기가 커져 컴팩션/디프래그 시 I/O가 폭증하는 경우다.
실제 디스크 지연을 재현·측정하려면 fio로 fsync 패턴을 흉내 낸다.
fio --rw=write --ioengine=sync --fdatasync=1 \
--directory=/var/lib/etcd --size=22m --bs=2300 \
--name=etcd-fsync-test
# 결과의 fdatasync 항목에서 lat percentiles 확인
# 99.00th 이 10ms를 크게 넘으면 이 디스크는 etcd에 부적합
해결: 디스크와 배치
가장 효과적인 조치는 etcd 데이터 디렉터리를 전용 저지연 SSD/NVMe에 두고 다른 워크로드와 분리하는 것이다. 클라우드라면 IOPS/스루풋이 고정 프로비저닝된 볼륨을 쓴다. OS 스케줄러 우선순위도 올린다.
# etcd 프로세스에 I/O 우선순위 부여 (best-effort 최고 등급)
ionice -c2 -n0 -p $(pgrep -x etcd)
# systemd unit 에 고정
# [Service]
# IOSchedulingClass=best-effort
# IOSchedulingPriority=0
# CPUSchedulingPolicy=fifo
DB가 커졌다면 주기적 컴팩션과 디프래그로 fsync 대상 크기를 줄인다. 단, 디프래그는 그 순간 블로킹이므로 트래픽이 적은 시간대에, 노드별로 순차 실행한다.
# 리비전 기준 컴팩션 후 디프래그 (노드 1대씩)
etcdctl compact $(etcdctl endpoint status --write-out=json \
| grep -o '"revision":[0-9]*' | head -1 | cut -d: -f2)
etcdctl defrag --endpoints=https://10.0.0.11:2379
선거 파라미터 튜닝
디스크를 당장 못 바꾸는 상황이라면 하트비트와 선거 타임아웃을 늘려 일시적 지연이 선거로 번지는 것을 완화할 수 있다. 근본 해결은 아니고 시간을 버는 수단이다. 권장 비율은 election-timeout이 heartbeat-interval의 10배다.
# etcd 실행 인자 (기본값: heartbeat 100ms, election 1000ms)
etcd \
--heartbeat-interval=250 \
--election-timeout=2500
주의: 타임아웃을 키우면 진짜 노드 장애를 감지하고 복구하는 시간도 그만큼 늘어난다. 무한정 키우면 안 되고, 대개 election-timeout 5000ms 이내로 제한한다.
주의점
몇 가지 흔한 함정이 있다. 첫째, 모니터링을 리더 노드에서만 보면 안 된다. 리더는 계속 바뀌므로 세 노드 모두에서 fsync 지표를 수집해야 병목 노드를 찾는다. 둘째, 디프래그를 전 노드에서 동시에 돌리면 그 시간 동안 쿼럼이 흔들려 오히려 리더 교체를 유발한다. 반드시 순차로 한다. 셋째, 클라우드 볼륨의 버스트 크레딧은 벤치 때는 멀쩡하다가 지속 부하에서 소진되며 fsync가 급락하니, 짧은 테스트가 아니라 지속 부하 기준으로 판단한다. 마지막으로, fsync가 정상인데도 leader change가 잦다면 네트워크 지연이나 CPU 스틸을 함께 의심해야 한다.