Post

[Book - 가상 면접 사례로 배우는 대규모 시스템 설계 기초] 5. 안정 해시 설계

수평적 규모 확장성을 달성하기 위해서는 요청 또는 데이터를 균등하게 나누는 것이 중요하다.
안정 해시는 이 목표를 달성하기 위해 보편적으로 사용하는 기술이다.

해시 키 재배치(rehash) 문제

서버들에 부하를 균등하게 나누는 보편적 방법은 아래의 해시 함수를 사용하는 것이다.

serverIndex = hash(key) % N (N은 서버의 개수)

flowchart TB
%% Before: 서버 0~3
    S["Before 서버 수 = 4 (hash % 4)"]
    S0[서버 0: K0, <span style='color:red'>K4</span>]
    S1[서버 1: K1, <span style='color:red'>K5</span>]
    S2[서버 2: K2, <span style='color:red'>K6</span>]
    S3[서버 3: <span style='color:red'>K3, K7</span>]

%% After: 서버 0~2
    T["After 서버 수 = 3 (hash % 3)"]
    T0[서버 0: K0, <span style='color:red'>K3, K6</span>]
    T1[서버 1: K1, <span style='color:red'>K4, K7</span>]
    T2[서버 2: K2, <span style='color:red'>K5</span>]

%% 세로 정렬용 invisible nodes
    S --- S0 --- S1 --- S2 --- S3
    T --- T0 --- T1 --- T2

%% 제목 스타일 적용
    style S font-weight:bold
    style T font-weight:bold

변화된 키 분포를 보면, 장애가 발생한 3번 서버에 보관되어 있는 키 뿐만 아닌 대부분의 키가 재분배되었다.
하나의 서버가 죽으면 대부분 캐시 클라이언트가 데이터가 없는 엉뚱한 서버에 접속하게 된다는 뜻이다.

그 결과로 대규모 cache miss가 발생하게 될 것이다.

안정 해시는 이 문제를 효과적으로 해결하는 기술이다.

안정 해시 (consistent hash)

안정 해시는 해시 테이블 크기가 조정될 때 평균적으로 오직 k(key의 개수)/n(slot의 개수)개의 키만 재배치하는 해시 기술이다.

이와 달리, 대부분의 전통적 해시 테이블은 슬롯의 수가 바뀌면 거의 대부분 키를 재배치한다.

해시 공간과 해시 링

안정 해시의 정의를 알아보았으니, 그 동작 원리를 살펴보자.

해시 함수 f로는 SHA-1을 사용한다고 하고, 그 함수의 출력 값 범위는 x0~xn과 같다고 하자.
SHA-1의 해시 공간 범위는 0부터 2^160 - 1까지라고 알려져 있다.

flowchart LR
    x0["x0"] --- x1["x1"] --- x2["x2"] --- xm["..."] --- xn["xn (2^160 - 1)"]

이 해시 공간의 양쪽을 구부려 접으면 해시 링이 만들어진다.

flowchart TB
    subgraph Hash Ring
        direction LR
        xn --- x0
        x0["x0"] --- x1["x1"]
        x1 --- x.["..."]
        x. --- xn["xn"]
    end

해시 서버

이 시 함수 f를 사용하면 서버 IP나 이름을 이 링 위에 대응시킬 수 있다.

flowchart TB
    subgraph Hash Ring
        direction LR
        xn --- x0
        x0["서버0"] --- x1["서버1"]
        x1 --- x2["서버2"]
        x2 --- xn["서버3"]
    end

해시 키

캐시할 키 또한 해시 링 위의 어느 지점에 배치할 수 있다.

flowchart TB
    subgraph Hash Ring
        direction LR
        k3((key3)) --- s3["서버3"] --- k0((key0))
        k0((key0)) --- s0["서버0"] --- k1((key1))
        k1((key1)) --- s1["서버1"] --- k2((key2))
        k2((key2)) --- s2["서버2"] --- k3((key3))
    end

서버 조회

어떤 키가 저장되는 서버는, 해당 키의 위치로부터 시계 방향으로 링을 탐색해 나가면서 만나는 첫 번째 서버다.

flowchart TB
    subgraph Hash Ring
        direction LR
        %% 순환 연결
        k3((key3)) --> s3["서버3"] --- k0((key0)) 
        k0((key0)) --> s0["서버0"] --- k1((key1)) 
        k1((key1)) --> s1["서버1"] --- k2((key2)) 
        k2((key2)) --> s2["서버2"] --- k3((key3))
        
        %% 키 → 서버 화살표
        k0 --- s0
        k1 --- s1
        k2 --- s2
        k3 --- s3
    end

서버 추가

서버를 추가하더라도 키 가운데 일부만 재배치하면 된다.

flowchart TB
    subgraph Hash Ring
        direction LR
        %% 순환 연결
        k3((key3)) --> s3["서버3"] --- k0((key0)) 
        k0((key0)) --> s4["서버4"] --- s0["서버0"] --- k1((key1)) 
        k1((key1)) --> s1["서버1"] --- k2((key2)) 
        k2((key2)) --> s2["서버2"] --- k3((key3))
        
        %% 키 → 서버 화살표
        k0 x-.-x s0
        k0 --- s4
        k1 --- s1
        k2 --- s2
        k3 --- s3
    end

서버4가 추가된 뒤에 key0는 서버 4에 저장된다.
key0의 위치에서 시계 방향으로 순회했을 때 처음으로 만나게 되는 서버는 서버4이기 때문이다.

서버 제거

하나의 서버가 제거되면 키 가운데 일부만 재배치된다.

flowchart TB
    subgraph Hash Ring
        direction LR
        k3((key3)) --- s3["서버3"] --- k0((key0))
        k0((key0)) --- s0["서버0"] --- k1((key1))
        k1 --> s2
        k1 --- k2((key2))
        k2((key2)) --- s2["서버2"] --- k3((key3))
    end

서버1이 삭제되었을 때 key1만이 서버2로 재배치되었다.

기본 구현법의 두 가지 문제

안정 해시 알고리즘의 절차는 아뢔와 같다.

  • 서버와 키를 균등 분포 해시 함수를 사용해 해시 링에 배치한다.
  • 키의 위치에서 링을 시계 방향으로 탐색하다 만나는 최초의 서버가 키가 저장될 서버다.

이 접근법에는 두 가지 문제가 있다.

  1. 서버가 추가되거나 삭제되는 상황을 감안하면 partition의 크기를 균등하게 유지하는 게 불가능하다.
    flowchart TB
     subgraph Hash Ring
         direction LR
         xn --- x0
         x0 <--> x2
         x0["서버0"] --- x1["X"]
         x1 --- x2["서버2"]
         x2 --- xn["서버3"]
     end
    
    • 만약 서버1이 삭제된다면 서버0~서버2 까지가 다른 partition 대비 거의 두 배로 커지게 된다.
    • partition: 인접한 서버 사이의 해시 공간
  2. 키의 균등 분포를 달성하기 어렵다.
    flowchart TB
     subgraph Hash Ring
         direction LR
         s3["서버3"] --- k3((key3)) --- k0((key0))
         k0((key0)) --- s0["서버0"] --- s1["서버1"]
         s1 --- k1((key1)) --- k2((key2))
         k2((key2)) --- s2["서버2"] --- s3
     end
    
    • 키가 서버1~서버2, 서버3~서버0 에 분포되어 있다면, 서버0과 서버3은 아무 데이터도 갖지 않을 것이다.

이 문제를 해결하기 위해 제안된 기법이 가상노드(virtual node) 또는 복제(replica) 기법이다.

가상 노드 (virtual node)

가상 노드는 실제 노드 또는 서버를 가리키는 노드로서, 하나의 서버는 링 위에 여러 개의 가상 노드를 가질 수 있다.

  • 서버0: 서버0_0, 서버0_1, 서버0_2
  • 서버1: 서버1_0, 서버1_1, 서버1_2
    flowchart TB
      subgraph Virtual Node
          direction LR
          x12 --- x00["서버0_0"]
          x00 --- x10("서버1_0")
          x10 --- x01["서버0_1"]
          x01 --- x11("서버1_1")
          x11 --- x02["서버0_2"]
          x02 --- x12("서버1_2")
      end
    

가상 노드의 개수를 늘리면 키의 분포는 점점 더 균등해진다.
표준 편자가 작아져서 데이터가 고르게 분포되기 때문이다.

재배치할 키 결정

서버가 추가되거나 제거되면 데이터 일부는 재배치해야 한다.

마치며

안정 해시가 왜 필요하며, 어떻게 동작하는지를 자세히 살펴보았다.

안정 해시의 이점은 아래와 같다.

  • 서버가 추가되거나 삭제될 때 재배치되는 키의 수가 최소화된다.
  • 데이터가 보다 균등하게 분포하게 되므로 수평적 규모 확장성을 달성하기 쉽다.
  • 데이터를 균등하게 분배하므로 핫스팟 키 문제를 줄인다.
    • 유명인의 데이터가 특정 샤드에 대한 접근이 지나치게 빈번하면 서버 과부하 문제가 생길 수 있다.
This post is licensed under CC BY 4.0 by the author.

© Yn3. Some rights reserved.