즐거운 연휴 되셨나요? 오늘 주간SaaS는 연휴를 피해 수요일에 발행했습니다. 오늘 소개하는 아티클은 Slack 엔지니어팀이 장애를 경험하며 아키텍처를 개선해 나간 과정을 소개 합니다. Cellular architecture로 개선하는 과정의 주요한 고려 사항과 설계 방향을 소개하고 있습니다. 개인적으로 오컴의 면도날 이 생각 났습니다. 특히 원문의 아래 부분에서요.
여러분들 생각은 어떠실지 궁금합니다. 고가용성 SaaS 아키텍처 설계를 고민하는 분들께 좋은 참고 자료가 되면 좋겠습니다. 그럼 즐거운 하루 되십쇼!
요약
최근 몇 년 동안, 대규모 온라인 서비스에서 셀 기반 아키텍처가 장애 피해 범위를 제한하고 재복구성을 높이기 위한 방법으로 점점 더 인기를 끌고 있습니다. 이러한 목표를 달성하기 위해 Slack의 가장 중요한 사용자 대면 서비스를 1.5년 동안 모놀리식 아키텍처에서 셀 기반 아키텍처로 이전했습니다. 이 블로그 게시물에서는 대규모 마이그레이션을 시작한 이유, 셀 기반 토폴로지의 디자인과 그 과정에서의 엔지니어링 트레이드 오프, 그리고 여러 연결된 서비스에 대한 큰 변화를 성공적으로 배포하기 위한 전략에 대해 다룰 예정입니다.
배경: 사건
Slack에서는 주목할 만한 서비스 중단이 발생할 때마다 인시던트 검토를 수행합니다. 다음은 그러한 인시던트 중 하나와 그 결과를 요약한 내부 보고서에서 발췌한 내용입니다:
언뜻 보기에는 별다른 문제가 아닌 것처럼 보이지만, 저희가 의존하고 있던 물리적 하드웨어에 장애가 발생하여 이 장비가 서비스에서 제거 될때까지 사용자에게 서비스 오류를 제공하게 되었습니다. 우리는 이 장애가 어떻게 사용자에게 보일 수 있었는지 궁금했습니다.
Slack은 글로벌, 다중 지역 에지 네트워크를 운영하지만 대부분의 핵심 컴퓨팅 인프라는 단일 지역인 미국 동부 1(us-east-1) 지역 내의 여러 가용 영역에 있습니다. 가용 영역(AZ)은 단일 지역 내에 격리된 데이터센터로, 물리적 격리를 제공할 뿐만 아니라 가상화, 스토리지, 네트워킹 등의 클라우드 서비스 구성 요소는 여러 AZ에서 동시에 장애가 발생하지 않도록 폭발 반경이 제한되어 있습니다. 따라서 클라우드에서 호스팅되는 서비스(예: Slack) 빌더는 한 지역의 전체 서비스 가용성이 기본 단일 AZ의 가용성보다 더 큰 방식으로 서비스를 설계할 수 있습니다.(*서비스를 여러 AZ에 걸쳐 위치 시킴으로써 보다 큰 가용성을 획득) 위의 질문을 다시 한 번 정리하자면, 왜 6월 30일에 이 전략이 제대로 작동하지 않았을까요? 왜 하나의 AZ가 실패하여 사용자가 볼 수 있는 오류가 발생했을까요?
결과적으로 분산 시스템에서 장애를 감지하는 것은 어려운 문제입니다. 사용자의 단일 Slack API 요청(예: 채널에 메시지 로드)은 수백 개의 서비스 백엔드로 분산될 수 있으며, 각 백엔드는 사용자에게 올바른 응답을 반환하기 위해 완료되어야 합니다. 서비스 프론트엔드에서는 장애가 발생해 실패한 백엔드를 감지하고 지속적으로 제외하려고 시도하지만, 실패한 서버를 제외하기 전에 몇 가지 실패를 기록해야 합니다! 더욱 어려운 점은, 우리가 사용하는 핵심 데이터 저장소인 Vitess를 비롯한 일부 주요 데이터 저장소가 매우 강력한 일관성을 제공한다는 점입니다. 이는 애플리케이션 개발자인 저희에게는 매우 유용하지만, 특정 쓰기에 사용할 수 있는 단일 백엔드가 반드시 있어야 한다는 단점도 있습니다. 즉 애플리케이션 프론트엔드에서 프라이머리 샤드를 사용할 수 없는 경우, 해당 샤드에 대한 쓰기는 프라이머리가 정상으로 돌아오거나 보조 샤드가 그 자리를 대신하도록 승격될 때까지 실패하게 됩니다.
위의 중단을 회색 장애로 분류할 수 있습니다. 회색 장애에서는 구성 요소마다 시스템의 가용성에 대해 서로 다른 관점을 갖습니다. 우리가 겪은 장애에서, 영향을 받은 AZ 내의 시스템은 해당 AZ 내의 백엔드를 완전히 사용할 수 있지만 AZ 외부의 백엔드는 사용할 수 없었고, 그 반대의 경우도 영향을 받지 않은 AZ의 시스템은 영향을 받은 AZ를 사용할 수 없는 것으로 인식했습니다. 동일한 AZ 내의 클라이언트 조차도 네트워크 흐름이 장애가 발생한 장비를 통과했는지 여부에 따라 영향을 받은 AZ의 백엔드에 대해 서로 다른 판단을 했을 겁니다. 외람되지만, 고객에게 메시지와 고양이 GIF를 제공하는 것이 우리의 주요 작업인데, 분산 시스템에게 이런 복잡한 일을 요구하는 것은 좀 과한 것 같았습니다.
회색 실패의 자동 복구 문제를 바로 해결하려고 하지 않았습니다. 대신에, 인간의 판단을 활용하여 컴퓨터의 작업을 간단하게 했습니다. 장애가 발생했을 때, 한 지역이 접속 불가능 했기 때문에 큰 문제가 발생했다는 것을 엔지니어들도 잘 알고 있었습니다. 만약 저희가 그 지역을 피하라는 버튼을 가지고 있었다면, 저희는 그 버튼을 반드시 눌렀을 겁니다. 그래서 우리는 그런 버튼을 만들기로 결정했습니다.
우리의 해결책: AZ는 하나의 셀(Cell)이며 이 셀은 배수(Drain)될 수 있다
AZ 배수(drain) 버튼은 개념적으로는 단순하지만, 실제로는 복잡합니다. 우리가 선택한 디자인 목표는:
- 5분 이내에 AZ에서 가능한 많은 트래픽을 제거한다. Slack의 99.99% 가용성 SLA는 연간 총 사용 불가 시간을 1시간 미만으로 허용하므로 이를 효과적으로 지원하려면 빠르게 작동하는 도구가 필요합니다.
- 배수 작업으로 인해 사용자에게 오류를 보여서는 안 된다. 드레인의 중요한 특성은 일반적인 완화 방법이라는 점입니다. 장애가 단일 AZ 내에 포함되어 있는 한, 근본 원인을 아직 파악하지 못한 경우에도 드레인을 효과적으로 사용하여 완화할 수 있습니다. 이는 인시던트 발생 시 운영자가 AZ를 배수하여 복구가 가능한지 확인한 다음 복구가 불가능 하면 배수 해제하는 실험적 접근 방식에 적합합니다. 배수로 인해 추가 오류가 발생하는 경우 이 접근 방식은 유용하지 않습니다.
- 배수와 배수 해제는 점진적으로 이루어져야 한다. 배수 해제 시 운영자는 AZ에 트래픽의 1%만 할당하여 실제로 복구되었는지 테스트할 수 있어야 합니다.
- 배수 메커니즘은 배수되는 AZ의 자원에 의존해서는 안 된다. 예를 들어, 모든 서버에 SSH로 접속하여 강제로 상태를 확인하여 드레인을 활성화하는 것은 좋지 않습니다. 이렇게 하면 AZ가 완전히 오프라인 상태인 경우에도 드레인이 적용될 수 있습니다.
간단한 구현 방법은 우리가 각 RPC 클라이언트에 신호를 전달하여, 해당 신호를 받을 때 특정 AZ에서 지정된 비율의 트래픽을 실패시키는 것입니다. 그러나 이 방법은 많은 복잡성을 가지고 있습니다. Slack은 공통의 코드베이스나 런타임을 공유하지 않습니다. 서비스 발견 인터페이스도 여러 개를 지원합니다. 특히, DNS는 AZ나 부분적인 배수와 같은 추상화를 제공하지 않습니다.
우리가 결정한 주요 전략은 'siloing'입니다. 서비스는 그들만의 AZ 내에서만 트래픽을 받고, 그 AZ의 서버로만 트래픽을 보낼 경우 'siloed'라고 할 수 있습니다. 이렇게 하면 각 서비스는 AZ당 하나의 가상 서비스처럼 보입니다. 중요한 것은, 우리가 사용자의 요청을 해당 AZ에서 리디렉션함으로써 AZ의 모든 siloed 서비스에서 효과적으로 트래픽을 제거할 수 있다는 것입니다.
이렇게 하면, 새로운 사용자 요청이 siloed AZ에 도착하지 않으면, 해당 AZ의 내부 서비스는 자연스럽게 새로운 작업이 없기 때문에 정지될 것입니다.
이렇게 해서 마침내 셀룰러 아키텍처에 도달했습니다. 모든 서비스는 모든 AZ에 존재하지만 각 서비스는 해당 AZ 내의 서비스와만 통신합니다. 한 AZ 내의 시스템 장애는 해당 AZ 내에 포함되며, 프론트엔드에서 리디렉션을 통해 트래픽을 동적으로 라우팅하여 이러한 장애를 피할 수 있습니다.
사일로화된 아키텍처를 통해 저희는 트래픽 이동에 관한 작업을 한 군데에서 집중적으로 처리할 수 있게 되었습니다. 바로 사용자의 요청을 us-east-1의 주요 서비스로 연결시키는 부분입니다. 최근 몇 년 동안, 우리는 HAProxy에서 Envoy / xDS 플랫폼으로 넘어가는 작업에 많은 시간과 노력을 투자했습니다. 그 결과, 모든 엣지 로드 밸런서는 이제 Envoy를 사용하며, 우리의 내부 시스템인 Rotor로부터 설정 정보를 받게 되었습니다. 이런 준비를 통해, 우리는 Envoy의 기본 기능 두 가지만을 활용해서 AZ 배수를 간단히 실행할 수 있게 되었습니다: 클러스터 가중치 조절과 RTDS를 통한 동적 가중치 변경 기능입니다. AZ 배수가 필요할 때, 우리는 Rotor를 통해 엣지 Envoy 로드 밸런서에게 us-east-1 지역의 AZ 가중치를 조절하라는 지시를 내립니다. 만약 us-east-1의 AZ 가중치가 0으로 설정되면, Envoy는 현재 처리 중인 요청은 계속 처리하면서 새로운 요청은 다른 AZ로 보내게 됩니다. 그 결과, 해당 AZ는 배수 처리가 됩니다. 이 방법이 어떻게 우리의 목표를 달성하는지 확인해보죠:
- 컨트롤 플레인을 통해 정보가 몇 초 안에 전달됩니다; Envoy 로드 밸런서는 바로 새로운 가중치 설정을 적용합니다.
- 배수 작업이 순조롭게 진행됩니다. 배수된 지역으로 보내진 요청은 중간에 멈추지 않습니다..
- 가중치 설정을 통해 최소 1% 단위로 조절하며 배수를 할 수 있습니다.
- 엣지 로드 밸런서는 다른 지역에 있고, 컨트롤 플레인은 여러 지역에 복사되어 있어서 한 지역에 문제가 생겨도 안정적으로 작동합니다.
다음은 우리가 한 AZ에서 트래픽을 두 개의 다른 AZ로 점진적으로 이동시키면서 각 AZ의 대역폭을 보여주는 그래프입니다. 그래프의 '무릎' 부분이 얼마나 뚜렷한지 주목하세요; 이것은 Envoy/xDS 구현이 우리에게 제공하는 낮은 전파 시간과 높은 세밀도를 반영합니다.
AZ별 초당 쿼리 수를 보여주는 그래프입니다. 하나의 AZ의 비율이 감소하는 동안 다른 두 AZ의 비율이 세 개의 명확한 시점에서 상승하고, 그 후 비율이 균등하게 다시 합쳐집니다.
다음 포스트에서는 우리의 기술적 구현에 대한 세부 사항을 더 깊게 들여다볼 것입니다. 내부 서비스에 대한 '사일로'를 어떻게 구현되었는지, 어떤 서비스는 '사일로화'될 수 없는지, 그리고 그런 서비스들에 대해 우리가 어떻게 대응하는지에 대해 논의할 것입니다. 또한, 이 강력한 새로운 도구를 사용할 수 있게 된 지금, Slack에서 서비스의 운영과 구축 방식이 어떻게 변화했는지에 대해서도 논의할 것입니다. 계속 기대해 주세요!
댓글
의견을 남겨주세요