JiwonDev

μ•ˆμ • ν•΄μ‹œ(Consistent Hashing)

by JiwonDev

 

 

 

πŸ€ ν•΄μ‹œλ₯Ό μ‚¬μš©ν•΄ μš”μ²­μ„ λΆ„μ‚°ν•˜λŠ” 방법

N개의 μΊμ‹œ μ„œλ²„κ°€ μžˆμ„ λ•Œ λΆ€ν•˜λ₯Ό λ‚˜λˆ„λŠ” 보편적인 λ°©λ²•μœΌλ‘œλŠ” ν•΄μ‹œ ν•¨μˆ˜λ₯Ό 많이 μ‚¬μš©ν•œλ‹€.

MD5 (2^128): 속도가 λ³΄μ•ˆλ³΄λ‹€ μ€‘μš”ν•œ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜. SHA-256 에 λΉ„ν•΄ ν•΄μ‹œ 곡간은 μž‘λ‹€.
SHA-256 (2^256): 더 κΈ΄ ν•΄μ‹œ 크기와 더 κ°•λ ₯ν•œ μ•”ν˜Έν™” 속성. μ†λ„λŠ” MD5 에 λΉ„ν•΄ λŠλ¦¬λ‹€. 맀우 큰 ν•΄μ‹œ 곡간을 κ°–κΈ° λ•Œλ¬Έμ— 좩돌이 거의 λ°œμƒν•˜μ§€ μ•ŠλŠ”λ‹€.

 

serverIndex = hash(key) % N (μ„œλ²„κ°œμˆ˜)

 

μœ„μ™€ 같은 방법은 λͺ¨λ“  μƒν™©μ—μ„œ 잘 λ™μž‘ν•  것 κ°™μ§€λ§Œ μ„œλ²„ 개수 (server pool)이 λ³€κ²½λ˜κ±°λ‚˜ 데이터가 ν•œμͺ½μ— λͺ°λ¦¬κ²Œλ˜μ—ˆμ„ λ•Œ λ¬Έμ œκ°€ λ°œμƒν•œλ‹€. ν‚€λ₯Ό μ μ ˆν•˜κ²Œ 재배치 ν•˜μ§€ μ•ŠλŠ”λ‹€λ©΄ λΆ€ν•˜κ°€ λͺ°λ¦¬κ²Œ 되고 λŒ€κ·œλͺ¨μ˜ μΊμ‹œ 미슀(cache miss)κ°€ λ°œμƒν•  μˆ˜λ„ μžˆλ‹€.

μ²˜μŒμ—” λ¬Έμ œκ°€ μ—†μ—ˆκ² μ§€λ§Œ, μ§€κΈˆμ€ ν•΄μ‹œν‚€λ₯Ό 재배치 해주지 μ•ŠμœΌλ©΄ λ¬Έμ œκ°€ λ°œμƒν•œλ‹€.

 

이λ₯Ό λ°©μ§€ν•˜λ €λ©΄ μ„œλ²„κ°€ μΆ”κ°€/μ‚­μ œ λ˜λ”λΌλ„ key 맀핑 변경은 μ΅œμ†Œν™” λ˜μ–΄μ•Ό ν•œλ‹€. 이λ•Œ μ‚¬μš©ν•˜λŠ” 것이 λ°”λ‘œ "μ•ˆμ • ν•΄μ‹œ" 이닀.

μ•ˆμ •ν•΄μ‹œ(Consistent Hashing)
MITμ—μ„œ 처음 μ œμ•ˆ λ˜μ—ˆμœΌλ©° ν•΄μ‹œ ν…Œμ΄λΈ”μ˜ 크기가 변경될 λ•Œ, ν‰κ· μ μœΌλ‘œ 였직  k(ν‚€μ˜κ°œμˆ˜) / n(slot의 개수)  개의 ν‚€λ§Œ μž¬λ°°μΉ˜ν•˜λŠ” κΈ°μˆ μ΄λ‹€. μ•ˆμ •ν•΄μ‹œλ‘œ κ΅¬ν˜„ν•˜μ§€ μ•ŠλŠ” ν•΄μ‹œν•¨μˆ˜λ“€μ€ 데이터λ₯Ό λ„£λŠ” 슬둯의 μˆ˜κ°€ μ€„μ–΄κ±°λ‚˜ λŠ˜μ–΄λ‚˜λ©΄ λ§Žμ€ ν‚€κ°€ 재배치 λ˜μ•Ό ν•œλ‹€. 

 

 

 

πŸ€ μ•ˆμ •ν•΄μ‹œλŠ” μ–΄λ–»κ²Œ κ΅¬ν˜„ν• κΉŒ

예λ₯Ό λ“€μ–΄ ν•΄μ‹œ ν•¨μˆ˜λ‘œ SHA-1을 μ‚¬μš©ν•œλ‹€κ³  κ°€μ •ν•˜λ©΄, ν•΄μ‹œ 곡간은 0 <= x < 2^160 κΉŒμ§€ μ‚¬μš©ν•  수 μžˆλ‹€. 이λ₯Ό μ‹œμž‘κ³Ό 끝을 λΆ™μ΄κ²Œλ˜λ©΄ μ›ν˜• 리슀트둜 μ‚¬μš©ν•  수 μžˆλ‹€.

class ConsistentHash<T>(
    private val hashFunction: (String) -> Long, // μ‚¬μš©ν•  ν•΄μ‹œ ν•¨μˆ˜
    private val circle: SortedMap<Long, T> = TreeMap() // ν•΄μ‹œ 값을 ν‚€λ‘œ ν•˜κ³  λ…Έλ“œλ₯Ό κ°’μœΌλ‘œ ν•˜λŠ” μ •λ ¬λœ 맡
){ 
   /* ... */ 
}

// ν•΄μ‹œ ν•¨μˆ˜μ˜ 예제 κ΅¬ν˜„μž…λ‹ˆλ‹€. Java의 MessageDigestλ₯Ό μ‚¬μš©ν•΄ λ¬Έμžμ—΄μ˜ SHA-1 ν•΄μ‹œ 값을 μƒμ„±ν•©λ‹ˆλ‹€.
fun hashFunction(key: String): Long {
    val md = MessageDigest.getInstance("SHA-1")
    return md.digest(key.toByteArray())
             .fold(0L, { acc, byte -> (acc shl 8) + (byte and 0xff) })
}
    
fun main() {
    val consistentHash = ConsistentHash(::hashFunction)
}

 

μ΄λ ‡κ²Œ κ΅¬μ„±ν•œ μ›ν˜• 리슀트μ—μ„œ μ„œλ²„(IPλ‚˜ μ„œλ²„ 이름)을 μ €μž₯ν•˜κ³  μ‚¬μš©ν•  hash key 듀을 μ›ν•˜λŠ” μ–΄λŠ μ§€μ μ—λ‚˜ λ°°μΉ˜ν•  수 있게 κ΅¬μ„±ν•œλ‹€.

  • 이 λ•Œ 기쑴의 ν•΄μ‹œκ°€ μ•„λ‹Œ κ· λ“± 뢄포 (uniform distribution) ν•΄μ‹œ ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•΄μ„œ μ›ν˜•μ— 골고루 λΆ„ν¬μ‹œν‚¨λ‹€.

 

 

 

이런 μ‹μœΌλ‘œ μ›ν˜•μœΌλ‘œ κ΅¬μ„±ν•˜κ²Œλ˜λ©΄ μΆ”κ°€/μ‚­μ œμ—μ„œ 기쑴의 ν‚€ 맀핑 변경이 μ΅œμ†Œν™” λ˜λ„λ‘ κ΅¬ν˜„ν•  수 μžˆλ‹€.

  • μ„œλ²„ 쑰회
    ν•΄λ‹Ή hash keyλ‘œλΆ€ν„° μ‹œκ³„λ°©ν–₯으둜 μ„œλ²„λ₯Ό νƒμƒ‰ν•˜μ—¬ λ§Œλ‚œ 첫번째 μ„œλ²„λ₯Ό μ‘°νšŒν•œλ‹€.
  • μ„œλ²„ μΆ”κ°€
    μ„œλ²„λ₯Ό μΆ”κ°€ν•˜더라도 μ‹œκ³„λ°©ν–₯으둜 hash keyλ₯Ό μ‚¬μš©ν–ˆλ˜ κΈ°μ‘΄ 1개의 μ„œλ²„λ§Œ ν‚€κ°€ λ³€κ²½λœλ‹€. λ‹€λ₯Έ ν‚€λŠ” 재배치 λ˜μ§€ μ•ŠλŠ”λ‹€.
  • μ„œλ²„ μ‚­μ œ
    μ„œλ²„ μ‚­μ œλ„ λ§ˆμ°¬κ°€μ§€λ‘œ κΈ°μ‘΄ 쑰회 κ·œμΉ™μ— 따라 κ°€μš΄λ° μΌλΆ€λ§Œ ν‚€λ§Œ 재배치 되게 λœλ‹€. 

각 μ„œλ²„(λ…Έλ“œ) 사이에 μžˆλŠ” key만 μž¬λ°°μΉ˜ν•˜λ©΄ λœλ‹€. μ„œλ²„κ°€ μΆ”κ°€/μ‚­μ œ λ˜λ”λΌλ„ key μž¬λ°°μΉ˜κ°€ μ΅œμ†Œν™” λœλ‹€.

 

class ConsistentHash<T>(
    private val hashFunction: (String) -> Long, // ν•΄μ‹œ ν•¨μˆ˜
    private val circle: SortedMap<Long, T> = TreeMap() // ν•΄μ‹œ 값을 ν‚€λ‘œ ν•˜κ³  λ…Έλ“œλ₯Ό κ°’μœΌλ‘œ ν•˜λŠ” μ •λ ¬λœ 맡
) {
    fun addNode(node: T) {
        // λ…Έλ“œλ₯Ό ν•΄μ‹œλ§μ— μΆ”κ°€ν•©λ‹ˆλ‹€.
        val hash = hashFunction(node.toString())
        circle[hash] = node
    }

    fun removeNode(node: T) {
        // λ…Έλ“œλ₯Ό ν•΄μ‹œλ§μ—μ„œ μ œκ±°ν•©λ‹ˆλ‹€.
        val hash = hashFunction(node.toString())
        circle.remove(hash)
    }

    fun getNode(key: String): T? {
        // 주어진 킀에 λŒ€ν•œ λ…Έλ“œλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.
        if (circle.isEmpty()) return null
        
        val hash = hashFunction(key)
        
        // 주어진 ν•΄μ‹œ 값을 μ‘°νšŒν•˜μ§€ λͺ»ν•œ 경우, κ°€μž₯ κ°€κΉŒμš΄ λ‹€μŒ ν‚€λ₯Ό μ°ΎμŠ΅λ‹ˆλ‹€.
        if (!circle.containsKey(hash)) {
            // key 보닀 μˆœμ„œκ°€ κ°™κ±°λ‚˜ 큰 μ„œλΈŒ 맡 생성(tailMap)
            val tailMap = circle.tailMap(hash)
            // λ‹€μŒ μˆœμ„œκ°€ μ—†λ‹€λ©΄ ν•œ 바퀴 λŒμ•„ 첫번째 μˆœμ„œ(circle.firstKey()) μ‚¬μš©
            hash = tailMap.firstKey() ?: circle.firstKey()
        }
        return circle[hash]
    }
}

 

 

πŸ€ μ΄λ ‡κ²Œ κ΅¬ν˜„ν•œ μ•ˆμ •ν•΄μ‹œλŠ” 단점이 μ—†μ„κΉŒ?

  • μœ„ 그림처럼 각 μ„œλ²„ μ‚¬μ΄μ˜ 빈 곡간(partition)을 항상 κ· λ“±ν•˜κ²Œ μœ μ§€ν•˜λŠ”κ²Œ λΆˆκ°€λŠ₯ν•˜λ‹€.
  • κ· λ“± 뢄포 ν•΄μ‹œν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜λ”λΌλ„ ν‚€ 뢄포 μžμ²΄κ°€ ν•œμͺ½ 빈 곡간(partition)에 λͺ°λ¦΄ μˆ˜λ„ μžˆλ‹€.

κ·Έλƒ₯ ν•΄μ‹œλ₯Ό μ“°λŠ” 것 보단 λ‚«κ² μ§€λ§Œ, λ¬Έμ œκ°€ μ™„λ²½ν•˜κ²Œ ν•΄κ²°λœ 건 μ•„λ‹ˆλ‹€.

 

 

πŸ”‘ ν•΄κ²°μ±…: 가상 λ…Έλ“œ(virtual node) μ‚¬μš©

μ‹€μ œ μ„œλ²„(λ…Έλ“œ)λ₯Ό 1:1둜 μ‚¬μš©ν•˜μ§€ μ•Šκ³ , κ°€μƒμ˜ λ…Έλ“œλ₯Ό λ§Œλ“€μ–΄μ„œ μ‚¬μš©ν•œλ‹€. μ„œλ²„ 1λŒ€κ°€ μ—¬λŸ¬κ°œμ˜ 가상 λ…Έλ“œλ₯Ό κ°€μ§ˆ 수 μžˆλ‹€.

  • 가상 λ…Έλ“œμ˜ 개수λ₯Ό λŠ˜λ¦ΌμœΌλ‘œμ„œ ν‚€μ˜ 뢄포λ₯Ό κ· λ“±ν•˜κ²Œ λ§Œλ“€ 수 μžˆλ‹€.
  • λ…Έλ“œμ˜ κ°œμˆ˜κ°€ λ§Žμ„ 수둝 ν‘œμ€€νŽΈμ°¨κ°€ 적어져 더 κ· λ“±ν•΄μ§€κ² μ§€λ§Œ 그만큼 λ©”λͺ¨λ¦¬κ°€ ν•„μš”ν•˜κ³  μΆ”κ°€/μ‚­μ œμ‹œ κ°€μƒλ…Έλ“œλ₯Ό 많이 정리해μ•Όν•œλ‹€. νŠΈλ ˆμ΄λ“œ μ˜€ν”„λ₯Ό κ³ λ €ν•΄μ•Όν•œλ‹€.
  • 참고둜 가상 λ…Έλ“œμ˜ μˆ˜λŠ” λ¬΄ν•œμ • 늘릴 수 μ—†λ‹€. ν•„μš”μ— 따라 전체 λ…Έλ“œ μˆ˜λŠ” 쀄이고 μ„±λŠ₯이 쒋은 μ„œλ²„κ°€ λ§Žμ€ λ…Έλ“œ 수λ₯Ό κ°€μ Έκ°€κ²Œλ” κ΅¬ν˜„ν•  μˆ˜λ„ μžˆλ‹€.

// 가상 λ…Έλ“œλ₯Ό λ‚˜νƒ€λ‚΄λŠ” 클래슀
class VirtualNode<T>(
    private val physicalNode: T,
    private val replicaIndex: Int
) {
    fun getPhysicalNode(): T = physicalNode // 물리 λ…Έλ“œ (μ‹€μ œ μ„œλ²„)

    fun getKey(): String = "$physicalNode#$replicaIndex" // 가상 λ…Έλ“œμ˜ 고유 ν‚€(ν•΄μ‹œμ— μ‚¬μš©)

    fun isVirtualOf(node: T): Boolean = this.physicalNode == node

    override fun toString(): String = getKey()
}
class ConsistentHash<T>(
    private val hashFunction: (String) -> Long,
    private val numberOfReplicas: Int,
    private val circle: SortedMap<Long, VirtualNode<T>> = TreeMap()
) {
    fun addNode(node: T) {
        for (i in 0 until numberOfReplicas) {
            val virtualNode = VirtualNode(node, i)
            circle[hashFunction(virtualNode.getKey())] = virtualNode
        }
    }

    fun removeNode(node: T) {
        circle.entries.removeIf { it.value.isVirtualOf(node) }
    }

    fun getNode(key: String): T? {
        if (circle.isEmpty()) return null

        var hash = hashFunction(key)
        if (!circle.containsKey(hash)) {
            val tailMap = circle.tailMap(hash)
            hash = tailMap.firstKey() ?: circle.firstKey()
        }
        return circle[hash]?.getPhysicalNode()
    }
}

 

μ΄λ ‡κ²Œ κ΅¬μ„±ν•¨μœΌλ‘œμ„œ μ•„λž˜μ™€ 같은 이점을 μ±™κΈΈ 수 μžˆλ‹€.

  • μ„œλ²„ μΆ”κ°€/μ‚­μ œμ—λ„ ν‚€ 맀핑 변경이 μ΅œμ†Œν™”λœλ‹€. cache miss λΉ„μœ¨μ„ 쀄일 수 μžˆλ‹€.
  • μ„œλ²„ μΆ”κ°€/μ‚­μ œλ₯Ό ꡉμž₯히 많이 ν•˜λ”λΌλ„ ν•œμͺ½μœΌλ‘œ λͺ°λ¦¬μ§€ μ•Šκ³  데이터가 κ· λ“±ν•˜κ²Œ λΆ„ν¬λœλ‹€.
  • μœ„ μž₯점으둜 μˆ˜ν‰μ  규λͺ¨ ν™•μž₯이 μ‰¬μ›Œμ§€κ³  hot spot key (ν•œμͺ½μœΌλ‘œ ν‚€κ°€ λͺ°λ¦¬λŠ” ν˜„μƒ)을 μ΅œμ†Œν™” ν•  수 μžˆλ‹€.

 

 

πŸ€ 수치적으둜 μ•ˆμ •ν•΄μ‹œλŠ” μ–Όλ§ˆλ‚˜ μ’‹μ•„μ§€λŠ”κ±ΈκΉŒ?

μ‹€ν—˜ 좜처: https://songkg7.github.io/posts/Consistent-Hashing/


λΌμš°νŒ…μ„ λ§Œλ“ λ‹€κ³  μƒκ°ν•΄λ³΄μž. μ—¬κΈ°μ—μ„œ key λŠ” request url, ip 등이 될 것이고 킀에 따라 μ μ ˆν•œ μ„œλ²„λ₯Ό μ°Ύμ•„μ£Όμ–΄μ•Όν•œλ‹€.

  • 이 λ•Œ key(μš”μ²­)에 따라 μ–΄λ””λ‘œ λΌμš°νŒ… λ˜μ—ˆλŠ”μ§€ 캐싱해둔닀. 이후 동일 μš”μ²­μ€ λΌμš°νŒ… 결정과정을 μƒλž΅ν•˜κ³  λ°”λ‘œ μ‘°νšŒν•œλ‹€.
  • μΊμ‹œκ°€ μ—†κ±°λ‚˜ ν•΄λ‹Ή cache λ₯Ό μ‚¬μš©ν–ˆλŠ”λ° 정보가 μœ νš¨ν•˜μ§€ μ•Šμ€ 경우 (λ…Έλ“œκ°€ λ³€κ²½λœ 경우) λ‹€μ‹œ λΌμš°νŒ… 결정과정을 κ±°μΉœλ‹€.

ChatGPT의 μΉœμ ˆν•œ μ„€λͺ…

 

 

 

# κ·Έλƒ₯ ν•΄μ‹œλ₯Ό μ‚¬μš©ν•΄μ„œ κ΅¬ν˜„ν–ˆμ„ λ•Œ

κ·Έλƒ₯ ν•΄μ‹œ ( https://songkg7.github.io/posts/Consistent-Hashing/ )

 

 

# μ•ˆμ •ν•΄μ‹œλ₯Ό μ‚¬μš©ν–ˆμ„ λ•Œ

μ•ˆμ •ν•΄μ‹œ(λ…Έλ“œ1:1)

 

 

# μ•ˆμ •ν•΄μ‹œλ₯Ό μ‚¬μš©ν•˜κ³ , 가상 λ…Έλ“œ 수λ₯Ό 늘렀 ν‘œμ€€νŽΈμ°¨λ₯Ό μ€„μ˜€μ„ λ•Œ

  • 가상 λ…Έλ“œ 수λ₯Ό 늘리면 ν™•μ‹€νžˆ 더 골고루 λΆ„μ‚°λœλ‹€. λ‹€λ§Œ 가상 λ…Έλ“œ κ°œμˆ˜κ°€ μ–΄λŠμ •λ„ 차게되면 이후 μˆ˜μ‹­λ§Œκ°œλ₯Ό λŠ˜λ €λ„ κ²°κ³ΌλŠ” 큰 λ³€ν™”κ°€ μ—†λ‹€λŠ”κ±Έ μΈμ§€ν•˜μž. 이것도 νŠΈλ ˆμ΄λ“œ μ˜€ν”„λ₯Ό κ³ λ €ν•΄μ•Όν•œλ‹€. 

μ•ˆμ •ν•΄μ‹œ( λ…Έλ“œ1:1 λ…Έλ“œ1:10 λ…Έλ“œ1:100 )

 

 

 

πŸ€ μ΄λŸ¬ν•œ κΈ°μˆ λ“€μ€ μ‹€μ œλ‘œ 어디에 많이 μ“°μΌκΉŒ?

 

Choosing the Right DynamoDB Partition Key | Amazon Web Services

September 2022: This post was reviewed and updated for accuracy. This blog post covers important considerations and strategies for choosing the right partition key for designing a schema that uses Amazon DynamoDB. Choosing the right partition key is an imp

aws.amazon.com

 

 

 

μ΄λ ‡κ²Œ λŒ€κ·œλͺ¨ μΌ€μ΄μŠ€κ°€ μ•„λ‹ˆλ”λΌλ„ λ‹Ήμž₯ μ‚¬μš©μžμˆ˜κ°€ λ§Žμ•„μ Έ DB μŠ€μΌ€μΌμ—…μ„ κ³ λ €ν•΄μ•Όν•œλ‹€λ©΄ νŒŒν‹°μ…”λ‹μ„ν• μ§€, 샀딩을 할지 κ³ λ―Όν•˜κ³  ν•œλ‹€λ©΄ μ–΄λ–€ keyλ₯Ό κΈ°μ€€μœΌλ‘œ λ‚˜λˆ μ•Όν• μ§€λ₯Ό κ³ λ―Όν•΄λ³΄μ•„μ•Όν•œλ‹€. MiRO μ—”μ§€λ‹ˆμ–΄λ§ - 데이터 샀딩 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•œ μ μ ˆν•œ ν•΄μ‹œν•¨μˆ˜ 선택

λ‹Ήμ—°ν•œ λ§μ΄μ§€λ§Œ Hashκ°€ 정닡이 아닐 μˆ˜λ„ μžˆλ‹€. ν•˜μ§€λ§Œ 이런 방법듀을 μ•Œκ³  μžˆλŠ”κ²Œ λΆ„λͺ… 선택에 도움이 될 것이닀.

 

λΈ”λ‘œκ·Έμ˜ 정보

JiwonDev

JiwonDev

ν™œλ™ν•˜κΈ°