在一个大型分布式系统中,任何部署的分布式节点都可能发生崩溃,试想如果用普通的办法,即先排查哪个节点down掉了,找到down掉的节点后采取调试工具gdb调试该节点,进而排查宕机的原因。这中排查方法对于人力物力都是无法接受的。
那么由此记录日志就变得至关重要,分布式RPC框架必定存在一个异步日志模块,用于记录所有分布式站点的调试信息。通过日志分析就能很容易排查出哪个节点出了问题。
一个日志模块必须是异步的,不能影响主程序的运行,例如在RPC框架中显然不能阻塞了RPCProvider(服务提供者)和RPCConsumer(服务调用者)的运行。
RPCProvider是一个能接受高并发rpc请求的高性能服务器(epoll+多线程),那么存在多个线程同时写日志
的情况,这里的“写日志”并不是真正意义上的写磁盘文件的操作,因为磁盘IO会严重拖累该线程原本执行的其他任务。所以这里的写日志
只是多个线程将日志写入一个异步缓冲队列(这个操作是在内存进行的非常快),并且这个队列必须是线程安全的。
此外,应该另起一个线程来读取队列里面的日志数据进行真正的磁盘写文件操作,这样写日志线程是单独工作的,它只是依赖于异步缓冲队列里面的数据,不会影响RPC服务线程和其他IO线程。
下面提供一个简单版本的实现
异步缓冲队列类:
#pragma once
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
//异步写日志的日志队列
template<typename T>
class LockQueue
{
public:
//多个work线程都会写日志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())
{
//日志队列为空, 线程进入wait状态
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 once
#include "lockqueue.h"
#include <utility>
//日志级别
enum LogLevel{
INFO = 1, //普通信息
ERROR //错误信息
};
//Mprpc框架提供的日志系统
class Logger
{
public:
//获取日志的单例
static Logger& GetInstance();
//写日志
void Log(std::pair<LogLevel,std::string> msg);
private:
LockQueue<std::pair<LogLevel,std::string>> m_lckQue; //日志缓冲队列
Logger();
Logger(const Logger&) = delete;
Logger(Logger&& ) = delete;
Logger& operator=(const Logger&) = delete;
};
//定义宏 LOG_XXX("xxx %d %s", 20, "sdasd");
#define LOG_INFO(logmsgformat, ...) \
do \
{ \
Logger &logger = Logger::GetInstance(); \
char c[1024] = {0}; \
snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
std::pair<LogLevel,std::string> pr = std::make_pair(INFO, std::string(c)); \
logger.Log(pr); \
} while (0);
#define LOG_ERR(logmsgformat, ...) \
do \
{ \
Logger &logger = Logger::GetInstance(); \
char c[1024] = {0}; \
snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
std::pair<LogLevel,std::string> pr = std::make_pair(ERROR, std::string(c)); \
logger.Log(pr); \
} while (0);
Logger.cc
#include "logger.h"
#include <time.h>
#include <iostream>
//获取日志的单例
Logger& Logger::GetInstance()
{
static Logger logger;
return logger;
}
Logger::Logger()
{
//启动专门的写日志线程
std::thread writeLogTask([&](){
for(;;)
{
//获取当前的日期,然后取日志信息,写入相应的日志文件当中 a+
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::pair<LogLevel,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,
(msg.first == INFO ? "info" : "error"));
msg.second.insert(0, time_buf);
msg.second.append("\n");
fputs(msg.second.c_str(), pf);
fclose(pf);
}
});
//设置分离线程, 守护线程
writeLogTask.detach();
}
//写日志,把日志信息写入到lockqueue缓冲区当中
void Logger::Log(std::pair<LogLevel,std::string> msg)
{
m_lckQue.Push(msg);
}
使用:
...
LOG_INFO("NotifyService UserService success");
LOG_ERR("eeeeeerrror%d", 9999999);
LOG_INFO("NotifyService GetFriendListService success");
...
查看日志文件:cat 2023-8-2-log.txt
21-35-26 => [info]NotifyService UserService success
21-35-26 => [error]eeeeeerrror9999999
21-35-26 => [info]NotifyService GetFriendListService success