외부 이벤트로 광고 트래픽이 평소보다 크게 늘었다.

평상시 부하 기준으로 튜닝된 오토 스케일링이 그 속도를 따라가지 못했다. 새 POD 가 Ready 되기 전에 기존 정상 POD 들이 무너졌고, 그 충격은 다음 시스템으로 번졌다.

장애 직후의 표면 진단은 “트래픽 스파이크 + 스케일링 지연” 이었다. 그런데 회고를 풀어보니 진짜 문제는 따로 있었다. 필터링 컴포넌트 한 곳이 무너졌는데, primary 와 fallback 이 같이 무너졌다 는 점이다.

사건의 흐름

먼저 무너진 건 필터링 컴포넌트였다. 트래픽 증가 속도가 HPA 의 새 POD 기동 속도보다 빨랐다. 새 POD 가 Ready 되기 전에 기존 정상 POD 들의 CPU 가 한계를 넘었고, Readiness Failed 가 줄줄이 발생했다. 스케일 아웃이 진행 중인 와중에 기존 POD 들도 무너지는 패턴 이었다. cascade 의 시작이었다.

그다음은 primary 광고 시스템이었다. primary 광고 시스템은 광고 후보를 결정할 때 필터링 컴포넌트에 필터 요청을 보낸다. 필터링 컴포넌트의 상태가 나빠지자 그 요청들이 TIMEOUT 으로 누적됐고, 누적된 실패가 primary 광고 시스템 자체의 상태까지 끌어내렸다. 자동으로 회복되지 않는 흐름이었다 — 수동 재시작이 필요했다.

세 번째는 추천 컴포넌트였다. primary 광고 시스템이 회복되자, 그동안 막혀 있던 요청이 한꺼번에 풀렸다. 줄어들었던 부하가 갑자기 정상치로 돌아오는 과정에서 5xx 가 발생했다. HPA 가 따라잡을 때까지 다시 시간차가 있었다.

cascade 는 문제가 풀리는 순간에도 다음 마디로 이어진다 는 것을 보여줬다. 필터링 컴포넌트가 회복돼도 primary 광고 시스템이 막혀 있었고, primary 광고 시스템이 회복되니 추천 컴포넌트가 휘청였다. 표면적인 타임라인만 따라가면 ‘서로 다른 세 장애’ 처럼 보였지만, 본질은 한 흐름이었다.

진단 — 공유 의존성과 단일 장애점

장애의 표면 원인을 정리하면 두 가지가 떠오른다. 외부 이벤트가 만든 갑작스러운 부하. 그리고 그 속도를 못 따라간 오토 스케일링.

여기서 멈추면 후속 개선은 “HPA 를 더 빠르게”, “알람을 더 빠르게”, “수동 대응을 더 빠르게” 로 흐른다. 다 맞는 말이지만, 모두 증상을 더 빠르게 다루는 답이다.

회고를 한 단계 더 밀어보면 다른 그림이 보였다. 우리 광고 시스템에는 필터링 컴포넌트가 있고, primary 광고 시스템이 그 컴포넌트에 의존한다. 장애에 대비해 fallback 시스템도 따로 두고 있었다. 평소엔 유휴 상태로 대기하다 primary 가 실패하면 활성화되는 구조다.

그런데 그 fallback 안에서 필터링 로직 은 코드 중복을 피하기 위해 필터링 컴포넌트 API 를 그대로 호출하는 방식으로 구성돼 있었다.

이 한 줄이 모든 걸 바꿨다.

primary 도, fallback 도 같은 필터링 컴포넌트에 묶여 있었다. 필터링 컴포넌트는 단일 장애점 이었고, 그 한 곳이 무너지자 양쪽이 동시에 무너졌다.

같은 의존성을 공유한 fallback 은 primary 의 부하를 흡수하지 못한다. 오히려 그 의존성으로 추가 트래픽을 보내 cascade 를 증폭 시킨다. 우리가 따로 띄운 fallback 서버가, 실제로는 두 번째 primary 처럼 같은 단일 장애점에 묶여 있던 셈이었다.

한 가지 더 있다. 그 단일 장애점이 왜 그렇게 빨리 무너졌는가. 필터링 컴포넌트는 광고 후보 평가와 필터링처럼 CPU 점유가 높은 작업이 많은 컴포넌트였다. Node 런타임의 단일 스레드 이벤트 루프는 그런 워크로드와 결이 잘 맞지 않는다. 한 POD 의 CPU 한계가 더 빨리 다가왔고, cascade 의 첫 마디가 그렇게 빨리 시작된 배경이기도 했다.

이번 장애의 진짜 문제는 외부 이벤트도, 오토 스케일링의 속도도 아니었다. 필터링 컴포넌트가 단일 장애점이었고, fallback 까지 같은 곳에 묶여 있었으며, 그 점 자체가 CPU 한계에 빠르게 도달하는 구조였다 는 것이다.

개선

진단이 명확하니 개선의 방향도 갈래로 나뉘었다. fallback 의 의존성을 끊어 양쪽을 떼어내는 길, 필터링 컴포넌트 자체에 보호를 두어 단일 장애점을 단단하게 만드는 길, 그리고 그 점의 처리량 자체를 늘리는 길. 서로 대신하지 않는다. 같이 가야 단일 장애점이 cascade 로 번지지 않는다.

fallback 의 광고 필터링 의존 제거

첫 번째 갈래는 fallback 의 필터링 로직을 독립적으로 갖추는 것이다.

운영 비용은 늘어난다. 필터링 컴포넌트의 로직을 fallback 쪽에도 두면 코드와 데이터 동기화가 추가된다. 처음 fallback 을 만들 때 그 비용 때문에 필터링 컴포넌트 API 호출 방식을 택했던 것이고, 그 결정 자체가 비합리적이지는 않았다.

다만 이번 장애가 보여준 건, 그 절약이 fallback 의 정의를 깎아먹었다 는 것이다. fallback 의 운영 비용을 fallback 의 존재 이유와 바꾼 셈이었다. 이 트레이드오프는 다시 들여다보면 한쪽이 명백히 무겁다고 봤다.

대안 하나는 fallback 의 필터링을 Serverless 로 구성하는 방식이다. 평소엔 유휴 상태가 fallback 의 본 모습이니, Serverless 의 idle 비용 0 특성이 잘 맞는다. 운영 비용을 줄이면서 독립성을 회복하는 방향이다.

필터링 컴포넌트에 Rate Limit

두 번째 갈래는 필터링 컴포넌트 자체를 단단하게 만드는 것이다.

cascade 의 첫 마디는 ‘스케일 아웃이 끝나기 전에 기존 정상 POD 들이 무너지는’ 패턴이었다. 오토 스케일링은 부하가 발생한 후 늘어나는 반응형이라, 갑작스러운 스파이크 앞에선 항상 시간차를 갖는다. 그 시간차 동안 정상 POD 들이 한계까지 끌려가는 것을 막아야 했다.

Rate Limit 이 그 역할이다. 새 POD 가 Ready 될 때까지 일부 요청을 일찍 거절해서, 정상 POD 들이 한계까지 내몰리지 않도록 한다. 또는 CircuitBreaker 로 의존성을 향한 요청 자체를 일정 조건에서 끊는 방식도 가능하다. 어느 쪽이든 단일 장애점이 한 번에 무너지지 않게 보호하는 장치다.

fallback 독립화가 양쪽을 떼어내는 작업이라면, Rate Limit 은 그 점 자체를 단단하게 만드는 작업이다. 단일 장애점을 양쪽에서 푸는 셈이다.

런타임 재검토

세 번째 갈래는 그 점의 처리량 자체를 늘리는 방향이다.

필터링 컴포넌트의 작업 특성을 다시 보면 Node 런타임은 그 워크로드와 결이 맞지 않는 선택이었다. CPU 시간을 길게 쓰는 평가/필터링 작업이 많고, 단일 스레드 이벤트 루프에서는 한 요청의 처리가 다음 요청의 응답을 늦춘다.

같은 POD 자원으로 더 많은 처리량을 내려면 시스템 언어 런타임이 자연스럽다. 런타임 재검토가 진행 단계에 있었다. Rate Limit 이 단일 장애점을 한 번에 무너지지 않게 막는 길이라면, 런타임 재검토는 그 점의 처리량 자체를 늘리는 길이다. 둘은 단일 장애점을 다른 각도에서 강화한다.

검토 단계에서 멈춘 상태지만, cascade 의 첫 마디가 ‘CPU 한계 도달’ 이라는 점을 보면 이 방향도 여전히 유효한 선택지로 봤다.

그 외 후속

운영 흐름도 같이 다듬을 부분이 있다. HPA 의 타겟을 CPU Utilization 대신 요청 수 로 두면 부하 증가를 더 일찍 감지할 수 있다. CPU 기반은 부하의 결과 를 보는 신호고, 요청 수는 부하의 원인 을 보는 신호다. 선제 감지에 가까워진다.

수동 Scale Out 이 필요한 순간에 k8s 설정을 직접 수정하는 흐름도 시간이 든다. Slack 봇으로 명령을 보내는 형태로 바꾸면 그 시간이 짧아진다.

둘 다 진단의 세 갈래만큼 본질적인 변화는 아니지만, cascade 의 다음 마디들을 짧게 만들어 준다.

배운 점

그날의 외부 이벤트는 트리거였을 뿐이다. 오토 스케일링의 한계도 사실이다. 그런데 그 모든 것 위에 우리가 만든 구조 가 있었다. 필터링 컴포넌트라는 단일 장애점이 있었고, fallback 까지 같은 곳에 묶여 있었으며, 그 점이 CPU 한계에 쉽게 도달하는 구조였다.

cascade 의 시간 흐름을 따라가다 보면 ‘HPA 를 더 빠르게, 알람을 더 빠르게’ 로 답이 모이기 쉽다. 그 자리에서 한 단계 더 미는 질문이 왜 fallback 이 cascade 를 막지 못했는가 였고, 거기서 필터링 컴포넌트가 사실은 양쪽의 단일 장애점이었다 는 진단이 나왔다. 그 진단을 한 번 더 밀면 그 점이 왜 그렇게 빨리 무너졌는가 까지 닿는다. 표면 진단과 구조 진단 사이의 거리가 회고의 가치였다.

단일 장애점은 fallback 까지 같은 의존성에 묶일 때 cascade 가 된다. 양쪽을 떼어내고, 그 점 자체에 보호를 두고, 그 점의 처리량 자체를 늘리는 것이 시스템이 단단해지는 길이었다.

참고

  • 광고 fallback 서버 설계 — 이 글에서 다룬 fallback 시스템의 초기 설계 회고. 이번 장애는 그 fallback 이 필터링 컴포넌트에 의존하던 한 줄이 만든 사건이었다.
  • Rate Limiting — cascade 의 시작점을 차단할 수 있는 보호 계층과 알고리즘 정리.
  • Circuit Breaker — 공유 의존성에 Circuit Breaker + Bulkhead 를 결합해 cascade 를 막는 패턴 정리.