浅谈Disruptor

news/2024/7/3 1:35:39

Disruptor是一个低延迟(low-latency),高吞吐量(high-throughput)的事件发布订阅框架。通过Disruptor,可以在一个JVM中发布事件,和订阅事件。相对于Java中的阻塞队列(ArrayBlockingQueue,LinkedBlockingQueue),Disruptor的优点是性能更高。它采用了一种无锁的数据结构设计,利用环形数组(RingBuffer)来存放事件,通过对象复用减少垃圾回收进一步提高性能。

从"慢日志"说起

线上有一个接口最近频繁报警(tp99变高),通过监控报警系统定位到问题主要出现在日志打印环节。接口方法入参和出参都会打印"info"日志,我们采用的日志是logback。它默认的是同步打印日志,在日志报文过大时,磁盘IO耗时会变得更加明显。某个慢请求90%的处理时间都消耗在日志打印中。于是我们决定采用异步的方式打印日志。sl4j2日志框架支持异步的日志打印,改成异步日志打印之后接口性能报警消失。而sl4j2高性能的秘密就在于Disruptor。

Disruptor解决的问题

设想一下,在一个JVM中当我们有多个消息的生产者线程,一个消费者线程时,他们之间如何进行高并发、线程安全的协调?很简单,用一个阻塞队列。 当我们有多个消息的生产者线程,多个消费者线程,并且每一条消息需要被所有的消费者都消费一次(这就不是一般队列,只消费一次的语义了),该怎么做? 这时仍然需要一个队列。但是:

  1. 每个消费者需要自己维护一个指针,知道自己消费了队列中多少数据。这样同一条消息,可以被多个人独立消费。
  2. 队列需要一个全局指针,指向最后一条被所有生产者加入的消息。消费者在消费数据时,不能消费到这个全局指针之后的位置——因为这个全局指针,已经是代表队列中最后一条可以被消费的消息了。
  3. 需要协调所有消费者,在消费完所有队列中的消息后,阻塞等待。
  4. 如果消费者之间有依赖关系,即对同一条消息的消费顺序,在业务上有固定的要求,那么还需要处理谁先消费,谁后消费同一条消息的问题。

总而言之,如果有多个生产者,多个消费者,并且同一条消息要给到所有的消费者都去处理一下,需要做到以上4点。这是不容易的。 LMAX Disruptor,正是这种场景下,满足以上4点要求的单机跨线程消息传递、分发的开源、高性能实现。

关键概念

  1. RingBuffer 应用需要传递的消息在Disruptor中称为Event(事件)。 RingBuffer是Event的数组,实现了阻塞队列的语义: 如果RingBuffer满了,则生产者会阻塞等待。 如果RingBuffer空了,则消费者会阻塞等待。

  2. Sequence 在上文中,我提到“每个消费者需要自己维护一个指针”。这里的指针就是一个单调递增长整数(及其基于CAS的加法、获取操作),称为Sequence。 除了每个消费者需要维护一个指针外,RingBuffer自身也要维护一个全局指针(如上一节第2点所提到的),记录最后一条可以被消费的消息。

生产场景实现

生产者往RingBuffer中发送一条消息(RingBuffer.publish())时:

  1. 生产者的私有sequence会+1
  2. 检查生产者的私有sequence与RingBuffer中Event个数的关系。如果发现Event数组满了(下图红框中的判断),则阻塞(下图绿框中的等待)。
  3. RingBuffer会在Event数组中(sequencer+1) % BUFFER_SIZE的地方,放入Event。这里的取模操作,就体现了Event数组用到最后,则回到头部继续放,所谓"Ring" Buffer的轮循复用语义。

消费场景实现

消费者从RingBuffer循环队列中获取一条消息时:

  1. 从消费者私有Sequence,可以知道它自己消费到了RingBuffer队列中的哪一条消息。
  2. 从RingBuffer的全局指针Sequence,可以知道RingBuffer中最后一条没有被消费的消息在什么位置。
  3. N = (RuingBuffer的全局指针Sequence - 消费者私有Sequence),就是当前消费者,还可以消费多少Event。
  4. 如果以上差值N为0,说明当前消费者已经消费过RingBuffer中的所有消息了。那么当前消费者会阻塞。等待生产者加入更多的消息。
  5. 如果RingBuffer中,还有可以被当前消费者消费的Event,即N > 0, 那么消费者,会一口气获取所有可以被消费的N个Event。这种一口气消费尽量多的Event,是高性能的体现。 从RingBuffer中每获取一个Event,都会回调绿框中的eventHandler——这是应用注册的Event处理方法,执行应用的Event消费业务逻辑。

高性能的实现细节

  1. 无锁,无锁就没有锁竞争。当生产者、消费者线程数很高时,意义重大。所以, 往大里说,每个消费者维护自己的Sequence,基本没有跨线程共享的状态。 往小里说,Sequence的加法是CAS实现的。 当生产者需要判断RingBuffer是否已满时,用CAS比较原先RingBuffer的Event个数,和假定放入新Event后Event的个数。 如果CAS返回false,说明在判断期间,别的生产者加入了新Event;或者别的消费者拿走了Event。那么当前判断无效,需要重新判断。

  2. 对象的复用,JVM运行时,一怕创建大对象,二怕创建很多小对象。这都会导致JVM堆碎片化、对象元数据存储的额外开销大。这是高性能Java应用的噩梦。 为了解决第二点“很多小对象”,主流开源框架都会自己维护、复用对象池。LMAX Disruptor也不例外。 生产者不是创建新的Event对象,放入到RingBuffer中。而是从RingBuffer中取出一个已有的Event对象,更新它所指向的业务数据,来代表一个逻辑上的新Event。


http://lihuaxi.xjx100.cn/news/236134.html

相关文章

Webhint开源了一种代码检查工具

Webhint项目提供了一种用于检查代码的可访问性、性能和安全的开源检查(Linting)工具。在创建Web站点和应用中,有越来越多的细节问题亟待完善。为此,Webhint力图帮助开发人员标记这些细节。\\Webhint以命令行接口(CLI&a…

Python基础01-Python环境搭建与HelloWorld

目录 从今天开始学习Python Python环境搭建 安装gcc Python源码包安装 开始Python第一个代码HelloWorld! 从今天开始学习Python 为啥选择Python,可能是跟随潮流吧。我现在不知道为什么学习Python,但是可能一年到一年半以后,…

构造函数不能为虚/重载函数总结

构造函数不能为虚/重载函数总结 作为一个类,他最基础的成员函数就要数构造函数了。这里我们先探讨一下构造函数为什么不能是虚函数。 在解决这个问题之前,要先明白类中函数的调用方式。一个类的函数共用一个函数空间,因此在实例化的对象中是不…

Python基础02-Python基础

脚本的第一行 Python脚本的第一行,写Python解释器的路径。这样就可以直接执行Python脚本。 脚本编码 Python2需要指定脚本的编码,Python3不需要指定。 # -*- coding:utf8 -*- 使用input做简单的交互 username input(请输入用户名密码:) password …

JVM GC算法

可达性算法 - 对象生死判定算法 Java通过可达性分析来判定对象是否还被引用。什么是可达性分析呢: Java会从一些叫做GCRoot的对象开始向下遍历,可以遍历到的对象,就是被引用的对象,不可以遍历到的对象就是不可达对象,就…

手把手教你如何新建scrapy爬虫框架的第一个项目(下)

前几天小编带大家学会了如何在Scrapy框架下创建属于自己的第一个爬虫项目(上),今天我们进一步深入的了解Scrapy爬虫项目创建,这里以伯乐在线网站的所有文章页为例进行说明。在我们创建好Scrapy爬虫项目之后,会得到上图…

拜托,面试别再问我时间复杂度了!!!

最烦面试官问,“为什么XX算法的时间复杂度是OO”,今后,不再惧怕这类问题。 快速排序分为这么几步: 第一步,先做一次partition; partition使用第一个元素tarr[low]为哨兵,把数组分成了两个半区&a…

Python基础03-运算符

运算符 算数运算符 算数运算符符号运算数字用法举例字符串用法举例加a 2 3 print(a) # 5s1 "hello" s2 "world" s s1 s2 print(s) # helloworld-减a 12 - 3 print(a) # 9*乘a 12 * 3 print(a) # 36s1 "hello" s2 "world" s…