최근 API 응답 최적화 관련 질문을 받았꼬 해당 방법에 대해 진짜 시작부터 끝까지 하나의 플로우로 정리해봤다. 일단 API 는 유저 정보를 불러오는 API 이며 내부망 API 2번 및 DB 1번 호출이 포함되어 있다고 가정한다.
API 병목지점 찾기
- 전체 호출에서 각 구간별 소요시간을 측정한다.
- 만약 모니터링 도구와 연동되어있다면 APM 으로 API 호출 후 DB 쿼리 호출타임을 확인해보고 인덱스 잘 걸려있는지 체크한다.
- 만약 APM 이 없다면 로그를 찍어서 grafana 로 통계를 내던 elk 로 체크하던 평균통계를 확인한다.
DB 병목?
- DB select 쿼리 인덱스 잘 걸려있는지 explain 으로 확인한다.
- 인덱스가 카디널리티가 낮은 필드에 걸려있다면 인덱스를 제거하거나 재설계한다.
- read 전용 replica DB 를 추가하여 읽기 부하를 분산시킨다.
- 쿼리에서 이상한 점 보이면 쿼리 튜닝을 진행한다. 이거 할 떄 마찬가지로 APM 툴 붙여놓으면 확인하기 편함. 아니면 따로 grafana 로 쿼리별 응답시간 통계내도 됨.
- 서브쿼리 줄이기
- 불필요한 필드 조회 줄이기
- N+1 쿼리 체크
- offset 대신 인덱스 타고 조회하도록
- DB 세션이 부족한지도 체크해서 커넥션 풀 늘리기
내부망 API 병목?
- 일단 동시요청 하도록 변경
- 이거 스레드가 천천히 늘어나기떄문에 미리 warm-up 시켜놓기
- gzip 페이로드 압축 굳이?
- 내부망은 RTT 가 낮기 때문에 gzip 압축에 따른 CPU 오버헤드가 더 커질 수 있음
- 이진포멧 페이로드 쓰는 gRPC 로 변경해서 더 빠르게 전송하도록 및 네트워크 대역폭도 아끼도록
마지막으로 전체적으로 개선
- 유저 요청자체가 모두 타임아웃 걸릴정도로 시간이 오래걸린다면?
- 서버에서 그냥 이벤트 id 를 먼저 발행 응답(클라에서 해당 id 로 폴링하던 sse, socket 으로 받던 비동기로 변경) 및 이벤트를 캐시나 db 에 올림.
- 큐를 읽고 다른 곳에서 처리하고 이벤트 id 로 해당 이벤트 저장된 곳에 완료/실패 처리 기록
- 유저정보 실시간 필요한지 확인
- 실시간 제공 아니라면 캐싱 레이어 추가
- DB, 내부망 API 호출 모두 캐싱 해서 개별 응답속도 개선할 수 있음
- 어플리케이션에서 스레드 개수 적당한지 체크
- Spring-boot 프레임워크에서 MVC 모델이면 톰캣 스레드풀 조정
- 부족하면 WebFlux 모델로 변경해서 논블로킹 I/O 모델로 변경 고려(이건 내부망 API 가 논블로킹 지원해야함)
- 내부망 API 요청에 N:M virtual thread 로 변경 고려. 외부 API 요청은 어차피 전송하고 CPU 안쓰고있어서 이거 써서 네이티브 스레드 쉬게해주는게 좋음.
- 내부적으로 사용하는 공통 정보들을 어플리케이션 뜰 때 미리 로딩해놓는 warm-up 프로세스 만들어놓기