场景:单系统每秒有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
如果你喜欢本文,
请长按二维码,关注 南山的架构笔记