• 分布式ID(CosId)之号段链模式性能(1.2亿/s)解析


    分布式ID(CosId)之号段链模式性能(1.2亿/s)解析

    上一篇文章《分布式ID生成器(CosId)设计与实现》我们已经简单讨论过CosId的设计与实现全貌。

    但是有很多同学有一些疑问:CosId的号段链模式(SegmentChainId)性能有些难以置信(TPS峰值性能1.2亿/s),甚至是不同性能级别号段分发器(RedisIdSegmentDistributorJdbcIdSegmentDistributor)均能够达到这样的性能级别。

    所以本篇文章将深度解析CosId的号段链模式(SegmentChainId)的设计思路与实现优化。

    新同学最好先查阅上一篇文章《分布式ID生成器(CosId)设计与实现》

    背景(为什么需要SegmentChainId

    通过上一篇文章《分布式ID生成器(CosId)设计与实现》我们知道号段模式(SegmentId)主要有以下问题:

    • 稳定性:SegmentId的稳定性问题主要是因为号段用完之后获取ID的线程需要同步进行NextMaxId导致的(会产生网络IO)。
    • 步长(Step):设置多大的步长是一个权衡问题,设置太小会影响整体性能,设置太大会导致全局ID乱序的程度增加。
    • 本机单调递增、全局趋势递增: 本地单调递增是我们期望看到的,但是如何理解/衡量全局趋势递增呢。下面我们来解释一下什么是全局趋势递增:

    号段模式

    从上图的号段模式设计中我们可以看出:

    • Instance 1每次获取的NextMaxId,一定比上一次大,意味着下一次的号段一定比上一次大,即F(Tn+1)>F(Tn),所以从单实例上来看是单调递增的。
    • Instance 1Instance 2实例各自持有的不同的号段,意味着在一个时间段内不同实例生成的ID是乱序的。但是多实例在获取NextMaxId时是单调递增的,所以整体趋势的递增,即全局趋势递增。

    趋势递增

    全局趋势递增反向说明的是ID在一个时间周期内是会乱序的,所以我们要尽可能让ID的乱序程度降低。这是一个优化点。

    号段模式生成的ID,在数据库视角可以近似的理解为上面的趋势递增图(Tn>Tn-s)。ID乱序的程度受到步长(Step)影响,步长越小ID乱序的程度越小。这里我们使用边界值(Step=1)作一下说明。

    • Step=1时,即每次都获取NextMaxId,将无限接近单调递增(数据库视角)。需要注意的是这里是无限接近而非等于单调递增,具体原因你可以思考一下这样一个场景:
      • 号段分发器T1时刻给Instance 1分发了ID=1,T2时刻给Instance 2分发了ID=2。因为机器性能、网络等原因,Instance 2网络IO写请求先于Instance 1到达。那么这个时候对于数据库来说,ID依然是乱序的。

    所以不难理解的是影响全局ID乱序的因素有俩个:集群规模、Step大小。集群规模是我们不能控制的,但是Step是可以调节的。

    所以SegmentChainId就是在这样的背景下诞生的。

    号段链模式(SegmentChainId)的优势

    号段链模式

    通过SegmentChainId设计图中我们可以看到,号段链模式新增了一个角色PrefetchWorker
    PrefetchWorker主要的职责是维护和保证号段链头部到尾部的安全距离,也可以近似理解为缓冲距离。
    有了安全距离的保障不难得出的结论是所有获取ID的线程只要从进程内存的号段里边获取下次ID即可,理想情况下不需要再进行NextMaxId(向号段分发器请求NextMaxId,网络IO)的,所以性能可以达到近似AtomicLongTPS 性能:12743W+/s的级别。

    SegmentChainIdSegmentId的增强版,相比于SegmentId有以下优势:

    • TPS性能:可达到近似 AtomicLongTPS 性能:12743W+/s JMH 基准测试。通过引入了新的角色PrefetchWorker用以维护和保证安全距离,理想情况下使得获取ID的线程几乎完全不需要进行同步的等待NextMaxId获取。
    • 稳定性:P9999=0.208(us/op),通过上面的TPS性能描述中我们可以看到,SegmentChainId消除了同步等待的问题,所以稳定性问题也因此迎刃而解。
    • 适应性:从SegmentId介绍中我们知道了影响ID乱序的因素有俩个:集群规模、Step大小。集群规模是我们不能控制的,但是Step是可以调节的。
      • Step应该尽可能小才能使得ID单调递增的可能性增大。
      • Step太小会影响吞吐量,那么我们如何合理设置Step呢?答案是我们无法准确预估所有时点的吞吐量需求,那么最好的办法是吞吐量需求高时,Step自动增大,吞吐量低时Step自动收缩。
      • SegmentChainId引入了饥饿状态的概念,PrefetchWorker会根据饥饿状态检测当前安全距离是否需要膨胀或者收缩,以便获得吞吐量与有序性之间的权衡,这便是SegmentChainId的自适应性。
      • 所以在使用SegmentChainId时我们可以配置一个比较小的Step步长,然后由PrefetchWorker根据吞吐量需求自动调节安全距离,来自动伸缩步长。

    常见问题

    RedisIdSegmentDistributor、JdbcIdSegmentDistributor 均能够达到TPS=1.2亿/s?

    RedisChainIdBenchmark-Throughput

    RedisChainIdBenchmark-Throughput

    MySqlChainIdBenchmark-Throughput

    MySqlChainIdBenchmark-Throughput

    上面的两张图给许多同学带来了困扰,为什么在Step=1000的时候RedisChainIdBenchmarkMySqlChainIdBenchmarkTPS性能几乎一致(TPS=1.2亿/s)。
    RedisIdSegmentDistributor应该要比JdbcIdSegmentDistributor性能更高才对啊,为什么都能达到AtomicLong性能上限呢?
    如果我说当Step=1时,只要基准测试的时间够长,那么他们依然能够达到AtomicLong性能级别(TPS=1.2亿/s),你会不会更加困惑。
    其实这里的障眼法PrefetchWorker饥饿膨胀导致的,SegmentChainId的极限性能跟分发器的TPS性能没有直接关系,因为最终都可以因饥饿膨胀到性能上限,只要给足够的时间膨胀。
    而为什么在上图的Step=1时TPS差异还是很明显的,这是因为RedisIdSegmentDistributor膨胀得更快,而基准测试又没有给足测试时间而已。

    SegmentChainId基准测试TPS极限性能可以近似使用以下的公式的表示:

    TPS(SegmentChainId)极限值=(Step*Expansion)*TPS(IdSegmentDistributor)*T/s<=TPS(AtomicLong)

    1. <=TPS(AtomicLong):因为SegmentChainId的内部号段就是使用的AtomicLong,所以这是性能上限。
    2. Step*ExpansionExpansion可以理解为饥饿膨胀系数,默认的饥饿膨胀系数是2。在MySqlChainIdBenchmarkMySqlChainIdBenchmark基准测试中这个值是一样的。
    3. TPS(IdSegmentDistributor): 这是公式中唯一的不同。指的是请求号段分发器NextMaxId的TPS。
    4. T: 可以理解为基准测试运行时常。

    从上面的公式中不难看出RedisChainIdBenchmarkMySqlChainIdBenchmark主要差异是分发器的TPS性能。
    分发器的TPS(IdSegmentDistributor)越大,达到TPS(AtomicLong)所需的T就越少。但只要T足够长,那么任何分发器都可以达到近似TPS(AtomicLong)
    这也就解释了为什么不同TPS性能级别的号段分发器(IdSegmentDistributor)都可以达到TPS=1.2亿/s。

    CosId需要部署服务端吗?

    CosId是以本地SDK的形式存在的,用户只需要安装一下CosId的依赖包做一些简单配置(快速开始DEMO)即可。

    分布式ID是不适合使用服务端部署模式的(C/S)。使用服务端部署模式,必然会产生网络IO(Client通过远程过程调用Server,获取ID),你想想我们费了那么大劲消除网络IO是为了什么?

    PrefetchWorker 是如何维护安全距离的?

    • 定时维护:每隔一段时间PrefetchWorker会主动检测安全距离是否满足配置要求,如果不满足则执行NextMaxId预取,保证安全距离。
    • 被动饥饿唤醒:当获取ID的线程获取ID时没有可用号段,会尝试获取新的号段,并主动唤醒PrefetchWorker并告诉他你太慢了,被唤醒的PrefetchWorker会检测安全距离是否需要膨胀,然后进行安全距离的维护。

    本机单调、全局趋势递增-为什么还要尽可能保证单调递增?

    从上文的论述中我们不难理解本机单调递增,全局趋势递增是权衡后的设计结果。
    但是全局趋势递增的背面是周期内ID乱序,所以尽可能向单调递增优化(降低ID乱序程度)是优化目标,这俩点并不冲突。

    如果各位同学还有其他问题请至 Issues 提交你的疑问。

    作者:Ahoo Wang (阿虎)

    Github: https://github.com/Ahoo-Wang/

    SmartSql(高性能、高生产力,超轻量级的ORM!): https://github.com/Ahoo-Wang/SmartSql

    SmartCode(不只是代码生成器!): https://github.com/Ahoo-Wang/SmartCode

    CoSky 高性能、低成本微服务治理平台 : https://github.com/Ahoo-Wang/CoSky

    CosId 通用、灵活、高性能的分布式 ID 生成器 : https://github.com/Ahoo-Wang/CosId

    Govern EventBus 历经多年生产环境验证的事件驱动架构框架: https://github.com/Ahoo-Wang/govern-eventbus


    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

  • 相关阅读:
    全局变量 static变量
    【Qt学习笔记】04_单选复选框
    【Qt学习笔记】03_特殊标签
    【Qt学习笔记】02_颜色对话框
    【Qt学习笔记】01_模态和非模态
    ThinkPad_E570 拆机
    VMware 共享文件夹
    【安装Flutter遇到的问题】 Android license status unknown
    VLC 外挂字幕乱码
    IE(IE6/IE7/IE8)支持HTML5标签--20150216
  • 原文地址:https://www.cnblogs.com/Ahoo-Wang/p/cosid-segment-chain.html
Copyright © 2020-2023  润新知