美团Leaf—唯一id生成工具解析
美团Leaf 分布式uuid生成方式
前言
关于为什么叫leaf
There are no two identical leaves in the world.
世界上没有两片完全相同的树叶。
Leaf默认提供了HTTP方式暴露生成唯一id的端口,在部署Leaf完成之后,可以直接通过HTTP接口调用的方式获取生成的唯一ID
当然,如果想要获得更好的性能,可以通过RPC方式暴露,不过这就需要自己基于Leaf-core
依赖进行封装。
Leaf提供了两种uuid的实现方式:
- Segment号段模式
- SnakeFlake雪花算法模式
使用方式
官方文档写的很直观,这里不再赘述,直接贴出链接
https://github.com/Meituan-Dianping/Leaf/blob/master/README_CN.md
什么是雪花算法SnakeFlake?
可以参考分布式唯一ID生成—雪花算法
SnakeFlake方式架构
雪花算法需要workerID来唯一表示不同机器,可以给每台机器手动配置,但如果机器过多,手动注册是十分不方便的,所以leaf基于ZK做了workerID的获取。
并且leaf-snakeFlake弱依赖于ZK,即从ZK上获取workerID的时候,会本地也缓存一份workerID文件
时钟问题
参见上图整个启动流程图,服务启动时首先检查自己是否写过ZooKeeper leaf_forever
节点:
- 若写过,则用自身系统时间与
leaf_forever
节点记录时间做比较,若小于leaf_forever
时间则认为机器时间发生了大步长回拨,服务启动失败并报警。 - 若未写过,证明是新服务节点,直接创建持久节点
leaf_forever
并写入自身系统时间,接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary
下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IP:Port
,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize
。
若abs( 系统时间-sum(time)/nodeSize )
< 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary维持租约。否则认为本机系统时间发生大步长偏移,启动失败并报警。 - 每隔一段时间(3s)上报自身系统时间写入leaf_forever。
- 由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回
ERROR_CODE
,等时钟追上即可。 - 或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警。
SnakeFlake方式—初始化环境
Leaf的SnakeFlake算法基于ZK实现,所以在服务启动的时候,需要先初始化ZK的环境。
public SnowflakeIDGenImpl(String zkAddress, int port, long twepoch) {
this.twepoch = twepoch;
Preconditions.checkArgument(timeGen() > twepoch, "Snowflake not support twepoch gt currentTime");
final String ip = Utils.getIp();
SnowflakeZookeeperHolder holder = new SnowflakeZookeeperHolder(ip, String.valueOf(port), zkAddress);
LOGGER.info("twepoch:{} ,ip:{} ,zkAddress:{} port:{}", twepoch, ip, zkAddress, port);
boolean initFlag = holder.init();
if (initFlag) {
workerId = holder.getWorkerID();
LOGGER.info("START SUCCESS USE ZK WORKERID-{}", workerId);
} else {
Preconditions.checkArgument(initFlag, "Snowflake Id Gen is not init ok");
}
Preconditions.checkArgument(workerId >= 0 && workerId <= maxWorkerId, "workerID must gte 0 and lte 1023");
}
SnakeFlow方式—核心逻辑
@Override
public synchronized Result get(String key) {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
try {
wait(offset << 1);
timestamp = timeGen();
if (timestamp < lastTimestamp) {
return new Result(-1, Status.EXCEPTION);
}
} catch (InterruptedException e) {
LOGGER.error("wait interrupted");
return new Result(-2, Status.EXCEPTION);
}
} else {
return new Result(-3, Status.EXCEPTION);
}
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
//seq 为0的时候表示是下一毫秒时间开始对seq做随机
sequence = RANDOM.nextInt(100);
timestamp = tilNextMillis(lastTimestamp);
}
} else {
//如果是新的ms开始
sequence = RANDOM.nextInt(100);
}
lastTimestamp = timestamp;
long id = ((timestamp - twepoch) << timestampLeftShift) | (workerId << workerIdShift) | sequence;
return new Result(id, Status.SUCCESS);
}
leaf会定期(间隔周期是3秒)上报更新timestamp。并且上报时,如果发现当前时间戳少于最后一次上报的时间戳,那么会放弃上报。之所以这么做的原因是,防止在leaf实例重启过程中,由于时钟回拨导致可能产生重复ID的问题
private void ScheduledUploadData(final CuratorFramework curator, final String zk_AddressNode) {
Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "schedule-upload-time");
thread.setDaemon(true);
return thread;
}
}).scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
updateNewData(curator, zk_AddressNode);
}
}, 1L, 3L, TimeUnit.SECONDS);//每3s上报数据
}
参考
https://tech.meituan.com/2017/04/21/mt-leaf.html
更多推荐
所有评论(0)