데이터베이스 B-Tree 페이지 스플릿(Page Split)이 유발하는 디스크 I/O 붕괴와 Fill Factor 튜닝
서비스 런칭 3개월 만에 데이터베이스 쓰기 성능이 반토막 났습니다. 트래픽 증가는 완만했는데도 불구하고, AWS RDS 인스턴스의 IOPS 메트릭은 이상할 정도로 상시 포화 상태를 가리키고 있었습니다. 덤프를 떠서 스토리지 레벨을 분석해보니 주범은 B-Tree 인덱스의 '페이지 스플릿(Page Split)' 현상이었습니다.
B-Tree 인덱스는 데이터를 디스크에 저장할 때 기본적으로 8KB 단위의 블록(페이지)에 차곡차곡 정렬하여 밀어 넣습니다. 만약 유저 아이디처럼 단조 증가하는 값이면 새 페이지를 뒤에 이어 붙이면 그만입니다. 그러나 UUID처럼 무작위 난수가 식별자(PK)로 삽입되거나 업데이트가 잦은 컬럼에 인덱스가 걸려 있다면 재앙이 시작됩니다. 꽉 차 있는 8KB 페이지 중간에 새로운 데이터가 끼어들어야 하는 순간, 데이터베이스 엔진은 어쩔 수 없이 기존 페이지를 반으로 쪼개어 두 개의 새로운 페이지로 분할(Split)한 뒤 빈 공간을 확보합니다. 이 페이지 스플릿 프로세스는 디스크 랜덤 쓰기를 유발할 뿐만 아니라, 쪼개진 두 페이지가 절반만 채워진 채 방치되어 거대한 인덱스 단편화(Fragmentation)를 양산합니다. 결국 디스크 공간 낭비와 SSD 수명 단축으로 이어집니다.
이 은밀한 스토리지 병목을 막기 위해 저희는 인덱스 노드의 'Fill Factor(채우기 비율)' 설정을 100%에서 70%로 자진 하향 조정했습니다. 이는 "데이터를 쓸 때 8KB 페이지를 꽉 채우지 말고, 무조건 30%의 여백 공간을 예약해 두어라"라는 명령어입니다. 이 조치 이후에 무작위 데이터가 삽입되더라도 스플릿 트리거가 발동하지 않고 예약된 여유 공간에 조용히 흡수되었습니다. 디스크 공간은 인위적으로 더 소모하게 되었지만, 빈번한 스플릿 폭풍을 잠재우면서 IOPS 부하는 60% 이상 드라마틱하게 안정화되었습니다. 논리적 쿼리만이 아닌 물리적 디스크 섹터의 레이아웃까지 지배해야 하는 아키텍트의 숙명을 맛본 경험이었습니다.
Related Posts
JVM JIT 컴파일러의 극단적 런타임 최적화: 탈출 분석(Escape Analysis)과 스칼라 치환의 마법
정적 컴파일 언어를 압도하는 자바 머신의 동적 스크립트 프로파일링 및 객체 힙 버림 최적화 기법.
리눅스 eBPF와 XDP를 활용한 커널 바이패스(Kernel Bypass) 초저지연 패킷 필터링 아키텍처
운영체제 네트워크 스택의 병목을 우회하여 디바이스 드라이버 레벨에서 직접 샌드박스 코드를 주입하는 eBPF의 혁명.
스플릿 브레인(Split-Brain) 붕괴를 막는 분산 락(Distributed Lock) 시스템과 펜싱(Fencing) 토큰의 도입
Zookeeper, Redis Redlock의 시계 위임 맹점을 찌르는 가비지 컬렉션 시간 정지(Stop-the-World) 현상 롤백 설계.