Chapter 2

안정성 안티패턴과 패턴

  • 2.1 Integration Points
  • 2.2 Chain Reactions
  • 2.3 Cascading Failures
  • 2.4 Users
  • 2.5 Blocked Threads
  • 2.6 Self-Denial Attacks
  • 2.7 Scaling Effects와 Unbalanced Capacities
  • 2.8 Dogpile, Force Multiplier, Slow Responses, Unbounded Result Sets
  • 2.9 Timeout
  • 2.10 Circuit Breaker
  • 2.11 Bulkhead
  • 2.12 Steady State
  • 2.13 Fail Fast
  • 2.14 Handshaking과 Test Harness
  • 2.15 Decoupling Middleware

프로덕션 장애의 대부분은 똑같은 안티패턴들이 조합을 바꿔가며 반복될 뿐이야. 다행히 이 안티패턴마다 대응하는 안정성 패턴이 존재하지.

**통합 지점(Integration Point)**이 시스템의 1번 킬러야. 소켓, REST API, DB 연결 — 외부와 만나는 모든 접점이 잠재적 폭탄이거든. TCP 연결은 상대가 죽어도 바로 못 알아채고, 패킷이 사라지면 재전송 타이머 만료까지 분 단위로 기다려야 해. 이 통합 지점에서 터진 문제가 같은 레이어로 퍼지면 **연쇄 반응(Chain Reaction)**이야. 로드밸런서 뒤에 서버 5대가 있는데 하나가 메모리 누수로 죽으면, 나머지 4대에 부하가 몰리고, 같은 버그를 가진 놈이 또 죽고, 도미노처럼 쓰러지는 거지. 레이어를 넘어 위로 퍼지면 **연쇄 장애(Cascading Failure)**야. 하위 서비스가 느려지면 상위 스레드가 블로킹되고, 스레드 풀이 고갈되고, 상위 서비스도 멈추고. 타임아웃 없는 통합 지점 + 리소스 풀 고갈 = 연쇄 장애의 공식이야.

근데 외부 공격자만 위험한 게 아니야. **사용자(Users)**도 안티패턴이 될 수 있어. 동시 접속 10만 명이 각각 1MB 세션을 가지면 100GB — GC가 미쳐 돌아가면서 Stop-the-World가 터지지. **블로킹 스레드(Blocked Threads)**는 더 무서워. 서버가 500 에러를 내뱉으면 최소한 뭔가 잘못된 건 알 수 있잖아. 근데 스레드가 조용히 블로킹되면 서버는 살아있는 것처럼 보이지만 사실상 죽은 거나 다름없거든. 마케팅팀이 100만 명에게 세일 이메일을 보내는 자기 부정 공격(Self-Denial Attack), 개발 환경 2대에서 프로덕션 200대로 갈 때 O(n²) 통신이 되는 Scaling Effects, 캐시 만료 순간 수백 요청이 원본으로 몰리는 Dogpile, 자동화 도구가 잘못된 설정을 200대에 일괄 배포하는 Force Multiplier — 이런 것들이 전부 조합으로 나타나.

특히 기억해야 할 건, **느린 응답(Slow Response)**이 에러보다 위험하다는 거야. 에러는 빠르게 실패해서 리소스를 반환하지만, 느린 응답은 리소스를 오래 잡고 있으면서 호출하는 쪽까지 느리게 만들어. 그리고 쿼리에 LIMIT 없이 던지는 Unbounded Result Sets — 개발 DB에서 100건이지만 프로덕션에서 100만 건이 올 수 있다는 거, 잊으면 안 돼.

이 안티패턴들에 대응하는 패턴을 보자. 가장 기본이면서 가장 중요한 건 **타임아웃(Timeout)**이야. 모든 외부 호출에 걸어야 해. TCP 기본 재전송 타임아웃이 분 단위라는 걸 생각해봐 — 그 동안 스레드는 죽어있는 거나 마찬가지잖아. P99 응답시간의 2배 정도에서 시작하고, **지수 백오프(exponential backoff)**로 재시도하되 최대 횟수를 제한하는 게 원칙이야. 타임아웃이 기초라면, **서킷 브레이커(Circuit Breaker)**는 이 책에서 가장 유명해진 패턴이지. 전기 차단기처럼 연속 실패가 감지되면 회로를 끊어버려. Closed(정상) 상태에서 실패가 임계치를 넘으면 Open(차단)으로 가고, 모든 요청을 즉시 실패시켜. 일정 시간 후 Half-Open으로 시험 요청을 보내서, 성공하면 다시 Closed로. 장애 난 서비스를 계속 호출하는 건 아무 의미 없거든. 호출 쪽 리소스만 낭비되고, 상대에겐 부하만 더 가중돼.

**격벽(Bulkhead)**은 선박 설계에서 온 건데, 스레드 풀을 분리하는 거야. 결제 API용과 상품 조회용 풀을 나눠두면, 결제가 느려져도 상품 조회는 멀쩡해. 용량의 일부를 희생해서 전체의 생존을 보장하는 전략이지. **정상 상태(Steady State)**는 시스템이 사람 개입 없이 무한히 돌아갈 수 있어야 한다는 원칙이야. 로그 파일, DB 히스토리, 메모리 캐시 — 쌓이기만 하고 줄지 않는 리소스가 있으면 그건 시간 폭탄이야. **빠른 실패(Fail Fast)**는 요청을 처리 못 한다는 걸 알면 즉시 503을 던지라는 거야. 기다리는 동안 스레드 잡고 있으면 다른 요청도 처리 못 하게 되니까.

그리고 동기 호출(HTTP, RPC)은 호출자와 피호출자를 강하게 결합시켜. **미들웨어로 결합을 끊으라(Decoupling Middleware)**는 가능한 곳에서 비동기 메시징을 쓰라는 이야기야. 생산자는 큐에 넣고 즉시 리턴, 소비자는 자기 페이스로 처리 — 한쪽이 죽어도 다른 쪽은 영향 없어. 이 패턴들은 단독으로도 강력하지만, 조합했을 때 진짜 힘을 발휘해.


정리

2장 읽고 기억할 거 세 가지:

  1. Integration Point가 1번 킬러이고, 서킷 브레이커가 최고의 방어야 — 외부 연결 지점에 타임아웃과 서킷 브레이커를 걸어서 장애 전파를 차단해.
  2. 느린 응답은 에러보다 위험해 — 에러는 빠르게 실패하지만, 느린 응답은 리소스를 잠식하면서 연쇄 장애를 유발해. Fail Fast가 답이야.
  3. 격벽(Bulkhead)으로 장애를 격리해 — 스레드 풀, 프로세스, 서버 그룹을 분리해서 한 곳의 장애가 전체로 퍼지지 않게 해.