Garnet集群模式多节点分片、动态键迁移和负载重平衡实现

Garnet集群模式多节点分片、动态键迁移和负载重平衡实现详解

概述

Garnet集群模式采用Redis兼容的分布式设计,通过16384个哈希槽(Hash Slots)来实现数据分片,支持动态键迁移和自动负载重平衡。本文档详细解析Garnet集群模式的核心实现机制。

1. 多节点分片实现

1.1 哈希槽分片原理

Garnet使用固定数量的16384个哈希槽来分配数据,每个键都通过CRC16算法映射到特定的槽位:

// 哈希槽计算实现 (HashSlotUtils.cs)
public static unsafe ushort HashSlot(byte* keyPtr, int ksize)
{
    var startPtr = keyPtr;
    var end = keyPtr + ksize;

    // 查找第一个 '{'
    while (startPtr < end && *startPtr != '{')
        startPtr++;

    // 如果没有找到 '{',对整个键进行哈希
    if (startPtr == end) 
        return (ushort)(Hash(keyPtr, ksize) & 16383);

    var endPtr = startPtr + 1;
    // 查找第一个 '}'
    while (endPtr < end && *endPtr != '}') 
        endPtr++;

    // 如果没有找到 '}' 或者括号为空,对整个键进行哈希
    if (endPtr == end || endPtr == startPtr + 1) 
        return (ushort)(Hash(keyPtr, ksize) & 16383);

    // 对括号内的内容进行哈希
    return (ushort)(Hash(startPtr + 1, (int)(endPtr - startPtr - 1)) & 16383);
}

CRC16哈希算法

internal static unsafe ushort Hash(byte* data, int len)
{
    ushort result = 0;
    ref var crc16Base = ref MemoryMarshal.GetReference(Crc16Table);
    var end = data + len;
    
    while (data < end)
    {
        var index = (nuint)(uint)((result >> 8) ^ *data++) & 0xff;
        result = (ushort)(Unsafe.Add(ref crc16Base, index) ^ (result << 8));
    }
    return result;
}

1.2 槽位状态管理

每个哈希槽都有对应的状态,由HashSlot结构体管理:

// HashSlot.cs - 槽位状态定义
public enum SlotState : byte
{
    STABLE,     // 稳定状态,正常服务
    MIGRATING,  // 迁移中,数据正在移出
    IMPORTING,  // 导入中,数据正在移入
    OFFLINE     // 离线状态,不可用
}

[StructLayout(LayoutKind.Explicit)]
public struct HashSlot
{
    [FieldOffset(0)]
    public ushort _workerId;    // 工作节点ID

    [FieldOffset(2)]
    public SlotState _state;    // 槽位状态

    // 迁移状态下,workerId指向目标节点
    public ushort workerId => _state == SlotState.MIGRATING ? (ushort)1 : _workerId;
}

1.3 槽位分配算法

集群中16384个槽位需要均匀分配给各个主节点:

// 槽位映射初始化 (ShardedRespPerfBench.cs)
private void InitSlotMap()
{
    var nodes = clusterConfig.Nodes.ToArray();
    ushort nodeIndex = 0;
    
    foreach (var node in nodes)
    {
        var slotRanges = node.Slots;
        foreach (var slotRange in slotRanges)
        {
            for (int slot = slotRange.From; slot <= slotRange.To; slot++)
                slotMap[slot] = nodeIndex;  // 建立槽位到节点的映射
        }
        nodeIndex++;
    }
}

// 验证所有槽位都已分配
private void CheckAllSlotsCovered()
{
    var slotMap = new byte[16384];
    foreach (var node in clusterConfig.Nodes)
    {
        foreach (var slotRange in node.Slots)
        {
            for (int slot = slotRange.From; slot <= slotRange.To; slot++)
                slotMap[slot] = 1;
        }
    }

    for (int i = 0; i < slotMap.Length; i++)
    {
        if (slotMap[i] == 0)
            throw new Exception($"Slot {i} not covered");
    }
}

2. 动态键迁移实现

2.1 迁移状态转换

键迁移过程涉及复杂的状态转换,需要确保数据一致性:

// ClusterManagerSlotState.cs - 槽位迁移准备
public bool TryPrepareSlotForMigration(
    int slot,
    string targetNodeId,
    out int configEpoch)
{
    configEpoch = clusterProvider.clusterManager.CurrentConfig.GetLocalNodeConfigEpoch();
    
    if (!clusterProvider.serverOptions.EnableCluster)
        return false;

    var current = clusterProvider.clusterManager.CurrentConfig;
    
    // 验证槽位所有权
    if (!current.IsLocal((ushort)slot))
        return false;

    // 检查目标节点是否存在
    if (!current.TryGetWorker(targetNodeId, out var targetNodeRole))
        return false;

    // 确保目标节点不是副本
    if (targetNodeRole.IsReplica)
        return false;

    // 原子性地更新槽位状态为MIGRATING
    if (clusterProvider.clusterManager.TrySetSlotForMigration(slot, targetNodeId))
    {
        // 触发gossip协议广播配置变更
        clusterProvider.FlushConfig();
        configEpoch = clusterProvider.clusterManager.CurrentConfig.GetLocalNodeConfigEpoch();
        return true;
    }

    return false;
}

2.2 MIGRATE命令实现

Garnet使用原子性MIGRATE命令来传输数据:

// 迁移槽位数据 (ShardedRespOnlineBench.cs)
public static void MigrateSlots(
    ConnectionMultiplexer redis, 
    IPEndPoint source, 
    IPEndPoint target, 
    List<int> slots, 
    bool range = false, 
    ILogger logger = null)
{
    var server = redis.GetServer(source);
    List<object> args = new()
    {
        target.Address.ToString(),
        target.Port,
        "",     // 空字符串表示迁移整个槽位
        0,      // 目标数据库ID
        -1,     // 超时时间(-1表示无限制)
        range ? "SLOTSRANGE": "SLOTS"  // 迁移模式
    };
    
    // 添加要迁移的槽位列表
    foreach (var slot in slots)
        args.Add(slot);

    try
    {
        var resp = server.Execute("migrate", args);
        if (!resp.Equals("OK"))
            logger?.LogError("{errorMessage}", resp.ToString());
    }
    catch (Exception ex)
    {
        logger?.LogError(ex, "Migration failed");
    }
}

2.3 键标签支持

为了支持多键操作,Garnet实现了键标签(Key Tags)机制:

// 键标签处理示例
public string GenerateKeyInSlot(out int slot)
{
    slot = (randomGen ? keyRandomGen.Next(DbSize) : (keyIndex++ % DbSize)) & 16383;
    // 使用大括号标记键标签,确保相关键在同一槽位
    string keyStr = "{" + slotPrefixes[slot] + "}" + PadRandom(keyLen);
    return keyStr;
}

// 为所有槽位生成CRC前缀
private void GenerateCRCPrefixesForAllSlots()
{
    HashSet<int> slots = new();
    for (int i = 0; i < 16384; i++)
        slots.Add(i);
        
    slotPrefixes = new int[16384];
    while (slots.Count > 0)
    {
        int keyPrefix = keyRandomGen.Next(0, int.MaxValue);
        int slot = HashSlotUtils.HashSlot(Encoding.ASCII.GetBytes(keyPrefix.ToString()));
        if (slots.Contains(slot))
        {
            slotPrefixes[slot] = keyPrefix;
            slots.Remove(slot);
        }
    }
}

3. 负载重平衡机制

3.1 自动迁移触发

Garnet支持后台自动迁移任务来实现负载重平衡:

// 自动迁移任务 (ShardedRespOnlineBench.cs)
private async void MigrationBgTask()
{
    if (!opts.migrate) return;
    
    var redis = ConnectionMultiplexer.Connect(opts.GetConfig());
    var r = new Random(opts.MigrateSeed);
    
    await Task.Delay(opts.MigrateStartAfter);
    
    while (running)
    {
        try
        {
            InitiateMigration(redis, r);
            await Task.Delay(opts.MigrateFreq);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Migration task error: {ex.Message}");
        }
    }
}

3.2 迁移决策算法

private void InitiateMigration(ConnectionMultiplexer redis, Random r)
{
    // 获取主节点列表
    var nodes = primaryNodes.Where(x => x.Slots.Count > 0).ToArray();
    if (nodes.Length < 2) return;
    
    // 随机选择源节点和目标节点
    var sourceNode = nodes[r.Next(nodes.Length)];
    var targetNode = nodes[r.Next(nodes.Length)];
    
    if (sourceNode.NodeId.Equals(targetNode.NodeId)) return;
    
    var migratingSlots = new List<int>();
    
    // 从源节点的槽位中随机选择要迁移的槽位
    foreach (var slotRange in sourceNode.Slots)
    {
        for (int slot = slotRange.From; slot < slotRange.To; slot++)
        {
            if (r.Next(0, 2) > 0 && migratingSlots.Count < opts.MigrateBatch)
                migratingSlots.Add(slot);
        }
    }
    
    Console.WriteLine($"{sourceNode.Address}:{sourceNode.Port} > {targetNode.Address}:{targetNode.Port} slots:{migratingSlots.Count}");
    
    // 执行槽位迁移
    if (migratingSlots.Count > 0)
        MigrateSlots(redis, sourceNode.EndPoint, targetNode.EndPoint, migratingSlots);
}

3.3 Gossip协议同步

集群配置通过Gossip协议在节点间传播:

// 配置更新和同步
private async void PeriodicConfigUpdate()
{
    var redis = ConnectionMultiplexer.Connect(opts.GetConfig());
    
    while (running)
    {
        try
        {
            // 获取最新集群配置
            var newConfig = GetClusterConfig();
            
            // 更新本地槽位映射
            if (!newConfig.Equals(clusterConfig))
            {
                clusterConfig = newConfig;
                UpdateSlotMap(clusterConfig);
                
                // 重新初始化客户端连接
                var nodes = clusterConfig.Nodes.ToArray();
                InitClients(primaryNodes);
            }
            
            await Task.Delay(opts.ConfigRefresh);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Config update error: {ex.Message}");
        }
    }
}

4. 一致性保证机制

4.1 Epoch保护

使用epoch机制确保配置变更的原子性:

public bool TryPrepareSlotForMigration(int slot, string targetNodeId, out int configEpoch)
{
    // 获取当前配置epoch
    configEpoch = clusterProvider.clusterManager.CurrentConfig.GetLocalNodeConfigEpoch();
    
    // epoch递增,标记配置变更
    if (clusterProvider.clusterManager.TrySetSlotForMigration(slot, targetNodeId))
    {
        clusterProvider.FlushConfig();
        // 返回新的epoch
        configEpoch = clusterProvider.clusterManager.CurrentConfig.GetLocalNodeConfigEpoch();
        return true;
    }
    
    return false;
}

4.2 状态机保护

槽位状态转换遵循严格的状态机:

STABLE ──migrate──> MIGRATING ──complete──> STABLE (at target)
   │                    │
   └────offline────> OFFLINE
   
STABLE ──import──> IMPORTING ──complete──> STABLE

4.3 原子性操作

迁移过程中的关键操作都是原子性的:

// 原子性槽位状态更新
public bool TrySetSlotForMigration(int slot, string targetNodeId)
{
    lock (configLock)  // 确保配置修改的原子性
    {
        var currentSlot = slots[slot];
        if (currentSlot._state != SlotState.STABLE)
            return false;
            
        // 原子性地设置迁移状态
        slots[slot] = new HashSlot
        {
            _workerId = GetNodeId(targetNodeId),
            _state = SlotState.MIGRATING
        };
        
        configEpoch++;  // 递增配置版本
        return true;
    }
}

5. 性能优化策略

5.1 批量迁移

支持批量槽位迁移以提高效率:

public bool TryPrepareSlotsForMigration(
    HashSet<int> slots,
    string targetNodeId,
    out int configEpoch)
{
    configEpoch = clusterProvider.clusterManager.CurrentConfig.GetLocalNodeConfigEpoch();
    
    // 批量验证和准备槽位
    var localSlots = new List<int>();
    foreach (var slot in slots)
    {
        if (clusterProvider.clusterManager.CurrentConfig.IsLocal((ushort)slot))
            localSlots.Add(slot);
    }
    
    if (localSlots.Count == 0) return false;
    
    // 批量设置迁移状态
    foreach (var slot in localSlots)
    {
        if (!clusterProvider.clusterManager.TrySetSlotForMigration(slot, targetNodeId))
            return false;
    }
    
    clusterProvider.FlushConfig();
    configEpoch = clusterProvider.clusterManager.CurrentConfig.GetLocalNodeConfigEpoch();
    return true;
}

5.2 交错槽位分配

优化槽位分配以提高并行度:

// 交错槽位分配算法
private void InitInterleaveSlots()
{
    var primaryNodes = clusterConfig.Nodes.Where(n => !n.IsReplica && n.Slots.Count > 0).ToArray();
    
    LinkedList<int>[] shardSlots = new LinkedList<int>[primaryNodes.Length];
    
    // 为每个分片收集槽位
    for (int i = 0; i < shardSlots.Length; i++)
    {
        shardSlots[i] = new LinkedList<int>();
        foreach (var slotRange in primaryNodes[i].Slots)
        {
            for (int slot = slotRange.From; slot <= slotRange.To; slot++)
                shardSlots[i].AddLast(slot);
        }
    }
    
    // 交错分配,确保负载均衡
    int shardIndex = 0;
    for (int i = 0; i < interleavedSlots.Length; i++)
    {
        interleavedSlots[i] = shardSlots[shardIndex].First();
        shardSlots[shardIndex].RemoveFirst();
        shardSlots[shardIndex].AddLast(interleavedSlots[i]);
        shardIndex = (shardIndex + 1) % shardSlots.Length;
    }
}

6. 故障处理和恢复

6.1 节点故障检测

// 节点健康检查
private bool IsNodeHealthy(ClusterNode node)
{
    try
    {
        var connection = GetConnection(node.EndPoint);
        return connection.IsConnected && connection.Database.Ping().TotalMilliseconds < healthCheckTimeout;
    }
    catch
    {
        return false;
    }
}

6.2 故障转移

当主节点故障时,副本节点自动提升为主节点:

private async void HandleNodeFailure(ClusterNode failedNode)
{
    if (failedNode.IsReplica) return;
    
    // 查找可用的副本节点
    var replicas = clusterConfig.Nodes.Where(n => 
        n.IsReplica && n.ReplicaOf.Equals(failedNode.NodeId)).ToArray();
    
    if (replicas.Length > 0)
    {
        var newPrimary = replicas.OrderBy(r => r.ConfigEpoch).First();
        
        // 提升副本为主节点
        await PromoteReplicaToPrimary(newPrimary, failedNode.Slots);
        
        // 更新集群配置
        await BroadcastConfigUpdate();
    }
}

总结

Garnet的集群模式通过以下机制实现了高可用、高性能的分布式缓存:

  1. 哈希槽分片:使用16384个固定槽位和CRC16算法确保数据均匀分布
  2. 状态机管理:通过严格的状态转换保证迁移过程的一致性
  3. 原子性操作:使用epoch和锁机制确保配置变更的原子性
  4. Gossip协议:实现集群配置的最终一致性传播
  5. 批量优化:支持批量迁移和交错分配提高性能
  6. 故障恢复:自动故障检测和副本提升机制

这些机制共同构成了Garnet集群模式的核心实现,为大规模分布式缓存应用提供了可靠的技术基础。

posted @ 2025-08-18 14:48  MadLongTom  阅读(42)  评论(0)    收藏  举报