一、超高并发,超高性能分布式ID服务的要求
在复杂的超高并发,分布式系统中,往往需要对大量的数据和消息进行唯一标识。
如在高并发,分布式的金融,支付,餐饮,酒店,电影等产品的系统中,数据日渐增长,对数据库分库分表后需要有一个唯一的标识一条数据或消息,数据库的自增ID显然不能满足需求;特别一点的如订单,骑手,优惠券也都需要有唯一的ID做标识。
此时一个能够生成全局唯一ID的系统是非常必要的。
概括下来,那业务系统对ID号的要求有哪些呢?
①、全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求
②、趋势递增:在MySQL InnoDB引擎中使用的是聚簇索引,由于多数RDBMS使用B-Tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能
③、单调递增:保证下一个ID一定是大于上一个ID,例如事务版本号,IM增量消息,排序等特殊需求。
④、信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更加危险了,可以直接知道我们一天的订单量,所以在一些应用场景下,会需要ID无规则。
注意:1234对应不同的场景
特别注意:3和4需求还是互斥的,无法使用同一个方案满足
同时除了对ID号码自身的要求,业务还对ID生成系统的可用性要求极高,并且处于业务的黄金链路上。
想象一下,如果ID生成系统瘫痪,整个系统黄金链路上关键动作都无法执行,这就会带来一场灾难。
二、有序ID能提升写入性能的根本原因:InnoDB存储引擎的数据页分裂
因为在mysql当中,我们写数据是写入到innodb当中的,而且底层的聚簇索引是分页存储的,每个页的大小默认是16kb,页与页之间是用双向链表关联的。
在查找的时候是二分查找,时间复杂度是o(logN)是最高性能的。但是写入的时候是低性能的,而且不能随机的写入,必须保证主键有序,页内ID是有序的。
在聚簇索引当中有两个约束:
①、ID有序存储
②、分页存储
如果在上面的过程当中,ID是无序的话,在一个页当中是满的话,把id插入进入的话,会发生页的分裂:一个页会分成两个页,并且在聚簇索引里面还有非聚簇索引字段,这个时候其他的数据也要进行磁盘的复制移动,会带来高频的磁盘IO。
三、ID生成服务的阶段
①、各自封装
企业内部,早期业务量比较少,各个系统基本都有自己的ID生成模块,类型也是五花八门。有基于TIDB自增的ID,有基于UUID的,也有基于雪花算法的。其中雪花算法也被称为snowflake,使用最为广泛。各自封装模块比较简单,但是实现分散,各自系模块的质量也很难统一保证。
②、集成框架
为了解决上述分散实现的问题,企业内部统一实现了一个综合各类ID生成功能的基础库,供业务方统一调用。
统一基础库解决了分散调用的问题,但是对于snowflake这种带有workerId的算法,需要业务系统关注workerId分配的逻辑
于是,我们把snowflake的逻辑封装到了服务治理框架内,服务启动时,由框架来负责workerId的分配和服务内的唯一性。
③、idGen服务
封装到框架后,同一服务的不同实例之间可以很好的处理workerId的分配的问题。
但是,workerID的逻辑也使得服务内多个实例成为了有状态实例,k8s部署也只能使用statefulset。随着业务量突飞猛进,系统数量增加,业务对系统的稳定性,弹性提出了更多的需求。我们需要ID生成逻辑非常稳定,高效,我们需要服务实例都是无状态实例Deployment,使服务具备快速滚动升级,弹性伸缩的能力。
基于这样的背景,我们决定提供一个单独的ID生成服务,需求如下:
①、支持DB号段模式和snowflake模式两种模式
②、ID生成器自身的可用性,稳定性非常高,具备时钟校准能力
③、吞吐量高,TP99:3毫秒内生成,必须非常低
④、兼容现有逻辑,业务迁移要非常方便
⑤、服务使用Deployment部署
四、ID服务架构设计
DB号段模式
号段模式,简单来说可以理解为对DB自增ID方案的优化
本质上是利用批量获取的方式,定期获取一个号段,缓存在本地供外部使用,减轻DB的压力,提升对外服务性能。
从上往下:
sdk层:
sdk给到第三方的应用sdk的去快速的生成id,单节点的要求:至少100万的qps压测的时候golang sdk600w的qps。java的吞吐量也是大几十万qps
接入层:
由k8s svc:kube-proxy负载云原生的负载均衡。并没有用springcloud gateway
流量通过K8s ingress组件反向代理把流量进来进入到svc:部署Ingress使用
HostNetWork+daemonSet+nodeSelector来保证高可用,选择k8s的两个节点做ingress的部署节点。ingress边缘节点保证高可用选择keepAlivevps来做id漂移的高可用:这里只是同一个idc机房做到高可用:同一个网段做到高可用,实现单个服务的故障的转移,还要做到同城多活/异地多活,idc机房之间的高可用。
号段微服务层:
sdk一定是高并发的。获取id并不是通过号段服务来获取id的,通过号段服务拿到id的区段,之后id的
分配由sdk在内存分配,因为在同内存具备高性能的分配。
号段服务无状态的,功能极致的弱化:伸缩,通过k8s的自动伸缩的资源控制器HPA,进行横向扩展,分配ID号段的。
如果把后端ID服务的压力变小的话,可以把号段弄大一点。
号段弄大的,比如弄成1百万,如果号段很大,每启动一次会浪费1百万个,id是long型的,2的32次方就到了10亿级别。如果无符号到了3-40亿,中间浪费了几百万id的空洞也没问题,而且 业务应用也不会频繁的启动。
号段服务,保证db:存id的区段的高可用:引用db的压力也不会大。
DB层:
DB层也要保证同城多活/异地多活。
snowflake模式
snowflake是Tiwitter于2010年首次对外公开,其值是64位整数,可以做到全局唯一
构造如下:
雪花算法的 ID 由以下几个部分组成:符号位:1 个 bit,始终为 0,用于区分正数和负数。时间戳:41 个 bit,精确到毫秒级别。使用当前时间减去一个固定的开始时间,可以得到一个时间差 值。由于时间戳占用了 41 个 bit,最大可表示的时间为 2^41 / (1000 * 60 * 60 * 24 * 365) = 69 年左右。数据中心 ID:5 个 bit,用于区分不同的数据中心。如果没有多个数据中心,可以将其设置为 0。机器 ID:5 个 bit,用于区分同一数据中心内不同的机器。同样地,如果没有多台机器,可以将其设置为 0。序列号:12 个 bit,用于区分同一毫秒内生成的不同 ID。由于序列号只有 12 个 bit,最大可表示的序列号为 2^12 - 1 = 4095。如果在同一毫秒内生成的序列号超过了 4095,需要等到下一毫秒再生成新的ID。综上所述,一个雪花算法生成的 ID 长度为 64 bit,可以保证在分布式系统中生成唯一的 ID。