多缓冲提高日志系统性能

前言:无论什么项目肯定都少不了日志系统,所以一个高性能的日志系统是不可避免的。

本文介绍的是自己用c++11实现的一个简单的多缓冲区日志系统,比较水,仅供参考^_^


主题:

  • 日志系统及重要性
  • 单缓冲日志系统模型及缺陷
  • 多缓冲buffer介绍及优势
  • 多缓冲区缺陷
  • Buffer类设计及分析
  • Logger类设计及分析

日志系统及重要性:

日志信息对于一个优秀项目来说是非常重要的,因为无论再优秀的软件都有可能产生崩溃或异常,此时,日志系统就能发挥它的作用。

快速定位到错误地点以及错误内容,或者查看最近信息等。

一般来说一个日志系统会分级写日志,比如INFO信息日志(用户的一些操作等),ERROR错误日志(系统崩溃或异常),FAIL失败日志(某项操作失败)等等。

由于日志系统非常重要,它会出现在我们程序的每个角落,所以一个好的日志系统就非常重要了,现存的有许多好的实现比如c++的log4,下面介绍是按自己的思路实现的一个非常简单的日志系统。


单缓冲日志系统模型及缺陷

最简单的日志系统就是单缓冲或者无缓冲的。

无缓冲:

无缓冲最简单,在个需要输出日志信息的地点都输出信息到文件并写入磁盘即可,但是注意现在程序一般都是并发执行,多进程或多线程写文件我们要加锁。

这样效率就比较低了,比如你有20个线程在运行,每次输出日志都要先抢到锁然后在输出,并且输出到磁盘本身就很慢,这样不仅输出日志效率低,更可能会影响到程序的运行(会阻塞程序,因为日志输出是无处不在的)。


单缓冲:

单缓冲就是我们开辟一块固定大小的空间,每次日志输出都先输出到缓冲中,等到缓冲区满在一次刷新到磁盘上,这样相比较无缓冲效率提高了一些,不用每次都输出到磁盘文件上,待到一定数量再刷新到磁盘上。但是每次输出到日志文件上的线程或进程都要加锁,还是存在一个抢锁的过程,效率也不高。

模型如下

从上图能看出来,每次写缓冲还是存在抢锁和阻塞的过程,这样效率还是比较低的,相对无缓冲来说,仅仅减少了磁盘IO的次数。但在磁盘IO时,程序依旧会阻塞

磁盘IO依旧是瓶颈


多缓冲 buffer介绍

既然单块缓冲满足不了我们的要求,效率依然比较低,那么我们可以尝试选择多块缓冲来实现。程序只关注当前缓冲,其余多块缓冲交给后台线程来处理。



模型如下

当前缓冲为我们程序写缓冲Buffer,备用缓冲Buffer为我们提前开辟缓冲,当当前curBuf缓冲满时交换Buffer。如下图

在实际中我们用指针来操控(代码中我使用的std::shared_ptr,只用交换指针即可),交换完毕后如下图

此时,我们可以唤醒后台线程处理已满缓冲,当前缓冲交换后为空,程序可以继续写当前缓冲而不会因为磁盘IO而阻塞。

如果程序写日志速度非常快,我们可以开大缓冲,或者将当前缓冲设置为两块,备用缓冲可设置为多块,在实际编写程序时,因为我用的是list<std::shared_ptr>这种结构来保存,当备用缓冲不够时,会创建一块,然后list会自动push_back,这样慢慢程序会达到最适应自己的缓冲大小。

优势很明显了,我们程序只管写,一切由后台线程来完成,不会阻塞在磁盘IO上。


多缓冲区缺陷

多缓冲区设计是有缺陷的,相比较单缓冲是避免了磁盘IO这一耗时的操作,但如果程序写日志量非常大时,每次写curBuf当前缓冲都要先抢锁,可见效率之低,等待锁的时间耗费非常大。多个线程或进程操作一块或两块缓冲,锁的颗粒度非常大。我们可以尝试减小锁的颗粒度

解决方案可以参考Java的ConcurrentHashMap原理,ConcurrentHashMap是内部建立多个桶,每次hash到不同的桶中,锁只锁相应的桶,那么等于减少了锁的颗粒度,阻塞在锁上的频率也就大大降低。

如下图

当前程序Buffer开辟多个,类似多个桶,锁只锁相应的桶即可,减小了锁的颗粒度

这么做算已时间换空间了,然后如果没有这么大需求上面方案即可解决。


Buffer类设计及分析

Buffer类的设计参考了netty中的buffer,一块缓冲,三个标记分别为可读位置readable,可写位置writable,容量capacity。

其实readable和writable位置都为0,capacity为容量大小。

写缓冲时writable移动,读缓冲时readable移动,writable <= capacity。

缓冲区我使用了vector<char>,参考了陈硕前辈的muduo,使用vector<char>一方面它内部和数组是一样的,其次我们还可以借助vector特性来管理它。Buffer类比较简单

class Buffer
{
    /* 初始化默认大小 */
    static size_t initializeSize;

    public:
        /* 构造函数,初始化buffer,并且设置可读可写的位置 */
        explicit Buffer(size_t BufferSize = initializeSize):
            readable(0), writable(0)
        {
            /* 提前开辟好大小 */
            buffer.resize(BufferSize);
        }

        /* 返回缓冲区的容量 */
        size_t Capacity()
        {
            return buffer.capacity();
        }

        /* 返回缓冲区的大小 */
        size_t Size()
        {
            return writable;
        }

        /* set Size */
        void setSize(void)
        {
            readable = 0;
            writable = 0;
        }

        /* 向buffer中添加数据 */
        void append(const char* mesg, int len)
        {
            strncpy(WritePoint(), mesg, len);
            writable += len;
        }

        /* 返回buffer可用大小 */
        size_t avail()
        {
            return Capacity()-writable;
        }

    private:
        /* 返回可读位置的指针 */
        char* ReadPoint()
        {
            return &buffer[readable];
        }

        /* 返回可写位置的指针 */
        char* WritePoint()
        {
            return &buffer[writable];
        }

        /* 返回可读位置 */
        size_t ReadAddr()
        {
            return readable;
        }

        /* 返回可写位置 */
        size_t WriteAddr()
        {
            return writable;
        }

    private:
        std::vector<char> buffer;
        size_t readable;
        size_t writable;
};

Logger类设计及分析

Logger类我的实现遵从与刚才说的多缓冲模型。

curBuf为一块,备用Buffer为两块,并且可自适应改变。

class Logger
{
    public:
        /* 创建日志类实例 */
        static std::shared_ptr<Logger> setLogger();
        static std::shared_ptr<Logger> setLogger(size_t bufSize);

        /* 得到日志类实例 */
        static std::shared_ptr<Logger> getLogger();

        /* 按格式输出日志信息到指定文件 */
        static void logStream(const char* mesg, int len);

    private:
        /* shared_ptr智能指针管理log类 */
        static std::shared_ptr<Logger> myLogger;

        /* 当前缓冲 */
        static std::shared_ptr<Buffer> curBuf;
        /* list管理备用缓冲 */
        static std::list<std::shared_ptr<Buffer>> bufList;
        /* 备用缓冲中返回一块可用的缓冲 */
        static std::shared_ptr<Buffer> useFul();
        /* 条件变量 */
        static std::condition_variable readableBuf;
        /* 后台线程需要处理的Buffer数目 */
        static int readableNum;
        /* 互斥锁 */
        static std::mutex mutex;
        /* 后台线程 */
        static std::thread readThread;
        /* 线程执行函数 */
        static void Threadfunc();
        static void func();
        /* 条件变量条件 */
        static bool isHave();
};

从上面代码可以看出当前Buffer和备用Buffer都用智能指针来管理,我们不用操心资源释放等问题,因为为指针,当前Buffer和备用Buffer交换起来速度非常快。



初始化函数

std::shared_ptr<Logger>
Logger::
setLogger()
{
    if(myLogger == nullptr)
    {
        /* 创建日志类 */
        myLogger = std::move(std::make_shared<Logger>());
        /* 创建当前Buffer */
        curBuf = std::make_shared<Buffer>();
        /* 创建两块备用Buffer */
        bufList.resize(2);
        (*bufList.begin()) = std::make_shared<Buffer>();
        (*(++bufList.begin())) = std::make_shared<Buffer>();
    }
    return myLogger;
}

都是由智能指针来管理



useful类,返回一个可用的备用Buffer

std::shared_ptr<Buffer>
Logger::
useFul()
{
    auto iter = bufList.begin();
    /* 查询是否存在可用的Buffer */
    for(; iter != bufList.end(); ++iter)
    {
        if((*iter)->Size() == 0)
        {
            break;
        }
    }
    /* 不存在则创建一块新Buffer并返回 */
    if(iter == bufList.end())
    {
        std::shared_ptr<Buffer> p = std::make_shared<Buffer>();
        /* 统一使用右值来提高效率 */
        bufList.push_back(std::move(p));
        return p;
    }
    return *iter;
}

这算是自适应过程了,随着程序的运行会返回适应的大小。



logStream写日志类

void
Logger::
logStream(const char* mesg, int len)
{
    /* 上锁,使用unique_lock为和condition_variable条件变量结合 */
    std::unique_lock<std::mutex> locker(mutex);
    /* 判断当前缓冲是已满,满了则与备用缓冲交换新的 */
    if(curBuf->avail() > len)
    {
        curBuf->append(mesg, len);
    }
    else
    {
        /* 得到一块备用缓冲 */
        auto useBuf = useFul();
        /* 交换指针即可 */
        curBuf.swap(useBuf);
        /* 可读缓冲数量增加 */
        ++readableNum;
        /* 唤醒阻塞后台线程 */
        readableBuf.notify_one();
    }
}


线程主要执行函数

void
Logger::func()
{
    std::unique_lock<std::mutex> locker(mutex);
    auto iter = bufList.begin();
    /* 如果备用缓冲并无数据可读,阻塞等待唤醒 */
    if(readableNum == 0)
    {
        readableBuf.wait(locker, Logger::isHave);
    }
    /* 找数据不为空的Buffer */
    for(; iter != bufList.end(); ++iter)
    {
        if((*iter)->Size() != 0)
            break;
    }
    /* 如果到末尾没找到,没有数据可读 */
    if(iter == bufList.end())
    {
        return;
    }
    else
    {
        /* 将满的缓冲写到文件中 */
        int fd = open("1.txt", O_RDWR | O_APPEND, 00700);
        if(fd < 0)
        {
            perror("open error\n");
            exit(1);
        }
        write(fd, iter->get(), (*iter)->Capacity());
        /* 清空缓冲 */
        bzero(iter->get(), (*iter)->Capacity());
        /* 归位readable和writable */
        (*iter)->setSize();
        /* 可读缓冲数量减1 */
        --readableNum;
    }
}

仅仅是一个简单的实现,如有更优方案或错误还望指出,谢谢~

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-13 10:44:38

多缓冲提高日志系统性能的相关文章

提高日志质量的 5 大技巧

最近涌现出各种各样能帮助你理解日志的新工具,有类似 Scribe.Logstash 这样的开源项目,也有类似 Splunk 的预付费工具,还有托管服务如 SumoLogic 和 PaperTrail.这些工具的共同点是对日志数据进行清洗,在大量日志中提取一些更有价值的文件. 但有一件事这些工具却爱莫能助,因为它们完全依赖你实际投入的日志数据,而如何保证数据的质量和数量则需要用户自行完成.因此,在关键时刻,如果你需要基于部分或者遗漏日志做代码调试时,事情可能会变得非常棘手. 为了减少这种情况发生,

将数据放至数据库外或文件系统来提高报表系统性能

在报表应用中,针对历史数据查询的报表占比很大,这类报表的特点是:第一,数据变化小,查询的历史数据几乎不会发生变化:第二,数据量大,数据量随时间跨度增大而不断增加.如果数据始终存放在数据库中,由于大多数数据库的JDBC性能都很低下(JDBC取数过程要做数据对象转换,比从文件中读取数据会慢一个数量级),这时涉及数据量较大或在并发较多的时候,报表的性能会急剧下降.如果能将这些变化不大的历史数据移出数据库,采用文件系统存储,将可能获得比数据库高得多的IO性能,从而提高报表的整体性能. 但是,报表并不是直

提高ubuntu系统性能的小技巧

在UBUNTU系统里面,并不是你的物理内存全部耗尽之后,系统才使用swap分区!系统的swappiness设定值,对如何使用swap分区是有着很大的联系,并不是当swappiness=0的时候就不使用swap分区, swappiness=0 的时候表示最大限度使用物理内存,然后才是 swap空间,swappiness=100的时候表示积极的使用swap分区,并且把内存上的数据及时的搬运到swap空间里面 cat /proc/sys/vm/swappiness //查看swappiness的值 s

Hibernate 利用缓存(一级、二级、查询)提高系统性能

在hibernate中我们最常用的有三类缓存,分别为一级缓存.二级缓存和查询缓存,下面我们对这三个缓存在项目中的使用以及优缺点分析一下. 缓存它的作用在于提高性能系统性能,介于应用系统与数据库之间而存在于内存或磁盘上的数据. 首先,来看一下一级缓存它默认开启且很常用. 一级缓存 同是一种缓存常常可以有好几个名字,这是从不同的角度考虑的结果,从缓存的生命周期角度来看一级缓存又可以叫做:sessin缓存.线程级缓存.事务级缓存.我们编程中线程.事务.session这三个概念是绑定到一起的放到了thr

logstash+elasticsearch+kibana日志收集

一. 环境准备 角色 SERVER IP logstash agent 10.1.11.31 logstash agent 10.1.11.35 logstash agent 10.1.11.36 logstash central 10.1.11.13 elasticsearch  10.1.11.13 redis 10.1.11.13 kibana 10.1.11.13 架构图如下: 整个流程如下: 1) 远程节点的logstash agent收集本地日志后发送到远程redis的list队列

MySQL-重做日志 redo log -原理

[redo log buffer][redo log file]-原理 目录: 1.重做日志写入过程图 2.相关知识点汇总图 3.redo_log_buffer 原理 4.redo_log_file 原理 1. 重做日志写入过程: 2. 相关知识点汇总: 3. redo log buffer 原理 重做日志缓冲(redo log buffer)是Innodb存储引擎的内存区域中的一部分. [重做日志信息--(1)-->redo log buffer--(2)-->重做日志文件] 在(2)中涉及

如何提高数据库性能

一个成熟的数据库架构并不是一开始设计就具备高可用.高伸缩等特性的,它是随着用户量的增加,基础架构才逐渐完善.这篇博文主要谈MySQL数据库发展周期中所面临的问题及优化方案,暂且抛开前端应用不说,大致分为以下五个阶段: 1.数据库表设计 项目立项后,开发部根据产品部需求开发项目,开发工程师工作其中一部分就是对表结构设计.对于数据库来说,这点很重要,如果设计不当,会直接影响访问速度和用户体验.影响的因素很多,比如慢查询.低效的查询语句.没有适当建立索引.数据库堵塞(死锁)等.当然,有测试工程师的团队

C# 超高速高性能写日志 代码开源

1.需求 需求很简单,就是在C#开发中高速写日志.比如在高并发,高流量的地方需要写日志.我们知道程序在操作磁盘时是比较耗时的,所以我们把日志写到磁盘上会有一定的时间耗在上面,这些并不是我们想看到的. 2.解决方案 2.1.简单原理说明 使用列队先缓存到内存,然后我们一直有个线程再从列队中写到磁盘上,这样就可以高速高性能的写日志了.因为速度慢的地方我们分离出来了,也就是说程序在把日志扔给列队后,程序的日志部分就算完成了,后面操作磁盘耗时的部分程序是不需要关心的,由另一个线程操作. 俗话说,鱼和熊掌

java的 IO流之缓冲流(转载)

java缓冲流本身不具IO功能,只是在别的流上加上缓冲提高效率,像是为别的流装上一种包装.当对文件或其他目标频繁读写或操作效率低,效能差.这时使用缓冲流能够更高效的读写信息.因为缓冲流先将数据缓存起来,然后一起写入或读取出来.所以说,缓冲流还是很重要的,在IO操作时记得加上缓冲流提升性能. 缓冲流分为字节和字符缓冲流 字节缓冲流为: BufferedInputStream-字节输入缓冲流 BufferedOutputStream-字节输出缓冲流 字符缓冲流为: BufferedReader-字符