
日志系统在游戏项目里往往是最不起眼、但一出问题就要命的基础设施。王者荣耀这种量级的实时对战游戏单局产生的日志量非常可观如果日志组件本身开销大、写盘慢、占用内存高轻则拖累帧率重则在对局关键时刻引发卡顿。BqLog 作为王者荣耀内部使用的日志组件被反复提到的一个特点就是快而它快的核心手段之一就是高性能实时压缩日志。这篇就围绕这个点把 BqLog 在实时压缩这条路上做的取舍、原理和实操细节拆开讲清楚适合做客户端性能优化、基础组件开发或者单纯对高性能日志实现感兴趣的同学参考。1. 为什么日志组件必须把实时压缩当成一等公民1.1 移动端日志的真实开销到底在哪很多人对日志开销的第一反应是写文件慢但实际压测下来写文件往往不是最大头。真正吃性能的是三块字符串格式化、内存分配、以及 I/O 等待。格式化阶段要把各种类型转成文本涉及大量临时字符串内存分配阶段如果每条日志都 new 一块 bufferGC 压力会非常明显I/O 阶段如果直接同步写盘主线程被阻塞的风险极高。在王者荣耀这种帧率敏感的场景里一帧只有 16.6ms60帧甚至 8.3ms120帧的预算。如果日志组件在某一帧里集中写入几十 KB 的文本光是格式化加写盘就可能吃掉好几毫秒。所以日志组件的设计目标从来不是能写就行而是在几乎不影响主流程的前提下把该记的都记下来。压缩在这里的价值就体现出来了它把写盘的数据量直接降下来。文本日志的压缩比通常很可观重复的字段名、时间戳格式、模块前缀这些在压缩算法眼里都是高度冗余的内容。数据量降下来I/O 时间、磁盘占用、后续上传带宽全都跟着降。这就是为什么 BqLog 把实时压缩放在这么核心的位置。1.2 实时两个字才是真正的难点压缩本身不难zlib、zstd、lz4 这些库都很成熟。难的是实时——日志是边产生边写的你不能等攒够一大块再压缩那样内存占用高、崩溃时还会丢数据。实时压缩意味着日志产生后很快就要进入压缩流程压缩过程不能长时间持有大 buffer也不能阻塞产生日志的线程。这就带来一系列约束。压缩算法不能太重否则 CPU 占用飙升压缩块不能太大否则延迟高压缩不能在主线程做否则一样卡帧。BqLog 的实时压缩本质上是在压缩比、CPU 开销、内存占用、延迟这四个维度上找平衡点而不是单纯追求压缩比最高。提示评估一个日志组件的压缩方案不要只看压缩比。要看它在目标设备上的 CPU 占用、单条日志的端到端延迟、以及峰值内存。压缩比高但 CPU 打满的方案在移动端是灾难。1.3 不压缩的日志在线上会付出什么代价举个量化的例子。假设某局游戏产生了 2MB 的原始文本日志不压缩直接落盘磁盘写入 2MB。如果开启压缩文本日志压缩到 20% 到 30% 是很常见的也就是 400KB 到 600KB。写入量直接降到四分之一左右。对于每天海量对局的线上环境这个差距在存储成本和上传流量上是数量级的。更关键的是崩溃场景。玩家反馈卡顿或闪退时日志是排查的第一手资料。如果日志因为写盘太慢被丢弃或者因为占用内存太大被系统杀掉那排查就无从谈起。实时压缩让完整记录和低开销这两个原本矛盾的目标变得可以同时成立。2. BqLog 实时压缩的底层思路拆解2.1 分块压缩把连续日志流切成可独立处理的单元BqLog 的实时压缩不是把整个日志文件当成一个流去压而是把日志流切成一个个块block每块独立压缩。这样做有几个直接好处第一压缩可以在后台线程按块处理主线程只管往队列里塞第二每块压缩完就能立刻落盘不需要等整个文件结束第三崩溃时已经压缩落盘的块是完整的最多丢最后一块。块的大小选择是个关键参数。块太小压缩算法发挥不出效果因为每个块都要重新建立压缩上下文头部开销占比高块太大内存占用高延迟也大。实践中常见的做法是把块大小控制在几十 KB 到几百 KB 之间。这个区间既能保证压缩算法有足够的冗余可利用又不会让单块处理时间过长。这里有个容易被忽略的点块边界怎么切。如果按固定字节数切可能把一条日志从中间截断解压后拼起来虽然能还原但可读性差。更稳妥的做法是按日志条目的边界切攒够一定条数或一定字节数就封一个块。BqLog 这类组件通常会维护一个写入缓冲区达到阈值就触发一次块提交。2.2 压缩算法的选型逻辑为什么不是无脑上 zstd压缩算法选型要看场景。zstd 压缩比好、速度也不错但它的高压缩级别在移动端 CPU 上依然偏重lz4 速度极快压缩比一般zlib 是老牌方案压缩比中等、速度中等。BqLog 面向的是实时场景所以更偏向速度优先、压缩比够用的算法。实际选型时通常会把压缩级别调低。比如 zstd 用最低级别或者 lz4 直接用默认级别。低级别下压缩比可能从 25% 变成 35%但 CPU 开销能降一大截。对于日志这种能还原就行、不需要极致压缩的数据这个取舍非常划算。还有一个常被忽视的维度是解压速度。日志压缩后是要给人看的排查问题时需要解压。如果压缩算法解压很慢排查效率就低。lz4 在这点上优势明显解压速度极快适合压缩时省 CPU、解压时也要快的双向需求。算法压缩速度解压速度压缩比适用场景lz4极快极快一般实时日志、低延迟优先zstd低级别快快较好通用日志、平衡型zstd高级别慢快很好归档日志、离线压缩zlib中等中等中等兼容性优先2.3 压缩与写入的流水线设计BqLog 快的另一个原因是把压缩和写入做成了流水线。日志产生线程只负责把格式化好的数据放进一个无锁或低锁的队列然后立刻返回。后台有专门的压缩线程从队列取数据、组块、压缩压缩完再交给写入线程落盘。三个阶段各司其职互不阻塞。这种流水线设计的关键在于队列的实现。如果用普通的互斥锁队列高并发写日志时锁竞争会很严重。所以高性能日志组件通常会用无锁队列或者每线程独立缓冲thread-local buffer的方式减少竞争。每线程独立缓冲的好处是写入几乎无锁缺点是内存占用会随线程数增长需要控制线程数量或缓冲大小。流水线还有一个好处是能利用多核。移动端虽然核心数不如桌面但现代手机普遍有 6 到 8 个核把压缩放到单独的核心上对主线程的影响就很小。当然这也要看系统的调度策略不能假设后台线程一定能拿到独立核心。3. 把实时压缩落地时要盯住的几个硬指标3.1 单条日志的端到端延迟怎么测延迟是实时压缩最核心的指标。测法很简单在日志产生处打一个时间戳在数据真正落盘后打一个时间戳两者之差就是端到端延迟。但要注意这个延迟在流水线架构下是异步的日志产生线程感知到的延迟入队延迟和实际落盘延迟是两回事。对游戏来说真正影响帧率的是日志产生线程被阻塞的时间也就是入队延迟。这个值应该控制在微秒级。而落盘延迟可以放宽到毫秒甚至几十毫秒只要不无限堆积就行。所以测延迟要分开测入队延迟看主线程影响落盘延迟看数据完整性风险。实测中常见的坑是队列满了之后日志产生线程被迫等待入队延迟突然飙升。所以队列容量和丢弃策略要设计好。BqLog 这类组件通常会设置队列上限满了之后要么丢弃低级别日志要么阻塞等待具体策略要看业务对日志完整性的要求。3.2 压缩线程的 CPU 占用如何压住压缩线程再独立也是要抢 CPU 的。如果压缩线程优先级设得太高会跟渲染线程抢资源设得太低又可能压缩跟不上产生速度导致队列堆积。合理的做法是给压缩线程设一个中等偏低的优先级并且在队列积压时动态调整压缩级别——积压多就用更快的低级别压缩积压少就用稍高的级别。另一个手段是限制压缩线程的处理节奏。比如每处理完一个块就主动让出一次 CPU避免长时间占用。这在移动端尤其重要因为移动端的调度器对长时间占用 CPU 的线程不太友好。注意不要在主线程做任何压缩相关的同步等待。哪怕只是等一个压缩块完成都可能造成掉帧。所有压缩必须异步化。3.3 内存峰值与崩溃安全实时压缩会引入额外的内存占用写入缓冲、压缩输入缓冲、压缩输出缓冲、队列里的待处理数据。这些加起来可能比不压缩时高不少。移动端内存紧张必须控制峰值。控制手段包括限制队列长度、复用缓冲避免频繁分配释放、压缩完立刻释放输入缓冲。复用缓冲这点很关键如果每个块都 new 一块内存GC 压力会很大。用对象池或者固定大小的环形缓冲能显著降低分配次数。崩溃安全方面因为日志是分块压缩落盘的崩溃时最多丢最后一个未完成的块。为了进一步降低丢失风险可以缩短块提交的阈值让块更频繁地落盘。但块太小又影响压缩比所以还是要平衡。有些实现会在崩溃信号处理里做一次强制 flush把当前缓冲刷出去但这要求 flush 过程本身足够快且信号安全。4. 实操中容易踩的坑与排查思路4.1 压缩后日志不可读排查反而变慢这是最常见的抱怨。日志压缩了出问题时得先解压才能看如果解压工具不顺手排查效率反而下降。解决办法是提供配套的解压/查看工具最好能支持流式解压边解压边看不用等整个文件解完。另一个做法是分级压缩低级别日志如 verbose压缩存储高级别日志如 error不压缩或轻压缩保证关键日志随时可读。BqLog 这类组件通常会支持按日志级别配置不同的处理策略。4.2 压缩线程把主线程的 CPU 抢了前面提过优先级问题但实际排查时往往不容易定位。表现是开启日志压缩后帧率下降但 profiling 又看不出明显的热点。这时候要看线程调度情况确认压缩线程是不是在某些时段占用了大量 CPU。排查方法用系统的性能分析工具看各线程的 CPU 占用时间线对比开启和关闭压缩两种情况。如果发现压缩线程在帧渲染期间活跃就要调整它的调度策略比如让它更倾向于在空闲时段工作。4.3 队列堆积导致日志延迟越来越大队列堆积的典型表现是日志文件里时间戳和实际发生时间对不上越到后面差得越多。原因是产生速度长期大于压缩写入速度。这时候要么提高压缩/写入吞吐要么降低日志产生量比如提高日志级别门槛。排查时先确认是压缩慢还是写入慢。可以分别统计压缩线程的处理速率和写入线程的落盘速率。如果是压缩慢考虑降压缩级别或换更快的算法如果是写入慢考虑批量写入或换更高效的 I/O 方式。4.4 不同机型表现差异巨大移动端机型差异是绕不开的。低端机 CPU 弱、内存小压缩方案要更保守高端机可以适当激进。BqLog 这类组件通常会提供配置项让业务根据机型档位调整压缩级别、块大小、队列长度等参数。实操建议是准备几档预设配置按机型性能分级下发。不要指望一套参数打天下低端机上跑高端机参数很可能直接卡死。5. 从 BqLog 的设计里能抄走哪些通用经验5.1 异步化是一切高性能日志的前提不管压不压缩日志写入必须异步。同步写日志在主线程上是性能杀手。异步化的核心是队列加后台线程队列要尽量无锁后台线程要能批量处理。这个模式适用于几乎所有高性能日志场景不只是 BqLog。5.2 压缩是手段不是目的别为了压缩比牺牲实时性很多人一上来就想用最高压缩比结果 CPU 打满。日志压缩的目标是降低 I/O 和存储开销同时不影响主流程压缩比只是其中一个维度。低级别压缩往往才是实时场景的正解。5.3 可观测性要覆盖日志组件自身日志组件自己也需要被监控队列长度、压缩速率、落盘速率、丢弃条数、端到端延迟。这些指标能帮你在出问题前就发现异常。很多团队只顾着用日志排查业务问题却忘了日志组件本身也需要排查。5.4 配置化让不同场景各取所需不同业务、不同机型、不同调试阶段对日志的需求不一样。把压缩级别、块大小、队列长度、日志级别门槛都做成可配置的能让组件适应更多场景。硬编码参数是维护噩梦。我在实际做客户端性能优化时的一个体会是日志组件的优化收益往往被低估。大家更关注渲染、网络、内存这些显性指标但日志这种基础设施一旦设计不好会在各种意想不到的地方拖后腿。BqLog 把实时压缩做扎实本质上是在为整个游戏的稳定性兜底——平时感觉不到它的存在出问题时它能把现场完整保留下来这才是日志组件最大的价值。