百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

C++高性能服务器框架——日志系统

mhr18 2024-12-06 16:10 20 浏览 0 评论

1|0日志文件系统

对文件系统进行修改时,需要进行很多操作。这些操作可能中途被打断,也就是说,这些操作不是“不可中断”(atomic)的。如果操作被打断,就可能造成文件系统出现不一致的状态

例如:删除文件时,先要从目录树中移除文件的标示,然后收回文件占用的空间。如果在这两步之间操作被打断,文件占用的空间就无法收回。文件系统认为它是被占用的,但实际上目录树中已经找不到使用它的文件了。

在非日志文件系统中,要检查并修复类似的错误必须对整个文件系统的数据结构进行检查。这个操作可能会花费很长的时间。

为了避免这样的错误,日志文件系统分配了一个称为日志的区域来提前记录要对文件系统做的更改。在崩溃后,只要读取日志重新执行未完成的操作,文件系统就可以恢复一致。这种恢复是原子的,因为只存在几种情况:

  • 不需要重新执行:这个事务被标记为已经完成
  • 成功重新执行:根据日志,这个事务被重新执行
  • 无法重新执行:这个事务会被撤销,就如同这个事务没有发生过一样
  • 日志本身不完整:事务还没有被完全写入日志,他会被简单忽略

2|0日志系统的设计

2|1为什么需要日志

对于一些高频操作(如心跳包、定时器、界面绘制下的某些高频重复的行为),可能在少量次数下无法触发我们想要的行为,而通过断点的暂停方式,我们不得不重复操作几十次、上百次甚至更多,这样排查问题效率是非常低下的。对于这类操作,我们可以通过打印日志,将当时的程序行为上下文现场记录下来,然后从日志系统中找出某次不正常行为的上下文信息。

2|2日志系统的技术上的实现

同步写日志

所谓同步写日志,指的是在输出日志的地方,将日志即时写入到文件中去。采用这种方式,势必造成CPU等待,进而导致主线程“卡”在写文件处,进而造成界面卡帧。

但是,很多时候我们不用担心这种问题,主要有两个原因:其一,是用户感觉不到这种同步写文件造成的延迟;其二,是客户端除了UI线程外,还有其他与界面无关的工作线程,在这些线程中写文件一般不会对用户的体验产生什么影响。

多线程同步写日志出现的问题一——不同线程的日志事件时间序列错乱

产生这种问题的主要原因是由于多个线程同时写日志到同一个文件时,产生日志的时间和实际写入磁盘的时间不是一个原子操作。我们可以这样来理解,一个线程T1在t1时刻产生了日志,另一个线程T2在t2时刻也产生了一个日志(t2 > t1)。但是由于一些原因导致线程T1发生阻塞,并没有把日志即刻写入到文件中,而线程T2并没有发生阻塞,即刻就把日志信息写入到了文件中。这种情况的存在就会导致不同线程的日志事件时间序列错乱。

多线程同步写日志出现的问题二——不同线程的日志输出错乱拼接

假设线程A的日志信息为AAAAAA,线程B的日志信息为BBBBBB,线程C的日志信息为CCCCCC。那么会不会产生一种情况使得日志文件中的输出结果为AABBCCAABBCCAABBCC。

实际上,在类Unix系统上(包括Linux),同一个进程内针对同一个FIEL*的操作是线程安全的,也就是说在Linux系统上不会产生上述的情况发生。

在Windows系统上,由于对FILE*的操作并不是线程安全的,可能会发生上述情况。

这种同步日志的实现方式,一般用于低频写日志的软件系统中,所以可以认为这种多线程同时写日志到同一个文件中是可行的。

异步写日志

所谓异步写日志,就是通过一些线程同步技术将日志先暂存下来,然后再通过一个或多个专门的日志写入线程将这些缓存的日志写入到磁盘中。

实现的时候可以使用一个队列来存储其他线程产生的日志,日志线程从该队列中取出日志,然后将日志内容写入文件。

关于C/C++ Linux后端开发网络底层原理知识 点击 「链接」 获取,内容知识点包括Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK等等。免费学习地址:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂

3|0日志系统的实现

设计一个日志系统需要考虑哪些问题?

既然是日志系统肯定需要记录日志(LogEvent),那么我们就需要一个类来表达日志的概念,这个类至少应该包含两个属性,一个是时间戳,另一个是消息本身。

其次是日志输出(LogAppender),可以输出到不同的地方,控制台、文件。

然后是将日志信息进行格式化输出(LogFormatter),LogAppender 可以引用 LogFormatter 这样就可以将 LogEvent 事件中的日志消息经过 LogFormatter 进行格式化,然后再由 LogAppender 输出。

如果想要获取日志,必须得先获取一个什么东西,这个东西可以成为 Logger,此外,还可以使用 LoggerManager 对这些不同的 Logger 进行管理。

LogLevel 类定义了日志的级别。

LogEventWarp 对 LogEvent 进行了封装,当 LogEventWarp 析构的时候,能够触发 LogEvent 将日志信息写入到指定位置。

3|1类图

3|2日志写入流程图

3|3LogEvent

LogEvent 是日志事件,所有的日志信息都是由 LogEvent 来管理,同时 LogEvent 也提供了格式化写入的功能。

// 日志事件
class LogEvent {
   public:
    // 指向日志事件的指针
    typedef std::shared_ptr<LogEvent> ptr;
    /*
     * 构造函数
     * 传入 Logger 指针,可以将该日志事件写入到对应的 Logger 中
     */
    LogEvent(std::shared_ptr<Logger> logger, LogLevel::Level level, const char* file, 
    ~LogEvent() {}

    // 获取该日志事件所对应的 Logger 类
    std::shared_ptr<Logger> getLogger() const {  return m_logger; }
    
    // 获取日志级别
    LogLevel::Level getLevel() const { return m_level; }

    // 获取文件名
    const char* getFileName() const { return m_file; }
    
    // 获取行号
    int32_t getLine() const { return m_line; }
    
    // 获取运行的时间
    uint32_t getElapse() const { return m_elapse; }
    
    // 获取线程 id
    uint32_t getThreadId() const { return m_threadId; }
    
    // 获取协程 id
    uint32_t getFiberId() const { return m_fiberId; }
    
    // 获取当前时间
    uint32_t getTime() const { return m_time; }
    
    // 获取日志内容
    std::string getContent() const { return m_ss.str(); }

    // 以 stringstream 的形式获取日志内容
    std::stringstream& getSS() { return m_ss; }
    
    // 将日志内容进行格式化
    void format(const char* fmt, ...);
    void format(const char* fmt, va_list al);
    
   private:
    const char* m_file = nullptr;  // 文件名
    int32_t m_line = 0;            // 行号
    uint32_t m_elapse = 0;         // 程序启动开始到现在的毫秒数
    uint32_t m_threadId = 0;       // 线程id
    uint32_t m_fiberId = 0;        // 协程id
    uint64_t m_time;               // 时间戳
    std::stringstream m_ss;             // 日志流
    std::shared_ptr<Logger> m_logger;   // 指向 Logger 类的指针
    LogLevel::Level m_level;            // 该日志事件的级别
};

下面的这两个函数提供了一种获取变长参数的方法,具体的原理可以查看这篇博客,大致的意思就是通过传入的第一个参数 fmt 来确定每一个变长参数在内存中的位置,进而获取参数的取值。

(gdb) p fmt
$3 = 0x414228 "test macro fmt error %s"

从上面调试的结果可以看出该函数获取变长参数的具体做法是在给定字符串的最后加上变长参数的格式化输出。

vasprintf 函数将变长参数的内容输出到 buf 中,若成功则返回输出内容的长度,若失败则返回 -1.

/**
 * 获取省略号指定的参数
 */
void LogEvent::format(const char* fmt, ...) {
    va_list al; 
    va_start(al, fmt);
    format(fmt, al);
    va_end(al);
}

/**
 * 将参数输出到m_ss中(格式化写入)
 */
void LogEvent::format(const char* fmt, va_list al) {
    char* buf = nullptr;
    int len = vasprintf(&buf, fmt, al);
    if (len != -1) {
        m_ss << std::string(buf, len);
        free(buf);
    }   
}

上面的这种格式化输出主要用在了下面的这个宏定义里面

#define RAINBOW_LOG_FMT_LEVEL(logger, level, fmt, ...) \
    if (logger->getLevel() <= level) \
        rainbow::LogEventWrap(rainbow::LogEvent::ptr(new rainbow::LogEvent(logger, level, \
                        __FILE__, __LINE__, 0, rainbow::GetThreadId(), \
                        rainbow::GetFiberId(), time(0)))).getEvent()->format(fmt, __VA_ARGS__)

3|4LogAppender

通过该类派生出的不同子类可以将日志信息输出到不同的位置。一个 Logger 可以有多个 Appender,LogAppender 有单独的日志级别,因此可以通过设置不同级别的 Appender 从而将日志输出到不同的位置。此外每一个 LogAppender 也都会有自己单独的日志格式,从而方便进行管理。

还可以通过 scoket 套接字,将日志输出到服务器上。

// 日志输出到目的地(控制台、文件)
class LogAppender {
   public:
    typedef std::shared_ptr<LogAppender> ptr;
    LogAppender();
    virtual ~LogAppender() {}
    // 纯虚函数,子类必须实现该方法
    virtual void log(std::shared_ptr<Logger> logger, LogLevel::Level level,
                     LogEvent::ptr event) = 0;
    // 按照给定的格式序列化输出
    void setFormatter(LogFormatter::ptr val) { m_formatter = val; }
    
    // 获取日志格式
    LogFormatter::ptr getFormatter() const { return m_formatter; }

    LogLevel::Level getLevel() { return m_level; }
    void setLevel(const LogLevel::Level& level);

   protected:
    LogLevel::Level m_level;
    LogFormatter::ptr m_formatter;
};
// 输出到控制台的Appender
class StdoutLogAppender : public LogAppender {
   public:
    typedef std::shared_ptr<StdoutLogAppender> ptr;
    virtual void log(Logger::ptr logger, LogLevel::Level level,
                     LogEvent::ptr event) override;
};

// 定义输出到文件的Appender
class FileLogAppender : public LogAppender {
   public:
    typedef std::shared_ptr<FileLogAppender> ptr;
    virtual void log(Logger::ptr logger, LogLevel::Level level,
                     LogEvent::ptr event) override;
    FileLogAppender(const std::string& filename);
    // 重新打开文件,如果文件打开成功则返回true
    bool reopen();

   private:
    std::string m_filename;
    std::ofstream m_filestream;
};

3|5LogFormatter

日志格式器(LogFormatter)可以将传入的日志格式进行解析,并可以和 LogEvent 指针结合将特定格式的日志信息输出到 stringstream 中,等待 LogEventWrap 析构的时候将日志信息写入到指定的 Appender 中。

// 日志格式器
class LogFormatter {
   public:
    // 指向该类的指针
    typedef std::shared_ptr<LogFormatter> ptr;
    
    // 日志格式
    LogFormatter(const std::string& pattern);

  // 对日志进行解析,并返回格式化之后的string
    std::string format(std::shared_ptr<Logger> ptr, LogLevel::Level level,
                       LogEvent::ptr event);
    std::ostream& format(std::ostream& ofs, std::shared_ptr<Logger> ptr, LogLevel::Level level, LogEvent::ptr event);

   public:
    class FormatItem {
       public:
        FormatItem(const std::string& fmt = ""){};
        virtual ~FormatItem() {}
        virtual void format(std::ostream& os, std::shared_ptr<Logger> logger,
                            LogLevel::Level level, LogEvent::ptr event) = 0;
        // 注意这里的指针类型是FormatItem类型的指针
        typedef std::shared_ptr<FormatItem> ptr;
    };  

    void init();

   private:
  // 日志格式
    std::string m_pattern;
  // 根据日志格式解析出的日志格式单元类的指针存放至数组中
    std::vector<FormatItem::ptr> m_items;
};

3|6Logger

日志可以用 Logger 类来进行表示,每一个 Logger 类含有多个 LoggerAppender,可以通过指针把 Logger 类传递给 LogEvent 从而使日志事件能够获取 Logger 类的一些信息。

std::enable_shared_from_this 能让一个对象(假设其名为 t ,且已被一个 std::shared_ptr 对象 pt 管理)安全地生成其他额外的 std::shared_ptr 实例(假设名为 pt1, pt2, ... ) ,它们与 pt 共享对象 t 的所有权。

// 日志器
class Logger : public std::enable_shared_from_this<Logger> {
   public:
    typedef std::shared_ptr<Logger> ptr;

    Logger(const std::string& name = "root");
    
    // 只有满足日直级别的日志才会被输出
    void log(LogLevel::Level level, LogEvent::ptr event);

    void debug(LogEvent::ptr event);
    void info(LogEvent::ptr event);
    void warn(LogEvent::ptr event);
    void error(LogEvent::ptr event);
    void fatal(LogEvent::ptr event);

    void addAppender(LogAppender::ptr appender);
    void delAppender(LogAppender::ptr appender);

    LogLevel::Level getLevel() const { return m_level; }
    void setAppender(LogLevel::Level val) { m_level = val; }

    const std::string getName() const { return this->m_name; }

   private:
    std::string m_name = "root";       // 日志名称
    LogLevel::Level m_level;  // 日志器的级别
    std::list<LogAppender::ptr> m_appenders;  // Appender集合
    LogFormatter::ptr m_formatter;            // 日志格式
};

3|7LoggerManager

管理所有的日志器,并且支持通过日志器的名字获取日志器。

// 管理所有的日志器
class LoggerManager {
public:
    LoggerManager();
    Logger::ptr getLogger(const std::string& name);

    void init();
    Logger::ptr getRoot() const { return m_root; }

private:
    // 通过日志器的名字获取日志器
    std::map<std::string, Logger::ptr> m_loggers;
    Logger::ptr m_root;
};

/**
 * 日志器管理类,单例模式
 */
typedef rainbow::Singleton<LoggerManager> LoggerMgr;

}  // namespace rainbow

111

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: