현실을 모방하는 알고리즘

대학에서 처음 컴퓨터 공학 과정을 밟으면서 실생활에서 알고리즘이 작동하는 방식에 큰 관심이 생겼습니다. 실제로 벌어지는 일들에 대해 생각하면 이러한 요소를 모방하는 알고리즘을 찾아낼 수 있습니다. 특히 식품점이나 공항, 대중 교통 수단을 이용할 때 오래 줄을 서 있으면서 이런 생각을 합니다. 줄을 서 있는 동안 지루하다 보니 대기열 이론에 대해 곰곰히 생각해볼 시간이 생기더군요.

10여년전에 저는 Amazon 물류 센터에서 일했었습니다. 그 당시 창고에서 상품을 내리고 다른 상자로 옮기며 통을 이리저리 옮기면서 알고리즘에 관심을 쏟게 되었습니다. 다른 많은 사람들과 함께 일하면서 정교하게 조정된 물리적 병합 정렬의 일부가 된다는 점이 굉장히 멋있다고 생각했습니다.

대기열 이론에서 짧은 대기열의 동작은 비교적 단조롭습니다. 그래도 대기열이 짧으면 모두가 행복하지요. 하지만 대기열이 밀리고(백로그) 이벤트를 대기하는 줄이 문을 넘어가 구석까지 몰리면 사람들은 처리량과 우선순위에 대해 생각하게 됩니다.

저는 이 글에서 대기열 백로그 시나리오를 해결하기 위해 Amazon에서 사용하던 전략에 대해 이야기하고자 합니다. 대기열을 빠르게 비우고(드레이닝) 워크로드 우선순위를 지정하기 위해 세운 설계 접근 방식입니다. 특히 애초에 대기열 백로그가 발생하지 않도록 하는 방법도 설명합니다. 전반부에서는 백로그를 발생시키는 시나리오에 대해 설명하고, 후반부에서는 백로그를 방지하거나 이를 점진적으로 처리하기 위해 Amazon에서 사용했던 많은 접근 방식에 대해 설명합니다.

대기열의 이중적 특성

대기열은 안정적인 비동기식 시스템을 구축하기 위한 강력한 도구입니다. 대기열을 사용하면 하나의 시스템이 다른 시스템의 메시지를 수락하고, 장기 가동 중단, 서버 장애 또는 종속 시스템의 문제가 발생해도 완전히 처리될 때까지 메시지를 지속할 수 있습니다. 장애가 발생했을 때 메시지를 삭제하는 대신 대기열은 처리에 성공할 때까지 메시지를 다시 유도합니다. 결국 대기열은 재시도로 인해 종종 지연 시간이 길어지긴 해도 시스템의 내구성과 가용성을 향상시킵니다.
 
Amazon에서 우리는 대기열을 활용하는 여러 비동기식 시스템을 구축했습니다. 이러한 시스템 중 일부는 시간이 오래 걸리고 amazon.com에 제출된 주문 이행과 같이 현실에서 물리적 사물을 이동시키는 작업과 관련된 워크플로를 처리합니다. 다른 시스템은 상당한 시간이 소요될 수 있는 단계를 조정합니다. 예를 들어, Amazon RDS는 EC2 인스턴스를 요청하고, 인스턴스 시작을 기다린 다음 자동으로 데이터베이스를 구성합니다. 다른 시스템은 배치 작업을 활용합니다. 예를 들어, CloudWatch 지표 및 로그 수집과 관련된 시스템은 많은 데이터를 가져온 후 집계하고 청크 단위로 "평면화"합니다.
 
비동기식 메시지 처리를 위한 대기열의 이점을 쉽게 확인할 수 있지만 대기열 사용 시 위험은 조금 더 미묘합니다. 저희는 수년 동안 가용성을 향상시키기 위한 대기열이 역효과를 발생시키는 상황들을 목격하곤 했습니다. 실제로 가동 중단 이후에 복구 시간이 크게 늘어날 수도 있습니다.
 
대기열 기반 시스템에서 처리가 중지되지만 메시지는 계속 수신되는 경우 메시지가 쌓여 큰 백로그로 누적되고 처리 시간이 길어질 수 있습니다. 작업이 너무 늦게 완료되면 결과의 유용성이 떨어지고, 특히 대기열을 통해 향상시키려는 가용성에 영향을 줄 수 있습니다.
 
다르게 표현하면, 대기열 기반 시스템은 두 가지 모드의 운영 또는 두 가지 모드의 동작을 사용합니다. 대기열에 백로그가 없으면 시스템 지연 시간은 짧고 시스템은 빠른 모드로 실행됩니다. 장애 또는 예상치 못한 로드 패턴으로 인해 도착 비율이 처리 비율을 초과하면 더 열악한 운영 모드로 빠르게 전환됩니다. 이 모드에서는 엔드 투 엔드 지연 시간이 더 길어지고, 빠른 모드로 돌아가기 위해 백로그에서 작업을 수행하는 데 상당한 시간이 걸릴 수 있습니다.

대기열 기반 시스템

이 글에서 대기열 기반 시스템을 설명하기 위해 두 개의 AWS 서비스에서 작업하는 핵심 방식을 소개합니다. 하나는 AWS Lambda로 실행하는 인프라에 대해 걱정하지 않고도 이벤트에 대한 응답으로 코드를 실행하는 서비스이고, 다른 하나는 AWS IoT Core로 연결된 디바이스에서 클라우드 애플리케이션 및 다른 디바이스와 쉽고 안전하게 상호작용하도록 지원하는 관리형 서비스입니다.

AWS Lambda에서는 함수 코드를 업로드한 후, 다음과 같은 두 가지 방식 중 하나로 함수를 호출합니다.

• 동기식: 함수 출력을 HTTP 응답에서 사용자에게 반환함
• 비동기식: HTTP 응답이 즉시 반환되고 함수는 백그라운로 실행 및 재시도됨

Lambda는 서버에서 장애가 발생해도 함수 실행을 보장하므로, 요청을 저장할 내구성이 뛰어난 대기열이 필요합니다. 내구성이 뛰어난 대기열을 사용하면 함수에서 처음 장애가 발생한 경우 요청을 다시 유도할 수 있습니다.

AWS IoT Core를 사용하면 디바이스와 애플리케이션을 연결하고 PubSub 메시지 주제를 구독할 수 있습니다. 디바이스나 애플리케이션에서 메시지를 게시하면 일치하는 구독을 사용하는 애플리케이션에서 메시지의 고유한 사본을 수신합니다. 이러한 대부분의 PubSub 메시징은 비동기식으로 발생합니다. 제한된 IoT 디바이스는 제한된 리소스를 소비하며 구독된 모든 디바이스, 애플리케이션 및 시스템에서 사본을 수신할 때까지 기다리려고 하지 않기 때문입니다. 특히 구독된 디바이스가 오프라인 상태일 때 다른 디바이스가 원하는 메시지를 게시하려는 경우에 중요합니다. 오프라인 디바이스를 다시 연결하면 처음 속도로 다시 돌아간 후 나중에 메시지가 전송되리라 예상합니다(다시 연결 후 메시지 전송을 관리하도록 시스템을 코딩하는 방법에 대한 자세한 내용은 AWS IoT 개발자 안내서에서 MQTT 영구 세션 참조). 이를 위해 백그라운드에 다양한 영구 및 비동기식 처리가 존재합니다.

이와 같은 대기열 기반 시스템은 종종 내구성이 뛰어난 대기열로 구현됩니다. SQS는 내구성 및 확장성이 뛰어나고 하나 이상의 메시지 전송 의미 체계를 제공하므로, Lambda 및 IoT를 포함한 Amazon 팀은 확장 가능한 비동기식 시스템을 구축할 때 SQS를 정기적으로 사용합니다. 대기열 기반 시스템에서 구성 요소는 대기열에 메시지를 입력하여 데이터를 생성하고 다른 구성 요소가 정기적으로 메시지를 요청하고 처리한 후 최종적으로 작업을 완료한 후 삭제하면서 해당 데이터를 소비합니다.

비동기식 시스템 장애

AWS Lambda에서 함수 호출이 정상적인 수준보다 느리거나(예를 들어, 종속성으로 인해 느린 경우) 일시적으로 장애가 발생한 경우 데이터는 유실되지 않으며 Lambda는 함수 실행을 재시도합니다. Lambda는 호출을 대기열에 입력하고 함수가 다시 시작되면 Lambda는 함수 백로그를 처리합니다. 하지만 백로그를 처리하고 정상으로 돌아오는 데 걸리는 시간을 고려해야 합니다.

메시지를 처리하는 동안 오랜 가동 중단이 발생하는 시스템을 가정합니다. 이 경우 지정된 비율과 처리 용량에 상관없이, 가동 중단에서 복구하려면 복구 후 발생하는 추가 시간 때문에 시스템 용량이 두 배 더 필요합니다. 실제로 시스템은 Lambda와 같은 탄력적 서비스에서 가용 용량을 두 배 넘게 보유할 수 있고, 그러면 복구가 더 빨라질 수 있습니다. 하지만 함수가 상호작용하는 다른 시스템은 백로그를 통해 작업할 때 처리가 크게 증가할 경우 이를 해결할 준비가 되지 않았을 수 있습니다. 이러한 상황이 발생하면 해결하는 데 시간이 더 걸릴 수 있습니다. 비동기식 서비스는 동기식 서비스와 달리 가동 중단 동안 백로그를 누적시켜 복구 시간이 더 길어집니다. 동기식 서비스의 경우 가동 중단 동안 요청을 삭제하지만 복구 시간은 더 빨라집니다.

수년 동안 대기열에 관해 고민하면서 비동기식 시스템의 경우 지연 시간이 중요하지 않다고 생각하는 경향이 있습니다. 비동기식 시스템은 종종 내구성을 위해서 또는 지연 시간으로부터 직접 호출자를 격리시키기 위해 구축하곤 합니다. 하지만 실제로 처리 시간이 중요하며, 종종 비동기식 시스템에서 1초 미만의 지연 시간을 기대한다는 점을 알게 되었습니다. 내구성을 위해 대기열을 도입한 경우 백로그를 처리할 때 긴 처리 지연 시간과 같은 단점을 놓치기 쉽습니다. 비동기식 시스템에서 숨겨진 위험은 대형 백로그를 처리하는 것입니다.

가용성과 지연 시간을 측정하는 방법

가용성을 위해 지연 시간을 포기하는 이번 논의에서 한 가지 흥미로운 질문이 발생합니다. 그렇다면, 비동기식 서비스에서 가용성과 지연 시간에 관련된 목표를 세우고 이를 측정하려면 어떻게 해야 할까요? 생산자 측에서 오류 비율을 측정하면 가용성에 대해 충분하지는 않지만 일부분이라도 확인할 수 있습니다. 생산자 가용성은 우리가 사용하는 시스템의 대기열 가용성에 비례합니다. 그래서 SQS에서 구축할 때 생산자 가용성은 SQS 가용성과 일치합니다.

하지만, 소비자 측에서 가용성을 측정할 때 시스템 가용성이 실제보다 더 낮게 나타날 수 있습니다. 장애 시 다시 시도하여 다음 번에 성공할 수 있기 때문입니다.

AWS에서는 DLQ(Dead-Leter Queue)로부터 가용성 측정값을 얻기도 합니다. 메시지 시도 횟수를 초과하면 메시지가 삭제되거나 DLQ에 배치됩니다. DLQ는 나중에 조사하고 간섭하기 위해 처리할 수 없는 메시지를 저장하는 데 사용되는 별도의 대기열을 말합니다. 삭제된 메시지나 DLQ 메시지의 비율은 좋은 가용성의 지표지만 문제를 너무 늦게 감지할 수 있습니다. DLQ 볼륨에 대한 경보를 설정하는 것도 좋은 방법이지만, DLQ 정보는 이 지표에만 의존해서 문제를 감지하기에는 너무 늦게 도착합니다.

지연 시간은 어떨까요? 다시, 생산자 측에서 관찰한 지연 시간은 대기열 서비스 자체의 지연 시간을 반영합니다. 따라서 대기열에 있는 메시지의 수명을 측정하는 데 더 초점을 맞춥니다. 이 방식은 시스템에 뒤쳐진 사례나 자주 오류가 발생하고 재시도가 수행되는 사례를 더 빠르게 포착할 수 있습니다. SQS와 같은 서비스에서는 각 메시지가 대기열에 도달할 때 타임스탬프를 제공합니다. 타임스탬프 정보를 통해 대기열에서 메시지를 가져갈 때마다 시스템이 뒤쳐진 정도에 대해 지표를 생성하고 로깅할 수 있습니다.

지연 시간 문제는 조금 의미가 다를 수 있습니다. 어쨌든 백로그가 예상되고, 일부 메시지에서는 실제로 백로그가 발생해도 괜찮습니다. 예를 들어, AWS IoT에서 디바이스가 오프라인이 되거나 메시지를 읽는 속도가 느려지는 상황이 생길 수 있습니다. 많은 IoT 디바이스의 전력이 낮아지고 인터넷 연결이 불규칙할 수 있기 때문입니다. AWS IoT Core의 운영자는 메시지를 느리게 읽도록 설정되었거나 오프라인 상태인 디바이스로 인해 예상되는 작은 백로그와 예상치 못한 시스템 전체의 백로그를 구별할 수 있어야 합니다.

AWS IoT에서 저희는 또 다른 지표인 AgeOfFirstAttempt로 서비스를 구현했습니다. 이제 이러한 지표 레코드는 메시지의 대기열 지연 시간을 제외합니다. 하지만 AWS IoT에서 디바이스로 메시지를 전달하려고 시도한 첫 번째만 해당됩니다. 이러한 방식으로 디바이스를 백업할 때 메시지를 대기열에 넣거나 메시지를 재시도하는 디바이스로 인해 오염되지 않는 깨끗한 지표를 얻을 수 있습니다. 더 깨끗한 지표를 얻기 위해 두 번째 지표인 AgeOfFirstSubscriberFirstAttempt를 생성합니다. AWS IoT와 같은 PubSub 시스템에서는 특정 주제를 구독할 수 있는 디바이스 개수나 애플리케이션 개수를 제한하지 않으므로, 단일 디바이스로 보낼 때보다 백만 개의 디바이스에 메시지를 전송할 때 지연 시간이 더 길어질 수 있습니다. 안정적인 지표를 얻기 위해 해당 주제의 첫 번째 구독자에게 메시지를 게시하는 첫 번째 시도에서 타이머 지표를 생성합니다. 그리고 남은 메시지를 게시할 때 시스템의 진행 상황을 측정하는 다른 지표도 있습니다.

AgeOfFirstAttempt 지표는 메시지를 느리게 읽도록 선택된 디바이스에서 노이즈를 필터링하기 때문에 전체 시스템의 문제에 대한 조기 경고 역할을 수행합니다. 특히, AWS IoT와 같은 시스템은 이보다 훨씬 더 많은 지표로 구현되었습니다. 하지만 지연 시간과 관련된 모든 지표가 사용 가능하기 때문에, 재시도 지연 시간을 첫 번째 시도의 지연 시간과 분리하여 범주화하는 전략은 Amazon에서 널리 사용되고 있습니다.

비동기식 시스템의 지연 시간과 가용성을 측정하는 작업은 어렵고 서버 간에 요청이 불규칙하고 각 시스템 외부에서 지연이 발생할 수 있기 때문에 디버깅도 까다로울 수 있습니다. 분산 추적을 지원하기 위해 모든 것을 함께 처리할 수 있도록 대기열에 들어가는 메시지에서 요청 ID를 전파합니다. 보통 X-Ray와 같은 시스템으로도 이를 지원합니다.

멀티테넌트 비동기식 시스템의 백로그

많은 비동기식 시스템이 수많은 다른 고객을 대신해 작업을 처리하는 멀티테넌트 체계입니다. 이 경우 지연 시간과 가용성을 관리하려면 한층 더 복잡해집니다. 멀티테넌시의 이점은 별도로 여러 플릿을 운영해야 한다는 운영 부담감에서 벗어날 수 있고, 더 높은 리소스 사용률로 결합된 워크로드를 실행할 수 있다는 점입니다. 그러나 고객은 다른 고객의 워크로드에는 관심이 없고 높은 가용성과 예측 가능한 지연 시간을 통해 자체 단일 테넌트 시스템과 같이 작동하길 바랍니다.

AWS 서비스는 호출자가 메시지를 직접 넣는 내부 대기열을 공개하지 않습니다. 대신, 호출자를 인증하고 대기열에 넣기 전에 각 메시지에 호출자 정보를 추가하는 경량형 API를 구현합니다. 앞서 설명한 Lambda 아키텍처와도 비슷합니다. 함수를 비동기식으로 호출하면 Lambda는 직접 사용자에게 Lambda 내부 대기열을 공개하는 대신 Lambda에서 소유한 대기열에 메시지를 넣고 즉시 반환합니다.

이러한 경량형 API를 통해 공정한 조절 기능도 추가할 수 있습니다. 멀티네넌트 시스템에서 공정성이란 특정 고객의 워크로드가 다른 고객에게 영향을 주지 않도록 한다는 점에서 매우 중요합니다. AWS가 공정성을 구현하는 일반적인 방식은 버스팅에 대한 유연성을 어느 정도 확보하고 고객당 속도 기반 제한을 설정하는 것입니다. 많은 AWS 시스템에서, 예를 들어 SQS에서 고객이 조직적으로 커지면 고객당 제한을 늘립니다. 이 한도는 예상치 못한 급증 조건에 대한 가드레일 역할을 하며, 내부적으로 조정을 프로비저닝할 시간을 확보할 수 있습니다.

어떤 면에서 비동기식 시스템의 공정성은 동기식 시스템의 조절 기능과 비슷하게 작동합니다. 하지만 비동기식 시스템의 경우 대형 백로그가 빠르게 구축된다는 점에서 더 중요합니다.

이 점을 설명하기 위해 비동기식 시스템이 잡음이 많은 이웃 보호 기능을 내재하지 않은 경우 어떤 일이 벌어지는지 살펴보겠습니다. 시스템의 한 고객에게 조절되지 않은 트래픽 급증이 나타났고 전체 시스템 백로그가 생성된 경우 현재 상황을 파악하고 문제를 완화하기 위해 운영자가 개입하는 데 30분 정도 걸릴 수 있습니다. 이 30분 동안 시스템의 생산자 측에서는 크기를 적절히 조절하고 모든 메시지를 대기열에 넣을 수 있습니다. 그러나 대기열에 들어간 메시지의 볼륨이 조절된 소비자 측 용량의 10배인 경우 시스템에서 백로그를 처리하고 복구하는 데 300분이 걸릴 수 있음을 의미합니다. 아무리 짧은 로드의 급증도 여러 시간의 복구 시간이 걸릴 수 있으므로, 여러 시간의 가동 중단도 발생할 수 있습니다.

실제로 AWS의 시스템에는 대기열 백로그로 인한 부정적인 영향을 최소화하거나 방지하는 수많은 보완 요소가 있습니다. 예를 들어, 자동 조정 기능은 로드가 늘어날 때 문제를 완화할 수 있습니다. 하지만 보완 요소를 고려하지 않고 대기열 영향을 단독으로 살펴보는 것이 유용합니다. 이렇게 하면 여러 계층에서 안정적인 설계 시스템에 도움이 되기 때문입니다. 다음은 대형 대기열 백로그와 긴 복구 시간을 방지하는 데 유용한 저희가 발견한 몇 가지 설계 패턴입니다.

모든 계층에서의 보호는 비동기식 시스템에서 중요합니다. 동기식 시스템은 백로그를 생성하지 않기 때문에 프런트 도어 조절 및 승인 제어 기능으로 보호됩니다. 비동기 시스템의 경우 AWS 시스템의 각 구성 요소를 오버로드로부터 보호해야 하며, 하나의 워크로드에서 공정하지 않은 리소스 공유를 사용하지 않도록 방지해야 합니다. 그리고 항상 프런트 도어 승인 제어를 처리하는 워크로드가 있으므로, 서비스가 오버로드되지 않도록 하는 고정용 벨트와 서스펜더 및 포켓 프로텍터가 필요합니다.
둘 이상의 대기열을 사용하면 트래픽 쉐이핑에 유용합니다. 어떤 면에서 단일 대기열과 멀티테넌시는 서로 상충됩니다. 작업이 공유 대기열에 들어갈 때까지 워크로드를 서로 격리하기란 어렵습니다.
실시간 시스템은 종종 FIFO 방식의 대기열로 구현되지만, LIFO 방식의 동작이 선호됩니다. 백로그가 발생했을 때 최신 데이터를 즉시 처리하길 선호한다는 이야기를 고객으로부터 들었습니다. 그리고 트래픽이 급증하거나 가동 중단 동안 누적된 데이터는 가용 용량이 존재하면 처리할 수 있습니다.

탄력적 멀티테넌트 비동기식 시스템을 구축하는 Amazon의 전략

Amazon 시스템에서 멀티테넌트 비동기식 시스템을 워크로드 변동에 탄력적으로 대응할 수 있도록 구성하기 위해 사용하는 몇 가지 패턴이 있습니다. 이러한 많은 기술이 있지만, 각각 자체 수명과 내구성 요구 사항을 갖추고 Amazon 전체에서 사용되는 시스템도 많습니다. 다음 섹션에서는 저희가 사용하는 몇 가지 패턴에 대해 설명하고, AWS 고객이 시스템에서 이를 사용해보고 전해준 이야기를 소개하고자 합니다.

별도의 대기열로 워크로드 구분

모든 고객에게 하나의 대기열을 공유하게 하는 대신, 일부 시스템에서는 각 고객에게 고유한 대기열을 제공합니다. 각 고객이나 워크로드에 대한 대기열을 추가하는 것이 항상 비용 효율적인 것은 아닙니다. 서비스에서 모든 대기열을 폴링하는 데 리소스를 소비해야 하기 때문입니다. 하지만 인접한 시스템이나 고객이 적은 시스템의 경우 이 간단한 솔루션은 큰 도움이 될 수 있습니다. 반면, 시스템에 수십 또는 수백 명의 고객이 있는 경우 별도의 대기열을 사용하면 통제하기 어려워질 수 있습니다. 예를 들어, AWS IoT는 대학의 모든 IoT 디바이스에 대해 별도의 대기열을 사용하지 않습니다. 이 경우 폴링 비용은 잘 조절되지 않습니다.

셔플 샤딩

AWS Lambda는 모든 Lambda 고객에 대해 별도의 대기열을 폴링할 때 많은 비용이 발생하는 시스템의 한 가지 예입니다. 하지만 단일 대기열을 사용할 경우 이 글에서 설명하는 몇 가지 문제가 발생합니다. 그래서 AWS Lambda는 하나의 대기열을 사용하는 대신, 고정된 수의 대기열을 프로비저닝하고 각 고객에게 적은 수의 대기열을 제공합니다. 그리고 메시지를 대기열에 넣기 전에 최소의 메시지를 포함하는 대상 대기열을 확인하고 여기에 메시지를 넣습니다. 한 고객의 워크로드가 늘어나면 매핑된 대기열로 백로그를 유도하지만, 다른 워크로드는 해당 대기열에서 자동으로 해제됩니다. 이러한 놀라운 리소스 격리 방식을 구축하는 데 많은 수의 대기열이 필요하지는 않습니다. Lambda에 구축된 많은 보호 기능 중 하나에 불과하지만, Amazon의 다른 서비스에서도 사용되는 기술이기도 합니다.

별도의 대기열로 초과 트래픽 열외(Sidelining)

어떤 면에서 대기열에 백로그가 생기면 트래픽 우선순위를 지정하기엔 너무 늦은 것입니다. 하지만, 메시지 처리 비용이 비교적 높거나 시간이 오래 걸리는 경우 그래도 메시지를 별도의 스필오버 대기열로 이동할만한 가치가 있습니다. Amazon의 일부 시스템에서 소비자 서비스는 분산 조절 기능을 구현하며, 구성된 비율을 초과하는 고객의 메시지가 대기열에서 해제되면 이러한 초과 메시지를 별도의 스필오버 대기열에 넣고 기본 대기열에서 메시지를 삭제합니다. 시스템은 가용 리소스가 생기면 즉시 스필오버 대기열에서 메시지를 계속 처리합니다. 근본적으로 우선순위 대기열과도 비슷합니다. 생산자 측에서 때때로 비슷한 논리가 구현됩니다. 이러한 방식으로 시스템이 단일 워크로드에서 많은 요청을 수락하면 해당 워크로드는 복잡한 경로 대기열에서 다른 워크로드와 경합하지 않습니다.

별도의 대기열로 오래된 트래픽 열외(Sidelining)

초과 트래픽을 별도 대기열로 지원하는 방법과 마찬가지로, 오래된 트래픽도 열외시킬 수 있습니다. 메시지를 대기열에서 내보내면 메시지가 얼마나 오래되었는지 확인할 수 있습니다. 그리고 그 기간을 단순히 로깅하는 대신, 그 정보를 사용하여 메시지를 백로그 대기열로 이동할지 여부를 결정할 수 있습니다. 그러면 활성 대기열에서 처리량을 따라잡은 후에만 백로그 대기열을 처리합니다. 많은 데이터를 수집하는 로드 급증이 발생하고 작업 속도가 뒤처진 경우 이러한 유형의 트래픽을 가능한 한 빨리 대기열에서 해제하고 다시 다른 대기열에 넣어 열외시킬 수 있습니다. 그러면 소비자 리소스를 확보하여 단순히 백로그를 순서대로 작업하는 것보다 더 빠르게 새로운 메시지를 처리합니다. LIFO 정렬 방식과 비슷합니다.

오래된 메시지 삭제(메시지 수명)

어떤 시스템에서는 너무 오래된 메시지 삭제를 허용할 수 있습니다. 예를 들어, 일부 시스템은 시스템에 대한 델타를 빠르게 처리하지만 주기적으로 전체 동기화도 수행합니다. 이러한 정기적 동기화 시스템을 반엔트로피 스위퍼라고도 합니다. 이 경우 대기열에 있는 오래된 트래픽을 열외하는 대신, 최근 스윕 전에 들어온 경우 적은 리소스로 삭제할 수 있습니다.

워크로드당 스레드 및 기타 리소스 제한

AWS의 동기식 서비스에서도 하나의 워크로드가 공정한 스레드 공유를 초과하여 사용하지 않도록 비동기식 시스템을 설계합니다. 아직 언급하지 않은 AWS IoT의 한 가지 측면은 바로 규칙 엔진입니다. 고객은 고객의 디바이스에서 고객이 소유한 Amazon Elasticsearch 클러스터, Kinesis Stream 등으로 메시지를 라우팅하도록 AWS IoT를 구성할 수 있습니다. 이때 고객이 소유한 리소스에 대한 지연이 느려도 수신 메시지 비율이 일정하면 시스템의 동시성(동시 작업) 크기가 증가합니다. 그리고 시스템이 처리할 수 있는 동시성 크기는 특정 순간에 제한되기 때문에 규칙 엔진을 통해 하나의 워크로드가 동시성과 관련된 리소스의 공정한 공유를 초과하여 소비하지 않도록 합니다.

작업의 하중은 Little’s Law에서 설명되어 있습니다. 즉, 시스템의 동시성은 각 요청의 평균 지연 시간에 도착 비율을 곱한 값과 같습니다. 예를 들어, 서버가 평균 100밀리초의 속도로 초당 100개의 메시지를 처리하는 경우 평균 10개의 스레드를 소비합니다. 지연 시간이 갑자기 10초로 급증하면 갑자기 1,000개의 스레드(평균적으로 이 값이지만 실제로는 더 커질 수 있음)를 사용하고, 손쉽게 스레프 풀을 소진할 수 있습니다.

규칙 엔진은 여러 기술을 사용하여 이러한 상황을 방지합니다. 특정 서버에서 작업량에 대한 다른 제한도 있긴 하지만(예를 들어, 클라이언트가 연결을 통해 전환되고 종속성 제한 시간을 초과한 경우 메모리와 파일 설명자), 스레드 소진을 방지하기 위해 비차단 방식의 I/O를 사용합니다. 사용 가능한 두 번째 동시성 보호 기능으로, 특정 순간에 단일 워크로드가 사용할 수 있는 동시성 크기를 측정하고 제한하는 세마포가 있습니다. 규칙 엔진은 속도 기반 공정성 제한 기능도 사용합니다. 하지만 시간에 따라 워크로드의 변동은 지극히 정상적인 상황이므로 규칙 엔진은 워크로드의 변동에 적응하기 위해 시간에 따라 제한을 자동으로 조정합니다. 그리고 규칙 엔진은 대기열에 기반하기 때문에 내부적으로 리소스와 보호용 제한의 자동 조정과 IoT 디바이스 사이에서 버퍼 역할을 합니다.

Amazon 서비스에서는 하나의 워크로드가 모든 가용 스레드를 소진하는 것을 방지하기 위해 각 워크로드에 대한 별도의 스레드 풀을 사용합니다. 또한 각 워크로드에 대해 AtomicInteger를 사용하여 각각에 대해 허용되는 동시성을 제한하며, 속도 기반 리소스를 격리하기 위해 속도 기반 조절 접근 방식을 사용합니다.

배압 업스트림 전송

워크로드에서 소비자 측에서 따라잡을 수 없는 상당한 백로그를 유도하는 경우 많은 AWS 시스템에서는 생산자 측에서 작업을 더 적극적으로 자동 거부하기 시작합니다. 워크로드에 대한 하루 길이의 백로그는 쉽게 생성됩니다. 워크로드를 격리해도 우연일 수 있으며, 전환 비용도 많이 들어갈 수 있습니다. 이 접근 방식의 구현은 워크로드가 자체 대기열에 있다고 가정하고 워크로드의 대기열 깊이를 가끔 측정하고 백로그 크기에 반비례하여 인바운드 조절 제한을 조정하는 것만큼 간단합니다.

여러 워크로드에 대해 SQS 대기열을 공유하는 경우 이 접근 방식은 조금 까다로워집니다. 대기열에 있는 메시지 수를 반환하는 SQS API는 있어도, 대기열에 있는 특정 속성의 메시지 수를 반환할 수 있는 API는 없습니다. 대기열 깊이를 계속 측정하고 적절히 배압을 적용할 수는 있지만, 동일한 대기열을 공유하는 정상 워크로드에 배압을 편파적으로 배치하게 됩니다. Amazon MQ와 같은 다른 시스템에서는 세분화된 백로그 가시성을 제공합니다.

배압이 Amazon의 모든 시스템에 적합한 것은 아닙니다. 예를 들어, amazon.com에 대한 주문 처리를 수행하는 시스템의 경우 백로그가 생성되어도 새 주문의 수락을 차단하는 대신 주문을 수락하려고 합니다. 물론, 가장 급한 주문을 먼저 처리하기 위해 내부적으로 상당한 우선순위를 지정하는 작업이 수반됩니다.

지연 대기열을 사용하여 나중으로 작업 지연

시스템이 특정 워크로드의 처리량을 줄여야 한다고 인식하면 해당 워크로드에 대해 백오프 전략을 사용하려고 합니다. 이를 구현하기 위해 나중으로 메시지 전송을 지연하는 SQS 기능을 종종 사용합니다. 메시지를 처리하다가 나중에 처리하기 위해 저장하려는 경우 때때로 별도의 급증 대기열에 해당 메시지를 다시 넣지만, 몇 분 동안 지연 대기열에 메시지를 숨겨두도록 지연 파라미터를 설정합니다. 그러면 시스템에서 대신 최신 데이터를 처리할 수 있습니다.

너무 많은 이동 중 메시지 방지

SQS와 같은 일부 대기열 서비스는 대기열의 소비자 측으로 전송할 수 있는 이동 중 메시지 수와 관련된 제한을 사용합니다. 대기열에 보유할 수 있는 메시지 수(이 경우 실질적인 제한은 없음)와는 다르지만, 소비자 플릿에서 즉시 처리하는 메시지 수를 의미합니다. 이 수치는 시스템이 메시지를 대기열에서 내보내지만 삭제하지 못한 경우에 급증할 수 있습니다. 예를 들어, 메시지를 처리하는 중 코드에서 예외를 포착하지 못하고 메시지 삭제를 잊어버리는 버그를 볼 수 있습니다. 이 경우 메시지는 메시지의 VisibilityTimeout에 대한 SQS의 관점에서 이동 중으로 남아 있습니다. 저희는 오류 처리 및 오버로드 전략을 설계할 때 이러한 제한을 염두에 두고 초과 메시지를 표시 상태로 두는 대신 다른 대기열로 이동하는 방식을 선호합니다.

SQS FIFO 대기열도 비슷하지만 조금 다른 제한을 사용합니다. SQS FIFO에서 시스템은 지정된 메시지 그룹에 대해 순서대로 메시지를 소비하지만, 다른 그룹의 메시지는 임의의 순서로 처리합니다. 따라서 하나의 메시지 그룹에서 작은 백로그가 생성되어도 다른 그룹의 메시지를 계속 처리합니다. 하지만 SQS FIFO는 최근 처리되지 않은 20,000개의 메시지만 폴링합니다. 메시지 그룹의 한 하위 집합에 처리되지 않은 20,000개가 넘는 메시지가 있으면, 최신 메시지를 포함하는 다른 메시지 그룹은 처리되지 않습니다.

처리할 수 없는 메시지에 대해 배달하지 못한 편지 대기열 사용

처리할 수 없는 메시지는 시스템 오버로드를 일으킬 수 있습니다. 시스템이 처리할 수 없는 메시지를 대기열에 넣으면(입력 검증 엣지 케이스로 트리거되기 때문일 수 있음) SQS는 이러한 메시지를 DLQ(배달하지 못한 편지 대기열) 기능을 보유한 별도의 대기열에 자동으로 이동시킬 수 있습니다. 이 대기열에 메시지가 있으면 수정해야 하는 버그가 있다는 것을 알리는 경보를 알립니다. DLQ의 장점은 버그를 수정한 후 메시지를 재처리할 수 있다는 점입니다.

워크로드당 스레드 폴링을 위해 추가 버퍼 지원

워크로드가 안정적 상태여도 폴링 스레드가 항상 사용량이 많은 지점까지 처리량을 유도하는 경우 시스템은 트래픽 급증을 흡수할 버퍼가 없는 상태에 이를 수 있습니다. 이 상태에서 수신 트래픽이 조금만 급증해도 처리하지 못한 백로그 크기가 계속 유지되고, 이로 인해 지연 시간이 길어질 수 있습니다. 이러한 버스트를 흡수하기 위해 폴링 스레드에 추가 버퍼를 계획하고 있습니다. 빈 응답을 생성하는 폴링 시도 수를 추적하는 것도 한 가지 방법입니다. 모든 폴링 시도에서 메시지를 하나 더 검색하면 정확히 필요한 폴링 스레드 수를 얻거나, 아니면 수가 부족하여 수신 트래픽을 따라잡지 못할 수 있습니다.

장기 실행 메시지 하트비트

시스템에서 SQS 메시지를 처리하면 SQS는 시스템에서 크래시가 발생했다고 가정하기 전에 메시지 처리를 완료하고 재시도를 위해 다른 소비자에게 메시지를 전송할 수 있는 일정 시간을 시스템에 제공합니다. 코드가 계속 실행되고 이 마감을 잊어버리면 동일한 메시지가 병렬로 여러 번 전송될 수 있습니다. 첫 번째 프로세서는 제한 시간이 지나면 메시지에서 여전히 해제되지만, 두 번째 프로세서가 해당 메시지를 선택해 처리하고 마찬가지로 제한 시간을 지나면 역시 해제되며, 세 번째 프로세서가 이를 이어받는 식으로 작동합니다. 메시지가 만료될 때 작업을 중지하거나 해당 메시지의 하트비트를 계속 진행하여 메시지를 계속 처리하고 있음을 SQS에 알리기 위한 메시지 처리 논리를 구현하는 이유는 바로 이러한 잠재적인 계단식 절전 기능 때문입니다. 이 개념은 리더 선택에서 임대와 비슷합니다.

하지만 조금 까다로운 문제입니다. 데이터베이스에 대한 쿼리가 너무 오래 걸리거나 서버가 처리할 수 있는 수준보다 더 많은 작업을 처리하기 때문에 발생하는 오버로드 중에 시스템 지연 시간이 길어질 수 있기 때문입니다. 시스템 지연 시간이 VisibilityTimeout 임계값을 지나면 이미 오버로드된 서비스가 특히 포크밤 자체를 일으킬 수 있습니다.

교차 호스트 디버깅 계획

분산 시스템에서 장애를 파악하기란 아직도 어렵습니다. 계측에 대한 관련 글에서는 정기적으로 대기열 깊이를 기록하는 작업부터 "추적 ID"를 전파하고 X-Ray에 통합하는 작업까지 비동기식 시스템을 계측하기 위한 여러 접근 방식을 소개하고 있습니다. 아니면 AWS 시스템에서 부수적인 SQS 대기열 외에도 복잡한 비동기식 워크플로를 사용하는 경우 종종 Step Functions와 같은 다른 비동기식 워크플로 서비스를 사용하곤 합니다. 이 서비스는 워크플로에 대한 가시성을 제공하고 분산 디버깅을 단순화합니다.

결론

비동기식 시스템에서는 지연 시간이 얼마나 중요한지 간과하기 쉽습니다. 어쨌든 비동기식 시스템은 안정적인 재시도를 수행하기 위해 대기열을 사용하기 때문에 때때로 더 오래 걸립니다. 하지만 오버로드와 장애 시나리오가 발생하면 서비스에서 합리적인 시간 안에 복구할 수 없는 상당한 백로그가 생성될 수 있습니다. 이러한 백로그는 예상치 못한 빠른 속도로 대기열에 메시지를 입력하는 하나의 워크로드 또는 고객, 예상 처리 수준보다 더 비용이 많이 드는 워크로드 또는 종속성에서의 장애나 지연 시간으로 인해 발생할 수 있습니다.

비동기식 시스템을 구축할 때는 우선순위 지정, 열외 및 배압과 같은 기술을 사용하여 이러한 백로그 시나리오에 초점을 맞춰 이를 예상하고 최소화해야 합니다.

추가 자료

Queueing theory
Little's law
Amdahl's law
• Little A Proof for the Queuing Formula: L = λW, Case Western, 1961
• McKenney, Stochastic Fairness Queuing, IBM, 1990
• Nichols 및 Jacobson, Controlling Queue Delay, PARC, 2011

저자에 대하여

David Yanacek는 AWS Lambda 서비스의 선임 수석 엔지니어입니다. David는 2006년부터 Amazon에서 소프트웨어 개발자로 근무하고 있으며, 전에는 Amazon DynamoDB와 AWS IoT는 물론, 내부 웹 서비스 프레임워크와 플릿 운영 자동화 시스템도 다루었습니다. David가 회사에서 가장 즐겨하는 활동 중 하나는, 로그를 분석하고 운영 지표를 면밀히 조사하며 시간에 따라 시스템을 보다 더 원활하게 실행시키는 방법을 찾는 것입니다.

분산 시스템에서 리더 선택 운영 가시성을 위한 분산 시스템 계측