Linux多线程服务端编程:使用muduo C++网络库 学习笔记 第五章 高效的多线程日志

news2024/11/14 20:56:34

“日志(logging)”有两个意思:
1.诊断日志(diagnostic log)。即log4j、logback、slf4j、glog、g2log、log4cxx、log4cpp、log4cplus、Pantheios、ezlogger等常用日志库提供的日志功能。

2.交易日志(trasaction log)。即数据库的write-ahead log、文件系统的journaling等,用于记录状态变更,通过回放日志可以逐步恢复每一次修改之后的状态。

本章的“日志”是前一个意思,即文本的、供人阅读的日志,通常用于故障诊断和追踪(trace),也可用于性能分析。日志通常是分布式系统中事故调查的唯一线索,用来追寻蛛丝马迹,查出元凶。

在服务端编程中,日志是必不可少的,在生产环境中应该做到“Log Evenrything All The Time”。对于关键进程,日志通常要记录:
1.收到的每条内部消息的id(还可以包括关键字段、长度、hash等)。

2.收到的每条外部消息的全文。

3.发出的每条消息的全文,每条消息都有全局唯一的id。

4.关键内部状态的变更,等等。

每条日志都有时间戳,这样就能完整追踪分布式系统中一个事件的来龙去脉。也只有这样才能查清楚发生故障时究竟发生了什么,比如业务处理流程卡在了哪一步。

诊断日志不光是给程序员看的,更多的时候是给运维人员看的,因此日志的内容应避免造成误解,不要误导调查故障的主攻方向,拖延故障解决的时间。

一个日志库大体可分为前端(frontend)和后端(backend)两部分。前端是供应用程序使用的接口(API),并生成日志消息(log message);后端则负责把日志消息写到目的地(destination)。这两部分的接口有可能简单到只有一个回调函数:

void output(const char *message, int len);

其中的message字符串是一条完整的日志消息,包含日志级别、时间戳、源文件位置、线程id等基本字段,以及程序出书的具体消息内容。

在多线程程序中,前端和后端都与单线程程序无甚区别,无非是每个线程有自己的前端,整个程序共用一个后端。但难点在于将日志数据从多个前端高效地传输到后端。这是一个典型的多生产者-单消费者问题,对生产者(前端)而言,要尽量做到低延迟、低CPU开销、无阻塞;对消费者(后端)而言,要做到足够大的吞吐量,并占用较少资源。

对C++程序而言,最好整个程序(包括主程序和程序库)都使用相同的日志库,程序有一个整体的日志输出,而不要各个组件有各自的日志输出。从这个意义上讲,日志库是个Singleton。

C++日志库的前端大体上有两种API风格:
1.C/Java的printf(fmt, …)风格,例如:

log_info("Received %d bytes from %s", len, getClientName().c_str());

2.C++的stream <<风格,例如:

LOG_INFO << "Received " << len << " bytes from " << getClientName();

muduo的日志库是C++ stream风格,这样用起来更自然,不必费心保持格式字符串与参数类型的一致性,可以随用随写,而且是类型安全(编译时或运行时没有不兼容或不合法的数据类型操作)的。

stream风格的另一个好处是当输出的日志级别高于语句的日志级别时,打印日志是个空操作,运行时开销接近零。比方说当日志级别为WARNING时,LOG_INFO<<是个空操作,这个语句根本不会调用std::string getClientName()函数,减小了开销。而printf风格不易做到这一点。

muduo没有用标准库中的iostream,而是自己写的LogStream class,这主要是出于性能原因(第十一章)。

常规的通用日志库如log4j/logback通常会提供丰富的功能,但这些功能不一定全都是必需的。

1.日志消息有多种级别(level),如TRACE、DEBUG、INFO、WARN、ERROR、FATAL等。

2.日志消息可能有多个目的地(appender),如文件、socket、SMTP等。

3.日志消息的格式可配置(layout),例如org.apache.log4j.PatternLayout(它是Apache Log4j 1.x中的一种日志输出布局,用于定义日志输出的格式,允许你指定日志消息中各种信息(例如日期、日志级别、日志消息内容等)的显示方式)。

4.可以设置运行时过滤器(filter),控制不同组件的日志消息的级别和目的地。

在上面几项中,作者认为除了第一项之外,其余三项都是非必需的功能。

日志的输出级别在运行时可调,这样同一个可执行文件可以分别在QA测试环境的时候输出DEBUG级别的日志,在生产环境输出INFO级别的日志(muduo默认输出INFO级别的日志,可通过环境变量控制输出DEBUG或TRACE级别的日志)。在必要的时候也可以临时在线调整日志的输出级别。例如某台机器的消息量过大、日志文件太多、磁盘空间紧张,那么可以临时调整为WARNING级别输出,减少日志数目。又比如某个新上线的进程的行为略显古怪,则可以临时调整为DEBUG级别输出,打印更细节的日志消息以便分析查错。调整日志的输出级别不需要重新编译,也不需要重启进程,只要调用muduo::Logger::setLogLevel()就能即时生效。

对于分布式系统中的服务进程而言,日志的目的地(destination)只有一个:本地文件。往网络写日志消息是不靠谱的,因为诊断日志的功能之一正是诊断网络故障,比如连接断开(网卡或交换机故障)、网络暂时不通(若干秒内没有收到心跳消息)、网络拥塞(消息延迟明显加大)等等。如果日志消息也是通过网络发到另一台机器上的,那岂不是一损俱损?如果接收网络日志消息的服务器(日志服务器)发生故障或者出现进程死锁(阻塞),通常会导致发送日志的多个服务进程阻塞,或者内存暴涨(用户态和内核态的TCP缓存),这无异于放大了单机故障。往网络写日志消息的另一个坏处是增加网络带宽消耗。试想收到一条业务消息、发出一条业务消息时都会写日志,如果写到网络上岂不是让网络带宽消耗翻倍,加剧trashing?同理,应该避免往网络文件系统(如NFS)上写日志,这等于掩耳盗铃。

以本地文件为日志的destination,那么日志文件的滚动(rolling)是必需的,这样可以简化日志归档(archive)的实现。rolling的条件通常有两个:文件大小(例如每写满1GB就换下一个文件)和时间(例如每天零点新建一个日志文件,不论前一个文件有没有写满)。muduo日志库的LogFile会自动根据文件大小和时间来主动滚动日志。既然能主动rolling,自然也就不必支持SIGUSR1了,毕竟多线程程序处理signal很麻烦(第四章)。

一个典型的日志文件的文件名如下:
在这里插入图片描述
这个文件名由以下几部分组成:
1.第1部分logfile_test是进程的名字。通常是main()参数中argv[0]的basename(3),这样容易区分究竟是哪个服务器程序的日志。必要时还可以把程序版本加进去。

2.第2部分是文件的创建时间(GMT时区,格林威治标准时间(Greenwich Mean Time)对应的本地时区)。这样很容易通过文件名来选择某一时间范围内的日志,例如用通配符*.20120603-14*表示2012年6月3日下午2点(GMT)左右的日志文件。

3.第3部分是机器名称。这样即使把日志文件拷贝到别的机器上也能追溯其来源。

4.第4部分是进程id。如果一个程序一秒内反复重启,那么每次都会生成不同的日志文件(第九章)。

5.第5部分是统一的后缀名.log。同样是为了便于周边配套脚本的编写。

muduo的日志文件滚动没有采用文件改名的办法,即dmesg.log是最新日志,dmesg.log.1是前一个日志,dmesg.log.2.gz是更早的日志等。这种做法的一个好处是dmesg.log始终是最新日志,便于编写某些及时解析日志的脚本。将来可以增加一个功能,每次滚动日志文件之后立刻创建(更新)一个symlink(符号链接,软链接),logfile_test.log始终指向当前最新的日志文件,这样达到相同的效果。

日志文件压缩与归档(archive,例如在非繁忙时段把压缩后的日志文件拷贝到某个NFS位置,以便集中保存和分析)不是日志库应有的功能,而应该交给专门的脚本去做,这样C++和Java的服务程序可以共享这一基础设施。如果想更换日志压缩算法或归档策略也不必动业务程序,改改周边配套脚本就行了。磁盘空间监控也不是日志库的必备功能。有人或许曾经遇到日志文件把磁盘占满的情况,因此希望日志库能限制空间使用,例如只分配10GB磁盘空间,用满之后就冲掉旧日志,重复利用空间,就像循环磁带一样。殊不知如果出现程序死循环拼命写日志的异常情况,那么往往是开头的几条日志最关键,它往往反映了引发异常(busy-loop)的原因(例如收到某条非法消息),后面都是无用的垃圾日志。如果日志库具备重复利用空间的“功能”,只会帮倒忙。磁盘写入的带宽按100MB/s计算,写满一个100GB的磁盘分组需要16分钟,这足够监控系统报警并人工干预了(第九章)。

往文件写日志的一个常见问题是,万一程序崩溃,那么最后若干条日志往往就丢失了,因为日志库不能每条消息都flush硬盘,更不能每条日志都open/close文件,这样性能开销太大。muduo日志库用两个办法来应对这一点,其一是定期(默认3秒)将缓冲区内的日志消息flush到硬盘;其二是每条内存中的日志消息都带有cookie(或者叫哨兵值/sentry),其值为某个函数的地址,这样通过在core dump文件中查找cookie就能找到尚未来得及写入磁盘的消息。

日志消息的格式是固定的,不需要运行时配置,这样可节省每条日志解析格式字符串的开销。作者认为日志的格式在项目的整个生命周期几乎不会改变,因为我们经常会为不同目的编写parse日志的脚本,既要解析最近几天的日志文件,也要和几个月之前,甚至一年之前的日志文件的同类数据做对比。如果在此期间日志格式变了,势必会增加很多无谓的工作量。如果真的需要调整消息格式,直接修改代码并重新编译即可。以下是muduo日志库的默认消息格式(图5-100):
在这里插入图片描述
日志消息格式有几个要点:
1.尽量每条日志占一行。这样很容易用awk、sed、grep等命令行工具来快速联机分析日志,比方说要查看“2012-06-03 08:02:00”至“2012-06-03 08:02:59”这一分钟内每秒打印日志的条数(直方图),可以运行:

grep -o '^20120603 08:02:..' | sort | uniq -c

2.时间戳精确到微秒。每条消息都通过gettimeofday(2)(可获得timeval时间结构)获得当前时间,这么做不会有什么性能损失。因为在x86-64 Linux上,gettimeofday(2)不是系统调用,不会陷入内核(可用strace(1)命令验证)。

3.始终使用GMT时区。对于跨州的分布式系统而言,可省去本地时区转换的麻烦(主要西方国家大多实行夏令时),更易于追查事件的顺序。

4.打印线程id。便于分析多线程程序的时序,也可以检测死锁。这里的线程id是指用调用LOG_INFO <<的线程,线程id的获取见第四章。

5.打印日志级别。在线查错的时候先看看有无ERROR日志,通常可加速定位问题。

6.打印源文件名和行号。修复bug的时候不至于搞错对象。

每行日志的前4个字段的宽度是固定的,以空格分隔,便于用脚本解析。另外,应该避免在日志格式(特别是消息id)中出现正则表达式的元字符(meta character),例如’[‘和’]'等等,这样在用less(1)命令查看日志文件的时候查找字符串更加便捷。

运行时的日志过滤器(filter)或许是有用的,例如控制不同部件(程序库)的输出日志级别,但作者认为这应该放到编译器去做,整个程序有一个整体的输出级别就足够好了。同时作者认为一个程序同时写多个日志文件(例如不同的日志级别或不同的组件写到不同的文件)是非常罕见的需求,这可以事后留给log archiver来分流,不必做到日志库中。不实现filter自然也能减小生成每条日志的运行时开销,可以提高日志库的性能。

编写Linux服务端程序的时候,我们需要一个高效的日志库。只有日志库足够高效,程序员才敢在代码中输出足够多的诊断信息,减小运维难度,提升效率。高性能体现在几方面:
1.每秒写几千上万条日志的时候没有明显的性能损失。

2.能应对一个进程产生大量日志数据的场景,例如1GB/min。

3.不阻塞正常的执行流程。

4.在多线程程序中,不造成争用(contention)。

这里列举一些具体的性能指标,考虑往普通7400 rpm SATA硬盘写日志文件的情况:
1.磁盘带宽约是110MB/s,日志库应该能瞬时写满这个带宽(不必持续太久)。

2.假如每条日志消息的平均长度是110字节,这意味着一秒要写100万条日志。

以上是“高性能”日志库的最低指标。如果磁盘带宽更高,那么日志库的预期性能指标也会相应提高。反过来说,在磁盘带宽确定的情况下,日志库的性能只要“足够好”就行了。假如某个神奇的日志库能往/dev/null写1000MB数据,那么到哪里去找这么快的磁盘来让程序写诊断日志呢?

这些指标初看起来有些异想天开,什么程序需要1秒写100万条日志消息呢?换一个角度其实很容易想明白,如果一个程序耗尽全部CPU资源和磁盘带宽可以做到1秒写100万条日志消息,那么当只需要1秒写10万条时候(比方说一秒处理两三万条消息,每条消息写三条日志:从哪里收到、计算结果如何、发到哪里),立刻就能腾出90%的资源来干正事(处理业务)。相反,如果一个日志库在满负荷的情况下只能1秒写10万条日志,真正用到生产环境,恐怕就只能1秒写1万条日志才不会影响正常业务处理,这其实钳制了服务器的吞吐量。

以下是muduo日志库在两台机器上的实测性能数据:
在这里插入图片描述
可见muduo日志库在现在的PC上能写到每秒200万条消息,带宽足够撑满两个千兆网连接或4个SATA组成的RAID10(也被称为RAID 1+0,是一种磁盘阵列级别,它将RAID 1(镜像)和RAID 0(条带化)两种技术结合在一起,最少需要4块硬盘,它提供了可靠的数据保护,同时在读写操作方面表现出色),性能是达标的(日志文件是顺序写入的,是对磁盘最友好的一种负载,对IOPS要求不高)。

为了实现这样的性能指标,muduo日志库的实现有几点优化措施值得一提:
1.时间戳字符串中的日期和时间两部分是缓存的,一秒之内的多条日志只需重新格式化微秒部分。图5-100中的3条日志消息中,“20120603 08:02:46”是复用的,每条日志只需要格式化微秒部分(“.125770Z”)。

2.日志消息的前4个字段是定长的,因此可以避免在运行期求字符串长度(不会反复调用strlen)。因为编译器认识memcpy(),对于定长的内存复制,会在编译期把它inline展开为高效的目标代码。

3.线程id是预先格式化为字符串,在输出日志消息时只需简单拷贝几个字节。见CurrentThread::tidString()。

4.每行日志消息的源文件名部分采用了编译期计算来获得basename,避免运行期strrchr(2)函数开销。见SourceFile class,这里利用了gcc的内置函数。

多线程程序对日志库提出了新的需求:线程安全,即多个线程可以并发写日志,两个线程的日志消息不会出现交织。线程安全不难办到,简单的办法是用一个全局mutex保护IO,或者每个线程单独写一个日志文件(Google C++日志库的默认多线程实现即如此),但这两种做法的高效性就堪忧了。前者会造成全部线程抢一个锁,后者有可能让业务线程阻塞在写磁盘操作上。

作者认为一个多线程程序的每个进程最好只写一个日志文件,这样分析日志更容易,不必在多个文件中跳来跳去。再说多线程写多个文件也不一定能提速(操作同一磁盘时,有磁盘读写队列)。解决办法不难想到,用一个背景线程负责收集日志消息,并写入日志文件,其他业务线程只管往这个“日志线程”发送日志消息,这称为“异步日志”。

在多线程服务程序中,异步日志(叫“非阻塞日志”似乎更准确)是必需的,因为如果在网络IO线程或业务线程中直接往磁盘写数据的话,写操作偶尔可能阻塞长达数秒之久(原因很复杂,可能是磁盘或磁盘控制器复位)。这可能导致请求方超时,或者耽误发送心跳消息,在分布式系统中更可能造成多米诺骨牌效应,例如误报死锁引发自动failover等。因此,在正常的实时业务处理流程中应该彻底避免磁盘IO,这在使用one loop per thread模型的非阻塞服务端程序中尤为重要,因为线程是复用的,阻塞线程意味着影响多个客户连接。

我们需要一个“队列”来将前端日志的数据发送到后端(日志线程),但这个“队列”不必是现成的BlockingQueue<std::string>,因为不用每次产生一条日志消息都通知(notify())接收方。

muduo日志库采用的是双缓冲(double buffering)技术,基本思路是准备两块buffer:A和B,前段负责往buffer A填数据(日志消息),后端负责将buffer B的数据写入文件。当buffer A写满后,交换A和B,让后端将buffer A的数据写入文件,而前端则往buffer B填入新的日志消息,如此往复。用两个buffer的好处是在新建日志消息的时候不必等待磁盘文件操作,也避免每条新日志消息都触发(唤醒)后端日志线程。换言之,前端不是将一条条日志分别传送到后端,而是将多条日志消息拼成一个大的buffer传送给后端,相当于批处理,减少了线程唤醒的频度,降低开销。另外,为了及时将日志消息写入文件,即使buffer A未满,日志库也会每3秒执行一次上述交换写入操作。

muduo异步日志的性能开销大约是前端每写一条日志消息耗时1.0us~1.6us。

muduo实际采用了四个缓冲区,这样可以进一步减少或避免日志前端的等待。数据结构如下(muduo/base/AsyncLogging.h):
在这里插入图片描述
boost::ptr_vector是Boost C++ 库中的一个容器类,用于管理指针的动态数组。它提供了一种在运行时创建和销毁对象的方式,这些对象以指针的形式存储在容器中。boost::ptr_vector已经过时,并不再推荐在新代码中使用。C++11和以后的标准库提供了更好的智能指针和容器,如std::vector与std::shared_ptr或std::unique_ptr结合使用,以实现更安全和高效的动态内存管理。

boost::ptr_vector::auto_type是boost::ptr_vector类中的一个成员类型,它用于表示boost::ptr_vector中存储的指针类型。

其中,LargeBuffer类型是FixedBuffer class template的一份具体实现(instantiation),其大小为4MB,可以存至少1000条日志消息。boost::ptr_vector<T>::auto_type类型类似C++11中的std::unique_ptr,具备移动语义(move semantics),而且能自动管理对象生命期。mutex_用户保护后面的4个数据成员。buffers_存放的是供后端写入的buffer。

先来看发送方代码:

void AsyncLogging::append(const char *logline, int len)
{
    muduo::MutexLockGuard lock(mutex_);
    // most common case: buffer is not full, copy data here
    if (currentBuffer_->avail() > len)
    {
        currentBuffer_->append(logline, len);
    }
    // buffer is full, push it, and find next spare buffer
    else 
    {
        // 将currentBuffer移入buffers_
        buffers_.push_back(currentBuffer_.release());
        
        // is there is one already, use it
        if (nextBuffer_)
        {
            // 移动,而非复制
            // 在Boost C++库的boost::ptr_container中,move是一个成员函数
            // 用于将元素从一个ptr_container移动到另一个
            // 这个函数对于在不同容器之间转移拥有的指针对象非常有用,而不是复制它们。
            currentBuffer_ = boost::ptr_container::move(nextBuffer_);
        }
        // allocate a new one
        else
        {
            // Rarely happens
            currentBuffer_.reset(new LargeBuffer);
        }
        currentBuffer_->append(logline, len);
        cond_.notify();
    }
}

前端在生成一条日志消息的时候会调用AsyncLogging::append()。在这个函数中,如果当前缓冲(currentBuffer_)剩余的空间足够大,则会直接把日志消息拷贝(追加)到当前缓冲中,这是最常见的情况。这里拷贝一条日志消息并不会带来多大开销。前后端代码的其余部分都没有拷贝日志,而是简单的指针交换。

否则说明当前缓冲已经写满,就把它送入(移入)buffers_( buffers_.push_back(currentBuffer_.release());),然后试图把预备好的另一块缓冲(next_Buffer_)移用(move)为当前缓冲,然后追加消息日志并通知(唤醒)后端开始写入日志数据。以上两种操作在临界区之内都没有耗时的操作,运行时间为常数。

如果前端写入速度太快,一下子把两块缓冲都用完了,那么只好分配一块新的buffer,作为当前缓冲,这是极少发生的情况。

再来看接收方(后端)实现,这里只给出了最关键的临界区内代码:

void AsyncLogging::threadFunc()
{
    BufferPtr newBuffer1(new LargeBuffer);
    BufferPtr newBuffer2(new LargeBuffer);
    // reserve()从略
    BufferVector buffersToWrite;
    while (running_)
    {
        // swap out what need to be written, keep CS short
        {
            muduo::MutexLockGuard lock(mutex_);
            // unusual usage!
            if (buffers_.empty())
            {
                cond_.waitForSeconds(flushInterval_);
            }
            // 移动,而非复制
            buffers_.push_back(currentBuffer_.release());
            // 移动,而非复制
            currentBuffer_ = boost::ptr_container::move(newBuffer1);
            // 内部指针交换,而非复制
            buffersToWrite.swap(buffers_);
            // 如果没有nextBuffer_,说明在append的时候第currentBuffer_已被写满,因此
            // currentBuffer_已经被移到了buffers_中,nextBuffer_的内容已被move到了currentBuffer_里
            if (!nextBuffer_)
            {
                // 移动,而非复制
                nextBuffer_ = boost::ptr_container::move(newBuffer2);
            }
        }
        // output buffersToWrite to file
        // re-fill newBuffer1 and newBuffer2
    }
    // flush output
}

首先准备好两块空闲的buffer,以备在临界区内交换。在临界区内,等待条件触发,这里的条件有两个:其一是超时,其二是前端写满了一个或多个buffer。这里是非常规的condition variable用法,它没有使用while循环,而且等待时间有上限。

当“条件”满足时,先将当前缓冲(currentBuffer_)移入buffers_,并立刻将空闲的newBuffer1移为当前缓冲。注意这整段代码位于临界区之内,因此不会有任何race condition。接下来将buffers_与buffersToWrite交换,后面的代码可以在临界区之外安全地访问buffersToWrite,将其中的日志数据写入文件。临界区里最后干的一件事情是用newBuffer2替换nextBuffer_,这样前端始终有一个预备buffer可供调配。nextBuffer_可以减少前端临界区分配内存的概率,缩短前端临界区长度。注意到后端临界区内也没有耗时操作,运行时间为常数。

// re-fill newBuffer1 and newBuffer2会将buffersToWrite内的buffer重新填充到newBuffer1和newBuffer2(newBuffer1和newBuffer2在临界区中被move了,指向空,此处重新将buffersToWrite里的缓冲区赋予newBuffer1和newBuffer2),这样下次执行的时候还有两个空闲buffer可用于替换前端的当前缓冲和预备缓冲。最后,这四个缓冲在程序启动的时候会全部填充为0,这样可以避免程序热身时page fault(页错误,缺页中断)引发性能不稳定(在程序启动时,将缓冲区填充为0可以让程序访问那些未加载到物理内存中的页面,从而将其加载到物理内存中,这样在程序的热身阶段,即程序刚开始运行时,不会因为打印日志时的缓冲区缺页而引起性能降低)。

以下用图表展示前端和后端的具体交互情况。一开始分配好四个缓冲区A、B、C、D,前端和后端各持有其中两个。前端和后端各有一个缓冲区数组,初始时都是空的。

第一种情况是前端写日志的频度不高,后端3秒超时后将“当前缓冲currentBuffer_”写入文件:
在这里插入图片描述
在第2.9秒的时候,currentBuffer_使用了80%,在第3秒的时候后端线程醒过来,先把currentBuffer_送入buffers_,再把newBuffer1移用为currentBuffer_。随后第3+秒,交换buffers_和buffersToWrite,离开临界区,后端开始将buffer A写入文件。写完(write done)之后再把newBuffer1重新填上,等待下一次cond_.waitForSeconds()返回。

后面在画图时将有所简化,不再画出buffers_和buffersToWrite交换的步骤。

第二种情况,在3秒超时之前已经写满了当前缓冲,于是唤醒后端线程开始写入文件:
在这里插入图片描述
在第1.5秒的时候,currentBuffer_使用了80%;第1.8秒,currentBuffer_写满,于是将当前缓冲送入buffers_,并将nextBuffer_移用为当前缓冲,然后唤醒后端线程开始写入。当后端线程唤醒之后(第1.8+秒),先将currentBuffer_送入buffers_,再把newBuffer1移用为currentBuffer_,然后交换buffers_和buffersToWrite,最后用newBuffer2替换nextBuffer_,即保证前端有两个空缓冲可用。离开临界区之后,将buffersToWrite中的缓冲区A和B写入文件,写完之后重新填充newBuffer1和newBuffer2,完成一次循环。

上面这两种情况都是最常见的,再来看一看前端需要分配新buffer的两种情况。

第三种情况,前端在短时间内密集写入日志消息,用完了两个缓冲,并重新分配了一块新的缓存:
在这里插入图片描述
在第1.8秒的时候,缓冲A已经写满,缓冲B也接近写满,并且已经notify()了后端线程,但是出于种种原因,后端线程并没有立刻开始工作。到了第1.9秒,缓冲区B也已经写满,前端线程新分配了缓冲E。到了第1.8+秒,后端线程终于获得控制权,将C、D两块缓冲交给前端,并开始将A、B、E依次写入文件。一段时间之后,完成写入操作,用A、B重新填充那两块空闲缓冲。注意这里有意用A和B来填充newBuffer1/2,而释放了缓冲E,这是因为使用A和B不会造成page fault。

第四种情况,文件写入速度较慢,导致前端耗尽了两个缓冲,并分配了新缓冲:
在这里插入图片描述
前1.8+秒的场景和前面“第二种情况”相同,前端写满了一个缓冲,唤醒后端线程开始写入文件。之后,后端花了较长时间(大半秒)才将数据写完。这期间前端又用完了两个缓冲,并分配了一个新的缓冲,这期间前端的notify()已经丢失。当后端写完(write done)后,发现buffers_不为空,立刻进入下一循环。即替换前端的两个缓冲,并开始一次写入C、D、E。假定前端在此期间产生的日志较少,后端会将C、D分别填充到new1、new2,然后释放E,之后进入下一次循环。

前面我们一共准备了四块缓冲,应该足以应付日常需求,如果需要进一步增加buffer数目,可以改用下面的数据结构:
在这里插入图片描述
初始化时在emptyBuffers_中放入足够多空闲buffer,这样前端几乎不会遇到需要在临界区内新分配buffer的情况,这是一种空间换时间的做法。为了避免短时突发写大量日志造成新分配的buffer占用过多内存,后端代码应该保证emptyBuffers_和fullBuffers_的长度之和不超过某个定值。buffer在前端和后端之间流动,形成一个循环:
在这里插入图片描述
万一前端陷入死循环,拼命发送日志消息,超过后端的处理(输出)能力,会导致什么后果?对于同步日志来说,这不是问题,因为阻塞IO自然就限制了前端的写入速度,起到了节流阀(throttling)的作用。但是对于异步日志来说,这就是典型的生产速度高于消费速度问题,会造成数据在内存中堆积,严重时引发性能问题(可用内存不足)或程序崩溃(分配内存失败)。

muduo日志库处理日志堆积的方法很简单:直接丢掉多余的日志buffer,以腾出内存,见muduo/base/AsyncLogging.cc第87~96行代码。这样可以防止日志库本身引起程序故障,是一种自我保护措施。将来或许可以加上网络报警功能,通知人工介入,以尽快修复故障。

当然在前端和后端之间高效传递日志消息的办法不止这一种,比方说使用常规的muduo::BlockingQueue<std::string>muduo::BoundedBlockingQueue<std::string>在前后端之间传递日志消息,其中每个std::string是一条消息。这种做法每条日志消息都要分配内存,特别是在前端线程分配的内存要由后端线程释放,因此对malloc的实现要求较高,需要针对多线程特别优化。另外,如果用这种方案,那么需要修改LogStream的Buffer,使之直接将日志写到std::string中,可节省一次内存拷贝。

相比前面展示的直接拷贝日志消息的做法,这个传递指针的方案(前文说的是传递string,这里的意思是改为传递string的指针?)似乎会更高效,但据作者测试,直接拷贝日志数据的做法比传递指针快3倍(在每条日志消息不大于4kB的时候),估计是内存分配的开销所致。因此muduo日志库只提供了C风格字符串、双缓冲区的异步日志机制。这再次说明性能不能凭感觉说了算,一定要有典型场景的测试数据作为支撑。

muduo现在的异步日志实现用了一个全局锁。尽管临界区很小,但是如果线程数目较多,锁争用(lock contention)也可能影响性能。一种解决办法是像Java的ConcurrentHashMap那样用多个桶子(bucket),前端写日志的时候再按线程id哈希到不同的bucket中,以减少contention。这种方案的后端实现较为复杂。

为了简化实现,目前muduo日志库只允许指定日志文件的名字,不允许指定其路径。日志库会把日志文件写到当前路径,因此可以在启动脚本(shell脚本)里改变当前路径,以达到相同的目的。

Linux默认会把core dump写到当前目录,而且文件名是固定的core。为了不让新的core dump文件冲掉旧的,我们可以通过sysctl命令设置kernel.core_pattern参数(也可以修改/proc/sys/kernel/core_pattern),让每次core dump都产生不同的文件。例如设为%e.%t.%p.%u.core,其中各个参数的含义见man 5 core。另外也可以使用Apport来收集有用的诊断信息,见https://wiki.ubuntu.com/Apport。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1143720.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023MathorCup高校数模挑战赛B题完整解题代码教程

赛道 B&#xff1a; 电商零售商家需求预测及库存优化问题 问题背景&#xff1a; 电商平台存在着上千个商家&#xff0c;他们会将商品货物放在电商配套的仓库&#xff0c; 电商平台会对这些货物进行统一管理。通过科学的管理手段和智能决策&#xff0c; 大数据智能驱动的供应链…

【C++】多态 ③ ( “ 多态 “ 实现需要满足的三个条件 | “ 多态 “ 的应用场景 | “ 多态 “ 的思想 | “ 多态 “ 代码示例 )

文章目录 一、" 多态 " 实现条件1、" 多态 " 实现需要满足的三个条件2、" 多态 " 的应用场景3、" 多态 " 的思想 二、" 多态 " 代码示例 一、" 多态 " 实现条件 1、" 多态 " 实现需要满足的三个条件 &q…

网络原理的讲解

网络原理 重要性: 网络原理知识 1.工作中非常重要的理论知识,尤其是正在调试一些bug的时候. 2.面试中非常重要的考点. 3.学习中非常关键的难点. 网络原理这里,主要给大家介绍, TCP/IP协议 这里的关键协议. 按照这里的这四层,分别进行介绍(物理层不涉及) 应用层 是和程序猿打…

蓝桥杯 第 2 场算法双周赛 第4题 通关【算法赛】c++ 优先队列 + 小根堆 详解注释版

题目 通关【算法赛】https://www.lanqiao.cn/problems/5889/learning/?contest_id145 问题描述 小蓝最近迷上了一款电玩游戏“蓝桥争霸”。这款游戏由很多关卡和副本组成&#xff0c;每一关可以抽象为一个节点&#xff0c;整个游戏的关卡可以抽象为一棵树形图&#xff0c;每…

asp.net教务管理信息系统VS开发sqlserver数据库web结构c#编程Microsoft Visual Studio计算机毕业设计

一、源码特点 asp.net 教务管理信息系统是一套完善的web设计管理系统&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为vs2010&#xff0c;数据库为sqlserver2008&#xff0c;使用c#语言 开发 asp.net教务管理系统 应用技术&a…

基于SSM的高校图书馆设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

真机环境配置教程

1.下载安装包 https://developers.google.com/android/images 2.刷机教程 Xposed精品连载 | 一篇文章彻底搞定安卓刷机与Root 3.配置root

【VTK】关于VTK图像的系列功能

很高兴在雪易的CSDN遇见你 &#xff0c;给你糖糖 欢迎大家加入雪易社区-CSDN社区云 前言 本文总结VTK中图像的系列功能&#xff0c;包括图像的导入&#xff08;读取&#xff09;、显示、交互和导出&#xff08;保存&#xff09;等。详细讲解VTK中提供的各种解决思路&#xff…

DreamTexture.js - 基于稳定扩散的3D模型自动纹理化开发包

DreamTexture.js 是面向 three.js 开发者的 3D 模型纹理自动生成与设置开发包&#xff0c;可以为 webGL 应用增加 3D 模型的快速自动纹理化能力&#xff0c;官方下载地址&#xff1a;DreamTexture.js自动纹理化开发包 。 图一为原始模型, 图二图三为贴图后的模型。提示词&#…

PN8370 超低待机功耗准谐振原边反馈交直流转换器 适用于5V 2A的充电器芯片

PN8370集成超低待机功耗准谐振原边控制器及650V高雪崩能力智能功率MOSFET&#xff0c;用于高性能、外围元器件精简的充电器、适配器和内置电源。 PN8370为原边反馈工作模式,可省略光耦和TL431。内置高压启动电路,可实现芯片空载损耗(230VAC)小于30mW。在恒压模式&#xff0c;采…

FindDiff_Qt找不同项目

文章目录 项目简介源代码widget.hwidget.cppwidget.ui配置文件找不同.json 项目简介 开发平台 win10Qt6.6msvc2022 简介 微信上有一些好玩的游戏, 找不同一种比较轻松有趣的游戏,也曾经在街机上被坑过N币, 玩了几次后,发现还是太难了,于是开始截屏放大,慢慢找,再然后就发展到截…

2023软件测试高频面试题

前言 今天&#xff0c;我们来整理与解析一些比较高频的测试行业面试题&#xff0c;大家可以通过面试题内的一些解析&#xff0c;再结合自己的真实工作经验来进行答题思路的提取、整理。 友情提示&#xff1a;硬背答案虽可&#xff0c;但容易翻车哦。 同时&#xff0c;我也准备…

【蓝桥杯选拔赛真题04】C++计算24数字游戏 青少年组蓝桥杯C++选拔赛真题 STEMA比赛真题解析

目录 C/C++计算24数字游戏 一、题目要求 1、编程实现 2、输入输出 二、算法分析

7、电路综合-基于简化实频的SRFT微带线巴特沃兹低通滤波器设计

7、电路综合-基于简化实频的SRFT微带线巴特沃兹低通滤波器设计 5、电路综合-超酷-基于S11参数直接综合出微带线电路图中已经介绍了如何从传输函数或S参数综合出电路图。 24、基于原型的切比雪夫低通滤波器设计理论&#xff08;插入损耗法&#xff09;中介绍了使用集总参数元件…

基于SSM的航班订票管理系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

【Python · PyTorch】线性代数 微积分

本文采用Python及PyTorch版本如下&#xff1a; Python&#xff1a;3.9.0 PyTorch&#xff1a;2.0.1cpu 本文为博主自用知识点提纲&#xff0c;无过于具体介绍&#xff0c;详细内容请参考其他文章。 线性代数 & 微积分 1. 线性代数1.1 基础1.1.1 标量1.1.2 向量长度&…

PyTorch中grid_sample的使用方法

官方文档首先Pytorch中grid_sample函数的接口声明如下&#xff1a; torch.nn.functional.grid_sample(input, grid, modebilinear, padding_modezeros, align_cornersNone)input : 输入tensor&#xff0c; shape为 [N, C, H_in, W_in]grid: 一个field flow&#xff0c; shape为…

PS 安装教程 2022版(全网最详细图文教程)

目录 一.简介 二.安装步骤 软件&#xff1a;PS版本&#xff1a;2022语言&#xff1a;简体中文大小&#xff1a;2.83G安装环境&#xff1a;Win10&#xff08;1903&#xff09;及以上版本&#xff0c;64位操作系统硬件要求&#xff1a;CPU2.0GHz 内存4G(或更高&#xff0c;不支…

【Unity小技巧】如何在 Unity 中使用我们的Cinemachine虚拟相机跟踪多个目标

文章目录 每篇一句前言安装虚拟相机跟随多个目标和间隙占比代码控制添加主角目标代码控制添加敌人目标扩展代码如何实现虚拟相机跟随玩家呢&#xff1f;我们来实现一下修改虚拟相机的视野修改虚拟相机的位置和角度 推荐完结 每篇一句 岂不闻天无绝人之路&#xff0c;只要我想走…

变压器分析

参考方向 如图所示&#xff0c;是变压器的原理图。其中&#xff0c; ϕ \phi ϕ是变压器铁芯的有效磁通&#xff0c; ϕ 1 \phi_1 ϕ1​是主线圈的漏磁通&#xff0c; ϕ 2 \phi_2 ϕ2​是副线圈的漏磁通。图中 u 1 u_1 u1​为初级线圈输入电压&#xff0c; i 1 i_1 i1​为初级…