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的集群模式通过以下机制实现了高可用、高性能的分布式缓存:
- 哈希槽分片:使用16384个固定槽位和CRC16算法确保数据均匀分布
- 状态机管理:通过严格的状态转换保证迁移过程的一致性
- 原子性操作:使用epoch和锁机制确保配置变更的原子性
- Gossip协议:实现集群配置的最终一致性传播
- 批量优化:支持批量迁移和交错分配提高性能
- 故障恢复:自动故障检测和副本提升机制
这些机制共同构成了Garnet集群模式的核心实现,为大规模分布式缓存应用提供了可靠的技术基础。
本文来自博客园,作者:MadLongTom,转载请注明原文链接:https://www.cnblogs.com/madtom/p/19044677
浙公网安备 33010602011771号