대용량 트래픽 테스트 - daeyonglyang teulaepig teseuteu

쿠팡 이커머스 앱의 활성 사용자 규모는 2021년에만 전년 대비 21% 증가했습니다. 저희 쿠팡의 서버 엔지니어는 고객 모두에게 매끄러운 “WOW” 경험을 보장하는 일을 책임지고 있습니다. 코어 서빙 레이어를 고가용성, 고처리량, 지연시간 최소화가 가능한 마이크로서비스로 개발해, 벡엔드 서비스가 고객에게 안전하게 데이터를 제공할 수 있도록 운영하고 있습니다. 코어 서빙 레이어와 관련해 자세한 정보는 이전 포스트에서 확인하실 수 있습니다.

이번 포스트에서는 코어 서빙 플랫폼에서 캐시 레이어가 코어 서빙 레이어와 함께 어떠한 역할을 하는지 간략히 설명하고, 코어 서빙 레이어를 운영하면서 저희가 지금껏 경험했던 기술적 어려움과 이를 개선하기 위해 들였던 노력을 공유하고자 합니다.

목차

· 코어 서빙 레이어의 캐시 레이어
· 과제 1: 캐시 노드 장애 관리
· 과제 2: 장애 노드의 빠른 복구
· 과제 3: 캐시 노드 간 트래픽 분산
· 과제 4: 로컬 캐시를 활용한 트래픽 급증 처리
· 마무리

코어 서빙 레이어의 캐시 레이어

코어 서빙 레이어는 read-through 캐시 레이어와 실시간 캐시 레이어, 이 두 개의 캐시 레이어로 구성되어 있습니다. 쿠팡의 대규모 고객 트래픽을 지원하기 위해 각 캐시 레이어는 60~100개의 노드로 구성되었고 분당 억단위의 요청을 처리하고 있습니다.

코어 서빙 레이어에서는 유입 트래픽의 95%를 데이터 스토리지 레이어가 아닌 캐시 레이어에서 처리합니다. 달리 말하면 코어 서빙 레이어 전체의 가용성은 캐시 레이어의 가용성에 크게 의존하고 있으며, 쿠팡이 제공하고자 하는 완벽한 고객 경험에 있어 캐시 레이어가 가장 중요하고 필수적인 부분이라고 볼 수 있습니다.

쿠팡 코어 서빙 레이어의 전체 아키텍처

그림 1. 코어 서빙 레이어의 전체 아키텍처

과제 1: 캐시 노드 장애 관리

저희가 마주한 첫번째 과제는 캐시 클러스터에서 캐시 노드 일부분에서 발생하는 장애를 관리하는 것이었습니다. 클라우드 서버에서 캐시 레이어를 운영하다보면, 개별 노드에 장애가 흔히 발생합니다. 때로는 클라우드 호스팅 쪽의 문제로 여러 노드에 동시다발적으로 장애가 발생할 수도 있습니다. 노드 장애는 짧으면 1분, 길면 10분까지도 이어져 시스템 전체가 불안정해지는 현상을 자주 겪었고, 저희는 이런 현상을 최소화할 방법을 찾아내야 했습니다.

인시던트(incident) 발생 시 해당 구성 요소를 격리하는 회로 차단(circuit breaker) 메커니즘이 적용되어 있지만, 단일 노드의 장애는 전체 트래픽의 극히 일부분에만 영향을 미치기 때문에 회로 차단기로는 감지할 수 없었습니다.

쿠팡이 노드 장애 처리 시 취했던 기존 방식

그림 2. 기존의 장애 노드 처리 방식

우선 토폴로지의 새로고침(topology refresh) 중에 장애 노드가 발견되면 캐시 클러스터가 클러스터 토폴로지를 변경하도록 프로그래밍했습니다. 다만, 해당 프로세스는 어느 정도의 시간이 필요하고, 그동안 미처리된 요청들이 타임아웃을 일으키거나 큐에 쌓일 수도 있습니다. 게다가 변경된 클러스터 토폴로지가 실제 TCP 연결 상태에대응하지 못하는 경우가 종종 발생했고, 트래픽은 계속 장애 노드로 유입됐습니다. 회로 차단기와 마찬가지로, 이 방법 또한 높은 가용성과 지연시간 최소화라는 요구사항을 충족시키지 못했습니다.

장애 노드의 빠른 감지

시스템 안전성 향상을 위해, 장애 노드로 가는 트래픽을 더 빠르게 재라우팅(re-route)할 수 있는 확실한 해결책이 필요했습니다. 그래서 토폴로지 새로고침뿐만 아니라, TCP 연결 속도도 모니터링하기로 했습니다. 캐시 레이어의 응답 시간은 보통 ms 수준이기 때문에 1초 이내에 응답이 없는 연결은 ‘문제있음’으로 마킹하고 해당 노드와의 연결을 자동으로 종료하도록 했습니다.

이를 통해 장애 노드를 찾는 속도가 획기적으로 빨라졌고, 안전성도 높아졌습니다.

TCP 접속의 모니터링을 통해 수 초 이내로 장애 노드를 감지하는 쿠팡의 방식

그림 3. TCP 접속을 모니터링하여 수 초 이내에 장애 노드를 감지할 수 있게 되었습니다.

과제 2: 장애 노드의 빠른 복구

장애 노드 감지에 더해 고처리량과 고가용성의 데이터 서빙을 유지하기 위해 장애 노드를 빠르게 복구해야 했습니다.

노드에 장애가 발생하면 전체 동기화(Full sync) 과정을 거쳐 노드를 복구합니다. 전체 동기화 중 마스터 노드에 수신되는 쓰기 명령은 버퍼에 저장됩니다. 하지만 캐시 레이어로 유입되는 트래픽은 버퍼에서 감당할 수 없는 규모였기에, 전체 동기화 및 노드 복구 프로세스 과정은 자주 실패했습니다.

단기 해결책으로 복구 중 5~10분간 캐시 무효화(cache invalidation)를 일시적으로 중지하거나 클러스터 전체를 새로 교체해야만 했습니다. 그러나 이런 방법은 운영에 부담이 되었고, 근본적인 해결책이 필요했습니다.

근본 원인 찾기

먼저 저희는 적정 버퍼 사이즈를 파악하기 위해 전체 동기화 과정에서 발생할 수 있는 쓰기 명령 횟수를 추정해 보았습니다. 쓰기 명령 횟수 예측치를 기반으로 여러 버퍼 사이즈를 테스트해 보았지만, 서로 상이한 테스트 결과들이 나왔고 딱 맞는 버퍼 사이즈는 없었습니다.

그러다가 매번 전체 동기화 과정에서 쓰기 명령 횟수가 예상을 크게 상회한다는 걸 알게 되었습니다. 좀 더 살펴보니, 전체 동기화 과정 중에 새로 생성되는 복제본에서 빈 데이터 세트가 코어 서빙 플랫폼으로 서빙되고 있었습니다. 이 때문에 시스템은 해당 캐시 클러스터에 적절한 데이터가 존재하지 않는다고 인식하게 되어, 쓰기 명령이 급격히 증가하는 것이었습니다.

캐시 전체 동기화 과정에서 쿠팡 서비스에 장애가 발생했던 이유: 빈 캐시 복제본으로 향하는 트래픽

그림 4. 트래픽이 빈 복제본으로 향하는 것이 전체 동기화 과정에서 발생하는 장애의 근본 원인이었습니다.

결함 있는 복제본으로 유입되는 트래픽 차단하기

클러스터 복구 장애의 근본 원인을 밝혀낸 이후, 저희는 장기 해결책을 마련할 수 있었습니다.

먼저 복제본의 데이터 크기 또는 상태 정보로 복제본이 정상적인 데이터 서빙을 할 수 없다는 판단되면, 해당 복제본으로 유입되는 트래픽 모두를 차단했습니다. 결함이 있는 복제본으로의 트래픽을 차단함으로써 쓰기 명령의 급격한 증가를 막을 수 있었고, 전체 동기화의 실패 또한 막을 수 있었습니다. 이러한 커스터마이제이션을 모든 캐시 클러스터에 적용하여 안정성을 상당 수준 강화했습니다.

해결책을 검증하기 위해 저희는 각각 60개의 노드로 구성된 두 개의 캐시 클러스터를 만들어 피크 트래픽 상황에서 들어오는 요청이 어떻게 처리되는지 실험해보았습니다. 첫 번째 클러스터는 기존 클러스터와 같은 설정으로 구성했고, 두 번째 클러스터는 결함이 있는 복제본의 트래픽을 차단하는 설정으로 구성했습니다.

극한의 장애 상황을 테스트하기 위해 각 클러스터의 노드 12개를 강제 종료하자 첫 번째 클러스터는 P95 지연시간(latency)에서 스파이크는 최대 500~1000 ms으로 나타났고, 애플리케이션의 CPU는 불안정해지고, 마이너 및 메이저 가비지 컬렉션(GC)이 발생했습니다. 반면, 커스터마이징된 설정의 두 번째 클러스터는 P95 지연시간에서 스파이크가 최대 100 ms 미만으로 나타났고, CPU는 안정적이었고, 마이너 가비지 컬렉션들은 별다른 오류 없이 처리되었습니다.

기존 설정과 커스터마이징 설정이 적용된 쿠팡 캐시 클러스터들 간의 비교

그림 5. 기존 설정과 커스터마이징 설정이 적용된 클러스터들 간의 비교

과제 3: 캐시 노드 간 트래픽 분산

세 번째 과제는 캐시 클러스터가 크다 보니 생기는 각 노드 별로 유입되는 트래픽의 불균등 문제였습니다. 각 노드가 처리하는 트래픽 규모가 적게는 다섯 배, 크게는 열 배까지 차이가 나 CPU 사용량에 있어 노드들 사이에 격차가 생겨났습니다. 당시엔 트래픽을 가장 많이 받는 노드를 기준으로 모든 클러스터의 크기를 산정해야 했고, 그로 인해 필요한 것보다 더 큰 클러스터가 생성되었습니다. 리소스를 비효율적으로 낭비하고 있었습니다.

캐시 클라이언트 재구성

쿠팡 캐시 클라이언트와 캐시 노드 사이의 트래픽 불균등 및 쏠림 현상

그림 6. 캐시 클라이언트가 동일 노드로 트래픽을 반복해 보내고 있었습니다.

조사 결과, 캐시 클라이언트로 인해 캐시 노드로의 트래픽에 불균등이 생긴다는 걸 알게 되었습니다. 캐시 클라이언트는 가장 빨리 응답하는 샤드(shard)와 노드를 찾아, 해당 노드와 반복적으로 통신하도록 되어 있었습니다.

이러한 이슈를 해결하기 위해 저희는 캐시 클라이언트의 연결 모드를 재설정했습니다. 최초 연결 때 정해지는 노드가 아닌 요청할 때마다 캐시 클라이언트가 노드를 무작위로 선정하게 만들어 모든 노드에 트래픽이 균등하게 분산되도록 처리했습니다.

쿠팡 캐시 클라이언트 재설정 조치 후 균등해진 캐시 클라이언트와 캐시 노드 사이의 트래픽

그림 7. 캐시 클라이언트의 재설정 이후 샤드 간 트래픽이 균등해졌습니다.

재설정의 결과는 아래의 그림에서 확인할 수 있습니다. X 축의 04:30 지점이 바로 캐시 클라이언트에 새로운 설정을 적용한 시점입니다. 좌측 그래프에서 알 수 있듯이 노드 간 큰 격차를 보이던 트래픽이 거의 균등해졌습니다. 우측 그래프에서는 요청이 동적으로 할당되는 것 때문에 CPU 사용량이 약간 증가했음을 알 수 있었습니다. 하지만 트래픽의 균등한 분배와 전체 시스템의 안정성을 위해서라면 충분히 지불할 수 있는 아주 작은 비용이었습니다.

쿠팡 캐시 클라이언트의 재설정 이후 균등해진 노드 간 트래픽과 안정적으로 변한 CPU 사용량

그림 8. 캐시 클라이언트의 재설정 이후 노드 간 트래픽은 균등해지고 CPU 사용량은 안정적으로 변했습니다.

과제 4: 로컬 캐시를 활용한 트래픽 급증 처리

마지막으로 소개할 과제는 데이터 처리량과 관련되어 있습니다. 사용자 유입의 증가는 항상 반길 일이지만 서버 엔지니어에겐 까다로운 문제입니다.

쿠팡에서는 사용자 트래픽이 서버 용량을 넘어선 이벤트가 몇 차례 진행된 적이 있습니다. 디지털 기기 및 스마트폰 사전예약 이벤트 등이 그랬습니다. 다행히도 이러한 이벤트는 계획에 따라 진행되므로 미리 추가 서버를 확보하고 트래픽 증가에 대비할 수 있었습니다.

하지만 코로나-19 이후 예상치 못한 트래픽 급증이 빈번히 발생하기 시작했습니다. 트래픽 급등의 원인이 정확히 파악되지 않다보니, 어쩔 수 없이 트래픽 대비 적정 서버 용량의 3배 정도 이상을 항상 유지하고 있어야했습니다.

트래픽 분석

적정 서버 용량의 3배를 유지하는 것은 비용이 많이 들고 비효율적인 일이었습니다. 불규칙한 트래픽 급증의 원인을 파악하기 위해, 유입되는 요청들을 메시지 큐에 저장하고 MapReduce를 사용해 분 단위로 해당 요청들의 사용자 및 제품 정보를 분석해 보았습니다.

분석 결과 특정 제품에 대한 정보 요청이 전체 트래픽의 상당 부분을 차지하고 있음을 발견할 수 있었습니다. 코로나-19 도중의 트래픽 급증 대부분은 마스크 재입고 정보 확인과 관련해 발생했습니다.

로컬 캐시

로컬 캐시 레이어가 추가된 쿠팡의 코어 서빙 레이어: 트래픽 급증 시점의 유입 트래픽 70%를 이슈 없이 처리할 수 있게 됨

그림 9. 로컬 캐시 레이어를 추가해 트래픽 급증 시점의 유입 트래픽 70%를 이슈 없이 처리할 수 있게 되었습니다.

일부 사용자의 특정 제품 정보 요청으로 인해 발생하는 트래픽 급증을 해결하기 위해 저희는 로컬 캐시 레이어를 추가했습니다. 다만, 로컬 캐시 레이어는 애플리케이션 성능 개선에는 도움이 되지만 캐시 무효화와 관련해 몇몇 이슈가 추가로 발생할 수 있고, 가비지 컬렉션(GC) 횟수 및 시간도 증가할 수 있었습니다.

해당 로컬 캐시 레이어에 대한 별도의 캐시 무효화 프로세스가 없는 이유로, 캐시 시간을 1분으로 제한하고 강력한 일관성(strong consistency)이 아닌 최종 일관성(eventual consistency)이 보장되는 데이터만을 업데이트하기로 했습니다. 또한, 가비지 컬렉션 이슈를 해결하기 위해 데이터를 DTO 형식 대신 바이트 배열(byte array) 형식으로 저장했습니다. 그 결과, 직렬화 비용은 증가했지만 가비지 컬렉션 비용은 상당히 절감할 수 있었습니다.

평상시 로컬 캐시 레이어는 트래픽의 35% 정도만 처리하지만, 트래픽이 급증할 때에는 로컬 캐시 레이어로 유입 트래픽의 70%까지 처리하게 되었습니다.

비차단 I/O

트래픽 급등에 서버 안정성을 강화하기 위한 마지막 방법으로 차단 I/O(blocking I/O) 기반의 애플리케이션은 비차단 I/O(non-blocking I/O) 기반의 애플리케이션으로 변경했습니다. 기존 데이터 스토리지 및 캐시 시스템이 NIO를 이미 지원하고 있어 이전 작업은 간단했습니다. 이런 서버 구조 변경으로 CPU 사용량은 50% 이상 줄이고 NIO 쓰레드를 사용해 CPU 오버헤드를 최소화할 수 있었습니다.

마무리

쿠팡에게는 어떤 과제든 그 크기와 상관없이 모두 중요합니다. 고객에게 지금보다 더 나은 서비스를 제공하겠다는 마음가짐으로 저희는 눈 앞에 주어지는 모든 과제들을 매일매일 해결해나가고 있습니다. 그렇게 위에 열거된 과제들을 해결하며 코어 서빙 레이어를 개선해, 저희는 고객에게 지속적으로 고가용성, 고처리량, 지연시간 최소화의 데이터 서빙을 제공할 수 있게 되었습니다.