强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 8 章:一致性哈希

第 8 章:一致性哈希

8.1 为什么需要一致性哈希?

Memcached 是"无中心"架构——服务端节点之间互不通信。客户端通过哈希算法决定将 Key 路由到哪个节点。

朴素哈希的问题

# 朴素哈希:hash(key) % N
def get_node(key, num_nodes):
    return hash(key) % num_nodes

# 3 个节点时:
# key1 → hash(key1) % 3 = 1 → Node 2
# key2 → hash(key2) % 3 = 0 → Node 1
# key3 → hash(key3) % 3 = 2 → Node 3

# 问题:增加 1 个节点(3 → 4)后:
# key1 → hash(key1) % 4 = 3 → Node 4  ← 命中节点改变!
# key2 → hash(key2) % 4 = 0 → Node 1
# key3 → hash(key3) % 4 = 2 → Node 3
朴素哈希取模的问题:

节点数 N 变化时,几乎所有 Key 的路由都会改变
→ 大量缓存失效 → 数据库压力激增 → 可能导致雪崩

影响比例: 约 (N-1)/N 的 Key 会受影响
例: 从 3 节点 → 4 节点,约 75% 的 Key 路由改变

一致性哈希的目标

目标说明
单调性新增节点只影响部分 Key 的路由
平衡性各节点负载大致均匀
分散性同一 Key 不应被映射到不同节点
负载均衡节点变更时影响最小化

8.2 一致性哈希原理

哈希环

一致性哈希将整个哈希空间组织成一个虚拟的环(0 ~ 2^32-1):

              0
              │
    ┌─────────┼─────────┐
    │         │         │
    │   Node A│         │
    │    ·    │    ·    │
    │         │ Node B  │
    │         │         │
    │    ·    │         │
    │  Node C │         │
    │         │    ·    │
    │         │ Node D  │
    └─────────┼─────────┘
              │
         2^32-1

节点映射: hash(node_id) → 环上位置
Key 路由: hash(key) → 环上位置 → 顺时针找到的第一个节点

路由示例

环上节点位置(顺时针):
  0° ─── Node A (100°) ─── Node B (200°) ─── Node C (300°) ─── 360°

Key X → hash(X) = 150° → 顺时针找到 Node B
Key Y → hash(Y) = 250° → 顺时针找到 Node C
Key Z → hash(Z) = 50°  → 顺时针找到 Node A

节点增减的影响

移除 Node B:

  0° ─── Node A (100°) ─── Node C (300°) ─── 360°

  Key X (150°) → 顺时针找到 Node C(改变!)
  Key Y (250°) → 顺时针找到 Node C(不变)
  Key Z (50°)  → 顺时针找到 Node A(不变)

影响范围: 只有 Node B 负责的 Key 需要重新路由
影响比例: 约 1/N(而非朴素哈希的 (N-1)/N)

8.3 虚拟节点

问题:节点分布不均匀

真实场景中,少量节点在环上可能分布不均,导致负载倾斜。

虚拟节点解决方案

每个物理节点映射多个虚拟节点到环上:

物理节点 → 虚拟节点映射:

Node A → VA-1 (50°), VA-2 (150°), VA-3 (250°)
Node B → VB-1 (70°), VB-2 (170°), VB-3 (270°)
Node C → VC-1 (90°), VC-2 (190°), VC-3 (290°)

环上: 50° 70° 90° 150° 170° 190° 250° 270° 290°
       A   B   C    A    B    C    A    B    C

负载更均匀!

虚拟节点数量选择

虚拟节点数负载均衡性内存开销推荐
1最小不推荐
50较好小规模
150适中通用推荐
256很好适中大规模
1000+极好较大超大规模

8.4 哈希算法选择

常用算法对比

算法速度分布均匀性推荐
CRC32★★★★★★★★一般
FNV1a_32★★★★★★★★★推荐
MurmurHash3★★★★★★★★★强烈推荐
MD5★★★★★★★不推荐(太慢)
xxHash★★★★★★★★★★推荐

8.5 客户端实现

Python 实现

import hashlib
import bisect

class ConsistentHash:
    """一致性哈希实现(带虚拟节点)"""

    def __init__(self, nodes=None, vnodes=150):
        self.vnodes = vnodes
        self.ring = []        # 排序的哈希值列表
        self.ring_map = {}    # 哈希值 → 节点
        self.nodes = set()

        if nodes:
            for node in nodes:
                self.add_node(node)

    def _hash(self, key):
        """计算哈希值"""
        h = hashlib.md5(key.encode('utf-8')).hexdigest()
        return int(h, 16)

    def add_node(self, node):
        """添加节点"""
        self.nodes.add(node)
        for i in range(self.vnodes):
            vnode_key = f"{node}:vnode{i}"
            h = self._hash(vnode_key)
            bisect.insort(self.ring, h)
            self.ring_map[h] = node

    def remove_node(self, node):
        """移除节点"""
        self.nodes.discard(node)
        for i in range(self.vnodes):
            vnode_key = f"{node}:vnode{i}"
            h = self._hash(vnode_key)
            self.ring.remove(h)
            del self.ring_map[h]

    def get_node(self, key):
        """获取 Key 对应的节点"""
        if not self.ring:
            return None
        h = self._hash(key)
        idx = bisect.bisect_right(self.ring, h)
        if idx == len(self.ring):
            idx = 0
        return self.ring_map[self.ring[idx]]


# 使用示例
ch = ConsistentHash(
    nodes=["mc1:11211", "mc2:11211", "mc3:11211"],
    vnodes=150
)

# 路由 Key
print(ch.get_node("user:1001"))    # mc2:11211
print(ch.get_node("user:1002"))    # mc1:11211
print(ch.get_node("session:abc"))  # mc3:11211

# 添加新节点
ch.add_node("mc4:11211")
print(ch.get_node("user:1001"))    # 大概率不变

# 验证分布均匀性
from collections import Counter
dist = Counter(ch.get_node(f"key:{i}") for i in range(10000))
for node, count in sorted(dist.items()):
    print(f"{node}: {count} ({count/100:.1f}%)")

Go 实现

package consistent

import (
    "hash/crc32"
    "sort"
    "strconv"
    "sync"
)

type ConsistentHash struct {
    mu       sync.RWMutex
    ring     []uint32
    ringMap  map[uint32]string
    nodes    map[string]bool
    vnodes   int
}

func New(vnodes int) *ConsistentHash {
    return &ConsistentHash{
        ring:    make([]uint32, 0),
        ringMap: make(map[uint32]string),
        nodes:   make(map[string]bool),
        vnodes:  vnodes,
    }
}

func (ch *ConsistentHash) hash(key string) uint32 {
    return crc32.ChecksumIEEE([]byte(key))
}

func (ch *ConsistentHash) AddNode(node string) {
    ch.mu.Lock()
    defer ch.mu.Unlock()

    ch.nodes[node] = true
    for i := 0; i < ch.vnodes; i++ {
        h := ch.hash(node + ":vnode" + strconv.Itoa(i))
        ch.ring = append(ch.ring, h)
        ch.ringMap[h] = node
    }
    sort.Slice(ch.ring, func(i, j int) bool {
        return ch.ring[i] < ch.ring[j]
    })
}

func (ch *ConsistentHash) RemoveNode(node string) {
    ch.mu.Lock()
    defer ch.mu.Unlock()

    delete(ch.nodes, node)
    newRing := make([]uint32, 0)
    for _, h := range ch.ring {
        if ch.ringMap[h] != node {
            newRing = append(newRing, h)
        } else {
            delete(ch.ringMap, h)
        }
    }
    ch.ring = newRing
}

func (ch *ConsistentHash) GetNode(key string) string {
    ch.mu.RLock()
    defer ch.mu.RUnlock()

    if len(ch.ring) == 0 {
        return ""
    }
    h := ch.hash(key)
    idx := sort.Search(len(ch.ring), func(i int) bool {
        return ch.ring[i] >= h
    })
    if idx >= len(ch.ring) {
        idx = 0
    }
    return ch.ringMap[ch.ring[idx]]
}

8.6 各语言客户端的一致性哈希

libmemcached(C/C++ 库)

# libmemcached 使用 Ketama 一致性哈希算法
# 配置字符串中指定:
# --SERVER=mc1:11211 --SERVER=mc2:11211 --SERVER=mc3:11211 --HASH=WHEEL

PHP Memcached 扩展

<?php
$mc = new Memcached();

// 设置一致性哈希算法
$mc->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true);

// 添加服务器
$mc->addServers([
    ['mc1', 11211, 100],  // [host, port, weight]
    ['mc2', 11211, 100],
    ['mc3', 11211, 100],
]);

// 自动使用一致性哈希路由
$mc->set('user:1001', $userData, 3600);
$mc->get('user:1001');  // 自动路由到正确的节点

Java Spymemcached

import net.spy.memcached.MemcachedClient;
import net.spy.memcached.ConnectionFactoryBuilder;

// 使用 Ketama 一致性哈希
ConnectionFactoryBuilder builder = new ConnectionFactoryBuilder();
builder.setHashAlg(DefaultHashAlgorithm.KETAMA_HASH);
builder.setLocatorType(ConnectionFactoryBuilder.LocatorType.CONSISTENT);

MemcachedClient mc = new MemcachedClient(
    builder.build(),
    AddrUtil.getAddresses("mc1:11211 mc2:11211 mc3:11211")
);

Go gomemcache

import "github.com/bradfitz/gomemcache/memcache"

// gomemcache 默认使用一致性哈希
mc := memcache.New("mc1:11211", "mc2:11211", "mc3:11211")
// 自动使用 fnv32a 一致性哈希路由

8.7 Ketama 算法

Ketama 是最广泛使用的一致性哈希算法,最初由 Last.fm 开发。

特点

特性说明
虚拟节点每个物理节点 160 个虚拟节点
哈希算法MD5 生成 4 个 32 位哈希值(一次 MD5 = 4 个 vnode)
权重支持可通过 weight 参数控制节点的虚拟节点数
兼容性几乎所有 Memcached 客户端都支持

Ketama 算法伪代码

for each node:
    for i in 0..39:  # 40 次循环 × 4 个哈希 = 160 个虚拟节点
        digest = md5(f"{node}:{i}")
        for j in 0..3:
            hash = digest[j*4..j*4+4] as uint32
            ring.append(hash, node)
sort ring by hash

8.8 节点增减的缓存失效分析

数据迁移量

假设 N 个节点,总 Key 数为 K

新增 1 个节点:
  需要迁移的 Key ≈ K / (N + 1)
  缓存命中率下降 ≈ 1 / (N + 1)

移除 1 个节点:
  需要回源的 Key ≈ K / N
  缓存命中率下降 ≈ 1 / N

实际影响

场景节点变化缓存失效比例
3 → 4 节点新增 1 台~25%
10 → 11 节点新增 1 台~9%
50 → 51 节点新增 1 台~2%
3 → 2 节点故障 1 台~33%
10 → 9 节点故障 1 台~10%

8.9 最佳实践

节点扩容策略

推荐:预分配虚拟节点
1. 启动时配置比实际更多的节点地址
2. 先部署空节点
3. 逐步将流量切换到新节点
4. 观察命中率和 DB 压力

Key 设计与哈希

# 好的做法:Key 有明确前缀
"user:1001"      # 可控的哈希分布
"session:abc123" # 不同前缀分散到不同节点

# 不好的做法:Key 有相同前缀
"cache_1", "cache_2", "cache_3"  # 可能集中在同一节点

节点故障处理

class ResilientMemcached:
    def __init__(self, servers):
        self.healthy = set(servers)
        self.all_servers = servers
        self.mc = memcache.Client(servers)
        self.fail_counts = {}

    def get(self, key):
        try:
            value = self.mc.get(key)
            self.fail_counts[key] = 0
            return value
        except Exception as e:
            self.fail_counts[key] = self.fail_counts.get(key, 0) + 1
            if self.fail_counts[key] >= 3:
                # 标记节点不健康,移除
                self._mark_unhealthy(key)
            return None

    def _mark_unhealthy(self, key):
        # 更新一致性哈希环,临时移除故障节点
        pass

扩展阅读

小结

要点内容
核心原理哈希环 + 顺时针查找,节点增减只影响部分 Key
虚拟节点每个物理节点映射 150+ 虚拟节点到环上,保证负载均衡
推荐算法Ketama(业界标准)或 MurmurHash3
扩容影响增减 1 个节点影响约 1/N 的 Key
客户端大多数 Memcached 客户端内置一致性哈希支持