设计logger日志系统
图中画圆圈的是我们实现的mprpc框架,这个框架是给别人使用的,把本地的服务发布成远程的RPC服务,框架里最重要的两个成员就是RpcProvider和RpcChannel,他们在运行的过程中会有很多正常的输出信息和错误的信息,我们不可能都cout它们到屏幕上,因为运行时间长了,屏幕上输出的信息特别多,如果万一有什么问题,我们也不好定位,真正用起来的话不方便。所以,一般出问题,我们最直接的方式就是看日志。
日志可以记录正常软件运行过程中出现的信息和错误的信息,当我们定位问题,就打开相应的日志去查看,查找。
假如我们在框架后边输出一个日志模块,我们想把框架运行中的正常的信息和错误信息都记录在日志文件,该怎么做?
左边的两个箭头表示RPC的请求和RPC的响应。 RPC请求过来的时候,我们的框架在执行的时候会产生很多日志文件,我们要写日志,写日志信息的过程是磁盘I/O,磁盘I/O速度不快,我们不能把磁盘I/O的花销算在RPC请求的业务执行部门里面(否则造成RPC请求处理的效率慢),所以,一般在我们的服务器中,我们把日志写在一个缓存队列里(这个队列相当于异步的日志写入机制),我们的框架做的就是写日志到内存的队列里面,不做磁盘I/O操作。然后我们在后面有一个专门写日志的线程,就是做磁盘I/O操作,从队列头取出日志信息,然后把日志信息写到日志文件中,这样它的磁盘I/O就不会算在我们的RPC请求的业务当中。
我们的mprpc框架的RpcProvier端是用muduo库实现的,采用的是epoll加多线程,很有可能RPC的处理过程是在多个线程中都会去做这个,多个线程都会去写日志,也就是多个线程都会在这个缓存队列里面添加数据,所以我们的这个缓存队列必须保证线程安全。
我们的C++中有queue这个队列,也就是C++的容器,但是C++容器只考虑使用应用,没有考虑线程安全,所以我们用的是线程互斥机制来维护入队出队的线程按照,写日志的线程也是一个独立的线程,用唯一的互斥锁实现。如果队列是空的话,也就是之前的日志都写到日志文件了,写日志的线程这时就不用抢这把互斥锁了,因为没有东西可写。
所以,我们还要处理的就是线程间的通信。
队列如果是空,写日志的线程就一直等待,队列不空的时候,写日志的线程才有必要去抢锁,把日志信息写到日志文件。
我们的日志文件放在当前目录下log。
我们的日志文件如果容量太大,比如说超过20M,就会产生新的!
实现logger日志系统
lockqueue.h
#pragma once
#include <condition_variable>
#include <mutex>
#include <queue>
#include <thread>
//用于实现异步写日志的日志队列类
template <typename T>
class LockQueue
{
public:
//多个worker线程都会写日志queue
void Push(const T &data)
{
std::lock_guard<std::mutex> lock(m_mutex); //获取锁,函数结束自动释放锁
m_queue.push(data); //将数据压入队列
m_condvariable.notify_one(); //唤醒一个线程
}
//一个线程读日志queue,写日志文件
T Pop()
{
std::unique_lock<std::mutex> lock(m_mutex); //获取锁,函数结束自动释放锁
while (m_queue.empty()) //队列为空
{
m_condvariable.wait(lock); //线程挂起,阻塞,释放锁
}
T data = m_queue.front(); //取队列头
m_queue.pop(); //弹出队列头元素
return data;
}
private:
std::queue<T> m_queue; //队列
std::mutex m_mutex; //互斥锁
std::condition_variable m_condvariable; //条件变量
};
logger.h
#pragma
#include "lockqueue.h"
#include <string>
//定义宏函数 LOG_INFO("xxx %d %s", 20, "xxxx")
//使用gcc可变参_VA_ARGS__,提供给用户更轻松的使用logger
//snprintf(缓冲区,缓冲区的长度,写的格式化字符串, ##__VA_ARGS__)
//代表了可变参的参数列表,填到缓冲区当中,然后 logger.Log(c)
#define LOG_INFO(logmsgformat, ...) \
do \
{ \
Logger &logger = Logger::GetInstance(); \
logger.SetLogLevel(INFO); \
char c[1024] = {0}; \
snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
logger.Log(c); \
} while (0)
#define LOG_ERR(logmsgformat, ...) \
do \
{ \
Logger &logger = Logger::GetInstance(); \
logger.SetLogLevel(ERROR); \
char c[1024] = {0}; \
snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
logger.Log(c); \
} while (0)
//定义日志级别
enum LogLevel
{
INFO, //普通信息
ERROR, //错误信息
};
//Mprpc框架提供的日志类,采用单例模式实现
class Logger
{
public:
//初始化并获取日志的单例
static Logger &GetInstance();
//设置日志级别
void SetLogLevel(LogLevel level);
//写日志
void Log(std::string msg);
private:
//记录日志级别
int m_loglevel;
//日志缓冲队列
LockQueue<std::string> m_lckque;
//构造函数,在构造Logger类时创建一个专门写日志的后台进程
Logger();
Logger(const Logger &) = delete;
Logger(Logger &&) = delete;
};
logger.cc
#include "logger.h"
#include <iostream>
#include <time.h>
//获取日志的单例
Logger &Logger::GetInstance()
{
static Logger logger;
return logger;
}
//构造函数,在构造Logger类时创建一个专门写日志的后台进程
Logger::Logger()
{
std::thread WriteLogTask([&]()
{
for (;;)
{
//获取当前的日期
time_t now = time(nullptr);
tm *nowtm = localtime(&now);
//构建文件名,在该文件中写入当天的日志信息
char file_name[128];
sprintf(file_name, "%d-%d-%d-log.txt",
nowtm->tm_year + 1900,
nowtm->tm_mon + 1,
nowtm->tm_mday);
//以追加的方式打开日志文件
FILE *pf = fopen(file_name, "a+");
if (pf == nullptr)
{
std::cout << "logger file: " << file_name << " open error! " << std::endl;
exit(EXIT_FAILURE);
}
//出队列,获取日志队列头中的信息
std::string msg = m_lckque.Pop();
//将时间信息插入到日志队列头的信息中
char time_buf[128] = {0};
sprintf(time_buf, "%d:%d:%d =>[%s]",
nowtm->tm_hour,
nowtm->tm_min,
nowtm->tm_sec,
(m_loglevel == INFO ? "info" : "error"));
msg.insert(0, time_buf);
msg.append("\n"); //添加一个换行符
//将日志队列头中的信息写入到日志文件中
fputs(msg.c_str(), pf);
fclose(pf);
}
});
//将该线程设置为分离线程,相当于一个守护线程,在后台运行写日志的操作
WriteLogTask.detach();
}
//设置日志级别
void Logger::SetLogLevel(LogLevel level)
{
m_loglevel = level;
}
//写日志
void Logger::Log(std::string msg)
{
m_lckque.Push(msg);
}
同步日志和异步日志的优缺点
- 同步的执行效率会比较低,耗费时间,但有利于我们对流程进行控制,避免很多不可掌控的意外情况。
- 异步的执行效率高,节省时间,但是会占用更多的资源,也不利于我们对进程进行控制。