当日志开始按业务对象组织,问题才刚刚开始

发布时间:2026/7/29 16:55:33

当日志开始按业务对象组织,问题才刚刚开始 最直觉的方案Key 映射文件上一篇聊到一个想法如果日志记录的是业务过程那日志的组织方式也许应该跟着业务走。想法很自然按业务 Key玩家、租户、订单把日志分发到不同文件每个业务对象一个专属日志。排查时直接打开对应文件不用 grep不用拼上下文。最直觉的实现思路大概长这样// 伪代码仅说明思路Writerwriterwriters.get(key);if(writernull){writercreateWriter(key);writers.put(key,writer);}writer.write(event);一个 MapKey 对应一个文件每次写日志时查找或创建。当 Key 数量不多的时候这个方案没问题。但当你真正面对线上环境——几十万玩家同时在线每个玩家都是一个 Key——你会发现问题远不是增加一个路由规则这么简单。这里先忽略一个重要问题谁负责写入这个文件以及如何保证同一个 Key 内的顺序。一个容易被忽略的前提日志是有顺序的在设计之前必须先想清楚一件事。日志不是普通文本。它记录的是一个业务对象经历过什么——状态变化、操作过程、事件轨迹。这些记录天然存在一个时间顺序。10:00:01 玩家登录 10:00:02 玩家进入匹配 10:00:03 匹配成功进入战斗 10:00:05 释放技能 10:00:06 技能结算完成 10:00:08 战斗结束这条时间线就是排查问题的依据。如果输出变成这样10:00:06 技能结算完成 10:00:03 匹配成功进入战斗 10:00:05 释放技能数据都在但顺序乱了。虽然技术上你可以在事后排序但日志通常不携带精确到毫秒的时间戳或者精度不够区分高并发事件而且跨文件排序本身就是额外的复杂度。更关键的是如果日志是不同线程写入的你可能根本无法恢复原始顺序。所以第一个约束出现了同一个业务 Key 的日志必须保持写入顺序。这不是性能优化需求而是正确性需求。一个 Key 一个线程行不通知道要保证顺序之后最直觉的方案是给每个 Key 分一个独立的写入线程。player-1001 → thread-1 → file-1001.log player-1002 → thread-2 → file-1002.log player-1003 → thread-3 → file-1003.log ...Key 之间互不干扰天然并行顺序也能保证。但这个方案有一个致命问题Key 的数量不可控。几十个 Key没问题。几百个也许还行。但游戏服务端在线上可能同时有几万甚至几十万个活跃玩家——每个都是一个 Key。如果每个 Key 一个线程线程数量爆炸线程切换成本远超写入本身如果每个 Key 都维护独立的写入上下文最终仍然需要管理大量文件句柄、缓冲区等资源内存中维护大量线程栈和缓冲区GC 压力剧增这条路走不通。所有日志一个线程也有问题既然每个 Key 一个线程不行那反过来所有 Key 的日志共用一个写入线程。顺序简单了——所有日志按到达顺序依次写入。但新问题出现了热 Key 效应。假设某个高活跃玩家每秒产生几百条日志而大多数玩家每秒只有几条。在单线程模型下这个热 Key 的日志会占据写入线程的大部分时间。其他玩家的日志虽然在队列里但必须等热 Key 写完才能被处理。单个热 Key 就能拖慢所有业务对象的日志写入。这其实就是全局顺序 vs Key 内顺序两个目标的冲突。单线程模型解决了全局顺序问题但牺牲了不同业务对象之间的并行能力。你需要的其实是同一个 Key 内部保持顺序不同 Key 之间可以并行。但 Key 数量不可控又不能为每个 Key 分配独立线程。这就成了一个设计矛盾。最初的文件映射方案为什么会失败回到最开始的那个直觉方案key → file用一个 Map 缓存 Key 到文件的映射写入时查找或创建。在低 Key 场景下这确实工作得很好。但 Key 数量增长后问题变了。假设线上同时有 10 万个活跃 Keyplayer-1.log player-2.log ... player-100000.log这时候问题已经不只是日志写到哪里而是变成了资源管理。文件数量增长后的连锁反应10 万个活跃 Key意味着系统可能需要面对数量级接近的日志输出目标。每个打开的文件背后有一套资源链创建文件 ↓ 打开 FileChannel ↓ 分配缓冲区 ↓ 占用 OS 文件描述符 ↓ 持续写入这些资源不是免费的。文件描述符是有限的。操作系统对单个进程可打开的文件数量存在限制。当你有 10 万个 Key 时不可能同时保持所有文件打开。缓冲区是内存。每个打开的文件都需要写缓冲区。10 万个文件意味着 10 万份缓冲区驻留在内存里。创建和关闭是有成本的。每打开一个新文件操作系统要分配文件描述符、分配文件系统资源每次关闭要 flush 缓冲、释放描述符。频繁创建和关闭的开销不容忽视。所以问题变成了当活跃 Key 数量远大于你能同时打开的文件数时怎么办缓存容易淘汰难答案似乎是缓存只保持最近活跃的 N 个文件打开不活跃的关闭。这个思路是对的。但真正困难的不在于缓存本身而在于淘汰。当缓存满了来了一个新的 Key新 Key 请求写入 ↓ 缓存未命中 ↓ 需要打开新文件 ↓ 但缓存已满必须淘汰一个旧文件 ↓ flush 旧文件缓冲区 ↓ 关闭 FileChannel ↓ 释放文件描述符 ↓ 创建新文件打开新 Channel ↓ 写入这套流程本身没问题。但请注意它可能发生在日志的写入路径上。flush 和 close 是 IO 操作会阻塞。如果淘汰恰好发生在一条日志的写入过程中这条日志的延迟就会突然飙升。更麻烦的是淘汰策略。选择淘汰哪个文件最近最少使用LRU但如果某个 Key 刚好处于一个高频操作序列中比如战斗阶段你淘汰了它的文件紧接着又来了一条日志又要重新打开——这就是缓存抖动。对于普通缓存淘汰只是释放内存。对于日志文件缓存淘汰还伴随着状态转换——flush、close、再打开。在写入路径上做资源淘汰就像在高速公路上换轮胎。能做但代价很高。高离散 Key 下的问题链当 Key 数量增长到一定规模你会观察到一条问题链为了验证这个问题我做了一组高 Key 数量测试。实际线上 Key 数量可能达到几十万但问题并不是在几十万这个数字才出现而是在活跃 Key 数量超过系统资源承载能力时就会开始暴露。测试并不是模拟实际玩家数量而是人为降低可缓存的文件 Channel 数量用于观察 Key 数量接近资源边界时系统的行为变化。测试关注的不是单纯 TPS而是观察文件切换次数0 表示缓存全部命中flush 行为写入调用次数消费队列拒绝次数IO 特征变化因为对于这种场景吞吐量并不是唯一指标。Key 数量Files TouchedFlushes/secWrites/secRejected/secIO MB/sec20008814,036028.362101,3271,3788,5959,97616.112202,0342,0344,68711,4537.42可以看到当 Key 数量增加后系统并不是简单地线性下降而是在资源复用开始失效后出现明显变化。例如Key 数量从 200 增加到 210 时Files Touched 从 0 跳升到 1,327说明活跃 Key 开始超过缓存有效覆盖范围大量日志目标无法持续复用已有写入资源。与此同时Rejected/sec 从 0 飙升到 9,976队列开始拒绝新事件。到 220 个 Key 时Files Touched 达到 2,034说明大量日志目标已经无法稳定复用已有写入资源文件切换和资源管理成本明显增加。Flushes/sec 从 88 增长到 2,034flush 频率翻了 23 倍。IO 吞吐量从 28.36 MB/sec 下降到 7.42 MB/secWrites/sec 腰斩。这正是一条完整的问题链Key 数量增加 ↓ 打开的文件无法持续复用Channel 缓存命中下降 ↓ 文件切换打开/关闭频率上升 ↓ flush / close 操作增加 ↓ 写入路径上的阻塞增多 ↓ 写入 Worker 的吞吐下降 ↓ 队列开始积压 ↓ 延迟上升这条链的起点不是写文件慢而是管理的文件太多了。最终瓶颈不在单纯的 IO 写入而在于管理大量业务对象对应的文件资源的成本。最终仍然存在的物理边界即使解决了文件管理问题最终仍然需要面对存储系统本身的限制。日志系统的写入链路最终都会落到磁盘上。当日志产生速度持续大于存储消费速度任何缓冲和优化都只能延缓问题不能消除它。这不是某个实现方案的问题而是所有日志系统共同面对的边界。你能做的无非是三件事缓冲用队列平滑突发、限制控制同时打开的文件数量、降低压力让写入路径尽量轻量。从这些问题的约束中沉淀出几条设计原则这些问题不一定有唯一的最优解但它们定义了几个必须尊重的约束顺序保证优先于简单并行。如果日志承担业务过程追踪职责那么同一个业务 Key 内部的日志顺序就是一个重要约束。设计必须在这个前提下考虑并行策略。Key 的生命周期决定资源模型。Key 不是固定资源——玩家会登录和下线租户会活跃和沉默。活跃 Key 和沉默 Key 应该得到不同的资源对待。热 Key 和高离散 Key 是两种不同的挑战需要不同的应对策略。文件的生命周期必须可控。不能让 Key 数量直接等于打开的文件数量。必须有淘汰、回收、限制的机制而且这些机制不应该阻塞核心写入路径。资源管理不应该成为写入瓶颈。写入路径应该尽量轻打开、关闭、淘汰这些重操作应该从写入路径上剥离出去。写在最后到这里才发现按业务 Key 路由日志本质上已经不是一个 Appender 或 Writer 的问题。它涉及顺序模型— 如何在多 Key 并行写入时保证单个 Key 的顺序并发模型— 如何在 Key 数量不可控时分配写入资源文件管理— 如何管理大量文件的打开和关闭资源控制— 如何在有限资源下做淘汰决策IO 边界— 如何在磁盘的物理限制下最大化写入效率这些问题每一项单独拿出来都不算特别复杂。但当它们同时出现在一个系统里而且互相约束、互相影响时设计难度就完全不同了。这也回到了第一篇提出的问题当日志开始承载业务对象完整过程时它的组织方式也需要重新思考。按 Key 分离日志看似只是改变日志输出位置实际上改变的是整个日志系统面对业务过程的方式。下一篇继续聊这些约束下的一种设计思路以及为什么最终选择这样的执行模型。

相关新闻