핫 키 하나가 샤드 전체를 태운다

샤드를 고르게 나눴는데 프로모션 상품 하나 때문에 한 샤드만 CPU 100% 가 됐어요. 나머지는 20% 인데요. 샤드를 늘려도 안 풀리는 이유와 인기 키 하나를 여러 곳으로 나누는 법을 알 수 있어요.

데이터를 샤딩해 뒀어요. 키를 해시해서 고르게 나누도록요. 평소에는 각 샤드의 부하가 비슷해요.

그런데 프로모션 상품 하나가 올라간 날, 한 샤드만 CPU 가 100% 가 됐어요. 나머지는 20% 예요. 전체 용량은 남아도는데 그 한 샤드 때문에 서비스가 느려져요.

샤드를 늘려도 소용없어요. 문제는 데이터 분포가 아니라 접근 분포거든요. 그 상품 하나가 어느 샤드에 있든 그 샤드가 뜨거워져요.

샤딩은 데이터를 나누지만 인기를 나누지는 못해요.

그리고 어느 키가 뜨거워질지는 대개 사전에 알 수 없어요. 어제까지 평범하던 항목이 오늘 갑자기 인기를 얻습니다.

왜 그런가

샤딩은 키를 기준으로 데이터를 나눠요. 해시를 쓰면 데이터 개수는 고르게 나뉘어요.

문제는 요청이 고르게 오지 않는다는 거예요. 실제 서비스의 접근 분포는 극단적으로 치우쳐요. 상위 몇 개의 항목이 전체 요청의 상당 부분을 차지하는 게 보통이에요. 인기 상품, 유명인의 게시물, 공지사항, 기본 설정값 같은 것들이요.

그러면 그 키를 담당하는 샤드 하나에 요청이 몰려요. 다른 샤드들은 놀고 있는데 그 하나만 한계에 부딪혀요. 전체 용량은 충분한데 서비스는 느려지는 상태예요.

여기서 중요한 건 샤드를 늘려도 안 풀린다는 점이에요. 샤드가 백 개가 돼도 그 키는 여전히 하나의 샤드에 있어요. 나눌 수 있는 건 서로 다른 키들이지 하나의 키가 아니에요.

그리고 이 문제는 예고 없이 와요. 어제까지 평범하던 항목이 오늘 갑자기 인기를 얻어요. 사전에 어느 키가 뜨거워질지 알 수 없는 경우가 많습니다.

캐시에서도 같은 일이 일어나요. 캐시를 여러 노드로 나눠도 인기 키를 담당하는 노드 하나가 뜨거워져요. 그리고 그 노드가 죽으면 그 키에 대한 요청이 전부 원본으로 가면서 앞서 본 스탬피드가 됩니다.

코드로 보면

하나의 키를 여러 개로 쪼갠다:

코드
 인기 상품 하나 → 항상 같은 샤드
 샤드를 늘려도 그 키는 여전히 한 곳에 있다

· 1) 키를 인위적으로 쪼갠다
 item:12345         → item:12345:0 ~ item:12345:9
 읽을 때 무작위로 하나를 고른다 → 열 곳에 분산
 쓰기는 전부에 반영해야 하니 읽기가 많을 때 쓴다

· 2) 뜨거운 것만 앞단에 캐시한다
 애플리케이션 로컬 캐시로 샤드까지 안 가게
 가장 효과가 큰 대응이다

· 3) 뜨거운 키를 자동으로 찾는다
 키별 요청 수를 근사 집계로 상시 추적
 임계값을 넘으면 자동으로 앞단 캐시에 올린다

✕ 문제 지점 · ✓ 해법 · 그대로 돌려 보기 전에 자기 환경에 맞게 고쳐 주세요.

실전에서는

대응이 몇 갈래고, 읽기 중심인지 쓰기 중심인지에 따라 달라요.

읽기가 많은 경우가 대부분이고, 그때 가장 효과적인 건 앞단 캐시예요. 애플리케이션 프로세스 안의 로컬 캐시에 그 키를 두면 샤드까지 아예 안 가요. 인스턴스마다 사본이 생기니 자연스럽게 분산되고요. 데이터가 조금 오래돼도 되는 경우에 특히 잘 맞습니다.

키를 쪼개는 방법도 있어요. 하나의 키를 여러 개의 부분 키로 나누고, 읽을 때 그중 하나를 무작위로 고르는 거예요. 그러면 요청이 여러 샤드로 흩어져요. 다만 쓰기는 전부에 반영해야 하니 쓰기 비용이 늘어요. 읽기가 압도적으로 많을 때 적합합니다.

쓰기가 많은 경우는 더 어려워요. 조회수 증가 같은 건 부분 카운터를 여러 개 두고 읽을 때 합치는 방식으로 풀 수 있어요. 앞 편에서 본 집계 테이블 경합과 같은 해법이에요.

그리고 뜨거운 키를 자동으로 찾는 장치가 필요해요. 사전에 예측할 수 없으니까요. 키별 요청 수를 근사적으로 세는 알고리즘들이 있어서, 적은 메모리로 상위 키를 추적할 수 있어요. 임계값을 넘으면 자동으로 앞단 캐시에 올리는 구조를 만들면 사람이 개입할 필요가 없습니다.

여기서 앞 편들과 연결돼요. 뜨거운 키에 요청이 몰리는 건 스탬피드와 같은 뿌리고, 그 키를 담당하는 샤드가 느려지면 앞서 본 꼬리 지연으로 나타나요. 그리고 그 샤드가 죽으면 페일오버 문제가 됩니다.

마지막으로 관점 하나. 완벽한 분산은 불가능해요. 인기가 치우치는 건 서비스의 본질이지 버그가 아니에요. 목표는 균등하게 만드는 게 아니라 치우쳐도 견디게 만드는 것입니다.

처방

  1. 뜨거운 키를 앞단 캐시로 흡수하세요. 읽기가 많다면 이게 가장 효과적입니다.
  2. 뜨거운 키를 자동으로 찾는 장치를 두세요. 사전 예측은 안 되니 실시간 추적이 필요합니다.
  3. 읽기가 압도적이면 키를 쪼개 여러 샤드로 분산하세요. 쓰기 비용이 늘어난다는 걸 감안하세요.
  4. 쓰기가 많은 카운터는 부분 카운터로 나누고 읽을 때 합치세요.
  5. 샤드별 부하를 개별로 보세요. 평균으로 보면 한 샤드의 과부하가 안 보입니다.
  6. 앞단 캐시의 만료를 흩으세요. 안 그러면 스탬피드가 그 키에서 일어납니다.
  7. 뜨거운 키가 생겼을 때의 대응 절차를 만들어 두세요. 수동으로라도 즉시 캐시에 올릴 수 있어야 합니다.
  8. 치우침을 없애려 하지 말고 견디게 만드세요. 인기 편중은 서비스의 본질입니다.
  9. 뜨거운 키가 수백 개면 특별 취급이 아니라 일반적인 부하입니다. 그때는 용량 자체를 늘리는 게 맞습니다.

지금 확인해 보세요

어느 키가 뜨거운가:

코드
· 샤드별 부하 — 평균이 아니라 개별로 본다
 샤드별 CPU · 요청 수 · 응답 시간
 하나만 높으면 핫 키다

· 키별 요청 수 상위
 Redis: redis-cli --hotkeys
 애플리케이션: 키별 카운터를 근사 집계로

· 상위 몇 개가 전체의 몇 퍼센트인지
 상위 10개가 30% 를 넘으면 앞단 캐시가 필요하다

· 앞단 캐시가 실제로 흡수하고 있는지
 로컬 캐시 적중률과 샤드 요청 수를 함께 본다

· 읽기와 쓰기를 나눠서 본다
 캐시는 읽기만 받아 준다

핵심 한 줄샤딩은 데이터를 나누지만 인기를 나누지는 못한다.

브루니가 적어요

색을 바꾸며 숨는 카멜레온이에요. 운영에서 실제로 터지는 문제를 한 편씩 적어요. 같은 실수를 두 번 하지 않으려고 쓰는 기록이에요.

장애·확장 글 23편 모두 보기 →