十分钟了解分布式系统中生成唯一ID

开发 架构
分布式唯一ID的架构虽然简单,但是如果要实现高性能高可用,还是需要根据业务场景来考虑。所以说简单的事情要做好并非易事,但是在这些年的工作中总是会有很多人为了追求效率,总想找到捷径而放弃架构的基本演进路径方法论.....

分布式系统中生成唯一ID在后台开发是经常遇到的架构设计,当然方案有很多,比如通过redis或者数据库实现自增。但是如果依赖redis或者数据库,会导致单点问题,在架构上反而需要考虑点更多,那怎么解决呢?

首先分布式唯一ID需要支持如下:

  • 全局唯一:必须保证生成的ID是全局性唯一的;
  • 有序性:生成的ID是有序,方便追溯和排序操作;
  • 可用性:需要保证高并发下的可用性,除了对ID号码自身的要求,业务还对ID生成系统的可用性要求极高;
  • 自主性:分布式环境下不依赖中心认证即可自行生成ID;
  • 安全性:不暴露系统和业务的信息;

方案:

  • 单机生成方式
  • 第三方模块生成方式
  • 号段生成方式

单机生成方式

1、UUID

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划分为多段,分开来标识机器、时间等信息,其中格式如下:

0   |00000...0000|000...0000|000000000000|
1bit| 41bit时间戳 |10bit机器号|12bit序列递增|
  • 1bit保留位:方便扩展;
  • 41bit时间戳:可以标识毫秒时间戳(最长支持69年),结合递增bit使用,可以保证有序,不过我觉得如果qps没有超过4000,使用秒时间戳也可以;
  • 10bit机器号:可以支持1024个机器ID,用于标识不同的机器;
  • 12bit序列递增:支持4096个序列递增,可以支持同一台机器同一毫秒内生成4096个ID;

snowflake算法优势是支持递增,可以根据自己的算法改造使用bit位,不过存在如下缺点:

  • 强依赖时间同步,如果某台机器的时钟出现回拨,递增就不准确;
  • ID不能完全支持全局递增,需要依赖定义的机器号;

第三方模块生成方式

通过mysql,redis,zk或者ticket server实现架构如下:

架构架构

1、Mysql

前面提到依赖mysql也可以实现序列号,mysql的auto_increment可以保证全局唯一,不过需要依赖数据库,性能上会有影响。

当然提升性能的方式就是将mysql设置主从模式,但是只是为了序列号生成,部署多个mysql实例确实有些浪费。

2、Redis

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

具体技术介绍:https://tech.meituan.com/2017/04/21/mt-leaf.html。Leaf-segment主要解决思路是:对直接用数据库自增ID充当分布式ID的一种优化,减少对数据库的访问频率,每次获取不是获取一个ID,而是获取一个号段,同时获取号段以后,将数据持久化到数据库中,这样可以解决分布式的抢占或者持久化问题,即使DB出现问题,也可以通过Master-Slave来解决。

Leaf-segment架构Leaf-segment架构

1.2 Leaf-snowflake

Leaf-snowflake继续使用snowflake方案,主要解决了时钟不同步的问题,其中中间10bit机器号定义为WorkerID,Leaf-snowflake是按照下面几个步骤启动的:

  • 启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点);
  • 如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务;
  • 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务;
  • 检查服务启动是否写过ZooKeeper leaf_forever节点,并进行如下处理:

若写过,则用自身系统时间与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};

Leaf-snowflake架构Leaf-snowflake架构

2、大厂方案:滴滴Tinyid和百度UidGenerator

2.1 滴滴Tinyid

开源方案:https://github.com/didi/tinyid。Tinyid和美团的Leaf-segment方案类似,从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如:(1,1000]代表1000个ID,业务服务将号段在本地生成1~1000的自增ID并加载到内存。Tinyid会将可用号段加载到内存中,并在内存中生成ID,可用号段在首次获取ID时加载,如当前号段使用达到一定比例时,系统会异步的去加载下一个可用号段,以此保证内存中始终有可用号段,以便在发号服务宕机后一段时间内还有可用ID。

Tinyid架构Tinyid架构

2.2 百度UidGenerator

百度UidGenerator是基于snowflake方案改造,旨在解决时钟回拨,workerid不够等问题。

百度UidGenerator百度UidGenerator

  • 1bit保留位:方便扩展;
  • 28bit时间戳:指当前时间与epoch时间的时间差,单位为秒,比如2024-01-01 00:00:00上线,那时间就是当前时间戳-1704038400;
  • 22bit节点id:22bit可以支持4194304台机器,同时这个数据是持久化到DB,保持每次新增或者重启都会自增;
  • 13bit序列递增:支持每秒生成8192个自增序列号(未调整boostPower情况下);
  • UidGenerator优化点还包括:

RingBuffer:UidGenerator不再在每次取ID时都实时计算分布式ID,而是利用RingBuffer数据结构预先生成若干个分布式ID并保存,引入boostPower可以控制每秒生成ID的上限能力;

时间递增:UidGenerator的时间类型是AtomicLong,且通过incrementAndGet()方法获取下一次的时间,从而脱离了对服务器时间的依赖,也就不会有时钟回拨的问题;

3、大厂方案:容灾

微信内部也是通过独立的seqsvr提供序列号生成,主要面对场景是微信中的消息版本,其中特性和挑战如下。

(1)两个特性:

  • 递增的32位整型;
  • 每个用户都有自己的32位sequence空间;

(2)面临两个挑战:

  • 1、分布式场景;
  • 2、每秒千万级别的QPS;
  • 3、每个Uin都需要存储max-seqid,存储量大,也带来容灾问题;

如何解决分布式场景下的问题?

提供两层:StoreSvr和AllocSvr,分别是存储层和缓存中间层,分层后就能利用堆机器就可以解决问题;

图片图片

每秒千万级别的QPS?

实现方案和美团Leaf-segment类似,每次提供一批seqid,这样从千万级别的qps就变成千级别的qps,不过不保证序列号是连续的,但是能保证是递增的。

每个Uin都需要存储max-seqid,存储量大?

每个用户需要加载一个max_seq(32bit),如果uin是2^32个,则需要存储数据大小为16GB,这样系统启动时候加载就会很慢,微信如何解决?通过区分Set,同一批共享同一个max_seqid,这样就减少加载的数据量。

容灾如何实现?

seqsvr服务虽然简单,解决了上述高性能的问题,但是要保证高可靠性还是非常难,我查了一下内部资料和infoQ一样,实现架构可以参考:https://www.infoq.cn/article/wechat-serial-number-generator-architecture。seqsvr最核心的点是什么呢?每个 uin 的sequence申请要递增不回退,但是约束条件是:任意时刻任意 uin 有且仅有一台 AllocSvr 提供服务,就可以比较容易地实现sequence递增不回退的要求。

(1)容灾1.0

容灾1.0容灾1.0

  • 一主一备:每个Set都是一主一备两台 AllocSvr ,主机出故障时,仲裁服务切换主备,原来的主机下线变成备机,原备机变成主机后加载 uid 号段提供服务;
  • 引入仲裁服务:探测 AllocSvr 的状态,决定每个 uin 走到哪一台 AllocSvr ,同时解决备机切换的问题;

但是上述面临问题:

  • 主从容灾扩缩容麻烦;
  • 单个Set的资源不一定能支撑高并发请求;

(2)容灾2.0

通过提供 Client 路由表方式解决访问 AllocSvr 切换的问题,执行步骤如下:

  • Client 根据本地共享内存缓存的路由表,选择对应的 AllocSvr,如果路由表不存在,随机选择一台 AllocSvr;
  • 对选中的 AllocSvr 发起请求,请求带上本地路由表的版本号;
  • AllocSvr 收到请求,除了处理 sequence 逻辑外,判断 Client 带上版本号是否最新,如果是旧版则在响应包中附上最新的路由表;
  • Client 收到响应包,除了处理 sequence 逻辑外,判断响应包是否带有新路由表,如果有,更新本地路由表,并决策是否返回第 1 步重试;

容灾2.0容灾2.0

总结

以上就是一些场景下生成分布式唯一ID的方案选择,分布式唯一ID的架构虽然简单,但是如果要实现高性能高可用,还是需要根据业务场景来考虑。所以说简单的事情要做好并非易事,但是在这些年的工作中总是会有很多人为了追求效率,总想找到捷径而放弃架构的基本演进路径方法论....

参考

(1)https://learning-guide.gitbook.io/system-design-interview/chapter-07-design-a-unique-id-generator-in-distributed-systems(2)https://tech.meituan.com/2017/04/21/mt-leaf.html(3)https://www.infoq.cn/article/wechat-serial-number-generator-architecture

责任编辑:武晓燕 来源: 周末程序猿
相关推荐

2019-11-25 09:32:26

软件程序员数据结构

2024-05-13 09:28:43

Flink SQL大数据

2024-06-19 09:58:29

2022-04-08 08:47:02

PGXLPostgreSQL数据库

2024-11-07 16:09:53

2023-07-15 18:26:51

LinuxABI

2015-11-06 11:03:36

2021-07-29 08:57:23

ViteReact模块

2009-11-03 11:01:45

VB.NET远程事件

2024-10-08 11:12:12

2024-12-13 15:29:57

SpringSpringBeanJava

2019-04-01 14:59:56

负载均衡服务器网络

2020-12-17 06:48:21

SQLkafkaMySQL

2022-04-13 22:01:44

错误监控系统

2020-12-09 16:41:22

LinuxIT开发

2024-10-06 12:50:25

2015-09-06 09:22:24

框架搭建快速高效app

2012-07-10 01:22:32

PythonPython教程

2023-11-30 10:21:48

虚拟列表虚拟列表工具库

2022-06-16 07:31:41

Web组件封装HTML 标签
点赞
收藏

51CTO技术栈公众号