컨테이너에서 PostgreSQL 가속화
Source: Dev.to
문제
오래된 CI 머신에서 디스크가 느린 상태로 테스트 스위트를 실행했을 때 PostgreSQL이 주요 병목 현상으로 나타났습니다. 각 테스트 실행에 1시간 이상이 소요되었습니다.
원인? 테스트에서 수많은 데이터베이스 작업을 수행했으며, 각 테스트 후 데이터를 정리하기 위해 TRUNCATE 명령을 사용했습니다. 디스크 I/O가 느린 환경에서는 PostgreSQL이 대부분의 시간을 디스크에 데이터를 동기화하는 데 소비했는데, 이는 데이터 영속성이 필요 없는 일시적인 CI 환경에서는 전혀 필요 없는 작업이었습니다.
테스트 실행 중 top을 확인했을 때 다음과 같은 증거가 보였습니다:
242503 postgres 20 0 184592 49420 39944 R 81.7 0.3 0:15.66 postgres: postgres api_test 10.89.5.6(43216) TRUNCATE TABLE
- PostgreSQL이 테이블을
TRUNCATE하는 데 **CPU 81.7 %**를 사용하고 있었습니다. - 단일
TRUNCATE가 15초 이상 실행되었습니다.
디스크가 느린 머신에서는 PostgreSQL이 커널이 데이터를 물리적 저장소에 기록했음을 확인하기를 기다리고 있었으며, 실제로는 테스트 사이에 테이블을 비우는 작업만 필요했을 뿐이었습니다.
해결책 – 세 가지 간단한 PostgreSQL 튜닝
services:
postgres:
image: postgres:16.11-alpine
environment:
POSTGRES_INITDB_ARGS: "--nosync"
POSTGRES_SHARED_BUFFERS: 256MB
tmpfs:
- /var/lib/postgresql/data:size=1g
1. --nosync 플래그
POSTGRES_INITDB_ARGS: "--nosync"은 데이터베이스 초기화 중에 PostgreSQL이fsync()호출을 건너뛰도록 합니다.- CI 환경에서는 내구성을 신경 쓸 필요가 없으므로, 컨테이너가 중단되면 그냥 다시 시작하면 됩니다.
- 이렇게 하면 데이터베이스 설정을 늦추던 비용이 많이 드는 디스크 동기화 작업이 사라집니다.
2. shared_buffers 증가
POSTGRES_SHARED_BUFFERS: 256MB(기본값 약 128 MB에서 증가) 로 PostgreSQL이 자주 접근하는 데이터를 캐시할 메모리를 더 많이 할당합니다.- 테스트에서 동일한 테이블에 반복적으로 접근할 때 유용합니다.
3. tmpfs에 데이터 디렉터리 마운트
tmpfs:
- /var/lib/postgresql/data:size=1g
tmpfs는 PostgreSQL 데이터 디렉터리를 메모리 기반 파일 시스템으로 만들어 줍니다.- 모든 데이터베이스 작업이 RAM에서 이루어지므로 다음과 같이 속도가 크게 향상됩니다:
TRUNCATE작업 – 테스트 간 즉시 정리- 인덱스 업데이트 – 디스크 탐색이 필요 없음
- WAL (Write‑Ahead Log) 기록 – 순수 메모리 작업
- 체크포인트 작업 – 디스크 플러시 대기 없음
1 GB 크기 제한은 대부분의 테스트용 데이터베이스에 충분히 관대합니다; 데이터 양에 따라 조정하세요.
결과
| 지표 | 전 | 후 | 개선 |
|---|---|---|---|
| 전체 테스트 실행 시간 | ~60 min | ~10 min | 6× faster |
단일 테스트 (예: API::FilamentSupplierAssortmentsTest#test_create_validation_negative_price) | 25.5 s | 0.47 s | ≈ 55× faster |
| 평균 테스트당 시간 (24 tests) | 27 s | 0.45 s | ≈ 60× faster |
샘플 출력
tmpfs 최적화 전
API::FilamentSupplierAssortmentsTest#test_create_validation_negative_price = 25.536s
API::FilamentSupplierAssortmentsTest#test_list_with_a_single_assortment = 29.996s
API::FilamentSupplierAssortmentsTest#test_list_missing_token = 25.952s
tmpfs 최적화 후
API::FilamentSupplierAssortmentsTest#test_list_as_uber_curator = 0.474s
API::FilamentSupplierAssortmentsTest#test_list_as_assistant = 0.466s
API::FilamentSupplierAssortmentsTest#test_for_pressman_without_filament_supplier = 0.420s
왜 이것이 작동하는가
TRUNCATE– PostgreSQL이 빈 테이블 상태를 디스크에 동기화하고 있었습니다.- Database initialization – 각 CI 실행마다 데이터베이스를 재생성했습니다.
INSERT– 테스트 픽스처(사용자, 역할 등)를 생성합니다.- Transaction commits – 각 테스트는 롤백되는 트랜잭션 내에서 실행됩니다.
- Frequent small writes – 테스트 실행 중에 발생하는 빈번한 작은 쓰기 작업입니다.
느린 디스크에서는 테스트 사용자를 생성하거나 테이블을 트렁케이트하는 것과 같은 간단한 작업조차 밀리초가 아니라 초가 걸렸습니다. 위의 top 출력은 단일 TRUNCATE가 디스크 I/O를 기다리는 동안 81.7 % CPU를 사용하고 있음을 보여줍니다. 이를 수백 개의 테스트에 곱하면 몇 시간에 달하는 CI 실행 시간이 됩니다.
실용적인 가이드
- Production –
fsync를 활성화한 상태로 유지하고 내구성을 위해 보수적인 설정을 사용하세요. - CI – 데이터는 일시적이며, 속도가 내구성보다 더 중요합니다.
- Profile 파이프라인 – 디스크 I/O가 병목임을 발견했으며, CPU나 메모리는 아닙니다.
- tmpfs는 궁극적인 디스크‑I/O 제거 수단입니다 – 모든 것이 RAM에 있으면 디스크 병목이 사라집니다.
- Memory –
tmpfs는 RAM을 사용합니다; CI 러너에 충분한 메모리가 있는지 확인하세요 (DB에 최소 1 GB 이상).
전체 PostgreSQL 서비스 구성
services:
postgres:
image: postgres:16.11-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: dbpgpassword
POSTGRES_DB: api_test
POSTGRES_INITDB_ARGS: "--nosync"
POSTGRES_SHARED_BUFFERS: 256MB
ports:
- 5432
tmpfs:
- /var/lib/postgresql/data:size=1g
Note:
tmpfs필드는 Woodpecker CI 백엔드에서 공식적으로 지원됩니다(pipeline/backend/types/step.go참고). 스키마 검증 경고가 발생한다면, 이는 오래된 문서 때문일 가능성이 높으며, 해당 기능은 기대대로 작동합니다.
요점
- 작은 설정 조정만으로도 CI 속도에 엄청난 영향을 미칠 수 있습니다.
- 임시 테스트 데이터베이스의 경우 내구성보다 속도에 최적화하는 것이 적절합니다.
tmpfs를 사용하면 디스크‑I/O 병목 현상이 사라져, 몇 시간 걸리던 테스트 실행이 몇 분 안에 끝납니다.
즐거운 테스트 되세요! 🚀
CI + tmpfs: 간단하고 효과적이며 코드 변경이 필요 없음
CI는 기본 Docker 지원을 통해 이를 매우 간단하게 만들어 줍니다 – tmpfs: 필드만 추가하면 끝입니다.
GitHub Actions, GitLab CI 또는 기타 플랫폼을 사용 중이라면 docker run에 --tmpfs 플래그를 사용하거나 커스텀 러너 설정과 같은 우회 방법이 필요할 수 있습니다.
TL;DR: 시도해봤습니다.
tmpfs는 여전히 더 빠르고 또한 더 간단합니다.
Source: …
공격적인 PostgreSQL 튜닝이 tmpfs와 매치될 수 있을까?
tmpfs로 얻은 극적인 개선을 보고 다음과 같이 생각해 보았습니다:
“PostgreSQL 설정을 공격적으로 튜닝하면 비슷한 성능을 낼 수 있을까?”
이는 tmpfs를 사용할 수 없거나 RAM이 제한된 환경에서 유용할 수 있습니다.
실험: 모든 내구성 기능 비활성화
services:
postgres:
command:
- postgres
- -c
- fsync=off # 강제 디스크 동기화 건너뛰기
- -c
- synchronous_commit=off # 비동기 WAL 쓰기
- -c
- wal_level=minimal # 최소 WAL 오버헤드
- -c
- full_page_writes=off # WAL 양 감소
- -c
- autovacuum=off # 백그라운드 자동 청소 비활성화
- -c
- max_wal_size=1GB # 체크포인트 횟수 감소
- -c
- shared_buffers=256MB # 메모리 캐시 확대
이러한 공격적인 설정을 적용해도 tmpfs가 여전히 더 빠른 결과를 보였습니다.
| 기능 / 측면 | 디스크 기반 (fsync=off 적용 시) | tmpfs 기반 |
|---|---|---|
| 파일 시스템 오버헤드 – ext4/xfs 메타데이터 작업 | ❌ | ✅ |
| 디스크 탐색 – 기계적 지연 / 제한된 IOPS | ❌ | ✅ |
| 커널 버퍼 캐시 – 메모리 복사 | ❌ | ✅ |
Docker overlay2 – 스토리지 드라이버 오버헤드 | ❌ | ✅ |
| 설정 복잡도 (7개 이상 옵션) | ❌ | ✅ |
| 순수 RAM 작업 – 물리적 저장소 없음 | ✅ | ✅ |
| 디스크 I/O 제로 | ❌ | ✅ |
| 최대 성능 – RAM보다 빠른 것은 없음 | ❌ | ✅ |
보너스: 고려할 기타 PostgreSQL CI 최적화
여전히 더 빠른 속도 향상을 원한다면, 다음과 같은 조정을 시도해 보세요.
쿼리 로깅 비활성화
command:
- postgres
- -c
- log_statement=none # Don't log any statements
- -c
- log_min_duration_statement=-1 # Don't log slow queries
추가 조정
postgresql.conf의fsync=off– 동기식 쓰기를 비활성화합니다(--nosync와 유사). 임시이거나 영구적이지 않은 환경에서만 사용하세요.work_mem증가 – 각 쿼리에 더 많은 메모리를 할당하여 테스트에서 복잡한 작업을 빠르게 수행할 수 있습니다.