Ahoo-Wang

分布式ID生成器(CosId)设计与实现

CosId 简介

CosId 旨在提供通用、灵活、高性能的分布式 ID 生成器。 目前提供了俩类 ID 生成器:

  • SnowflakeId : 单机 TPS 性能:409W/s JMH 基准测试 , 主要解决 时钟回拨问题机器号分配问题 并且提供更加友好、灵活的使用体验。
  • SegmentId: 每次获取一段 (Step) ID,来降低号段分发器的网络IO请求频次提升性能。
    • IdSegmentDistributor: 号段分发器(号段存储器)
      • RedisIdSegmentDistributor: 基于 Redis 的号段分发器。
      • JdbcIdSegmentDistributor: 基于 Jdbc 的号段分发器,支持各种关系型数据库。
    • SegmentChainId(推荐):SegmentChainId (lock-free) 是对 SegmentId 的增强。性能可达到近似 AtomicLongTPS 性能:12743W+/s JMH 基准测试
      • PrefetchWorker 维护安全距离(safeDistance), 并且支持基于饥饿状态的动态safeDistance扩容/收缩。

背景(为什么需要分布式ID

在软件系统演进过程中,随着业务规模的增长,我们需要进行集群化部署来分摊计算、存储压力,应用服务我们可以很轻松做到无状态、弹性伸缩。
但是仅仅增加服务副本数就够了吗?显然不够,因为性能瓶颈往往是在数据库层面,那么这个时候我们就需要考虑如何进行数据库的扩容、伸缩、集群化,通常使用分库、分表的方式来处理。
那么我如何分片(水平分片,当然还有垂直分片不过不是本文需要讨论的内容)呢,分片得前提是我们得先有一个ID,然后才能根据分片算法来分片。(比如比较简单常用的ID取模分片算法,这个跟Hash算法的概念类似,我们得先有key才能进行Hash取得插入槽位。)

当然还有很多分布式场景需要分布式ID,这里不再一一列举。

分布式ID方案的核心指标

  • 全局(相同业务)唯一性:唯一性保证是ID的必要条件,假设ID不唯一就会产生主键冲突,这点很容易可以理解。
    • 通常所说的全局唯一性并不是指所有业务服务都要唯一,而是相同业务服务不同部署副本唯一。
      比如 Order 服务的多个部署副本在生成t_order这张表的Id时是要求全局唯一的。至于t_order_item生成的IDt_order是否唯一,并不影响唯一性约束,也不会产生什么副作用。
      不同业务模块间也是同理。即唯一性主要解决的是ID冲突问题。
  • 有序性:有序性保证是面向查询的数据结构算法(除了Hash算法)所必须的,是二分查找法(分而治之)的前提。
    • MySq-InnoDB B+树是使用最为广泛的,假设 Id 是无序的,B+ 树 为了维护 ID 的有序性,就会频繁的在索引的中间位置插入而挪动后面节点的位置,甚至导致频繁的页分裂,这对于性能的影响是极大的。那么如果我们能够保证ID的有序性这种情况就完全不同了,只需要进行追加写操作。所以 ID 的有序性是非常重要的,也是ID设计不可避免的特性。
  • 吞吐量/性能(ops/time):即单位时间(每秒)能产生的ID数量。生成ID是非常高频的操作,也是最为基本的。假设ID生成的性能缓慢,那么不管怎么进行系统优化也无法获得更好的性能。
    • 一般我们会首先生成ID,然后再执行写入操作,假设ID生成缓慢,那么整体性能上限就会受到限制,这一点应该不难理解。
  • 稳定性(time/op):稳定性指标一般可以采用每个操作的时间进行百分位采样来分析,比如 CosId 百分位采样 P9999=0.208 us/op,即 0% ~ 99.99% 的单位操作时间小于等于 0.208 us/op
    • 百分位数 WIKI :统计学术语,若将一组数据从小到大排序,并计算相应的累计百分点,则某百分点所对应数据的值,就称为这百分点的百分位数,以Pk表示第k百分位数。百分位数是用来比较个体在群体中的相对地位量数。
    • 为什么不用平均每个操作的时间:马老师的身价跟你的身价能平均么?平均后的值有意义不?
    • 可以使用最小每个操作的时间、最大每个操作的时间作为参考吗?因为最小、最大值只说明了零界点的情况,虽说可以作为稳定性的参考,但依然不够全面。而且百分位数已经覆盖了这俩个指标。
  • 自治性(依赖):主要是指对外部环境有无依赖,比如号段模式会强依赖第三方存储中间件来获取NexMaxId。自治性还会对可用性造成影响。
  • 可用性:分布式ID的可用性主要会受到自治性影响,比如SnowflakeId会受到时钟回拨影响,导致处于短暂时间的不可用状态。而号段模式会受到第三方发号器(NexMaxId)的可用性影响。
    • 可用性 WIKI:在一个给定的时间间隔内,对于一个功能个体来讲,总的可用时间所占的比例。
    • MTBF:平均故障间隔
    • MDT:平均修复/恢复时间
    • Availability=MTBF/(MTBF+MDT)
    • 假设MTBF为1年,MDT为1小时,即Availability=(365*24)/(365*24+1)=0.999885857778792≈99.99%,也就是我们通常所说对可用性4个9。
  • 适应性:是指在面对外部环境变化的自适应能力,这里我们主要说的是面对流量突发时动态伸缩分布式ID的性能,
    • SegmentChainId可以基于饥饿状态进行安全距离的动态伸缩。
    • SnowflakeId常规位分配方案性能恒定409.6W,虽然可以通过调整位分配方案来获得不同的TPS性能,但是位分配方法的变更是破坏性的,一般根据业务场景确定位分配方案后不再变更。
  • 存储空间:还是用MySq-InnoDB B+树来举例,普通索引(二级索引)会存储主键值,主键越大占用的内存缓存、磁盘空间也会越大。Page页存储的数据越少,磁盘IO访问的次数会增加。总之在满足业务需求的情况下,尽可能小的存储空间占用在绝大多数场景下都是好的设计原则。

不同分布式ID方案核心指标对比

分布式ID 全局唯一性 有序性 吞吐量 稳定性(1s=1000,000us) 自治性 可用性 适应性 存储空间
UUID/GUID 完全无序 3078638(ops/s) P9999=0.325(us/op) 完全自治 100% 128-bit
SnowflakeId 本地单调递增,全局趋势递增(受全局时钟影响) 4096000(ops/s) P9999=0.244(us/op) 依赖时钟 时钟回拨会导致短暂不可用 64-bit
SegmentId 本地单调递增,全局趋势递增(受Step影响) 29506073(ops/s) P9999=46.624(us/op) 依赖第三方号段分发器 受号段分发器可用性影响 64-bit
SegmentChainId 本地单调递增,全局趋势递增(受Step、安全距离影响) 127439148(ops/s) P9999=0.208(us/op) 依赖第三方号段分发器 受号段分发器可用性影响,但因安全距离存在,预留ID段,所以高于SegmentId 64-bit

有序性(要想分而治之·二分查找法,必须要维护我)

刚刚我们已经讨论了ID有序性的重要性,所以我们设计ID算法时应该尽可能地让ID是单调递增的,比如像表的自增主键那样。但是很遗憾,因全局时钟、性能等分布式系统问题,我们通常只能选择局部单调递增、全局趋势递增的组合(就像我们在分布式系统中不得不的选择最终一致性那样)以获得多方面的权衡。下面我们来看一下什么是单调递增与趋势递增。

有序性之单调递增

单调递增

单调递增:T表示全局绝对时点,假设有Tn+1>Tn(绝对时间总是往前进的,这里不考虑相对论、时间机器等),那么必然有F(Tn+1)>F(Tn),数据库自增主键就属于这一类。
另外需要特别说明的是单调递增跟连续性递增(F(n+1)=F(n)+step)是不同的概念。

有序性之趋势递增

趋势递增

趋势递增:Tn>Tn-s,那么大概率有F(Tn)>F(Tn-s)。虽然在一段时间间隔内有乱序,但是整体趋势是递增。从上图上看,是有上升趋势的(趋势线)。

  • SnowflakeIdn-s受到全局时钟同步影响。
  • 在号段模式(SegmentId)中n-s受到号段可用区间(Step)影响。

分布式ID分配方案

UUID/GUID

分类:

技术点:

相关文章:

  • 2021-11-13
  • 2021-12-28
  • 2018-10-30
  • 2021-11-13
  • 2021-10-20
  • 2022-01-21
  • 2021-04-15
猜你喜欢
  • 2021-12-15
  • 2021-06-26
  • 2021-06-30
  • 2021-07-01
  • 2021-07-03
  • 2021-07-08
  • 2022-01-01
相关资源
相似解决方案