문서 검색

1주차 (Ch1~2)

2장. 느려진 서비스, 어디부터 봐야 할까

📌

챕터 훑어보기 (Survey)

서비스가 느려지면 서버 개발자는 어떤 항목을 살펴보며 병목을 줄여야 하는가?

처리량과 응답 시간

다양한 지표 중에서 서버 성능과 관련 있는 중요한 2가지 지표는 응답 시간과 처리량이다.

응답 시간

응답 시간은 사용자의 요청을 처리하는 데 걸린 시간이다.

응답 데이터의 크기가 작다면 응답 데이터 중 첫 번째 바이트가 도착할 때까지 걸린 시간(TTFB)과 응답 데이터의 마지막 바이트가 도착할 때까지 걸린 시간(TTLB)의 차이가 크지 않다.

데이터 특성이나 네트워크 환경을 고려해 TTFB 와 TTLB 중 적절한 지표를 선택해 측정해야 한다.

서버 개발자는 주로 서버의 처리 시간을 확인하며, 서버 처리 시간은 아래와 같은 요소를 포함한다.

📌
  • 로직 수행 (애플리케이션의 비즈니스 코드)
  • DB 연동
  • 외부 API 연동
  • 응답 데이터 생성(전송)

이 중에서도 DB 연동과 외부 API 연동이 큰 비중을 차지한다.

  • 내/외부 네트워크를 통한 통신을 하는 작업들이기 때문
이러한 이유로 응답 시간을 줄일 때 DB 연동과 API 연동 시간에 집중한다.

처리량

처리량은 단위 시간당 시스템이 처리하는 작업량을 의미하며, 흔히 TPS 나 RPS 로 처리량을 나타낸다.

최대 TPS는 시스템이 처리할 수 있는 최대 요청 수를 의미하며, 서버가 한 번에 5개의 요청을 처리할 수 있고 요청당 처리 시간이 1초라면 최대 TPS는 5가 된다.

동시에 들어오는 요청 수가 최대 TPS 를 초과하면 서버는 초과한 요청을 나중에 처리한다.

최대 TPS가 5인 서버에 동시에 7개의 요청이 들어오면 이 중 5개만 바로 처리할 수 있고, 나머지 2개는 먼저 실행된 5개의 요청이 끝난 후에야 처리할 수 있다.

응답 시간의 증가는 사용자 이탈로 이어질 수 있다. 이를 방지하기 위해서는 아래 2가지 방법을 고려해야 한다. 참고로, 이 두 가지 방법은 모두 TPS 를 높일 수 있다.

  • 서버가 동시에 처리할 수 있는 요청 수를 늘려 대기 시간 줄이기
  • 처리 시간 자체를 줄여 대기 시간 줄이기

성능을 개선하려면 먼저 현재 서버의 TPS 와 응답 시간을 알아야 한다. 막연히 성능이 느리다고 말하면서 이것저것 시도하면 안 된다.

트래픽이 많은 시간대의 TPS와 응답 시간이 얼마인지 측정하고, 결과를 바탕으로 목표 TPS 와 응답 시간을 설정하고 효과적인 성능 개선안을 도출해야 한다.

TPS를 확인하는 가장 간단한 방법은 모니터링 시스템을 활용하는 것이며, 스카우터, 핀포인트, 뉴렐릭 같은 도구를 사용하면 실시간 TPS 뿐 아니라 특정 시점의 TPS 도 확인이 가능하다.

  • TPS 를 구할 때 근사치를 사용한다. 예를 들어, 5초 간격으로 처리한 요청 수를 구한 뒤 이를 5로 나눠 TPS 를 계산하는 식이다.
  • 정확한 TPS 를 알고 싶다면 웹 서버 접근 로그를 활용하면 된다.

서버 성능 개선 기초

성능 문제가 발생하는 주된 이유는 시스템이 수용할 수 있는 최대 TPS 를 초과하는 트래픽이 유입되기 때문이다.

문제 지점을 찾는 간단한 방법은 처리 시간이 오래 걸리는 작업을 식별하는 것이다. 성능 문제는 응답 시간이 길어지면서 발생하는 경우가 많기 때문이다. 이때, 모니터링 도구를 사용하면 유용한데 모니터링 도구는 실행 시간 추적 기능을 제공하며 이 기능을 활용하면 코드 실행 시간이 오래 걸린 곳을 찾을 수 있다.

적절한 모니터링 도구가 없다면 로그라도 남겨야 한다. 대부분의 성능 문제는 주로 DB나 외부 API 를 연동하는 과정에서 발생한다.

수직 확장과 수평 확장

사용자가 서비스를 이용하지 못하는 상황에서는 일단 급한 불을 끄고 나서 근본적인 해결책을 모색해야 한다.

급한 불을 끄는 방법 중 하나는 수직 확장이다.

수직 확장은 즉각적인 효과를 얻을 수 있지만 트래픽이 지속해서 증가하면 결국 또다시 성능 문제가 발생한다.

수직 확장은 비용이 많이 들고, 한 대의 장비가 감당할 수 있는 용량에도 한계가 있어 서버를 늘리는 수평 확장 방식을 사용한다.

수평 확장 방식은 TPS 를 높이기 위해 무턱대고 서버를 추가해서는 안 된다.

  • DB에서 성능 문제가 발생하고 있는데 서버를 추가로 투입하면 불에 기름을 붓는 격이 된다.

DB 커넥션 풀

네트워크에서 DB를 연결하고 종료하는 시간은 전체 응답 시간에 영향을 준다.

DB 커넥션 풀은 DB에 연결된 커넥션을 미리 생성해서 보관한다. 커넥션 풀을 사용하면 이미 연결된 커넥션을 재사용하기 때문에 응답 시간이 줄어드는 장점이 있다.

커넥션 풀 크기

커넥션 풀 크기는 커넥션 풀 설정에서 가장 중요하다.

📋

예시

커넥션 풀 크기는 5이며, 한 요청에서 쿼리를 실행하는 데 1초가 걸린다.

서버에 6개의 요청이 동시에 들어왔을 때 이 중 5개 요청은 풀에서 커넥션을 가져올 수 있다. 반면, 나머지 1개 요청은 사용할 수 있는 커넥션이 없으므로 다른 요청이 커넥션 사용을 끝내고 풀에 반환할 때까지 기다려야 한다.

풀에서 커넥션을 얻기 위해 대기하는 시간을 줄이려면 전체 응답 시간과 TPS 를 고려해 커넥션 풀 크기를 지정해야 한다.

📋

예시

커넥션 풀 크기가 5이며, 한 요청에서 쿼리를 실행하는 데 0.1초가 걸린다.

여기서 1초에 처리할 수 있는 요청 수는 50이 된다. 즉, 동시에 50개의 요청이 들어오더라도 모두 1초 안에 처리가 끝난다.

반면, 커넥션 풀 크기가 5이고 쿼리 실행 시간이 5초라면 1초에 처리할 수 있는 요청 수는 10으로 줄어든다. 이 상태에서 동시에 50개의 요청이 들어오면 모든 요청을 처리하는 데 5초가 걸리게 된다.

  • 이때는 응답 시간을 줄이기 위해 커넥션 풀 크기를 50으로 늘려 모든 요청을 0.5초 이내에 처리할 수 있다.

일반적인 커넥션 풀은 최소 크기와 최대 크기를 설정할 수 있다. 최대 크기가 20이라면 동시 요청 수가 증가할 때 커넥션 풀은 20개까지 커넥션을 늘린다. 이후 동시 요청이 20개 이하로 줄어들면 커넥션 풀의 커넥션도 점차 줄어든다.

이러한 특성에 맞게 커넥션 풀 크기도 트래픽이 적은 시간대는 최소 크기로 유지하고 트래픽이 높은 시간대에는 최대 크기로 확장해서 커넥션 개수를 필요한 만큼만 유지할 수 있다.
  • 트래픽이 순간적으로 급증하는 패턴을 보인다면 커넥션 풀의 최소 크기를 최대 크기에 맞추는 것이 좋다.
  • 트래픽이 급증할 경우 DB 연결 시간도 성능 저하의 주요 원인이 될 수 있다.

커넥션 대기 시간

커넥션 풀은 대기 시간을 설정할 수 있다.

  • 대기 시간이란 풀에 사용할 수 있는 커넥션이 없을 때 커넥션을 얻기 위해 기다릴 수 있는 최대 시간을 의미한다.
  • 지정된 대기 시간 안에 커넥션을 구하지 못하면 DB 연결 실패 에러가 발생한다.
응답 시간이 중요한 서비스는 커넥션 대기 시간을 가능한 한 짧게 설정해야 한다.
  • 보통의 경우라면 0.5초에서 3초 이내로 지정하자.

대기 시간을 짧게 설정하면 커넥션 풀이 모두 사용 중일 때 빠르게 “일시적 오류”와 같은 에러 응답을 사용자에게 보여줄 수 있다.

대기 시간 때문에 긴 시간 동안 무응답 상태로 유지되는 것보다는 빠르게 에러를 반환하는 것이 더 낫고, 커넥션을 얻지 못했을 때 빠르게 에러를 응답해야 서버의 부하가 증가하는 것도 방지할 수 있다.

📋

커넥션 풀의 크기가 10이고, 대기 시간이 30초이다.

클라이언트가 5초 만에 요청을 취소하고 다시 요청을 보내면서 새로운 대기를 시작한다면, 클라이언트가 요청을 취소하더라도 서버는 일정 시간 동안 하던 작업을 즉시 중단하지 않기 때문에 이 시점의 대기 중인 요청 수는 더 증가한다.

즉, 몇 초 만에 요청을 취소하고 재요청이 반복되면 동시에 처리해야 할 요청 수는 계속 증가한다.

이를 해결하기 좋은 방법은 대기 시간을 짧게 설정하여, 새로운 요청이 들어오더라도 커넥션을 구하지 못하면 바로 에러를 응답하는 것이다.

최대 유휴 시간, 유효성 검사, 최대 유지 시간

커넥션이 사용되지 않는 시간이 길어지면 연결이 끊길 수 있다.

  • MySQL과 같은 DB 클라이언트와 일정 시간 동안 상호작용이 없으면 자동으로 연결을 끊는 기능을 제공한다.

DB와의 연결이 끊긴 커넥션을 사용하면 에러가 발생하는데, 이 에러를 방지하기 위해 커넥션 풀은 2가지 기능을 제공한다.

  • 최대 유휴 시간 지정
  • 유효성 검사 지원

최대 유휴 시간

DB에 설정된 비활성화 유지 시간보다 짧게 설정하면, DB가 연결을 끊기 전에 풀에서 커넥션을 제거할 수 있다.

유효성 검사

연결이 유효하지 않은 커넥션을 식별하고 풀에서 제거할 수 있다.

  • 이때는 SELECT 1 FROM dual 이나 SELECT 1 과 같이 간단한 쿼리를 사용한다.
💡

최대 유휴 시간과 최대 유지 시간을 무한대로 설정하지 않는 것이 좋다. 기본값이 무제한으로 되어 있다면 DB 설정을 참고하여 적절한 값으로 지정해야 한다.

서버 캐시

오래된 데이터는 캐시에 있어도 사용되지 않을 가능성이 크다. 따라서, 캐시가 가득 차 있지 않더라도 오래된 데이터는 미리 삭제하는 것이 좋다.

로컬 캐시와 리모트 캐시

로컬 캐시는 서버 프로세스와 동일한 메모리를 캐시 저장소로 사용한다.

  • 로컬 캐시의 구현 기술은 Caffeine 과 같이 자바에서 활용하고 있고, 로컬 캐시의 장점은 속도다.
  • 로컬 캐시의 단점은 캐시에 저장할 수 있는 데이터 크기에 제한이 있다는 점이다. 서버 프로세스를 재시작하면 메모리에 있던 캐시 데이터가 모두 삭제되어 캐시 적중률이 일시적으로 떨어진다.

리모트 캐시는 별도 프로세스를 캐시 저장소로 사용한다.

  • 리모트 캐시는 로컬 캐시와 정반대의 장단점을 가진다.
  • 큰 장점은 캐시 크기를 유연하게 확장할 수 있다는 것이다. 대표적으로 레디스는 캐시 서버를 수평 확장할 수 있고, 서버 프로세스가 재시작되더라도 레디스에 저장된 캐시 데이터는 남아 있다.
  • 단점은 속도다. 캐시 프로세스와 서버가 데이터를 주고받기 위해 네트워크 통신을 해야 하기 때문에, 로컬 캐시보다 상대적으로 느리다.
캐시에 보관할 데이터 규모가 작고 변경 빈도가 매우 낮다면 로컬 캐시로 충분하다.
  • 예) 홈 화면에 표시할 최신 공지글 목록

반면에, 데이터 규모가 크다면 리모트 캐시를 사용해야 한다.

  • 예) 트래픽이 많은 대형 쇼핑 사이트의 개별 제품 정보 배포 빈도가 높은 서비스라면 리모트 캐시 사용을 적극적으로 고려하여야 하며, 로컬 캐시를 사용할 경우 서버를 재시작할 때마다 캐시 데이터를 처음부터 다시 쌓아야 한다.

캐시 사전적재

특정 사용자에게 알림을 발송하여, 요금을 조회하는 행위를 한다고 해보자. 만약, 위 상황에서 특정 시간에 모든 사용자에게 발송한다면 트래픽이 갑자기 몰릴 것이고, 데이터를 처음 조회하는 경우 캐시에 데이터가 존재하지 않아, 캐시 미스가 발생한다. 이 경우 캐시 적중률은 순간적으로 0%까지 떨어진다.

위와 같은 상황을 해결하기 위해서 푸시 알림을 보내기 전 캐시에 데이터를 미리 저장해두면 적중률을 99%에 가깝게 유지하며 성능에도 큰 이점을 얻을 수 있다.

캐시 무효화

유효하지 않은 데이터를 적절한 시점에 캐시에서 삭제하는 행위

  • 캐시에 저장된 데이터의 원본이 바뀐 후 갱신되지 않으면 잘못된 정보를 사용자에게 제공할 수 있기 때문에 적절한 캐시 무효화가 필요하다.

캐시에 저장된 데이터의 특성에 따라 캐시를 무효화하는 시점을 다르게 설정해야 한다.

  • 가격 정보, 게시글 내용처럼 민감한 데이터는 변경되는 “즉시” 캐시를 무효화해야 한다.
  • 위 예시처럼 변경에 민감하다면, 로컬이 아닌 리모트 캐시로 관리해야 한다.
    • 로컬 캐시는 자신의 데이터만 변경할 뿐, 다른 서버의 로컬 캐시는 변경하지 않기 때문이다.
  • 반면에, 변경에 민감하지 않은 데이터이거나 데이터의 양이 적다면 캐시의 유효 시간을 설정하여 주기적으로 갱신하는 방식을 사용해도 된다.

가비지 컬렉터와 메모리

한 번에 대량으로 객체를 생성하는 것을 주의하라.

조회 API 가 한 번에 10만 개의 게시글을 반환한다고 했을 때, 게시글 하나당 0.5KB 로 신경 쓰지 않아도 될 용량일 것 같지만 동시에 100명의 사용자가 호출한다면 이는 약 4.9GB 의 메모리가 필요하게 된다.

이를 방지하기 위해서는 조회 범위를 제한해야 한다. 파일 다운로드와 같은 기능을 구현할 때 스트림을 활용하는데, 파일 데이터를 한 번에 메모리에 로딩한 후 응답하는 방식은 피해야 한다.