몇 년 동안 저는 Amazon의 서비스 프레임워크 팀에서 일했습니다. 저희 팀은 Amazon Route 53 및 Elastic Load Balancing과 같은 AWS 서비스 소유자가 자체 서비스를 보다 빠르게 구축하고 서비스 클라이언트가 보다 쉽게 이러한 서비스를 호출할 수 있도록 도움이 되는 도구를 만들었습니다. 다른 Amazon 팀에서는 서비스 소유자에게 계측, 인증, 모니터링, 클라이언트 라이브러리 생성, 문서 생성과 같은 기능을 제공했습니다. 그리고 각 서비스 팀이 이러한 기능을 서비스에 수동으로 통합하는 대신, 서비스 프레임워크 팀에서 이러한 통합을 한 번에 수행하고 구성을 통해 각 서비스에 기능을 공개했습니다.

저희가 직면한 한 가지 과제는, 특히 성능이나 가용성과 관련된 기능에 대해 합리적인 기본값을 제공하는 방식을 결정하는 것이었습니다. 가령, 클라이언트 측 기본 제한 시간은 쉽게 설정할 수 없습니다. 프레임워크에 API의 지연 시간 특성이 어떨지 전혀 정보가 없기 때문입니다. 서비스 소유자나 클라이언트가 직접 알아내기도 쉽지 않기 때문에 저희는 계속 노력하며 유용한 인사이트를 확보했습니다.

그러면서 생기는 한 가지 공통된 질문은, 클라이언트에 대해 동시에 열 수 있는 서버의 기본 연결 수를 어떻게 결정하는가였습니다. 이 설정은 서버에 너무 많은 작업을 부과하여 오버로드되지 않도록 하기 위해 설계된 것입니다. 특히, 저희는 로드 밸런서에 대한 최대 연결에 비례하여 서버의 최대 연결 설정을 구성하고자 했습니다. 당시는 Elastic Load Balancing이 나오기 전이어서, 하드웨어 로드 밸런서가 널리 사용되던 때였습니다.

Amazon 서비스 소유자와 서비스 클라이언트가 로드 밸런서에서 설정한 최대 연결에 대해 바람직한 값과 저희가 제공하는 프레임워크에서 설정할 해당되는 값을 쉽게 알아낼 수 있도록 작업에 착수했습니다. 그리고 사람의 판단을 통해 선택하는 방법을 모색할 수 있다면 이러한 판단을 에뮬레이션하도록 소프트웨어를 작성할 수 있으리라 생각했습니다.

바람직한 값을 결정하려던 작업이 굉장히 큰 일이 되고 말았지요. 최대 연결을 너무 낮게 설정하면 로드 밸런서는 서비스에 용량이 많아도 요청 수의 증가를 차단할 수 있습니다. 최대 연결을 너무 높게 설정하면 서버 속도가 느려지고 응답하지 않을 수 있습니다. 워크로드에 맞게 최대 연결을 설정하면 워크로드가 전환되거나 종속된 요소의 성능이 변경됩니다. 그러면 다시 잘못된 값이 되고 불필요한 가동 중단이나 오버로드가 발생할 수 있습니다.

결국 최대 연결의 개념은 너무 부정확하여 문제에 대한 완벽한 답을 찾을 수 없다는 결론을 내렸습니다. 이 글에서는 문제 해결에 도움이 되었던 로드 차단과 같은 다른 접근 방식에 대해 설명하고자 합니다.

오버로드의 분석

Amazon에서는 오버로드 상황이 발생하기 전에 사전에 확장하도록 시스템을 설계하여 오버로드를 방지합니다. 그러나 시스템 보호에는 계층의 보호도 포함됩니다. 이는 자동 조정으로 시작되지만 초과 로드를 정상적으로 차단하는 메커니즘, 이러한 메커니즘을 모니터링하는 기능, 그리고 가장 중요한 지속적인 테스트도 포함합니다.
 
서비스의 로드 테스트를 수행할 때 사용률이 낮은 서버의 지연 시간은 사용률의 높은 서버의 지연 시간보다 더 낮다는 점을 확인했습니다. 로드가 과중한 상황에서 스레드 경합, 컨텍스트 전환, 가비지 수집 및 I/O 경합은 더욱 두드러집니다. 결국 서비스는 성능이 더 급속하게 저하되기 시작하는 변곡점에 도달합니다.
 
이 관찰 이면에 숨겨진 이론은 암달의 법칙에서 파생된 USL(Universal Scalability Law)이라고 합니다. 이 이론에서는 시스템의 처리량이 병렬화를 사용하여 증가할 수 있지만, 궁극적으로는 직렬화 지점의 처리량(즉, 병렬화할 수 없는 태스크)을 통해 제한됩니다.
 
안타깝지만, 처리량은 시스템 리소스로 제한될 뿐만 아니라, 시스템이 오버로드되면 보통 처리량도 떨어집니다. 리소스에서 지원하는 것보다 시스템에 작업이 더 많으면 속도가 느려집니다. 컴퓨터는 오버로드되어도 작업을 받지만, 컨텍스트를 전환하는 데 더 많은 시간을 소비하고 속도도 너무 느려져서 유용성이 떨어집니다.
 
클라이언트가 서버와 대화하는 분산 시스템의 경우 보통 클라이언트는 참을성이 없으며 얼마 후에는 서버 응답을 더 이상 기다리지 않습니다. 이 기간을 제한 시간이라고 합니다. 서버가 오버로드되고 지연 시간이 클라이언트의 제한 시간을 초과하면 요청은 실패하기 시작합니다. 다음 그래프는 제공된 처리량(초당 트랜잭션 수)이 증가하면 서버 응답 시간이 증가하고, 응답 시간이 사실상 빠르게 떨어지는 변곡점에 도달하는 과정을 보여줍니다.

이전 그래프에서 응답 시간이 클라이언트 제한 시간을 초과하면 상황이 잘못되었다는 사실이 명확해지지만 그래프만으로는 얼마나 잘못되었는지 알 수 없습니다. 이를 설명하기 위해 지연 시간과 함께 클라이언트에서 인지하는 가용성을 구성해볼 수 있습니다. 일반적인 응답 시간 측정을 사용하는 대신, 평균 응답 시간을 사용하도록 전환할 수 있습니다. 평균 응답 시간이란 요청의 50%가 평균값보다 더 빠름을 의미합니다. 서비스의 평균 지연 시간이 클라이언트 제한 시간과 같으면 요청의 절반은 제한 시간을 초과하고 가용성은 50%가 됩니다. 이때 지연 시간 증가로 인해 지연 시간 문제가 가용성 문제로 바뀌게 됩니다. 다음은 이러한 상황을 보여주는 그래프입니다.

이 그래프는 보기 조금 까다롭습니다. 가용성 문제를 설명하는 더 단순한 방법은 굿풋처리량과 구별하는 것입니다. 처리량은 서버로 전송되는 초당 총 요청 수입니다. 굿풋은 클라이언트에서 응답을 활용하기에 충분한 낮은 지연 시간으로 오류 없이 처리할 수 있는 처리량의 하위 집합입니다.

긍정적인 피드백 루프

오버로드 상황이 길어지면 피드백 루프에서 그 효과가 증대됩니다. 클라이언트에서 제한 시간을 초과하면 클라이언트에서 오류가 생길 수 있기 때문에 좋지 않습니다. 상황이 더 악화되면 서버에서 해당 요청에서 지금까지 수행한 모든 진행 상황이 헛수고가 될 수도 있습니다. 용량이 제한되는 오버로드 상황에서 시스템에서 벌어질 수 있는 최악의 상태는 바로 낭비되는 작업입니다.

게다가 클라이언트는 종종 요청을 재시도하면서 상황이 악화됩니다. 그리고 시스템에 부과되는 로드가 더 가중됩니다. 서비스 중심 아키텍처(즉, 클라이언트가 서비스를 호출하고, 여기서 다른 서비스를 호출하며, 여기서 또 다른 서비스를 호출하는 방식)에서 호출 그래프가 충분히 깊고 각 계층이 여러 번 재시도를 수행하면 맨 아래 계층에서 오버로드가 발생하여 계단식으로 재시도가 수행되고, 이로 인해 제공되는 로드가 기하급수적으로 증가합니다.

이러한 요소가 결합되면 오버로드로 인해 피드백 루프가 생성되고, 오버로드는 정적인 상태가 됩니다.

작업의 낭비 방지

표면적으로 로드 차단은 간단합니다. 서버가 오버로드에 다가가면 초과 요청을 거부하기 시작하여 허용하기로 결정한 요청에 집중할 수 있습니다. 로드 차단의 목표는, 클라이언트에서 제한 시간을 초과하기 전에 서비스에서 회신할 수 있도록 서버가 수락하기로 결정한 요청에 대해 지연 시간을 낮게 유지하는 것입니다. 이 접근 방식을 사용하면 서버는 수락하는 요청에 대해 높은 가용성을 유지하고, 초과 트래픽의 가용성만 영향을 받게 됩니다.

초과 로드를 차단하여 지연 시간을 관리하면 시스템의 가용성이 향상됩니다. 하지만 이 접근 방식의 이점은 이전 그래프에서 시각화하기 어렵습니다. 전반적인 가용성 라인은 여전히 아래쪽에 있고, 상황이 나빠 보입니다. 핵심은, 서버가 수락하기로 결정한 요청은 빠르게 지원되기 때문에 가용성을 유지한다는 점입니다.
로드 차단은 서버가 굿풋을 유지하고 제공되는 처리량이 늘어나도 가능한 한 많은 요청을 완료할 수 있습니다. 하지만 로드 차단의 조치는 자유롭지 않으므로, 실제로 서버는 암달의 법칙에 따르게 되며 굿풋이 떨어집니다.

테스트

다른 엔지니어와 로드 차단에 대해 이야기해보면 이들이 차단 지점과 차단 지점 이후까지 서비스에서 로드 테스트를 해본 적이 없는 경우 서비스가 가장 원치 않는 방식으로 실패한다고 가정한다는 것을 지적하고 싶습니다. Amazon에서는 서비스의 로드 테스트에 많은 시간을 할애합니다. 이 글에 앞서 나온 것과 같은 그래프를 생성하면 오버로드 성능의 기준선을 마련하고 서비스를 변경할 때 시간에 따라 저희가 하는 작업을 추적하는 데 도움이 됩니다.

로드 테스트에는 여러 종류가 있습니다. 일부 로드 테스트에서는 로드가 증가하면 플릿이 자동으로 확장되도록 보장하고, 또 어떤 테스트에서는 고정된 플릿 크기를 사용하기도 합니다. 오버로드 테스트에서 처리량이 증가함에 따라 서비스의 가용성이 빠르게 0으로 떨어지면 서비스에 추가 로드 차단 메커니즘이 필요하다는 좋은 증거입니다. 이상적인 로드 테스트 결과는, 서비스가 거의 100%에 가깝게 사용되고 더 많은 처리량이 부가되어도 일정한 상태를 유지하는 굿풋이 안정적인 상태입니다.

Chaos Monkey와 같은 도구를 사용하면 서비스에 대한 복잡한 엔지니어링 테스트를 수행하는 데 도움이 됩니다. 예를 들어, CPU에 과도한 로드나 패킷 손실을 부가하여 오버로드 중에 나타나는 상황을 시뮬레이션할 수 있습니다. 저희가 사용하는 또 다른 테스트 기술은, 기존 로드 생성 테스트나 Canary를 사용하여 테스트 환경에서 로드를 늘리는 대신 일관된 로드를 유도하지만 테스트 환경에서 서버를 제거하기 시작합니다. 그러면 인스턴스당 제공되는 처리량이 늘어나고, 이때 인스턴스 처리량을 테스트할 수 있습니다. 인위적으로 플릿 크기를 줄여 로드를 늘리는 이 기술은 격리 상태의 서비스를 테스트하는 데 유용하지만, 전체 로드 테스트에 대한 완벽한 대체물은 아닙니다. 완전한 포괄적인 로드 테스트는 서비스의 종속된 요소에도 로드를 늘리므로, 다른 병목 현상도 탐지할 수 있습니다.

테스트 중에 서버 측 가용성과 지연 시간 외에도 클라이언트에서 인지하는 가용성과 지연 시간도 측정해야 합니다. 클라이언트 측 가용성이 떨어지기 시작하면 이 지점 이후로도 로드를 계속 부가합니다. 로드 차단이 작동하면 제공된 처리량이 서비스의 확장된 용량을 초과하여 증가해도 굿풋이 안정적인 상태를 유지합니다.

오버로드 테스트는 오버로드를 방지하는 메커니즘을 탐색하기 전의 핵심 요소입니다. 각 메커니즘은 복잡성을 낳습니다. 예를 들어, 이 글의 초반에 언급한 서비스 프레임워크에서 모든 구성 옵션과 올바른 기본값을 구하기 어렵다는 점을 고려해 보십시오. 오버로드를 방지하는 각 메커니즘도 또 다른 보호를 부가하고 효율성을 제한합니다. 테스트를 진행하면서 팀은 시스템 병목 현상을 감지하고 오버로드를 처리하기 위해 필요한 보호 수단의 조합을 결정합니다.

가시성

Amazon에서는 오버로드로부터 서비스를 보호하기 위해 사용하는 기술에 상관없이, 이러한 오버로드 보호 조치가 적용될 때 필요한 지표와 가시성을 신중하게 검토합니다.

부분 정전 보호에서 요청을 거부하면 이러한 거부로 서비스 가용성이 감소합니다. 최대 연결 수가 너무 낮게 설정된 경우와 같이 서비스에 문제가 생겨 용량이 있어도 요청을 거부하면 거짓 긍정이 생성됩니다. 저희는 거짓 긍정 비율을 0으로 유지하려고 노력합니다. 팀에서 정기적으로 거짓 긍정 비율이 0이 아니라는 사실을 알게 되면 서비스를 너무 민감하게 조정한 것이거나 개별 호스트가 지속적으로 타당한 수준에서 오버로드되는 것이고 이는 확장이나 로드 밸런싱 문제를 나타낼 수 있습니다. 이 경우 일부 애플리케이션 성능을 조정하거나 로드 불균형을 보다 점진적으로 처리할 수 있는 더 큰 인스턴스 유형으로 전환할 수 있습니다.

가시성 관점에서 로드 차단이 요청을 거부하면 클라이언트가 누구인지, 어떤 작업을 호출했는지, 그리고 보호 조치를 조정하는 데 유용한 기타 정보를 파악하기 위해 적절한 계측을 수행해야 합니다. 또한 보호 조치에서 상당한 트래픽을 거부하는지 여부를 감지하기 위해 경보도 사용합니다. 부분 정전이 발생한 경우 가장 먼저 할 일은 용량을 추가하고 현재 병목 현상을 해결하는 것입니다.

그리고 로드 차단의 가시성과 관련하여 미묘하지만 중요한 고려 사항이 있습니다. 저희는 서비스 지연 시간 지표를 실패한 요청 지연 시간으로 오염시키지 않는 것이 중요하다는 점을 알게 되었습니다. 어쨌든 요청 로드 차단 지연 시간은 다른 요청에 비해 매우 낮아야 합니다. 예를 들어, 서비스가 트래픽의 60%를 로드 차단하는 경우 성공한 요청의 지연 시간이 매우 길어도 서비스의 평균 지연 시간은 괜찮을 편일 수 있습니다. 빠르게 실패하는 요청의 결과로 수치가 덜 보고되었기 때문입니다.

로드 차단이 자동 조정 및 가용 영역 실패에 미치는 영향

로드 차단을 잘못 구성하면 반응형 자동 조정을 비활성화할 수 있습니다. 다음 예제를 살펴보겠습니다. 서비스가 CPU 기반 반응형 확장에 대해 구성되었고, 유사한 CPU 대상에서 요청을 거부하도록 로드 차단도 구성되었습니다. 이 경우 로드 차단 시스템은 CPU 로드를 낮게 유지하기 위해 요청 수를 줄이고, 반응형 조정은 새 인스턴스를 시작할 지연 신호를 전혀 받거나 가져오지 못합니다.

또한 가용 영역 실패를 처리하기 위한 자동 조정 제한을 설정할 때도 로드 차단 논리를 신중하게 고려합니다. 지연 시간 목표를 유지하면서 가용 영역의 유효 용량을 사용할 수 없는 지점까지 서비스가 확장됩니다. Amazon 팀은 서비스가 용량 제한에 도달하는 정도를 대략적으로 계산하기 위해 종종 CPU와 같은 시스템 지표를 확인합니다. 하지만 로드 차단을 사용하면 플릿은 시스템 지표가 의미하는 것보다 요청이 거부되는 지점에 훨씬 더 가깝게 실행될 수 있고, 가용 영역 실패를 처리하기 위해 초과 용량을 프로비저닝하지 못할 수 있습니다. 로드 차단을 사용하는 경우 언제라도 플릿의 용량과 헤드룸을 파악하기 위해 한층 더 확실히 서비스를 테스트해야 합니다.

실제로 로드 차단을 사용하면 중요하지 않고 사용량이 적은 트래픽을 지정하여 비용을 절감할 수 있습니다. 예를 들어, 플릿이 amazon.com의 웹 사이트 트래픽을 처리하는 경우 전체 가용 영역 중복성을 위해 확장하는 대가로 검색 크롤러 트래픽까지 희생할 필요는 없다고 결정할 수 있습니다. 하지만 이 접근 방식을 사용할 때 매우 신중했습니다. 모든 요청 비용이 같지 않고, 서비스가 동시에 초과 크롤러 트래픽 차단과 사용자 트래픽을 위해 가용 영역 중복성을 제공해야 한다는 점을 증명하려면 신중한 설계와 지속적 테스트 그리고 비즈니스 측의 승인이 필요합니다. 그리고 서비스의 클라이언트가 해당 서비스가 이렇게 구성되었는지 알지 못하는 경우 가용 영역 실패 동안 해당 동작은 중요하지 않은 로드 차단이 아니라, 대규모의 중요한 가용성이 감소한 것처럼 보일 수 있습니다. 이러한 이유로 서비스 중심 아키텍처에서는 전체 스택에서 글로벌 우선순위 결정을 내리는 대신, 클라이언트로부터 초기 요청을 수신하는 서비스와 같이 가능한 한 조기에 이러한 종류의 트래픽 구성을 푸시하려고 합니다.

로드 차단 메커니즘

로드 차단과 예측할 수 없는 시나리오에 대해 논의하는 경우 부분 정전으로 이어지는 많은 예측 가능한 조건에도 집중해야 합니다. Amazon에서 서비스는 용량을 더 추가하지 않고 가용 영역 실패를 처리하기 위해 충분한 초과 용량을 유지합니다. 그리고 조절 기능을 통해 클라이언트 사이에서 공정성을 보장합니다.

하지만 이러한 보호 기능과 운영 사례에도 불구하고, 서비스에는 특정 시점에 일정 용량을 보유하고, 이로 인해 다양한 이유로 오버로드가 발생할 수 있습니다. 그 이유로는, 예상치 못한 트래픽 급증, 잘못된 배포 등으로 인한 갑작스러운 플릿 용량 손실, 캐시된 읽기와 같이 저렴한 요청에서 캐시 누락 또는 쓰기와 같은 높은 비용의 요청으로 전환하는 클라이언트가 있습니다. 서비스에서 오버로드가 발생하면 여기서 맡은 요청을 완료해야 합니다. 즉, 서비스는 부분 정전으로부터 자신을 보호해야 합니다. 이 글의 나머지 부분에서는 수년 간 오버로드를 관리하기 위해 사용해왔던 기술과 몇 가지 고려 사항에 대해 설명합니다.

요청 삭제 비용 이해

저희는 굿풋이 안정화되는 지점 이후에도 서비스의 로드 테스트를 수행합니다. 이 접근 방식의 한 가지 중요한 이유는 로드 차단 중에 요청을 삭제할 때 요청 삭제 비용을 최대한 낮게 유지하기 위함입니다. 소켓 설정이나 우연한 로그 문은 놓치기 쉬워서, 이로 인해 필요한 수준보다 훨씬 더 높은 비용으로 요청을 삭제할 수 있는 상황을 목격했습니다.

드물지만, 요청을 빠르게 삭제해도 요청을 보유했을 때보다 비용이 더 높을 수도 있습니다. 이 경우 거부되는 요청 속도를 느리게 하여 성공한 응답의 지연 시간에 최소한의 수준으로 맞춥니다. 하지만 요청을 보유하는 비용이 최대한 낮을 때, 가령 요청이 애플리케이션 스레드를 묶어두지 않을 때 이를 수행해야 합니다.

요청의 우선순위 지정

서버에서 오버로드가 발생하면 수락할 요청과 거부할 요청을 결정하기 위해 수신 요청을 분류할 수 있습니다. 서버가 수신하는 가장 중요한 요청은 로드 밸런서의 Ping 요청입니다. 서버가 제때 Ping 요청에 응답하지 않으면 로드 밸런서는 일정 기간 해당 서버로 새 요청을 전송하는 작업을 중지하고, 서버는 유휴 상태가 됩니다. 그리고 부분 정전 시나리오에서 최악의 상황은 플릿 크기를 줄이는 것입니다. Ping 요청 외에도 요청 우선순위를 지정하는 옵션은 서비스마다 다릅니다.

amazon.com을 렌더링하기 위해 데이터를 제공하는 웹 서비스를 고려해봅니다. 검색 인덱스 크롤러에 대한 웹 페이지 렌더링을 지원하는 서비스 호출은 사람이 생성하는 요청보다 덜 중요할 수 있습니다. 크롤러 요청도 중요하지만, 사용량이 낮은 시간으로 전환하는 것이 바람직할 수 있습니다. 하지만 amazon.com과 같이 많은 서비스가 함께 작동하는 복잡한 환경에서 서비스가 경험에 따른 서로 충돌하는 우선순위를 사용하면 시스템 전반의 가용성이 영향을 받고 작업이 낭비될 수 있습니다.

우선순위와 조절 기능은 엄격한 조절 제한을 방지하면서 서비스를 오버로드로부터 보호하기 위해 함께 사용할 수 있습니다. Amazon에서는 클라이언트가 구성된 조절 제한을 초과하여 버스트하도록 허용한 경우 이러한 클라이언트의 초과 요청 우선순위를 다른 클라이언트의 할당량 범위 내 요청보다 낮게 지정할 수 있습니다. 버스트 용량을 사용할 수 없게 될 가능성을 최소화하기 위해 배치 알고리즘에 많은 시간을 투자하고 있지만, 단점도 고려하여 예측할 수 없는 워크로드보다 예측 가능한 프로비저닝된 워크로드를 선호합니다.

지속적 클럭 확인

서비스가 요청을 어느 정도 진행했지만 클라이언트에서 제한 시간을 초과했다는 알림을 받으면 작업의 남은 부분을 건너뛰고 해당 지점에서 요청에 실패할 수 있습니다. 그렇지 않고, 서버는 요청을 계속 작업하고, 늦게 회신을 보내면 그건 아무도 없는 숲에서 나무 쓰러지는 소리와도 같습니다. 서버 관점에서 보면 성공적인 응답을 반환한 것입니다. 하지만 제한 시간을 초과한 클라이언트 관점에서 보면 오류입니다.

낭비되는 작업을 피하기 위한 한 가지 방법은, 클라이언트에서 각 요청에 제한 시간 힌트를 포함하여 클라이언트가 기다리는 시간을 서버에 알려주는 것입니다. 서버는 이러한 힌트를 평가하고 적은 비용으로 삭제 예정된 요청을 삭제할 수 있습니다.

이러한 제한 시간 힌트는 기간이나 절대 시간으로 표현할 수 있습니다. 하지만 분산 시스템의 서버는 정확한 현재 시간에 합의하는 데 매우 비협조적입니다. Amazon Time Sync Service는 Amazon Elastic Compute Cloud(Amazon EC2) 인스턴스의 클럭과 각 AWS 리전에서 위성으로 제어되는 중복된 원자 시계 플릿을 동기화하여 이를 보완합니다. 잘 동기화된 클럭은 Amazon에서 로깅 용도로도 매우 중요합니다. 클럭이 동기화되지 않은 서버에서 두 개 로그 파일을 비교하면 처음보다 문제를 해결하기 더 어렵습니다.

"클럭을 확인"하는 또 다른 방법은 단일 머신의 지속 시간을 측정하는 것입니다. 서버는 다른 서버와의 합의를 도출할 필요가 없기 때문에 로컬로 경과한 지속 기간을 측정하면 됩니다. 하지만 지속 기간으로 제한 시간을 표현하는 데에도 여러 문제가 있습니다. 한 가지 문제는, 사용하는 타이머가 단조로워야 하고, 서버가 NTP(Network Time Protocol)와 동기화할 때 역행하지 않아야 한다는 점입니다. 훨씬 더 어려운 문제는 지속 시간을 측정하기 위해 서버가 스톱워치를 시작할 때를 알아야 한다는 점입니다. 극단적인 일부 오버로드 시나리오에서 많은 요청이 TCP(Transmission Control Protocol) 버퍼 대기열에 쌓일 수 있고 서버가 해당 버퍼에서 요청을 읽을 때가 되면 클라이언트에서 이미 제한 시간을 초과합니다.

Amazon의 시스템에서 클라이언트 제한 시간 힌트를 표현할 때마다 이를 외부의 요인으로 적용하려고 합니다. 서비스 중심 아키텍처에 여러 홉이 포함된 경우 각 홉 사이에 "남은 시간" 마감을 전달하여, 이를 통해 호출 체인의 끝에서 다운스트림 서비스가 응답의 유용성을 보장하도록 남은 시간을 인식할 수 있습니다.

서버가 클라이언트 마감을 인식하면 서비스 구현에서 마감을 시행할 위치를 정해야 합니다. 서비스에 요청 대기열이 있으면 이를 사용하여 각 요청을 대기열에서 내보낸 후 제한 시간을 평가합니다. 하지만 이 작업은 요청에 걸리는 시간을 알지 못하기 때문에 여전히 매우 까다롭습니다. 일부 시스템은 API 요청에 걸리는 대략적인 시간을 보유하고 클라이언트에서 보고한 마감이 예상 지연 시간을 초과하면 조기에 요청을 삭제합니다. 하지만 이렇게 단순한 상황은 극히 드뭅니다. 예를 들어, 캐시 히트는 캐시 누락보다 더 빠르고, 예측 도구는 사전에 히트인지 누락인지를 알지 못합니다. 아니면 서비스 백엔드 리소스가 파티셔닝되었을 수 있고, 일부 파티션만 느려질 수도 있습니다. 기교를 부릴 곳은 많지만, 오히려 이러한 기교는 예측할 수 없는 상황에서 역효과를 낼 수도 있습니다.

경험상 서버에서 클라이언트 제한 시간을 시행하는 작업은 복잡하고 단점이 있긴 해도 다른 대안보다는 여전히 훨씬 더 낫습니다. 요청을 누적시키며 서버가 누구에게도 중요하지 않은 요청을 작업하는 대신, "요청당 수명"을 시행하고 삭제 예정인 요청을 제거하는 편이 더 유용하다는 사실을 깨달았습니다.

시작한 작업의 마무리

특히 오버로드 상황에서는 유용한 작업을 낭비하고 싶지 않습니다. 작업을 버리면 오버로드를 증가시키는 긍정적 피드백 루프가 생성됩니다. 서버가 제시간에 응답하지 않은 경우 클라이언트가 종종 요청을 재시도하기 때문입니다. 이 상황에서 리소스를 소비하는 요청 하나가 리소스를 소비하는 여러 요청으로 전환되고, 이로 인해 서비스에 로드가 기하급수적으로 가중됩니다. 클라이언트에서 제한 시간을 초과하고 재시도하면 첫 번째 연결에서는 더 이상 회신을 기다리지 않지만, 별도의 연결에서 새 요청을 수행하곤 합니다. 서버가 첫 번째 요청을 마치고 회신해도 클라이언트는 더 이상 이 요청 회신을 기다리지 않을 수도 있습니다. 이제 재시도한 요청의 회신을 기다리고 있기 때문입니다.

낭비되는 작업의 문제는 제한된 작업을 수행하도록 서비스를 설계하는 이유이기도 합니다. 대형 데이터 세트(또는 실제로 목록)를 반환할 수 있는 API를 공개하는 경우 페이지 지정을 지원하는 API로 공개합니다. 이러한 API는 부분 결과와 클라이언트가 추가 데이터를 요청하는 데 사용할 수 있는 토큰을 반환합니다. 서버가 메모리, CPU 및 네트워크 대역폭에 상한을 둔 요청을 처리하면 서비스에 부가된 추가 로드를 더 쉽게 예측할 수 있다는 점을 확인했습니다. 서버가 요청을 처리하는 데 얼마가 걸리는지 모르는 상태에서 승인 제어를 수행하기란 매우 어렵습니다.

요청 우선순위를 지정하는 조금 더 미묘한 기능은 클라이언트가 서비스 API를 사용하는 방식과 관련이 있습니다. 예를 들어, 서비스에 start()end()와 같은 두 개의 API가 있다고 가정합니다. 클라이언트가 작업을 마치려면 두 API를 호출할 수 있어야 합니다. 이 경우 서비스는 start() 요청보다 end() 요청 우선순위를 더 높게 지정해야 합니다. start()를 우선하면 클라이언트가 시작한 작업을 완료할 수 없고, 이로 인해 부분 정전이 발생합니다.

페이지 지정을 통해서도 낭비되는 작업을 확인할 수 있습니다. 클라이언트가 서비스에서 결과를 페이지로 지정하기 위해 여러 순차 요청을 수행해야 하는 경우 N-1페이지 이후 실패를 확인하고 결과를 버려야 하면 N-2개 서비스 호출과 그 과정에서 수행한 모든 재시도를 낭비하는 셈입니다. 이 작업은 end() 요청과 마찬가지로, 첫 번째 페이지 요청을 후속 페이지 지정 요청보다 우선순위로 지정합니다. 또한 이러한 특성 때문에 동기 작업 중에 호출하는 서비스를 끝없이 페이지 지정하지 않고, 제한된 작업을 수행하도록 서비스를 설계하는 것입니다.

대기열 확인

또한 내부 대기열을 관리할 때 요청 지속 시간을 확인하는 데 유용합니다. 많은 최신 서비스 아키텍처는 인메모리 대기열을 사용하여 스레드 풀에 연결해 다양한 작업 스테이지 동안 요청을 처리합니다. 실행기를 포함하는 웹 서비스 프레임워크는 전방에 대기열을 구성했을 수 있습니다. TCP 기반 서비스를 사용하는 경우 운영 체제는 각 소켓에 대한 버퍼를 유지하고, 해당 버퍼는 억제된 요청을 많이 포함할 수 있습니다.

대기열에서 작업을 빼올 때 작업이 대기열에 있던 기간을 검사하는 기능을 사용합니다. 적어도 서비스 지표에서 이 지속 시간을 기록하려고 합니다. 대기열 크기에 제한을 두는 방식 외에도, 대기열에 수신 요청을 보관하는 기간에 상한을 두는 것도 매우 중요하며 기간이 너무 오래된 경우 이를 삭제하였습니다. 그러면 서버가 성공 가능성이 높은 최신 요청을 작업할 리소스를 확보할 수 있습니다. 이 접근 방식을 극단적으로 고려했을 때, 프로토콜이 지원하는 경우 LIFO(후입선출) 대기열을 사용하는 방법을 살펴보았습니다. (해당 TCP 연결에서 요청의 HTTP/1.1 파이프라인 연결은 LIFO 대기열을 지원하지 않지만, 보통 HTTP/2에서는 지원합니다.)

또한 로드 밸런서는 급증 대기열이라고 하는 기능을 사용하여 서비스에서 오버로드가 발생하면 수신 요청이나 연결을 대기열에 넣을 수도 있습니다. 이러한 대기열은 부분 정전으로 이어질 수 있습니다. 최종적으로 서버가 요청을 가져올 때 요청이 대기열에 있던 기간을 알 수 없기 때문입니다. 일반적으로 기본적인 안전책은 초과 요청을 대기열에 넣는 대신 빠르게 실패를 유도하는 스필오버 구성을 사용하는 것입니다. Amazon에서 이 방식은 차세대 Elastic Load Balancing(ELB) 서비스로 구현되었습니다. Classic Load Balancer는 급증 대기열을 사용하지만, Application Load Balancer는 초과 트래픽을 거부합니다. 구성에 상관없이 Amazon 팀은 서비스에 대한 급증 대기열 깊이나 스필오버 수치와 같은 관련 로드 밸런서 지표를 모니터링합니다.

경험에 비추어 보면 대기열을 확인하는 중요성은 몇 번을 강조해도 지나치지 않습니다. 제가 의존하고 있는 시스템과 라이브러리에서 인메모리 대기열을 찾으면 종종 놀라곤 합니다. 사실 여기에 있을 거라고 생각하지 않았으니까요. 시스템을 자세히 살펴볼 때 아직도 제가 모르는 어딘가에 대기열이 있다고 가정하면 꽤 도움이 됩니다. 물론, 오버로드 테스트는 올바른 현실적인 테스크 사례를 진행하는 한, 코드를 자세히 분석하는 것보다 더 유용한 정보를 제공합니다.

하위 계층에서 오버로드에 대해 보호

서비스는 로드 밸런서에서 netfilteriptables 기능을 사용하는 운영 체제, 그리고 서비스 프레임워크, 코드에 이르기까지 여러 계층으로 구성되며, 각 계층에서는 서비스를 보호하기 위한 기능을 제공합니다.

NGINX와 같은 HTTP 프록시는 최대 연결 기능(max_conns)을 제공하여 백엔드 서버로 전달되는 활성 요청 또는 연결 수를 제한합니다. 이 기능은 유용한 메커니즘이 될 수 있지만, 이 방법은 기본 보호 옵션 대신 최후의 방법으로 사용해야 한다는 점을 배웠습니다. 프록시를 통해 중요한 트래픽의 우선순위를 정하기는 쉽지 않고, 때론 이동 중인 원시 요청 개수 추적을 사용하면 실제로 서비스에서 오버로드가 발생했는지 여부에 대한 정확하지 않은 정보를 제공합니다.

이 글의 초반에서 서비스 프레임워크 팀에서 근무하던 당시 직면한 과제를 이야기한 바 있습니다. 로드 밸런서에서 구성할 최대 연결에 대한 권장 기본값을 Amazon 팀에 제공하려고 노력했었지요. 결국 저희는 로드 밸런서와 프록시에 대한 최대 연결을 높게 설정하고 서버에서 로컬 정보로 보다 정확한 로드 차단 알고리즘을 구현하도록 제안했습니다. 그러나 최대 연결 값이 서버의 리스너 스레드, 리스너 프로세스 또는 파일 설명자를 초과하지 않는 것이 중요합니다. 그래야 서버는 로드 밸런서에서 중요한 상태 확인 요청을 처리할 리소스를 확보할 수 있습니다.

서버 리소스 사용을 제한하는 운영 체제 기능은 강력하고 긴급 상황에서 사용하기에 유용합니다. 그리고 오버로드가 발생할 수 있다는 점을 알고 잇기 때문에 특정 명령을 포함한 올바른 런북을 사용하여 미리 준비할 수 있습니다. iptables 유틸리티는 서버가 수락하는 연결 수에 상한을 둘 수 있으며, 서버 프로세스보다 훨씬 더 낮은 비용으로 초과 연결을 거부할 수 있습니다. 또한 제한된 비율로 새 연결을 허용하는 기능이나 소스 IP 주소당 제한된 연결 비율이나 개수를 허용하는 기능처럼 보다 정교한 제어로 구성할 수 있습니다. 소스 IP 필터는 강력하지만 기존 로드 밸런서에는 적용되지 않습니다. 하지만 ELB Network Load Balancer는 네트워크 가상화를 사용하는 운영 체제 계층에서도 호출자의 소스 IP를 유지하여 소스 IP 필터와 같은 iptables 규칙이 예상대로 작동하도록 지원합니다.

계층에서 보호

서버에서 리소스가 부족하여 속도를 늦추지 않고 요청을 거부하는 경우가 생기기도 합니다. 이 점을 염두에 두고 서버와 클라이언트 사이의 모든 홉을 확인하여 이를 조정하고 초과 로드를 차단하는 방법을 검토합니다. 예를 들어, 여러 AWS 서비스에는 기본적으로 로드 차단 옵션이 포함되어 있습니다. Amazon API Gateway에서 서비스를 전면에 배치하면 API가 수락하는 최대 요청 비율을 구성할 수 있습니다. API Gateway, Application Load Balancer 또는 Amazon CloudFront에서 AWS 서비스를 전면에 배치하면 차원 수에서 초과 트래픽을 차단하도록 AWS WAF를 구성할 수 있습니다.

가시성도 골치 아픈 문제입니다. 조기 거부는 초과 트래픽을 삭제하는 가장 저렴한 위치이기 때문에 중요하지만 가시성을 희생해야 합니다. 이것이 계층에서 보호하는 이유입니다. 계층은 서버에서 작업 가능한 수준보다 더 많은 작업을 수행하게 하고, 초과분을 삭제하며, 삭제하는 트래픽에 대한 충분한 정보를 로깅합니다. 서버가 삭제할 수 있는 트래픽은 제한적이므로, 이 전면의 계층에 의존하여 상당히 큰 트래픽으로부터 서버를 보호할 수 있습니다.

오버로드에 대한 또 다른 시각

이 글에서는 너무 많은 동시 작업이 부가된 경우 리소스 제한 및 경합과 같은 요소가 발생하기 때문에 시스템 속도가 느려진다는 사실을 기반으로 로드 차단의 필요성이 발생하는 과정을 설명했습니다. 오버로드 피드백 루프는 지연 시간에 의해 유도되며, 궁극적으로 작업 낭비, 요청 비율의 증가, 심지어 오버로드까지 발생시킵니다. USL(Universal Scalability Law) 및 암달의 법칙으로 발생하는 이러한 요인은 초과 로드 차단 및 오버로드 발생 시 예측 가능한 일관된 성능 유지를 통해 방지해야 합니다. 예측 가능한 일관된 성능에 초점을 맞추는 것이 Amazon에서 서비스를 구축하는 핵심적인 설계 원칙입니다.

예를 들어, Amazon DynamoDB는 예측 가능한 성능과 가용성을 대규모로 제공하는 데이터베이스 서비스입니다. 워크로드가 빠르게 급증하고 프로비저닝된 리소스를 초과하더라도 DynamoDB는 해당 워크로드에 대해 예측 가능한 굿풋 지연 시간을 유지합니다. DynamoDB 자동 조정, 적응형 용량온디맨드와 같은 요소는 빠르게 대처하여 워크로드의 증가에 따라 적절하게 굿풋 비율을 늘립니다. 이 진행 과정에서 굿풋은 안정적인 상태를 유지하며, DynamoDB 위의 계층에서 서비스를 예측 가능한 성능으로 유지하고 전체 시스템의 안정성을 향상시킵니다.

AWS Lambda에서는 예측 가능한 성능에 초점을 맞춘 보다 포괄적인 예제를 제시합니다. Lambda를 사용하여 서비스를 구현할 때 각 API 호출은 일정한 컴퓨팅 리소스가 할당된 자체 실행 환경에서 실행되고, 해당 실행 환경은 한 번에 하나의 요청만 처리합니다. 지정된 서버가 여러 API에서 작업한다는 점에서 서버 기반 패러다임과는 차이가 납니다.

독립된 자체 리소스(컴퓨팅, 메모리, 디스크, 네트워크)에 대한 각 API 호출을 격리하면 어느 정도 암달의 법칙을 우회할 수 있습니다. 한 API 호출의 리소스가 다른 API 호출의 리소스와 경합하지 않기 때문입니다. 따라서 처리량이 굿풋을 초과하면 굿풋은 기존 서버 기반 환경에서와 같이 떨어지는 대신 일정한 상태를 유지합니다. 하지만 종속된 요소가 느려질 수 있고, 이로 인해 동시성이 증가할 수 있으므로 만병통치약은 아닙니다. 하지만 이 시나리오에서는 이 글에서 논의했던 호스트상의 리소스 경합 유형은 해당되지 않습니다.

이 리소스 격리는 조금 미묘하지만 AWS Fargate, Amazon Elastic Container Service(Amazon ECS) 및 AWS Lambda와 같은 최신 서버리스 컴퓨팅 환경의 중요한 장점입니다. Amazon에서 저희는 스레드 풀 조정에서 최대 로드 밸런서 연결을 위한 완벽한 구성 선택에 이르기까지 로드 차단을 구현하기 위해 많은 작업을 했습니다. 이러한 구성 유형에 대해 합리적인 기본값을 찾기란 정말 어렵습니다. 불가능에 가까울 수도 있습니다. 각 시스템의 고유한 운영 특성에 의존하기 때문입니다. 이러한 최신 서버리스 컴퓨팅 환경에서는 더 낮은 수준의 리소스 격리 기능을 제공하며, 오버로드로부터 보호하기 위해 조절 및 동시성 제어와 같은 더 높은 수준의 제어 기능을 소개합니다. 어떤 면에서 보면, 완벽한 기본 구성 값을 찾으려는 대신 이러한 구성을 함께 우회하고, 아무런 구성 작업 없이 오버로드 범주로부터 보호하는 방법을 제공할 수 있습니다.

추가 자료

USL(Universal Scalability Law)
암달의 법칙
SEDA(Staged Event-Driven Architecture)
Little's law(시스템의 동시성과 분산 시스템의 용량을 확인하는 방법을 설명함)
Telling Stories About Little’s Law, Marc의 블로그
Elastic Load Balancing Deep Dive and Best Practices, re:Invent 2016의 프레젠테이션(초과 요청을 더 이상 대기열에 넣지 않는 Elastic Load Balancing의 진화에 대해 설명함)
• Burgess, Thinking in Promises: Designing Systems for Cooperation, O’Reilly Media, 2015



저자에 대하여

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

시간 제한, 재시도 및 지터를 사용한 백오프 상태 확인 구현 운영 가시성을 위한 분산 시스템 계측