Etcd가 자꾸 죽는다면? 범인은 디스크 레이턴시일 수 있습니다 (feat. 위험한 ZFS 튜닝)


분산 시스템을 운영하다 보면 가장 허탈한 순간이 언제인지 아십니까? 애플리케이션 로직도 완벽하고, 네트워크 정책도 문제없는데, 정작 가장 밑단의 ‘물리적인 한계’ 때문에 시스템이 뻗어버릴 때입니다.

최근 Hacker News와 기술 블로그에서 꽤 흥미로운 디버깅 사례를 하나 발견했습니다. ‘Kubernetes 파드가 이유 없이 계속 죽는다면 디스크부터 확인하라’ 는 내용인데, etcd의 내부 동작 원리와 스토리지 계층의 상관관계를 아주 적나라하게 보여주는 사례라 공유해 봅니다.

솔직히 말해서, 이 글에서 소개된 해결책은 프로덕션 환경에서는 ‘절대 금기’에 가깝습니다. 하지만 엔지니어로서 왜 이런 현상이 발생했고, 그들이 왜 그런 선택을 했는지 이해하는 과정은 꽤나 영양가 있습니다.

미스터리: 5분마다 죽어 나가는 파드들

상황은 이렇습니다. 한 팀이 MLSysOps 데모를 위해 엣지 컴퓨팅 환경을 구축하고 있었습니다. NUC, 라즈베리 파이, Jetson AGX Orin을 엮어서 Karmada로 오케스트레이션 하는 구조입니다.

그런데 클러스터를 띄우자마자 Karmada의 파드들이 5~10분 간격으로 계속해서 CrashLoopBackOff 상태에 빠지는 겁니다. 리소스 제한(Limit)도 늘려보고, 네트워크도 확인했지만 원인은 오리무중이었습니다. 로그를 쥐 잡듯이 뒤진 끝에 찾아낸 원인은 애플리케이션이 아니라, etcd의 타임아웃 이었습니다.

원인: Etcd는 I/O 레이턴시에 병적으로 민감하다

많은 엔지니어들이 간과하는 사실이 있습니다. etcd는 단순한 DB가 아닙니다. Strong Consistency(강한 일관성) 를 보장해야 하는 분산 키-값 저장소입니다. 이게 무슨 뜻이냐고요? 데이터가 디스크에 확실히 기록(fsync)되었다는 보장이 없으면 다음 단계로 넘어가지 않는다는 뜻입니다.

Etcd는 WAL(Write-Ahead Log)을 사용하는데, 스토리지 속도가 느려서 fsync 호출이 제때 리턴되지 않으면 다음과 같은 연쇄 작용이 일어납니다.

  1. 디스크 쓰기 지연 발생
  2. Etcd가 내부 하트비트(Heartbeat) 전송 실패
  3. 리더 선출(Leader Election) 실패 또는 쿼럼(Quorum) 상실
  4. API 서버가 응답 불능 상태가 됨
  5. 의존성 있는 파드들이 줄줄이 사망

이 팀의 경우, NUC 장비 위에서 VM을 돌리고 있었는데, 공유 스토리지의 I/O 성능이 etcd의 까다로운 요구사항을 맞춰주지 못했던 겁니다. 프로덕션에서는 보통 etcd 전용으로 고성능 SSD를 할당하지만, 이런 데모용 홈랩 환경에서는 흔히 겪을 수 있는 병목입니다.

해결책: ZFS 튜닝 (혹은 악마와의 거래)

이들이 문제를 해결한 방식이 아주 흥미롭습니다. (동시에 위험합니다.) 스토리지 백엔드로 ZFS를 사용하고 있었는데, 다음과 같이 설정을 변경했습니다.

zfs set sync=disabled default # 동기 쓰기 비활성화 (핵심)
zfs set compression=lz4 default # LZ4 압축 사용
zfs set atime=off default # Access Time 기록 끄기
zfs set recordsize=8k default # etcd 쓰기 패턴에 최적화

하나씩 뜯어보겠습니다.

  • sync=disabled: 이게 바로 ‘매직’이자 ‘폭탄’입니다. ZFS가 데이터가 실제로 디스크에 쓰이기 전에 “어, 다 썼어!”라고 거짓말(ack)을 하게 만듭니다. 덕분에 fsync 레이턴시는 거의 0에 수렴하게 되고 etcd는 행복해집니다. 하지만 전원이 나가면? 마지막 몇 초간의 데이터는 그냥 증발하는 겁니다.
  • recordsize=8k: 이건 칭찬할 만한 튜닝입니다. etcd의 스토리지 엔진인 bbolt는 페이지 단위로 데이터를 쓰는데, ZFS의 기본 레코드 사이즈(128k)와 맞지 않으면 Write Amplification(쓰기 증폭)이 발생합니다. 이걸 8k로 맞춰서 I/O 효율을 높인 건 아주 교과서적인 접근입니다.

결과적으로 이 설정 덕분에 데모는 성공적으로 돌아갔습니다. I/O 대기 시간이 사라지니 클러스터가 안정화된 것이죠.

Hacker News의 반응: “그건 고친 게 아니잖아?”

예상대로 Hacker News의 반응은 뜨거웠습니다. 특히 sync=disabled 설정에 대한 우려가 많았습니다.

“Strong Consistency를 보장하는 DB에서 디스크 지속성(Durability)을 끄면 그게 무슨 의미가 있나? 데이터 유실보다 가용성을 택한 건 알겠는데, 결과가 너무 위험해 보인다.”

“데모 환경이라서 전원 꺼졌을 때 데이터 좀 날아가도 상관없다는 건 알겠다. 하지만 이걸 보고 프로덕션에 적용하는 사람이 생길까 봐 무섭다.”

저도 이 의견에 전적으로 동의합니다. etcd가 fsync에 민감한 건 버그가 아니라 기능(Feature) 입니다. 데이터의 무결성을 지키기 위한 방어 기제인 셈이죠. 이걸 우회하는 건 마치 화재 경보기가 시끄럽다고 배터리를 빼버리는 것과 비슷합니다.

Principal Engineer의 결론

이 사례는 우리에게 두 가지 중요한 교훈을 줍니다.

  1. Kubernetes 문제의 절반은 인프라 문제다: 파드가 죽는다고 해서 무조건 kubectl logs만 볼 게 아닙니다. etcd_disk_wal_fsync_duration_seconds 같은 메트릭을 확인하세요. 99분위(p99) 레이턴시가 100ms를 넘어가면 스토리지를 의심해야 합니다.
  2. 튜닝에는 대가가 따른다: sync=disabled는 데모 환경이나 일회성 테스트 클러스터에서는 훌륭한 팁이 될 수 있습니다. 하지만 프로덕션이라면? 제발 NVMe SSD 를 쓰십시오. 소프트웨어 설정으로 하드웨어의 한계를 극복하려다가는 언젠가 데이터 정합성 깨짐이라는 더 큰 청구서를 받게 될 겁니다.

그래도 recordsize=8k 설정은 etcd를 ZFS 위에서 돌린다면 꼭 기억해둘 만한 팁이네요. 기술 블로그라는 게 원래 정답만 적는 곳이 아니라, 이런 삽질과 트레이드오프를 배우는 곳 아니겠습니까?