disruptor设计方案值得借鉴的地方以及实际中的应用

个人看来,disruptor设计方案中值得借鉴的地方主要有以下几处:

1,环形数据结构

为了避免垃圾回收,使用了数组而非链表,能够快速获取到数据

网上搜索了下,看到了这篇文章利用环形队列解决项目中的实际问题,这里我也自己写了个测试例子:

public class RingTimer {
    private static int currentIndex = 0;

    public static void main(String[] args){
        final Map[] list = new Map[30];
        for(int i=0;i<30;i++){
            list[i] = new ConcurrentHashMap<Integer, Integer>();
        }

        final Map<Integer,Integer> uid2index = new ConcurrentHashMap<Integer, Integer>();

        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                currentIndex = currentIndex%30;
                System.out.println("current check at "+new Date().toString()+",index:"+currentIndex);
                Map map = list[currentIndex];
                if(map.size()>0){
                    for(Object key : map.keySet()){
                        System.out.print(key+"\t");
                        uid2index.remove(key);
                    }
                    map.clear();
                    System.out.println("timeout!");
                }
                currentIndex++;
            }
        },0,1000);
        System.out.println("start timer at:"+new Date().toString());

        for(int i=0;i<10;i++){
            int index = (currentIndex-1+30)%30;
            uid2index.put(i,index);
            list[index].put(i,i);
            System.out.println("add uid:"+i+" to index:"+index+" at:"+new Date().toString());
            try {
                Thread.currentThread().sleep(3000);
            }catch (Exception e){
                e.printStackTrace();
            }

        }

        int count = Integer.MAX_VALUE/2;
        while (count-->0);
    }
}

2,缓存行填充与防伪共享

下面我们写个测试程序来看下缓存行填充对性能的提升:

public class CacheLineTest {
    public static long[][] arr;//一个long占8字节,缓存行一般为64字节,linux下可以通过 cat /proc/cpuinfo 查看cache_alignment的大小来确定缓存行大小

    private static final int SIZE = 4096*4096;

    public static void main(String[] args) {
        arr = new long[SIZE][];//可以设置更大点,更能看出效果
        for (int i = 0; i < SIZE; i++) {
            arr[i] = new long[8];
            for (int j = 0; j < 8; j++) {
                arr[i][j] = i*j+1;
            }
        }
        long sum = 0L;
        long start = System.currentTimeMillis();
        for (int i = 0; i < SIZE; i+=1) {
            for(int j =0; j< 8;j++){//这里相当于从cacheline中访问
                sum += arr[i][j];
            }
        }
        System.out.println("sum="+sum+",time cost with cache line access:" + (System.currentTimeMillis() - start) + "ms");

        sum = 0L;
        start = System.currentTimeMillis();
        for (int i = 0; i < 8; i++) {
            for(int j =0; j< SIZE;j++){
                sum += arr[j][i];
            }
        }
        System.out.println("sum="+sum+",time cost without cache line access:" + (System.currentTimeMillis() - start) + "ms");
    }
}

在本人机器上(i7-3770,3.4Ghz,16G内存)运行结果如下,可以明显看到性能的差距:

sum=3940649573285888,time cost with cache line access:133ms
sum=3940649573285888,time cost without cache line access:685ms

我们再写个程序,看下伪共享对性能的影响:

public final class FalseSharingTest
        implements Runnable
{
    public final static int NUM_THREADS = 8;
    public final static long ITERATIONS = 500L * 1000L * 1000L;
    private final int arrayIndex;

    private static ValueWithoutPadding[] arrs= new ValueWithoutPadding[NUM_THREADS];
    static
    {
        for (int i = 0; i <arrs.length; i++)
        {
            arrs[i] = new ValueWithoutPadding();
        }
    }

    public FalseSharingTest(final int arrayIndex)
    {
        this.arrayIndex = arrayIndex;
    }

    public static void main(final String[] args) throws Exception
    {
        final long start = System.currentTimeMillis();
        runTest();
        System.out.println("duration = " + (System.currentTimeMillis() - start)+"ms");
    }

    private static void runTest() throws InterruptedException
    {
        Thread[] threads = new Thread[NUM_THREADS];

        for (int i = 0; i < threads.length; i++)
        {
            threads[i] = new Thread(new FalseSharingTest(i));
        }

        for (Thread t : threads)
        {
            t.start();
        }

        for (Thread t : threads)
        {
            t.join();
        }
    }

    public void run()
    {
        long i = ITERATIONS + 1;
        while (0 != --i)
        {
            arrs[arrayIndex].value = i;
        }
    }

    public final static class ValueWithPadding {
        protected long p1, p2, p3, p4, p5, p6, p7;
        protected volatile long value = 0L;
        protected long p9, p10, p11, p12, p13, p14;
        protected long p15;
    }
    public final static class ValueWithoutPadding {
        protected volatile long value = 0L;
    }
}

运行结果如下,第一行是做了填充的Value:

duration = 6792ms
duration = 32587ms

3,数组长度2^n,通过位运算而不是取余,加快定位的速度。下标采取递增的形式。不用担心index溢出的问题。index是long类型,即使是10万乃至100万QPS的处理速度。

4,无锁设计

生产者的入队和消费者的出队,都是先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据。disruptor中的无锁体现在对cas的运用上,比如多个生产者的情况下,多个线程重复写同一个元素的问题。Disruptor的解决方法是,每个线程获取到下一个sequence,然后在while循环中采用cas来判断sequence是否被分配出去。不过这样会遇到一个新问题:因为是循环数据结构,线程在读取的时候,读到还未写的元素,因为多生产者下的cursor已经不是用于可以消费的标记了,使用与cas的操作。Disruptor在多个生产者的情况下,引入了一个与RingBuffer大小相同的buffer:availableBuffer。当某个位置写入成功的时候,便把availble Buffer相应的位置置位,我理解这里的置位还是比较巧妙的,举例说明disruptor是如何做的:

我们假设有4个生产者,那初始时availableBuffer=new int[4],且将每个元素的初始值设置为-1,然后生产者开始进行生产:

假设第0轮,sequence分别会是0,1,2,4,然后在availableBuffer中的index=sequence&index_mask, 其中index_mask=3,此时availableBuffer[0...3]=0,表示第0轮已经写入有效了,如果此时消费者获取到的sequence是5,那么应该是消费第1轮写入的数据,此时index=5&3=1,即判断availableBuffer[1]是否等于1就知道生产者是否已经写入数据了。

再思考个问题,为什么不直接将sequence存入availableBuffer,因为这样sequence值会过大,很容易溢出。为什么availableBuffer没有做缓存行填充?availableBuffer可能是一个大数组,其中的每个元素都会改变,但是同一时刻只会有一个线程读取访问同一个index,所以,没必要做缓冲行填充。

cas这个操作虽然是无锁的(对于java语言来说),但是如果并发很大,盲目的在while中使用cas会带来缓存一致性风暴(可以通过尝试几次后让线程阻塞来减少这种情况),实际使用中需要根据实际业务场景来决策。

网上搜了下,发现log4j从2.x版本开始引入了disruptor,下面我们来看下disruptor在log4j中的应用。在官方文档中,有如下说明:

Asynchronous logging can improve your application's performance by executing the I/O operations in a separate thread. Log4j 2 makes a number of improvements in this area.

Asynchronous Loggers are a new addition in Log4j 2. Their aim is to return from the call to Logger.log to the application as soon as possible. You can choose between making all Loggers asynchronous or using a mixture of synchronous and asynchronous Loggers. Making all Loggers asynchronous will give the best performance, while mixing gives you more flexibility.
LMAX Disruptor technology. Asynchronous Loggers internally use the Disruptor, a lock-free inter-thread communication library, instead of queues, resulting in higher throughput and lower latency.
As part of the work for Async Loggers, Asynchronous Appenders have been enhanced to flush to disk at the end of a batch (when the queue is empty). This produces the same result as configuring "immediateFlush=true", that is, all received log events are always available on disk, but is more efficient because it does not need to touch the disk on each and every log event. (Async Appenders use ArrayBlockingQueue internally and do not need the disruptor jar on the classpath.)

大意是log4j2内部使用了无锁的disruptor库,异步记录日志,可以获取搞吞吐和低延迟。log4j2的目标是使得Logger.log尽快返回,不用等待I/O写入磁盘文件。在这之前,log4j追加日志(同步)内部是采用的ArrayBlockingQueue。如果是处于灵活性考虑,可以采用混合的方式。同时官方也给出了性能测试图:

在我们的系统中,大多数情况下都可以采用这种异步的方式可以获得较高的性能。不过如果你的日志记录有极高的可靠性要求,比如你的日志是为了审计用途等,使用异步的追加方式可能就不太合适了,此时应该采用同步的追加方式。log4j2官方文档给出了不适用异步log的几种场景,在本人的实际工作中,是根据业务场景来实际选取的,采用的是异步+同步这种混合的方式,在关键的地方使用同步,其他地方使用异步的方式,这样即使中间信息有遗漏也可以结合上下文log以及代码分析出来具体的业务状况!

disruptor以上的优秀设计思想为我们在项目中的实际队列设计提供了极高的参考价值,是否可以在分布式环境中借鉴这个思想呢?目前我还未看到类似的框架或者paper,后续如果发现了类似的paper以及disruptor在其他场景中有具体应用时再来补充。


参考文章:

https://logging.apache.org/log4j/2.x/manual/async.html#Performance


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值