之前博主实现了日志系统,实现的模块有日志等级、消息结构体、格式化消息、实际落地、日志器整合这些模块。并且利用全局的单例对日志器进行管理。后来针对同步写日志出现的问题,主要是阻塞、也有网络波动等。衍生出异步工作器,实现双缓冲区下的异步写日志系统。由于创建日志器需要多个属性,用户难以控制,引入建造者模式辅助构建日志器。另外用户创建全局的日志器,需要手动添加到单例中,为了简化操作,全局函数代理直接对单例的调用。
综上部分基于多设计模式下的同步异步日志系统就算完成了
下面进行日志系统的测试部分。主要是测试写日志的性能。
测试的环境
博主的采用的腾讯云服务器编写的日志系统
云服务器的配置如下
- CPU:2-核
- RAM:内存是2GB
- ROM:SSD云硬盘 40GB
- OS:Ubuntu Server 24.04 LTS 64bit
测试的内容
测试在单线程和多线程下同步/异步日志的输出性能
性能主要以俩种方式展现
- 每秒输出的消息条数:(总消息条数)/(总耗时)
- 每秒输出消息的大小MB:(总消息条数*消息长度)/(总耗时*1024*1024)
测试的方法
利用控制变量法
分别测试单线程下同步和异步写日志的性能。再测试多线程下同步和异步写日志的性能。
为了做到统一,对这些参数进行规定。
- 写入消息的条数:100万条
- 每条消息的长度:100字节
- 多线程的数量:5个线程
测试的程序
编写一个接口,可以控制传入的日志器类型、传入线程数量、消息数量、消息长度等
设计思路:
- 获取日志器
- 构造指定长度的消息
- 创建线程
- 开始计时
- 循环写指定数量的消息
- 结束计时
- 得出性能
void bench(const std::string &logger_name, size_t thr_count, size_t msg_count, size_t msg_len)
{
// 1.获取日志器
LoggerPtr lg = getLogger(logger_name);
if (!lg.get())
{
std::cout << "不存在日志器:" << logger_name << std::endl;
return;
}
// 2.创建消息
std::string msg(msg_len - 1, 'A');
std::cout << "线程数量:" << thr_count << "个,消息数量:" << msg_count << "条,消息总大小:" << (msg_count * msg_len) / (1024*1024) << "(MB)" << std::endl;
// 3.创建线程
std::vector<std::thread> threads;
std::vector<double> cost(thr_count);
// 计算每个线程处理多少条消息
size_t msg_per_thr = msg_count / thr_count;
for (int i = 0; i < thr_count; i++)
{
threads.emplace_back([&, i]()
{
//开始计时
auto start=std::chrono::high_resolution_clock::now();
//开始循环写日志
for(int j=0;j<msg_per_thr;j++){
lg->Fatal("%s",msg.c_str());
}
//结束计时
auto end=std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff=end-start;
cost[i]=diff.count();
std::cout<<"线程"<<i<<"耗时:"<<diff.count()<<"s,处理"<<msg_per_thr<<"条消息\n"; });
}
for (auto &thr : threads)
{
thr.join();
}
double maxcost = cost[0];
for (int i = 1; i < thr_count; i++)
{
maxcost = std::max(maxcost, cost[i]);
}
size_t msg_per_sec = msg_count / maxcost;
size_t size_per_sec = (msg_count * msg_len) / (maxcost * 1024*1024);
std::cout << "总耗时:" << maxcost << std::endl;
std::cout << "每秒输出:" << msg_per_sec << "条消息" << std::endl;
std::cout << "每秒输出:" << size_per_sec << "(MB)" << std::endl;
}
测试结果
同步单线程写100万条日志
每秒输出67万条消息,每秒输出63MB
异步单线程写100万条日志
异步写日志的性能每秒输出55万条消息,每秒写53MB
同步多线程写100万条消息
在五个线程同步写日志的情况下,每秒输出64万条消息,每秒输出61MB
异步多线程写100万条消息
在五个多线程异步写日志的情况下,每秒输出90万条消息,每秒写入86MB消息
结果分析
绘制成表格
同步单线程写 | 异步单线程写 | 同步多线程写 | 异步多线程写 | |
耗时(s) | 14 | 18 | 15.5 | 11 |
每秒写入 (万条) | 67 | 56 | 64 | 90 |
每秒输出(MB) | 63 | 53 | 61 | 86 |
排名 | 2 | 4 | 3 | 1 |
比较性能的优越
多线程异步写>>单线程同步写>多线程程异步写>单线程异步写
几个问题
为什么单线程同步写日志性能由于多线程同步写日志?
1.多线程存在锁的竞争与释放会消耗时间。
2.另外写磁盘是被加锁保护的,同一时间只能由一个线程写。
3.其次线程间上下文切换也需要消耗时间。
为什么单线程异步写的性能最低?
现在的OS都会用户开辟一块用户缓冲区,用户往磁盘写数据的时候,会先被拷贝到 用户缓冲区中,等到缓冲区慢了,再将数据刷到磁盘中。这是减少IO的体现,也就是是磁盘的性能到达极限(几乎不消耗时间)
异步写数据将消息由用户用户态队列拷贝到内核缓冲区。涉及到俩次申请释放锁。线程上下文切换的时间。
为什么异步多线程最快?
同步多线程写日志会存在磁盘写日志时候的锁冲突。
异步多线程写日志只需要将消息放到队列即可,不存在写日志的锁冲突。
对于同步写日志影响最大的因素是磁盘的性能,线程数多了效率反而更低,因为存在锁冲突,线程间上下文切换。
对于异步影响写日志的最大因素是CPU性能。因为异步不会去实际落地,CPU调度越快,写数据就会越多。
本项目到此结束了,这个项目运用了大量的继承与多态,对C++的学习非常有帮助。同时也涉及到很多常见的设计模式,对提高代码质量有很大的帮助。