최근 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 프로세스 만들어놓기
I recently received a question about API response optimization, so I organized the method from start to finish as one flow. For this post, assume the API loads user information and includes two internal network API calls and one DB call.
Finding API bottlenecks
- Measure the time spent in each section of the full call.
- If the service is connected to a monitoring tool, call the API through APM, check the DB query call time, and verify that indexes are configured properly.
- If there is no APM, print logs and check average statistics through Grafana or ELK.
DB bottleneck?
- Check whether indexes are configured properly for DB select queries with
explain.- If an index is on a low-cardinality field, remove it or redesign it.
- Add a read-only replica DB to distribute read load.
- If the query looks odd, tune the query. This is also easier to check with an APM tool attached. Otherwise, you can separately collect response-time statistics per query with Grafana.
- Reduce subqueries.
- Reduce unnecessary selected fields.
- Check for N+1 queries.
- Use index-based lookup instead of offset.
- Check whether DB sessions are insufficient and increase the connection pool if needed.
Internal network API bottleneck?
- First, change the calls to run concurrently.
- Since threads increase slowly, warm them up in advance.
- Is gzip payload compression really necessary?
- In an internal network, RTT is low, so the CPU overhead from gzip compression can become larger than the benefit.
- Change to gRPC, which uses a binary payload format, to transmit faster and save network bandwidth.
Finally, improve the overall flow
- What if the user request itself takes so long that it almost always times out?
- The server can first issue and return an event id. The client can poll with that id, or receive the result asynchronously through SSE or a socket. The event is stored in cache or DB.
- Another worker reads the queue, processes it elsewhere, and records completed or failed status where the event id is stored.
- Check whether user information really needs to be real-time.
- If real-time data is not required, add a caching layer.
- Cache both DB calls and internal network API calls to improve each response time.
- Check whether the application has a proper number of threads.
- If it is a Spring Boot MVC model, adjust the Tomcat thread pool.
- If that is not enough, consider changing to WebFlux and using a non-blocking I/O model. This requires the internal network API to support non-blocking behavior.
- For internal network API requests, consider changing to N:M virtual threads. Since external API requests mostly wait after sending and do not use CPU, this can let native threads rest.
- Create a warm-up process that preloads shared information used internally when the application starts.