引言
雪花ID(Snowflake ID)作为分布式系统中广泛采用的唯一ID生成方案,在微服务架构中扮演着至关重要的角色。调研了百度的Uid-generator与美团的leaf算法,出于节约机器的考虑(对于公司来说leaf需要新部署zk节点),最终选择了百度的Uid-generator。
但我想要的是别的微服务通过rpc去调用百度的雪花id生成算法。这就存在一个问题:通过源码会发现,同一个host与port可能会对应多个服务,遂想改造一下百度的源码。
一、原生雪花ID生成原理
1.1 雪花ID位数分配
原生雪花ID采用64位长整型,具体位数分配如下:
diff
+------+----------------------+----------------+-----------+
| sign | delta seconds | worker node id | sequence |
+------+----------------------+----------------+-----------+
1bit 28bits 22bits 13bits
根据源码中的BitsAllocator
类实现:
java
public class DefaultUidGenerator implements UidGenerator, InitializingBean {
/** Bits allocate */
protected int timeBits = 28; // 时间戳位数:支持约8.7年
protected int workerBits = 22; // 工作节点ID位数:支持约420万个节点
protected int seqBits = 13; // 序列号位数:同一秒内支持8192个ID
/** Customer epoch, unit as second */
protected String epochStr = "2016-05-20";
protected long epochSeconds = TimeUnit.MILLISECONDS.toSeconds(1463673600000L);
}
1.2 核心生成算法
雪花ID的生成算法实现在DefaultUidGenerator.nextId()
方法中:
java
protected synchronized long nextId() {
long currentSecond = getCurrentSecond();
// 时钟回退检测
if (currentSecond < lastSecond) {
long refusedSeconds = lastSecond - currentSecond;
throw new UidGenerateException("Clock moved backwards. Refusing for %d seconds", refusedSeconds);
}
// 同一秒内序列号递增
if (currentSecond == lastSecond) {
sequence = (sequence + 1) & bitsAllocator.getMaxSequence();
// 序列号用尽,等待下一秒
if (sequence == 0) {
currentSecond = getNextSecond(lastSecond);
}
} else {
// 不同秒,序列号重置
sequence = 0L;
}
lastSecond = currentSecond;
// 分配位数生成最终ID
return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence);
}
1.3 WorkerId分配机制
原生实现通过DisposableWorkerIdAssigner
分配WorkerId:
java
private WorkerNodeEntity buildWorkerNode() {
WorkerNodeEntity workerNodeEntity = new WorkerNodeEntity();
if (DockerUtils.isDocker()) {
workerNodeEntity.setType(WorkerNodeType.CONTAINER.value());
workerNodeEntity.setHostName(DockerUtils.getDockerHost());
workerNodeEntity.setPort(DockerUtils.getDockerPort());
} else {
workerNodeEntity.setType(WorkerNodeType.ACTUAL.value());
workerNodeEntity.setHostName(NetUtils.getLocalAddress());
// 关键问题:使用时间戳+随机数作为端口标识
workerNodeEntity.setPort(System.currentTimeMillis() + "-" + RandomUtils.nextInt(100000));
}
return workerNodeEntity;
}
二、原生方案存在的问题
2.1 RPC服务架构下的WorkerId冲突风险
在我们的架构中,雪花ID生成被封装为独立的RPC服务,其他微服务通过RPC调用获取ID。在这种架构下,原生方案在Kubernetes环境中存在严重的WorkerId冲突问题:
架构背景:
markdown
┌─────────────┐ RPC调用 ┌─────────────────────┐
│ 用户服务 │ ──────────► │ 雪花ID生成RPC服务 │
└─────────────┘ │ (snowflake-api) │
┌─────────────┐ RPC调用 │ │
│ 订单服务 │ ──────────► │ 单独部署的服务 │
└─────────────┘ └─────────────────────┘
┌─────────────┐ RPC调用 │
│ 商品服务 │ ──────────► │
└─────────────┘ ▼
┌─────────────────┐
│ WORKER_NODE表 │
│ 管理WorkerId │
└─────────────────┘
问题根源:
- 单一RPC服务多实例部署:为保证高可用,雪花ID生成RPC服务需要部署多个实例
- IP地址相同:同一个Kubernetes Node上的多个RPC服务实例共享Node IP
- 端口标识重复 :
System.currentTimeMillis() + "-" + RandomUtils.nextInt(100000)
在多实例同时启动时极易产生相同值 - WorkerId重复分配:WORKER_NODE表通过host_name+port组合判断唯一性,重复组合导致多个实例使用相同WorkerId
影响分析:
sql
-- RPC服务实例可能出现的冲突情况
SELECT host_name, port, COUNT(*) as conflict_count
FROM WORKER_NODE
WHERE host_name LIKE '%snowflake-rpc%' -- RPC服务实例
GROUP BY host_name, port
HAVING conflict_count > 1;
-- 示例冲突记录(两个RPC服务实例)
id | host_name | port | type | created
1 | 192.168.1.100 | 1703123456789-1234 | 1 | 2024-01-01 10:00:00 -- RPC实例1
2 | 192.168.1.100 | 1703123456789-1234 | 1 | 2024-01-01 10:00:05 -- RPC实例2 冲突!
2.2 RPC架构下的业务影响
RPC服务中的WorkerId冲突将导致更严重的问题:
-
集中式ID重复风险:
- 多个RPC实例使用相同workerId,在相同时间戳下可能生成完全相同的ID
- 所有调用方微服务都可能收到重复ID,影响面广泛
-
高并发场景问题放大:
- RPC服务承载所有微服务的ID生成请求,并发量大
- 重复ID在高QPS场景下出现概率显著增加
-
故障影响面广:
- 单一RPC服务的WorkerId冲突会影响所有依赖的微服务
- 问题排查需要跨多个服务和调用链路
-
扩容运维复杂:
- RPC服务扩容时,新实例容易与现有实例产生WorkerId冲突
- 负载均衡下,同一个调用方可能从多个有冲突的实例获取ID
-
数据一致性风险:
- 重复ID可能导致用户数据、订单数据等核心业务数据的主键冲突
- 分布式事务场景下,重复ID可能造成严重的数据不一致问题
三、Kubernetes Pod感知优化方案
3.1 解决方案核心思想
针对RPC服务架构的特点,利用Kubernetes Pod Name在集群中的全局唯一性,确保雪花ID生成RPC服务的每个实例都拥有独一无二的WorkerId:
bash
# RPC服务Pod Name格式
<deployment-name>-<replicaset-hash>-<random-suffix>
# 雪花ID生成RPC服务示例
snowflake-rpc-service-7d4b8c9f6d-k8s9x # RPC服务实例1
snowflake-rpc-service-7d4b8c9f6d-m2n4p # RPC服务实例2
snowflake-rpc-service-6c5a7b8e9f-q1w3r # RPC服务实例3(扩容后)
# 架构优势
┌─────────────┐ RPC调用 ┌─────────────────────────────────┐
│ 用户服务 │ ──────────► │ snowflake-rpc-...-k8s9x │ WorkerId: 1
└─────────────┘ │ (唯一Pod标识) │
┌─────────────┐ RPC调用 ├─────────────────────────────────┤
│ 订单服务 │ ──────────► │ snowflake-rpc-...-m2n4p │ WorkerId: 2
└─────────────┘ │ (唯一Pod标识) │
┌─────────────┐ RPC调用 ├─────────────────────────────────┤
│ 商品服务 │ ──────────► │ snowflake-rpc-...-q1w3r │ WorkerId: 3
└─────────────┘ │ (扩容实例,唯一Pod标识) │
└─────────────────────────────────┘
3.2 核心实现:KubernetesPodAwareWorkerIdAssigner
新增的KubernetesPodAwareWorkerIdAssigner
类实现了Pod感知的WorkerId分配:
java
private String getPortIdentifier() {
// 1. 优先使用K8s Pod名称
String kubernetesPodName = System.getenv("KUBERNETES_POD_NAME");
if (StringUtils.isNotBlank(kubernetesPodName)) {
LOGGER.info("🎯 Using Kubernetes Pod name as port identifier: {}", kubernetesPodName);
return kubernetesPodName;
}
// 2. 其次使用HOSTNAME(在某些K8s环境中也是Pod名称)
String hostname = System.getenv("HOSTNAME");
if (StringUtils.isNotBlank(hostname) && !hostname.equals("localhost")) {
LOGGER.info("🔍 Using HOSTNAME as port identifier: {}", hostname);
return hostname;
}
// 3. 降级:使用服务名称+时间戳(确保唯一性)
String fallbackPort = serviceName + ":" + System.currentTimeMillis() + "-" + RandomUtils.nextInt(10000);
LOGGER.warn("⚠️ No Pod name found, using fallback port identifier: {}", fallbackPort);
return fallbackPort;
}
3.3 配置策略与自动降级
通过Spring Boot的条件配置实现智能策略选择:
java
@Bean
@Primary
@ConditionalOnProperty(
name = "qx.id.generator.worker.assigner.type",
havingValue = "kubernetes-pod-aware",
matchIfMissing = true // 设为默认策略
)
public WorkerIdAssigner kubernetesPodAwareWorkerIdAssigner() {
LOGGER.info("✅ Using KubernetesPodAwareWorkerIdAssigner for unique WorkerId assignment per Pod/Service");
// 环境检测
String kubernetesPodName = System.getenv("KUBERNETES_POD_NAME");
String hostname = System.getenv("HOSTNAME");
String kubernetesServiceHost = System.getenv("KUBERNETES_SERVICE_HOST");
if (kubernetesPodName != null) {
LOGGER.info("🎯 Detected Kubernetes Pod: {} - using as port identifier", kubernetesPodName);
} else if (kubernetesServiceHost != null) {
LOGGER.info("🔍 Kubernetes environment detected, using hostname: {} as port identifier", hostname);
} else {
LOGGER.info("🔧 Non-Kubernetes environment, will use service-based port identifier");
}
return new KubernetesPodAwareWorkerIdAssigner();
}
四、方案优势与效果
4.1 RPC服务唯一性保障
改进后的方案确保雪花ID生成RPC服务每个实例的WorkerId唯一性:
sql
-- 改进后的RPC服务数据库记录
SELECT id, host_name, port, type, created
FROM WORKER_NODE
WHERE port LIKE 'snowflake-rpc%' -- 只查看RPC服务实例
ORDER BY created;
-- 预期结果:每个RPC服务实例都有唯一标识
id | host_name | port | type | created
1 | 192.168.1.100 | snowflake-rpc-service-7d4b8c9f6d-k8s9x | 1 | 2024-01-01 10:00:00
2 | 192.168.1.100 | snowflake-rpc-service-7d4b8c9f6d-m2n4p | 1 | 2024-01-01 10:00:05
3 | 192.168.1.101 | snowflake-rpc-service-6c5a7b8e9f-q1w3r | 1 | 2024-01-01 10:01:00
-- 验证:不同RPC实例确保无冲突
SELECT host_name, port, COUNT(*) as instance_count
FROM WORKER_NODE
WHERE port LIKE 'snowflake-rpc%'
GROUP BY host_name, port
HAVING instance_count > 1;
-- 应该返回空结果,表示无冲突
4.2 配置简化
RPC服务应用配置:
yaml
# snowflake-rpc-service/application.yml
server:
port: 8080 # RPC服务端口
qx:
id:
generator:
worker:
assigner:
type: kubernetes-pod-aware # Pod感知WorkerId分配
# 数据源配置(指向WORKER_NODE表所在数据库)
datasource:
name: dataSource
spring:
application:
name: snowflake-rpc-service # RPC服务名称,降级时使用
datasource:
# 配置WORKER_NODE表所在的数据库连接
url: jdbc:mysql://mysql-server:3306/snowflake_db
username: snowflake_user
password: snowflake_pass
4.3 性能与兼容性
性能影响:
- ✅ 零性能损失:不增加额外网络调用
- ✅ 启动效率:仅在首次启动时进行WorkerId分配
- ✅ 运行时性能:与原有方案完全相同
兼容性保障:
- ✅ 无数据库改动:复用现有WORKER_NODE表结构
- ✅ 向后兼容:支持传统环境自动降级
- ✅ 多环境适配:同时支持K8s和传统部署
实测性能:4核8G机器下rpc服务单个生成ID请求QPS:3w/s
4.4 扩展性优势
场景 | 原生方案 | Pod感知方案 |
---|---|---|
RPC服务多实例 | ❌ 实例间可能WorkerId冲突 | ✅ 每个RPC实例独立WorkerId |
高可用部署 | ❌ 多实例启动时高冲突风险 | ✅ 3个及以上实例安全部署 |
自动扩缩容 | ❌ 扩容新实例冲突风险高 | ✅ 新RPC实例自动获得唯一WorkerId |
滚动更新 | ⚠️ 需手动避免冲突 | ✅ RPC实例替换时自动处理 |
负载均衡 | ❌ 冲突实例可能同时提供服务 | ✅ 所有实例WorkerId唯一,安全负载 |
故障恢复 | ⚠️ 重启可能产生新冲突 | ✅ 实例重启复用原WorkerId |
五、方案对比总结
5.1 技术对比
维度 | 原生uid-generator | 改进版Pod感知方案 |
---|---|---|
WorkerId分配 | IP+随机PORT | ✅ K8s Pod感知 + 智能降级 |
K8s环境支持 | ❌ 存在冲突风险 | ✅ 原生支持,零冲突 |
配置复杂度 | ⚠️ 需手动避免冲突 | ✅ 自动配置,开箱即用 |
故障排查 | ❌ 冲突问题隐蔽 | ✅ 完整的调试API |
环境适配 | ⚠️ 仅适配传统部署 | ✅ 多环境智能适配 |
5.2 业务价值
安全性提升:
- 消除WorkerId冲突导致的ID重复风险
- 保障分布式系统数据一致性
- 提供可靠的唯一性保证
运维效率:
- 简化RPC服务在Kubernetes环境下的配置工作
- 减少因WorkerId冲突导致的RPC服务故障排查成本
- 支持RPC服务自动化部署和无感知扩缩容
- 负载均衡下确保所有RPC实例安全提供服务
开发体验:
- 业务微服务调用RPC接口完全无感知,保持原有API兼容
- RPC服务提供丰富的调试和监控能力,便于运维
- 支持传统部署和容器化部署的无缝切换
- 为业务方提供可靠的分布式ID生成服务
总结
通过引入Kubernetes Pod感知机制,我们成功解决了传统雪花ID生成方案在容器化环境下的WorkerId冲突问题。该方案在保持原有性能和兼容性的基础上,显著提升了系统的可靠性和运维效率。
核心改进点:
- 利用Pod Name的全局唯一性替代不可靠的时间戳+随机数
- 智能环境检测和自动降级机制确保多环境兼容
- 零侵入式改进保持原有API和数据库结构不变
- 完整的监控和调试能力提升运维效率