데스 스파이럴이란 무엇인가요?
단일 노드 데스 스파이럴
제 전 직장에서 세계 최대 기업 중 한 곳의 이메일 라우팅 시스템을 구축하는 계약을 체결한 적이 있습니다. 제가 설계를 하고 하드웨어 사양을 정한 다음 다른 그룹이 구축에 착수했습니다. 이 설계에는 2개의 전용 DNS 확인자, 인바운드 이메일을 처리하고 스팸 및 바이러스 스캐너를 위해 대기하는 5개의 서버, 스팸 및 바이러스 검사를 위한 별도의 호스트, 발신 메일 라우팅을 위한 별도의 호스트가 포함되었습니다.
시스템이 프로덕션에 투입된 주에 저는 시스템이 너무 느리고 메일이 백업되고 연결이 거부된다는 전화를 받았습니다. 저는 질문을 하기 시작했고, 실제로 구현된 것이 비용을 절약하기 위해 원래 설계에서 '약간' 변경되었다는 것을 알게 되었습니다. 전용 DNS 확인기를 폐기하고 대신 각 호스트에서 BIND를 실행하고 수신 이메일을 처리하는 호스트에서 바이러스 스캐너를 실행하는 것이었습니다. 물론 바이러스 스캐너는 말할 것도 없고 BIND는 자체적으로도 많은 양의 RAM을 사용합니다.
문제가 시작되었을 때 시스템 관리자가 가장 먼저 한 일은 서버가 동시에 수락하는 인바운드 SMTP 연결 수를 늘리는 것이었습니다. 이로 인해 문제가 크게 악화되었습니다. 제가 그 수를 기존보다 훨씬 낮은 수준인 10개 정도로 줄여야 한다고 말하자, 그는 당연히 동시 전송 수를 늘리면 속도가 빨라질 것이고, 줄이면 속도가 느려질 것이라고 저와 논쟁을 벌였습니다! 제 대답은 간단했습니다. 스왑은 불가능하다는 것이었습니다. 시스템이 너무 많은 RAM을 사용하고 있었기 때문에 디스크로 스왑하고 있었기 때문에 모든 것이 느려지고 있었습니다.
동시 SMTP 연결 수를 줄이자 이메일 전송 속도가 크게 빨라지고 문제가 거의 사라졌습니다. 결국 여러 서비스에서 호스트를 공유함으로써 절약할 수 있을 것으로 생각했던 것과 거의 동일한 수의 서버를 추가하게 되었습니다.
서비스가 작동하는 동시성 또는 동시에 서비스하는 요청의 수는 다음과 같이 들어오는 요청의 속도 및 개별 요청을 서비스하는 데 걸리는 시간과 관련이 있습니다:
R = C/T
여기서 C는 동시성, R은 초당 요청 수, T는 시스템에서 요청을 처음부터 끝까지 처리하는 데 걸리는 시간(초)입니다. 이 방정식을 보면 시스템이 처리할 수 있는 요청 수를 늘리는 방법은 최대 동시성을 높이는 것임이 분명해 보일 수 있으며, 실제로 많은 시스템 관리자가 이를 시도합니다. 즉, Apache가 MaxClients(또는 다른 것)에 도달하는 것에 대해 불평하는 것을 보고 MaxClients를 늘리려고 합니다. 그러면 요청 속도가 급격히 떨어집니다.
문제는 T가 고정된 양이 아니라는 점입니다. 모든 종류의 요인에 따라 달라집니다: 요청이 디스크에 도달하는 경우의 I/O, 사용 가능한 메모리, 사용 가능한 네트워크 대역폭, 클라이언트의 속도, 이 요청이 스폰되는 요청의 지연 시간 및 서비스 시간 등 여러 가지 요인에 따라 달라집니다. C가 증가하고 사용 가능한 리소스 중 하나가 완전히 사용되거나 과부하가 걸리기 시작하면 T가 C보다 빠르게 증가하고 요청 속도가 떨어집니다.
동시성 제한이 전혀 없는 경우, 많은 서비스에서 요청이 처리되는 속도가 급격히 떨어지고 단일 요청을 처리하는 데 걸리는 시간이 한두 배로 늘어나는 '절벽 효과'가 나타납니다. 이는 호스트가 교체되기 시작하거나, 스케일링 알고리즘이 제대로 확장되지 않거나, 그다지 중요하지 않은 여러 가지 미묘한 효과로 인해 발생할 수 있습니다.
여기서 기억해야 할 가장 중요한 점은 동시성이 적을수록 더 좋은 경우가 많으며, 동시성은 항상 제한되어야 한다는 것입니다. 실제로 최대 동시 접속자 수를 파악하는 가장 좋은 방법은 직접 측정하는 것입니다. 다양한 동시성 수준에서 시스템을 벤치마킹하고 동시성 제한에 도달했을 때 합리적인 시간 내에 개별 요청을 처리하면서 최대 속도를 제공하는 수준을 선택하세요.
분산된 죽음의 나선
제 전 직장에서는 데이터베이스 액세스를 웹 서비스로 대체하는 야심찬 프로젝트에 착수했습니다. 수많은 PHP 개발자를 고용하여 애플리케이션 코드의 각 SQL을 검토하고 동일한 작업을 수행하는 웹 서비스를 작성한 다음 애플리케이션 코드를 웹 서비스에 대한 호출로 대체했습니다. 프로젝트는 재앙이었습니다.
하나의 요청으로 여러 데이터베이스에 대한 요청이 발생하여 단일 웹 서비스로 작성하는 것이 합당한 곳이 여러 군데 있었습니다. 개별 데이터베이스 쿼리와 복합 쿼리를 모두 호출해야 했기 때문에 둘 다 웹 서비스로 노출되었습니다. 몇몇 웹 서비스는 동일한 클러스터에서 호스팅되는 다른 웹 서비스를 호출했습니다. 결국 이 클러스터는 잠재적으로 겹칠 수 있는 호스트 그룹에 여러 개의 서로 다른 URL 네임스페이스를 가진 약 100개의 호스트로 구성되었으며, 그 앞에 여러 개의 로드 밸런서가 Pound를 실행했습니다.
몇몇 웹 서비스는 기존 SQL 및 C++ 코드보다 훨씬 느려졌습니다. 약 5,000대의 애플리케이션 서버와 100대 정도의 웹 서비스 호스트만 있는 상황에서 이는 문제가 되었습니다. 하지만 더 흥미롭고 교육적인 것은 소수의 애플리케이션 서버만 웹 서비스를 가리킬 때에도 어떤 일이 일어났는지였습니다. 개별 호스트가 최대 연결 수에 도달하면 웹 서비스 호스트의 부하가 크게 달라져 로드 밸런서가 로테이션에서 해당 호스트를 제외했습니다. 호스트에 부하가 다시 떨어지면 다시 추가됩니다. 이는 대부분의 클러스터에 과부하가 걸리고 애플리케이션 서버에 문제가 생길 때까지 계속 진행되었습니다. 개별 노드를 다시 시작해도 도움이 되지 않았습니다.
결국 클러스터의 부하를 제어하는 가장 쉽고 빠른 방법은 모든 노드에서 Apache를 중지한 다음 다시 시작하는 것이라는 사실을 알게 되었습니다. 이 방법이 효과가 있었지만 개별 노드(심지어 모든 노드)를 재시작해도 효과가 없었던 이유는 개별 웹 서비스 요청이 다른 노드에 다른 요청을 생성하고 있었기 때문입니다. 같은 클러스터에 있습니다. 개별 노드가 느려지면 결국 모든 클라이언트가 해당 노드에 대한 요청을 대기하게 되고, 전체 클러스터가 클러스터 내 다른 노드의 응답을 대기할 때까지 모든 노드가 대기하게 됩니다.
저는 분산 시스템에서 이러한 상황과 이와 관련된 상황을 분산형 죽음의 나선이라고 부릅니다. 단순히 과부하가 걸려 들어오는 요청을 따라잡지 못하는 단일 노드의 경우보다 더 복잡하고 혼란스럽기 때문에 분산형이라고 부릅니다.
죽음의 소용돌이 피하기
클라이언트에 가까운 동시성 제한
이 회사에서 반복적으로 발생하는 문제 중 하나는 다음과 같은 상황이었습니다. 특정 서비스의 동시성이 서버 자체 내에서 제한되는 경우(예: Apache의 MaxClients 또는 MySQL의 max_connections 사용). 부하가 폭주하면 서버가 동시성 제한에 도달하고 클라이언트와 가까운 일부 서비스(애플리케이션 서버, 웹 프론트엔드, 로드 밸런서 또는 Squid)가 사용자에게 표시되는 오류를 반환할 수 있습니다. 예를 들어 Squid는 "부모" 서버가 2분 동안 응답하지 않으면 다운된 것으로 표시하고 HTTP 500 오류를 반환합니다. 로드 밸런서는 503을 반환합니다. 이로 인해 개발자가 오류를 반환하는 서비스를 문제의 원인으로 지목하는 경우가 종종 있었는데, 이에 대해 저는 "오류를 반환하는 것이 작동하는 것입니다."라고 대답했습니다.
결국 저는 해당 고용주를 설득하여 대부분의 장소에서 Pound를 Haproxy로 대체했습니다. Haproxy를 사용하면 특정 백엔드로 전송할 최대 연결 수를 구성할 수 있으며, 그 이후에는 들어오는 연결을 대기열에 추가하고 연결이 도달하려는 URL에 따라 연결의 우선순위를 지정할 수도 있습니다. 통계 보고와 함께 Haproxy는 새로운 웹 서비스를 배포할 때 매우 유용했습니다. 들어오는 연결이 대기열에 얼마나 많이, 얼마나 자주 밀려드는지 간단히 관찰하여 서비스에 과부하가 걸리는지 확인할 수 있었고, 대부분의 애플리케이션 서버는 연결이 대기열에 들어가면 오류를 표시하여 사용자를 화나게 하거나 다시 로드를 반복해서 작업을 시도하여 문제를 악화시키는 대신 사용자가 조금 기다리게 하여 시스템 과부하 시 속도를 늦추는 올바른 작업을 수행했기 때문이죠.
연결 대기열에 대해 말씀드리자면
작업 대기열 사용
더스틴은 이 점을 많이 강조합니다. 웹 개발자는 상태 비저장 서버를 선호하기 때문에 클라이언트에 결과를 반환하기 전에 주어진 클라이언트 요청과 관련된 모든 작업을 완료하려고 하는 것이 일반적입니다. 즉, 요청과 관련된 모든 리소스(예: 처리 중 Apache 프로세스에서 할당된 메모리, 데이터베이스에 보관된 잠금 등)가 요청이 진행되는 동안 유지됩니다.
대신 모든 것을 미리 처리할 필요 없이 클라이언트로부터 정보를 수락하여 대기열에 넣은 다음 책임을 다했음을 나타내는 성공 메시지를 보내는 것이 더 좋습니다. 예를 들어 이메일이 이런 식으로 작동합니다: SMTP는 일반적으로 메시지가 사용자의 사서함에 전달된 후가 아니라 디스크에 스풀링되는 즉시 성공 코드를 반환합니다. 이러한 방식으로 작업을 수행할 때의 장점 중 하나는 대기열을 사용하면 스트리밍 쓰기가 빠르다는 점을 활용할 수 있는 반면, 데이터베이스 업데이트는 일반적으로 무작위 검색이 필요하므로 속도가 훨씬 느리다는 점입니다.
통화 그래프에서 루프 방지
위에서 설명한 웹 서비스는 동일한 클러스터의 다른 노드에 요청을 생성하지 않았다면 훨씬 더 안정적인 부하를 가졌을 것입니다. 이와 같은 루프가 있는 시스템에서는 느린(또는 약간 느린) 노드 하나 때문에 전체 시스템이 중단될 수 있으며, 내결함성 분산 서비스가 노드가 추가될수록 안정성이 떨어지는 서비스로 바뀔 수 있습니다.
또한, 이 회사는 매우 고가의 클러스터형 스토리지 솔루션도 보유하고 있었는데, 이 솔루션은 하나의 느린 노드로 인해 전체 클러스터의 속도가 느려지고 전체가 잠겨 모든 노드를 재부팅해야 하는 등 똑같은 종류의 증상을 겪었습니다. 따라서 이는 자체 개발 솔루션만의 문제가 아닙니다.
서버를 데드 상태로 표시하거나 대상 서버당 아웃바운드 동시성을 제한합니다.
이는 앞의 요점과 관련이 있는데, 어쨌든 호출 그래프에 루프가 생기더라도 실패의 전파를 완화하거나 제거할 수 있기 때문입니다.
이 회사의 또 다른 문제는 각 웹 서비스가 일반적으로 여러 대의 MySQL 서버에 의존한다는 것이었습니다. 개별 요청에는 일반적으로 하나의 MySQL 서버만 필요하지만, MySQL 서버 중 하나가 느려지거나 네트워크에서 사라지면 같은 클러스터에 있는 호스트로의 호출과 마찬가지로 모든 웹 서비스 호스트의 모든 아웃바운드 연결이 결국 느리거나 죽은 호스트와의 통신에 소모되기 때문에 결국 모든 웹 서비스 호스트가 응답을 멈추게 됩니다. 수동 블랙리스트 메커니즘이 있었지만 호스트가 데이터베이스가 여러 번 시간 초과되었음을 기록하는 상태 저장 방식이나 특정 데이터베이스 호스트에 대한 동시 요청 수 제한이 있었다면 이 문제를 방지할 수 있었을 것입니다.
결론
이상적인 조건에서는 완벽하게 작동하지만 실제 환경에서는 실패하는 분산 서비스를 구축하는 것은 쉽습니다. 데스 스파이럴은 이러한 실패 중 가장 흔한 실패 중 하나이며, 몇 가지 간단한 설계 지침을 따르면 가장 흔한 원인을 피할 수 있습니다.
이 정보가 여러분만의 분산 서비스를 구축하는 데 도움이 되었기를 바랍니다! 여러분의 죽음의 소용돌이에서 벗어난 사연을 보내주시고, 제가 여러분을 구해준 적이 있다면 꼭 알려주세요.