分布式ID:基于K8s-PodName的百度雪花ID生成方案优化

引言

雪花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冲突将导致更严重的问题:

  1. 集中式ID重复风险

    • 多个RPC实例使用相同workerId,在相同时间戳下可能生成完全相同的ID
    • 所有调用方微服务都可能收到重复ID,影响面广泛
  2. 高并发场景问题放大

    • RPC服务承载所有微服务的ID生成请求,并发量大
    • 重复ID在高QPS场景下出现概率显著增加
  3. 故障影响面广

    • 单一RPC服务的WorkerId冲突会影响所有依赖的微服务
    • 问题排查需要跨多个服务和调用链路
  4. 扩容运维复杂

    • RPC服务扩容时,新实例容易与现有实例产生WorkerId冲突
    • 负载均衡下,同一个调用方可能从多个有冲突的实例获取ID
  5. 数据一致性风险

    • 重复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冲突问题。该方案在保持原有性能和兼容性的基础上,显著提升了系统的可靠性和运维效率。

核心改进点:

  1. 利用Pod Name的全局唯一性替代不可靠的时间戳+随机数
  2. 智能环境检测和自动降级机制确保多环境兼容
  3. 零侵入式改进保持原有API和数据库结构不变
  4. 完整的监控和调试能力提升运维效率
相关推荐
虾条_花吹雪29 分钟前
Using Spring for Apache Pulsar:Message Production
java·ai·中间件
tomorrow.hello32 分钟前
Java并发测试工具
java·开发语言·测试工具
Moso_Rx41 分钟前
javaEE——synchronized关键字
java·java-ee
张小洛1 小时前
Spring AOP 是如何生效的(入口源码级解析)?
java·后端·spring
DKPT1 小时前
Java设计模式之行为型模式(观察者模式)介绍与说明
java·笔记·学习·观察者模式·设计模式
追风少年浪子彦2 小时前
mapstruct与lombok冲突原因及解决方案
java·spring boot·spring·spring cloud
why技术2 小时前
也是出息了,业务代码里面也用上算法了。
java·后端·算法
她说人狗殊途2 小时前
java.net.InetAddress
java·开发语言
天使day2 小时前
Cursor的使用
java·开发语言·ai
咖啡进修指南2 小时前
代理模式——Java
java·代理模式