disruptor 高性能之道
disruptor是一个高性能的线程间异步通信的框架,即在同一个JVM进程中的多线程间消息传递。应用disruptor知名项目有如下的一些:Storm, Camel, Log4j2,还有目前的美团点评技术团队也有很多不少的应用,或者说有一些借鉴了它的设计机制。 下面就跟着笔者一起去领略下disruptor高性能之道吧~
disruptor是一款开源的高性能队列框架,github地址为 https://github.com/LMAX-Exchange/disruptor。
分析disruptor,只要把event的生产和消费流程弄懂,基本上disruptor的七寸就已经抓住了。话不多说,赶紧上车,笔者以下面代码为例讲解disruptor:
public static void main(String[] args) {
    Disruptor<StringEvent> disruptor = new Disruptor<>(StringEvent::new, 1024,
            new PrefixThreadFactory("consumer-pool-", new AtomicInteger(0)), ProducerType.MULTI,
            new BlockingWaitStrategy());
    // 注册consumer并启动
    disruptor.handleEventsWith((EventHandler<StringEvent>) (event, sequence, endOfBatch) -> {
        System.out.println(Util.threadName() + "onEvent " + event);
    });
    disruptor.start();
    // publisher逻辑
    Executor executor = Executors.newFixedThreadPool(2,
            new PrefixThreadFactory("publisher-pool-", new AtomicInteger(0)));
    while (true) {
        for (int i = 0; i < 2; i++) {
            executor.execute(() -> {
                Util.sleep(1);
                disruptor.publishEvent((event, sequence, arg0) -> {
                    event.setValue(arg0 + " " + sequence);
                }, "hello world");
            });
        }
        Util.sleep(1000);
    }
}
class StringEvent {
    private String value;
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }
    @Override
    public String toString() {
        return "StringEvent:{value=" + value + "}";
    }
}
class PrefixThreadFactory implements ThreadFactory {
    private String prefix;
    private AtomicInteger num;
    public PrefixThreadFactory(String prefix, AtomicInteger num) {
        this.prefix = prefix;
        this.num = num;
    }
    @Override
    public Thread newThread(Runnable r) {
        return new Thread(r, prefix + num.getAndIncrement());
    }
}
class Util {
    static String threadName() {
        return String.format("%-16s", Thread.currentThread().getName()) + ": ";
    }
    static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
测试相关类
event生产流程
- 获取待插入(到ringBuffer的)位置,相当于先占个位
- 往该位置上设置event
- 设置sequence对应event的标志,通知consumer
public <A> void publishEvent(EventTranslatorOneArg<E, A> translator, A arg0)
{
// 获取当前要设置的sequence序号,然后进行设置并通知消费者
final long sequence = sequencer.next();
translateAndPublish(translator, sequence, arg0);
} // 获取下一个sequence,直到获取到位置才返回
public long next(int n) {
long current;
long next; do {
// 获取当前ringBuffer的可写入sequence
current = cursor.get();
next = current + n; long wrapPoint = next - bufferSize;
long cachedGatingSequence = gatingSequenceCache.get(); if (wrapPoint > cachedGatingSequence || cachedGatingSequence > current) {
// 如果当前没有空位置写入,获取多个consumer中消费进度最小的那个的消费进度
long gatingSequence = Util.getMinimumSequence(gatingSequences, current); if (wrapPoint > gatingSequence) {
// 阻塞1ns,然后continue
LockSupport.parkNanos(1); // TODO, should we spin based on the wait strategy?
continue;
} gatingSequenceCache.set(gatingSequence);
}
// cas设置ringBuffer的sequence
else if (cursor.compareAndSet(current, next)) {
break;
}
} while (true); return next;
} private <A> void translateAndPublish(EventTranslatorOneArg<E, A> translator, long sequence, A arg0) {
try {
// 设置event
translator.translateTo(get(sequence), sequence, arg0);
} finally {
sequencer.publish(sequence);
}
}
public void publish(final long sequence) {
// 1. 设置availableBuffer,表示对应的event是否设置完成,consumer线程中会用到
// - 注意,到这里时,event已经设置完成,但是consumer还不知道该sequence对应的event是否设置完成,
// - 所以需要设置availableBuffer中sequence对应event的sequence number
// 2. 通知consumer
setAvailable(sequence);
waitStrategy.signalAllWhenBlocking();
}
从translateAndPublish中看,如果用户的设置event方法抛出异常,这时event对象是不完整的,那么publish到consumer端,consumer消费的不是完整的数据怎么办呢?在translateAndPublish中需不需要在异常情况下reset event对象呢?关于这个问题笔者之前是有疑问的,关于这个问题笔者提了一个issue,可点击 https://github.com/LMAX-Exchange/disruptor/issues/244 进行查看。
笔者建议在consumer消费完event之后,进行reset event操作,这样避免下次设置event异常consumer时取到不完整的数据,比如log4j2中的AsyncLogger中处理完log4jEvent之后就会调用clear方法进行重置event。
event消费流程
- 获取当前consumer线程消费的offset,即nextSequence
- 从ringBuffer获取可用的sequence,没有新的event时,会根据consmer阻塞策略进行执行某些动作
- 获取event,然后执行event回调
- 设置当前consumer线程的消费进度
private void processEvents() {
    T event = null;
    long nextSequence = sequence.get() + 1L;
    while (true) {
        try {
            // 获取可用的sequence,默认直到有可用sequence时才返回
            final long availableSequence = sequenceBarrier.waitFor(nextSequence);
            if (batchStartAware != null) {
                batchStartAware.onBatchStart(availableSequence - nextSequence + 1);
            }
            // 执行消费回调动作,注意,这里获取到一个批次event,可能有多个,个数为availableSequence-nextSequence + 1
            // nextSequence == availableSequence表示该批次只有一个event
            while (nextSequence <= availableSequence) {
                // 获取nextSequence位置上的event
                event = dataProvider.get(nextSequence);
                // 用户自定义的event 回调
                eventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);
                nextSequence++;
            }
            // 设置当前consumer线程的消费进度sequence
            sequence.set(availableSequence);
        } catch (final Throwable ex) {
            exceptionHandler.handleEventException(ex, nextSequence, event);
            sequence.set(nextSequence);
            nextSequence++;
        }
    }
}
public long waitFor(final long sequence)
        throws AlertException, InterruptedException, TimeoutException{
    long availableSequence = waitStrategy.waitFor(sequence, cursorSequence, dependentSequence, this);
    if (availableSequence < sequence) {
        return availableSequence;
    }
    // 获取ringBuffer中可安全读的最大的sequence number,该信息存在availableBuffer中的sequence
    // 在MultiProducerSequencer.publish方法中会设置
    return sequencer.getHighestPublishedSequence(sequence, availableSequence);
}
// 默认consumer阻塞策略 BlockingWaitStrategy
public long waitFor(long sequence, Sequence cursorSequence, Sequence dependentSequence, SequenceBarrier barrier)
    throws AlertException, InterruptedException
{
    long availableSequence;
    if (cursorSequence.get() < sequence) {
        // 当前ringBuffer的sequence小于sequence,阻塞等待
        // event生产之后会唤醒
        synchronized (mutex) {
            while (cursorSequence.get() < sequence) {
                barrier.checkAlert();
                mutex.wait();
            }
        }
    }
    while ((availableSequence = dependentSequence.get()) < sequence) {
        barrier.checkAlert();
        ThreadHints.onSpinWait();
    }
    return availableSequence;
}
从上面的event消费流程来看,消费线程会读取ringBuffer的sequence,然后更新本消费线程内的offset(消费进度sequence),如果有多个event的话,那么就是广播消费模式了(单consumer线程内还是顺序消费),如果不想让event被广播消费(重复消费),可使用如下方法添加consumer线程(WorkHandler是集群消费,EventHandler是广播消费):
disruptor.handleEventsWithWorkerPool((WorkHandler<StringEvent>) event -> {
    System.out.println(Util.threadName() + "onEvent " + event);
});
disruptor高性能之道
event生产流程中获取并自增sequence时用的就是CAS,获取之后该sequence对应位置的操作只会在单线程,没有了并发问题。
集群消费模式下获取sequence之后也会使用CAS设置为sequence新值,设置本地消费进度,然后再执行获取event并执行回调逻辑。
注意,disruptor中较多地方使用了CAS,但并不代表完全没有了锁机制,比如默认consumer阻塞策略 BlockingWaitStrategy发挥作用时,consumer消费线程就会阻塞,只不过这只会出现在event生产能力不足是才会存在。如果consumer消费不足,大量event生产导致ringBuffer爆满,这时event生产线程就会轮询调用LockSupport.parkNanos(1),这里的成本也不容小觑(涉及到线程切换损耗)。
伪共享讲的是多个CPU时的123级缓存的问题,通常,缓存是以缓存行的方式读取数据,如果A、B两个变量被缓冲在同一行之内,那么对于其中一个的更新会导致另一个缓冲无效,需要从内存中读取,这种无法充分利用缓存行的问题就是伪共享。disruptor相关代码如下:
class LhsPadding {
    protected long p1, p2, p3, p4, p5, p6, p7;
}
class Value extends LhsPadding {
    protected volatile long value;
}
ringBuffer是一个环形队列,本质是一个数组,size为2的幂次方(方便做&操作),数据位置sequence值会和size做&操作得出数组下标,然后进行数据的读写操作(只在同一个线程内,无并发问题)。
disruptor初衷是为了解决内存队列的延迟问题,作为一个高性能队列,包括Apache Storm、Camel、Log4j 2在内的很多知名项目都在使用。disruptor的重要机制就是CAS和RingBuffer,借助于它们两个实现数据高效的生产和消费。
disruptor多生产者多消费者模式下,因为RingBuffer数据的写入是分为2步的(先获取到个sequence,然后写入数据),如果获取到sequence之后,生产者写入RingBuffer较慢,consumer消费较快,那么生产者最终会拖慢consumer消费进度,这一点需注意(如果已经消费到生产者占位的前一个数据了,那么consumer会执行对应的阻塞策略)。在实际使用过程中,如果consumer消费逻辑耗时较长,可以封装成任务交给线程池来处理,避免consumer端拖慢生成者的写入速度。
disruptor的设计对于开发者来说有哪些借鉴的呢?尽量减少竞争,避免多线程对同一数据做操作,比如disruptor使用CAS获取只会在一个线程内进行读写的event对象,这种思想其实已经在JDK的thread本地内存中有所体现;尽量复用对象,避免大量的内存申请释放,增加GC损耗,disruptor通过复用event对象来保证读写时不会产生对象GC问题;选择合适数据结构,disruptor使用ringBuffer,环形数组来实现数据高效读写。
参考资料:
disruptor 高性能之道的更多相关文章
- Netty 系列之 Netty 高性能之道
		1. 背景 1.1. 惊人的性能数据 最近一个圈内朋友通过私信告诉我,通过使用 Netty4 + Thrift 压缩二进制编解码技术,他们实现了 10 W TPS(1 K 的复杂 POJO 对象)的跨 ... 
- Netty系列之Netty高性能之道
		转载自http://www.infoq.com/cn/articles/netty-high-performance 1. 背景 1.1. 惊人的性能数据 最近一个圈内朋友通过私信告诉我,通过使用Ne ... 
- Netty高性能之道
		1. 背景 1.1. 惊人的性能数据 最近一个圈内朋友告诉我,通过使用Netty4 + Thrift压缩二进制编解码技术,他们实现了10W TPS(1K的复杂POJO对象)的跨节点远程服务调用.相比于 ... 
- 转:Netty系列之Netty高性能之道
		1. 背景 1.1. 惊人的性能数据 最近一个圈内朋友通过私信告诉我,通过使用Netty4 + Thrift压缩二进制编解码技术,他们实现了10W TPS(1K的复杂POJO对象)的跨节点远程服务调用 ... 
- 【读后感】Netty 系列之 Netty 高性能之道 - 相比 Mina 怎样 ?
		[读后感]Netty 系列之 Netty 高性能之道 - 相比 Mina 怎样 ? 太阳火神的漂亮人生 (http://blog.csdn.net/opengl_es) 本文遵循"署名-非商 ... 
- Netty 系列之 Netty 高性能之道   高性能的三个主题   Netty使得开发者能够轻松地接受大量打开的套接字   Java 序列化
		Netty系列之Netty高性能之道 https://www.infoq.cn/article/netty-high-performance 李林锋 2014 年 5 月 29 日 话题:性能调优语言 ... 
- Disruptor 高性能并发框架二次封装
		Disruptor是一款java高性能无锁并发处理框架.和JDK中的BlockingQueue有相似处,但是它的处理速度非常快!!!号称“一个线程一秒钟可以处理600W个订单”(反正渣渣电脑是没体会到 ... 
- Netty(五)Netty 高性能之道
		4.背景介绍 4.1.1 Netty 惊人的性能数据 通过使用 Netty(NIO 框架)相比于传统基于 Java 序列化+BIO(同步阻塞 IO)的通信框架,性能提升了 8 倍多.事 实上,我对这个 ... 
- 从构建分布式秒杀系统聊聊Disruptor高性能队列
		前言 秒杀架构持续优化中,基于自身认知不足之处在所难免,也请大家指正,共同进步.文章标题来自码友 简介 LMAX Disruptor是一个高性能的线程间消息库.它源于LMAX对并发性,性能和非阻塞算法 ... 
随机推荐
- (后端)mybatis中使用Java8的日期LocalDate、LocalDateTime
			原文地址:https://blog.csdn.net/weixin_38553453/article/details/75050632 MyBatis的型处理器是属性“createdtime参数映射为 ... 
- spring学习总结——装配Bean学习一(自动装配)
			一.Spring配置的可选方案 Spring容器负责创建应用程序中的bean并通过DI来协调这些对象之间的关系.但是,作为开发人员,你需要告诉Spring要创建哪些bean并且如何将其装配在一起.当描 ... 
- Jupyter Notebook默认工作路径的修改
			相信每一个学习Python的童鞋,都尝试过Jupyter Notebook,所以我也就不多介绍,真的还不错哎这软件. 不过美中不足的,就是它的默认工作路径,每次打开都是系统盘的Administrato ... 
- postgresql自定义类型并返回数组
			转自 https://blog.csdn.net/victor_ww/article/details/44415895 create type custom_data_type as ( id int ... 
- window.onunload中使用HTTP请求
			在页面关闭时触发window.onunload 在onunload中要使用http请求,需要使用同步请求: 如: $.ajax({ url: url, async: false }); iframe页 ... 
- 8. svg学习笔记-文本
			毫无疑问,文本也是svg中组成的重要部分,在svg中,用<text>元素来创建文本,文本的使用格式如下: <text x="20" y="30" ... 
- 学习笔记---json和xml区别
			测试web时经常和网页数据打交道,会遇到json格式和xml格式,整理整理,记录下来. json最常用的格式是键值对. {"firstName": "Brett" ... 
- array_walk函数与call_user_func_array函数
			一, php手册的解释: call_user_func_array - 调用回调函数,并把一个数组参数作为回调函数的参数 说明: mixed call_user_func_array ( cal ... 
- 《Java大学教程》—第8章 通过继承扩展类
			8.2 继承(inheritance):继承是指在类之间共享属性和方法.继承关系是一种层次关系.在继承关系中位于顶部的类称为超类(或基类),位于下面的类称为子类(或派生类).类型转换(type ... 
- ORM版学员管理系统2
			学生信息管理 展示学生信息 URL部分 url(r'^student_list/', app01_views.student_list, name="student_list"), ... 
