Logtail优化揭秘:提高多行日志采集性能7倍的技术解析

Logtail 通过一行代码优化多行日志采集,性能提升 7 倍!

原文标题:一行代码改进:Logtail的多行日志采集性能提升7倍的奥秘

原文作者:阿里云开发者

冷月清谈:

本文分析了Logtail在处理多行日志时,由于使用`boost::regex_match`进行全量匹配导致性能下降的问题。`boost::regex_match`会对整行日志进行匹配,即使行首正则表达式只匹配一小部分。文章指出,当日志很长,且大部分内容与行首正则表达式无关时,这种全量匹配会造成 significant 的性能损耗。

为解决这个问题,文章建议使用`boost::regex_search`函数并设置`boost::match_continuous`标志,仅匹配行首部分。此外,Logtail可自动移除用户行首正则表达式中的`.*`后缀,并在正则前添加`^`以提升匹配效率。

通过对比测试Logtail 1.8.7版本和优化后的2.1.1版本,结果显示采集速度从90MB/s提升至633MB/s,性能提升了7倍。

怜星夜思:

1、除了文中提到的优化方法,还有哪些方法可以提升 Logtail 的多行日志采集性能?
2、文章提到日志行首很长时优化效果明显,那么在实际应用中,哪些场景下的日志行首通常比较长?
3、如果我的日志格式不固定,或者行首标识符经常变化,该如何有效地使用 Logtail 进行多行日志采集?

原文内容

阿里妹导读


一个有趣的现象引起了作者的注意:当启用行首正则表达式处理多行日志时,采集性能出现下降。究竟是什么因素导致了这种现象?本文将探索Logtail多行日志采集性能提升的秘密。

背景

在日志分析领域,Logtail作为一款广泛使用的日志采集工具,其性能的任何提升都能显著提升整体效率。最近,在对Logtail进行性能测试时,一个有趣的现象引起了我的注意:当启用行首正则表达式处理多行日志时,采集性能出现下降。究竟是什么因素导致了这种现象?接下来,让我们一起探索Logtail多行日志采集性能提升的秘密。

分析

要理解这一现象,首先需了解Logtail在处理多行日志时的工作原理。Logtail的多行日志合并功能基于特定的日志格式将分散的多行数据聚合为完整事件。其工作流程如下:

1.用户配置行首正则表达式。
2.Logtail对每行日志开头应用此正则。

3.若某行不匹配,Logtail继续等待直至找到匹配的行首。





举个例子,假设我们有如下的日志格式,通常我们会配置行首正则为 cnt.*,Logtail会拿着这个正则对每行进行匹配,将这些单行日志合并成一个完整的多行日志。

cnt:13472391,thread:2,log:Exception in thread "main" java.lang.NullPointerExceptionat  com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitle
   at com.example.myproject.Book.getTitle
   at com.example.myproject.Book.getTitle
   at com.example.myproject.Book.getTitle

从Logtail的实现机制来看,Logtail使用了boost::regex_match 函数进行的正则匹配。这个正则函数根据输入正则reg,会对输入的日志buffer进行全量匹配。例如上面这个日志,cnt.*会全量匹配第一行的1072个字符。

bool BoostRegexMatch(const char* buffer, size_t size, const boost::regex& reg, string& exception) {
   // ...
   if (boost::regex_match(buffer, buffer + size, reg))
       return true;
   // ...
}





我编写了以下测试代码发现,随着与行首正则无关的日志长度变长(.*匹配的那部分日志),boost::regex_match 也线性地增长了。

static void BM_Regex_Match(int batchSize) {
   std::string buffer = "cnt:";
   std::string regStr = "cnt.*";
   boost::regex reg(regStr);
   std::ofstream outFile("BM_Regex_Match.txt", std::ios::trunc);
   outFile.close();

   for (int i = 0; i < 1000; i++) {
       std::ofstream outFile(“BM_Regex_Match.txt”, std::ios::app);
       buffer += “a”;

       int count = 0;
       uint64_t durationTime = 0;
       for (int i = 0; i < batchSize; i++) {
           count++;
           uint64_t startTime = GetCurrentTimeInMicroSeconds();
           if (!boost::regex_match(buffer, reg)) {
               std::cout << “error” << std::endl;
           }
           durationTime += GetCurrentTimeInMicroSeconds() - startTime;
       }
       outFile << i << ‘\t’ << "durationTime: " << durationTime << std::endl;
       outFile << i << ‘\t’ << "process: " << formatSize(buffer.size() * (uint64_t)count * 1000000 / durationTime)
               << std::endl;
       outFile.close();
   }
}

int main(int argc, char** argv) {
   logtail::Logger::Instance().InitGlobalLoggers();
   std::cout << “BM_Regex_Match” << std::endl;
   BM_Regex_Match(10000);
   return 0;
}

图片

这时候我们就需要注意了,我们使用行首正则时,其实往往只需要匹配单行日志开头的一部分,例如这个日志就是cnt,我们并不需要整个.* 部分,因为匹配这部分会消耗不必要的性能。特别是当日志非常长时,这种影响尤为明显。

其实boost库提供了boost::regex_search函数
只需设置合适的标志(如boost::match_continuous

就能实现仅匹配前缀的需求,而这正是行首正则匹配所需求的。我们来看一下如何使用 boost::regex_search

bool BoostRegexSearch(const char* buffer, size_t size, const boost::regex& reg, string& exception) {
   // ...
   if (boost::regex_search(buffer, buffer + size, what, reg, boost::match_continuous)) {
       return true;
   }
   // ...
}

在 Logtail 中,由于现有的行首正则实现方式需要,用户的行首正则都带有.*后缀,我们可以自动移除.*并在正则前添加^,以提升匹配效率。





boost::regex_match 一样,我也对boost::regex_search根据日志长度进行了测试。可以发现,boost::regex_search的耗时基本稳定,没有随着日志变大,耗时变长。

图片

static void BM_Regex_Search(int batchSize) {
   std::string buffer = "cnt:";
   std::string regStr = "^cnt";
   boost::regex reg(regStr);
   std::ofstream outFile("BM_Regex_Search.txt", std::ios::trunc);
   outFile.close();

   for (int i = 0; i < 1000; i++) {
       std::ofstream outFile(“BM_Regex_Search.txt”, std::ios::app);
       buffer += “a”;

       int count = 0;
       uint64_t durationTime = 0;
       for (int i = 0; i < batchSize; i++) {
           count++;
           uint64_t startTime = GetCurrentTimeInMicroSeconds();
           if (!boost::regex_search(buffer, reg)) {
               std::cout << “error” << std::endl;
           }
           durationTime += GetCurrentTimeInMicroSeconds() - startTime;
       }

       outFile << i << ‘\t’ << "durationTime: " << durationTime << std::endl;
       outFile << i << ‘\t’ << "process: " << formatSize(buffer.size() * (uint64_t)count * 1000000 / durationTime)
               << std::endl;
       outFile.close();
   }
}

int main(int argc, char** argv) {
   std::cout << “BM_Regex_Search” << std::endl;
   BM_Regex_Search(10000);
   return 0;
}

性能测试

通过这样调整后,我对改进前后的Logtail性能进行了对比测试,测试结果显示性能有显著提升。测试环境如下:

  • 相同的ACK集群,相同规格的机器(ecs.c7.4xlarge,16 vCPU,32 GiB,计算型 c7);
  • 2048GB ESSD云盘,PL3规格;
  • Logtail 启动参数保持默认;
  • 打印日志的程序一致;
  • 相同的采集配置,只配置行首正则 cnt.*;
  • 实时生成相同的日志,该日志的特点是行首的长度比较长;

cnt:13472391,thread:2,log:Exception in thread "main" java.lang.NullPointerExceptionat  com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitleat com.example.myproject.Book.getTitle
   at com.example.myproject.Book.getTitle
   at com.example.myproject.Book.getTitle
   at com.example.myproject.Book.getTitle
  • 首次采集大小10485760,启动参数修改"max_fix_pos_bytes" : 10485760,防止首次读时,日志已经写入了很多;
  • 同时运行8个pod输出日志到本地文件。

测试发现,优化后的Logtail 2.1.1 版本 日志采集速度达到了每秒633MB/s,而Logtail 1.8.7版本,也就是行首正则优化前的日志每秒处理速率只有90M/s不到,提升了7倍。

指标

原始情况

多行优化后

iLogtail采集速率(MB/s)

90MB/s

633MB/s





总结

我们观察到,当日志内容非常长时,仅通过简单的一行代码修改,就能使Logtail的采集性能显著提升数倍。理论上讲,对于那些与行首正则表达式无关的日志部分越长,这种优化带来的性能提升效果就越为明显。


日志安全审计与合规性评估


日志安全审计与合规性评估方案旨在通过集中化采集、存储、分析来自多个系统、应用和设备的日志数据,确保企业数据和系统安全性与合规性。企业合规团队可基于日志审计来输出合规信息,帮助企业优化安全态势,确保业务连续性和数据安全。   


点击阅读原文查看详情。



关于“如果我的日志格式不固定,或者行首标识符经常变化,该如何有效地使用 Logtail 进行多行日志采集?”,我想到的是,可以结合其他工具,比如Fluentd或Filebeat,对日志进行预处理和格式化,再交给 Logtail 进行采集,这样可以更灵活地处理各种日志格式。

针对“除了文中提到的优化方法,还有哪些方法可以提升 Logtail 的多行日志采集性能?”这个问题,我觉得可以从硬件方面考虑,比如提升磁盘IO性能,使用SSD或者 NVMe SSD,或者增加采集机器的内存和CPU核数。

对于“除了文中提到的优化方法,还有哪些方法可以提升 Logtail 的多行日志采集性能?”这个问题,我想到的是,可以优化网络传输,比如使用更高效的网络协议或压缩算法,减少网络传输时间,也可以提升整体性能。

针对“如果我的日志格式不固定,或者行首标识符经常变化,该如何有效地使用 Logtail 进行多行日志采集?”,可以考虑使用更灵活的配置方式,比如使用 grok 模式或者自定义的解析规则,而不是仅仅依赖行首正则表达式。就像玩游戏,一套固定的出装肯定不行,要根据情况灵活调整。

关于提升 Logtail 多行日志采集性能,除了优化正则匹配,还可以考虑异步处理和批量写入。异步处理可以避免阻塞,批量写入可以减少IO次数,都能有效提高性能。就像快递打包,一个一个发慢,不如凑一批一起发。

“文章提到日志行首很长时优化效果明显,那么在实际应用中,哪些场景下的日志行首通常比较长?”,我认为一些安全审计日志也可能会有很长的行首,因为它们需要记录详细的操作信息,例如用户、IP地址、操作类型等等,来保证安全性。

关于“文章提到日志行首很长时优化效果明显,那么在实际应用中,哪些场景下的日志行首通常比较长?”这个问题,我认为在包含大量上下文信息的日志中,行首通常比较长,例如带有时间戳、日志级别、线程ID、请求ID等信息的日志。

对于“如果我的日志格式不固定,或者行首标识符经常变化,该如何有效地使用 Logtail 进行多行日志采集?”这个问题,我觉得如果日志格式变化很大,可能需要预处理日志,将其转换成固定的格式,然后再使用 Logtail 采集。虽然麻烦点,但为了稳定性,值了!

对于这个问题,我觉得分布式追踪系统生成的日志,行首通常会比较长,因为它需要包含各种追踪信息,以便后续分析和排查问题。你想啊,查问题的时候信息越多越好嘛。