์์ ํด์(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๊ฐ์ ์๋ฒ๋ง ํค๊ฐ ๋ณ๊ฒฝ๋๋ค. ๋ค๋ฅธ ํค๋ ์ฌ๋ฐฐ์น ๋์ง ์๋๋ค. - ์๋ฒ ์ญ์
์๋ฒ ์ญ์ ๋ ๋ง์ฐฌ๊ฐ์ง๋ก ๊ธฐ์กด ์กฐํ ๊ท์น์ ๋ฐ๋ผ ๊ฐ์ด๋ฐ ์ผ๋ถ๋ง ํค๋ง ์ฌ๋ฐฐ์น ๋๊ฒ ๋๋ค.

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 (ํ์ชฝ์ผ๋ก ํค๊ฐ ๋ชฐ๋ฆฌ๋ ํ์)์ ์ต์ํ ํ ์ ์๋ค.
๐ ์์น์ ์ผ๋ก ์์ ํด์๋ ์ผ๋ง๋ ์ข์์ง๋๊ฑธ๊น?

๋ผ์ฐํ
์ ๋ง๋ ๋ค๊ณ ์๊ฐํด๋ณด์. ์ฌ๊ธฐ์์ key ๋ request url, ip ๋ฑ์ด ๋ ๊ฒ์ด๊ณ ํค์ ๋ฐ๋ผ ์ ์ ํ ์๋ฒ๋ฅผ ์ฐพ์์ฃผ์ด์ผํ๋ค.
- ์ด ๋ key(์์ฒญ)์ ๋ฐ๋ผ ์ด๋๋ก ๋ผ์ฐํ ๋์๋์ง ์บ์ฑํด๋๋ค. ์ดํ ๋์ผ ์์ฒญ์ ๋ผ์ฐํ ๊ฒฐ์ ๊ณผ์ ์ ์๋ตํ๊ณ ๋ฐ๋ก ์กฐํํ๋ค.
- ์บ์๊ฐ ์๊ฑฐ๋ ํด๋น cache ๋ฅผ ์ฌ์ฉํ๋๋ฐ ์ ๋ณด๊ฐ ์ ํจํ์ง ์์ ๊ฒฝ์ฐ (๋ ธ๋๊ฐ ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ) ๋ค์ ๋ผ์ฐํ ๊ฒฐ์ ๊ณผ์ ์ ๊ฑฐ์น๋ค.

# ๊ทธ๋ฅ ํด์๋ฅผ ์ฌ์ฉํด์ ๊ตฌํํ์ ๋

# ์์ ํด์๋ฅผ ์ฌ์ฉํ์ ๋

# ์์ ํด์๋ฅผ ์ฌ์ฉํ๊ณ , ๊ฐ์ ๋ ธ๋ ์๋ฅผ ๋๋ ค ํ์คํธ์ฐจ๋ฅผ ์ค์์ ๋
- ๊ฐ์ ๋ ธ๋ ์๋ฅผ ๋๋ฆฌ๋ฉด ํ์คํ ๋ ๊ณจ๊ณ ๋ฃจ ๋ถ์ฐ๋๋ค. ๋ค๋ง ๊ฐ์ ๋ ธ๋ ๊ฐ์๊ฐ ์ด๋์ ๋ ์ฐจ๊ฒ๋๋ฉด ์ดํ ์์ญ๋ง๊ฐ๋ฅผ ๋๋ ค๋ ๊ฒฐ๊ณผ๋ ํฐ ๋ณํ๊ฐ ์๋ค๋๊ฑธ ์ธ์งํ์. ์ด๊ฒ๋ ํธ๋ ์ด๋ ์คํ๋ฅผ ๊ณ ๋ คํด์ผํ๋ค.

๐ ์ด๋ฌํ ๊ธฐ์ ๋ค์ ์ค์ ๋ก ์ด๋์ ๋ง์ด ์ฐ์ผ๊น?
- ์๋ง์กด ๋ค์ด๋๋ชจ ๋ฐ์ดํฐ๋ฒ ์ด์ค(DynamoDB)์ ํํฐ์
๋ ๊ด๋ จ ์ปดํฌ๋ํธ
Dynamo๋ ๋ฆฌ๋๊ฐ ์๋ ๋ณต์ ์์คํ ์ผ๋ก ๋ชจ๋ ๋ ธ๋์์ ์ฐ๊ธฐ ์์ฒญ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค. ๋ฐ์ดํฐ๋ฅผ ์ฌ๋ฌ ํํฐ์ ์ ๋ถ๋ฐฐํด์ ์ฌ์ฉํ ๋ ์์ ํด์๋ฅผ ์ด์ฉํด ๋ฐ์ดํฐ ๋ฆฌ๋ฐธ๋ฐ์ฑ์ ์ต์ํํ๋ค. (๋ค์ด๋๋ชจ๊ฐ ์ด๋ป๊ฒ ๊ตฌ์ฑ๋์๋์ง ๊ถ๊ธํ๋ค๋ฉด > ์ข์๊ธ1 / ์ข์๊ธ2 ) - ์ํ์น ์นด์ฐ๋๋ผ(Apache Cassandra) ํด๋ฌ์คํฐ์์์ ๋ฐ์ดํฐ ํํฐ์
๋
๋ฐ์ดํฐ๋ฅผ ํด๋ฌ์คํฐ ๋ด์ ์ฌ๋ฌ ๋ ธ๋์ ๋ถ์ฐ์ํค๋๋ฐ ํํฐ์ ๋ ํค๋ฅผ ํด์ฑํ ๋ ์์ ํด์๋ฅผ ์ฌ์ฉํ๋ค. - ๋์ค์ฝ๋(Discord)์ฑํ
์ดํ๋ฆฌ์ผ์ด์
- Elixir๋ฅผ ๋์ ์ฌ์ฉ์ ์ค๋ฐฑ๋ง๋ช
์ผ๋ก ํ์ฅํ ๋ฐฉ๋ฒ
์๋ก์ด ์ ์ ๊ฐ ๋ค์ด์์ ๋, ์ ์ธ๊ณ์ ๊ฑฐ์น ์๋ฒ ํด๋ฌ์คํฐ ๋ด์์ ์ฑ๋, ์ฌ์ฉ์ ๊ทธ๋ฃน์ ํน์ ๋ ธ๋์ ํ ๋นํ์ฌ ์ฌ์ฉํ๋ค. ์ด ๋ ์์ ํด์๋ฅผ ์ฌ์ฉํ์ฌ ๋ ธ๋์ ์ถ๊ฐ/์ญ์ ์ ๋ฐ๋ฅธ ๋ฆฌ๋ฐธ๋ฐ์ฑ์ ์ต์ํํ๋ค. - ์์นด๋ง์ด(Akamai) CDN
์ฌ์ฉ์์ ์์ฒญ์ ์ ์ธ๊ณ์ ๋ถ์ฐ๋ ์บ์(์ปจํ ์ธ ) ์๋ฒ์ ํจ์จ์ ์ผ๋ก ๋ผ์ฐํ ํ๋ค. ์๋ฒ๊ฐ ์ถ๊ฐ/์ญ์ ๋๋๋ผ๋ ์์ฒญ ๋ฆฌ๋ฐธ๋ฐ์ฑ์ด ์ต์ํ ๋๋๋ก ์์ ํด์๋ฅผ ์ฌ์ฉํ๋ค. - ๋งค๊ทธ๋ํ(Meglev) ๋คํธ์ํฌ ๋ก๋ ๋ฐธ๋ฐ์
๊ตฌ๊ธ์ด ๋ง๋ ๋คํธ์ํฌ ๋ก๋๋ฐธ๋ฐ์๋ก ํธ๋ํฝ์ ๊ท ๋ฑํ๊ฒ ๋ถ์ฐ์ํจ๋ค.
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 ์์ง๋์ด๋ง - ๋ฐ์ดํฐ ์ค๋ฉ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ ์ ์ ํ ํด์ํจ์ ์ ํ

๋ธ๋ก๊ทธ์ ์ ๋ณด
JiwonDev
JiwonDev