목차7개 항목
출처와 참고
쿠팡 엔지니어링의 「Lessons learned from operating our data serving layer」(2022.08)를 바탕으로 재구성한 노트다.
캐시가 가용성을 정한다
상품 정보처럼 읽기가 압도적으로 많은 데이터는 영속 저장소 앞에 캐시 계층을 겹겹이 둔다.
flowchart LR app["앱 페이지"] --> svc["서빙 서비스"] svc --> l["로컬 캐시 (서버 메모리, 핫 키만)"] svc --> rt["읽기 관통 캐시(read-through) 클러스터"] svc --> rc["실시간 캐시 클러스터"] svc --> st["공용 영속 저장소 (캐시 미스일 때만)"]
요청의 95%가 캐시에서 끝나는 구조라면 저장소가 아니라 캐시 클러스터의 가용성이 서비스 가용성이다. 캐시를 「없어도 느려질 뿐인 부가 장치」로 다루면 안 된다. 캐시가 빠지는 순간 저장소가 감당하지 못할 트래픽이 그대로 쏟아진다.
클라우드 위의 수십백 대 캐시 노드에서 노드 한두 대의 고장은 일상이다. 같은 물리 호스트 문제로 여러 대가 함께 죽기도 하고, 장애는 110분쯤 이어진다. 그래서 문제는 「고장을 막는 것」이 아니라 고장 난 노드를 얼마나 빨리 빼고 다시 넣느냐다.
운영에서 만나는 장애와 대응
| 증상 | 원인 | 대응 |
|---|---|---|
| 노드 일부가 죽었는데 타임아웃과 큐 적체가 몇 분씩 이어진다 | 서킷 브레이커는 전체 실패율을 보므로 일부 트래픽만 실패하면 열리지 않는다. 클러스터 토폴로지 갱신은 분 단위로 느리고, 실제 TCP 연결과 어긋나 죽은 노드로 계속 요청이 간다 | 연결 단위 감시: 평소 응답이 밀리초 단위이므로 1초 동안 응답 없는 연결은 문제로 보고 닫는다. 감지가 수십 초~분에서 초 단위로 줄었다 |
| 장애 노드가 다시 붙지 못하고 동기화가 계속 실패한다 | 새 복제본이 전체 동기화 중인데도 트래픽을 받아 빈 데이터로 응답한다. 서빙 쪽은 이를 캐시 미스로 보고 쓰기를 쏟아붓고, 쓰기가 동기화 버퍼를 넘쳐 동기화가 다시 실패한다 | 준비 안 된 복제본은 트래픽에서 뺀다. 데이터 크기나 복제 상태로 서빙 가능 여부를 판단한다 |
| 노드별 트래픽이 5~10배 차이 나고, 가장 바쁜 노드 기준으로 증설하느라 자원이 남는다 | 캐시 클라이언트가 가장 빨리 응답하는 노드로 요청을 몰아 준다. 빠른 노드에 몰리면 그 노드가 느려질 때까지 쏠림이 계속된다 | 요청마다 무작위로 노드를 고른다. CPU가 조금 늘지만 노드 간 부하가 고르게 된다 |
| 예고 없는 트래픽 급증에 대비해 권장 용량의 몇 배를 상시 띄워 둔다 | 요청 로그를 분 단위로 집계해 보면 급증은 소수의 인기 상품(핫 키) 에서 나온다 | 핫 키를 서버 메모리의 로컬 캐시로 흡수한다. 평소 트래픽의 약 35%, 급증 때 최대 70%를 받는다 |
장애 노드 복구가 악순환이 되는 이유
flowchart TB a["복제본 재동기화 시작"] --> b["동기화 중인데 트래픽을 받음 → 빈 응답"] b --> c["서빙 계층이 캐시 미스로 판단 → 저장소 조회 후 캐시에 쓰기 폭증"] c --> d["쓰기가 동기화 버퍼 초과 → 동기화 실패"] d -->|"처음으로"| a
「응답이 왔다」와 「올바른 데이터가 왔다」는 다르다. 헬스 체크가 프로세스 생존만 보면 이 고리를 끊지 못한다. 서빙 가능 여부(readiness)를 생존 여부(liveness)와 따로 판단해야 한다. 쿠버네티스의 readiness probe와 같은 발상이다. 이렇게 바꾼 클러스터는 60대 중 12대를 피크 시간에 꺼도 P95 지연이 100ms 아래로 유지됐다. 바꾸기 전에는 500~1000ms까지 튀었다.
로컬 캐시로 핫 키 흡수하기
공유 캐시 클러스터 앞에 서버마다 작은 메모리 캐시를 두면, 같은 상품을 묻는 요청이 네트워크를 타지 않고 끝난다.
| 결정 | 이유 |
|---|---|
| TTL 1분 정도로 짧게 | 서버마다 따로 갖는 사본이라 무효화가 어렵다. 짧게 두고 저절로 만료되게 한다 |
| 최종 일관성을 허용하는 데이터만 | 1분 늦은 값이 문제가 되는 가격·재고 확정값은 넣지 않는다 |
| 객체(DTO)가 아니라 바이트 배열로 저장 | 꺼낼 때 역직렬화 비용이 들지만, 오래 사는 객체가 줄어 GC 부담이 크게 준다 |
로컬 캐시는 트래픽 급증을 공유 캐시와 저장소로부터 격리하는 장치이기도 하다. 핫 키 하나 때문에 캐시 노드 한 대가 과열되는 일을 막는다.
블로킹 I/O를 논블로킹으로
서빙 서비스는 대부분의 시간을 캐시·저장소 응답을 기다리는 데 쓴다. 요청마다 스레드가 응답을 기다리며 묶이는 블로킹 I/O를 논블로킹 I/O로 바꾸면 같은 서버로 훨씬 많은 요청을 처리한다. 저장소와 캐시 클라이언트가 이미 논블로킹을 지원하면 전환 비용이 낮다. 이 전환으로 CPU 사용량이 절반 넘게 줄었다. 개념은 동시성·병렬·비동기·논블로킹과 동시성 모델에 있다.
장애 격리의 층위
| 층위 | 장치 | 격리하는 범위 |
|---|---|---|
| 의존 서비스 | 서킷 브레이커 (대규모 트래픽에서 서비스를 지키는 설계) | 서비스 전체가 실패할 때. 일부 실패에는 둔하다 |
| 노드 | 연결 단위 타임아웃과 연결 끊기 | 죽은 노드 한 대 |
| 복제본 | readiness 기반 트래픽 차단 | 복구 중인 노드 |
| 키 | 로컬 캐시 | 핫 키 하나의 급증 |
굵은 장치 하나로는 부족하다. 장애 단위마다 감지와 차단 장치를 따로 둔다.
정리
- 캐시가 트래픽 대부분을 받으면 캐시 클러스터의 가용성이 서비스 가용성이다.
- 일부 노드 장애는 서킷 브레이커로 잡히지 않는다. 연결 단위로 빨리 끊는다.
- 복구 중인 복제본은 생존과 별개로 「서빙 가능」할 때만 트래픽을 준다.
- 「가장 빠른 노드」 라우팅은 쏠림을 만든다. 무작위 분산이 더 고르다.
- 핫 키는 짧은 TTL의 로컬 캐시로 흡수하고, GC를 고려해 바이트 배열로 저장한다.
관련 노트: 대규모 트래픽에서 서비스를 지키는 설계, 고가용성과 재해 복구 설계, HTTP 캐시, 동시성·병렬·비동기·논블로킹과 동시성 모델, 쿠버네티스 위 웹 서비스의 요청 경로와 구성 요소, 캐시 전략, Redis