✏️ 성능에 핵심인 DB
풀 스캔
- 모든 데이터를 순차적으로 읽는 행위
- where절이 없을 때
- 인덱스보다 전체 데이터 탐색이 빠를 때
- 풀스캔에 대해서는 항상 고려해야 합니다.
- DB 부하 → 연결된 모든 서비스 응답시간 증가
✏️ 조회 트래픽을 고려한 인덱스 설계
일반 시스템
- 대부분의 서비스는 GET 응답의 비율이 POST 요청 실행 비율보다 많습니다.
- DB 설계 → 조회 + 트래픽 규모 고려해야 합니다.
- 조회 패턴 기준 인덱스 설계
B2B 기업의 경우
- 직원 수가 한정적
- 게시물은 특정 일에 올리고 1번만 조회하는 경우가 많음
- ex) 공지 사항을 매주 1건씩 올리고 10년간 등록을 할 경우 520건의 데이터,
1000명의 직원이 5분 이내로 글을 읽는다고 가정할 경우, 초당 게시글 읽기 요청 6.67 TPS가 된다.
- TPS = 총 요청 수 / 총 시간(초) → 여기서는 아마 5분 이내를 평균 치로 했을 때 150초가 나온듯 합니다.
결론
- 게시글 수가 적고, 최대 TPS가 10 미만인 상황에서 조회 성늘을 올리기 위해 인덱스를 사용할 이유가 없습니다.
- 테이블이 풀스캔되어도 성능 문제 발생 X
B2C 기업의 경우
- 게시물을 볼 수 있는 사용자가 무한적
- 전체 게시글도 사용자 수의 비례해서 증가
- ex) 전체 게시글 1000만 건 → 사용자 게시글 목록 조회 시 특정 카테고리 속한 게시글을 찾기 위해 1000만 건의 데이터를 비교한다.
많은 사용자가 동시에 게시글 목록 조회 사용 → 다수의 풀 스캔이 발생 → DB 장비의 CPU 사용률이 100%에 도달하면서 DB가 기능을 잃음
결론
- 풀 스캔이 발생하지 않도록 조회 패턴을 기준으로 인덱스 설계
전문 검색 인덱스
- 제목에 특정 단어 포함 게시글 검색 기능
- like를 이용해서 조건을 지정한다.
결론
- 엘라스틱 서치 이용 → DB를 사용하지 않고 검색 기능 구현
- 오라클 TEXT
- MySQL의 FULLTEXT
단일 인덱스와 복합 인덱스
사용자당 가질 수 있는 데이터가 얼마나 될지, 어떤 인덱스를 해야 이득일지 고민합니다.
단일 인덱스
- 필요한 쿼리가 두개 있어도 한개가 많이 호출 안되면 단일 인덱스 사용
- ex) 개별 사용자 기준으로 1주일에 하루 방문, 평균 활동 데이터 5건 → 1년 260건, 5년 1500건
- 몇천 건이 안되는 데이터를 비교할 때 시간은 짧습니다 (DB기준 약 0.1초 이내)
복합 인덱스
- 활동성이 좋은 칼럼을 조합해야 할 경우
- ex) 개별 사용자가 매일 방문, 30번 이상의 활동 → 1년 1만 건이상
- 데이터가 많아서 조회 속도 느려짐
- 복합 인덱스 사용 시 조회 성능 문제 발생 X
- 단, 하루에 1번만 실행해도 되는 쿼리문일 경우 단일 인덱스 고려
- 서비스가 성장하여 데이터가 수백만개 이상일 경우는 복합 인덱스 고려
커버링 인덱스를 사용할 경우 실제 데이터를 읽지 않기 때문에 조회 시간을 단축할 수 있습니다.
선택도를 고려한 인덱스 칼럼 선택
선택도
- 인덱스에서 특정 칼럼의 고유한 값 비율
- 선택도가 높으면 인덱스를 이용한 조회 효율이 높아진다.
선택도가 낮은 예시
- 성별 칼럼
- M,F,N의 3개 값 중 하나를 갖는다.
- 절반 정도의 데이터를 다시 확인해야 한다.
- 선택도가 낮아 인덱스 효율이 떨어지는 예제입니다.
- 상태 칼럼
- 대기, 처리 중, 완료 값 중 하나를 갖는다.
- 대부분 완료이고 적은 수의 데이터만 대기와 처리중이다.
- 선택도가 낮은 칼럼이다.
- But, 작업 큐를 처리하는 코드는 대기인 데이터를 조회한다
- 작업 실행기는 쿼리를 반복해서 실행하여야 한다.
- 쿼리가 오래 걸리면 모든 작업 실행이 지연되는 문제 발생
- 인덱스가 걸려 있지 않으면 풀스캔 문제 발생
- 인덱스의 적합한 예제입니다.
커버링 인덱스 활용하기
커버링 인덱스 : 특정 쿼리를 실행하는 데 필요한 칼럼을 모두 포함하는 인덱스
- 인덱스에 포함한 값만 호출하는 방법
- 직접 테이블 연동이 아닌 인덱스와 연동하여 조회하기 때문에 빠른 실행 능력이 있습니다.
인덱스는 필요한 만큼만 만들기
인덱스에서 추가로 데이터를 추가한 복합인덱스를 만들때 고려
- 효과가 적은 인덱스를 추가하면 성능이 감소할 수 있습니다.
- 데이터 추가, 변경, 삭제 시에는 인덱스 관리에 따른 비용(시간)이 추가됩니다.
- 인덱스가 많아질수록 메모리와 디스크 사용량도 증가
- 기존 인덱스 사용안할 경우 요구사항을 일부 변경할 수 있는 지 검토한다.
- 기존 인덱스를 최대한 활용할 수 있도록 노력합니다.
- 새로운 인덱스 추가 전에 꼭 기존 인덱스를 확인하고 고려해보기.
✏️ 몇 가지 조회 성능 개선 방법
미리 집계하기
집계 데이터를 미리 계산해서 별도 칼럼에 저장하는 방법
- 서브 쿼리 시간 만큼 쿼리 실행 시간이 줄어들어 조회 속도가 빨라진다.
- ex) 좋아요, 조회수 칼럼 보관
정규화의 데이터 무결성 문제 위반
- 데이터 무결성 : 데이터의 정확성, 일관성, 유효성을 유지되는 것을 말합니다.
- 개체 무결성 : 모든 테이블이 기본 키로 선택된 필드를 가져야 한다.
- 참조 무결성 : 두 테이블의 데이터가 항상 일관된 값을 갖도록 유지되어야 한다.
- 도메인 무결성 : 조건에 따른 올바른 데이터가 입력되었는지를 확인해야 한다.
- 연산 값이 불일치하여 데이터 무결성 위반
- BUT, 데이터의 심각한 문제 X < 성능적 이득 → 실행해도 좋은 경우가 있다.
동시성 문제는 없을까??
- 쿼리는 원자적으로 실행하기도 하고 그렇지 않기도 한다.
- 증가 / 감소 쿼리를 사용할 때는 트랜잭션 격리 수준에서 원자적으로 처리하는 지 검증해야 한다.
페이지 기준 목록 조회 대신 ID 기준 목록 조회 방식 사용하기
특정 Id를 기준으로 조회하는 방식
- 마지막 데이터 id를 사용해서 결과 값 도출
- 스크롤 방식에 어울리는 방법
- 추가 데이터 존재 여부를 알려주는 방법
- 오프셋을 사용하지 않아서 데이터를 세는 시간이 줄어듬
- Spring Slice<>() 기법이다.
조회 범위를 시간 기준으로 제한하기
일자 기준이나 최근 데이터로 조회하도록 설정하는 방법
- 디폴트 : 해당 일자 or 최근 데이터 설정
- 사용자는 대부분 최근에 들어온 데이터를 원함 → 오래된 데이터를 원하지 않는다.
- 필요 시 직접 설정하기
- DB 성능 향상
- DB는 메모리 캐시를 이용하는데, 일반적으로 최신 데이터가 많이 조회되기 때문에
캐시에 최신 데이터가 적재될 확률이 높아진다. → 캐시 효율이 높아지면 응답속도 빨라진다.
전체 개수 세지 않기
전체 개수를 함꼐 표시하지 않는 방법
- 데이터가 많으면 Count 함수 실행 시간이 증가한다.
오래된 데이터 삭제 및 분리 보관하기
데이터 개수가 늘어날 수록 쿼리 실행 시간이 증가하기 때문에 오래된 데이터를 삭제하는 방법
- 데이터 증가 속도를 늦추면, 실행 시간 증가 폭을 줄일 수 있습니다.
- ex) 로그인 시도 내역
- 이상 징후를 탕지 하기 위한 로직
- 장기간 보관할 필요는 없기 때문에 181일 이후 데이터는 별도의 저장소로 분리 보관
단편화와 최적화
- DELETE 쿼리를 실행하더라도 DB가 사용하는 디스크 용량은 줄어들지 않는다.
- 삭제되었다는 표시만 남기고, 삭제된 공간은 향후 재사용한다.
- 이 과정에서 단편화 현상이 발생한다.
- 단편화가 심해지면 디스크 I/O가 증가하면서 쿼리 성능이 저하될 수 있다.
해결 방안 : 최적화 작업
- 데이터를 재배치해 단편화를 줄이고 물리적인 디스크 사용량까지 줄어주는 효과가 있습니다.
DB 장비 확장하기
수직 확장이나 수평 확장을 통해 성능을 향상시키는 방법
수직 확장
- 클라우드 사용하면 짧은 시간안 안에 성능을 높일 수 있습니다.
- 사용한 비용만큼 성능을 향상 가능
수평 확장
- 조회를 하는 복제 DB
- 추가, 수정, 삭제를 하는 주 DB
- 조회 기능에 대한 트래픽이 증가하면 복제 DB를 추가적으로 더 확장시킨다.
- DB 서버는 API 서버보다 좋은 장비를 쓰는 경우가 많습니다.
- 가격이 많이 비싸고 한 번 추가할 경우 고정 비용도 같이 증가합니다.
- 서버 확장 대비 얻는 이점을 고려하여 확장을 해야한다.
별도 캐시 서버 구성하기
DB가 트래픽을 처리하는 데 어려움이 있다면 별도의 캐시 서버를 구성하는 방안
- DB 확장 대비 적은 비용으로 트래픽을 처리할 수도 있음
- 코드 수정에 드는 비용 < 캐시로 증가 시킬 수 있는 처리량이면 고려해볼만 합니다.
✏️ 알아두면 좋을 몇 가지 주의 사항
쿼리 타임아웃
응답 지연으로 인해 재시도를 막기 위해 쿼리 실행 시간을 제한두는 방법
- 응답 지연으로 인한 재시도는 서버 부하를 증가시킵니다.
- 쿼리 실행 시간을 제한을 두어 시간 초과 시 에러 발생
- 단, 결제 처리 로직은 후속 처리와 데이터 정합성이 복잡해지는 문제로 길게 설정할 필요가 있습니다.
상태 변경 기능은 복제 DB에서 조회하지 않기
모든 SELECT 쿼리를 무조건 복제 DB에 실행하는 방법
주 DB와 복제 DB는 순간적으로 데이터가 일치하지 않을 수 있습니다.
- 네트워크를 통해 복제 DB 전달
- 복제 DB는 자체 데이터에 변경 내용 반영
트랜잭션 문제가 발생할 수 있습니다.
- 주 DB와 복제 DB 간 데이터 복제는 트랜잭션 커밋 시점에 이뤄집니다.
- 데이터를 변경하면서 복제DB에 변경 대상 데이터를 조회할 경우
- 주 DB에서 쿼리를 실행할 때 변경 대상 데이터를 조회해야 한다면 복제DB가 아닌 주 DB를 이용합니다.
배치 쿼리 실행 시간 증가
집계 쿼리는 특성상 많은 양의 메모리를 사용하게 되며, 특정 임계점을 넘기면 실행 시간이 예측할 수 없을 만큼 길어질 수 있습니다.
- 쿼리의 실행 시간을 지속적으로 추적해야 합니다.
- 쿼리 실행 시간이 갑자기 큰 폭을 증가했는 지를 감지 가능
- 원인을 찾아 해결 가능
해결 방안
- 가장 쉬운 해결 방안은 DB 장비의 사양을 높이는 방안
- 커버링 인덱스 활용
- 처리 속도 빨라지고 DB가 사용하는 메모리도 줄어든다.
- 데이터를 일정 크기로 나눠 처리
- 기준을 나눠서 데이터를 받아오는 방법
- 쿼리 실행 시간을 단축하면서도 필요한 집계 데이터를 안정적으로 생성할 수 있습니다.
타입이 다른 칼럼 조인 주의
칼럼의 타입이 서로 다르기 때문에 발생하는 문제
- DB는 타입 변환을 수행하게 된다.
- 타입 변환은 각 행마다 발생
- 인덱스를 온전히 활용 X
- 문자열 타입 비교 시 캐릭터셋이 같은지 확인해야 한다.
- 다르면 그 자체로도 변환이 발생할 수 있습니다.
테이블 변경은 신중하게
DB를 변경하는 동안 DML을 허용하지 않기 때문에
- 데이터가 많은 테이블은 점검 시간을 잡고 변경하는 경우가 많습니다.
- 테이블 변경을 점검에 할지, 서비스 제공 중에 할지도 고려 대상
DB 최대 연결 개수
트래픽 증가로 인해 DB 개수를 증가할 때 문제 상황
예제
- 트래픽 증가로 API 서버 4개, DB의 최대 연결 개수 100개로 설정되어 있습니다.
- API 서버의 커넥션 풀 개수가 30개일 때 * 4 = 최대 커넥션 120개
- 20개의 커넥션을 얻지 못하고 연결 실패 발생
- 최대 연결 개수를 늘려주는 것으로 문제 해결
- DB 서버의 CPU 사용률이 70% 이상으로 높다면 연결 개수르 늘리면 안 됩니다.
- 연결 개수 많아질수록 DB 부하 증가, 성능 저하 발생
- 캐시 서버 구성이나 쿼리 튜닝 같은 조치 필요 → DB 부하 낮추고 필요할 대 연결 개수를 늘림
✏️ 실패와 트랜잭션 고려하기
트랜잭션 없이 여러 데이터를 수정하는 문제
- 트랜잭션의 시작과 종료 경계를 명확히 설정했는지 반드시 확인해야 합니다.
- 로직 관련 DB들에 하나는 추가가 되고 하나는 추가가 안되고의 문제 발생
- 트랜잭션이 있을 경우 데이터가 전부 롤백된다.
자료 출처
https://product.kyobobook.co.kr/detail/S000216376461