컴퓨터공학 300 주제 시리즈의 129번째 글이다. 전체 지도는 여기.

한 줄 요약

“분산 컴퓨팅의 오해(Fallacies of Distributed Computing)” 는 처음 분산 시스템을 만드는 사람이 무심코 믿는 여덟 가지 가정이다. 네트워크는 믿을 만하다, 지연은 0이다, 대역폭은 무한하다, 네트워크는 안전하다, 토폴로지는 바뀌지 않는다, 관리자는 한 명이다, 전송 비용은 0이다, 네트워크는 균질하다. 여덟 개 모두 틀렸고, 하나라도 믿으면 언젠가 장애로 돌아온다.

왜 필요한가

Part 7 앞부분은 한 기계 안의 이야기였다. 파이프, 공유 메모리, 스레드, 이벤트 루프. 한 기계 안에서는 함수를 부르면 함수가 실행되고, 메모리에 쓰면 메모리에 있다. 실패하면 프로세스 전체가 같이 죽는다.

기계가 두 대가 되는 순간 이 전제가 무너진다. 원격 호출은 성공할 수도, 실패할 수도, 그리고 성공했는지 모를 수도 있다. 상대가 죽었는지 느린지 구별할 수 없다. 일부만 실패하는 부분 실패(partial failure)가 기본이 된다.

이 목록은 썬 마이크로시스템즈에서 피터 도이치(L. Peter Deutsch)가 일곱 개를 정리하고 제임스 고슬링(James Gosling)이 여덟 번째를 더한 것으로 알려져 있다. 오래된 목록이지만 마이크로서비스, 쿠버네티스, 클라우드 시대에 오히려 더 자주 인용된다. 서비스 경계마다 네트워크가 끼어들기 때문이다.

핵심 개념

여덟 가지 오해와 그 결과

# 오해 현실 무시하면 생기는 일
1 네트워크는 믿을 만하다 패킷은 사라지고, 연결은 끊기고, 스위치는 재부팅된다 타임아웃 없는 호출이 영원히 매달린다
2 지연은 0이다 같은 데이터센터도 왕복 수백 μs, 대륙 간은 수십~수백 ms 루프 안의 원격 호출(N+1)이 응답을 수 초로 늘린다
3 대역폭은 무한하다 링크마다 상한이 있고 공유된다 큰 페이로드·과한 로그가 다른 트래픽을 굶긴다
4 네트워크는 안전하다 내부망도 도청·위조가 가능하다 평문 내부 통신, 인증 없는 내부 API
5 토폴로지는 바뀌지 않는다 파드 IP 는 재시작마다 바뀌고 노드는 들고 난다 IP 하드코딩, 오래된 DNS 캐시
6 관리자는 한 명이다 팀·조직·클라우드 업체가 각자 일부를 관리한다 방화벽·설정 변경을 아무도 모른다
7 전송 비용은 0이다 직렬화 CPU 비용과 클라우드 송신 요금이 있다 존(zone) 간 트래픽 요금 폭탄, 직렬화가 CPU 병목
8 네트워크는 균질하다 기기·OS·프로토콜·MTU 가 제각각 특정 경로에서만 깨지는 패킷, 이상한 지연

가장 근본적인 것: 부분 실패와 불확실성

원격 호출이 타임아웃으로 끝났다. 가능한 경우는 셋이다.

1) 요청이 가는 길에 사라졌다         → 서버는 아무것도 안 했다
2) 서버가 처리하다 죽었다            → 일부만 했을 수 있다
3) 서버는 처리했고 응답이 사라졌다    → 다 했다

클라이언트는 이 셋을 구별할 수 없다. 이 불확실성이 분산 시스템 문제 대부분의 뿌리다. 다시 보내면 중복 실행이 되고(→ 멱등성), 안 보내면 유실이 된다. 상대가 죽은 건지 느린 건지 모르니 장애 감지가 확률 문제가 된다(→ 장애 감지). 누가 리더인지 서로 다르게 믿을 수 있다(→ 리더 선출, 합의). Part 7 의 나머지 글은 거의 전부 이 한 그림에서 출발한다.

원격 호출을 지역 호출처럼 보이게 하는 것의 위험

RPC 프레임워크는 원격 호출을 함수 호출처럼 보이게 해 준다. 편리하지만 위험하다. 짐 왈도(Jim Waldo) 등은 1994년 썬 연구소 보고서 “A Note on Distributed Computing” 에서, 지연·메모리 접근·부분 실패·동시성의 차이 때문에 지역 객체와 원격 객체를 같은 것처럼 다루면 안 된다고 주장했다. 인터페이스가 같아 보여도 실패 모델이 다르다.

오해를 피하는 설계 습관

  • 모든 원격 호출에 타임아웃을 둔다. 기본값이 무한인 클라이언트 라이브러리가 많다.
  • 재시도는 멱등한 연산에만, 지수 백오프와 지터를 섞어서 한다.
  • 원격 호출을 루프 밖으로 빼고 묶어서(batch) 보낸다.
  • 서비스 주소는 이름(DNS, 서비스 디스커버리)으로 찾는다.
  • 내부 통신도 TLS·인증을 기본으로 한다.
  • 지연은 평균이 아니라 꼬리(p99) 로 본다. 사용자 요청 하나가 원격 호출 여러 개를 거치면 그중 하나만 느려도 전체가 느리다.

직접 해 보기

2% 의 메시지가 사라지고 1% 가 2초씩 늦는 가짜 네트워크를 만들고, “네트워크를 믿는” 클라이언트와 “타임아웃 + 재시도” 클라이언트를 비교한다.

import random

random.seed(7)

def network_call():
    """요청 한 번. (성공 여부, 걸린 시간 ms) 를 돌려준다."""
    if random.random() < 0.02:                 # 2% 는 요청이나 응답이 사라진다
        return False, float("inf")
    latency = random.lognormvariate(3.0, 0.5)  # 대개 20ms 안팎, 가끔 긴 꼬리
    if random.random() < 0.01:
        latency += 2000                        # 1% 는 GC·재전송 등으로 2초 지연
    return True, latency

def naive():                                   # 오해 1·2: 네트워크는 믿을 만하고 빠르다
    return network_call()                      # 사라지면 영원히 기다린다(inf)

def careful(timeout=200, retries=2):          # 타임아웃 + 재시도
    spent = 0.0
    for _ in range(retries + 1):
        ok, t = network_call()
        if ok and t <= timeout:
            return True, spent + t
        spent += timeout                       # 타임아웃까지 기다리고 포기
    return False, spent

for name, fn in (("naive", naive), ("careful", careful)):
    res = [fn() for _ in range(10000)]
    fails = sum(1 for ok, _ in res if not ok)
    hung = sum(1 for _, t in res if t == float("inf"))
    ts = sorted(t for ok, t in res if ok)
    p50, p99 = ts[len(ts)//2], ts[int(len(ts)*0.99)]
    print(f"{name:8} 실패 {fails:4}  영원히 대기 {hung:4}  p50 {p50:6.1f}ms  p99 {p99:7.1f}ms  max {ts[-1]:7.1f}ms")
naive    실패  217  영원히 대기  217  p50   20.2ms  p99  2010.7ms  max  2110.7ms
careful  실패    1  영원히 대기    0  p50   20.3ms  p99   225.7ms  max   436.6ms

중앙값은 둘이 같다. 평소에는 차이가 안 보인다는 뜻이다. 차이는 꼬리에서 난다. 순진한 클라이언트는 1만 번 중 217번을 영원히 기다리고, p99 가 2초다. 타임아웃과 재시도를 넣은 클라이언트는 매달리는 요청이 없고 p99 가 0.23초로 내려간다. 단, 이 재시도는 요청이 멱등할 때만 안전하다. “응답만 사라진” 경우 서버는 같은 일을 두 번 하게 된다.

현업에서는

  • 쿠버네티스. 파드 IP 는 재시작하면 바뀐다(오해 5). 그래서 Service 와 DNS 이름으로 접근한다. 노드 사이 통신은 오버레이 네트워크(VXLAN 등)를 거치며 MTU 가 줄어드는데, 이를 무시하면 큰 패킷만 사라지는 이상한 장애가 난다(오해 8).
  • 홈랩 클러스터. 와이파이로 붙은 노드가 있는 소규모 클러스터에서는 순간적인 신호 저하가 노드 NotReady 나 etcd 리더 재선출로 이어지기도 한다. “네트워크는 믿을 만하다” 가 가장 먼저 깨지는 곳이 가정용 네트워크다.
  • 클라우드 비용. 같은 리전의 다른 가용 영역 사이 트래픽에 요금이 붙는 클라우드가 많다(오해 7). 복제 트래픽이 많은 데이터베이스나 메시지 브로커는 배치에 따라 비용이 크게 달라진다.
  • 연쇄 장애. 구글 SRE 책은 느린 의존 서비스가 호출자의 스레드·커넥션을 붙잡아 장애가 위로 번지는 연쇄 장애를 다룬다. 타임아웃, 재시도 예산, 부하 차단이 대책이다.

확인 문제

  1. 원격 호출이 타임아웃으로 끝났을 때 클라이언트가 구별할 수 없는 세 가지 경우는?
  2. 중앙값 지연은 같은데 p99 가 크게 다른 두 시스템이 있다. 사용자 체감은 어떻게 다를까?
  3. 파드 IP 를 설정 파일에 직접 적으면 어떤 오해를 범한 것인가?
  4. 위 실험의 careful 재시도가 위험해지는 조건은?

풀이

  1. 요청이 가다가 사라짐, 서버가 처리 도중 죽음, 서버가 처리했지만 응답이 사라짐.
  2. 요청 하나가 여러 원격 호출을 거치면 그중 하나라도 꼬리에 걸릴 확률이 커져, p99 가 큰 시스템은 자주 느리게 느껴진다.
  3. “토폴로지는 바뀌지 않는다” (오해 5).
  4. 요청이 멱등하지 않을 때다. 응답만 사라진 경우 재시도가 같은 작업(예: 결제)을 두 번 실행한다.

더 읽을거리 (References)