오래된 데이터가 보일 때 확인할 순서

엣지, API, worker, cache, DB를 나눠 지연 원인을 좁힌 기준

한강자리의 백엔드 실행 환경은 미니PC 위의 K3s다. 사용자가 앱을 열었는데 주차 정보가 오래됐다고 해보자. 이때 바로 코드를 열면 늦다. 먼저 요청이 서버까지 닿았는지, API는 살아 있는지, worker가 새 값을 가져왔는지 나눠 봐야 한다.

사용자용 API는 엣지와 터널을 거쳐 K3s ingress로 들어온다. 원본 서버를 직접 공개하는 방식은 피했고, 사용자가 들어오는 길과 운영자가 손보는 길을 섞지 않았다.

미니PC라도 App Store에 올라간 앱이 호출하는 API가 그 위에서 돈다면 실제 서버다. 사용자는 서버가 집에 있는지 cloud VM에 있는지 알 필요가 없다. 느리거나 오래된 데이터를 보면 앱을 믿기 어려워진다.

사용자 요청이 API까지 닿는지 먼저 봤다

첫 확인은 사용자 요청이 어떤 경로를 지나 API까지 도착하는지다. 이 경로가 막혔는지, API 안에서 특정 기능만 실패하는지, 데이터가 오래된 것인지는 서로 다른 문제다.

flowchart LR
  Client["iOS 앱 / 위젯"] --> Edge["Cloudflare 엣지"]
  Edge --> Tunnel["Cloudflare Tunnel"]
  Tunnel --> Ingress["K3s 인그레스"]
  Ingress --> API["FastAPI 서비스"]
  API --> Redis["Redis"]
  API --> PG["Postgres"]

브랜드 사이트와 지원/개인정보 페이지는 API와 분리했다. 정적 웹과 앱 API는 장애 성격이 다르다. 같은 앱의 이름을 달고 있어도 실행 환경을 나누는 편이 문제를 찾기 쉽다.

flowchart TB
  Site["hangangjari.app<br/>브랜드 · 지원 · 개인정보"] --> Pages["Cloudflare Pages"]
  APIName["API 도메인"] --> Edge["엣지"]
  Edge --> Tunnel["터널"]
  Tunnel --> Runtime["MiniPC K3s 실행 환경"]

터널은 origin을 직접 공개하지 않고 엣지 네트워크를 통해 서비스로 라우팅한다. 그래서 사용자가 들어오는 길과 서버를 손보러 들어가는 길을 분리할 수 있다.

이 구분은 장애를 볼 때도 도움이 된다. 사용자가 들어오는 길이 막힌 것인지, 운영자가 쓰는 제어면이 문제인지가 섞이면 복구 순서가 흐려진다.

구성 목록보다 확인 순서가 필요했다

K3s 안에는 API, worker, stateful services, ingress, metrics/logs 쪽 구성 요소가 있다. 이름을 모두 외우는 것보다 오래된 값이 보였을 때 어느 부분을 먼저 확인할지 정해두는 편이 낫다.

구성역할
FastAPI APIiOS 앱과 위젯이 읽는 /v2 API
주차 worker주차 상태 수집
나들이 worker행사, 공지, 시설, 실시간 상황 수집
예측 worker예측 만들기와 home summary warmup
알림 worker후보 만들기, 보내기 규칙 확인, outbox 발송
Postgres/PostGIS기준 데이터, 이력, 감사 기록
Rediscache, 최신 상태, 보조 index
Ingress사용자 API routing
Metrics/logs stackmetrics, logs, dashboard
flowchart TB
  Desired["서버 상태 저장소<br/>원하는 상태"] --> GitOps["GitOps 동기화"]
  GitOps --> API["API 배포"]
  GitOps --> Workers["워커 배포"]
  GitOps --> State["Postgres / Redis"]
  GitOps --> Ingress["인그레스"]
  API --> Signals["메트릭 · 로그"]
  Workers --> Signals
  State --> Signals

미니PC라고 해서 확인할 것이 줄어들지는 않는다. 사용자가 쓰는 API라면 올리는 방법, 백업, metrics/logs, 복구 방법이 있어야 실제 서버로 볼 수 있다.

대시보드는 원인 후보를 줄여야 했다

대시보드는 꾸미는 화면이 아니다. 사용자 제보가 왔을 때 어디를 볼지 줄여주는 화면이어야 한다.

flowchart TD
  Checks["확인할 항목"] --> APIQ["API<br/>상태 · 지연 · 오류율"]
  Checks --> WorkerQ["워커<br/>마지막 성공 · 행 수 · 최신성"]
  Checks --> DataQ["데이터<br/>캐시 적중/미스 · 예측 실행 · outbox 적체"]
  Checks --> InfraQ["실행 환경<br/>인그레스 · GitOps 상태 · 백업 상태"]
  APIQ --> Triage["어디가 흔들렸는지 보기"]
  WorkerQ --> Triage
  DataQ --> Triage
  InfraQ --> Triage

한강자리에서 먼저 보고 싶은 신호는 다음과 같다.

  • API가 살아 있는가.
  • protected route가 정상적으로 거절·허용되는가.
  • 특정 endpoint latency가 튀고 있는가.
  • worker가 최신 데이터를 계속 수집하는가.
  • 출처별 마지막 성공 시각이 오래됐는가.
  • Redis cache가 비정상적으로 비어 있는가.
  • forecast가 최신 run을 만들고 있는가.
  • push outbox가 쌓이고 있지 않은가.
  • DB backup과 restore drill이 정상인가.

Prometheus에서는 애플리케이션이 내부 metric을 노출하고, Prometheus가 pull하는 방식이 기본이다. 한강자리도 API와 worker가 위 신호를 남기도록 맞췄다.

제보가 오면 코드보다 상태를 먼저 봤다

문제가 생기면 바로 코드를 의심하지 않는다. 어디서 실패했는지 먼저 본다.

flowchart TD
  Alert["사용자 제보 또는 스모크 실패"] --> Public{"사용자 API 상태 정상?"}
  Public -->|아니오| Runtime{"인그레스와 실행 환경이 정상인가?"}
  Runtime -->|예| Edge["엣지와 터널 확인"]
  Runtime -->|아니오| Pods["K3s 실행 환경과 GitOps 상태 확인"]
  Public -->|예| Feature{"특정 API만 실패하는가?"}
  Feature -->|예| Fresh{"최신성이 떨어졌는가?"}
  Fresh -->|예| Worker["워커와 수집 실행 확인"]
  Fresh -->|아니오| Cache["캐시/화면용 응답과 DB 조회 확인"]
  Feature -->|아니오| Client["클라이언트 캐시 또는 위젯 스냅샷 확인"]

이 순서가 있으면 API가 죽은 문제인지, 출처가 실패한 문제인지, 캐시가 오래된 문제인지, 클라이언트가 오래된 snapshot을 보고 있는지 나눠 볼 수 있다. 작은 서버에서 돈다고 해서 사용자가 만나는 실패까지 작아지지는 않았다.

복구해야 할 데이터부터 나눴다

백업은 “파일이 있다”로 끝나지 않는다. 복구해봐야 백업이다.

한강자리에서는 DB 백업 상태와 복구 가능성도 확인 대상에 포함한다. 공공데이터는 다시 수집할 수 있는 것도 많지만, 사용자 설정, push subscription, 앱 event, forecast/backtest 이력은 사라지면 복구가 어렵다.

그래서 어떤 데이터는 다시 만들 수 있고, 어떤 데이터는 반드시 남아야 하는지 구분해야 한다. Redis cache가 비는 것과 Postgres의 기준 데이터가 손상되는 것은 같은 장애가 아니다. 복구 연습은 이 차이를 실제로 확인하는 과정이다.

서버 크기보다 실패의 모양을 봤다

이 글에서 남기고 싶은 것은 K3s 구성 목록이 아니다. 사용자가 오래된 데이터를 봤을 때 요청 길, API, worker, cache, 클라이언트 snapshot을 어떤 순서로 나눠 볼 수 있어야 한다는 점이다.

한강자리에서 서버 쪽을 정리하며 가장 크게 바뀐 생각은 서버 크기와 해야 할 일이 비례하지 않는다는 점이었다. 사용자가 붙는 순간, 작은 서버도 느림과 실패와 복구를 설명할 수 있어야 했다.

이미지 확대