Kubernetes
弹性扩容,自动重启等场景,无需维护现在雪花算法中使用的的WorkerID
Leaf-segment数据库方案
(业务中不可接受出现连续ID可跳过)SQL
)DB宕机
会造成整个系统不可用,有做缓存号段优化(双buffer优化[1])在架构中允许多个发号器实例,使用同一个库中的分配表biz_tag
用来区分业务,max_id
表示该biz_tag
目前所被分配的ID号段的最大值,step
表示每次分配的号段长度。原来获取ID每次都需要写数据库,现在只需要把step
设置得足够大,比如1000。那么只有当1000个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从1减小到了1/step
。
image
对于第二个缺点,Leaf-segment做了优化,Leaf 取号段的时机是在号段消耗完的时候进行的,也就意味着号段临界点的ID下发时间取决于下一次从DB取回号段的时间,并且在这期间进来的请求也会因为DB号段没有取回来,导致线程阻塞。如果请求DB的网络和DB的性能稳定,这种情况对系统的影响是不大的,但是假如取DB的时候网络发生抖动,或者DB发生慢查询就会导致整个系统的响应时间变慢。
为此,我们希望DB取号段的过程能够做到无阻塞,不需要在DB取号段的时候阻塞请求线程,即当号段消费到某个点时就异步的把下一个号段加载到内存中。而不需要等到号段用尽的时候才去更新号段。这样做就可以很大程度上的降低系统的TP999
指标。详细实现如下图所示
image
采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。
QPS
的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。!! Leaf现状 Leaf在美团点评公司内部服务包含金融、支付交易、餐饮、外卖、酒店旅游、猫眼电影等众多业务线。目前Leaf的性能在4C8G的机器上QPS能压测到近5w/s,TP999 1ms,已经能够满足大部分的业务的需求。每天提供亿数量级的调用量,作为公司内部公共的基础技术设施,必须保证高SLA和高性能的服务,我们目前还仅仅达到了及格线,还有很多提高的空间。
测试代码
@PostMapping("test")
public BaseResponse<Boolean> test() {
ConcurrencyTester tester = ThreadUtil.concurrencyTest(10, () -> {
ConcurrentHashSet<String> set = new ConcurrentHashSet<>();
TimeInterval timer = DateUtil.timer();
// 测试的逻辑内容
for (int i=0; i<10000; i++) {
int r = RandomUtil.randomInt(6);
String s = HttpUtil.get("http://10.0.20.150:909" + r + "/api/segment/get/saving-test");
set.add(s);
}
long l = timer.intervalRestart();
log.info("{} test finished , time:{}ms , set size:{}", Thread.currentThread().getName(), l, set.size());
});
// 获取总的执行时间,单位毫秒
log.warn("总执行时间:{}ms", tester.getInterval());
return ResponseUtil.returnSuccess(Boolean.TRUE);
}
代码输出
20:16:25.381 pool-5-thread-4 test finished , time:34346ms , set size:10000
20:16:25.465 pool-5-thread-9 test finished , time:34430ms , set size:10000
20:16:25.497 pool-5-thread-6 test finished , time:34462ms , set size:10000
20:16:25.511 pool-5-thread-10 test finished , time:34476ms , set size:10000
20:16:25.520 pool-5-thread-5 test finished , time:34485ms , set size:10000
20:16:25.569 pool-5-thread-7 test finished , time:34534ms , set size:10000
20:16:25.573 pool-5-thread-1 test finished , time:34538ms , set size:10000
20:16:25.580 pool-5-thread-2 test finished , time:34545ms , set size:10000
20:16:25.606 pool-5-thread-3 test finished , time:34571ms , set size:10000
20:16:25.623 pool-5-thread-8 test finished , time:34588ms , set size:10000
20:16:25.623 总执行时间:34599ms
在本地开发机6U32G启动6个发号器实例,9090,9091,9092测试中进行随机调用
3台机器1500线程并发压测:4038+3795+4211=12044qps(未压到上限仅供参考)
(设置固定请求9090实例,在14虚拟机2U4G配置上运行)
4台机器压测:1493+2357+2425+2194=8469qps
!! 测试环境本机测试,启动6个发号器实例 9090,9091,9092,9093,9094,9095 在下面测试中进行随机调用
测试代码
@PostMapping("test")
public BaseResponse<Boolean> test() {
BasicLoginInfo basicLoginInfo = new BasicLoginInfo();
basicLoginInfo.setTenantId(9);
ThreadLocalContext.set(basicLoginInfo);
ConcurrencyTester tester = ThreadUtil.concurrencyTest(10, () -> {
// 测试的逻辑内容
for (int i=0; i<10000; i++) {
TimeInterval timer = DateUtil.timer();
int r = RandomUtil.randomInt(6);
// String s = HttpUtil.get("http://10.0.20.150:909" + r + "/api/snowflake/get/test");
String s = HttpUtil.get("http://10.0.20.150:909" + r + "/api/segment/get/saving-test");
log.info(s);
MemberDBO memberDBO = MemberDBO.builder().build();
memberDBO.setId(Long.valueOf(s));
memberMapper.insert(memberDBO);
long l = timer.intervalRestart();//返回花费时间,并重置开始时间
log.info("tcp消耗时间:{}ms", l);
}
log.info("{} test finished", Thread.currentThread().getName());
});
// 获取总的执行时间,单位毫秒
log.warn("总执行时间:{}ms", tester.getInterval());
return ResponseUtil.returnSuccess(Boolean.TRUE);
十个线程每个循环执行1w次,总执行10w获取ID并且模拟插入用户数据,总执行时间
未出现重复ID,入库数据10W条,主键ID全唯一
模拟业务中约等于854qps
Leaf-snowflake方案
雪花ID算法image
ZooKeeper
,需要维护多一个中间件,使用其的持久有序节点,进行分配workerID
用于进行生成雪花算法(ZooKeeper
挂了后,不影响id生成,并且每3秒循环重连机制)Zookeeper
的持久有序节点,进行了时间校验workerID
限制最大维度下存在1024台发号器因为这种方案依赖时间,如果机器的时钟发生了回拨,那么就会有可能生成重复的ID号,需要解决时钟回退的问题。
image
参见上图整个启动流程图,服务启动时首先检查自己是否写过ZooKeeper leaf_forever节点:
leaf_forever/${self}
并写入自身系统时间,接下来综合对比其余Leaf
节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary
下的所有临时节点(所有运行中的Leaf-snowflake
节点)的服务IP:Port
,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize
。abs( 系统时间-sum(time)/nodeSize ) < 阈值
,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary/${self}
维持租约。leaf_forever/${self}
由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警,如下:
//发生了回拨,此刻时间小于上次发号时间
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
try {
//时间偏差大小小于5ms,则等待两倍时间
wait(offset << 1);//wait
timestamp = timeGen();
if (timestamp < lastTimestamp) {
//还是小于,抛异常并上报
throwClockBackwardsEx(timestamp);
}
} catch (InterruptedException e) {
throw e;
}
} else {
//throw
throwClockBackwardsEx(timestamp);
}
}
//分配ID
!! Leaf现状 Leaf在美团点评公司内部服务包含金融、支付交易、餐饮、外卖、酒店旅游、猫眼电影等众多业务线。目前Leaf的性能在4C8G的机器上QPS能压测到近5w/s,TP999 1ms,已经能够满足大部分的业务的需求。每天提供亿数量级的调用量,作为公司内部公共的基础技术设施,必须保证高SLA和高性能的服务,我们目前还仅仅达到了及格线,还有很多提高的空间。
测试代码
@PostMapping("test")
public BaseResponse<Boolean> test() {
ConcurrencyTester tester = ThreadUtil.concurrencyTest(10, () -> {
ConcurrentHashSet<String> set = new ConcurrentHashSet<>();
TimeInterval timer = DateUtil.timer();
// 测试的逻辑内容
for (int i=0; i<10000; i++) {
int r = RandomUtil.randomInt(6);
String s = HttpUtil.get("http://10.0.20.150:909" + r + "/api/snowflake/get/test");
set.add(s);
}
long l = timer.intervalRestart();
log.info("{} test finished , time:{}ms , set size:{}", Thread.currentThread().getName(), l, set.size());
});
// 获取总的执行时间,单位毫秒
log.warn("总执行时间:{}ms", tester.getInterval());
return ResponseUtil.returnSuccess(Boolean.TRUE);
}
代码输出
20:08:26.019 pool-5-thread-2 test finished , time:29172ms , set size:10000
20:08:26.036 pool-5-thread-7 test finished , time:29189ms , set size:10000
20:08:26.101 pool-5-thread-1 test finished , time:29254ms , set size:10000
20:08:26.117 pool-5-thread-5 test finished , time:29270ms , set size:10000
20:08:26.126 pool-5-thread-10 test finished , time:29279ms , set size:10000
20:08:26.151 pool-5-thread-6 test finished , time:29304ms , set size:10000
20:08:26.185 pool-5-thread-4 test finished , time:29338ms , set size:10000
20:08:26.194 pool-5-thread-8 test finished , time:29347ms , set size:10000
20:08:26.201 pool-5-thread-9 test finished , time:29354ms , set size:10000
20:08:26.219 pool-5-thread-3 test finished , time:29372ms , set size:10000
20:08:26.220 总执行时间:29382ms
在本地开发机6U32G启动3个发号器实例,9090,9091,9092测试中进行随机调用
3台机器1500线程并发压测:3326+3631+3040=9997qps (未压到上限仅供参考)
(设置固定请求9090实例,在14虚拟机2U4G配置上运行)
4台机器1500线程并发压测:945+3131+5732+3751=13559qps
!! 测试环境本机测试,启动6个发号器实例 9090,9091,9092,9093,9094,9095 在下面测试中进行随机调用
测试代码
@PostMapping("test")
public BaseResponse<Boolean> test() {
BasicLoginInfo basicLoginInfo = new BasicLoginInfo();
basicLoginInfo.setTenantId(9);
ThreadLocalContext.set(basicLoginInfo);
ConcurrencyTester tester = ThreadUtil.concurrencyTest(10, () -> {
// 测试的逻辑内容
for (int i=0; i<10000; i++) {
TimeInterval timer = DateUtil.timer();
int r = RandomUtil.randomInt(6);
String s = HttpUtil.get("http://10.0.20.150:909" + r + "/api/snowflake/get/test");
log.info(s);
MemberDBO memberDBO = MemberDBO.builder().build();
memberDBO.setId(Long.valueOf(s));
memberMapper.insert(memberDBO);
long l = timer.intervalRestart();//返回花费时间,并重置开始时间
log.info("tcp消耗时间:{}ms", l);
}
log.info("{} test finished", Thread.currentThread().getName());
});
// 获取总的执行时间,单位毫秒
log.warn("总执行时间:{}ms", tester.getInterval());
return ResponseUtil.returnSuccess(Boolean.TRUE);
}
十个线程每个循环执行1w次,总执行10w获取ID并且模拟插入用户数据,总执行时间
未出现重复ID,入库数据10W条,主键ID全唯一
模拟业务中约等于885qps
使用的也是雪花算法,利用DB分配WorkerID
因为与美团发号器的雪花方案相似,和使用未来时间进行借用,还会产生节点使用时长限制,放弃选择
GitHub:https://github.com/baidu/uid-generator
采取的是在美团发号器号段模式进行了改进实现
因为与美团号段模式相似,放弃选择
GitHub:https://github.com/didi/tinyid
!! Leaf现状 Leaf在美团点评公司内部服务包含金融、支付交易、餐饮、外卖、酒店旅游、猫眼电影等众多业务线。目前Leaf的性能在4C8G的机器上QPS能压测到近5w/s,TP999 1ms,已经能够满足大部分的业务的需求。每天提供亿数量级的调用量,作为公司内部公共的基础技术设施,必须保证高SLA和高性能的服务,我们目前还仅仅达到了及格线,还有很多提高的空间。
测试接口--多实例测试环境——在本地开发机6U32G启动3个发号器实例,9090,9091,9092测试中进行随机调用
测试接口--单实例测试环境——(设置固定请求9090实例,在14虚拟机2U4G配置上运行)4台机器1500线程并发压测
模拟DB操作--多实例——十个线程每个循环执行1w次,总执行10w获取ID并且模拟插入用户数据
方案一(Leaf-segment数据库方案) | 方案二(Leaf-snowflake雪花方案) | |
---|---|---|
测试接口--多实例 | 12044qps | 9997qps |
测试接口--单实例 | 8469qps | 13559qps |
模拟DB操作--多实例 | 854qps | 885qps |
PS:多实例数据仅供参考
!! @author Saving@date 2021.07.14
[1]双buffer优化: #双buffer优化
[2]结果汇总: #方案测试报告
[3]已解决: #解决时钟问题
[4]结果汇总: #方案测试报告
[5]uid-generator: https://github.com/baidu/uid-generator
[6]tinyid: https://github.com/didi/tinyid
[7]Leaf——美团点评分布式ID生成系统: https://tech.meituan.com/2017/04/21/mt-leaf.html
[8]Leaf: https://github.com/Meituan-Dianping/Leaf
[9]uid-generator: https://github.com/baidu/uid-generator
[10]tinyid: https://github.com/didi/tinyid