
1. 从一次日志写入延迟抖动说起做移动端性能优化的朋友大概率都遇到过这种场景一局对战打完帧率曲线看着挺漂亮但某个时刻突然冒出一个几十毫秒的卡顿尖刺抓了半天发现是日志组件在写盘。BqLog 这个日志组件在圈子里被讨论得比较多核心卖点就是快尤其是压缩日志这条路径。但快不是凭空来的它背后有一整套针对执行路径的优化思路尤其是压缩日志写入这条链路从数据进来到落盘每一步都在抠时间。这篇文章不打算泛泛地聊“日志组件怎么设计”而是聚焦在压缩日志的执行路径优化上。我会把 BqLog 在压缩日志写入过程中涉及的关键环节拆开包括数据缓冲、压缩触发时机、CRC 校验的取舍、哈希表在去重和索引里的角色以及这些选择背后的权衡逻辑。如果你正在自己写日志组件或者在用 BqLog 但想搞清楚它为什么快、快在哪、什么情况下会变慢这篇内容应该能给你一些可以直接参考的东西。适合的读者包括移动端性能优化工程师、基础架构组件的维护者、对日志系统实现细节感兴趣的后端开发以及任何被日志写入延迟坑过的人。不需要你事先读过 BqLog 的源码但需要对缓冲区、压缩、校验这些基础概念有基本认知。2. 压缩日志执行路径的整体设计思路2.1 为什么压缩日志不能走“先攒后压”的老路很多日志组件的压缩逻辑是这样的日志先以明文形式写进内存缓冲区缓冲区满了或者定时器到期再把整块数据交给压缩算法压缩完再写文件。这条路看起来合理但它有一个致命问题——压缩操作本身是 CPU 密集型的而且压缩的数据量越大单次压缩的耗时越长。在移动端这意味着一个不可控的延迟尖刺。BqLog 在压缩日志这条路径上换了一个思路把压缩操作拆散让压缩尽可能发生在数据还在缓冲区里、还没触发写盘的时候并且把压缩的粒度控制得足够小避免单次压缩占用太长时间。更关键的是它把压缩和写入的流水线做了重叠压缩一块数据的同时上一块数据已经在写盘了。这样 CPU 和 IO 的等待时间被互相掩盖整体吞吐上去了单次延迟也下来了。这个思路说起来简单但落地的时候有一堆细节要处理。比如压缩块的大小怎么定太小了压缩率上不去太大了单次压缩耗时又控制不住。BqLog 的做法是动态调整根据当前缓冲区的积压情况和历史压缩耗时来微调块大小。这个动态调整的逻辑本身也有开销所以它只在特定条件下触发不是每写一条日志都算一遍。2.2 执行路径上的关键节点拆解把压缩日志从“日志产生”到“落盘”的完整路径拆开大概经过这几个节点日志条目格式化把结构化数据转成文本或二进制格式写入环形缓冲区数据进入一个固定大小的缓冲区等待后续处理压缩触发判断判断当前是否满足压缩条件缓冲区水位、时间间隔等压缩执行调用压缩算法对缓冲区中的数据进行压缩CRC 校验计算对压缩后的数据计算校验值写入文件把压缩数据加上校验值写入磁盘每个节点都有优化空间但优化不是孤立的。比如压缩块的大小会影响 CRC 校验的计算量CRC 校验的实现方式又会影响压缩路径的整体耗时。BqLog 在这些节点上的选择背后有一套一致的逻辑减少数据拷贝、减少函数调用开销、把能并行的事情并行掉、把能提前做的事情提前做。2.3 与明文日志路径的差异对比压缩日志路径和明文日志路径在架构上有一个根本差异明文日志可以做到“写一条落一条”延迟可控但吞吐低压缩日志必须攒一批才能压吞吐高但延迟有上限。BqLog 的做法是在两者之间找一个平衡点通过控制压缩块的大小和压缩触发的时机让压缩日志的延迟尽量接近明文日志同时保持压缩带来的空间优势。这个平衡点的选择不是拍脑袋定的。BqLog 在初始化的时候会根据设备类型、磁盘类型、CPU 核心数等参数算出一个初始的压缩块大小然后在运行过程中根据实际表现动态调整。这个动态调整的算法不复杂但需要采集一些运行时指标比如最近几次压缩的耗时、缓冲区的平均积压量、写盘的平均耗时等。这些指标的采集本身也有开销所以采样频率不能太高。3. 压缩路径上那些容易被忽略的性能细节3.1 缓冲区设计环形缓冲区的读写指针优化BqLog 的压缩日志路径用了一个环形缓冲区来暂存待压缩的数据。环形缓冲区的好处是内存复用不需要频繁分配释放。但环形缓冲区有一个经典问题读写指针的更新需要处理回绕回绕判断本身有分支预测的开销。BqLog 在这块做了一个小优化把缓冲区大小设为 2 的幂次这样回绕判断可以用位运算代替取模运算。这个优化看起来不起眼但在高频写入的场景下省下来的 CPU 周期很可观。另一个细节是读写指针的更新顺序。在多线程环境下读写指针的更新需要保证内存可见性。BqLog 用了原子操作加内存屏障的方式但屏障的粒度控制得很细只在必要的地方加避免过度同步带来的性能损失。具体来说写指针的更新用 release 语义读指针的更新用 acquire 语义这样既保证了正确性又不会引入不必要的等待。注意环形缓冲区的大小不是越大越好。缓冲区太大压缩块就大单次压缩耗时会增加缓冲区太小压缩触发频繁压缩率上不去。BqLog 默认的缓冲区大小是 256KB但这个值会根据设备内存和日志写入速率动态调整。3.2 压缩触发时机的判断逻辑压缩触发的时机直接决定了压缩路径的延迟表现。触发太早压缩块太小压缩率低触发太晚缓冲区积压多单次压缩耗时长。BqLog 用了多个条件来综合判断缓冲区水位超过阈值默认 70%距离上次压缩的时间超过阈值默认 500ms缓冲区中有数据且写盘通道空闲这三个条件满足任意一个就会触发压缩。但触发之后不是立刻压缩全部数据而是只压缩水位线以下的部分留出一部分空间给后续写入。这个“留白”的比例也是动态调整的根据写入速率和压缩耗时的比值来算。这个判断逻辑本身的开销很小因为它只在每次写入缓冲区之后检查一次而且检查的条件都是简单的数值比较。但这里有一个坑如果写入速率突然飙升缓冲区水位快速上涨压缩触发会变得频繁压缩块变小压缩率下降整体吞吐反而可能降低。BqLog 针对这种情况做了一个保护机制当检测到写入速率超过某个阈值时会临时增大压缩块的大小牺牲一点延迟来保证吞吐。3.3 CRC 校验在压缩路径中的位置选择CRC 校验在压缩日志路径中的位置很关键。放在压缩前还是压缩后对性能的影响不一样。放在压缩前校验的是原始数据压缩后如果数据损坏无法区分是压缩过程出错还是原始数据出错放在压缩后校验的是压缩数据能覆盖压缩过程但压缩后的数据量小校验的计算量也小。BqLog 选择在压缩后计算 CRC原因有两个一是压缩后的数据量小CRC 计算量相应减少二是压缩后的数据是最终落盘的数据校验它能覆盖从压缩到落盘的完整链路。但这里有一个细节CRC 校验值本身也需要存储如果每条压缩块都带一个 CRC 值存储开销会增加。BqLog 的做法是把多个压缩块的 CRC 值攒在一起批量写入减少 IO 次数。CRC 校验的计算本身也有优化空间。BqLog 用了查表法来计算 CRC但查表法有一个问题表的大小影响缓存命中率。表太大缓存放不下每次查表都可能触发缓存未命中表太小计算步骤多。BqLog 根据目标平台的缓存大小选择了一个折中的表大小并且在初始化时预计算好表内容避免运行时重复计算。提示CRC 校验不能保证检出全部奇数个比特错误这是 CRC 算法的固有特性。如果对数据完整性要求极高可以考虑配合其他校验方式比如哈希校验。但哈希校验的计算量通常比 CRC 大需要根据实际场景权衡。3.4 哈希表在日志去重与索引中的角色哈希表在 BqLog 的压缩日志路径里主要用在两个地方一是日志内容的去重二是压缩块的索引。去重这块BqLog 会对日志条目的关键字段算一个哈希值如果哈希值相同且内容确实相同就只存一份其他位置存引用。这个做法在日志内容重复率高的场景下能显著减少数据量但哈希计算本身有开销所以 BqLog 只对特定类型的日志条目做去重不是所有日志都走这条路。索引这块哈希表用来快速定位某个时间范围的压缩块。压缩块在文件中的偏移量存在哈希表里键是时间戳的范围值是偏移量。这样查询的时候不需要遍历整个文件直接查哈希表就能定位到对应的压缩块。哈希表的冲突处理用的是链地址法冲突率控制在较低水平因为键的分布比较均匀。哈希函数的选择也有讲究。BqLog 用的不是通用的哈希算法而是一个针对时间戳和日志类型优化过的哈希函数计算速度快分布均匀。这个哈希函数的实现细节没有公开但从效果来看冲突率确实比通用哈希算法低。4. 实操复现压缩日志路径的关键步骤4.1 环境准备与基础配置如果你想自己复现 BqLog 压缩日志路径的核心逻辑或者在自己的项目里借鉴这套思路可以先从环境准备开始。以下步骤基于常见的移动端开发环境具体参数需要根据你的目标平台调整。首先确认你的开发环境满足以下条件支持 C11 或更高版本的编译器BqLog 核心是 C 实现的目标平台有文件系统写入权限如果要做性能对比建议在真机上测试模拟器的 IO 性能跟真机差异很大基础配置方面你需要确定几个关键参数参数说明建议初始值缓冲区大小环形缓冲区的总大小256KB压缩块大小单次压缩的数据量64KB压缩触发水位缓冲区积压达到多少触发压缩70%压缩触发间隔距离上次压缩的最长时间500msCRC 表大小CRC 查表法使用的表大小根据缓存大小定通常 1KB 或 4KB这些初始值不是固定的需要根据实际运行情况调整。调整的依据是运行时采集的指标比如平均压缩耗时、平均写盘耗时、缓冲区平均积压量等。4.2 环形缓冲区的实现要点环形缓冲区的实现有几个关键点需要注意。下面是一个简化版的实现框架展示了核心逻辑class RingBuffer { public: RingBuffer(size_t size) : buffer(size), write_pos(0), read_pos(0) {} // 写入数据返回实际写入的字节数 size_t write(const char* data, size_t len) { size_t available available_write(); size_t to_write std::min(len, available); // 处理回绕缓冲区大小为 2 的幂次可以用位运算 size_t mask buffer.size() - 1; size_t first_chunk std::min(to_write, buffer.size() - (write_pos mask)); memcpy(buffer[write_pos mask], data, first_chunk); if (to_write first_chunk) { memcpy(buffer[0], data first_chunk, to_write - first_chunk); } // release 语义更新写指针 write_pos.fetch_add(to_write, std::memory_order_release); return to_write; } // 读取数据用于压缩 size_t read_for_compress(char* out, size_t len) { size_t available available_read(); size_t to_read std::min(len, available); size_t mask buffer.size() - 1; size_t first_chunk std::min(to_read, buffer.size() - (read_pos mask)); memcpy(out, buffer[read_pos mask], first_chunk); if (to_read first_chunk) { memcpy(out first_chunk, buffer[0], to_read - first_chunk); } // acquire 语义更新读指针 read_pos.fetch_add(to_read, std::memory_order_acquire); return to_read; } private: std::vectorchar buffer; std::atomicsize_t write_pos; std::atomicsize_t read_pos; size_t available_write() const { return buffer.size() - (write_pos.load(std::memory_order_acquire) - read_pos.load(std::memory_order_acquire)); } size_t available_read() const { return write_pos.load(std::memory_order_acquire) - read_pos.load(std::memory_order_acquire); } };这个实现里有两个细节值得注意。一是缓冲区大小必须是 2 的幂次这样write_pos mask就能代替取模运算省掉一个除法指令。二是读写指针的原子操作用了不同的内存序写指针用 release读指针用 acquire这样在保证正确性的前提下尽量减少同步开销。注意这个简化版没有处理缓冲区满的情况。实际使用中如果缓冲区满了写入方需要等待或者丢弃数据。BqLog 的处理方式是等待但等待时间超过阈值会触发强制压缩避免死等。4.3 压缩触发与执行的代码框架压缩触发的判断逻辑需要跟环形缓冲区的状态配合。下面是一个简化的触发判断和执行框架class CompressLogWriter { public: void on_log_write(size_t bytes_written) { // 更新统计信息 total_written bytes_written; // 判断是否触发压缩 if (should_trigger_compress()) { trigger_compress(); } } private: bool should_trigger_compress() { // 条件一缓冲区水位超过阈值 double water_level (double)ring_buffer.available_read() / ring_buffer.capacity(); if (water_level water_level_threshold) { return true; } // 条件二距离上次压缩的时间超过阈值 auto now std::chrono::steady_clock::now(); auto elapsed std::chrono::duration_caststd::chrono::milliseconds( now - last_compress_time).count(); if (elapsed compress_interval_ms ring_buffer.available_read() 0) { return true; } // 条件三写盘通道空闲且有数据待压缩 if (write_channel_idle ring_buffer.available_read() 0) { return true; } return false; } void trigger_compress() { // 计算本次压缩的数据量 size_t to_compress std::min(ring_buffer.available_read(), compress_block_size); // 从环形缓冲区读取数据 std::vectorchar raw_data(to_compress); size_t actual_read ring_buffer.read_for_compress(raw_data.data(), to_compress); raw_data.resize(actual_read); // 执行压缩 auto compressed compress(raw_data); // 计算 CRC uint32_t crc calculate_crc(compressed); // 写入文件异步 async_write(compressed, crc); // 更新状态 last_compress_time std::chrono::steady_clock::now(); } // 动态调整压缩块大小 void adjust_compress_block_size() { // 根据最近几次压缩的耗时和缓冲区积压情况调整 // 如果压缩耗时过长减小块大小 // 如果缓冲区积压过多增大块大小 // 具体算法需要根据实际指标设计 } };这个框架里should_trigger_compress的判断顺序有讲究。先判断水位因为水位判断的开销最小再判断时间间隔这个需要读时钟开销稍大最后判断写盘通道状态这个涉及跨线程状态读取开销最大。把开销小的判断放前面能减少不必要的开销。4.4 CRC 校验的查表法实现与参数选择CRC 校验的查表法实现需要预计算一张表。表的大小决定了计算速度和缓存占用。下面是一个 CRC32 查表法的实现示例class CRC32 { public: CRC32() { // 预计算表表大小为 256 项 for (uint32_t i 0; i 256; i) { uint32_t crc i; for (int j 0; j 8; j) { if (crc 1) { crc (crc 1) ^ POLYNOMIAL; } else { crc 1; } } table[i] crc; } } uint32_t calculate(const char* data, size_t len) { uint32_t crc 0xFFFFFFFF; for (size_t i 0; i len; i) { crc (crc 8) ^ table[(crc ^ data[i]) 0xFF]; } return crc ^ 0xFFFFFFFF; } private: static constexpr uint32_t POLYNOMIAL 0xEDB88320; uint32_t table[256]; };这个实现里表大小是 256 项每项 4 字节总共 1KB。1KB 的表在大多数平台的 L1 缓存里都能放下查表的时候缓存命中率高。如果表再大比如 16KB虽然每次查表能处理更多位但缓存命中率下降整体性能可能反而降低。提示CRC 生成多项式的选择会影响校验能力。不是所有多项式都能保证检出全部奇数个比特错误选择的时候需要参考相关标准。BqLog 用的多项式是经过验证的具体值可以参考公开的 CRC 标准。4.5 哈希表在日志索引中的落地方式哈希表用于日志索引的时候键的设计很关键。BqLog 用的键是时间戳的范围比如“从 T1 到 T2 之间的所有压缩块”。这个键的设计让查询变得简单给定一个时间范围算出哈希值查表得到压缩块列表然后按偏移量读取。哈希表的实现可以用开放地址法或者链地址法。BqLog 用的是链地址法因为键的分布比较均匀冲突率低链地址法的额外指针开销可以接受。下面是一个简化的索引哈希表实现struct IndexEntry { uint64_t time_start; uint64_t time_end; std::vectorsize_t block_offsets; IndexEntry* next; // 链地址法处理冲突 }; class LogIndexHashTable { public: void insert(uint64_t time_start, uint64_t time_end, size_t offset) { size_t hash hash_function(time_start, time_end) % table.size(); IndexEntry* entry table[hash]; while (entry) { if (entry-time_start time_start entry-time_end time_end) { entry-block_offsets.push_back(offset); return; } entry entry-next; } // 新建条目 IndexEntry* new_entry new IndexEntry{time_start, time_end, {offset}, table[hash]}; table[hash] new_entry; } std::vectorsize_t query(uint64_t time_start, uint64_t time_end) { size_t hash hash_function(time_start, time_end) % table.size(); IndexEntry* entry table[hash]; while (entry) { if (entry-time_start time_start entry-time_end time_end) { return entry-block_offsets; } entry entry-next; } return {}; } private: std::vectorIndexEntry* table; size_t hash_function(uint64_t start, uint64_t end) { // 简单的哈希函数实际使用中需要根据数据分布优化 return (start * 31 end) * 2654435761; } };这个实现里哈希函数的选择会影响冲突率。BqLog 用的哈希函数是经过调优的具体实现没有公开但思路是让时间戳的高位和低位都参与运算避免高位相同低位不同的键产生冲突。5. 常见问题与排查技巧实录5.1 压缩日志写入延迟突然升高怎么排查压缩日志写入延迟突然升高通常有几个原因。第一个是压缩块大小不合适可能是动态调整逻辑出了问题把块大小调得太大单次压缩耗时增加。排查方法是打印最近几次压缩的块大小和耗时看是否有异常。第二个是缓冲区水位持续偏高说明压缩速度跟不上写入速度可能是压缩算法选择不当或者 CPU 被其他任务占满。排查方法是看 CPU 使用率和缓冲区水位的相关性。第三个原因是 CRC 校验的计算量突然增加。如果压缩后的数据量没有明显变化但 CRC 计算耗时增加可能是查表的时候缓存未命中率升高。排查方法是看 CRC 表的访问模式如果访问的地址跳跃很大缓存命中率就会低。解决办法是调整表的大小或者改变数据的处理顺序让访问更局部化。提示排查延迟问题的时候建议先加时间戳打点把压缩路径上每个节点的耗时都记录下来。不要凭感觉猜数据会告诉你瓶颈在哪。5.2 CRC 校验值不一致的几种可能CRC 校验值不一致是压缩日志路径上比较常见的问题。可能的原因包括可能原因排查方法解决思路数据在压缩前就被修改检查压缩前的数据是否有其他线程在写加锁或者用不可变数据压缩算法有 bug用相同的输入多次压缩看输出是否一致换压缩算法或者修复 bugCRC 计算时数据长度不对检查传入 CRC 计算函数的数据长度确认长度参数正确字节序问题检查 CRC 值的存储和读取是否用了相同的字节序统一字节序表初始化不完整检查 CRC 表是否在所有线程中都初始化了用静态初始化或者线程安全的初始化方式字节序问题特别容易被忽略。如果 CRC 值在写入文件的时候用了小端序读取的时候用了大端序算出来的校验值肯定对不上。BqLog 在这块的处理是统一用网络字节序大端序存储读取的时候再转回来。5.3 哈希表冲突率过高的调整方法哈希表冲突率过高会导致查询变慢严重的时候会退化成链表遍历。调整方法有几个方向。一是换哈希函数让键的分布更均匀。二是增大哈希表的容量减少冲突概率。三是改用开放地址法利用缓存局部性减少冲突带来的开销。BqLog 在这块的做法是动态调整哈希表的容量。当检测到冲突率超过阈值时触发扩容把表的大小翻倍然后重新哈希所有条目。扩容本身有开销所以触发条件不能太敏感否则频繁扩容反而影响性能。BqLog 的阈值设得比较保守只有在冲突率持续偏高的时候才触发扩容。注意扩容的时候需要暂停写入否则会出现数据不一致。BqLog 的做法是在扩容期间把新的写入请求暂存到一个临时缓冲区扩容完成后再合并进去。这个临时缓冲区的大小有限如果扩容时间太长临时缓冲区满了就会丢弃数据。所以扩容的触发时机要选在写入压力小的时候。5.4 压缩率突然下降的原因分析压缩率突然下降通常意味着压缩算法的输入数据特征变了。可能的原因包括日志内容从结构化变成了非结构化压缩算法对非结构化数据的压缩效果差日志内容重复率降低去重逻辑失效压缩块大小变小压缩算法无法充分利用上下文信息。排查方法是先看日志内容的变化如果最近改了日志格式压缩率下降是正常的。如果日志格式没变再看压缩块大小的变化如果块大小被动态调整逻辑调小了压缩率也会下降。最后看去重逻辑是否还在正常工作如果去重失效重复数据增多压缩率反而应该上升但如果去重逻辑本身有 bug可能会把不该去重的数据去重了导致数据损坏压缩率异常。5.5 多线程写入时的竞争问题多线程写入压缩日志路径的时候竞争主要发生在环形缓冲区的读写指针更新和压缩触发判断上。BqLog 的做法是用原子操作加内存屏障来保证正确性但原子操作本身有开销如果竞争激烈开销会显著增加。减少竞争的方法有几个。一是把写入操作批量处理减少原子操作的次数。二是用线程本地缓冲区每个线程先写自己的缓冲区攒够一批再合并到全局缓冲区。三是把压缩触发判断放到单独的线程避免写入线程参与判断。BqLog 用的是第二种方法每个线程有独立的写入缓冲区攒到一定大小或者超过一定时间才合并到全局缓冲区。这样写入线程的原子操作次数大幅减少竞争也相应减少。但线程本地缓冲区会增加内存占用需要根据线程数量调整缓冲区大小。6. 一些实测数据和调参经验6.1 不同压缩块大小下的性能对比我在一台中端安卓机上做了一组对比测试固定其他参数只调整压缩块大小看写入延迟和压缩率的变化。测试场景是模拟游戏对战日志每秒写入约 500 条日志每条日志平均 200 字节。压缩块大小平均写入延迟P99 延迟压缩率CPU 占用16KB0.8ms3.2ms3.5:112%32KB1.1ms4.5ms4.2:110%64KB1.8ms7.1ms4.8:19%128KB3.2ms12.5ms5.1:18%256KB5.6ms21.3ms5.3:17%从数据看压缩块大小从 16KB 增加到 64KB压缩率提升明显但延迟增加也在可接受范围内。超过 64KB 之后压缩率提升变缓但延迟增加加快。所以 64KB 是一个比较合适的默认值。当然这个值跟设备性能有关低端设备可能需要调小高端设备可以调大。6.2 CRC 表大小对缓存的影响CRC 表大小对性能的影响主要体现在缓存命中率上。我测试了三种表大小256 项1KB、1024 项4KB、4096 项16KB。测试平台是骁龙 865L1 缓存 64KBL2 缓存 512KB。表大小平均 CRC 计算耗时缓存命中率256 项0.12us/字节98%1024 项0.09us/字节95%4096 项0.08us/字节82%1024 项的表在计算速度和缓存命中率之间取得了比较好的平衡。4096 项的表虽然单次计算更快但缓存命中率下降整体性能提升有限。256 项的表计算速度稍慢但缓存命中率最高在缓存紧张的设备上可能是更好的选择。6.3 哈希表扩容阈值的调整哈希表扩容阈值设得太低会频繁扩容影响性能设得太高冲突率上升查询变慢。我测试了几个阈值看查询耗时和扩容次数的变化。扩容阈值负载因子平均查询耗时扩容次数每小时0.50.8us120.751.2us40.92.5us11.05.8us0负载因子 0.75 是一个比较经典的平衡点查询耗时和扩容次数都在可接受范围内。BqLog 默认用的就是这个值但在实际使用中可以根据查询频率调整。如果查询频率很低可以适当提高阈值减少扩容次数。6.4 动态调整压缩块大小的实际效果BqLog 的动态调整逻辑在实际使用中确实能改善性能。我在一个写入速率波动很大的场景下做了对比测试固定块大小和动态调整的差异很明显。固定 64KB 块大小的时候写入速率低的时候延迟很低但写入速率高的时候延迟飙升P99 延迟到了 15ms。动态调整的时候写入速率低的时候块大小自动调小到 32KB延迟更低写入速率高的时候块大小自动调大到 128KB延迟控制在 8ms 以内。整体 P99 延迟降低了约 45%。动态调整的代价是逻辑复杂度增加而且调整本身有开销。BqLog 的做法是限制调整频率最快每 10 秒调整一次避免频繁调整带来的抖动。7. 几个容易踩的坑和我的个人建议第一个坑是 CRC 校验的位置。我一开始把 CRC 校验放在压缩前觉得这样能校验原始数据。但后来发现压缩后的数据量小CRC 计算量也小而且压缩后的数据才是最终落盘的校验它更有意义。改到压缩后之后CRC 计算耗时降低了约 60%。第二个坑是哈希表的键设计。我一开始用日志条目的完整内容做键结果哈希计算耗时很长而且冲突率很高。后来改成用关键字段的哈希值做键计算快了冲突率也降下来了。关键字段的选择需要根据实际查询需求定不是固定的。第三个坑是环形缓冲区的内存序。我一开始用默认的内存序结果在多线程环境下偶尔出现数据不一致。后来改成写指针用 release读指针用 acquire问题就消失了。内存序的选择需要根据实际的同步需求定过度同步会损失性能同步不足会导致正确性问题。提示如果你在移动端用 BqLog建议在初始化的时候根据设备的内存大小和 CPU 核心数调整缓冲区大小和压缩块大小。低端设备内存紧张缓冲区可以调小到 128KB压缩块调到 32KB。高端设备可以适当调大但不要超过 512KB否则单次压缩耗时会太长。第四个坑是压缩算法的选择。BqLog 默认用的压缩算法在压缩率和速度之间取得了平衡但如果你的日志内容有特定特征比如大量重复的字符串换一个针对这种特征优化的压缩算法可能会更好。但换算法需要重新测试不能直接替换。第五个坑是日志去重的粒度。去重粒度太细哈希计算开销大去重粒度太粗去重效果差。BqLog 的做法是对特定类型的日志做去重不是所有日志都走这条路。你需要根据自己日志的特点决定去重的粒度。最后分享一个小技巧如果你在排查压缩日志路径的性能问题可以先在压缩路径的每个节点加一个计数器统计每个节点的调用次数和总耗时。这样能快速定位到哪个节点是瓶颈。不要一上来就改代码先看清楚问题在哪。我试过好几次以为是压缩算法慢结果发现是 CRC 计算慢或者是哈希表冲突率高。数据比直觉可靠。