异步日志
- 0. buffer
- 1. 版本1
- 1.1 介绍
- 1.2 思路
- 1.3 分析
- 1.4 核心代码
- 1.4.1 改进
- 1.4.2 最终版本
- 2. 版本2
- 2.1 思路
- 2.2 核心代码
- 3. [上一篇:简单的日志系统]()
真正的大菜来了。
之前的那个日志滚动仅仅只是一个过渡。前面的实现依靠的是一个全局变量。相当于在依靠一个全局的文件IO锁。当多个线程同时做日志时,会抢一个锁浪费时间。
举例
A线程写日志,抢到锁后,向文件写。
此时其他所有线程如果写日志,则需要在这里同时阻塞。这简直就是灾难。
而异步日志(准确来说是非阻塞日志)恰恰可以解决这个问题。这个类型的问题甚至可以被这种模型解决。
其准确思想就是分为将这个日志系统分为C/S架构。
一个日志线程被我们称为后端。所有在打日志的线程称为前端。
前端线程并非每打一次日志都通知后端,而是将所有的日志信息发送到一个队列
上。然后后端线程从这个队列
上接收数据而后写到文件上。
队列
是一种抽象的说法,实际上使用的是双缓冲技术。基本思路就是前端负责向A填数据(日志消息),后端负责将buffer B的数据写入文件。当buffer A写满之后,就交换A和B。让后端将buffer A的数据写入文件,而前端则往buffer B中继续写。同时设计一个定时时间,即前端没有写满数据,时间一到,也会交换buffer。
0. buffer
前面的设计中,我们都用到了buffer这么一个概念。但是仅仅使用string来做了替代。现在我们特别抽象出一个buffer来。
其学习自muduo::LogStream中的FixBuffer。非常简单。
- 用模板特例化。
FixBuffer<1024>
类似array
的方式使用。 - append。
- 其他的都是STL常用接口。
1. 版本1
1.1 介绍
使用最简单的双缓冲区设置,用来应对日常环境我认为足以。文章中给出的代码一般代表我思考的过程,最终的github地址给出的代码则是最终成型后的版本。我认为coding过程中,不断思考,不断改进才是coding最有趣的地方
。
1.2 思路
思路其实非常简单,就是设置俩个Buffer,日志一开始输出到currentBuffer上去,当currentBuffer满了之后。发生交换。(我们这里仅仅交换指针,这里有个小坑请注意,即数组名作为指针是不能发生交换的,需要做一个假的dummy指针)
char data[SIZE];
char * dummy = data;
char * cur = data;
// 发生交换时
swap(dummy, rhs.dummy);
swap(cur, rhs.cur);
// swap(data, rhs.data); 是一个无效操作
交换后,使用条件变量通知后端线程,后端线程唤醒,然后开始将backBuffer中的数据向文件中写。写完毕后,记得要reset backBuffer的cur指针,即cur = dummy(backBuffer此时指向的数组并非data)。
1.3 分析
这个模型比较有趣。先思考我们的LogFile这个模型。
这个模型就是,一条日志触发一次写操作。
我们都知道,磁盘的读写是非常耗时的,高频率的磁盘读写操作会十分的降低我们程序的性能。
AsyncLogging则将这个模型中间添加了一个buffer。一条日志仅仅触发一次向buffer中的添加操作。当buffer满了的时候才会去触发一次写操作。
将原来的一条日志触发一次写改为了多条日志触发一次写,有效地降低了磁盘写的频率。
1.4 核心代码
// 我这里故意写了是为了方便进行测试,这个buffer的大小可以自己设定
FixBuffer<4*100*1024> currentBuffer;
FixBuffer<4*100*1024> backBuffer;
MutexLock mutex; // 用来保护append操作和writeToFile操作
// append操作
void
AsyncLogging::append(const char*data, int len)
{
MutexLockGuard lock(mutex);
if(currentBuffer.avail() > len)
{ // 最常见的操作
currentBuffer.append(data, len);
}
else{ // currentBuffer满了,需要进行swap
currentBuffer.swap(backBuffer);
// TODO 验证交换过来的currentBuffer
currentBuffer.append(data, len);
cond.notify();
}
}
// 后端线程写,所以是一个单独线程操作
void
AsyncLogging::writeToFile()
{
LogFile logFile(basename, rollSize, interval);
while(isRunning)
{
MutexLockGuard lock(mutex);
if(backBuffer.empty()) // 这里用了if,是希望interval事件到了,即使没有写的东西,也去触发日志滚动。
cond.waitForSecond(interval);
logFile.append(backBuffer.data(), backBuffer.length());
logFile.flush(); // 这个可以移动到锁外面
backBuffer.reset();
}
}
1.4.1 改进
不难发现,我们后端这个writeToFile这里的临界区是有点长的。
可以的改进就是当cond被唤醒之后,我们将backBuffer中的数据移动到栈区域。然后backBuffer就可以reset了。
void
AsyncLogging::threadFunc()
{
assert(isRunning_ == true);
latch_.countDown();
LogFile logFile(basename_, rollSize_, interval_);
LargeBuffer newBuffer; // 做成成员变量更好一些
while(isRunning_)
{
{
MutexLockGuard lock(mutex_);
// 如果backBuffer_为empty,后端等待
if(!backBuffer_.length()) // 注意这里使用的是if而不是while,
// 我们希望interval时间到了之后,不再等待
cond_.waitForSeconds(interval_);
// 将backBuffer中的数据交给newBuffer
// 这里实际上也可以用swap来做
newBuffer.swap(backBuffer_);
}
logFile.append_unlocked(newBuffer.data(), newBuffer.length());
newBuffer.reset();
logFile.flush();
}
// 这里可能会有认为存在一个析构先后关系的问题。即newBuffer析构的时候,此时其实际data存储的数据
// 可能是其他,如果此时再次执行日志输出,可能会引发segment fault。
// 1. stop的需要一些设计。要保证stop的时候,其他线程不能在append
// 2. 或者可以将newBuffer做成AsyncLogging的成员。注释中要表明其只能被后端线程使用。
}
其本质上如这幅图表示。有三段内存。
currBuffer满了,此时和backBuffer进行交换
交换完毕后,currBuffer变空了,backBuffer变满了
然后backBuffer和newBuffer进行交换
最终结果就是newBuffer变满了,然后将里面的数据写向文件。而currBuffer又可以append数据。因为newBuffer是不会暴露给其他线程的。所以其可以在临界区域外执行。注意要在其内数据写向文件后,进行reset中心变回最开始的状态。
其中俩段临界区内均只是指针的变化。所以非常高效。
那么限制这里log速度最终瓶颈就变成了磁盘的写速度。这就不是编程能解决的问题了。
但是这里有个缺陷,就是在interval时间到达的时候,我们希望其应该将currBuffer中的数据也应该输出到文件中去。而这里是没有的。
1.4.2 最终版本
void
AsyncLogging::threadFunc()
{
assert(isRunning_ == true);
latch_.countDown();
LogFile logFile(basename_, rollSize_, interval_);
LargeBuffer newBuffer;
newBuffer.reset();
while(isRunning_)
{
{
MutexLockGuard lock(mutex_);
// 如果backBuffer_为empty,后端等待
if(!backBuffer_.length()) // 注意这里使用的是if而不是while,
// 我们希望interval时间到了之后,不再等待
cond_.waitForSeconds(interval_);
// 将backBuffer中的数据交给newBuffer
if(backBuffer_.length())
{
newBuffer.swap(backBuffer_);
}
else{ // 因为时间到了唤醒
newBuffer.swap(currentBuffer_);
}
}
logFile.append_unlocked(newBuffer.data(), newBuffer.length());
newBuffer.reset();
logFile.flush();
}
}
2. 版本2
这个是muduo实现的版本。muduo使用的实际上是4缓冲区的方式。前提是需要boost::指针容器中的一些知识。
这里给出参考:
- 英语好的直接去看boost官方文档
- 英语不好的去看《boost程序库探秘》
2.1 思路
muduo采用的是所谓4缓冲区的方式。
append思路
- 如果currBuffer足够大,就直接currBuffer.append(data, len);
- 如果不够,就将其按照移动语义的方式扔到个buffer指针数组中去。然后把nextBuffer调上来。
- 如果nextBuffer也被用了,就去新开辟一个buffer。
writeToFile思路。
- 临界区内,按照移动语义方式将currBuffer扔到指针数组中去。
- 临界区内,填充currBuffer和nextBuffer
- 临界区内,将指针数组中的指针交换到局部变量中去。
- 临界区
外
,开始写操作。然后进行琐碎的收尾工作。
知道大概思路,而且掌握了指针容器,这里才好写
2.2 核心代码
MutexLock mutex;
Condition cond;
using BufferPtrVector = boost::ptr_vector<LargeBuffer>;
using BufferMovablePtr = BufferPtrVector::auto_type;
BufferMovablePtr currBuffer;
BufferMovablePtr nextBuffer;
BufferPtrVector buffers;
// BufferMovablePtr的使用方式类似unique_ptr
void
AsyncLogging::append(const char*data, int len)
{
MutexLockGuard lock(mutex);
if(currBuffer->avail() > len)
currBuffer->append(data, len);
else{
// 放弃currBuffer的拥有权
buffers.push_back(currBuffer.release());
if(nextBuffer)
currentBuffer = ptr_container::move(nextBuffer);
else{ // 此时currentBuffer内部没有指针管理
currentBuffer.reset(new LargeBuffer);
}
currBuffer->append(data, len);
cond.notify();
}
}
void
AsyncLogging::writeToFileThread()
{
BufferMovablePtr newBuffer1(new LargeBuffer);
BufferMovablePtr newBuffer2(new LargeBuffer);
BufferPtrVector localBuffers;
LogFile log(basename, rollSize, interval);
while(running)
{
{
MutexLockGuard lock(mutex);
if(buffers.empty())
cond.waitForSeconds(interval);
buffers.push_back(currBuffer.release());
localBuffers.swap(buffers);
currBuffer = ptr_container::move(newBuffer1);
if(!nextBuffer) // nextBuffer被用完了
nextBuffer = ptr_container::move(newBuffer2);
}
for(auto iter = localBuffer.begin(); iter != localBuffer.end(); ++iter)
{
log.append(iter->data(), iter->length());
log.fflush();
}
localBuffers.reset(); // 释放所有指针的伪代码,简单做法
newBuffer1.reset(new LargeBuffer);
newBuffer2.reset(new LargeBuffer);
}
}
就localBuffer的释放指针操作而言,muduo这里有非常优雅的做法。
if(localBuffers.size() > 2)
localBuffer.resize(2);
newBuffer1 = ptr_vector::move(localBuffers.front());
newBuffer2 = ptr_vector::move(localBuffers.back());
newBuffer1->reset();
newBuffer2->reset();
localBuffer.pop_back();
localBuffer.pop_back();
localBuffer.clear();
3. 上一篇:简单的日志系统