지난 4개월간 Fly.io가 겪은 신뢰성 문제와 주요 사고들(서비스 디스커버리/Corrosion, Vault, Postgres, 용량, 볼륨, 상태 페이지)을 솔직하게 공유하고, 원인과 진행 중인 개선 계획을 설명합니다.
지난 4개월은 험난했습니다. 우리가 용인할 수 있는 수준보다 더 많은 문제가 있었습니다.
이걸 공유하는 데 망설였던 이유는, 음, 마비될 듯한 실패감과 싸우고 있었기 때문입니다. 두려움도 있죠. 우리가 개선하지 못하면 우리 회사는 존속할 수 없고, 저는 이 회사를 일하는 걸 정말 좋아하거든요.
흥미로운 문제 중 하나는 우리의 인기가 폭발적으로 늘었다는 점입니다. 듣기에는 좋은 문제처럼 보이죠! 하지만 그로 인해 플랫폼을 애초 설계 범위를 넘어 사용하게 됐습니다. 플랫폼을 키우고 엔지니어링 조직을 성숙시키기 위해 많은 노력과 자원을 투입했지만, 그 작업이 성장 속도를 따라가지 못했습니다.
이건 여러분 각자에게 정말 안 좋은 일입니다. 여러분은 우리의 인기가 어떻든 크게 신경 쓰지 않죠. 뭐, 많은 분들이 신경 쓰기도 합니다만, 결국 여러분이 원하는 건 앱을 자신 있게 배포하는 일입니다.
우리도 같은 바람입니다. 다만 이건 고된 일이고, 우리는 우리의 어려움을 충분히 솔직하게 공유하지 못했던 것 같습니다. 여러분도 우리처럼 개발자니까, 더 지저분한 디테일까지 믿고 알려드렸어야 했죠. 그래서, 일부를 여기에서 공유합니다.
우리 플랫폼은 여러 구성 요소가 맞물려 함께 동작해야 합니다. 그래야 여러분이 앱을 배포하고, 다시 배포하고, 자리를 비웠다가 24개월 뒤에 돌아와도 여전히 잘 돌아가는 걸 볼 수 있죠. 이를 위해 필요한 것들은 다음과 같습니다:
flyctl이 사용하는 WireGuard 게이트웨이flyctl이 앱을 Docker 이미지로 빌드하기 위해 사용하는 원격 Docker 빌더 VM이 모든 구성 요소가 각자의 독특하고 놀라운 방식으로 실패했습니다. 종종 이런 일이 발생해도 운이 좋아서 여러분이 hiccup을 눈치채지 못하기도 합니다. 하지만 때로는 운이 따르지 않죠.
순서는 상관없이, 지난 4개월 동안의 주요 사건들입니다:
우리는 모든 리전 전체에 앱 인스턴스와 상태(헬스) 정보를 전파합니다. 이걸 통해 프록시는 요청을 어디로 라우팅할지, DNS 서버는 어떤 이름을 내줄지 알게 됩니다.
처음엔 HashiCorp Consul을 사용했습니다. 하지만 Consul은 단일 데이터센터 배포를 위해 설계된 중앙화된 서버 모델인데, 우리는 그것을 전역 서비스 디스커버리라는 역할에 억지로 끼워 맞췄습니다. 그 결과: 지속적으로 오래된 데이터, 만료된 인터페이스로 라우팅하는 프록시, 그리고 자주 낡은 엔트리를 가진 프라이빗 DNS가 생겼죠.
이는 모든 상태 업데이트(모든 VM 시작/중지)를 중앙 서버 클러스터를 통해 왕복시킨 결과였고, 종종 대륙 간 왕복이 되었습니다.
이에 대응해 Corrosion이라는 프로젝트를 출시했습니다. Corrosion은 가십 기반 서비스 디스커버리 시스템입니다. VM이 올라오면 해당 호스트가 인스턴스 정보를 가십으로 퍼뜨립니다. Corrosion의 목표는 전 세계적으로 1초 이내(가능하면 즉시)에 변경 사항을 전파하는 것입니다.
문제는 Corrosion이 새롭고, 가십 기반에서 일관성을 유지하는 일은 매우 어렵다는 점입니다.
사용자에게 Consul이 문제를 일으키고 있었기 때문에 Corrosion을 서둘러 내보냈습니다. 새 소프트웨어라서 두 가지 문제가 발생했습니다. 둘 다 전역 서비스 디스커버리 상태가 손상되는 형태로 나타났습니다. 첫 번째는 우리 내부의 어떤 프로세스가 업데이트를 남발해 Corrosion을 사실상 내부 DDoS 상태로 만든 경우였습니다. 두 번째는 일상적인 업데이트 중 예기치 않게 데이터베이스를 망가뜨린 경우였습니다.
이 두 문제의 영향으로 배포 중 애플리케이션이 깨졌습니다. VM이 올라갔다 내려가는 동안, 프록시와 DNS 서버가 오래된 데이터를 붙잡고 있게 된 것이죠.
Corrosion은 더 높은 내고장성을 가져야 합니다. 이를 위해 점진적인 개선(예를 들어 속도 제한으로 "내부 DDoS" 위험을 완화)을 하고 있습니다. 동시에 아키텍처 수준의 변화도 진행 중입니다. 가십은 어려운 방식입니다. 문제가 특정 깨진 노드로 쉽게 추적되지 않고, 빠르게 전파되는데, 이는 문제가 있을 때 가장 원치 않는 특성이기 때문입니다.
Nomad에서 벗어나는 것도 Corrosion 문제를 완화하는 데 도움이 됩니다. Nomad는 배포마다 완전히 새로운 인스턴스를 만들기 때문에 서비스 디스커버리가 요란해집니다. 초당 매우 많은 이벤트 업데이트가 발생하거든요. 반면 Fly Machine 기반 앱은 덜 부산합니다. Machines에서 실행 중인 앱을 업데이트할 때는 제자리에서(in-place) 업데이트하기 때문입니다.
마지막으로, 이건 서비스 디스커버리만의 이야기는 아닙니다. 우리는 주중에 플랫폼에 많은 변경을 배포합니다. 때때로 우리의 변경이 여러분의 변경과 맞물리면서, 타이밍이 좋지 않은 앱 배포가 그 앱을 이상한 상태로 남겨두기도 했습니다. 그래서 그런 시기에는 앱 배포를 일시 중지하도록 도구를 업데이트하고 있고, 그럴 때는 왜 일시 중지됐는지도 최대한 명확하게 드러나게 할 것입니다.
애플리케이션 시크릿은 HashiCorp Vault에 저장합니다. HashiCorp Vault는 Consul과 비슷하게 중앙 서버 클러스터로 동작합니다.
Vault에서 겪는 문제는 Consul에서 겪었던 것만큼 심각하지는 않지만, 그와 닮아 있습니다. 새 VM이 부팅될 때마다, 해당 작업자(worker)는 Vault에서 시크릿을 가져와야 합니다. 여기에는 두 가지 기본 문제가 있습니다:
서비스 디스커버리와 마찬가지로, 이러한 문제는 Nomad에서는 더 심해지고 Fly Machines에서는 완화됩니다. 하지만 Vault 상태가 나쁘면 새로운 Fly Machine 생성도 실패할 수 있습니다.
이건 하나의 주제이기도 합니다. 기존 오픈 소스는 전역 배포를 염두에 두고 설계되지 않았습니다. 그래서 기존 인프라 소프트웨어를 "구입"하기로 선택하면, 대가의 일부로 글로벌 탄력성을 희생하는 일이 자주 생깁니다.
우리 Postgres 클러스터에는 두 가지 큰 문제가 있었습니다: (1) Stolon과 Consul 클러스터와의 실시간 연결에 대한 의존, (2) "비관리형 Postgres"에 대해 우리가 만들어 버린 기대치.
첫 번째는 아키텍처적 문제입니다. Postgres가 의존하는 Consul 클러스터는 우리가 서비스 디스커버리에 사용하는 것과는 다르지만, 여전히 이상한 방식으로 "고장"날 수 있습니다. 초기에 Fly Postgres를 구축할 때 사용한 Postgres 클러스터 소프트웨어인 Stolon은 Consul 연결 문제를 잘 처리하지 못합니다.
새로 만드는 Postgres 클러스터는 Stolon을 사용하지 않고, 대신 repmgr로 구성됩니다. repmgr는 추가 데이터베이스 없이 클러스터 내부에서 리더 선출을 처리합니다. 이 새로운 Postgres 클러스터들도 구성을 공유하기 위해 여전히 Consul을 사용하지만, Consul이 녹아내려도 클러스터는 계속 동작합니다.
이미 프로비저닝된 Postgres DB들을 새 repmgr 구성으로 업그레이드하기 위해 작업 중입니다. 복잡한 점들이 있지만, 이에 대해 계속 공유하겠습니다.
둘째 문제는 제 잘못된 선택에서 비롯되었습니다. 우리는 적합한 관리형 Postgres 제공업체가 나타날 때까지 시간을 벌기 위해 "비관리형 Postgres"를 제공하기로 했습니다. 문제는, fly pg create라는 명령이 사람들이 관리형 Postgres 클러스터를 받는다고 암시한다는 겁니다. 왜냐하면 "간단히 Postgres 받기"를 제공하는 다른 모든 업체는 함께 관리형 스택을 제공하기 때문이죠.
지금 와서야 당연해 보이지만, 저에겐 놀라운 교훈이었습니다. 많은 것을 약속하는 UX를 보여주고는, 그 약속을 지키지 못하는 결과가 되었죠. 우리는 가치 선언문을 적는 타입의 회사는 아니지만, 만약 적는다면 "개발자의 기대를 어겨서 불쾌한 놀라움을 만들지 말자" 같은 내용을 적었을 겁니다.
우리는 관리형 Postgres를 해결할 것입니다. 거기까지 가는 데 시간이 걸리겠지만, 인프라 스택의 핵심 구성 요소인 만큼 외면하고 넘어갈 수 없습니다.
새로운 사용자가 급증하면서 여러 리전에서 서버 용량이 바닥났고, 어떤 곳에선 한 번도 아니고 여러 번 그랬습니다(안녕, 프랑크푸르트).
이는 두 가지 차원의 실패였습니다. 서버를 충분히 빨리 사들이지 못했고, 특정 리전에 걸린 압력을 덜어낼 좋은 도구도 부족했습니다.
작년에 저는 만약 용량 문제가 생기면 특정 리전에서 신규 사용자의 런치를 막을 수 있으리라 가정했는데, 그렇게 되지 않았습니다.
Heroku 대이탈이 우리의 가정을 깨버렸습니다. Heroku 이전에는 우리가 운영하던 앱 대부분이 리전 전반에 분산되어 있었습니다. 그리고 월 15% 정도 성장하고 있었죠. 하지만 Heroku 이후에는 몇몇 핫스팟에 앱이 대거 유입되었고 — 성장률도 월 30%로 뛰었습니다.
돌이켜보면, 투자금이 들어온 즉시, 훨씬 더 일찍부터 "진짜 비즈니스(srsbzns)"처럼 행동했어야 했습니다.
우리는 용량 계획과 물류를 더 잘 해 나가고 있습니다. 저는 그동안 본업과 병행해 사이드로 용량 계획을 맡아왔습니다. 회사는 제 스프레드시트를 넘어 확장할 필요가 있었죠. 이 부분에 채용을 했고, 약간의 재조직도 했습니다. 지금은 조금 나아졌고, 빠르게 더 좋아질 것입니다.
fly volumes 명령은 특정 호스트 하드웨어에 블록 디바이스를 생성합니다. 처음 이 기능을 출시했을 때, 우리는 이 접근 방식의 한계를 설명하는 많은 콘텐츠를 제공했습니다. 우리는 볼륨을 2개 이상의 세트로 운영하도록 설계했습니다.
이는 곧, 볼륨이 올라간 호스트가 다운되면 여러분의 앱도 다운된다는 뜻입니다. 또 그 호스트에 앱 VM을 실행할 만큼의 메모리나 CPU가 없다면, 배포조차 못할 수도 있습니다.
문서가 개선되는 과정에서 이런 세부 사항이 희미해졌고, 꽤 불쾌한 놀라움을 낳았습니다. 또한 이건 직관에도 반합니다. 사람들은 AWS EBS의 마법 같은 동작에 익숙하니까요. 하지만 우리의 볼륨은 EBS가 아닙니다(볼륨의 초기 버전은 제가 직접 만들었습니다!).
이 역시 UX가 잘못된 기대를 만들게 된 사례입니다.
우리는 상태 페이지에서 애매하게 글을 올리거나, 아예 올리지 않는 일로 정당한 비판을 많이 받고 있습니다. 한편으로는 블로그에서 우리 기술 스택을 뻔뻔할 정도로 자랑하고 있죠. 문제가 발생하는데, 우리가 그럴 때 적극적으로 소통하지 않았습니다. 그럼 우리는 한가하게 논 것처럼 보입니다.
이건 어렵습니다. 이 글을 쓰는 것도 어렵습니다. 우리의 자존심이 이 일에 다 묶여 있으니까요. 우리는 여러분이 벌어지는 일을 모두 알길 원하지만, 지치면 쉽게 미끄러집니다.
여기 적은 도전 과제 중 일부는, 전산학적 의미에서의 "어려운(Hard)" 문제입니다. 하지만 이 문제는 그렇지 않습니다. 변명의 여지가 없습니다. 우리는 즉각적인 커뮤니케이션을 더 잘하겠습니다.
우리는 인프라/운영 조직을 구축하기 위해 훌륭한 사람을 영입했습니다. 그 팀을 더 이상 비쳐 보일 만큼 얇게 펴지지 않게 보강하는 것과 더불어, 인시던트 대응을 표준화하고 있습니다. 사태가 크게 터질 때는 가능한 한 결정해야 할 일을 줄여 정보를 더 빨리 전달하려 합니다.
개인화된 상태 페이지도 출시합니다. 우리의 플릿이 커지고 서버를 더 많이 랙에 올릴수록, 어느 순간 하드웨어 장애를 겪을 확률은 높아집니다. 이 때문에 완전히 솔직한 상태 페이지를 유지하는 게 까다로웠습니다. 개인화된 상태 페이지를 통해, 하드웨어 장애의 영향을 받은 특정 고객에게 "이 리전에서 드라이브가 죽었고, 우리가 처리 중입니다"라고 더 쉽게 알려드릴 수 있을 것입니다.
이 글은 커뮤니티 포럼에 올리는 이유가 여러분이 여기에 답글을 달 수 있게 하기 위함입니다. 원하시면 토마토를 던지셔도 됩니다. 아니면 질문을 하셔도 되고요. 지금 우리는, 회사가 아직 충분히 성숙하지 않아 좋은 개발자 UX를 제공하기 위해 필요한 인프라를 받쳐주지 못하는 어색한 단계에 있습니다. 그 변화가 일어날 때까지 좋고 나쁜 것을 함께 감수하겠습니다.