장애 발생

하나의 서비스 또는 시스템이 서로 호출할 때마다 장애가 발생할 수 있습니다. 이러한 장애는 다양한 요인에서 비롯됩니다. 서버, 네트워크, 로드 밸런서, 소프트웨어, 운영 체제 또는 시스템 운영자의 실수까지 다양한 것들이 포함됩니다. 장애 가능성이 낮은 시스템을 설계하기는 했지만 결코 실패하지 않는 시스템을 구축하는 것은 불가능합니다. 따라서 아마존에서는 시스템이 장애 가능성을 줄이고 실패를 허용할 수 있는 시스템을 설계하여 작은 규모의 장애가 전면적인 가동 중단으로 확대되는 것을 방지하고 있습니다. 복원력이 우수한 시스템을 구축하려면 3가지 핵심 도구인 시간 제한, 재시도, 백오프를 적용해야 합니다.

많은 장애 유형은 요청이 평소보다 오래 걸리고 완료되지 않을 때 명확하게 확인됩니다. 요청이 완료될 때까지 클라이언트가 평소보다 오래 기다려야 할 경우 클라이언트는 요청을 위한 리소스를 오랜 시간 붙잡고 있어야 합니다. 많은 요청이 리소스에서 오래 대기 중인 경우 서버에서는 리소스 부족 현상이 나타날 수 있습니다. 이러한 리소스에는 메모리, 스레드, 연결, 임시 포트 또는 사용이 제한적인 다른 항목이 포함될 수 있습니다. 이 문제를 해결하기 위해 클라이언트는 시간 제한을 설정합니다. 시간 제한은 클라이언트가 요청이 완료될 때까지 대기하는 최대 시간입니다.

때로 동일한 요청을 재시도하면 요청이 성공하기도 합니다. 이것은 구축된 시스템 유형이 단일한 단위로 장애가 발생하는 경우가 많지 않기 때문에 그렇습니다. 오히려 부분적이거나 일시적인 장애가 발생하기 쉽습니다. 부분적인 장애는 일정 비율의 요청이 성공하는 것을 말합니다. 일시적인 장애는 잠시 동안 요청이 실패하는 것을 말합니다. 재시도는 클라이언트가 동일한 요청을 다시 전송하여 이러한 부분적인 장애와 단기적인 일시적 장애에서 살아남을 수 있도록 합니다.

재시도가 항상 안전한 것은 아닙니다. 시스템이 오버로드에 가까워져 이미 장애가 발생한 경우 재시도로 인해 호출 중인 시스템의 로드가 증가할 수 있습니다. 이 문제를 방지하기 위해 아마존에서는 백오프를 사용하도록 클라이언트를 구축합니다. 이렇게 하면 후속 재시도 간의 대기 시간이 증가하여 백엔드에서 부하가 일정하게 유지됩니다. 재시도의 다른 문제점은 일부 원격 호출에 부작용이 수반된다는 것입니다. 장애 또는 시간 제한이 반드시 부작용이 발생하지 않았음을 의미하지는 않습니다. 부작용이 여러 번 나타나는 것이 바람직하지 않은 경우 가장 좋은 방법은 API를 멱등적으로 설계하여 안전하게 재시도할 수 있도록 하는 것입니다.

결국에는 트래픽이 일정한 속도로 아마존 서비스에 도달하지 않게 됩니다. 대신 요청의 도착률이 빈번하면 대량의 버스트가 발생합니다. 이러한 버스트는 클라이언트 동작, 장애 복구 및 주기적인 cron 작업과 같은 단순한 작업으로 인해 발생할 수 있습니다. 부하로 인해 오류가 발생한 경우에는 모든 클라이언트가 동시에 재시도하므로 재시도가 비효율적일 수 있습니다. 이 문제를 방지하기 위해 우리는 지터를 적용합니다. 이것은 임의로 지정한 시간으로, 이 시간이 지나면 도착률을 분산시켜 대량의 버스트를 방지하도록 요청을 작성하거나 재시도하게 됩니다.

이러한 해결 방법은 다음 단원에서 상세하게 논의합니다.

시간 제한

아마존에서 적용하는 모범 사례는 모든 원격 호출과, 동일한 시스템인 경우에도 프로세스 간의 모든 호출에 일반적으로 시간 제한을 설정하는 것입니다. 여기에는 연결 시간 제한과 요청 시간 제한이 모두 포함됩니다. 많은 표준 클라이언트는 기본적으로 견고한 시간 제한 기능을 제공합니다.
일반적으로 가장 까다로운 문제는 설정할 시간 제한 값을 선택하는 것입니다. 시간 제한을 너무 높게 설정하면 클라이언트가 대기하는 제한 시간 동안에도 리소스가 계속 사용되기 때문에 유용성이 떨어집니다. 시간 제한을 너무 낮게 설정하면 두 가지 위험이 대두됩니다.
 
• 요청의 재시도가 너무 많아져 백엔드의 트래픽과 대기 시간이 증가합니다.
• 모든 요청의 재시도가 시작되기 때문에 소규모 백엔드 지연 시간 증가가 완전한 가동 중단으로 이어집니다.
 
AWS 리전 내의 호출 시간 제한을 적절하게 선택하기 위한 모범 사례는 다운스트림 서비스의 대기 시간 지표에서 시작하는 것입니다. 아마존에서 우리는 하나의 서비스가 다른 서비스를 호출할 때 허용 가능한 잘못된 시간 제한 비율을 선택하고 있습니다(예: 0.1%). 그런 다음 해당하는 대기 시간 비중을 다운스트림 서비스에서 관찰합니다(이 예에서는 p99.9). 이 방식은 대부분의 경우 잘 작동하지만 다음과 같은 몇 가지 함정이 있습니다.
 
• 이 방식은 인터넷과 같이 클라이언트가 상당한 네트워크 대기 시간을 갖는 경우에는 적합하지 않습니다. 이러한 경우 클라이언트가 전 세계에 걸쳐 있을 수 있음을 염두에 두고 합리적인 수준으로 최악의 네트워크 대기 시간을 고려해야 합니다.
• 이 방식은 대기 시간 한계가 짧은 서비스(p99.9가 p50과 가까움)에는 적절하지 않습니다. 이러한 경우 채우기 값을 추가하면 시간 제한 초과 횟수를 증가시키는 짧은 대기 시간의 문제를 방지할 수 있습니다.
• 시간 제한 값을 구현할 때 우리는 일반적인 함정에 직면했습니다. Linux의 SO_RCVTIMEO는 강력하기는 하지만 엔드 투 엔드 소켓 시간 제한으로는 적합하지 않은 단점이 있습니다. Java와 같은 일부 언어에서는 이러한 제어를 직접적으로 노출합니다. Go와 같은 다른 언어에서는 보다 강력한 시간 제한 메커니즘을 제공합니다.
• 시간 제한이 DNS 또는 TLS 핸드셰이크 같은 모든 원격 호출에 적용되지 않는 구현도 있습니다. 일반적으로는 잘 테스트된 클라이언트에 구축된 시간 제한을 사용하는 것이 좋습니다. 자체적인 시간 제한을 구현할 경우 시간 제한 소켓 옵션의 정확한 의미와 어떤 작업이 수행되고 있는지에 대해 보다 주의를 기울여야 합니다.
 
아마존에서 작업한 시스템 하나에서, 배포 직후에 종속성과 관련한 소수의 시간 제한 초과 문제가 나타났습니다. 당시, 시간 제한이 20밀리초 정도로 너무 낮게 설정되었습니다. 이 시간 제한 값이 낮기는 했지만 배포 시점 외에 정기적으로 시간 제한 초과 문제가 발생하지는 않았습니다. 이 문제를 살펴보면서 타이머에 새로운 보안 연결 설정이 포함되어 있으며 후속 요청에서 재사용되었다는 것을 발견했습니다. 연결 설정에 20밀리초 이상 걸리기 때문에 배포 후에 새로운 서버의 서비스가 시작될 때도 적은 수의 요청 시간 초과가 확인되었습니다. 일부 경우에는 요청 재시도가 성공했습니다. 처음에는 연결이 설정된 경우 시간 제한 값을 늘려 이 문제를 해결했습니다. 나중에서는, 프로세스가 시작된 후 트래픽을 수신하기 전에 이러한 연결을 설정하여 시스템을 개선했습니다. 시간 제한 초과 관련 문제가 함께 발생했습니다.

재시도와 백오프

재시도는 “이기적”입니다. 클라이언트는 재시도할 때 더 큰 성공 기회를 얻기 위해 서버 시간을 더 많이 소비한다는 의미입니다. 장애가 드물게 발생하거나 일시적인 경우에는 문제가 되지 않습니다. 재시도 요청 수가 전체적으로 많지 않거나 증가하는 겉보기 가용성의 절충이 잘 이루어지고 있기 때문입니다. 오버로드로 인해 장애가 발생한 경우에는 부하를 증가시키는 재시도가 문제를 크게 악화시킬 수 있습니다. 원래 문제가 해결된 후 높은 부하가 오래 유지됨에 따라 복구가 지연될 수도 있습니다. 재시도는 강한 약물과도 같습니다. 적정량을 쓰면 유용하지만 과량을 섭취하는 경우 치명적인 손상을 입을 수도 있습니다. 불행히도 분산 시스템에서는 재시도 횟수를 적정량으로 유지하기 위해 전체 클라이언트 간에 조정할 수 있는 방법이 거의 없습니다.

아마존에서 선호하는 방식은 백오프입니다. 적극적이면서도 즉각적으로 재시도하는 대신 클라이언트는 재시도 간에 일정한 수준의 대기 시간을 유지합니다. 가장 일반적인 패턴은 지수 백오프로, 매 시도 후에 대기 시간이 기하급수적으로 증가합니다. 지수 백오프는 지수 기능이 빠르게 성장하기 때문에 매우 긴 백오프 시간으로 이어질 수 있습니다. 너무 오래 재시도하지 않도록 하기 위해서는 구현 시에 백오프를 제한할 최대값을 설정합니다. 이를 제한된 지수 백오프라고 합니다. 그러나 이로 인해 또 다른 문제가 생길 수 있습니다. 이제 모든 클라이언트가 제한 속도로 지속적으로 재시도하고 있습니다. 거의 대부분 우리가 채택하는 해결 방법은 클라이언트 재시도 횟수를 제한하고 서비스 지향 아키텍처에서 보다 빠른 시점에 장애 문제를 해결하는 것입니다. 대부분의 경우 클라이언트는 자체 시간 제한 값이 있기 때문에 호출을 포기합니다.

재시도와 관련한 다른 문제도 있습니다.

• 분산 시스템에는 여러 층의 레이어가 있는 경우가 많습니다. 고객의 호출로 인해 5개 스택의 서비스 호출이 발생한 경우를 생각해 보십시오. 이 호출은 데이터베이스에 대한 쿼리로 종료되며 각 레이어에서 3번의 재시도가 있게 됩니다. 부하가 있을 때 데이터베이스의 쿼리가 실패하기 시작하면 어떤 일이 발생할까요? 각 레이어는 독립적으로 재시도하며 데이터베이스에 대한 부하가 243배 증가하여 복구 가능성이 거의 없어집니다. 각 레이어의 재시도는 배수로 증가하기 때문입니다. 즉 처음에는 3회, 두 번째는 9회와 같은 방식으로 증가합니다. 반면에 스택의 최상단 레이어에서 재시도할 경우에는 이전 호출에서 작업이 낭비되어 효율성이 감소할 수 있습니다. 일반적으로 저비용 제어 영역과 데이터 영역 작업을 위한 모범 사례는 스택의 단일 지점에서 재시도하는 것입니다.
• 부하. 단일 레이어 재시도의 경우에도 오류가 시작될 때 트래픽이 대폭 증가합니다. 오류 임계값을 초과할 때 다운스트림 서비스에 대한 호출이 완전히 멈추는 회로 차단기가 이 문제의 해결을 위해 널리 권장됩니다. 불행히도 회로 차단기는 테스트가 어려울 수 있는 모달 동작을 시스템에 도입하므로 복구 시간이 크게 증가할 수 있습니다. 우리는 토큰 버킷을 사용하여 로컬에서 재시도를 제한함으로써 이 위험을 완화할 수 있다는 것을 알게 되었습니다. 이 경우 토큰이 있는 한 모든 호출을 재시도한 다음 토큰이 소진되면 고정 속도로 재시도할 수 있습니다. AWS는 2016년에 AWS SDK에 이러한 동작을 추가했습니다. 따라서 SDK를 사용하는 고객에게는 이러한 스로틀링 동작이 기본 제공됩니다.
• 재시도 시점 결정. 일반적으로 우리는 부작용을 수반하는 API가 멱등성을 제공하지 않는 한 안전하지 않다고 여기고 있습니다. 이것은 재시도 빈도와 상관없이 부작용이 한번만 발생하게 합니다. 읽기 전용 API는 대개 멱등적이지만 리소스 생성 API는 그렇지 않을 수 있습니다. Amazon Elastic Compute Cloud(Amazon EC2) RunInstances API와 같은 일부 API는 명시적인 토큰 기반 메커니즘을 통해 멱등성을 제공하고 안전한 재시도를 지원합니다. 중복된 부작용을 방지하기 위해서는 바람직한 API 설계와 함께 클라이언트 구현 시 여러 가지 사항을 주의해야 합니다.
• 어떤 장애 발생 시 재시도할 가치가 있는지 파악하기. HTTP는 클라이언트서버 오류 간에 분명한 구분을 제공합니다. 클라이언트 오류는 나중에도 성공하지 못할 것이기 때문에 동일한 요청으로 재시도해서는 안 되지만, 서버 오류는 후속 시도에서 성공할 수 있습니다. 불행히도 시스템의 최종 일관성은 이 경계를 모호하게 만듭니다. 한 순간의 클라이언트 오류는 상태가 전파될 경우 다음 순간에 성공으로 바뀔 수 있습니다.

이러한 위험과 과제에도 불구하고 재시도는 일시적이면서도 무작위로 발생하는 오류에 대응하여 고가용성을 제공하기 위한 강력한 메커니즘입니다. 각 서비스에 대한 올바른 절충점을 찾기 위한 바른 판단이 필요합니다. 우리의 경험상, 재시도가 이기적이라는 것을 명심하는 것에서부터 시작해야 합니다. 재시도는 클라이언트가 요청이 중요하다고 선언하고 서비스가 더 많은 리소스를 사용하여 요청을 처리하도록 요구하는 방식입니다. 클라이언트가 너무 이기적인 경우에는 넓은 범위에서 문제가 발생할 수 있습니다.

지터

오버로드 또는 경합으로 인해 장애가 발생한 경우 백오프는 대개 보이는 것만큼 큰 도움이 되지 않습니다. 상관관계 때문입니다. 모든 실패한 호출이 동시에 백오프한 경우 재시도할 때 오버로드 또는 경합을 다시 야기할 수 있습니다. 이에 대해 우리가 제시하는 해결 방법은 지터입니다. 지터는 백오프에 일정 수준의 임의성을 추가하여 재시도가 시간을 두고 분산되게 합니다. 추가할 지터의 양과 바람직한 추가 방법에 대한 자세한 내용은 지수 백오프 및 지터를 참조하십시오.

지터는 재시도만을 위한 것은 아닙니다. 기존의 운영 경험에 따르면 제어 영역과 데이터 영역을 모두 포함하는 서비스에서 트래픽이 급증하는 경향이 있습니다. 이러한 트래픽 급증은 매우 짧아, 집계된 지표에서 드러나지 않는 경우가 많습니다. 시스템을 구축할 때 우리는 모든 타이머, 주기적인 작업 및 기타 지연된 작업에 약간의 지터을 추가하는 것을 고려합니다. 이를 통해 급증하는 업무를 분산하고 워크로드에 맞게 다운스트림 서비스를 보다 쉽게 확장할 수 있습니다.

예약된 작업에 지터를 추가할 때 우리는 각 호스트에서 무작위로 지터를 선택하지 않습니다. 대신 동일한 호스트에서 매 시간마다 동일한 횟수로 생성하도록 일관된 방법을 사용합니다. 서비스의 오버로드 시 또는 경쟁 조건의 경우에는 동일한 방식으로 패턴이 발생합니다. 인간은 패턴을 식별하는 데 능숙하므로 근본 원인을 좀 더 쉽게 판별할 수 있습니다. 무작위 방법을 사용할 경우 리소스 과부하 시에 무작위로만 발생할 수 있게 됩니다. 그러면 문제 해결이 좀 더 어려워집니다.

Amazon Elastic Block Store(Amazon EBS)와 AWS Lambda처럼 과거에 작업했던 시스템에서 우리는 클라이언트가 일정한 간격으로(예: 1분당 한 번씩) 요청을 전송한다는 것을 발견했습니다. 그러나 동일한 방식으로 동작하는 여러 대의 서버가 있을 때 그들은 동시에 요청을 정렬하고 트리거할 수 있습니다. 이것은 1분의 처음 몇 초이거나 매일 수행되는 작업의 경우에는 자정 후 처음 몇 초일 수 있습니다. 초당 부하에 주의를 기울이고 클라이언트와 협력하여 주기적인 워크로드에 지터를 추가한 결과 우리는 서버 용량을 줄이면서 동일한 양의 작업을 처리할 수 있게 되었습니다.

고객 트래픽 급증에 대한 제어권은 줄었습니다. 그러나 고객이 트리거한 작업의 경우에도 고객 경험에 영향을 미치지 않는 수준에서 지터를 추가하는 것이 바람직합니다.

결론

분산 시스템에서 일시적인 장애 또는 원격 상호 작용의 지연은 불가피합니다. 시간 제한은 시스템이 과도하게 오래 중단되지 않게 하고, 재시도는 이러한 장애를 거를 수 있게 해 주며, 백오프 및 지터는 활용도를 향상시키고 시스템의 정체를 줄여 줍니다.

우리는 재시도를 조심스럽게 다루는 것이 중요하다는 것을 배웠습니다. 재시도는 종속 시스템의 부하를 증폭시킬 수 있습니다. 시스템에 대한 호출이 시간 제한을 초과하고 시스템에서 오버로드가 발생한 경우 재시도는 문제를 개선하는 것이 아니라 더욱 악화시킬 수 있습니다. 우리는 종속성이 양호한 경우에만 재시도를 수행하여 문제가 증폭되는 것을 방지하고 있습니다. 재시도는 가용성 향상을 지원하지 않기 때문에 우리는 재시도를 중단하고 있습니다.


저자에 대하여

Marc Brooker는 Amazon Web Services의 선임 수석 엔지니어입니다. 그는 2008년부터 AWS에서 근무하며 EC2, EBS 및 IoT를 포함하여 여러 서비스를 다루었습니다. 지금은 확장 및 가상화 작업을 포함하여 AWS Lambda에 집중하고 있습니다. Marc는 COE와 포스트 모텀에 대한 이야기도 즐겨 읽습니다. 그리고 전기 엔지니어링 박사 학위도 갖고 있습니다.

분산 시스템의 도전 과제 오버로드를 방지하기 위해 로드 차단 사용 분산 시스템의 폴백 방지