在《分布式服务化系统一致性的“最佳实干”》一文中提出了保证系统最终一致性的按期校对模式,在按期校对模式中最常使用的方法是在每一个系统间传递和保存一个统一的惟一流水号(或称为traceid),经过系统间两两核对或者第三方统一核对惟一流水号来保证各个系统之间步伐一致、没有掉队的行为,也就是系统间状态一致,在互联网的世界里,产生惟一流水号的服务系统俗称发号器。git
Twitter的Snowflake是一个流行的开源的发号器的实现。Slowfake是由Scala语言实现的,而且文档简单、发布模式单1、缺乏支持和维护,很难在现实的项目中直接使用。github
为了能让Java领域的小伙伴们在不一样的环境下快速使用发号器服务,本文向你们推荐一款自主研发的多场景分布式发号器Vesta,这是由Java语言编写的,能够经过Jar包的形式嵌入到任何Java开发的项目中,也能够经过服务化或者REST服务发布,发布样式灵活多样,使用简单、方便、高效。sql
Vesta是一款通用的惟一流水号产生器,它具备全局惟1、粗略有序、可反解和可制造等特性,它支持三种发布模式:嵌入发布模式、中心服务器发布模式、REST发布模式,根据业务的性能需求,它能够产生最大峰值型和最小粒度型两种类型的ID,它的实现架构使其具备高性能,高可用和可伸缩等互联网产品须要的质量属性,是一款通用的高性能的发号器产品。数据库
本文聚焦在笔者原创的多场景分布式发号器Vesta的设计、实现、性能评估等方面,同时介绍Vesta的发布模式以及使用方式,并在最后给读者介绍如何在你的项目中使用Vesta。apache
当前业务系统的ID使用数据库的自增字段,自增字段彻底依赖于数据库,这在数据库移植、扩容、洗数据、分库分表等操做时带来了不少麻烦。缓存
在数据库分库分表时,有一种办法是经过调整自增字段或者数据库sequence的步长来达到跨数据库的ID的惟一性,但仍然是一种强依赖数据库的解决方案,有诸多的限制,而且强依赖数据库类型,咱们并不推荐这种方法。服务器
UUID虽然可以保证ID的惟一性,可是,它没法知足业务系统须要的不少其余特性,例如:时间粗略有序性、可反解和可制造型。另外,UUID产生的时候使用彻底的时间数据,性能比较差,而且UUID比较长,占用空间大,间接致使数据库性能降低,更重要的是,UUID并不具备有序性,这致使B+树索引在写的时候会有过多的随机写操做(连续的ID会产生部分顺序写),另外写的时候因为不能产生顺序的append操做,须要进行insert操做,这会读取整个B+树节点到内存,而后插入这条记录后写整个节点回磁盘,这种操做在记录占用空间比较大的状况下,性能降低比较大,具体压测报告请参考:Mysql性能压测实践报告。网络
既然数据库自增ID和UUID有诸多的限制,咱们须要整理一下发号器的需求。数据结构
有些业务系统可使用相对小范围的惟一性,例如,若是用户是惟一的,那么同一用户的订单采用自增序列在用户范围内也是惟一的,可是若是这样设计,订单系统就会在逻辑上依赖用户系统,所以,不如咱们保证ID在系统范围内的全局惟一性更实用。多线程
分布式系统保证全局惟一的一个悲观策略是使用锁或者分布式锁,可是,只要使用了锁,就会大大的下降性能。
所以,咱们决定利用时间的有序性,而且在时间的某个单元下采用自增序列,达到全局的惟一性。
上面讨论了UUID的最大问题就是无序的,任何业务都但愿生成的ID是有序的,可是,分布式系统中要作到彻底有序,就涉及到数据的汇聚,固然要用到锁或者布式锁,考虑到效率,只能采用折中的方案,粗略有序,到底有多粗略,目前有两种主流的方案,一种是秒级有序,一种是毫秒级有序,这里又有一个权衡和取舍,咱们决定支持两种方式,经过配置来决定服务使用其中的一种方式。
一个 ID 生成以后,ID自己带有不少信息量,线上排查的时候,咱们一般首先看到的是ID,若是根据ID就能知道何时产生的,从哪里来的,这样一个可反解的 ID 能够帮上不少忙。
若是ID 里有了时间并且能反解,在存储层面就会省下不少传统的timestamp 一类的字段所占用的空间了,这也是一箭双雕的设计。
一个系统即便再高可用也不会保证永远不出问题,出了问题怎么办,手工处理,数据被污染怎么办,洗数据,但是手工处理或者洗数据的时候,假如使用数据库自增字段,ID已经被后来的业务覆盖了,怎么恢复到系统出问题的时间窗口呢?
因此,咱们使用的发号器必定要可复制,可恢复,可制造。
无论哪一个业务,订单也好,商品也好,若是有新记录插入,那必定是业务的核心功能,对性能的要求很是高,ID生成取决于网络IO和CPU的性能,CPU通常不是瓶颈,根据经验,单台机器TPS应该达到10000/s。
首先,发号器必须是一个对等的集群,一台机器挂掉,请求必须可以转发到其余机器,另外,重试机制也是必不可少的。最后,若是远程服务宕机,咱们须要有本地的容错方案,本地库的依赖方式能够做为高可用的最后一道屏障。
做为一个分布式系统,永远都不能忽略的就是业务在不断地增加,业务的绝对容量不是衡量一个系统的惟一标准,要知道业务是永远增加的,因此,系统设计不但要考虑能承受的绝对容量,还必须考虑业务增加的速度,系统的水平伸缩是否能知足业务的增加速度是衡量一个系统的另外一个重要标准。
根据最终的客户使用方式,可分为嵌入发布模式、中心服务器发布模式和REST发布模式。
嵌入发布模式:只适用于Java客户端,提供一个本地的Jar包,Jar包是嵌入式的原生服务,须要提早配置本地机器ID(或者服务启动时候Zookeeper动态分配惟一的ID,在第二版中实现),可是不依赖于中心服务器。
中心服务器发布模式:只适用于Java客户端,提供一个服务的客户端Jar包,Java程序像调用本地API同样来调用,可是依赖于中心的ID产生服务器。
REST发布模式:中心服务器经过Restful API导出服务,供非Java语言客户端使用。
发布模式最后会记录在生成的ID中。也参考下面数据结构段的发布模式相关细节。
根据时间的位数和序列号的位数,可分为最大峰值型和最小粒度型。
1) 最大峰值型:采用秒级有序,秒级时间占用30位,序列号占用20位。
字段 |
版本 |
类型 |
生成方式 |
秒级时间 |
序列号 |
机器ID |
---|---|---|---|---|---|---|
位数 | 63 | 62 | 60-61 | 40-59 | 10-39 | 0-9 |
2) 最小粒度型:采用毫秒级有序,毫秒级时间占用40位,序列号占用10位
字段 |
版本 |
类型 |
生成方式 |
毫秒级时间 |
序列号 |
机器ID |
---|---|---|---|---|---|---|
位数 | 63 | 62 | 60-61 | 20-59 | 10-19 | 0-9 |
最大峰值型可以承受更大的峰值压力,可是粗略有序的粒度有点大,最小粒度型有较细致的粒度,可是每一个毫秒能承受的理论峰值有限,为1k,同一个毫秒若是有更多的请求产生,必须等到下一个毫秒再响应。
ID类型在配置时指定,须要重启服务才能互相切换。
1) 机器ID
10位, 2^10=1024, 也就是最多支持1000+个服务器。中心发布模式和REST发布模式通常不会有太多数量的机器,按照设计每台机器TPS 1万/s,10台服务器就能够有10万/s的TPS,基本能够知足大部分的业务需求。
可是考虑到咱们在业务服务可使用内嵌发布方式,对机器ID的需求量变得更大,这里最多支持1024个服务器。
2) 序列号
最大峰值型
20位,理论上每秒内平都可产生2^20= 1048576个ID,百万级别,若是系统的网络IO和CPU足够强大,可承受的峰值达到每毫秒百万级别。
最小粒度型
10位,每毫秒内序列号总计2^10=1024个, 也就是每一个毫秒最多产生1000+个ID,理论上承受的峰值彻底不如咱们最大峰值方案。
3) 秒级时间/毫秒级时间
最大峰值型
30位,表示秒级时间,2^30/60/60/24/365=34,也就是可以使用30+年。
最小粒度型
40位,表示毫秒级时间,2^40/1000/60/60/24/365=34,一样可使用30+年。
4) 生成方式
2位,用来区分三种发布模式:嵌入发布模式,中心服务器发布模式,REST发布模式。
00:嵌入发布模式
01:中心服务器发布模式
02:REST发布模式
03:保留未用
5) ID类型
1位,用来区分两种ID类型:最大峰值型和最小粒度型。
0:最大峰值型
1:最小粒度型
6) 版本
1位,用来作扩展位或者扩容时候的临时方案。
0:默认值,以避免转化为整型再转化回字符串被截断
1:表示扩展或者扩容中
做为30年后扩展使用,或者在30年后ID将近用光之时,扩展为秒级时间或者毫秒级时间来挣得系统的移植时间窗口,其实只要扩展一位,彻底能够再使用30年。
对于中心服务器和REST发布方式,ID生成的过程涉及到网络IO和CPU操做,ID的生成基本都是内存到高速缓存的操做,没有IO操做,网络IO是系统的瓶颈。
相对于CPU计算速度来讲网络IO是瓶颈,所以,ID产生的服务使用多线程的方式,对于ID生成过程当中的竞争点time和sequence,咱们使用concurrent包的ReentrantLock进行互斥。
咱们将机器ID分为两个区段,一个区段服务于中心服务器发布模式和REST发布模式,另一个区段服务于嵌入发布模式。
0-923:嵌入发布模式,预先配置,(或者由Zookeeper产生,第二版中实现),最多支持924台内嵌服务器。
924 – 1023:中心服务器发布模式和REST发布模式,最多支持300台,最大支持300*1万=300万/s的TPS。
若是嵌入式发布模式和中心服务器发布模式以及REST发布模式的使用量不符合这个比例,咱们能够动态调整两个区间的值来适应。
另外,各个垂直业务之间具备天生的隔离性,每一个业务均可以使用最多1024台服务器。
对于嵌入发布模式,服务启动须要链接Zookeeper集群,Zookeeper分配一个0-923区间的一个ID,若是0-923区间的ID被用光,Zookeeper会分配一个大于923的ID,这种状况,拒绝启动服务。
若是不想使用Zookeeper产生的惟一的机器ID,咱们提供缺省的预配的机器ID解决方案,每一个使用统一发号器的服务须要预先配置一个默认的机器ID。
注:此功能在第二版中实现。
使用Linux的定时任务crontab,定时经过授时服务器虚拟集群(全球有3000多台服务器)来核准服务器的时间。
ntpdate -u pool.ntp.orgpool.ntp.org
时间相关的影响以及思考:
调整时间是否会影响ID产生功能?
1) 未重启机器调慢时间,Vesta抛出异常,拒绝产生ID。重启机器调快时间,调整后正常产生ID,调整时段内没有ID产生。
2) 重启机器调慢时间,Vesta将可能产生重复的时间,系统管理员须要保证不会发生这种状况。重启机器调快时间,调整后正常产生ID,调整时段内没有ID产生。
每4年一次同步润秒会不会影响ID产生功能?
1) 原子时钟和电子时钟每四年偏差为1秒,也就是说电子时钟每4年会比原子时钟慢1秒,因此,每隔四年,网络时钟都会同步一次时间,可是本地机器Windows,Linux等不会自动同步时间,须要手工同步,或者使用ntpupdate向网络时钟同步。
2) 因为时钟是调快1秒,调整后不影响ID产生,调整的1s内没有ID产生。
咱们根据不一样的信息分段构建一个ID,使ID具备全局惟一,可反解和可制造。
咱们使用秒级别时间或者毫秒级别时间以及时间单元内部序列递增的方法保证ID粗略有序。
对于中心服务器发布模式和REST发布模式,咱们使用多线程处理,为了减小多线程间竞争,咱们对竞争点time和sequence使用ReentrantLock来进行互斥,因为ReentrantLock内部使用CAS,这比JVM的Synchronized关键字性能更好,在千兆网卡的前提下,至少可达到1万/s以上的TPS。
因为咱们支持中心服务器发布模式,嵌入式发布模式和REST发布模式,若是某种模式不可用,能够回退到其余发布模式,若是Zookeeper不可用,能够会退到使用本地预配的机器ID。从而达到服务的最大可用。
因为ID的设计,咱们最大支持1024台服务器,咱们将服务器机器号分为两个区段,一个从0开始向上,一个从128开始向下,而且可以动态调整分界线,知足了可伸缩性。
一款软件的发布必须保证知足性能需求,这一般须要在项目初期提出性能需求,在项目进行中作性能测试来验证,请参考本文末尾的源码链接下载源代码,查看性能测试用例,本章节只讨论性能需求和测试结果,以及改进点。
最终的性能验证要保证每台服务器的TPS达到1万/s以上。
笔记本,客户端服务器跑在同一台机器
双核2.4G I3 CPU, 4G内存
设置:
**并发数:**100
测试结果:
测试 | 测试1 | 测试2 | 测试3 | 测试4 | 测试5 | 平均值/最大值 |
---|---|---|---|---|---|---|
QPS | 431000 | 445000 | 442000 | 434000 | 434000 | 437200 |
平均时间(us) | 161 | 160 | 168 | 143 | 157 | 157 |
最大响应时间(ms) | 339 | 304 | 378 | 303 | 299 | 378 |
设置:
**并发数:**100
测试结果:
测试 | 测试1 | 测试2 | 测试3 | 测试4 | 测试5 | 平均值/最大值 |
---|---|---|---|---|---|---|
QPS | 1737 | 1410 | 1474 | 1372 | 1474 | 1493 |
平均时间(us) | 55 | 67 | 66 | 68 | 65 | 64 |
最大响应时间(ms) | 785 | 952 | 532 | 1129 | 1036 | 1129 |
设置:
**并发数:**100
**Boss线程数:**1
**Workder线程数:**4
测试结果:
测试 | 测试1 | 测试2 | 测试3 | 测试4 | 测试5 | 平均值/最大值 |
---|---|---|---|---|---|---|
QPS | 11001 | 10611 | 9788 | 11251 | 10301 | 10590 |
平均时间(ms) | 11 | 11 | 11 | 10 | 10 | 11 |
最大响应时间(ms) | 25 | 21 | 23 | 21 | 21 | 25 |
设置:
**并发数:**100
**Boss线程数:**1
**Workder线程数:**2
Exececutor线程数:最小25最大200
测试结果:
测试 | 测试1 | 测试2 | 测试3 | 测试4 | 测试5 | 平均值/最大值 |
---|---|---|---|---|---|---|
QPS | 4994 | 5104 | 5223 | 5108 | 5100 | 5105 |
平均时间(ms) | 20 | 19 | 19 | 19 | 19 | 19 |
最大响应时间(ms) | 75 | 61 | 61 | 61 | 67 | 75 |
参考上面总结第三条,中心服务器的性能问题须要在后期版本跟进和优化。
Vesta多场景分布式发号器支持嵌入发布模式、中心服务器发布模式、REST发布模式,每种发布 模式的API文档以及使用向导可参项目主页的文档链接。
点击下载:
若是你经过源代码方式安装Vesta的发布包到你的Maven私服,你能够直接从你的Maven私服下载此安装包:
解压:
tar xzvf vesta-rest-netty-0.0.1-bin.tar.gz
属性文件:
vesta-rest-netty-0.0.1/conf/vesta-rest-netty.properties
文件内容:
vesta.machine=1022
vesta.genMethod=2
vesta.type=0
注意:
- 机器ID为1022, 若是你有多台机器,递减机器ID,同一服务中机器ID不能重复。
- genMethod为2表示使用嵌入发布模式
- type为0, 表示最大峰值型,若是想要使用最小粒度型,则设置为1
启动文件:
vesta-rest-netty/target/vesta-rest-netty-0.0.1/bin/server.sh
文件内容:
port=10010
进入目录:
cd vesta-rest-netty-0.0.1/bin
执行命令:
chmod 755 *
进入目录:
cd vesta-rest-netty-0.0.1/bin
执行命令:
./start.sh
输出:
apppath: /home/robert/vesta/vesta-rest-netty-0.0.1
Vesta Rest Netty Server is started.
命令:
结果:
1138729511026688
命令:
结果:
{“genMethod”:0,”machine”:1,”seq”:0,”time”:12235264,”type”:0,”version”:0}
JSON字符串显示的是反解的ID的各个组成部分的数值。
命令:
结果:
Fri May 22 14:41:04 CST 2015
命令:
结果:
1138729511026688
发号器做为分布式服务化系统不可或缺的基础设施之一,它在保证系统正确运行和高可用上发挥着不可替代的做用。而本文介绍了一款原创开源的多场景分布式发号器Vesta,并介绍了Vesta的设计、实现、以及使用方式,读者在现实项目中能够直接使用它的任何发布模式,既装既用,读者也能够借鉴其中的设计思路和思想,开发本身的分布式发号器,除了发号器自己,本文按照一款开源项目的生命周期构思文章结果,从设计、实现、验证到使用向导,以及论述遗留的问题等,并提供了参考的开源实现,帮助读者学习如何建立一款平台类软件的过程的思路,帮助读者在技术的道路上发展愈来愈好。
在《分布式服务化系统一致性的“最佳实干”》一文中提到全局的惟一流水ID能够把一个请求在分布式系统中流转的路径聚合,而调用链中的spanid能够把聚合的请求路径经过树形结构进行展现,让技术支持人员轻松的发现系统出现的问题,可以快速定位出现问题的服务节点,提升应急效率,下一篇《如何设计一款分布式服务化调用链追踪》