본문 바로가기

소프트웨어-이야기/프로그래밍 언어와 프레임워크

(Django) Django와 PostgreSQL 성능 개선을 위한 7가지 패턴

Django와 PostgreSQL으로 구성된 서비스를 6개월간 최적화를 하면서, 패턴화한 것들을 정리해보고자 합니다. 

아래의 9가지 원칙을 반복하면, 점진적으로 시스템을 개선할 수 있습니다. 

 

1. 인덱스 추가하기

2. 인덱스를 잘 타도록 쿼리 개선하기

3. N+1 쿼리 줄이기

4. 불필요한 트래픽 줄이기

5. Transaction은 짧게 유지하기

6. 실시간성이 필요하지 않은 기능은 비동기로 처리하기

7. 캐시 적용하기

 

1. 인덱스 추가하기

a. 빈번하게 사용되는 검색조건에는 인덱스를 추가해줍니다. 

디스크 사용량이 우려된다면, 인덱스 파티셔닝을 적절하게 활용하여 인덱스를 추가하는게 좋습니다. 

b. FK에 BTREE Index 추가하기

Ruby On Rails와 Django의 마이그레이션은 FK 관계로 설정되어 있는 컬럼에 자동으로 인덱스를 추가해줍니다. 

Django에 마이그레이션 기능을 사용하지 않는 경우, PostgreSQL DB의 FK에 인덱스를 추가하는 것을 놓치곤 합니다.

때문에 FK에는 꼭 인덱스를 추가해줍니다. 

c. created_at 컬럼에 BRIN Index 추가하기

검색범위를 지정한 조회가 빈번한 테이블이라면, 레코드의 생성 시각을 의미하는 컬럼 (ex. created_at)에 BRIN Index를 추가해줍니다. BRIN INDEX은 B-TREE INDEX보다 디스크 사용량도 적고, 쿼리 속도도 빠릅니다. 

 

2. 인덱스를 잘 타도록 쿼리 개선하기

잘못된 인덱스를 타게되면, 단순 쿼리여도 DB 부하를 일으키는 경우가 있습니다. 이 경우, PostgreSQL의 쿼리 실행 계획을 살펴본 후, 개선지점을 찾아서 튜닝해야합니다. 

여러 테이블을 조인한 쿼리인 경우, 쿼리 실행 계획을 이해하기 어려운 경우가 있습니다. 이 경우, 쿼리 실행 계획 시각화 툴을 사용하면 이해도를 높일 수 있습니다.  

a. 검색범위를 줄이기 위해서, 검색 기간 조건을 지정하기

주문내역 목록 조회 처럼, 히스토리성 데이터를 조회하는 경우에는 검색 기간 조건을 필수로 지정하는 게 좋습니다.

b. 범위가 넓게 설정되어 있는 인덱스들은 삭제하기

인덱스가 너무 넓은 범위를 포함하고 있는 경우, 인덱스가 비효율적으로 사용되는 경우가 있습니다.

이 경우, 파티셔닝 수준의 효과밖에 내지못하고, 다른 인덱스를 탈 수 있는 기회를 뺏고는 합니다.

때문에 이러한 인덱스들은 지워주는게 좋습니다. 

 

3. N+1 쿼리 줄이기

django queryset은 lazy 방식으로 실행됩니다. 때문에 django의 queryset 결과를 적절하게 캐싱해서 사용하는게 좋습니다.

그리고 select_related, prefetch_related 함수를 사용해서, 연관있는 데이터를 한 번에 가져올 수 있도록 하는게 좋습니다. 

 

a. Django QuerySet의 select_related를 남용하지 않는게 좋습니다.

N+1 쿼리를 줄이기 위하여, select_related 함수에 여러개의 모델을 주입하는 경우가 있습니다.

여러 테이블이 한번에 join 되는 경우, 쿼리 실행계획이 복잡해져서 예상하지 못한 방식으로 쿼리가 실행될 수 있습니다.

이 경우, 롱쿼리가 발생하기 쉽습니다. 때문에 prefetch_related 함수를 적절하게 사용하는게 좋습니다. 

 

4. 불필요한 트래픽 줄이기

때로는 프론트에서 API를 호출하는 방식을 개선함으로써 문제를 간단하게 해결할 수 있는 경우가 있습니다. 

이 경우, API 호출수를 줄이거나, 사용되지 않는 데이터는 내려주지 않도록 처리함으로써 성능을 개선할 수 있습니다.

 

5. Transaction은 짧게 유지하기

Trasaction이 길어지는 경우, Lock이 발생하기 쉽습니다. Lock이 늘어나면 LockBlocking이 발생하기 쉽습니다. 이 경우, DB connection, Web Server Connection이 무한정 늘어나기 때문에, 서비스 장애로 이어지기 쉽습니다. 

때문에 Transcation에는 데이터 정합성이 꼭 필요한 비즈니스 로직만 포함하는게 좋습니다. 

 

6. 실시간성이 필요하지 않은 기능은 비동기로 처리하기

실시간성 필요성이 떨어지는 기능은 비동기로 처리하는게 좋습니다.

그래야 API Latency를 줄일 수 있고, 장애의 영향범위를 줄일 수 있습니다. 

예를 들어 주문이 완료 된 후, 이메일을 보내야하는 경우를 생각해봅시다.

이 경우, 주문서를 만드는 것은 실시간성을 필요로 하지만, 이메일을 보내는 기능은 실시간성이 다소 떨어집니다.

이 경우, 이메일전송 기능은 Celery Task으로 빼내서, 비동기로 처리하는게 좋습니다. 

 

7. 캐시 적용하기

DB는 스케일 아웃이 어렵기 때문에, 빈번하게 조회되는 데이터에는 캐시를 적용하는게 좋습니다. 

Cacheops를 사용하면, Django에서 ORM에 좀더 수월하게 캐시를 적용할 수 있습니다.