【项目】 :C++ 高性能日志系统设计与实现(代码实现)
- 一、日志系统框架设计
-
- 模块划分
-
- 1. 日志等级模块
- 2. 日志消息模块
- 3. 日志消息格式化模块
- 4. 日志消息落地模块
- 5. 日志器模块
- 6. 日志器管理模块
- 7. 异步线程模块
- 模块关系图
- 二、代码设计
-
- 2.1 实用类设计 util.hpp
- 2.2 日志等级类设计 level.hpp
- 2.3 日志消息类设计 message.hpp
- 2.4 日志输出格式化类设计 formatter.hpp
- 2.5 日志落地类设计(简单工厂模式)sink.hpp
- 2.6 日志器类设计(建造者模式)logger.hpp
- 2.7 双缓冲区异步任务处理器设计 looper.hpp
- 2.8 异步日志器设计 logger.hpp
- 2.9 单例日志器管理类设计(单例模式)logger.hpp
- 2.10 日志宏&全局接⼝设计(代理模式)log.h
- 使用样例
- 三、性能测试代码 bench.h
一、日志系统框架设计
本项目实现的是一个 多日志器日志系统,主要功能是让程序员能够轻松地将程序运行日志信息落地到指定位置,并支持 同步 与 异步 两种落地方式。
模块划分
1. 日志等级模块
对输出日志的等级进行划分,以便于控制日志输出,并提供等级枚举转字符串功能。
- OFF:关闭
- DEBUG:调试,调试时的关键信息输出
- INFO:提示,普通提示型日志信息
- WARN:警告,不影响运行,但需要注意
- ERROR:错误,程序运行出现错误
- FATAL:致命,一般是代码异常导致程序无法继续运行
2. 日志消息模块
中间存储日志输出所需的各项信息。
- 时间:日志输出时间
- 线程ID:输出日志的线程ID
- 日志等级:日志的等级
- 日志数据:日志有效载荷数据
- 日志文件名:输出日志的源文件名
- 日志行号:输出日志的源文件行号
3. 日志消息格式化模块
设置日志输出格式,并提供格式化功能。
默认日志输出格式: %d{%H:%M:%S}%T[%t]%T[%p]%T[%c]%T%f:%l%T%m%n
- %d{格式}:日期时间
- %T:制表符缩进
- %t:线程ID
- %p:日志级别
- %c:日志器名称(不同开发组可使用不同日志器,互不影响)
- %f:源文件名
- %l:源代码行号
- %m:日志有效载荷数据
- %n:换行
设计思想:设计不同的子类,从日志消息中提取并处理不同的数据。
4. 日志消息落地模块
决定日志的落地方向。
- 标准输出:日志打印到标准输出
- 日志文件输出:将日志写入指定文件末尾
- 滚动文件输出:按文件大小控制,达到上限切换下一个文件
- 扩展方向:支持远程日志输出,将日志发送到远程分析服务器
设计思想:设计不同的子类以控制不同的日志落地方向。
5. 日志器模块
整合前面几个模块,用户通过日志器进行日志输出,降低使用难度。 包含:
- 日志消息落地模块对象
- 日志消息格式化模块对象
- 日志输出等级
6. 日志器管理模块
为了降低项目耦合度,不同项目组可拥有自己的日志器,控制输出格式与落地方向。
- 统一管理所有日志器
- 提供一个默认日志器进行标准输出
7. 异步线程模块
- 实现异步日志输出功能。 用户只需将日志任务放入任务池,由异步线程负责落地输出,实现高效的非阻塞日志输出。
模块关系图
二、代码设计
2.1 实用类设计 util.hpp
提前完成一些零碎的功能接口,方便在项目中使用。
#pragma once
/*
通⽤功能类,与业务⽆关的功能实现
1. 获取系统时间
2. 获取⽂件是否存在
3. 获取⽂件所在⽬录
4. 创建⽬录
*/
#include <iostream>
#include <ctime>
#include <string>
#include <sys/stat.h>
#include <sys/types.h>
namespace Log
{
namespace util
{
class date
{
public:
static size_t now()
{
//time_t 是一个 平台相关的类型,在不同操作系统/编译器上可能不同
//强转成目的是为了让返回值是一个 平台统一、明确大小、无符号整数类型:
return (size_t)time(nullptr);
}
};
class file
{
public:
static bool exists(const std::string &name)
{
// access stat 函数可以判断文件是否存在
// 但是access是linux下系统调用接口 不具备可移植性
struct stat st;
return stat(name.c_str(), &st) == 0;
}
static std::string path(const std::string &name)
{
if (name.empty())
return ".";
// 为了兼容不太的操作系统, 我们需要找到'/\\'。
// 但是\\'是转义字符,我们需要用\\\\来表示
size_t pos = name.find_last_of("/\\\\");
if (pos == std::string::npos)
return ".";
return name.substr(0, pos + 1);
}
static void create_directory(const std::string &name)
{
if (name.empty())
return;
if (exists(name))
return;
size_t pos = 0, idx = 0;
while (idx < name.size())
{
pos = name.find_first_of("/\\\\", idx);
if (pos == std::string::npos)
{
// "abc"
//mkdir 函数不能创建普通文件,它的作用是创建目录(文件夹)
mkdir(name.c_str(), 0755);
return;
}
// "/abc/acd/aaa/" 当没有./ 的时候
if (pos == idx)
{
idx = pos + 1;
continue;
}
// "./abc/acd"
std::string subdir = name.substr(0, pos);
// 判断是否存在
if (exists(subdir))
{
idx = pos + 1;
continue;
}
mkdir(subdir.c_str(), 0755);
idx = pos + 1;
}
}
};
}
}
2.2 日志等级类设计 level.hpp
日志等级共分为7个等级,具体含义如下:
OFF | 关闭所有日志输出 |
DEBUG | 进行调试时打印日志的等级 |
INFO | 打印一些用户提示信息 |
WARN | 打印警告信息 |
ERROR | 打印错误信息 |
FATAL | 打印致命错误信息,导致程序崩溃 |
#pragma once
#include <iostream>
namespace Log
{
class LogLevel
{
public:
enum class value // c++11 引进的更安全的枚举
{
UNKNOW = 0,
DEBUG, // DRBUG 进⾏debug时候打印⽇志的等级
INFO, // INFO 打印⼀些⽤⼾提⽰信息
WARN, // WARN 打印警告信息
ERROR, // ERROR 打印错误信息
FATAL, // FATAL 打印致命信息- 导致程序崩溃的信息
OFF // OFF 关闭所有⽇志输出
};
static const char *tostring(LogLevel::value level)
{
switch (level)
{
case LogLevel::value::DEBUG:
return "DEBUG";
case LogLevel::value::INFO:
return "INFO";
case LogLevel::value::WARN:
return "WARN";
case LogLevel::value::ERROR:
return "ERROR";
case LogLevel::value::FATAL:
return "FATAL";
case LogLevel::value::OFF:
return "OFF";
default:
return "UNKNOW";
}
return "UNKNOW";
}
};
}
2.3 日志消息类设计 message.hpp
日志消息类主要封装一条完整日志消息所需的内容,包括:
- 日志等级(LogLevel)
- 对应的 Logger 名称(logger name)
- 打印日志的源文件名和行号
- 线程 ID
- 时间戳信息
- 具体的日志内容(文本)
#pragma once
#include "util.hpp"
#include "level.hpp"
#include <thread>
namespace Log
{
class LogMsg
{
public:
size_t _ctime; // 时间
size_t _line; // 行号
std::thread::id _tid; // 线程ID
std::string _file; // 文件名
std::string _name; // 日志器名称
std::string _payload; // 日志消息
LogLevel::value _level; // 日志等级
public:
LogMsg(size_t line, std::string file, std::string name, std::string payload, LogLevel::value level)
: _ctime(util::date::now()),
_line(line),
_tid(std::this_thread::get_id()),
_file(file),
_name(name),
_payload(payload),
_level(level)
{
}
};
}
2.4 日志输出格式化类设计 formatter.hpp
日志格式化(Formatter)类主要负责格式化日志消息。其主要包含以下内容:
-
pattern 成员:保存日志输出的格式字符串。支持的格式化标记包括:
- %d:日期
- %T:缩进(制表符)
- %t:线程ID
- %p:日志级别
- %c:日志器名称
- %f:文件名
- %l:行号
- %m:日志消息
- %n:换行
-
items 成员:std::vector<FormatItem::ptr>,用于按序保存格式化字符串对应的子格式化对象。
FormatItem 类主要负责日志消息子项的获取及格式化。包含以下子类:
-
MsgFormatItem 表示要从 LogMessage 中取出有效日志数据(日志消息内容)
-
LevelFormatItem 表示要从 LogMessage 中取出日志等级
-
NameFormatItem 表示要从 LogMessage 中取出日志器名称
-
ThreadFormatItem 表示要从 LogMessage 中取出线程ID
-
TimeFormatItem 表示要从 LogMessage 中取出时间戳,并按照指定格式进行格式化
-
CFileFormatItem 表示要从 LogMessage 中取出源码所在文件名
-
CLineFormatItem 表示要从 LogMessage 中取出源码所在行号
-
TabFormatItem 表示一个制表符缩进
-
NLineFormatItem 表示一个换行符
-
OtherFormatItem 表示非格式化的原始字符串
代码实现:
#pragma once
#include "message.hpp"
#include <vector>
#include <assert.h>
#include <iostream>
#include <memory>
#include <sstream>
#include <utility>
#include <cstdlib>
namespace Log
{
class FormatItem
{
public:
using ptr = std::shared_ptr<FormatItem>;
//virtual ~FormatItem() {}
virtual void format(std::ostream &out, const LogMsg &msg) = 0;
};
class TimeFormatItem : public FormatItem
{
private:
std::string _format; // 将时间戳转化成某种格式
public:
TimeFormatItem(const std::string &format = "%H:%M:%S") : _format(format)
{
if (format.empty())
_format = "%H:%M:%S";
}
void format(std::ostream &out, const LogMsg &msg) override
{
time_t t = msg._ctime;
struct tm lt;
localtime_r(&t, <);
char tmp[128];
strftime(tmp, 127, _format.c_str(), <);
out << tmp;
}
};
class CFileFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._file;
}
};
class LevelFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
// level是自定义类型,这里我们要自己实现一个tostring
out << LogLevel::tostring(msg._level);
}
};
class CLineFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._line;
}
};
class NameFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._name;
}
};
class MsgFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._payload;
}
};
class ThreadFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._tid;
}
};
class NlineFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << "\\n";
}
};
class TabFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << "\\t";
}
};
class OtherFormatItem : public FormatItem
{ // 输出任意用户定义的字符串(可能是空格、符号、固定文本等)。
private:
std::string _str;
public:
OtherFormatItem(std::string str = "") : _str(str) {}
void format(std::ostream &out, const LogMsg &msg) override
{
out << _str;
}
};
class Formatter
{
// %d ⽇期
// %T 缩进
// %t 线程id
// %p ⽇志级别
// %c ⽇志器名称
// %f ⽂件名
// %l ⾏号
// %m ⽇志消息
// %n 换
private:
std::string _pattern; // 自定义的格式化样式
std::vector<FormatItem::ptr> _items; // 存储 实例化Item 的数组
public:
using ptr = std::shared_ptr<Formatter>;
Formatter(const std::string pattern = "[%d{%H:%M:%S}][%t][%p][%c][%f:%l] %m%n")
: _pattern(pattern)
{
assert(parsePattern());
}
void format(std::ostream &out, const LogMsg &msg)
{
for (auto &it : _items)
{
it->format(out, msg);
}
}
std::string format(const LogMsg &msg)
{
std::stringstream ss;
format(ss,msg);
return ss.str();
}
// %d ⽇期
// %T 缩进
// %t 线程id
// %p ⽇志级别
// %c ⽇志器名称
// %f ⽂件名
// %l ⾏号
// %m ⽇志消息
// %n 换
//"[%d{%H:%M:%S}][%t][%p][%c][%f:%l] %m%n"
FormatItem::ptr CreateItem(const std::string &key, const std::string &val)
{
if (key == "d")
return std::make_shared<TimeFormatItem>(val);
if (key == "T")
return std::make_shared<TabFormatItem>();
if (key == "t")
return std::make_shared<ThreadFormatItem>();
if (key == "p")
return std::make_shared<LevelFormatItem>();
if (key == "c")
return std::make_shared<NameFormatItem>();
if (key == "f")
return std::make_shared<CFileFormatItem>();
if (key == "l")
return std::make_shared<CLineFormatItem>();
if (key == "m")
return std::make_shared<MsgFormatItem>();
if (key == "n")
return std::make_shared<NlineFormatItem>();
if (key == "")
return std::make_shared<OtherFormatItem>(val);
std::cerr << "未知的格式化字符%" << key << "!!!" << std::endl;
abort();
}
private:
// 解析pattern 是否合法性
bool parsePattern()
{
std::vector<std::pair<std::string, std::string>> arry;
std::string key, val;
size_t pos = 0;
while (pos < _pattern.size()) // abcsd%%asd[%d{%H:%M:%S}][%t][%p][%c][%f:%l]
{
// 如果_pattern[pos] 不是‘%’ 则这些字符都不是格式化字符
if (_pattern[pos] != '%')
{
val.push_back(_pattern[pos]);
pos++;
continue;
}
// 如果 _pattern[pos] == '%' 我们需要判断 是否是 '%%'
if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%')
{
val.push_back('%');
pos += 2;
continue;
}
// 将用户自定的非格式化字符插入到arry中
if (!val.empty())
{
arry.push_back(std::make_pair("", val));
val.clear(); // 清空val
}
// 如果是正常的格式化字符 我们需要判断是否存在子规则,%d{%H:%M:%S}
pos++; // 这里指的是 % 之后的格式化字符
if (pos == _pattern.size())
{
std::cerr << "%之后缺少格式化字符!!!" << std::endl;
return false;
}
key.push_back(_pattern[pos]);
pos++;
if (pos < _pattern.size() && _pattern[pos] == '{')
{
pos++;
while (pos < _pattern.size() && _pattern[pos] != '}')
{
val.push_back(_pattern[pos]);
pos++;
}
// while循环 可能因为 pos 越界而退出,所以我们需要判断
if (pos == _pattern.size())
{
std::cerr << "子规则'{}'匹配失败!!!" << std::endl;
return false;
}
pos++;
}
arry.push_back(std::make_pair(key, val));
key.clear();
val.clear();
}
for (auto &it : arry)
{
_items.push_back(CreateItem(it.first, it.second));
}
return true;
}
};
}
2.5 日志落地类设计(简单工厂模式)sink.hpp
日志落地类主要负责将日志消息落地到指定目标。
主要内容:
-
Formatter 日志格式化器 负责格式化日志消息。
-
mutex 互斥锁 保证多线程环境下日志落地过程的线程安全,避免日志输出交叉混乱。
类成员函数 log 设置为纯虚函数(接口),方便新增日志输出目标时继承该类并重写 log 方法,实现具体的日志落地逻辑。
已实现的日志落地方式:
StdoutSink | 标准输出 |
FileSink | 固定文件输出 |
RollSink | 滚动文件输出 |
滚动日志文件输出的必要性:
- 机器磁盘空间有限,不能无限向单个文件追加数据。
- 超大日志文件难以打开和查找信息。
- 实际开发中会控制单个日志文件大小,例如当超过 1GB 时,重新创建新的日志文件写入。
- 过期日志通常由运维人员定时清理,或通过系统定时任务自动清理。
日志文件滚动策略: 日志文件滚动条件主要有两个
文件大小 当日志文件大于指定大小(如 1GB)时,切换到新的日志文件。
时间 按天等周期定点滚动日志文件。
本项目基于文件大小判断,实现日志文件滚动生成新文件。
#pragma once
#include "formatter.hpp"
#include <fstream>
#include <time.h>
namespace Log
{
class LogSink
{
public:
using ptr = std::shared_ptr<LogSink>;
virtual ~LogSink() {}
virtual void log(const char *data, size_t len) = 0;
};
class StdOutSink : public LogSink
{
public:
void log(const char *data, size_t len) override
{
// 这里不能使用 cout << 因为它是通过 '\\0'判断结尾的
std::cout.write(data, len);
}
};
class FileSink : public LogSink
{
public:
FileSink(const std::string &filename)
: _filename(filename)
{
// 创建文件所在的路径上的文件夹
util::file::create_directory(util::file::path(_filename));
// 文件操作句柄统一管理起来
_ofs.open(_filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
void log(const char *data, size_t len) override
{
_ofs.write(data, len);
if (_ofs.good() == false) // good函数用于检测流对象的当前状态是否“良好”,即没有发生任何错误。
{
std::cout << "⽇志输出⽂件失败!\\n";
}
}
private:
std::string _filename; // 文件的名字
std::ofstream _ofs; // 文件输出流
};
class RollBySizeSink : public LogSink
{
public:
RollBySizeSink(const std::string &basename, size_t max_fsize)
: _basename(basename),
_max_fsize(max_fsize),
_cur_fsize(0)
{
std::string filename = create_file();
// 创建文件所在的路径上的文件夹
util::file::create_directory(util::file::path(filename));
// 文件操作句柄统一管理起来
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
void log(const char *data, size_t len) override
{
initLogFile();
_ofs.write(data, len);
if (_ofs.good() == false) // good函数用于检测流对象的当前状态是否“良好”,即没有发生任何错误。
{
std::cout << "⽇志输出⽂件失败!\\n";
}
_cur_fsize += len;
}
private:
void initLogFile() // 如果文件超出大小则需要创造文件进行写入
{
if (_cur_fsize >= _max_fsize)
{
_ofs.close();
// 创建文件所在的路径上的文件夹
std::string filename = create_file();
util::file::create_directory(util::file::path(filename));
// 文件操作句柄统一管理起来
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
_cur_fsize = 0;
}
}
std::string create_file()
{
/*
当日志数据在短时间内迅速增长,
文件大小达到了设定的最大值(例如,一秒钟内),
文件输出流 (_ofs) 可能没有及时关闭并创建一个新的日志文件,
这导致后续的数据仍然写入同一个文件,而没有按预期进行文件切割
*/
static time_t last_time = 0;
static int counter = 0;
time_t t = util::date::now();
struct tm lt;
localtime_r(&t, <);
if (t != last_time)
{
// 时间变化,重置计数器
last_time = t;
counter = 0;
}
std::stringstream ss;
ss << _basename;
ss << lt.tm_year + 1900;
ss << lt.tm_mon + 1;
ss << lt.tm_mday;
ss << lt.tm_hour;
ss << lt.tm_min;
ss << lt.tm_sec;
ss << "_" << counter++; // 加入自增编号
ss << ".log";
return ss.str();
}
private:
size_t _max_fsize; // 用户自定义规定的 文件大小
size_t _cur_fsize; // 当前文件的大小
std::string _basename; // 文件基础名字
std::ofstream _ofs; // 文件输出流
};
/* 扩展实现一个rollbytime */
class RollByTimeSink : public LogSink
{
public:
enum class GapTime
{
Second = 1,
Minute = 60,
Hour = 3600,
Day = 3600 * 24
};
public:
RollByTimeSink(const std::string &basename, GapTime gap_type)
: _basename(basename),
_gap_type(static_cast<size_t>(gap_type))
{
_cur_gap = _cur_gap == 1 ? util::date::now() : util::date::now() % _gap_type; /*当间隔为second时,%任何东西都是0*/
std::string filename = create_file();
// 创建文件所在的路径上的文件夹
util::file::create_directory(util::file::path(filename));
// 文件操作句柄统一管理起来
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
void log(const char *data, size_t len) override
{
initLogFile();
_ofs.write(data, len);
if (_ofs.good() == false) // good函数用于检测流对象的当前状态是否“良好”,即没有发生任何错误。
{
std::cout << "⽇志输出⽂件失败!\\n";
}
}
private:
void initLogFile() // 如果文件超出大小则需要创造文件进行写入
{
size_t new_gap = _gap_type == 1 ? util::date::now() : util::date::now() % _gap_type; /*当间隔为second时,%任何东西都是0*/
if (new_gap != _cur_gap)
{
_ofs.close();
// 创建文件所在的路径上的文件夹
std::string filename = create_file();
util::file::create_directory(util::file::path(filename));
// 文件操作句柄统一管理起来
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
_cur_gap = new_gap;
}
}
std::string create_file()
{
time_t t = util::date::now();
struct tm lt;
localtime_r(&t, <);
std::stringstream ss;
ss << _basename;
ss << lt.tm_year + 1900;
ss << lt.tm_mon + 1;
ss << lt.tm_mday;
ss << lt.tm_hour;
ss << lt.tm_min;
ss << lt.tm_sec;
ss << ".log";
return ss.str();
}
private:
size_t _gap_type; // 间隔时间
size_t _cur_gap; // 当前的 时间%间隔时间
std::string _basename; // 文件基础名字
std::ofstream _ofs; // 文件输出流
};
class SinkFactory
{
public:
template <class SinkType, class... Args>
static LogSink::ptr create(Args &&...args)
{
/* Args&&… —— 这是一种 万能引用(universal reference),它可以绑定到左值或右值,并保持原来的值类别。
如果你直接写 args…,所有参数都被当作 左值 传递
而 std::forward<Args>(args)… 可以让:
左值参数以左值身份传递;
右值参数以右值身份传递。*/
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
}
2.6 日志器类设计(建造者模式)logger.hpp
日志器主要用于和前端交互,当我们需要使用日志系统打印日志时,只需创建 Logger 对象,调用该对象的 debug、info、warn、error、fatal 等方法输出想打印的日志。支持解析可变参数列表和输出格式,类似 printf 函数的使用体验。
当前日志系统支持两种模式:
-
同步日志器 直接对日志消息进行输出。
-
异步日志器 将日志消息放入缓冲区,由异步线程进行输出。
两个日志器唯一不同点在于日志的落地方式。
设计架构
- 设计一个 Logger 基类,作为日志器的统一接口。
- 在 Logger 基类基础上,继承出:
- SyncLogger(同步日志器)
- AsyncLogger(异步日志器)
日志器创建配置
创建一个日志器时,需要配置以下内容:
- 日志器名称
- 日志输出等级
- 日志器类型(同步/异步)
- 日志输出格式
- 日志落地方向(可支持多个落地目标)
由于日志器的创建过程较为复杂,为保持良好的代码风格和优雅设计,采用建造者模式来实现日志器的创建。
建造者模式优势:
- 解耦复杂的日志器创建过程
- 灵活配置多种参数
- 保持代码简洁清晰
#pragma once
#include "formatter.hpp"
#include "message.hpp"
#include "level.hpp"
#include "sink.hpp"
#include "looper.hpp"
#include <mutex>
#include <atomic>
#include <cstdarg>
#include <cstdio>
#include <unordered_map>
namespace Log
{
class Logger
{
public:
using ptr = std::shared_ptr<Logger>;
Logger(LogLevel::value limit_level, Formatter::ptr formatter, std::vector<LogSink::ptr> &sinks, std::string logger_name)
: _limit_level(limit_level),
_formatter(formatter),
_sinks(sinks),
_logger_name(logger_name)
{
}
const std::string &getName() { return _logger_name; }
void debug(const char *file, size_t line, const char *fmt, ...)
{
// 判断当前level等级是否大于等于_limit_level
if (shouldLog(LogLevel::value::DEBUG) == false)
return;
// 不定参数
va_list al;
va_start(al, fmt);
logMsgToStr(file, line, fmt, al, LogLevel::value::DEBUG);
va_end(al);
}
void info(const char *file, size_t line, const char *fmt, ...)
{
// fmt 是格式化字符串(format string)的缩写,用于支持可变参数函数,就像 printf 一样。
// 判断当前level等级是否大于等于_limit_level
if (shouldLog(LogLevel::value::INFO) == false)
return;
// 不定参数
va_list al;
va_start(al, fmt);
logMsgToStr(file, line, fmt, al, LogLevel::value::INFO);
va_end(al);
}
void warn(const char *file, size_t line, const char *fmt, ...)
{
// 判断当前level等级是否大于等于_limit_level
if (shouldLog(LogLevel::value::WARN) == false)
return;
// 不定参数
va_list al;
va_start(al, fmt);
logMsgToStr(file, line, fmt, al, LogLevel::value::WARN);
va_end(al);
}
void error(const char *file, size_t line, const char *fmt, ...)
{
// 判断当前level等级是否大于等于_limit_level
if (shouldLog(LogLevel::value::ERROR) == false)
return;
// 不定参数
va_list al;
va_start(al, fmt);
logMsgToStr(file, line, fmt, al, LogLevel::value::ERROR);
va_end(al);
}
void fatal(const char *file, size_t line, const char *fmt, ...)
{
// 判断当前level等级是否大于等于_limit_level
if (shouldLog(LogLevel::value::FATAL) == false)
return;
// 不定参数
va_list al;
va_start(al, fmt);
logMsgToStr(file, line, fmt, al, LogLevel::value::FATAL);
va_end(al);
}
protected:
bool shouldLog(LogLevel::value level)
{
return (level >= _limit_level);
}
void logMsgToStr(const char *file, size_t line, const char *fmt, va_list &al, LogLevel::value level)
{
char *buf;
int n = vasprintf(&buf, fmt, al);
if (n < 0)
{
std::cerr << "日志消息格式化失败!!\\n";
return;
}
// 初始化一个日志消息对象
LogMsg lm(line, file, _logger_name, buf, level);
// 将日志消息对象中的信息格式化成一个字符串
std::string s;
s = _formatter->format(lm);
// 将格式化完成的字符串放入到 logsink中
logSink(s);
}
virtual void logSink(const std::string &Msg) = 0;
protected:
std::mutex _mutex; // 锁
std::atomic<LogLevel::value> _limit_level; // 限制等级
Formatter::ptr _formatter; // 日志格式化对象
std::vector<LogSink::ptr> _sinks; // 日志落地数组
std::string _logger_name; // 日志器名称
};
class SyncLogger : public Logger
{
public:
using ptr = std::shared_ptr<SyncLogger>;
SyncLogger(LogLevel::value limit_level, Formatter::ptr formatter, std::vector<LogSink::ptr> &sinks, std::string logger_name)
: Logger(limit_level, formatter, sinks, logger_name) // 这 不是创建一个新的 Logger 对象,而是 初始化 SyncLogger 作为 Logger 子类中继承来的那一部分。
{
//std::cout << "同步日志器:" << logger_name << "创造成功…\\n";
}
void logSink(const std::string &Msg) override
{
std::unique_lock<std::mutex> lock(_mutex); // 当前业务中只有一个主线程调用是可以不加锁的,但是考虑到未来可能有多个线程调用,所以提前加锁
if (_sinks.empty())
{
std::cerr << "落地数组为空!!\\n";
return;
}
for (auto &sn : _sinks)
{
sn->log(Msg.c_str(), Msg.size());
}
}
};
class AsyncLogger : public Logger
{
public:
using ptr = std::shared_ptr<AsyncLogger>;
AsyncLogger(LogLevel::value limit_level, Formatter::ptr formatter, std::vector<LogSink::ptr> &sinks, std::string logger_name, AsyncSafeType &async_type)
: Logger(limit_level, formatter, sinks, logger_name), // 这 不是创建一个新的 Logger 对象,而是 初始化 SyncLogger 作为 Logger 子类中继承来的那一部分。
_looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::readLog, this, std::placeholders::_1), async_type))
{
//std::cout << "异步日志器:" << logger_name << "创造成功…\\n";
}
void logSink(const std::string &Msg) override
{
_looper->push(Msg.c_str(), Msg.size());
}
void readLog(Buffer &buffer)
{
if (_sinks.empty())
{
std::cerr << "落地数组为空!!\\n";
return;
}
for (auto &sn : _sinks)
{
sn->log(buffer.begin(), buffer.readAbleSize());
}
}
private:
AsyncLooper::ptr _looper; // 异步日志处理器
};
enum class BuildType
{
LOGGER_SYNC,
LOGGER_ASYNC
};
// 日志器建造者类
class LoggerBuilder
{
public:
using ptr = std::shared_ptr<LoggerBuilder>;
LoggerBuilder() : _limit_level(LogLevel::value::DEBUG), _logger_type(BuildType::LOGGER_SYNC), _async_type(AsyncSafeType::SAFE) {}
void buildAsyncUnSafe() { _async_type = AsyncSafeType::UNSAFE; }
void buildLoggerType(BuildType type) { _logger_type = type; }
void buildloggerName(std::string logger_name) { _logger_name = logger_name; }
void buildloggerLevel(LogLevel::value level) { _limit_level = level; }
void buildloggerFormater(const std::string pattern) { _formatter = std::make_shared<Formatter>(pattern); }
// void buildloggerFormatter(const Formatter::ptr &formatter) { _formatter = formatter; }
/*
关于 const & 指针的理由:
shared_ptr 本身虽然是轻量封装,但 复制仍涉及引用计数的原子操作。
如果你传值(Formatter::ptr formatter),那么:
会多一次引用计数 +1 和 -1 的开销。
如果你不需要对指针本身做修改(例如 reset),则传引用更合理。
*/
template <typename SinkType, typename... Args>
void buildsink(Args &&...args)
{
LogSink::ptr sink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);
_sinks.push_back(sink);
}
virtual Logger::ptr build() = 0;
protected:
AsyncSafeType _async_type; // 异步日志类型
BuildType _logger_type; // 日志器种类
LogLevel::value _limit_level; // 限制等级
Formatter::ptr _formatter; // 日志格式化对象
std::vector<LogSink::ptr> _sinks; // 日志落地数组
std::string _logger_name; // 日志器名称
};
class LocalLoggerBuilder : public LoggerBuilder
{
public:
using ptr = std::shared_ptr<LocalLoggerBuilder>;
Logger::ptr build() override
{
// 如果日志器名字为空
if (_logger_name.empty())
{
std::cerr << "日志器名字不能为空!!!\\n";
abort();
}
if (_formatter.get() == nullptr)
{
//std::cout << "当前日志器:" << _logger_name << " 未检测到日志格式,设置默认设置\\n";
_formatter = std::make_shared<Formatter>();
}
if (_sinks.empty())
{
//std::cout << "当前日志器:" << _logger_name << " 未检测到落地方向,默认设置为标准输出!\\n";
_sinks.push_back(std::make_shared<StdOutSink>());
}
Logger::ptr lg;
if (_logger_type == BuildType::LOGGER_SYNC)
{
lg = std::make_shared<SyncLogger>(_limit_level, _formatter, _sinks, _logger_name);
}
else
{
lg = std::make_shared<AsyncLogger>(_limit_level, _formatter, _sinks, _logger_name, _async_type);
}
return lg;
}
};
// 日志管理器
class LoggerManager
{
public:
static LoggerManager &getInstance()
{
// 在c++11之后,针对静态局部变量,编译器在编译的层面实现了线程安全
// 当静态局部变量在没有构造完全之前,其它的线程进入就会阻塞
static LoggerManager lm;
return lm;
}
// 禁止拷贝和赋值
LoggerManager(const LoggerManager &) = delete;
LoggerManager &operator=(const LoggerManager &) = delete;
void addLogger(const std::string &name, const Log::Logger::ptr new_logger)
{
std::unique_lock<std::mutex> lock(_mutex);
_loggers.insert(std::make_pair(name, new_logger));
}
bool hasLogger(const std::string &name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _loggers.find(name);
if (it == _loggers.end())
{
return false;
}
return true;
}
Logger::ptr getLogger(const std::string &name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _loggers.find(name);
if (it == _loggers.end())
{
return Logger::ptr();
}
return it->second;
}
//若返回的是引用,指向内部 _default_logger 的真实对象。这意味着调用方可以修改它的指针本身
Logger::ptr getDefaultLogger()
{
std::unique_lock<std::mutex> lock(_mutex);
return _default_logger;
}
private:
LoggerManager()
{
// 这里一定要是LocalBuilder 否则会陷入循环卡死,因为下面会调用build()
std::shared_ptr<LocalLoggerBuilder> builder(new LocalLoggerBuilder());
builder->buildloggerName("default");
_default_logger = builder->build();
_loggers.insert(std::make_pair("default", _default_logger));
}
~LoggerManager() = default;
private:
std::mutex _mutex;
std::unordered_map<std::string, Logger::ptr> _loggers; // 管理日志器map
Logger::ptr _default_logger; // 默认日志器
};
//全局日志建造器
class GlobalLoggerBuilder : public LoggerBuilder
{
public:
using ptr = std::shared_ptr<GlobalLoggerBuilder>;
Logger::ptr build() override
{
// 如果日志器名字为空
if (_logger_name.empty())
{
std::cerr << "日志器名字不能为空!!!\\n";
abort();
}
if (_formatter.get() == nullptr)
{
//std::cout << "当前日志器:" << _logger_name << " 未检测到日志格式,设置默认设置\\n";
_formatter = std::make_shared<Formatter>();
}
if (_sinks.empty())
{
//std::cout << "当前日志器:" << _logger_name << " 未检测到落地方向,默认设置为标准输出!\\n";
_sinks.push_back(std::make_shared<StdOutSink>());
}
Logger::ptr lg;
if (_logger_type == BuildType::LOGGER_SYNC)
{
lg = std::make_shared<SyncLogger>(_limit_level, _formatter, _sinks, _logger_name);
}
else
{
lg = std::make_shared<AsyncLogger>(_limit_level, _formatter, _sinks, _logger_name, _async_type);
}
LoggerManager::getInstance().addLogger(_logger_name,lg);
return lg;
}
};
}
2.7 双缓冲区异步任务处理器设计 looper.hpp
设计思想:异步处理线程 + 任务池(双缓冲区阻塞数据池)
- 使用者将需要完成的任务添加到任务池中,由异步线程来实际执行这些任务。
- 任务池采用双缓冲区设计,称为“双缓冲区阻塞数据池”。
双缓冲区设计思想:
-
通过两个缓冲区交替使用:
- 一组缓冲区用于生产者(任务添加)
- 另一组缓冲区用于消费者(异步线程执行任务)
-
当消费者处理完当前缓冲区中的所有任务后,与生产者缓冲区交换角色,继续处理新任务。
#pragma once
#include <iostream>
#include <vector>
#include <cassert>
#include <string>
namespace Log
{
#define BUFFER_DEFAULT_SIZE (1 * 1024 * 1024)
#define BUFFER_THRESHOLD_SIZE (10 * 1024 * 1024)
#define BUFFER_INCREMENT_SIZE (1 * 1024 * 1024)
class Buffer
{
public:
Buffer() : _buffer(BUFFER_DEFAULT_SIZE), _writer_idx(0), _reader_idx(0) {}
bool empty() { return _reader_idx == _writer_idx; }
size_t readAbleSize() { return _writer_idx – _reader_idx; }
size_t writeAbleSize() { return _buffer.size() – _writer_idx; }
// 初始化缓冲区
void reset() { _reader_idx = _writer_idx = 0; }
void swap(Buffer &buf)
{
_buffer.swap(buf._buffer);
std::swap(_writer_idx, buf._writer_idx);
std::swap(_reader_idx, buf._reader_idx);
}
void push(const char *data, size_t len)
{
// 1 如果空间不够则进行扩容操作
ensureEnoughSpace(len);
std::copy(data, data + len, &_buffer[_writer_idx]);
_writer_idx += len;
}
const char *begin() { return &_buffer[_reader_idx]; }
void pop(size_t len)
{
assert(len <= readAbleSize());
_reader_idx += len;
}
protected:
void ensureEnoughSpace(size_t len)
{
if (len <= writeAbleSize())
return;
size_t new_capacity = 0;
if (_buffer.size() <= BUFFER_THRESHOLD_SIZE)
new_capacity = _buffer.size() * 2 + len; /*这里需要加上len 因为 可能*2 之后扩容空间可能仍然不足*/
else
new_capacity = _buffer.size() + BUFFER_INCREMENT_SIZE + len;
_buffer.resize(new_capacity);
}
private:
std::vector<char> _buffer; // 存放格式化后字符串的缓冲区
size_t _writer_idx; // buffer中可写位置索引
size_t _reader_idx; // buffer中读写位置索引
};
}
优势:
- 避免空间频繁申请与释放:缓冲区空间固定复用,降低内存分配开销。
- 减少生产者与消费者之间锁冲突概率: 双缓冲区减少了频繁锁竞争的场景,因为任务是批量交换处理,而不是一条一条地锁操作。
- 提高任务处理效率:批量处理减少同步开销。
设计对比:
- 传统任务池设计如循环队列,虽然简单,但每次任务添加和取出都涉及锁冲突。
- 双缓冲区设计通过“交换缓冲区”方式,降低了锁的争用,提高并发性能。
总结:
双缓冲区阻塞数据池适合生产者消费者场景,尤其在异步日志处理、任务调度等场景中,能够有效提升性能和资源利用率。
任务处理器实现:
#pragma once
#include "buffer.hpp"
#include <functional>
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <thread>
namespace Log
{
enum class AsyncSafeType // 这里用于控制生产消费buff是否是扩容
{ // 当数据很大时buff一直扩容是不安全的
SAFE,
UNSAFE
};
class AsyncLooper
{
public:
using Functor = std::function<void(Buffer &)>;
using ptr = std::shared_ptr<AsyncLooper>;
AsyncLooper(const Functor &cb, AsyncSafeType type = AsyncSafeType::SAFE)
: _running(true), _callback(cb), _thread(std::thread(&AsyncLooper::worker_loop, this)), _looper_type(type)
{
}
~AsyncLooper() { stop(); }
void stop()
{
// exchange函数原子地将变量设置为一个新值,并返回原来的旧值。
if (_running.exchange(false) == false) return; // 防止重复 stop
_cums_cond.notify_all();
if (_thread.joinable())
_thread.join();
}
void push(const char *data, size_t len)
{
// 由(主线程)业务线程进行
if (_running == false)
return;
{
std::unique_lock<std::mutex> lock(_mutex);
// 生产者条件变量 wait , lambda如果生产缓冲区中可写数据大于len 返回true
if (_looper_type == AsyncSafeType::SAFE) // 如果Asy是安全的则需要进行 判断 len的大小是否符合缓冲区的容量
{
_prod_cond.wait(lock, [&]()
{ return _prod_buff.writeAbleSize() >= len; });
}
_prod_buff.push(data, len);
}
// 生产缓冲区有了数据后,便可以唤醒消费者
/*使用 notify_all() 是为了确保:
所有等待的线程在 _running == false 时都能退出;
保证不会有线程永远阻塞在 wait 上。*/
_cums_cond.notify_all();
}
private:
void worker_loop()
{
// 由工作线程来进行
while (1)
{
{
std::unique_lock<std::mutex> lock(_mutex);
// 如果生产缓冲区中仍然有数据存在,那么不能退出 而是要等到数据全部处理完全才可以退出
if (_running == false && _prod_buff.empty())
return;
// 消费者条件变量 wait , lambda如果生产缓冲区不为空(因为缓冲区有数据需要进行处理) 或者 _runing为false(这里我们需要唤醒所有的线程) 那么返回true
_cums_cond.wait(lock, [&]()
{ return !_prod_buff.empty() || !_running; });
_cums_buff.swap(_prod_buff);
}
// 唤醒生产者条件变量
if (_looper_type == AsyncSafeType::SAFE)
{
_prod_cond.notify_all();
}
// 通过回调函数 处理消费缓冲区中的数据
_callback(_cums_buff);
// 初始化消费缓冲区
_cums_buff.reset();
}
}
private:
Functor _callback;
AsyncSafeType _looper_type; // 用于控制缓冲区是否扩容
private:
std::mutex _mutex;
std::atomic<bool> _running; // 控制异步线程是否继续运行或安全退出,它是异步线程生命周期中的一个控制标志
Buffer _prod_buff; // 生产缓冲区
Buffer _cums_buff; // 消费缓冲区
std::condition_variable _prod_cond; // 生产者条件变量
std::condition_variable _cums_cond; // 消费者条件变量
std::thread _thread; // 工作线程,这里虽然只有一个线程但是后续可以引入线程池等待,符合扩展性,但是一个日志系统就不需要多个线程来实现
};
}
2.8 异步日志器设计 logger.hpp
异步日志器的实现放在了logger.hpp 中。 异步日志器类继承自日志器(Logger)类,并在同步日志器基础上扩展了异步消息处理功能。
主要功能
-
异步日志输出 通过异步消息处理器将日志数据异步写入,避免阻塞调用线程。
-
异步日志器和消息处理器 需要同时创建异步日志器对象和对应的消息处理器。
关键函数
logsink | 重写自 Logger 类,将日志数据加入异步队列缓冲区中(异步发送) |
realLog | 由异步线程调用(异步消息处理器的回调函数),完成日志的实际落地 |
使用流程
class AsyncLogger : public Logger
{
public:
using ptr = std::shared_ptr<AsyncLogger>;
AsyncLogger(LogLevel::value limit_level, Formatter::ptr formatter, std::vector<LogSink::ptr> &sinks, std::string logger_name, AsyncSafeType &async_type)
: Logger(limit_level, formatter, sinks, logger_name), // 这 不是创建一个新的 Logger 对象,而是 初始化 SyncLogger 作为 Logger 子类中继承来的那一部分。
_looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::readLog, this, std::placeholders::_1), async_type))
{
//std::cout << "异步日志器:" << logger_name << "创造成功…\\n";
}
void logSink(const std::string &Msg) override
{
_looper->push(Msg.c_str(), Msg.size());
}
void readLog(Buffer &buffer)
{
if (_sinks.empty())
{
std::cerr << "落地数组为空!!\\n";
return;
}
for (auto &sn : _sinks)
{
sn->log(buffer.begin(), buffer.readAbleSize());
}
}
private:
AsyncLooper::ptr _looper; // 异步日志处理器
};
2.9 单例日志器管理类设计(单例模式)logger.hpp
为了在任意位置都能方便地输出日志,避免日志器对象作用域和访问权限的限制,我们设计了日志器管理类。
设计思想:
-
单例模式 日志器管理类设计为单例,确保全局唯一实例,方便任意位置访问。
-
全局日志器管理 通过管理器单例,能够根据日志器名称获取指定的日志器对象,进行日志输出。
结合日志器建造者
- 继承日志器建造者类,设计一个全局日志器建造者类。
- 在日志器创建完成后,自动将其添加到单例的日志器管理器中。
- 这样,创建的日志器可全局访问,无需手动管理生命周期。
优点
- 突破访问域限制,日志器随时可用。
- 统一管理日志器,方便维护和扩展。
- 保持代码优雅和高内聚。
代码实现:
// 日志管理器
class LoggerManager
{
public:
static LoggerManager &getInstance()
{
// 在c++11之后,针对静态局部变量,编译器在编译的层面实现了线程安全
// 当静态局部变量在没有构造完全之前,其它的线程进入就会阻塞
static LoggerManager lm;
return lm;
}
// 禁止拷贝和赋值
LoggerManager(const LoggerManager &) = delete;
LoggerManager &operator=(const LoggerManager &) = delete;
void addLogger(const std::string &name, const Log::Logger::ptr new_logger)
{
std::unique_lock<std::mutex> lock(_mutex);
_loggers.insert(std::make_pair(name, new_logger));
}
bool hasLogger(const std::string &name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _loggers.find(name);
if (it == _loggers.end())
{
return false;
}
return true;
}
Logger::ptr getLogger(const std::string &name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _loggers.find(name);
if (it == _loggers.end())
{
return Logger::ptr();
}
return it->second;
}
//若返回的是引用,指向内部 _default_logger 的真实对象。这意味着调用方可以修改它的指针本身
Logger::ptr getDefaultLogger()
{
std::unique_lock<std::mutex> lock(_mutex);
return _default_logger;
}
private:
LoggerManager()
{
// 这里一定要是LocalBuilder 否则会陷入循环卡死,因为下面会调用build()
std::shared_ptr<LocalLoggerBuilder> builder(new LocalLoggerBuilder());
builder->buildloggerName("default");
_default_logger = builder->build();
_loggers.insert(std::make_pair("default", _default_logger));
}
~LoggerManager() = default;
private:
std::mutex _mutex;
std::unordered_map<std::string, Logger::ptr> _loggers; // 管理日志器map
Logger::ptr _default_logger; // 默认日志器
};
为什么我要把日志器管理类 和 日志器建造者类放到一起??
- 将日志器管理类和日志器建造者类放到一起实现,是为了方便模块间的调用协作、减少代码耦合复杂度,便于维护和生命周期统一管理。这是一种合理的工程实践,使日志系统设计更加紧凑、高效、易扩展。
- 并且这里我实现的管理类和建造者 都有相互调用,若分开两个文件实现,则会出现 循环包含 问题。
2.10 日志宏&全局接⼝设计(代理模式)log.h
- 提供全局日志器获取接口,简化用户操作。
- 使用代理模式,通过全局函数或宏函数代理 Logger 类的各种日志接口: log、debug、info、warn、error、fatal。
- 自动捕获源码文件名和行号,方便调试和定位。
- 支持通过主日志器进行标准输出日志。
- 用户只需通过宏函数即可轻松打印日志,操作简单方便
#pragma once
#include "logger.hpp"
namespace Log
{
// 提供获取指定日志器的全局接口
Logger::ptr getlLogger(const std::string &name)
{
return LoggerManager::getInstance().getLogger(name);
}
Logger::ptr getDefaultLogger()
{
return LoggerManager::getInstance().getDefaultLogger();
}
// 使用宏函数对日志器的接口代理
#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define warn(fmt, ...) warn(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
// 提供宏函数 提供默认日志器进行日志的标准输出打印
#define DEBUG(fmt,...) getDefaultLogger()->debug(fmt,##__VA_ARGS__)
#define INFO(fmt,...) getDefaultLogger()->info(fmt,##__VA_ARGS__)
#define WARN(fmt,...) getDefaultLogger()->warn(fmt,##__VA_ARGS__)
#define ERROR(fmt,...) getDefaultLogger()->error(fmt,##__VA_ARGS__)
#define FATAL(fmt,...) getDefaultLogger()->fatal(fmt,##__VA_ARGS__)
}
使用样例
#include "../log/log.h"
int main()
{
std::unique_ptr<Log::GlobalLoggerBuilder> builder(new Log::GlobalLoggerBuilder());
builder->buildloggerName("async_logger");
builder->buildLoggerType(Log::BuildType::LOGGER_ASYNC);
builder->buildAsyncUnSafe();
builder->buildsink<Log::StdOutSink>();
builder->buildsink<Log::FileSink>("./logfile/async.log");
builder->buildsink<Log::RollBySizeSink>("./logfile/roll-", 1024 * 1024);
builder->build();
Log::Logger::ptr logger = Log::getlLogger("async_logger");
logger->debug("%s", "测试全局接口");
logger->info("%s", "测试全局接口");
logger->warn("%s", "测试全局接口");
logger->error("%s", "测试全局接口");
logger->fatal("%s", "测试全局接口");
return 0;
}
三、性能测试代码 bench.h
测试日志系统在不同模式下(同步/异步 & 单线程/多线程)平均每秒能够打印多少条日志消息到文件。
测试指标
- 打印日志总条数:100万+条指定长度的日志
- 总打印耗时(秒)
- 每秒打印日志条数 = 打印日志条数 / 总耗时
- 每秒打印日志大小(KB)= 日志条数 × 单条日志大小 / 总耗时 / 1024 / 1024
测试要素
同步 | 日志写入为同步方式 |
异步 | 日志写入为异步方式 |
单线程 | 单个线程写日志 |
多线程 | 多线程并发写日志 |
测试环境
CPU | AMD Ryzen 7 7840H w/ Radeon 780M Graphics 3.801 GHz |
内存(RAM) | 8GB DDR5 5600 |
存储(ROM) | 512G SSD |
操作系统 OS | Centos7.6 云服务器(2核/2G内存) |
备注
- 测试应多次运行取平均值,减少偶然误差。
- 多线程测试时,线程数需要明确。
- 注意磁盘IO和CPU资源的瓶颈影响。
测试代码实现: bench.h
#pragma once
#include "../log/log.h"
#include <chrono>
#include <vector>
namespace Log
{
void bench(const std::string &name, size_t thread_num, size_t msg_count, size_t msg_len)
{
// 1.获取日志器
Log::Logger::ptr lg = getlLogger(name);
if (lg.get() == nullptr)
{
return;
}
// 2. 组织指定长度的日志消息
std::string msg(msg_len, 'M');
// 3. 创造指定个数线程
std::vector<std::thread> threads;
std::cout << "输入线程数量: " << thread_num << " 输出日志数量: " << msg_count << " 输出总日志大小: " << msg_len * msg_count / 1024 / 1024 << "MB" << std::endl;
std::vector<double> cost_time(thread_num); // 该vector 保存每个线程的耗时 时间
size_t msg_count_per_thread = msg_count / thread_num; // 每个线程要写日志的个数
for (int i = 0; i < thread_num; i++)
{
// emplace_back直接在 vector 内部构造这个线程对象,更高效,无额外 move 构造;
threads.emplace_back([&, i]()
{
// 4. 线程函数内部开始计时
auto start = std::chrono::high_resolution_clock::now();
// 5. 循坏写日志
for(size_t j = 0; j < msg_count_per_thread; j++)
{
lg->fatal("%s",msg.c_str());
}
// 6. 线程函数结束计时
auto end = std::chrono::high_resolution_clock::now();
auto cost = std::chrono::duration_cast<std::chrono::duration<double>>(end – start);
cost_time[i] = cost.count();
auto avg = msg_count_per_thread / cost_time[i];
std::cout << "线程" << i << " 耗时:" << cost.count() << "s" << std::endl; });
}
for (auto &thr : threads)
{
thr.join();
}
// 7. 计算总耗时
double max_cost = 0;
for (auto cost : cost_time)
{
max_cost = (max_cost < cost ? cost : max_cost);
}
std::cout << "总消耗时间: " << max_cost << "s" << std::endl;
std::cout << "平均每秒输出: " << (size_t)(msg_count / max_cost) << "条日志" << std::endl;
std::cout << "平均每秒输出: " << (size_t)(msg_len * msg_count / max_cost / 1024/ 1024) << "MB" << std::endl;
}
}
bench.cc
#include "bench.h"
#include "../log/log.h"
void sync_bench_thread_log(size_t thread_count, size_t msg_count, size_t msglen)
{
static int num = 1;
std::string logger_name = "sync_bench_logger" + std::to_string(num++);
printf("************************************************\\n");
printf("同步日志测试: %d threads, %d messages\\n", thread_count, msg_count);
Log::GlobalLoggerBuilder::ptr lbp(new Log::GlobalLoggerBuilder());
lbp->buildloggerName(logger_name);
lbp->buildloggerFormater("%m");
lbp->buildsink<Log::FileSink>("./logfile/sync.log");
lbp->buildLoggerType(Log::BuildType::LOGGER_SYNC);
lbp->build();
Log::bench(logger_name, thread_count, msg_count, msglen);
printf("************************************************\\n");
}
void async_bench_thread_log(size_t thread_count, size_t msg_count, size_t msglen)
{
static int num = 1;
std::string logger_name = "async_bench_logger" + std::to_string(num++);
printf("************************************************\\n");
printf("异步日志测试: %d threads, %d messages\\n", thread_count, msg_count);
Log::GlobalLoggerBuilder::ptr lbp(new Log::GlobalLoggerBuilder());
lbp->buildloggerName(logger_name);
lbp->buildloggerFormater("%m");
lbp->buildAsyncUnSafe();
lbp->buildsink<Log::FileSink>("./logfile/async.log");
lbp->buildLoggerType(Log::BuildType::LOGGER_ASYNC);
lbp->build();
Log::bench(logger_name, thread_count, msg_count, msglen);
printf("************************************************\\n");
}
int main()
{
sync_bench_thread_log(1, 1000000, 100);
sync_bench_thread_log(3, 1000000, 100);
async_bench_thread_log(1, 1000000, 100);
async_bench_thread_log(3, 1000000, 100);
return 0;
}
测试结果如下:
************************************************
同步日志测试: 1 threads, 1000000 messages
输入线程数量: 1 输出日志数量: 1000000 输出总日志大小: 95MB
线程0 耗时:1.73775s
总消耗时间: 1.73775s
平均每秒输出: 575458条日志
平均每秒输出: 54MB
************************************************
************************************************
同步日志测试: 3 threads, 1000000 messages
输入线程数量: 3 输出日志数量: 1000000 输出总日志大小: 95MB
线程1 耗时:1.588s
线程0 耗时:1.69305s
线程2 耗时:1.71149s
总消耗时间: 1.71149s
平均每秒输出: 584286条日志
平均每秒输出: 55MB
************************************************
************************************************
异步日志测试: 1 threads, 1000000 messages
输入线程数量: 1 输出日志数量: 1000000 输出总日志大小: 95MB
线程0 耗时:1.49739s
总消耗时间: 1.49739s
平均每秒输出: 667827条日志
平均每秒输出: 63MB
************************************************
************************************************
异步日志测试: 3 threads, 1000000 messages
输入线程数量: 3 输出日志数量: 1000000 输出总日志大小: 95MB
线程0 耗时:1.70396s
线程1 耗时:1.70998s
线程2 耗时:1.73196s
总消耗时间: 1.73196s
平均每秒输出: 577381条日志
平均每秒输出: 55MB
************************************************
能够通过上边的测试看出来,一些情况:
-
在单线程情况下,异步效率看起来还没有同步高,这一点需要我们理解。 现在的 IO 操作在用户态都会有缓冲区进行缓冲,因此当前测试用例看起来的同步其实大多时候也是在操作内存,只有在缓冲区满了才会涉及到阻塞写磁盘操作。 而异步单线程效率看起来低,也有一个很重要的原因就是单线程同步操作中不存在锁冲突,而单线程异步日志操作存在大量的锁冲突,因此性能也会有一定的降低。
-
我们也要看到限制同步日志效率的最大原因是磁盘性能,写日志的线程多少并无明显区别,线程多了反而会降低性能,因为增加了磁盘的读写争抢。
-
对于异步日志的限制,并非磁盘性能,而是 CPU 的处理性能,写日志并不会因为落地而阻塞。因此在多线程写日志的情况下,性能有了显著的提升。
评论前必须登录!
注册