分布式系统中生成唯一ID在后台开发是经常遇到的架构设计,当然方案有很多,比如通过redis或者数据库实现自增。但是如果依赖redis或者数据库,会导致单点问题,在架构上反而需要考虑点更多,那怎么解决呢?
首先分布式唯一ID需要支持如下:
方案:
单机生成方式
UUID(Universally Unique Identifier,即通用唯一标识码)算法的目的是生成某种形式的全局唯一ID来标识系统中的任一元素,尤其是在分布式环境下,UUID可以不依赖中心认证即可自动生成全局唯一ID。UUID的标准形式为32个十六进制数组成的字符串,且分割为五个部分,例如:执行:cat /proc/sys/kernel/random/uuid,输出:70048d49-6ef3-4ba6-84c4-1e6e37ec2f4a。
缺点:
2、Snowflake
snowflake(雪花算法)是一个开源的分布式ID生成算法,结果是一个long型的ID。snowflake算法将64bit划分为多段,分开来标识机器、时间等信息,其中格式如下:
bit bit时间戳 bit机器号bit序列递增
snowflake算法优势是支持递增,可以根据自己的算法改造使用bit位,不过存在如下缺点:
第三方模块生成方式
通过mysql,redis,zk或者ticket server实现架构如下:
前面提到依赖mysql也可以实现序列号,mysql的auto_increment可以保证全局唯一,不过需要依赖数据库,性能上会有影响。
当然提升性能的方式就是将mysql设置主从模式,但是只是为了序列号生成,部署多个mysql实例确实有些浪费。
redis同样可以实现递增,而且可以保证原子,比如通过incr或者incrby,虽然性能比mysql要好很多,我测试下来4c8g情况下可以支持10W+qps,不过存在单点维护问题。
3、Zookeeper
利用zookeeper的znode也可以生成序列号,可以生成32位和64位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号,不过zk的性能比较差,在高并发场景下基本不建议采用。
4、Ticket Server
Ticket Server类似单独的票据服务,可以通过自己的逻辑生成唯一序列号,比如实现上可以使用原子递增,或者根据各个业务的特性进行适配。
不过要实现完整的容灾体系下可持久的服务工作量是不小的,对于没有太多特殊需求的场景,更建议依赖redis或者mysql。
号段生成方式
1、大厂方案:美团Leaf-segment和Leaf-snowflake方案
1.1 Leaf-segment
具体技术介绍:。Leaf-segment主要解决思路是:对直接用数据库自增ID充当分布式ID的一种优化,减少对数据库的访问频率,每次获取不是获取一个ID,而是获取一个号段,同时获取号段,将数据持久化到数据库中,这样可以解决分布式的抢占或者持久化问题,即使DB出现问题,也可以通过Master-Slave来解决。
1.2 Leaf-snowflake
Leaf-snowflake继续使用snowflake方案,主要解决了时钟不同步的问题,其中中间10bit机器号定义为WorkerID,Leaf-snowflake是按照下面几个步骤启动的:
若写过,则用自身系统时间与leaf_forever/节点记录时间做比较,若小于{self}时间则认为机器时间发生了大步长回拨,服务启动失败并报警;
若未写过,证明是新服务节点,直接创建持久节点leaf_forever/${self}并写入自身系统时间,接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IP:Port,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize;
若abs( 系统时间-sum(time)/nodeSize ) < 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary/${self} 维持租约;
否则认为本机系统时间发生大步长偏移,启动失败并报警;
每隔一段时间(3s)上报自身系统时间写入leaf_forever/${self};
2、大厂方案:滴滴Tinyid和百度UidGenerator
2.1 滴滴Tinyid
开源方案:。Tinyid和美团的Leaf-segment方案类似,从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如:(1,1000]代表1000个ID,业务服务将号段在本地生成1~1000的自增ID并加载到内存。Tinyid会将可用号段加载到内存中,并在内存中生成ID,可用号段在首次获取ID时加载,如当前号段使用达到一定比例时,系统会异步的去加载下一个可用号段,以此保证内存中始终有可用号段,以便在发号服务宕机后一段时间内还有可用ID。
2.2 百度UidGenerator
百度UidGenerator是基于snowflake方案改造,旨在解决时钟回拨,workerid不够等问题。
RingBuffer:UidGenerator不再在每次取ID时都实时计算分布式ID,而是利用RingBuffer数据结构预先生成若干个分布式ID并保存,引入boostPower可以控制每秒生成ID的上限能力;
时间递增:UidGenerator的时间类型是AtomicLong,且通过incrementAndGet()方法获取下一次的时间,从而脱离了对服务器时间的依赖,也就不会有时钟回拨的问题;
3、大厂方案:容灾
微信内部也是通过独立的seqsvr提供序列号生成,主要面对场景是微信中的消息版本,其中特性和挑战如下。
(1)两个特性:
(2)面临两个挑战:
如何解决分布式场景下的问题?
提供两层:StoreSvr和AllocSvr,分别是存储层和缓存中间层,分层后就能利用堆机器就可以解决问题;
每秒千万级别的QPS?
实现方案和美团Leaf-segment类似,每次提供一批seqid,这样从千万级别的qps就变成千级别的qps,不过不保证序列号是连续的,但是能保证是递增的。
每个Uin都需要存储max-seqid,存储量大?
每个用户需要加载一个max_seq(32bit),如果uin是2^32个,则需要存储数据大小为16GB,这样系统启动时候加载就会很慢,微信如何解决?通过区分Set,同一批共享同一个max_seqid,这样就减少加载的数据量。
容灾如何实现?
seqsvr服务虽然简单,解决了上述高性能的问题,但是要保证高可靠性还是非常难,我查了一下内部资料和infoQ一样,实现架构可以参考:。seqsvr最核心的点是什么呢?每个 uin 的sequence申请要递增不回退,但是约束条件是:任意时刻任意 uin 有且仅有一台 AllocSvr 提供服务,就可以比较容易地实现sequence递增不回退的要求。
(1)容灾1.0
但是上述面临问题:
(2)容灾2.0
通过提供 Client 路由表方式解决访问 AllocSvr 切换的问题,执行步骤如下:
总结
以上就是一些场景下生成分布式唯一ID的方案选择,分布式唯一ID的架构虽然简单,但是如果要实现高性能高可用,还是需要根据业务场景来考虑。所以说简单的事情要做好并非易事,但是在这些年的工作中总是会有很多人为了追求效率,总想找到捷径而放弃架构的基本演进路径方法论....
参考
(1)(2)(3)