본문 바로가기
개발 잡담

DB ) N+1 문제는 왜 나쁜 걸까?

by 휴일이 2024. 12. 9.

N+1 이 왜 나쁜 걸까?

OOM 과 CPU 대기 상태를 일으킬 수 있습니다~!

OOM 이란

시스템이 메모리를 다 사용해 더 이상 추가 메모리를 할당할 수 없는 상태.

  • 프로세스가 너무 많은 메모리를 요청하거나, 동시에 많은 프로세스가 실행되어 물리 메모리(RAM), 가상 메모리가 모두 고갈됐을 경우 발생.
  • 운영체제는 OOM Killer 를 작동시켜 가장 많은 메모리를 사용하거나, 우선순위가 낮은 프로세스를 종료하여 메모리를 확보하려고 함.
  • 시스템 성능 저하, 프로세스 강제 종료, 프로그램 비정상적인 종료.

CPU 대기 상태

프로세스가 실행을 기다리면서 CPU 자원을 할당받지 못해 대기하는 상황.

I/O 작업 등 때문에 프로세스가 일시적으로 실행되지 못하고 대기하는 것을 포함하기도 함.

  • CPU 사용이 높은 작업이 많거나, I/O 작업을 기다리는 프로세스가 많을 때 발생 가능.
  • I/O 작업을 하는 상태일 때도 CPU 를 사용할 수 없기 때문에, 프로세스는 계속 대기 상태가 된다.
  • 시스템 응답 속도가 느려지고, 처리 성능이 저하됨. I/O 작업이 많으면 CPU 는 작업이 끝날 때까지 다른 프로세스를 실행 함. → 전체 시스템 성능에 영향이 있을 수 있음.

 

N+1 에서 OOM 과 CPU 대기 상태가 일어나는 이유는?

N+1 은 기본적으로 1개의 메인 쿼리 실행 후, 관련 데이터를 가져오기 위해 N개의 개별 쿼리가 실행되는 상황.

웹 애플리케이션에서 쿼리를 요청하는 상황을 가정해보자.

  1. 웹 애플리케이션은 DB 에 쿼리를 요청하기 위해 I/O 요청을 하고 그를 위해 시스템콜을 한다.
    • 보통 DB 는 애플리케이션 서버와 다른 네트워크에 존재하므로 I/O 요청이 필수지요!
  2. 시스템콜을 하면 일단 현재 웹 애플리케이션이 실행하던 컨텍스트는 커널 스택에 저장하고 프로세스의 현재 상태는 PCB 에 저장한다.
    • PCB 는 커널 메모리에 있음.
  3. 이후 커널 모드로 전환되고, 커널이 요청을 확인하니 → DB 서버에 이 쿼리 요청을 해줘 라고 되어있다.
    그러면 커널은 세션 테이블을 확인하여 DB 서버의 정보가 있는지 확인한다. (커넥션 연결 유무 확인)
  4. NIC 에 쿼리 요청을 보내어 네트워크를 타고 DB 서버로 요청이 간다.
  5. DB 서버의 NIC 가 전기 신호를 받으면 데이터를 DMA 컨트롤러를 이용해 메모리의 네트워크 버퍼로 옮긴다. → 그 다음 인터럽트 발생!
  6. 인터럽트가 발생하면 DB 서버가 당장 하던 일을 중단하고 역시 컨텍스트를 커널 스택에 집어 넣고, PCB 에 현재 상태를 저장한 뒤 커널 모드로 전환되어 인터럽트를 처리한다.
  7. 이후 커널이 네트워크 버퍼에 차있는 데이터를 TCP/IP 스택으로 (패킷의 IP 주소, 포트번호, 연결 상태) 확인 후 세션 테이블을 보고 기존 연결인지 새로운 연결인지 확인함.
  8. 패킷 순서와 무결성을 확인하고 바로 애플리케이션 계층으로 데이터 전달.

만약 3번에서 커넥션 연결이 되지 않은 요청일 경우.

  • TCP 연결 설정을 우선 수행하기 위해 쿼리 요청 데이터는 잠시 네트워크 버퍼에 보관한다.
  • 커널이 SYN 패킷을 DB 서버로 전송해 연결 요청을 시작한다.
  • DB 서버에서는 인터럽트가 오면 ~ 위의 6번~7번으로 확인하고, 애플리케이션 연결 요청 확인을 위한 SYN-ACK 패킷을 보낸다.
  • 애플리케이션 서버는 해당 패킷을 잘 받았다는 SYN 패킷을 보낸 즉시 → 커널이 네트워크 버퍼에 대기중인 쿼리 요청 데이터를 NIC 로 전송하고 4번~8번이 실행된다.

그런데 N+1 이면 이 작업들이 연속해서 일어날 것이다.

  • 최대 커넥션 10개를 유지할 수 있는 상황이며, N+1 요청이 10개 발생했다.
  • 각 요청은 커넥션을 물고 있는 상태.
  • 애플리케이션은 추가 쿼리를 요청하려고 했으나 커넥션 풀에 아직 반환된 커넥션이 없는 상태
    • 예시로 HikariCP 에서 커넥션 풀에 반환된 커넥션이 없다고 가정하면
    • 스프링 앱에서는 커넥션이 반환 될 때까지(DB서버에서 응답이 되돌아올 때까지)요청을 대기 상태로 둠.
  • 일정 시간 대기를 하다가 커넥션이 반환되지 않으면 timeout 발생

만약 애플리케이션이 커넥션 풀을 관리해주지 않는다면?

TCP 연결을 맺을 때까진 괜찮지만 해당 요청이 DB 서버까지 도달했을 경우.

  1. 최대 커넥션 수에 도달했기 때문에 커넥션 연결을 거부함.
  2. 또는 커넥션 대기열을 설정 가능한 DB라면 일정 시간동안 대기할 수 있도록 허용하기도 함.
    • DB의 커넥션 대기열에서 대기하다가 대기 시간이 초과되면 타임아웃 발생.
    • 만약 커넥션 대기열이 꽉 차있는데도 요청이 들어오면 해당 요청은 즉시 거부

→ 최종적으로는 애플리케이션에서 커넥션 풀 타임아웃 예외 발생

  • 커넥션을 맺고 끊는 비용이란 TCP 3방향 핸드셰이크와 / 4방향 핸드셰이크(연결 끊기) 를 하는 비용을 이야기 함.

4방향 핸드셰이크

  1. FIN 플래그 설정 패킷 전송 : 클라이언트 → 서버
    • 더 이상 전송할 데이터가 없다.
  2. ACK 플래그 패킷을 전송하여 FIN 패킷을 받았음을 확인 : 서버 → 클라이언트
  3. 서버도 더 이상 보낼 패킷 없다고 FIN 패킷 전송 : 서버 → 클라이언트
  4. 클라이언트가 FIN 을 받았다는 ACK 패킷 전송 : 클라이언트 → 서버

그럼 TCP 연결은 얼마나 유지되나.

  • 애플리케이션(HTTP 서버, 웹 브라우저) : 일반적으로 몇 분
  • 데이터베이스 : 유지 시간을 정할 수 있음. (MySQL 은 기본적으로 8시간) → wait_timeout, interactive_timeout
  • 라우터, 방화벽, 프록시 장비 : 장비에 따라 기본적으로 5~30분 사이
    • NAT 장비나 방화벽은 TCP 연결 10~30분 동안 유지하기도.
  • TCP Keep-Alive 옵션 : 연결이 유휴 상태일 때 주기적으로 신호를 보내서 연결 유지를 확인할 수 있음. (기본 몇 분에서 몇 시간)
  • TIME_WAIT : 연결이 종료될 때 클라이언트에서 TIME_WAIT 상태로 잠시 대기. → 몇 초에서 수십 초.
    • 이전 연결에 속한 패킷이 네트워크에 남아있을 가능성 대비하여 : 새로운 포트로 새로운 연결이 바로 시작되는 것을 방지.

 

DB 서버에서는 어떤 일이 일어날까?

  1. 긴 쿼리 실행으로 인한 자원 점유
    • N+1 문제로 인해 한 커넥션에서 많은 쿼리가 순차적으로 실행 됨.
    • 쿼리가 매우 많은 데이터를 조회하고 조인, 정렬 등의 작업을 포함하면 → DB 서버의 CPU 와 메모리를 장시간 점유.
    • 게다가 다수의 요청이 동시에 들어온다?
      • CPU 와 메모리가 지속적으로 높은 사용률 유지 → DB 서버 성능 저하
  2. 디스크 I/O 부하 증가
    • 당연히 디스크에서 읽고 쓰는 작업이 빈번하게 발생할 것이고 I/O burst 로 병목 현상 초래.
  3. DB 서버 메모리 캐시 및 버퍼 고갈
    • DB 서버가 자주 조회되는 데이터를 메모리에 캐싱하여 빠른 응답을 제공하나, N+1 쿼리가 반복되면 캐시나 버퍼에 데이터가 과도하게 적재되어 기존의 캐시가 무효화 됨.
    • 캐시와 버퍼가 무효화되면 쿼리마다 새로운 데이터를 디스크에서 읽어와야 함. → 성능 저하.
    • 메모리 과부하 및 디스크 I/O 부하.
  4. 데드락과 락 대기 증가
    • 트랜잭션이 포함된 N+1 쿼리가 다량으로 발생하면, 동일한 테이블이나 행에 다수의 락이 발생.
    • 이로 인해 다른 쿼리가 대기 상태로 전환 + 데드락 발생. → DB 서버 리소스 사용 증가.
  5. 메모리 부족으로 OOM
    • N+1 문제로 대량의 데이터를 동시에 처리 → 쿼리 실행 시점에 많은 메모리 사용.
    • DB 서버가 보유한 메모리 한계 초과 시 OOM 발생해 강제 종료 가능성.
  6. DB 커넥션 풀과는 별개로 시스템 스레드 오버헤드 증가.
    • 각 쿼리가 메모리와 CPU 리소스를 많이 소모하면, DB 서버의 스레드 풀이나 연결 관리 스레드가 계속해서 작업 대기.
    • 스레드가 많아지면 스레드의 컨텍스트 스위치 잦아져 CPU 오버헤드 증가 → 성능 저하 초래.
커넥션 풀이 가득 차서 애플리케이션이 요청을 거부하더라도 이미 열린 커넥션들이 DB 서버의 자원을 과도하게 사용하면
: “데드락”, “메모리 부족 문제” 발생.
결과적으로 DB 서버가 과부하로 인해 응답하지 못하거나, 심한 경우 OOM 으로 서버 중단 가능성.

애플리케이션에서의 OOM

애플리케이션이 결과 데이터를 직접 메모리에 적재함.

  • DB 에서 반환된 쿼리 결과를 앱 내부의 메모리에 저장할 것임.
  • 애플리케이션에서는 “한 개”의 요청을 처리하지만 N+1 로 파생되는 쿼리가 많다면
  • 대량의 데이터가 반환되어 애플리케이션의 메모리가 초과될 가능성이 큼.

(스프링의 경우) JVM 메모리 제한

  • JVM 의 메모리는 물리 메모리보다 작게 설정되는 경우가 많음.
  • DB 서버에서는 (JVM 보다)상대적으로 더 많은 메모리를 사용하도록 구성하는 경우가 많음.
  • 그렇다면 애플리케이션이 먼저 메모리 부족에 직면할 수 있음.

캐싱 및 객체 유지

  • 애플리케이션에서 ORM(JPA) 를 사용하는 경우, 데이터가 캐시에 유지되거나 객체로 변환되며 메모리 누적 발생 가능성.
  • 예1) JPA 로 유저를 조회했는데 User의 Adress 도 함께 조회해야 하여 User, Adress 객체가 각각 만 개씩 메모리에 저장 됨. → 당연히 OOM 가능성 증가.
  • 예2) 영속성 컨텍스트에 user, adress 객체가 저장될 것이고 (캐싱을 위해) 한 요청의 영속성 컨텍스트는 트랜잭션이 끝날 때까지 계속 유지 됨.

 

DB 서버에서는 어떻게 OOM 이 발생할까?

모든 사람들은 애플리케이션의 “첫 페이지”를 방문한다.

  • 그 때, 만약 메인 페이지에 개인화된 정보가 있고 그 정보를 조회하기 위한 쿼리가 N+1 문제를 유발시킨다고 가정해보자.

배달의 민족 첫 페이지에 내가 이제까지 시킨 주문의 갯수가 나온다면?

만약 그 갯수가 이제까지 한 모든 주문을 SUM() 하여 구해야 하는 것이라면?

  • 유저가 한 주문을 전부 찾아서 더하는 조회 쿼리가 발생할 것이다.
  • 게다가 N+1 문제 때문에 주문에 관한 모든 테이블도 전부 조회하는 상황이 발생한다면
    • 당연히 OOM 발생 가능성이 크다.

꼭 N+1 문제가 아니어도, 개인화 된 쿼리는 문제가 발생할 수 있다.

배달의 민족 이벤트 푸쉬 알림이 와서 평소의 500배의 트래픽이 한번에 발생한다고 가정해보자.

  • 만약 메인페이지에서 “총 주문 갯수” 를 보여주는 란이 있어 집계 쿼리를 날려야 한다면?
  • 나의 모든 주문을 찾기 위해 주문 테이블을 뒤져 집계해야 한다면..
  • 당연히 메모리에 많은 데이터가 올려질 것이다. 왜냐하면 aggregation 쿼리는 관련된 모든 로우를 가져와서 처리해야 하니까.
  • 평소에는 괜찮을(?) 수도 있지만 이벤트 푸쉬 알림 같은 상황에서 한 번에 500배의 트래픽이 발생한다면 당연히 DB 서버에서도 OOM 발생 가능성이 크다!

그러면 해결 방법은?

  1. 일단 “메인 페이지” 에는 개인화된 쿼리를 사용하는 정보를 보여주는 기능은 최대한 지양하자!
  2. 만약 굳이 꼭! 기획 상으로 보여줘야 한다면 미리 계산하여 컬럼으로 추가해두는 것이 좋다.

그럼 다른 플랫폼을 한 번 구경해보자!

배달의 민족

 

  • 주소가 “우리집” 으로 되어있다는 정보와 우리집 근처 추천 맛집들 외에 개인 정보는 나와있지 않다.

당근

  • 내가 설정해 놓은 주소만 뜬다. 나머진 그 주소와 관련된 당근할 물품들.

토스

  • 이름과 내가 어떤 통장을 가지고 있는지만 나와있다.

그렇다 ! 유명(?) 애플리케이션 메인 페이지에는 “최소 정보”만 나와있다.

  • 특히 배민이나 토스는 특정 시간에 트래픽이 몰리는 이벤트들이 많다.
    • 배민 : 선착순 쿠폰 발행이나 7시부터 치킨 할인 이벤트 등
    • 토스 : 빼빼로 데이 이벤트 등 특정 이벤트 등
  • 최소한 필요한 개인 정보만 보여주고, aggregation 쿼리가 필요한 집계 데이터들은 보여주지 않는다.
  • 멋진 개발자들은 “기능이 되냐 안 되냐” 를 넘어서 서비스/비즈니스 레벨로 봤을 때 보이는 문제들도 고려할 수 있어야 한다.
728x90