C++日志系统选型与持久化配置:glog核心机制与生产环境避坑指南
1. 日志系统选型的底层逻辑1.1 为什么轮子满天飞还要用 glog做 C 后端或者基础架构的同学迟早会撞上日志这件事。刚入行那会儿我习惯用std::cout加个时间戳凑合项目小的时候确实够用一旦服务跑起来、多线程一并发控制台输出瞬间乱成一锅粥排查问题时翻日志比翻代码还累。后来试过自己封装一个简单的日志类写着写着就发现坑越来越多多线程写文件会不会交错、日志文件怎么按天切、程序崩了最后几行日志丢没丢、日志级别怎么在运行时动态调……每一个问题单拎出来都不难凑在一起就是一套小型基础设施。glog 就是在这个背景下进入视野的。它是 Google 开源的一套 C 日志库全称 Google Logging Library设计目标很明确简单、可靠、高性能并且开箱即用。它不像某些日志框架那样需要你写一堆配置文件、注册一堆 appender引入头文件、链接库、调一行google::InitGoogleLogging()就能跑起来。对于追求工程效率的团队来说这种约定优于配置的思路非常讨喜。我选 glog 的核心原因有三点。第一是日志分级天然清晰INFO、WARNING、ERROR、FATAL 四个级别覆盖了绝大多数场景而且 FATAL 级别会直接终止程序并打印堆栈这在定位崩溃问题时极其有用。第二是文件管理自动化glog 会自动按级别分文件、按大小或时间滚动、自动清理旧日志省去了大量手写文件管理的代码。第三是线程安全内部用了锁和原子操作保证多线程写入不会串行错乱这一点在并发服务里是刚需。当然glog 也不是没有短板。它的默认输出格式比较固定想自定义格式得费点劲它默认把日志写到本地磁盘分布式场景下需要额外做收集它的文档相对简略很多行为得靠读源码或者踩坑才能摸清楚。但瑕不掩瑜对于单机服务、嵌入式组件、基础库这类场景glog 依然是性价比极高的选择。1.2 持久化记录到底要解决什么问题标题里持久化记录这四个字其实点出了日志系统最核心的价值。很多人对日志的理解停留在打印出来看看但真正在生产环境跑过服务的人都知道日志的第一使命是事后可追溯。程序崩了、请求超时了、数据对不上了你唯一能依赖的现场证据就是日志文件。如果日志只打在控制台、进程一退就没了那等于没有。持久化要解决的具体问题可以拆成几层。最基础的是落盘可靠性日志写进文件后要保证即使进程异常退出已经写入的内容也不会丢这涉及到缓冲区刷新策略。往上一层是文件生命周期管理日志不能无限增长把磁盘撑爆需要按大小或时间切分并保留合理的份数。再往上是可检索性日志文件命名要能一眼看出日期和级别内容格式要便于 grep 和解析。最后是性能与可靠性的平衡同步写盘最安全但最慢异步写盘快但可能丢日志怎么取舍取决于业务对日志的容忍度。glog 在这几层上都有对应的机制但默认行为未必符合你的预期。比如它默认是缓冲写入的程序正常退出会 flush但被kill -9就可能丢最后一段。比如它的日志文件默认放在/tmp下很多生产环境这个目录会被定期清理。这些细节如果不在配置阶段处理好等到出事的时候才发现日志没了那真是欲哭无泪。所以这篇内容我会把重点放在怎么配和怎么持久化上把那些文档里一笔带过、实际却至关重要的点掰开揉碎讲清楚。2. glog 核心机制与配置项拆解2.1 日志级别与文件命名规则glog 的日志级别设计得很克制只有四级从低到高分别是INFO、WARNING、ERROR、FATAL。这里有个容易踩的坑glog 默认只把ERROR及以上级别的日志输出到 stderrINFO和WARNING默认是静默的很多新手第一次用会发现自己的LOG(INFO)怎么没打印其实是被默认配置过滤了。要看到 INFO 日志得通过环境变量或者SetStderrLogging来调整。文件命名规则是 glog 的一大特色格式为program.hostname.user.log.severity.date-time.pid举个例子一个叫myserver的程序在主机node01上以root运行产生的 INFO 日志文件名可能长这样myserver.node01.root.log.INFO.20240515-103045.12345这个命名看起来有点长但信息密度很高。program让你知道是哪个服务hostname在多机部署时能区分来源severity直接标明级别date-time和pid则精确到某一次进程运行。排查问题时你甚至不需要打开文件光看文件名就能定位到大概范围。我个人的习惯是保留这个默认命名因为它的可检索性确实强用ls配合通配符就能快速筛选。不过有一点要注意glog 还会创建一个软链接比如myserver.INFO指向当前正在写的那个 INFO 文件。这个软链接在实时tail -f的时候特别方便你不用去猜当前文件名是什么直接tail -f myserver.INFO就行。但这个软链接在程序重启后会重新指向新文件所以脚本里依赖它的时候要留意。2.2 日志目录与滚动策略配置glog 的日志目录通过FLAGS_log_dir指定默认是/tmp。这个默认值在生产环境基本不能用原因有两个一是/tmp在很多系统上会被 tmpfiles 机制定期清理二是/tmp通常挂载在内存或较小的分区上日志量一大就出问题。所以第一件事就是把它改到一个专门的、容量充足的磁盘路径。FLAGS_log_dir /data/logs/myserver;设置完目录后glog 不会自动创建这个目录如果目录不存在日志会静默失败——这是个大坑。所以初始化之前一定要确保目录存在或者自己写代码mkdir -p一下。滚动策略方面glog 提供了两个维度的控制。第一个是按文件大小滚动通过FLAGS_max_log_size设置单位是 MB默认值是 1800MB。这个默认值偏大实际生产中我一般设成 100MB 到 500MB 之间太大了单个文件打开慢、grep 也慢。第二个是按时间滚动通过FLAGS_log_split相关机制配合但 glog 原生对按天滚动的支持不算特别直接常见做法是结合外部脚本或者用FLAGS_logbufsecs配合定时任务。还有一个关键参数是FLAGS_stop_logging_if_full_disk默认是 false。这意味着磁盘写满时 glog 会继续尝试写可能拖慢整个服务。生产环境建议设为 true磁盘满了就停止写日志虽然会丢日志但至少不会把主业务拖垮。配置项默认值生产建议值说明FLAGS_log_dir/tmp/data/logs/服务名独立磁盘避免被清理FLAGS_max_log_size1800100~500单位 MB控制单文件大小FLAGS_stop_logging_if_full_diskfalsetrue磁盘满时停止写日志FLAGS_logbufsecs300~5缓冲刷新间隔越小越安全FLAGS_minloglevel000 表示 INFO 也记录2.3 缓冲刷新与崩溃安全FLAGS_logbufsecs这个参数值得单独拎出来讲因为它直接关系到崩溃时日志丢多少。glog 默认是缓冲写入的缓冲区里的内容每隔logbufsecs秒默认 30 秒才真正 flush 到磁盘。这意味着如果程序在第 29 秒崩溃这 29 秒内的日志可能全丢了。对于排查崩溃问题来说这几乎是致命的。所以我的建议是对可靠性要求高的服务把FLAGS_logbufsecs设为 0也就是每条日志都立即 flush。代价是性能下降因为每次写都要触发系统调用。实测下来在日志量不是特别夸张的场景每秒几千条以内设成 0 对整体性能的影响可以接受。如果日志量确实很大设 0 扛不住那可以折中设成 1 到 5 秒。同时配合FLAGS_logtostderr或者FLAGS_alsologtostderr把关键日志同时输出到 stderr由外部的进程管理器比如 systemd 的 journal来兜底收集。这样即使文件缓冲丢了stderr 那边还有一份。另外glog 在遇到LOG(FATAL)时会自动 flush 并终止程序这个行为是可靠的。所以对于程序即将崩溃这类场景用 FATAL 级别能保证日志不丢。但普通的 ERROR 不会触发 flush这点要心里有数。3. 从零搭建一套可持久化的日志配置3.1 初始化流程与代码骨架下面这套初始化代码是我在多个项目里反复打磨过的直接可以拿去用。核心思路是先建目录、再设参数、最后初始化顺序不能乱。#include glog/logging.h #include sys/stat.h #include sys/types.h #include string // 递归创建目录的辅助函数 bool CreateDirRecursive(const std::string path) { if (path.empty()) return false; std::string cmd mkdir -p path; return system(cmd.c_str()) 0; } void InitLogging(const std::string program_name, const std::string log_dir) { // 第一步确保日志目录存在 if (!CreateDirRecursive(log_dir)) { fprintf(stderr, Failed to create log dir: %s\n, log_dir.c_str()); return; } // 第二步设置各项参数 FLAGS_log_dir log_dir; FLAGS_max_log_size 200; // 单文件最大 200MB FLAGS_stop_logging_if_full_disk true; // 磁盘满停止写 FLAGS_logbufsecs 0; // 立即刷新保证不丢 FLAGS_minloglevel 0; // 记录 INFO 及以上 FLAGS_alsologtostderr false; // 不重复输出到 stderr FLAGS_colorlogtostderr false; // 生产环境关掉颜色 // 第三步初始化 google::InitGoogleLogging(program_name.c_str()); // 第四步设置日志前缀格式可选 google::InstallFailureSignalHandler(); LOG(INFO) Logging initialized, dir log_dir; }这段代码里有几个细节值得说明。CreateDirRecursive我用了system(mkdir -p)这种偷懒写法实际项目中建议用std::filesystem::create_directoriesC17或者自己写递归mkdir避免system调用的开销和潜在的命令注入风险。InstallFailureSignalHandler是 glog 提供的一个很实用的功能它能在程序收到SIGSEGV、SIGABRT等信号时打印出调用栈对定位崩溃原因帮助巨大强烈建议开启。3.2 参数设置背后的取舍计算FLAGS_max_log_size 200这个值不是拍脑袋定的。假设你的服务平均每秒产生 500 条日志每条平均 200 字节那么每秒写入约 100KB一天就是约 8.6GB。如果单文件设 200MB一天会产生约 43 个文件。这个数量级用ls和grep都还能接受。如果设成默认的 1800MB一天只有 5 个文件单个文件太大打开和检索都慢如果设成 10MB一天 860 个文件目录里文件太多也会影响文件系统性能。FLAGS_logbufsecs 0的性能代价可以这样估算每条日志一次write系统调用假设系统调用开销约 1 微秒每秒 500 条就是 0.5 毫秒相对于业务逻辑的耗时几乎可以忽略。但如果你的日志量是每秒 10 万条那就是 100 毫秒这就不能忽视了。所以这个参数要根据实际日志量来定没有万能值。关于日志保留份数glog 本身没有直接的保留 N 天参数它只控制单文件大小。清理旧日志需要靠外部机制常见做法是写一个定时脚本每天凌晨删除 7 天前的日志文件#!/bin/bash LOG_DIR/data/logs/myserver find $LOG_DIR -name *.log.* -mtime 7 -delete这个脚本配合 crontab 每天跑一次即可。注意-mtime 7表示修改时间在 7 天前删除前最好先echo一下确认路径没写错我见过有人路径写错把整个目录删了的惨案。3.3 多级别日志的分流与合并glog 默认会把不同级别的日志写到不同文件比如 INFO 一个文件、ERROR 一个文件。这个设计的好处是排查错误时可以只看 ERROR 文件不用在大量 INFO 里翻找。但坏处是当你想看某个请求的完整链路时INFO 和 ERROR 分散在不同文件里时间戳对起来很麻烦。我的做法是保留分级文件同时额外做一份合并输出。具体来说通过FLAGS_alsologtostderr把日志同时输出到 stderr然后用进程管理器把 stderr 重定向到一个统一的文件。这样既有分级文件用于快速定位又有合并文件用于链路追踪。FLAGS_alsologtostderr true;然后在启动脚本里./myserver 2 /data/logs/myserver/combined.log注意这里用追加而不是覆盖否则每次重启都会清空。合并文件同样需要配合清理脚本不然它会无限增长。4. 持久化落地的关键细节与避坑4.1 磁盘满、权限、路径的三大经典坑磁盘满是日志系统最常见的故障。glog 在磁盘满时的行为取决于FLAGS_stop_logging_if_full_disk。设为 false 时它会不断重试写入可能拖慢服务设为 true 时它会停止写日志并打印一条警告到 stderr。但无论哪种磁盘满本身都是灾难。所以监控磁盘使用率是必须的建议在 80% 时告警留出处理时间。权限问题也很隐蔽。如果日志目录的属主和运行服务的用户不一致glog 会创建文件失败而且失败信息可能只打在 stderr 上如果你没重定向 stderr就完全看不到。我遇到过服务以nobody用户运行但日志目录是root创建的结果日志一条没写排查了半天才发现是权限。解决办法很简单初始化前用access()检查一下写权限或者干脆在部署脚本里chown好。路径问题主要是相对路径和绝对路径的混淆。FLAGS_log_dir如果设成相对路径实际写入位置取决于进程的工作目录而工作目录在不同启动方式下可能不一样。所以永远用绝对路径这是铁律。4.2 日志格式自定义与解析友好性glog 默认的日志格式是I0515 10:30:45.123456 12345 file.cc:88] message开头一个字母表示级别I/W/E/F然后是月日、时间、线程 ID、文件名行号最后是消息。这个格式对机器解析还算友好但有个问题是没有年份。跨年的时候日志文件里只有月日你得靠文件名里的日期来推断年份。如果日志被归档后文件名丢了那就麻烦了。要自定义格式glog 提供了google::InstallLogMessageHandler或者重写LogSink接口。但说实话glog 的自定义格式能力比较弱如果你对格式有强需求可能得考虑换库或者自己在消息里拼。我的折中方案是在消息内容里带上关键上下文比如请求 ID、用户 ID这样即使格式固定也能通过 grep 消息内容来检索。LOG(INFO) [req_id req_id ] processing request;这样排查时grep req_idabc123就能把同一个请求的所有日志捞出来比依赖格式本身更灵活。4.3 性能压测与参数调优实录光说理论不够我做过一组简单的压测来验证不同参数下的性能差异。测试环境是 4 核 8G 的虚拟机SSD 磁盘单线程循环写 100 万条日志每条约 100 字节。配置耗时吞吐量崩溃丢日志风险logbufsecs30, max_size18002.1s47万条/秒高最多丢30秒logbufsecs5, max_size2002.8s35万条/秒中logbufsecs0, max_size2006.5s15万条/秒低几乎不丢logbufsecs0, 关闭日志0.3s330万条/秒-这组数据很直观地说明了问题立即刷新logbufsecs0的吞吐量只有缓冲模式的约三分之一。但即便是 15 万条/秒对于绝大多数服务来说也远远够用了。所以我的建议是除非你的日志量真的到了每秒几十万条否则优先选logbufsecs0用性能换可靠性这笔买卖划算。如果确实日志量巨大那可以考虑分级处理INFO 级别用缓冲ERROR 和 FATAL 用立即刷新。glog 本身不支持这种细粒度控制但可以通过自定义 LogSink 实现或者干脆把高频 INFO 日志改成采样输出。5. 常见问题排查与实战经验5.1 日志不输出、输出不全的排查路径遇到日志不输出按这个顺序排查基本能覆盖 90% 的情况。第一检查FLAGS_minloglevel如果它大于你要输出的级别日志会被过滤掉。第二检查日志目录是否存在且可写用ls -ld看权限用touch测试写权限。第三检查是否调用了InitGoogleLogging没调用的话所有 FLAGS 都不生效。第四检查 stderr 是否被重定向到了/dev/null有些启动脚本会这么干导致 glog 的警告信息看不到。日志输出不全通常是缓冲没刷新。程序正常退出时 glog 会 flush但如果是kill -9或者崩溃缓冲区内容就丢了。解决办法就是前面说的把logbufsecs设小或者在关键代码路径手动调用google::FlushLogFiles(google::GLOG_INFO)。还有一个隐蔽的问题多进程写同一个日志目录。如果两个进程用同样的 program name 和同样的日志目录它们的日志文件会混在一起因为文件名里的 pid 不同所以文件是分开的但软链接会互相覆盖。这种情况要么给每个进程不同的 program name要么用不同的日志目录。5.2 日志文件异常增长的定位与治理日志文件异常增长一般有三个原因。一是日志级别设太低把 DEBUG 级别的日志也写进去了生产环境应该只保留 INFO 及以上。二是循环里打日志比如在一个每秒执行几千次的循环里LOG(INFO)日志量瞬间爆炸。三是异常被反复捕获打印比如网络请求失败后重试每次重试都打一条 ERROR如果对端一直不可用日志就会刷屏。治理手段上除了调整级别还可以引入日志采样。比如对于高频的同类日志每 100 条只打 1 条static std::atomicint counter{0}; if (counter % 100 0) { LOG(INFO) sampled log, count counter; }另外给日志加上速率限制也是个好办法比如每秒最多打 N 条超过就丢弃并记录丢弃数量。这些手段能有效防止日志把磁盘写爆。5.3 跨平台与编译链接的注意事项glog 在 Linux 上通常用包管理器装apt install libgoogle-glog-dev或者yum install glog-devel。但要注意版本差异老版本可能没有某些 FLAGS。编译时链接-lglog -lgflags如果用的是静态库还要加-lpthread。在 Windows 上用 glog 相对麻烦需要自己编译而且某些 POSIX 相关的功能比如信号处理支持不完整。如果项目要跨平台建议把日志初始化封装成一层抽象不同平台用不同实现避免直接依赖 glog 的特定行为。还有一个编译期的坑glog 的 FLAGS 是全局变量多个翻译单元包含头文件时可能重复定义。解决办法是在一个.cc文件里定义GLOG_NO_ABBREVIATED_SEVERITIES之类的宏或者用gflags的DECLARE_和DEFINE_机制。这个坑在大型项目里很常见链接时报 multiple definition 基本就是它。5.4 常见问题速查表现象可能原因排查方法解决完全没有日志文件目录不存在/无权限/未初始化检查目录、权限、Init 调用建目录、改权限、补初始化INFO 日志看不到minloglevel 过滤打印 FLAGS_minloglevel设为 0崩溃后日志缺失缓冲未刷新检查 logbufsecs设为 0 或手动 flush日志文件过大max_log_size 太大查看文件大小调小到 100~500磁盘被写满无清理机制查看磁盘使用率加清理脚本、设 stop_logging多进程日志混乱program name 相同查看文件名区分 program name 或目录6. 日志持久化的进阶玩法6.1 日志轮转与归档的自动化glog 自身的滚动是按大小来的但生产环境往往还需要按天归档。我的做法是写一个每日执行的脚本把前一天的日志压缩归档然后删除原始文件。这样既节省空间又保留了历史记录。#!/bin/bash LOG_DIR/data/logs/myserver ARCHIVE_DIR/data/logs/archive YESTERDAY$(date -d yesterday %Y%m%d) mkdir -p $ARCHIVE_DIR # 找出昨天的日志文件并压缩 find $LOG_DIR -name *.log.* -newermt $YESTERDAY ! -newermt $(date %Y%m%d) \ -exec gzip {} \; # 移动压缩包到归档目录 find $LOG_DIR -name *.gz -exec mv {} $ARCHIVE_DIR \; # 删除 30 天前的归档 find $ARCHIVE_DIR -name *.gz -mtime 30 -delete这个脚本的关键在于用-newermt精确圈定时间范围避免误伤正在写的文件。归档目录最好和日志目录在同一块盘上避免跨盘移动的开销。6.2 结合外部系统做集中收集单机日志再多也架不住服务一多就分散在各台机器上。这时候就需要集中收集。常见的做法是在每台机器上跑一个采集 agent把日志文件的内容实时推送到中心存储。glog 写的是普通文本文件所以任何支持文件采集的工具都能对接。采集的时候要注意两点。一是采集位置要跟着软链接走因为 glog 会滚动文件如果采集工具盯着一个固定文件名滚动后就采不到了。二是要处理多行日志glog 的堆栈信息是多行的采集时要配置好多行合并规则否则堆栈会被拆成一条条独立的记录看起来很难受。6.3 日志内容的脱敏与合规日志里很容易不小心写入敏感信息比如用户手机号、身份证号、密码。这些一旦落盘就是合规风险。我的做法是在日志输出前做一层过滤对已知的敏感字段做掩码处理。std::string MaskPhone(const std::string phone) { if (phone.size() ! 11) return phone; return phone.substr(0, 3) **** phone.substr(7); }然后在打日志的时候调用这个函数。更彻底的做法是封装一个日志宏所有日志都走这个宏在宏里统一做脱敏。这样即使开发同学忘了处理也不会漏出去。当然脱敏规则要定期 review因为业务字段会变。7. 我踩过的那些坑与最终建议7.1 三个让我印象深刻的故障第一个故障是日志把磁盘写满导致服务不可用。当时一个服务的日志级别设成了 DEBUG上线后日志量是预估的十倍半夜磁盘满了服务开始报错。事后复盘根本原因是没设stop_logging_if_full_disk也没做磁盘监控。现在我的标准配置里这两个是必选项。第二个故障是崩溃时日志全丢。一个服务偶发崩溃但日志文件里最后一条记录距离崩溃时间有 20 多秒的空白完全看不到崩溃前的现场。原因就是logbufsecs默认 30 秒缓冲区里的日志没来得及落盘。改成 0 之后崩溃前的日志就完整了很快就定位到了问题。第三个故障是多进程日志互相覆盖。一个服务用了多进程模型所有进程用同样的 program name结果日志软链接指向混乱tail -f看到的文件一直在变。后来给每个进程加了不同的后缀问题解决。7.2 一套可以直接抄的配置模板综合以上经验我整理了一套生产环境可直接用的配置放在这里供参考void SetupLogging(const std::string service_name) { std::string log_dir /data/logs/ service_name; system((mkdir -p log_dir).c_str()); FLAGS_log_dir log_dir; FLAGS_max_log_size 200; FLAGS_stop_logging_if_full_disk true; FLAGS_logbufsecs 0; FLAGS_minloglevel 0; FLAGS_alsologtostderr false; FLAGS_colorlogtostderr false; FLAGS_logtostderr false; google::InitGoogleLogging(service_name.c_str()); google::InstallFailureSignalHandler(); LOG(INFO) service_name logging started; }配套的清理脚本和监控告警前面都提过了这里不再重复。核心思想就一句话日志是给未来的自己看的配置的时候多花十分钟排查的时候少熬一晚上。7.3 关于日志的一点个人体会做了这么多年我越来越觉得日志系统的价值不在于技术多复杂而在于稳定和可预期。一个配置得当的日志系统应该让你在出问题时能快速找到答案而不是让你在找日志的过程中产生新的问题。glog 不是最强大的日志库但它的简单和可靠恰恰是很多场景下最需要的品质。如果你正在选型我的建议是先想清楚你的日志要解决什么问题是排查崩溃、是审计追踪、还是性能分析不同目标对应的配置策略完全不同。想清楚了再动手比上来就抄一份配置要靠谱得多。日志这东西平时不起眼关键时刻能救命值得你认真对待。