본문 바로가기
{ETC}/2023 아티클 분석

kakao 엔터 FE 기술블로그 - 마법소녀 이세계 아이돌 웹툰 런칭! BFF 장애 대응기

by jay2022 2023. 11. 27.

원본 아티클

1. 이슈 요약

유명 스트리머 우왁굳의 버추얼 걸그룹 이세계 아이돌의 웹툰이 카카오페이지에 런칭하는 날 트래픽이 3배 이상 몰리게 되며 BFF 서버의 CPU가 100%를 찍는 장애가 발생하게 됩니다.

  • 카카오 기술블로그에 설명된 글

2. 문제를 찾는 과정

점진적으로 문제 원인을 찾는 과정을 보여 준다.

성능비교

2.1. 백엔드 API 자체의 문제(BFF 서버 외적인 문제)

  • nGrinder를 사용하여 측정한 결과 백엔드 API 서버의 TPS가 BFF 서버 TPS 보다 10배가 빠른것을 알 수 있었다.
  • 즉 API 서버 문제는 아니다.

2.2. 인프라적 문제(BFF 서버 외적인 문제)

  • BFF의 인프라는 k8s를 사용하고 있다.
  • k8s는 ingress → service → pod 순으로 트래픽이 흐른다.
  • 싱글코어를 사용하는 node서버가 병목없이 실행되려면 pod의CPU 코어수를 넘지 않게 node가 실행되면 된다.
  • BFF의 인프라는 pod당 CPU 코어수를 넘지 않게 셋팅 하였다.
  • 실제로 pod를 늘려도 큰 차이는 없었다
  • 즉, 인프라 계층도 문제가 아니다.

2.3. 요청 시 처리하는 미들웨어의 문제

  • BFF서버에 설정된 미들웨어의 문제
  • 사용중인 미들웨어들을 하나하나 개선하여 테스트 진행
  • 그럼에도 문제는 해결되지 않음.

2.4. apollo-server 라이브러리 자체의 문제

  • BFF서버는 apollo-server-express 라이브러리를 사용하여 gql을 처리하고 있다.
  • 타 라이브러리에 비해 요청 성능이 떨어지지만 그래도 Requests/s가 2486라는 성능이 나온다.
  • 하지만 BFF서버는 이 성능을 못내고 있다.
  • 즉, 라이브러리도 문제가 아니다.

2.5. request 처리 문제

  • 앞서 테스트 한 결과를 정리하면 API 서버, 인프라, 미들웨어나 gql 라이브러리는 문제가 없음을 확인했다.
  • TCP TIME-WAIT에 관한 글을 보고 원인에 접근하게 된다.
  • OS에 설정된 TIME_WAIT 설정 일반적으로 사용하는 60초로 되어 있었다.
  • 원인은 BFF서버의 graphql을 처리하는 nodejs http라이브러리의 기본값에서 발견된다.
  • nodejs 19버전 이하에서는 keepAlive 기본값이 false이다.
  • keepAlive을 사용하지 않는 설정이 어떠한 영향을 주고 있었는지는 아래서 설명한다.

3. request 문제 분석

3.1. 서버는 OS마다 동시 소켓 연결 최댓값이 있다.

클라이언트가  connect()시 로컬 포트를 임의로(ephemeral port) 바인딩하면서 서버의 소켓과 연결된다. 클라이언트가 패킷을 전송할 때 아직 할당된 로컬 포트가 없다면 오토 바인드를 진행한다. 리눅스의 로컬 포트 범위는 3만 개 정도이고, 로컬 포트가 고갈된 경우 문제가 생길 수 있다.

  • 카카오 기술블로그에 설명된 글

임시포트(EPHEMERAL PORTS): 일반적으로 리눅스에서는 2^16 (65,536) 개의 '임시포트'가 있습니다. 임시포트 범위는 49152 ~ 65535 입니다. 이 포트들은 클라이언트가 서버에 접속할 때 사용됩니다. 따라서 동시에 맺을 수 있는 연결의 수는 이 포트의 개수에 따라 달라집니다.

  • 소켓 통신 동시 요청에 관한 GPT의 답변 (검증 완료)

정리하면

  1. HTTP 통신은 TCP/IP 위에서 동작한다.
  2. OS에서 TCP/IP 통신의 구현체는 소켓이다.
  3. 소켓을 사용하여 통신이 연결될 때 임시포트 중 빈 포트를 점유하여 클라이언트와 연결된다.

만약 빈 포트가 없다면 더 이상 연결을 하지 못하고 대기하게 된다.
즉, 임시포트의 개수는 동시 접속가능한 클라이언트 수가 된다.

3.2. TCP 연결이 안정성을 주기 위한 옵션 TIME_WAIT

TIME_WAIT 라는 옵션은 소켓통신의 안전성을 위해 존재한다.
소켓 통신이 종료 후 일정시간 동안 클라이언트와 연결을 유지하는 옵션이다.

이것은 HTTP 통신의 Keep-Alive 헤더와 비슷한 역할을 수행한다.

3.3. Keep-Alive 사용하지 않음

HTTP통신은 TCP위에서 동작한다.

TCP 기반이라고 하지만 연결의 지속성 측면에서 완전하게 다르다 TCP 통신은 종료 시그널을 보내기 전까지 계속 연결되는 반면 HTTP 통신은 일회성 연결을 지향한다.

하지만 일회성 통신은 TCP에서 한계(비싼 비용, 딜레이)가 있었기 때문에 HTTP 통신 1.1 버전부터는 Keep-Alive라는 헤더를 통해 연결을 잠시 유지하는 기능이 도입되었다.

Keep-Alive에 지정된 시간만큼 TCP 연결을 종료하지 않고 대기하며 동일한 클라이언트에게 요청이 오는 경우 바로 패킷 교환이 이루어진다.
(3-way handshaking, 4-way handshaking를 패스하는 것이다.)

위의 내용을 토대로 요청 과정을 정리하면 병목의 문제를 알 수 있다.

  1. 클라언트는 gql 요청(http)을 서버에 요청한다.
  2. 서버의 OS는 임시포트중 비어있는 포트를 점유하여 소켓을 생성하여 클라이언트와 연결하고 트래픽을 BFF가 실행중인 node에 보낸다.
  3. BFF 앱에서 응답을 종료하면, Keep-Alive를 사용하지 않기 때문에 HTTP 통신은 바로 종료된다.
  4. HTTP 통신은 종료되었지만 소켓의 연결은 TIME_WAIT 설정 값에 의해 60초간 임시 포트를 점유한다.

요청이 임시포트가 고갈될 때까지 반복되면 결국 병목을 야기한다.

4. 문제 해결

const apiHttpsAgent = new https.Agent({
  keepAlive: true,
});
  • keepAlive: true 로 변경: nGrinder 부하 테스트를 진행해 보니 이전보다 나아진 결과를 얻을 수 있었다.

성능비교-개선

  • TPS 기준 프록시 720 -> 3600, graphql 560 -> 1800

해결 결과

프록시 쪽 최대 4000 TPS정도 나오는데 graphql은 2000 TPS 정도 나오고 있으나, 위의 graphql 벤치마크에서 apollo-server-express 성능이 생각보다 낮았던걸 생각해 보면 더 개선할 여지는 있지만 괜찮은 수준이라고 생각되었습니다. keepAlive 변경 이후 트래픽이 몰리는 시간대가 아닌 평소 CPU 상태도 많이 안정적이게 되었습니다.

  • 카카오 기술블로그에 설명된 글