query-optimization

6 개의 포스트

netflix원문

똑같은 질문에 두 번 답하지 마세요: 넷플릭스 규모의 Druid를 위한 인터벌 인식 캐싱 (새 탭에서 열림)

넷플릭스는 Apache Druid를 통해 초당 1,500만 건 이상의 이벤트를 처리하며 대규모 실시간 분석을 수행하고 있으나, 대시보드의 롤링 윈도우(Rolling Window) 쿼리가 생성하는 중복 부하 문제를 해결해야 했습니다. 이를 위해 쿼리에서 시간 범위를 분리하여 처리하는 '구간 인식 캐싱(Interval-Aware Caching)' 레이어를 구축하여 Druid의 계산 리소스를 효율화했습니다. 이 시스템은 과거의 안정된 데이터는 캐시에서 불러오고 오직 최신 데이터만 Druid에 요청함으로써, 대규모 트래픽 상황에서도 쿼리 성능을 안정적으로 유지합니다. ### 기존 캐싱 방식의 한계와 문제점 * **롤링 윈도우의 비효율성**: 실시간 모니터링 대시보드는 10~30초마다 "최근 3시간"과 같은 쿼리를 반복해서 보냅니다. 시간 범위가 계속 이동하기 때문에 Druid의 기존 전체 결과 캐시(Full-result cache)는 매번 미스(Miss)가 발생합니다. * **실시간 데이터 캐싱 제한**: Druid는 데이터의 정확성을 위해 실시간 인덱싱 중인 세그먼트의 결과는 캐싱하지 않습니다. 이로 인해 대시보드가 갱신될 때마다 동일한 실시간 세그먼트를 반복해서 스캔하는 낭비가 발생합니다. * **하드웨어 확장의 한계**: 수십 명의 엔지니어가 동일한 대시보드를 볼 때 발생하는 수천 개의 중복 쿼리를 처리하기 위해 단순히 하드웨어를 증설하는 것은 비용 효율성이 매우 낮습니다. ### 구간 인식 캐싱의 핵심 아이디어 * **데이터의 안정성 활용**: 3시간 전의 데이터는 이미 확정되어 변하지 않지만, 최근 1분 내의 데이터는 지연 도착 등으로 인해 변할 수 있습니다. 이 차이를 이용해 오래된 데이터는 캐시에서 즉시 반환하고, 최신 구간만 Druid에 쿼리합니다. * **쿼리 구조와 시간의 분리**: 쿼리문에서 시간 범위(Interval)를 제외한 나머지 구조(필터, 집계 등)를 SHA-256으로 해싱하여 캐시 키로 사용합니다. 이를 통해 서로 다른 시간 범위를 가진 동일한 목적의 쿼리들이 동일한 캐시 항목을 참조할 수 있게 합니다. * **버킷팅(Bucketing) 구조**: 데이터를 쿼리 단위(예: 1분)별로 잘게 쪼개어 'Map-of-Maps' 형태로 저장합니다. 쿼리가 들어오면 필요한 시간 범위에 해당하는 버킷들을 캐시에서 조회하고, 없는 부분만 골라냅니다. ### 지수적 TTL을 통한 효율적인 데이터 관리 * **신선도와 부하의 트레이드오프**: 데이터 파이프라인의 지연 시간을 고려해 최신 데이터에 약 5초의 캐시 유지 시간(TTL)을 부여합니다. 이는 대시보드 사용자에게는 거의 실시간으로 느껴지면서도 Druid의 부하를 대폭 줄여줍니다. * **데이터 연령에 따른 TTL 차등화**: 데이터가 생성된 지 얼마 안 된 버킷은 5~10초의 짧은 TTL을 가집니다. 데이터가 오래될수록 나중에 도착하는 이벤트가 적어지므로, TTL을 지수적으로 늘려 최대 1시간까지 캐시에 보관합니다. * **자동 보정**: 짧은 TTL 덕분에 최신 데이터 구간에서 발생하는 수정 사항은 빠르게 캐시에 반영되며, 오래된 구간은 긴 TTL을 통해 캐시 적중률을 극대화합니다. ### 시스템 구현 및 작동 워크플로우 * **투명한 프록시 구조**: Druid Router 단계에서 요청을 가로채는 외부 서비스 형태로 구현되었습니다. 클라이언트 앱을 수정할 필요 없이 캐싱 기능을 끄거나 켤 수 있습니다. * **쿼리 분해 및 병합**: 1. 들어온 쿼리를 파싱하여 시간 구간을 확인하고 캐시 키(해시)를 생성합니다. 2. 캐시 저장소(예: Redis/Memcached)에서 요청된 구간에 해당하는 연속된 버킷들을 확인합니다. 3. 캐시에 없는 '가장 최신의 불안정한 구간'으로 쿼리 범위를 축소하여 Druid에 요청합니다. 4. 캐시된 결과와 Druid에서 새로 가져온 결과를 병합하여 클라이언트에 반환합니다. 롤링 윈도우 기반의 대규모 대시보드를 운영하는 환경이라면, 모든 데이터를 매번 다시 계산하기보다 이처럼 시간 구간을 나누어 캐싱하는 전략이 Druid 클러스터의 비용 절감과 성능 향상에 매우 효과적입니다. 특히 데이터가 확정되는 속도에 따라 TTL을 다르게 가져가는 '지수적 TTL' 방식은 데이터 정확도와 효율성 사이의 균형을 잡는 유용한 기술적 패턴입니다.

kakao원문

MongoDB 8.0 업그레이드 해야하는 12가지 이유 (새 탭에서 열림)

MongoDB 8.0은 기존 버전에서 지적받았던 성능상의 아쉬움을 해결하고 안정성을 극대화하는 데 초점을 맞춘 중대한 업데이트입니다. 약 5년의 장기 지원 정책을 도입하여 운영의 지속성을 보장하며, 쓰기 처리량 향상과 쿼리 최적화 등 기술적 아키텍처 개선을 통해 실질적인 성능 이득을 제공합니다. 특히 대규모 트래픽을 처리하는 환경에서 쓰기 지연 시간을 줄이고 복제 효율을 높인 점이 이번 버전의 핵심적인 결론입니다. **장기 지원 정책과 온프레미스 지원 확대** * MongoDB 8.0은 출시 후 5년간(2029년 10월까지) 지원되는 사실상의 LTS(Long-Term Support) 버전으로, 잦은 업그레이드 부담을 줄여줍니다. * 기존에 클라우드(Atlas)에만 우선 적용되던 최신 기능들을 온프레미스 환경에서도 마이너 릴리스를 통해 빠르게 도입할 수 있도록 정책이 변경되었습니다. * 이를 통해 운영 조직은 안정 중심의 운영과 신규 기능 도입 사이에서 유연한 전략을 선택할 수 있는 기반을 마련했습니다. **Write Concern "majority" 성능의 혁신적 개선** * 쓰기 완료 판단 기준을 데이터가 파일에 물리적으로 기록되는 시점(`lastApplied`)에서 Oplog에 기록되는 시점(`lastWritten`)으로 변경했습니다. * 이러한 내부 동작 방식의 변화로 세컨더리 노드의 적용 대기 시간이 단축되어, 쓰기 처리량이 이전 버전 대비 약 30~47% 향상되었습니다. * 세컨더리에서 즉시 읽기 시 발생할 수 있는 데이터 일관성 문제는 '인과적 일관성 세션'을 통해 보완 가능하도록 설계되었습니다. **벌크 쓰기(Bulk Write) 및 Oplog 처리 최적화** * 단일 요청으로 여러 컬렉션에 대한 대량 작업을 동시에 수행할 수 있는 새로운 데이터베이스 명령어가 도입되었습니다. * 기존에 문서마다 개별적으로 생성되던 Oplog 엔트리를 최대 500개까지 하나로 묶어 기록하는 최적화가 적용되었습니다. * 이 개선을 통해 세컨더리 노드의 복제 지연(Replication Lag) 발생 가능성이 크게 낮아지고 전체적인 쓰기 효율이 개선되었습니다. **단건 조회 최적화를 위한 Express Plan 도입** * `_id` 기반의 단건 조회나 유니크 인덱스를 사용하는 쿼리에 대해 복잡한 옵티마이저 과정을 생략하는 'Express Plan'이 추가되었습니다. * 쿼리 파싱 직후 즉시 실행 경로를 확보함으로써 불필요한 플래닝 오버헤드를 제거하고 응답 속도를 극대화했습니다. * 이는 빈번하게 발생하는 PK 기반 조회의 효율을 높여 전체 시스템의 리소스 소모를 줄여주는 효과를 제공합니다. MongoDB 8.0은 성능 저하에 대한 우려를 불식시키기 위해 아키텍처 수준의 최적화를 대거 반영한 버전입니다. 5년이라는 긴 지원 기간과 가시적인 성능 향상을 고려할 때, 대규모 분산 환경을 운영하는 조직이라면 안정화 기간을 거친 후 8.0으로의 업그레이드를 적극적으로 검토할 것을 추천합니다. 특히 쓰기 성능 병목이나 복제 지연 문제를 겪고 있는 서비스에 강력한 해결책이 될 것입니다.

toss원문

레거시 정산 개편기: 신규 시스템 투입 여정부터 대규모 배치 운영 노하우까지 (새 탭에서 열림)

토스페이먼츠는 20년 동안 운영되어 온 레거시 정산 시스템의 한계를 극복하기 위해 대대적인 개편을 진행했습니다. 거대하고 복잡한 단일 쿼리 기반의 로직을 객체지향적인 코드로 분산하고, 데이터 모델링을 집계 중심에서 거래 단위로 전환하여 정산의 정확성과 추적 가능성을 확보했습니다. 이를 통해 폭발적인 거래량 증가에도 대응할 수 있는 고성능·고효율의 현대적인 정산 플랫폼을 구축하는 데 성공했습니다. ### 거대 쿼리 중심 로직의 분할 정복 * **문제점:** 수많은 JOIN과 중첩된 DECODE/CASE WHEN 문으로 이루어진 거대한 공통 쿼리가 모든 비즈니스 로직을 처리하고 있어 유지보수와 테스트가 매우 어려웠습니다. * **도메인 및 기능 분리:** 거대 쿼리를 분석하여 도메인별, 세부 기능별로 카테고리를 나누는 분할 정복 방식을 적용했습니다. * **비즈니스 규칙의 가시화:** 복잡한 SQL 로직을 Kotlin 기반의 명확한 객체와 메서드로 재구성하여, 코드상에서 비즈니스 규칙이 명확히 드러나도록 개선했습니다. * **점진적 전환:** 기능을 단위별로 분할한 덕분에 전체 시스템 개편 전이라도 특정 기능부터 우선적으로 새 시스템으로 전환하며 실질적인 가치를 빠르게 창출했습니다. ### 데이터 모델링 개선을 통한 추적 가능성 확보 * **최소 단위 데이터 관리:** 기존의 집계(Sum) 기반 저장 방식에서 벗어나 모든 거래를 1:1 단위로 관리함으로써, 오류 발생 시 원인 추적을 용이하게 하고 데이터 재활용성을 높였습니다. * **설정 정보 스냅샷 도입:** 계산 결과와 함께 당시의 계약 조건(수수료율 등)을 스냅샷 형태로 저장하여, 시간이 지나도 과거의 정산 맥락을 완벽히 복원할 수 있게 했습니다. * **상태 기반 재처리:** 거래별로 독립적인 상태를 기록하는 설계를 통해, 장애 발생 시 전체 재처리가 아닌 실패한 건만 선별적으로 복구할 수 있도록 효율화했습니다. ### 고해상도 데이터 대응을 위한 DB 최적화 * **파티셔닝 및 인덱스 전략:** 정산일자 기준의 Range 파티셔닝과 복합 인덱스를 활용해 데이터량 증가에 따른 조회 성능 저하를 방지했습니다. * **조회 전용 테이블 및 데이터 플랫폼 활용:** 실시간 조회가 필요한 핵심 기능은 전용 테이블로 대응하고, 복잡한 어드민 통계는 고성능 데이터 플랫폼에 위임하여 시스템 부하를 분산했습니다. ### 배치 시스템 성능 극대화 * **I/O 횟수 최소화:** 배치 실행 시점에 가맹점 설정 정보를 전역 캐시에 로드하여 반복적인 DB 조회를 제거했습니다. * **Bulk 조회 및 처리:** Spring Batch의 `ItemProcessor`에서 개별 건별로 I/O가 발생하지 않도록 Wrapper 구조를 도입하여 묶음 단위(Bulk)로 조회하도록 개선했습니다. * **멀티 스레드 활용:** `AsyncItemProcessor`와 `AsyncItemWriter`를 도입하여 단일 스레드 제약을 극복하고 처리 속도를 비약적으로 향상시켰습니다. 이번 개편은 단순히 기술적인 스택을 바꾸는 것을 넘어, 레거시 시스템에 숨겨진 복잡한 비즈니스 맥락을 명확한 도메인 모델로 추출해냈다는 점에서 큰 의미가 있습니다. 대규모 트래픽과 복잡한 정산 규칙을 다루는 시스템이라면, 데이터를 최소 단위로 관리하고 I/O 최적화와 캐싱을 적극적으로 활용하는 아키텍처를 검토해볼 것을 추천합니다.

datadog원문

Husky: Datadog 규모의 효율적인 컴팩션 (새 탭에서 열림)

Husky는 대규모 관측(observability) 데이터를 처리하기 위해 객체 스토리지 위에 구축된 분산 저장 시스템으로, 매일 수조 개의 이벤트를 효율적으로 관리하는 데 최적화되어 있습니다. 이 시스템은 데이터를 '조각(fragment)' 단위로 저장하고 컴팩션(compaction) 과정을 통해 쿼리 성능과 스토리지 비용 사이의 최적의 균형을 맞추는 것을 핵심 전략으로 삼습니다. 특히 파운데이션DB(FoundationDB)를 활용한 원자적 메타데이터 관리와 병렬 워커 기반의 스캔 구조를 통해 데이터 가용성을 유지하면서도 대규모 분석 쿼리를 신속하게 처리합니다. ## Husky의 쿼리 실행 및 조각화 구조 * Husky는 유입된 이벤트를 조각(fragment)이라 불리는 파일로 묶어 객체 스토리지(S3, GCS 등)에 저장하며, 각 조각에 대한 메타데이터를 별도로 관리합니다. * 쿼리 실행 시 시스템은 메타데이터를 검색하여 관련 있는 조각들을 식별하고, 이를 워커(worker) 풀에 분산하여 병렬로 스캔합니다. * 전체 쿼리 비용은 객체 스토리지에서 가져와야 하는 조각의 수와 해당 파일 내에서 스캔해야 하는 이벤트 수에 비례합니다. * 따라서 효율적인 조회를 위해 파일 수를 제어하는 '스트리밍 머지' 방식의 컴팩션과 쿼리당 스캔 이벤트를 줄이는 데이터 조직화 전략을 사용합니다. ## 컴팩션의 "골디락스(Goldilocks)" 문제 컴팩션은 여러 작은 조각을 하나의 큰 조각으로 병합하는 과정으로, 시스템의 효율성을 결정하는 핵심 요소입니다. Husky는 다음 요소들 사이에서 최적의 균형점(Goldilocks)을 찾습니다. * **파일 크기의 상충 관계:** 파일이 너무 작으면 객체 스토리지 접근 지연 시간과 메타데이터 부하가 커지며, 반대로 너무 크면 쿼리 워커 간의 병렬 처리가 제한되어 대규모 쿼리 속도가 느려집니다. * **컴팩션 비용과 성능:** 컴팩션 작업 자체도 CPU와 객체 스토리지 I/O 비용을 발생시키므로, 작업을 최소화하면서도 쿼리 성능을 높일 수 있는 적정 수준의 병합이 필요합니다. * **데이터 레이아웃 최적화:** 컴팩션 시 시간적 혹은 공간적(태그 등) 유사성에 따라 데이터를 재배치하면 압축률이 향상되고 쿼리 시 스캔해야 할 데이터 범위를 좁힐 수 있습니다. * **벡터화 실행:** Husky 워커는 많은 행을 빠르게 스캔하기 위해 벡터화된 실행(vectorized execution) 방식을 사용하며, 이는 적절한 크기의 조각에서 가장 효율적으로 작동합니다. ## FoundationDB를 통한 원자적 상태 관리 * 데이터 유입이 빈번한 환경에서 사용자가 즉시 데이터를 조회할 수 있도록, Husky는 유입 경로에서 짧은 버퍼링 후 작은 조각들을 빠르게 생성합니다. * 수많은 조각의 메타데이터를 관리하기 위해 트랜잭션 보장이 강력한 FoundationDB를 메타데이터 저장소로 사용합니다. * 컴팩션이 완료되면 FoundationDB의 트랜잭션 기능을 이용해 이전 조각들을 새 조각으로 '원자적(atomic)으로 교체'합니다. * 이를 통해 쿼리 시스템은 컴팩션 진행 중에도 데이터 중복이나 누락 없이 항상 일관된 상태의 테이블을 조회할 수 있습니다. 대규모 시계열 및 관측 데이터를 다루는 시스템을 설계할 때는 무조건적인 데이터 병합보다는 쿼리 패턴과 객체 스토리지의 특성을 고려한 컴팩션 정책이 중요합니다. 특히 메타데이터 계층에서 원자성을 확보하여 데이터 일관성을 유지하고, 병렬 스캔의 이점을 극대화할 수 있는 '적정 크기'의 데이터 블록을 유지하는 설계가 권장됩니다.

figma3분 읽기큐레이션 요약

Figma에서의 속도 탐

Figma는 검색 지연의 원인을 OpenSearch 자체의 검색 속도보다 쿼리 전후 처리와 잘못된 모니터링 지표에서 발견했다. OpenSearch가 보고한 평균 8ms는 전체 검색 시간이 아니라 개별 샤드 쿼리 시간에 불과했으며, 실제 애플리케이션 호출은 평균 150ms에 달했다. 조사 결과 권한 필터를 만드는 사전 처리와 검색 결과를 검증하는 사후 처리가 전체 시간의 대부분을 차지했고, 이를 개선하는 것이 확장 가능한 검색 기반을 마련하는 핵심이었다. ## OpenSearch 마이그레이션과 검색 성능 문제 - Figma는 2023년 말까지 오래된 Elasticsearch 버전을 사용하다가 AWS 관리형 OpenSearch로 이전하기 시작했다. - OpenSearch는 Elasticsearch의 라이선스 변경 이후 분기된 프로젝트로, 기본적으로 호환되지만 3년간 세부적인 차이가 누적되어 마이그레이션이 예상보다 어려웠다. - 사용자와 데이터가 증가하면서 검색 시스템이 원하는 콘텐츠를 안정적으로 찾기 어려워졌고, 장기적인 확장을 위한 검색 인프라 재정비가 필요해졌다. ## 잘못 해석한 8ms 지표 - Datadog의 OpenSearch 연동에서는 평균 검색 시간이 약 8ms로 나타났다. - 하지만 Figma 검색 API의 p99 지연 시간은 거의 1초였고, 애플리케이션에서 OpenSearch API를 호출하는 데 걸린 시간은 다음과 같았다. - 평균 약 150ms - p99 약 200~400ms - 최소 지연 시간도 40ms 이상 - OpenSearch와 애플리케이션이 같은 AWS 가용 영역에서 실행되고 있었기 때문에 네트워크 지연만으로는 이 차이를 설명할 수 없었다. - 원인은 8ms가 전체 검색 시간이 아니라 **각 샤드에서 실행된 개별 쿼리의 평균 시간**이었기 때문이다. ## OpenSearch의 쿼리 및 fetch 단계 - 검색 요청은 먼저 coordinator 노드에 전달된다. - coordinator 노드는 인덱스의 각 샤드가 있는 worker 노드에 쿼리를 보낸다. - 이 과정이 OpenSearch의 **query 단계**다. - coordinator는 각 샤드의 결과를 수집하고 정렬한 뒤, 상위 결과에 대한 상세 정보를 다시 요청한다. - 이 후속 과정이 **fetch 단계**이며, 최종 결과가 클라이언트에 반환된다. - Figma의 초기 구성에서는 사용자 검색 하나가 최대 500개의 샤드 쿼리를 발생시킬 수 있었다. - 샤드 쿼리 대부분은 병렬 실행되지만 모두 동시에 처리되는 것은 아니므로, 개별 샤드 시간과 전체 검색 시간 사이에 큰 차이가 발생했다. ## 전체 검색 시간을 측정하도록 계측 개선 - Figma는 검색 코드의 주요 구간에 메트릭과 트레이스를 추가했다. - 조사 결과 OpenSearch가 보고하는 지표와 실제 API 호출 시간 사이에 큰 불일치가 있음을 확인했다. - OpenSearch의 기본 메트릭과 로그에는 coordinator 관점의 전체 쿼리 시간이 포함되지 않았다. - 전체 검색 시간은 검색 API 응답의 `took` 필드에만 제공됐다. - Figma는 모든 검색 응답에서 `took` 값을 추출해 모니터링 시스템에 추가했고, 이를 통해 실제 백엔드 검색 지연을 보다 정확하게 파악했다. ## 병목은 OpenSearch 검색 자체가 아니었다 - 실제로 OpenSearch에서 검색을 기다리는 시간은 전체 쿼리 API 시간의 30% 미만이었다. - 나머지 시간은 검색 전후의 애플리케이션 처리에 사용됐다. - **사전 처리** - 사용자가 접근할 수 있는 파일 관련 정보를 조회한다. - 접근 권한이 없는 파일을 대부분 제외하도록 OpenSearch 필터 절을 생성한다. - **사후 처리** - OpenSearch가 반환한 각 파일 결과에 대해 사용자의 실제 접근 권한을 다시 확인한다. - 권한 검증을 통해 사용자가 볼 수 없는 파일이 검색 결과에 포함되지 않도록 보장한다. - 특히 사후 처리가 매우 느렸으며, Figma는 권한 시스템과 협력해 이 부분을 개선하기 시작했다. 검색 성능을 개선하려면 검색 엔진이 표시하는 단일 지표만 믿지 말고, coordinator부터 애플리케이션의 사전·사후 처리까지 전체 요청 경로를 측정해야 한다. Figma 사례처럼 샤드별 지연 시간보다 실제 사용자 요청의 전체 지연 시간을 기준으로 병목을 찾아야 하며, 권한 필터 생성과 결과 검증도 검색 성능의 핵심 구성 요소로 다뤄야 한다.

원문 읽기(새 탭에서 열림)
figma4분 읽기큐레이션 요약

사후 분석: 202

2020년 1월 21~22일 Figma 장애는 장시간 실행된 고비용 쿼리와 PostgreSQL의 잘못된 쿼리 실행 계획, 그리고 이로 인해 누적된 공격적 autovacuum이 복합적으로 발생한 사건이었다. 1월 21일에는 문제 쿼리를 취소해 복구했지만, 그 여파로 데이터베이스 정리 작업이 밀리면서 다음 날 쓰기 IOPS와 잠금 경합이 급증했다. Figma는 PostgreSQL 11로 업그레이드해 문제를 안정화했고, 이후 고비용 쿼리 모니터링과 실행 시간 제한을 강화하기로 했다. ## 장애 발생 경과 ### 1월 21일: 장시간 실행 쿼리 - 오전 6시 11분, 자동 모니터링에서 오류율 증가를 감지했다. - 조사 결과, 데이터베이스 CPU를 과도하게 사용하는 장시간 실행 쿼리가 발견됐다. - 오전 6시 54분 해당 쿼리를 취소하자 성능이 정상으로 돌아왔다. - 그러나 쿼리 취소 과정에서 데이터베이스 내부에 정리해야 할 작업이 누적되었고, 이것이 다음 날 장애의 배경이 됐다. ### 1월 22일: 쓰기 부하와 잠금 경합 - 데이터베이스 CPU는 평소보다 낮았지만 쓰기 IOPS와 잠금 경합이 증가했다. - 오전 11시 42분에는 API 요청이 대기열에 쌓이며 일부 사용자가 서비스를 이용할 수 없게 됐다. - 불필요한 쿼리를 취소하고 할당된 IOPS를 늘려 일시적으로 안정화했지만, 오후 2시경 다시 성능이 악화됐다. - 데이터베이스를 재시작해 문제를 일으킨 것으로 추정한 백그라운드 프로세스를 임시 비활성화했다. - 오후 7시부터 긴급 점검을 진행하며 PostgreSQL을 업그레이드했고, 오후 8시 15분 서비스와 데이터베이스 지표가 정상화됐다. ## 공격적 autovacuum의 영향 - PostgreSQL의 autovacuum은 오래된 행 버전을 정리하고 트랜잭션 ID 고갈을 방지하는 자동 유지보수 작업이다. - 1월 21일 장시간 쿼리가 종료된 뒤 정리 대상 데이터가 대량으로 쌓였다. - 이 backlog가 임계치를 넘으면서 트랜잭션 ID 래핑을 막기 위한 더 공격적인 autovacuum이 실행됐다. - 당시 사용하던 PostgreSQL 버전에서는 이 작업이 테이블 잠금과 쓰기 작업에 큰 영향을 줬다. - Figma가 대형 테이블에서 실행 중인 autovacuum을 취소하자 지표가 일시적으로 개선됐지만, 작업은 다시 재개됐다. - `autovacuum_freeze_max_age` 값을 조정하고 데이터베이스를 재시작해야 공격적 autovacuum을 완전히 억제할 수 있었다. - 다만 autovacuum을 비활성화한 뒤에도 쓰기 지연과 잠금 경합이 계속되어, 이것만이 유일한 원인은 아니라고 판단했다. ## 잘못된 쿼리 실행 계획 - 문제의 핵심 쿼리는 복잡한 서브쿼리를 포함하고 있었고, 잠금 경합에 반복적으로 관여했다. - PostgreSQL 9의 통계 정보가 변경된 뒤 쿼리 플래너가 비효율적인 실행 계획을 선택한 것으로 분석됐다. - 실행 계획은 실제 결과가 3개 행뿐인데도 2천만 개 이상의 행을 반환할 것으로 잘못 추정했다. - 그 결과 인덱스를 사용하지 않고 전체 테이블 스캔을 수행했다. - 처리 과정에서 임시 버퍼에 대량의 데이터를 기록해 높은 쓰기 IOPS와 임시 데이터 사용량을 유발했다. - 즉, 낮은 CPU 사용률만으로 데이터베이스 상태가 양호하다고 판단하기 어려웠으며, 쓰기 지연·잠금·임시 버퍼 사용량을 함께 살펴야 했다. ## PostgreSQL 11 업그레이드 - PostgreSQL 9.6 이후에는 autovacuum과 관련된 중요한 최적화가 포함됐다. - PostgreSQL 10 이상에서는 Amazon RDS의 성능 분석 도구가 개선되어 원인 조사에 도움이 된다. - Figma는 이미 스테이징 환경을 PostgreSQL 11로 업그레이드하고 수개월간 테스트한 상태였다. - 운영 환경 업그레이드 절차도 사전에 마련해 두었기 때문에 긴급 상황에서도 업그레이드를 진행할 수 있었다. - PostgreSQL 11의 개선된 쿼리 플래너는 문제가 된 실행 계획이 선택될 가능성을 제거했다. - autovacuum의 성능 특성도 PostgreSQL 9에서 11로 오면서 개선되어 두 가지 주요 원인을 함께 완화했다. ## 후속 조치 - 고비용·장시간 실행 쿼리에 대한 모니터링을 강화한다. - 쿼리가 실행될 수 있는 시간에 더 엄격한 제한을 둔다. - 실행 계획의 예상 행 수와 실제 행 수 사이의 큰 차이를 지속적으로 점검한다. - CPU뿐 아니라 쓰기 IOPS, 쓰기 지연, 잠금 경합, 임시 버퍼 사용량, autovacuum backlog를 함께 모니터링해야 한다. - 데이터베이스 버전 업그레이드는 장애 발생 후 처음 검토하기보다, 스테이징 테스트와 운영 전환 계획을 미리 준비해야 한다. 실무적으로는 장시간 쿼리에 타임아웃을 설정하고, 정기적으로 `EXPLAIN ANALYZE`와 실행 계획 변화를 검토하며, autovacuum 상태를 별도 지표로 관리하는 것이 중요하다. alc-vesm.

원문 읽기(새 탭에서 열림)