如何设计一个高并发、高性能的写磁盘功能?

本文探讨了在高并发场景下,如何优化系统日志的写磁盘逻辑。通过借鉴JVM年轻代内存管理思想,设计了双缓冲区策略,避免频繁的磁盘I/O操作。在日志达到一定量时批量写入,同时采用分段加锁,确保并发安全并提高性能。这种方案类似于Hadoop HDFS的日志处理方式,实现了高效的数据持久化。
摘要由CSDN通过智能技术生成

场景:单系统每秒有10万的访问量,产生了大量操作日志,需要落地到磁盘文件里,如何设计这个写磁盘的逻辑?

为了尽量避免涉及过多的业务逻辑,笔者这里从具体的业务系统中独立出了这个功能。

假设每条日志100字节,每秒10w的访问量,那就是10M的日志,对于性能比较高的硬件设备,不存在什么瓶颈。不过,作为软件开发人员,技术开发能力不应停留在适可而止。

如果没来一条日志,就写一次磁盘,那性能就太差了,频繁的打开关闭文件,肯定影响性能。那就批量写!

于是开辟一块固定大小的内存空间,操作日志先写这块内存,等写满之后,一次性刷到磁盘里。伪代码如下:

public void writeLog(String content) {
    // 多线程写必须加锁
    synchronized (this) {
        localCache.write(content);
        如果缓存满了,刷磁盘
        if (localCache.full()) {
            localCache.flush();
        }
    }
}

可以看到,每次写一条日志,就会加synchronized锁,相当于顺序写,效率不够高,有没有优化空间呢?答案是肯定的!

可以借鉴下jvm年轻代的设计思想,jvm的年轻代分为1个Eden区,2个Survivor区,其中Eden区占80%内存空间,每一块Survivor区各占10%内存空间,比如说Eden区有800MB内存,每一块Survivor区就100MB内存,如下图。

图片

那么借鉴这个思想,我们的系统可以设计成两块缓冲区。先向第一块缓冲区写日志,等到第一块缓冲写满之后,再切换到第二块缓冲区写,并把第一块缓冲里的数据刷到磁盘。

下面直接上代码,代码中会有详细的注释,就不另外解释了。

图片


public OpLog {
    // 全局唯一的递增序列号
    private long txidSeq = 0;
    // 每个线程自己的txid
    private ThreadLocal<Long> localTxid = new ThreadLocal<>();
    // 是否正在调度一次刷盘的操作
    private volatile boolean isSchedulingSync = false;
    // 是否正在刷盘
    private volatile boolean isSyncing = false;
    // 上次刷盘后的最大txid
    private long syncTxid;
    // 缓冲类,管理了两块缓冲区
    private DoubleBuffer doubleBuffer;

    public void writeLog(String content) {
        synchronized (this) {
            try {
                // 如果正在调度刷盘操作,等待1s后重试
                while (isSchedulingSync) {
                    wait(1000);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            txidSeq++;
            localTxid.set(txidSeq);
            // 缓冲区没满,直接返回
            if (!doubleBuffer.full()) {
                return;
            }
            // 缓冲区满了,需要调度刷盘操作
            isSchedulingSync = true;
        }
        // 释放锁,刷盘
        syncLog();
    }

    private void syncLog() {
        synchronized (this) {
            long txid = localTxid.get();
            if (isSyncing) {
                // 小于说明,已经刷过盘了
                if (txid < syncTxid) {
                    return;
                }
                try {
                    while (isSyncing) {
                        wait(1000);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            // 交换两块缓冲区
            doubleBuffer.exchangeBuffer();
            syncTxid = txid;
            isSchedulingSync = false;
            notifyAll();
            isSyncing = true;
        }
        // 此处只有一个线程能到,不用加锁
        try {
            // 执行刷磁盘
            doubleBuffer.flush();
        } catch(Exception e) {
            e.printStackTrace();
        }
        synchronized (this) {
            isSyncing = false;
            notifyAll();
        }
    }
}

总体上,上述代码使用了分段加锁的方式,写文件的时候加锁,缓存没满的时候不停的向内存写,缓存写满后开启刷盘操作,此时再加锁,等切两块缓存切换过后释放锁刷盘,系统又可以向缓存写日志了。

每次写日志文件,如果都发生在内存里,大概需要0.01ms,1ms就可以执行100次操作,1秒就可以执行100 *1000 = 10万次操作,所以说性能是非常高的。

其实,大名鼎鼎的hadoop hdfs写操作日志,就是这样实现的,完美的实现了高并发高性能的写磁盘。

END

如果你喜欢本文,

请长按二维码,关注 南山的架构笔记

图片

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值