- 분산 시스템의 핵심 난점은 지연 시간보다 부분 실패이며, 한 머신에서 단순한 예외 처리도 여러 머신에서는 프로토콜 설계의 일부가 됨
- 견고한 분산 시스템은 실제 여러 머신, 데이터센터 네트워크 조건, 큰 데이터셋에서만 드러나는 버그가 있어 단일 머신 시스템보다 비용이 큼
- 운영 가능한 시스템을 만들려면 조율을 줄이고 독립성을 높이며, 백프레셔·부분 가용성·메트릭·백분위 지표를 기본 도구로 삼아야 함
- 기능 플래그, ID 공간 설계, 데이터 지역성, 서비스 추출은 마이그레이션과 확장 과정에서 실패 범위와 조율 비용을 낮추는 실무 기법임
- CAP 정리는 시스템 구축의 출발점보다 설계 비판 도구에 가깝고, 분산 시스템에서는 기술 문제만큼 팀과 조직 사이의 사회적 조율도 어려움
분산 시스템을 처음 배울 때 놓치기 쉬운 것
- 분산 시스템 엔지니어의 많은 교훈은 프로덕션 트래픽에서 겪은 실수와 운영 상처에서 나오지만, 초보 엔지니어에게는 더 직접적이고 실행 가능한 맥락이 필요함
- Fallacies of Distributed Computing과 CAP theorem은 자기 학습에 도움이 되지만, 경험이 적은 엔지니어가 바로 움직이기에는 추상적임
- 분산 시스템은 더 많은 머신과 자본이 필요하므로 더 많은 팀과 큰 조직이 얽히기 쉬움
- 사회적 문제는 소프트웨어 개발자의 일에서 대체로 가장 어려운 부분이며, 분산 시스템 개발에서는 특히 두드러질 수 있음
- 기술 해법보다 사회적 해법이 더 효율적이고 만족스러울 때도 있지만, 배경·교육·경험은 엔지니어를 기술 해법 쪽으로 기울게 함
실패와 비용을 전제로 설계하기
- 분산 시스템은 자주 실패하고, 더 나쁘게는 부분적으로 실패함
- 단일 프로세스의 mutex unlock 실패는 프로세스를 크래시시키면 될 수 있지만, 분산 mutex의 unlock 실패는 락 프로토콜 안에 포함돼야 함
- “두 머신에 모두 write를 보내면 된다”거나 “성공할 때까지 재시도하면 된다”는 접근은 한쪽 write만 성공하고 다른 쪽은 실패하는 상황을 충분히 다루지 못함
- 스위치 장애, GC pause로 인한 리더 실종, 성공한 것처럼 보이는 socket write의 원격 실패, 느린 디스크 하나가 클러스터 통신 프로토콜 전체를 늦추는 상황이 생김
- 견고한 분산 솔루션은 단일 머신 솔루션보다 더 많은 비용이 듦
- 여러 머신에서만 발생하는 실패가 있고, VM과 클라우드는 비용을 낮추지만 이미 가진 컴퓨터 한 대에서 설계·구현·테스트하는 것만큼 싸지는 않음
- 공유 머신에 담기 어려운 데이터셋 크기나 데이터센터 네트워크 조건에서만 드러나는 실패는 실제 분산 환경이 필요함
- 시뮬레이션은 유용하지만, 분산 시스템 버그를 모두 대체하지는 못함
- 견고한 오픈소스 분산 시스템은 견고한 단일 머신 시스템보다 훨씬 드묾
- 많은 머신을 오래 실행하는 비용은 오픈소스 커뮤니티에 부담이 됨
- 취미 개발자와 딜레탕트는 이미 가진 머신과 여가 시간으로 오픈소스를 만들기 때문에, 여러 머신을 띄우고 유지하며 비용을 지불할 개발자를 찾기 어려움
- 기업 엔지니어가 일부 공백을 메우지만, 그 조직의 우선순위가 사용자의 조직 우선순위와 맞지 않을 수 있음
조율을 줄이고 장애를 흡수하기
- 머신 간 조율은 가능한 피해야 함
- 수평 확장성의 핵심은 머신들이 서로 통신하고 합의하는 일을 최소화할 수 있을 만큼 독립적으로 데이터를 배치하는 것임
- 두 머신이 어떤 것에 합의해야 할 때마다 서비스 구현은 더 어려워짐
- 정보 전달 속도에는 상한이 있고, 네트워크 통신은 생각보다 불안정하며, 합의에 대한 직관은 틀릴 수 있음
- Two Generals, Byzantine Generals, Paxos 구현의 어려움을 이해하는 것이 도움이 됨
- 문제가 메모리에 들어가면 분산 시스템 엔지니어에게는 대체로 사소한 문제에 가까움
- 데이터가 포인터 몇 번이 아니라 스위치 몇 개 건너편에 있을 때 빠르게 처리하기가 더 어려움
- 단일 머신 알고리듬과 구현은 풍부하지만, 분산 시스템을 위한 문헌과 구현은 훨씬 적음
- “느리다”는 가장 디버깅하기 어려운 문제임
- 시스템 전반에 백프레셔를 구현해야 함
- 백프레셔는 서비스하는 시스템이 요청 시스템에 실패를 신호하고, 요청 시스템이 그 실패를 처리해 자신과 상대 시스템의 과부하를 막는 방식임
- 과부하나 시스템 실패 동안 리소스 사용을 제한하는 것이 견고한 분산 시스템의 기본 구성 요소임
- 구현은 리소스가 제한되거나 실패가 발생했을 때 새 메시지를 버리거나 사용자에게 에러를 돌려주는 방식이 많으며, 두 경우 모두 메트릭을 증가시켜야 함
- 다른 시스템 연결과 요청에는 타임아웃과 지수 백오프가 필수임
- 백프레셔가 없으면 연쇄 실패나 의도치 않은 메시지 손실이 발생하기 쉬움
- 부분 가용성을 제공하는 방법을 찾아야 함
- 부분 가용성은 시스템 일부가 실패해도 일부 결과를 반환할 수 있는 능력임
- 검색 시스템은 결과 품질과 사용자를 기다리게 하는 시간 사이에서 절충하며, 제한 시간이 지나면 수집한 결과만 반환할 수 있음
- 프라이빗 메시징에서는 일부 사용자에게만 장애가 나는 편이 모든 사용자에게 일부 메시지가 사라지는 것보다 나을 수 있음
- 프라이빗 메시징 장애가 공개 이미지 업로드 같은 무관한 기능에 영향을 주지 않게 실패 도메인을 얼마나 분리할지 결정해야 함
관측, 용량, 배포를 운영 도구로 삼기
- 메트릭은 프로덕션에서 시스템이 실제로 무엇을 하는지 알기 위한 필수 수단임
- 지연 시간 백분위, 특정 동작의 증가 카운터, 변화율 같은 메트릭은 믿고 있는 시스템 동작과 실제 동작 사이의 간극을 줄임
- 20일째의 시스템 동작이 15일째와 어떻게 다른지 아는 것이 성공적인 엔지니어링과 실패한 주술을 가름함
- 메트릭은 문제와 동작을 이해하는 데 필요하지만, 다음에 무엇을 해야 하는지 알려주기에는 충분하지 않음
- 로그 파일은 유용하지만 거짓말을 하기 쉬움
- 몇몇 에러 클래스 로그가 파일 공간의 큰 비중을 차지해도 실제 요청 비율은 낮을 수 있음
- 성공 로그는 대부분 중복이고 디스크를 터뜨릴 수 있으며, 엔지니어는 어떤 에러 로그가 유용한지 자주 잘못 추측함
- 로그는 코드를 본 적 없는 사람이 읽는다고 가정하고 남기는 편이 좋음
- 로그에서 본 이상한 점을 메트릭으로 확인하지 않고 과대평가하면 장애가 길어질 수 있음
- 평균보다 백분위를 써야 함
- 50th, 99th, 99.9th, 99.99th 백분위는 대부분의 분산 시스템에서 평균보다 정확하고 유용함
- 평균은 평가 지표가 종 모양 분포를 따른다고 가정하지만, 엔지니어가 중요하게 보는 지표 중 그런 경우는 매우 적음
- 분산 시스템 지연 시간이 종 모양 분포를 따르지 않으면 평균 지연 시간은 잘못된 의사결정과 이해로 이어짐
- 용량 추정 능력을 익혀야 함
- 작업에 필요한 머신 수를 아는 것은 오래가는 시스템과 3개월 만에 교체해야 하는 시스템을 가름함
- 2012년 말 기준 일반적인 머신은 24GB 메모리를 갖고, OS에 4~5GB, 요청 처리에 최소 몇 GB가 필요하며, tweet id는 8바이트라는 식의 대략 계산이 필요함
- Jeff Dean의 Numbers Everyone Should Know는 기대치를 잡는 데 도움이 됨
- 기능 플래그는 인프라를 롤아웃하는 방법임
- 기능 플래그는 프론트엔드 A/B 테스트뿐 아니라 인프라 교체에도 강력하게 쓰임
- “big cutover” 방식은 늦게 발견된 버그 때문에 롤백을 강요받으며 실패한 프로젝트가 많음
- 단일 데이터베이스에서 새 저장소를 감춘 서비스로 옮길 때, 새 서비스로의 write 병렬 전송을 천천히 늘리고, 백필 완료 후 read를 사용자 응답에 쓰지 않는 상태로 시험하고, 구·신 시스템 read 비교를 수행한 뒤 실제 read를 점진적으로 늘릴 수 있음
- 문제가 생기면 플래그 값을 낮추거나 0으로 즉시 되돌릴 수 있고, 트래픽 양을 조절하며 디버깅과 실험을 할 수 있음
- 기능 플래그는 코드 안의 지역 복잡성을 전역 단순성과 회복력으로 교환하는 절충임
- 여러 버전의 인프라와 데이터가 예외가 아니라 일반적이라는 사실을 받아들여야 함
데이터 모델과 경계 설계
- ID 공간은 시스템의 형태를 결정함
- 데이터 하나에 도달하는 데 필요한 ID가 많을수록 데이터를 파티셔닝할 선택지가 많아짐
- 데이터 하나에 필요한 ID가 적을수록 시스템 출력을 소비하기 쉬움
- Twitter API v1은 tweet을 단일 64비트 숫자 ID로 조회·생성·삭제했으며, 이 ID는 다른 데이터와 연결되지 않음
- tweet 수가 늘어나면 같은 사용자의 tweet을 같은 머신에 저장해 사용자 tweet timeline과 구독 timeline을 효율적으로 구성할 수 있지만, 공개 API가 tweet id만으로 모든 tweet을 주소 지정하게 만들었기 때문에 user가 어떤 tweet id를 소유하는지 아는 조회 서비스가 필요해짐
- 대안으로 tweet 조회에 user id를 요구하거나, tweet id 자체에 user id를 포함할 수 있지만, 후자는 tweet id가 더 이상 k-sortable numeric이 아니게 되는 비용이 있음
- ID 구조는 비공개 데이터 익명성 해제, 예상치 못한 크롤링, 자동 증가 ID 문제, Insecure Direct Object References 같은 공격에 영향을 줄 수 있음
- 데이터 지역성을 활용해야 함
- 데이터 처리와 캐싱을 영구 저장소 가까이에 둘수록 처리 효율이 높고 캐시 일관성과 속도를 유지하기 쉬움
- 네트워크는 포인터 역참조나
fread(3)보다 실패와 지연 시간이 많음 - 지역성은 공간뿐 아니라 시간에도 적용됨
- 여러 사용자가 거의 동시에 같은 비싼 요청을 하면 하나로 합칠 수 있고, 같은 종류의 데이터 요청이 시간적으로 가깝게 발생하면 더 큰 요청 하나로 묶을 수 있음
- 이런 방식은 통신 오버헤드를 낮추고 장애 관리를 쉽게 만듦
- 캐시된 데이터를 영구 저장소에 다시 쓰는 것은 나쁨
- 특히 분산 시스템 경험이 적은 사람이 처음 설계한 시스템에서 이런 결함이 나타날 수 있음
- “Russian-doll caching”을 말하는 구현에서는 눈에 띄는 버그를 만날 가능성이 큼
- 흔한 증상은 사용자 정보, 예를 들어 screenname, email, hashed password가 이전 값으로 되돌아가는 현상임
- 컴퓨터는 생각보다 더 많은 일을 할 수 있음
- 2012년 말의 가벼운 웹 서버도 6개 이상 프로세서, 24GB 메모리, 매우 큰 디스크 공간을 갖고 있음
- 현대 언어 런타임의 비교적 복잡한 CRUD 애플리케이션도 단일 머신에서 수천 requests per second를 수백 밀리초 안에 처리할 수 있음
- 대부분의 경우 머신당 수백 requests per second는 운영 능력 관점에서 자랑할 만한 수치가 아님
- 애플리케이션을 프로파일링하고 측정에 기반한 효율화를 도입하면 더 높은 성능을 얻기 어렵지 않음
- CAP 정리는 시스템 구축법이 아니라 설계 비판 도구로 써야 함
- CAP 정리는 너무 일반적이고 가능한 해법 공간이 너무 넓어, 첫 원리로 삼아 작동하는 시스템을 도출하기에 적합하지 않음
- 시스템 설계를 놓고 CAP이 하위 시스템에 부과하는 제약을 반복적으로 검토하면 더 나은 설계에 도달할 수 있음
- C, A, P 중에서 CA는 선택할 수 없음
- 서비스를 추출하면 캡슐화와 배포의 장점을 얻을 수 있음
- 여기서 서비스는 저장 시스템보다 높은 수준의 로직을 포함하고 보통 요청-응답 스타일 API를 가진 분산 시스템을 뜻함
- 특정 코드가 시스템 내부가 아니라 별도 서비스에 있으면 변경이 쉬울지 계속 살펴야 함
- 추출된 서비스는 라이브러리처럼 캡슐화를 제공하면서도, 클라이언트 시스템의 라이브러리를 업그레이드하는 것보다 변경 배포를 빠르고 쉽게 만들 수 있음
- 작은 서비스는 코드와 운영 의존성이 적고, 엄격한 경계가 라이브러리에서 허용되는 지름길을 줄임
- 여러 클라이언트 시스템이 있을 때 공유 라이브러리는 각 클라이언트 배포를 조율해야 하며, 데이터 손상이 배포 순서에 따라 생길 수 있으면 더 어려워짐
- 클라이언트 시스템 유지자가 다르면 우선순위가 맞지 않아 업그레이드 동의를 얻는 사회적 조율 비용도 커짐
- 대표적인 사용 사례는 변경 중인 저장 계층을 더 편리하고 표면적이 작은 API를 가진 서비스 뒤에 숨기는 것임