Hindsight这个词老读者应该都知道字面意思是后见之明。说白了就是事情发生后回头看一切都清清楚楚但在发生的那一刻却毫无察觉。做后端开发这几年我对这个词体会特别深每次线上故障复盘最扎心的时刻就是把日志、监控、告警一项项摊开所有线索明明都在但事故当时就是没人把它们串成一条完整的因果链。Hindsight 这个项目就是我想把这件事后诸葛亮的事用工程化的方式固定下来。这个项目的核心定位是一个轻量级的事件回放系统。它不追求高大上的全链路追踪也不打算替代 APM、监控平台这些老牌工具它只做一件事把系统运行时产生的高价值事件以低侵入的方式完整记录下来让你在故障发生之后能够像回放行车记录仪一样把导致故障的那段现场录像按因果顺序重新播放一遍。适合谁看如果你日常被线上问题折腾得焦头烂额尤其是微服务架构、分布式系统环境下的后端、SRE、稳定性工程师这篇文章里的方案可以给你一个真正落地的排障思路。1. 项目整体设计与思路拆解1.1 Hindsight 到底想解决什么问题先摆一个最常见的场景。你负责的一个订单服务突然大面积超时用户投诉接踵而至。你打开监控大盘发现下单接口 P99 从 80ms 飙到了 3s然后又掉回去了。你再看日志发现网关层报了几个 timeout订单服务本身却看起来正常只有零星几条慢查询的 Warning。这种局面我相信所有人都遇到过。问题出在哪出在传统观测体系的视角上。监控大盘展示的是时序指标的聚合结果日志系统展示的是单条记录的孤立信息APM 链路追踪虽然把一次调用的跨服务路径串起来了但它默认只保留采样数据而且它的核心服务是追踪不是回放。我需要的不是那条 trace 长什么样而是它背后完整的事件流每个服务在什么时间收到了什么请求、做了哪些处理、调用了哪个下游、返回了什么结果、当时的系统水位如何。这些信息日志里都有但分散在十几个文件时间线对不齐因果顺序全靠猜。Hindsight 的破局思路就是借鉴分布式事务里事件溯源的思想把所有关键的、可观测的事件按照统一的时间模型和因果模型集中写入一套独立的存储。故障发生时不是我翻日志看哪一分钟发生了什么而是直接按 trace 维度或者时间窗口维度把这条链路上所有事件按因果顺序回放出来。从用户请求进入网关开始到网关转发给订单服务到订单服务调用库存服务每一步都清清楚楚。1.2 为什么不用现成的 APM 和日志平台有人会问SkyWalking、Jaeger 不是已经做了链路追踪了吗ELK 不也能做日志关联吗为什么还要自造轮子这个质疑是对的所以我在设计 Hindsight 之前认真梳理过这些工具的能力边界。APM 链路追踪的核心是span它天然假设调用一定是从一个服务流向另一个服务的树形结构因此在服务内部的多步处理细节、异步分支、消息中间件的发布订阅场景下span 的建模会非常别扭。更关键的是传统 APM 为了避免存储压力默认只记录调用元数据请求参数和返回体往往只保留极小字段真正能定位业务逻辑错误的信息根本不在里面。日志平台的问题更直接它是被动写入各服务自己打自己的日志日志格式、时间格式、时区都可能不一样。就算你强制要求统一格式日志也无法体现事件 A 是事件 B 的起因这种因果关系。多打印一行日志容易靠日志推断因果链难如登天。Hindsight 不是要替代它们而是做一个逻辑上的上位层。它把 APM 最擅长的 trace 维度作为事件的天然分组键把日志系统最擅长的细粒度业务上下文放进了 event payload再引入一个统一的因果事件模型。本质上它是在 APM 的骨架里灌入了日志级别的血肉。1.3 架构选型的关键取舍整体架构上我没有走全量采样的激进路线而是做了几个很关键、也很务实的取舍。第一采集端采用异步旁路方式不做线程级 agent 注入。所谓旁路就是业务代码只需要手动调用一个 SendEvent 函数把结构化事件丢给本地 collector 进程collector 异步批量上报。这样做的好处是业务线程完全不需要阻塞在网络等待上避免了在排障工具身上又引入新的故障源。第二存储层采用追加写 按时间分桶的方式而不是走通用数据库。事件流本质上是 append-only 的用类 WAL 的格式按天分桶落盘配合索引文件就能做到非常高的写入吞吐。查询侧再通过一个轻量加载层把目标时间窗口的事件加载进内存做排序和回放。这是我测试了多种方案后最终敲定的路线后面会详细展开存储细节。第三回放引擎提供快照回放和区间回放两种模式。快照模式用于观察某一时刻全链路的瞬时状态区间模式用于观察一段连续时间内的事件流。两种模式共用同一个事件存储只是查询逻辑不同。这个设计让排障场景和容量分析场景都能覆盖。从这些取舍你可以看到Hindsight 的第一原则是低侵入第二原则是可裁剪。它不追求一步到位而是保证每个环节都能独立使用、独立替换。我在实际搭建过程中也深刻体会到排障工具最怕自己被拖下水所以任何会让业务侧感知到额外开销的设计都被我砍掉了。2. 核心细节解析与实操要点2.1 事件模型的五要素事件模型是 Hindsight 的灵魂它决定了你能从回放中还原出多少信息。我把它抽象成五个核心字段缺一不可。字段名类型含义为什么必须有它event_iduint64全局唯一事件 ID后续因果索引的锚点parent_iduint64上游事件 ID这是构建因果链的核心后面细讲trace_idstring业务追踪分组 ID一次用户请求的全链路分组hlcuint64混合逻辑时钟值解决分布式环境下的时间排序问题payloadmap/bytes事件负载具体的业务上下文、出入参、状态变化parent_id 是 Hindsight 区别于普通日志系统的最重要字段。它允许事件之间形成虽然不在同一个 trace_id 下、但逻辑上是因果触发关系的链接。举个例子一个定时任务触发了下单这个下单操作内部的 trace_id 是新的但从事件流角度看它一定有一个上游源头事件——定时任务的调度事件。有了 parent_id回放引擎就能跨 trace 重建因果路径。还要补充一个容易被忽略的点事件模型必须包含 systime 和 hlc 两个时间维度。systime 是采集节点所在机器的物理时间用于向人类展示这个事件大概发生在几点几分hlc 用于排序引擎判断事件的逻辑先后顺序。如果你只存物理时间分布式环境下的时钟漂移会让排序结果出现假象如果只存逻辑时间面对用户究竟是哪一秒出错的这种灵魂拷问你又答不上来。2.2 混合逻辑时钟解决排序痛点分布式系统里的事件排序是个永远的坑。问题根源就是机器物理时钟不可能完全一致哪怕用了 NTP也会有几百毫秒的偏差。最常见的表现就是同样的两个事件A 服务记录的时间是 12:00:01.200B 服务记录的是 12:00:01.500但实际发生的顺序是 B 先发生A 后发生。监控报表上看到的时序就会是错乱的。Hindsight 采用了混合逻辑时钟来做事件排序。HLC 的核心思想不复杂每个节点维护一个持有物理时间 逻辑计数器的二元组。本地产生新事件时如果本地物理时间已经大于当前元组里的物理时间就重置逻辑计数器为 0否则就把逻辑计数器加 1。收到过远端事件时需要对元组做一次合并取物理时间的最大值如果本地逻辑计数不够大就进行调整。具体实现上我用 Go 写过一个简化版核心逻辑是这样type HLC struct { Physical int64 Logical uint32 } func (h *HLC) LocalNow(physicalNow int64) uint64 { if physicalNow h.Physical { h.Physical physicalNow h.Logical 0 } else { h.Logical } return h.encode() } func (h *HLC) Recv(remote uint64) uint64 { remotePhysical, remoteLogical : h.decode(remote) switch { case remotePhysical h.Physical: h.Physical, h.Logical remotePhysical, remoteLogical1 case remotePhysical h.Physical remoteLogical1 h.Logical: h.Logical remoteLogical 1 default: h.Logical } return h.encode() }编码的时候我把 64 位值做了切分高 48 位存物理毫秒时间低 16 位存逻辑计数。这里有个细节——当逻辑计数溢出到 16 位上限时会进位到物理时间域实际系统中逻辑计数溢出概率极低我测试时一天跑几十亿事件也没碰到过但如果你追求极限严谨可以用高 32 位 低 32 位的切分方式。很多人会问那直接用数据库自增 ID、或者用雪花 ID 排序不就行了不行。自增 ID 只能体现单机写入顺序雪花 ID 只能保证同一个节点内有序跨节点的因果先后关系依然无法从 ID 推断。HLC 的价值在于它把物理时间贴近 逻辑顺序可比两个能力结合到了一起这也是它成为事件回放系统首选时间模型的原因。2.3 事件采集器的实现思路采集器的核心任务是把业务侧的事件拿到并且以足够低的损耗传输出去。我最初用 Java 写过一版基于字节码增强的自动埋点效果不错但侵入性太强改了项目依赖还可能导致类加载冲突后来推倒重来改用显式埋点 薄客户端 SDK的方案。这个方案的核心不是自动化而是约定。我给团队定的规矩是以下四条1. 所有服务的关键业务节点接收请求、调用下游、异常抛出、状态变更必须调用一次 Record() 2. 事件必须携带 trace_id不能偷懒 3. 业务上下文用结构化字段严禁拼一大段字符串 4. 拿不准时宁可多埋点也不要漏埋采集器 SDK 内部维护一个无锁环形队列业务线程把事件入队后立即返回由后台批量线程每 100ms 或者每积累 200 条向上层上报一次。环形队列的容量设置为 65536当队列满载时触发降级策略丢弃最旧的一批事件同时记录一条事件被降级丢弃的元事件。之所以优先丢旧的而不是丢新的是因为越新的事件越可能与当前正在发生的故障有关。还有一个非常关键但容易踩坑的环节是异步上下文的传递。Java 里有 ThreadLocal但在异步线程池、MQ 消费者这些场景下ThreadLocal 是传不过去的trace_id 会断。我在实践中是强制要求 SDK 暴露一个静态方法用于手动设置当前线程的 trace_id同时配合拦截器在入口处统一注入。具体到代码层面是用了类似下面的机制func WithTrace(ctx context.Context, traceID string) context.Context { return context.WithValue(ctx, traceKey{}, traceID) } func TraceFrom(ctx context.Context) string { if v, ok : ctx.Value(traceKey{}).(string); ok { return v } return }所有异步代码必须显式传递 context不允许从全局变量里拿。这虽然增加了开发负担但保证了事件流的 trace_id 一致性。没有这个前提后面的因果链重建就是空中楼阁。2.4 存储设计与事件压缩策略事件存储的设计我遵循的是顺序写、快读取、可压缩三原则。Hindsight 的数据文件按天分目录目录下面是按小时分的分段文件。每个分段文件内部按 hlc 值升序追加写入文件头部写入一个 512 字节的文件头记录文件的事件数量、起始 hlc、结束 hlc 等信息。查询时只需快速定位到对应小时段再通过内存索引二分跳过不相关的块。存储的稳定性关键在于压缩。如果把原始事件直接落盘一天的数据量能撑爆磁盘。我的做法是三层压缩策略。第一层是属性字典化。服务名、操作名、事件类型这些字段字符串可读但体积大我用一个字典文件为每个唯一字符串分配一个自增 ID事件流里只存 ID。比如 event_type 字段原始字符串create_order_failed占 20 个字节字典化之后只占 2 个字节。这一层通常能砍掉 40% 的体积。第二层是时间戳差分编码。事件之间的 hlc 物理时间差值往往很小用定长 int64 存每个事件的时间戳太奢侈。我在分段文件内只存第一个事件的完整时间戳后续事件存与前一个事件的 Delta 值再用 varint 编码。大多数 Delta 值都小于 128一个字节就能搞定存储时间字段的成本瞬间降了七八倍。第三层是 payload 的列式裁剪。如果一个事件的 payload 里有大量重复的键名比如 {user_id: 82, order_id: 128}我就把键名独立成 schema 存到文件头事件内只存值数组。这套策略执行后我从原始设计的每事件 200 字节压到了平均 45 字节压缩比达到了 4.4 倍查询性能几乎没有损失。3. 实操过程与核心环节实现3.1 最小可行版本的技术栈与目录结构很多人讨论项目喜欢直接聊架构我觉得顺序反了。对于这种基础设施性质的工具先把代码跑起来、看见真实数据再谈优化才有底气。所以我建议你从最小可行版本做起目录结构可以参考我这个。hindsight/ ├── cmd/ │ ├── collector/main.go // 采集端入口 │ └── server/main.go // 回放服务入口 ├── internal/ │ ├── model/event.go // 事件模型定义 │ ├── hlc/hlc.go // 混合逻辑时钟实现 │ ├── store/segment.go // 分段存储引擎 │ ├── ingest/http.go // 上报接口 │ └── replay/replayer.go // 回放引擎 ├── sdk/ │ ├── go/sdk.go // Go 客户端 SDK │ └── java/ // Java 客户端 SDK └── configs/collector.yaml // 采集端配置技术栈选了 Go原因很直接它的并发模型非常适合做高吞吐事件采集goroutine 处理海量连接和批量上报天然没有 Java 那种线程池调优的痛苦。如果你对 Go 不熟用 Java 或 C 写也是一样的但 Go 的部署便利性让我省了很多事编译出来一个静态二进制扔到容器里就能跑。事件模型的定义我放在 model 包这是最重要的数据结构type Event struct { ID uint64 json:id ParentID uint64 json:parent_id TraceID string json:trace_id Service string json:service,omitempty Op string json:op,omitempty HLC uint64 json:hlc SysTime int64 json:sys_time Payload map[string]any json:payload,omitempty }这段代码配合事件流协议已经能支撑起后面所有的回放功能。不要急着上 gRPC 或者 protobuf初期用 JSON 反而更直观排查问题的时候 curl 一下就能看见完整数据结构。3.2 事件采集与上报的完整流程事件采集的第一步是初始化 SDK 并注册 collector 地址。SDK 的初始化代码很简单sdk.Init(sdk.Options{ CollectorAddr: http://collector.example.com:8080, ServiceName: order-service, FlushInterval: 100 * time.Millisecond, QueueCapacity: 65536, })初始化之后业务代码里只要在关键位置调一次 sdk.Record 就能上报事件。比如下单服务收到请求这个节点我是这样埋的func (s *OrderService) CreateOrder(ctx context.Context, req *OrderRequest) (*OrderResponse, error) { traceID : trace.FromContext(ctx) eventID : sdk.NextID() sdk.Record(sdk.Event{ ID: eventID, ParentID: sdk.ContextEventID(ctx), // 上一个事件的 ID TraceID: traceID, Op: order.create.request, HLC: sdk.CurrentHLC(), SysTime: time.Now().UnixMilli(), Payload: map[string]any{user_id: req.UserID, sku: req.SKU}, }) // 业务处理... }注意这里的地一个细节ParentID 是从 ctx 里取的也就是当前请求链路的上游事件。SDK 内部维护一个线程安全的 mapkey 是 trace_idvalue 是最近一次事件 ID。每次 Record 之后都要在入队时顺手更新这个值。这样即使业务代码里隔了很多层调用、很多次函数跳转只要 context 一路传下去新的事件就能挂在已有的事件链上。批量上报的接口协议我用的是简单的 POST JSONcurl -X POST http://collector.example.com:8080/api/v1/events \ -H Content-Type: application/json \ -d { service: order-service, node: pod-7c9f, events: [ { id: 10452, parent_id: 10451, trace_id: tr_9f2e1b, op: order.create.request, hlc: 1789012345678200005, sys_time: 1789012345678, payload: {user_id: 82, sku: A128} } ] }collector 收到这批事件后会做两件事。先走一遍校验把 hlc 异常、id 冲突的数据剔除然后按小时分桶写入分段文件。写入是纯顺序追加所以 collector 的吞吐极稳定实测 4 核 8G 的容器能跑到每秒 5 万条事件以上CPU 占用不到 30%。3.3 回放引擎的实现思路回放引擎是 Hindsight 最有技术含量的部分。它的任务是把用户给定的 trace_id 或者时间区间还原成一条因果上有序的事件流。最朴素的实现就是查询所有属于该 trace_id 的事件按 hlc 排序输出。这在大多数场景下够用但有一个致命缺陷如果一次业务请求跨了多个服务而这些服务的物理时间有偏差那么单纯按 hlc 排序而忽略 parent_id就会出现子事件排在父事件前面的诡异结果。所以真正的回放逻辑必须结合两个维度。我用的是先分层、后排序的两阶段策略。第一阶段从存储中取出该 trace_id 所有事件构建一个以 parent_id 为指针的前驱索引这样就能从一个入口事件沿着 parent_id 一路向后走形成一条条独立的分支链。第二阶段把分支链的每条链内部按 hlc 排序链与链之间再按父链最后一个事件的 hlc决定嵌套层级。这样回放出来的事件流既符合因果顺序又保有了时间顺序。回放引擎的伪代码组织如下func (r *Replayer) ReplayByTrace(traceID string) ([]*model.Event, error) { events : r.store.QueryByTrace(traceID) index : make(map[uint64][]*model.Event) var roots []*model.Event for _, ev : range events { if _, ok : index[ev.ParentID]; !ok { index[ev.ParentID] make([]*model.Event, 0) } index[ev.ParentID] append(index[ev.ParentID], ev) if ev.ParentID 0 { roots append(roots, ev) } } var sorted []*model.Event var walk func(*model.Event, int) walk func(node *model.Event, depth int) { sorted append(sorted, node) children : index[node.ID] sort.Slice(children, func(i, j int) bool { return children[i].HLC children[j].HLC }) for _, child : range children { walk(child, depth1) } } for _, root : range roots { walk(root, 0) } return sorted, nil }实际操作中你会发现这个递归回溯的效率非常高因为它只扫描一遍数据而且 parent_id 的索引是纯内存操作。我在线上回放一条涉及 4 个服务、6 万个事件的 trace 时耗时不到 300ms完全够用。3.4 部署与资源评估部署 Hindsight 的生产环境我总结了几个可以抄作业的指标。collector 进程按服务所在节点部署每个节点一个推荐至少 2 核 4G 起步。server回放服务可以集中部署也可以和存储混部视数据量而定。存储容量可以提前估算。假设你的系统峰值 QPS 是 5000每个请求平均产生 800 个事件每事件压缩后大概 50 字节。那么每秒产生的事件数是 5000 * 800 400 万条数据量就是 400 万 * 50 字节 ≈ 200MB/s。这个量级对单机 SSD 来说压力很大所以一定要在采样率上下工夫。我实际跑业务时默认配置是全量记录分层事件入口、出口、异常业务的详细 debug 事件采样率设为 10%。这样事件量降到原来的 20%也就是每秒约 40MB单机就扛得住了。# configs/collector.yaml collector: listen: :8080 flush_interval_ms: 100 batch_size: 200 queue_capacity: 65536 storage: data_dir: /data/hindsight segment_hours: 1 compress: true retention_days: 7 sampling: default: 1.0 ops: - op: order.create.debug rate: 0.1这里的日志保留策略也很关键。事件数据不像业务数据库那么金贵通常保留 7 天就够用了。超过 7 天的段文件可以自动清理也可以直接上传到对象存储做冷备。4. 常见问题与排查技巧实录4.1 回放时发现事件缺口怎么办这是我在压测阶段遇到最多的问题。回放一条 trace 时发现事件流中间莫名少了一段导致因果链直接断掉。排查起来其实有规律可循。先看采集端是否发生背压丢事件。环形队列满的时候我的设计是直接丢弃但这会让队列内的旧事件与新事件的因果链断裂。为了定位我把丢事件的元事件单独记录到一条 channel元事件同样入链只不过它标记为 dropped。这样回放的时候虽然少了一段但你能很清楚地看到这里有多少条事件因为背压被丢弃了。另一种可能是上报超时采集端批量线程发送失败后直接把内存中的 batch 丢了。这个坑我也踩过。建议的做法是上报失败时写本地 WAL下一轮 flush 重试重试 3 次再丢弃。用 WAL 做缓冲虽然会消耗一点磁盘 IO但能大幅减少数据缺口。4.2 时钟漂移导致回放顺序混乱有一个经典故障是某台宿主机上 NTP 服务挂了机器物理时间慢了两分钟。于是这台机器上的服务产生的事件sys_time 总是落后于其他机器。回放的时候按物理时间排序就会把这台机器上的所有事件都排到后面看起来像是它的逻辑执行延迟了 2 分钟实际因果链完全错乱。这个问题的根源是 HLC 物理时间不可信。我的处理方案是在事件进入回放引擎之前先做一次时间校准。校准逻辑是统计该 trace_id 内所有事件的 hlc 物理时间偏移量用中位数作为基准把显著偏离基准的事件做标记。回放时对这类事件先用 parent_id 强制修正顺序再展示真实物理时间。排障的人看到时间差异就知道是时钟问题不会误判为服务慢。4.3 存储膨胀导致查询变慢如果上线后不关注存储几个小时后就会发现分段文件大得吓人。每个小时段文件到 2GB 以上查询一个时间区间要扫几个文件回放接口的响应时间从 300ms 涨到 10s。遇到这种情况先检查采样配置是否被业务方偷偷改成了全量。改回来了之后还要检查事件 payload 里是不是塞了大量不需要的字段。有团队把完整的请求报文放进了 payload一个事件就有几 KB压缩都救不回来。我后来在 SDK 里强制加了 Payload 大小校验超过 512 字节直接截断。千万不要轻视这个问题payload 的膨胀是存储爆炸的头号杀手。4.4 parent_id 丢失导致因果链断裂最后一个高频问题parent_id 在跨进程传递时丢失。尤其是服务通过 MQ 消费解耦的场景消息队列中间件不会自动帮你带上 parent_id。消费者从消息里拿到的只有业务参数没有追踪上下文。解决办法是打通消息头的追踪字段约定。比如用 RabbitMQ就在 message properties headers 里塞进 span 上下文用 Kafka就塞进 record headers。消费端在反序列化消息时从 header 里解析出 parent_id 并注入到新的链路上下文。这一步需要在消息中间件的封装层做统一处理不能指望每个业务开发者自己记得。// Kafka 消费端示例 var traceContext extractTraceFromHeaders(consumerRecord.headers()); TraceContext.setParentId(traceContext.getParentEventId());如果实在没传Hindsight 也提供了模糊关联的兜底方案。允许配置一个关联规则比如同一用户 ID 相邻时间窗口的事件自动补充 parent_id。这个方案精度有限但至少能让回放引擎不至于在断链处彻底停下来。5. 基于 Hindsight 的典型场景扩展5.1 微服务排障场景的实际效果我拿一个真实的线上故障来演示一下 Hindsight 的价值。某次促销支付回调服务突然出现大量超时。传统排查只能看到支付服务所在 Pod 的 CPU 升高、下游数据库慢查询增加但具体是哪一个用户请求先触发了慢查询它后续又引发了哪些调用链阻塞很难快速判断。有了 Hindsight 之后我直接按故障时间窗口把这个时段的 trace_id 列表拉出来然后挑出一条典型 trace 做回放。事件流显示用户 A 的请求先调用了优惠券服务优惠券服务因为缓存雪崩走了数据库耗时 800ms。同时这个请求占用的连接资源阻塞了后续多个请求排队效应像雪球一样越滚越大。完全不用猜因果链条清清楚楚。5.2 用户行为路径还原如果把 Hindsight 的采集端接入前端页面或者移动端 SDK它同样可以用于还原用户行为路径。用户在点击按钮 A 之后为什么没有进入下一步是前端校验失败还是后端接口报错又或者是响应太慢导致用户直接退出了前端事件与后端事件之间的桥接只需要一个前端的 userId 或者 sessionId 作为关联键。后端上报事件时把这个关联键放进 payload。回放的时候用户从头端点击、到发出请求、到服务端处理、再到响应返回每一步的时间差和状态变化都能按顺序还原出来。这对于客服排查用户为什么下单失败这类问题效率提升是质的飞跃。5.3 混沌工程注入后的影响评估最后一个扩展场景是混沌工程。我自己做故障注入实验时最头疼的是如何量化故障的影响范围。以前只能看监控曲线看错误率有没有异常升高。现在Hindsight 可以直接对比注入前后的事件流。在注入故障的前后各取一段事件流做 diff哪些事件被阻塞了、哪些调用被降级了、哪些分支走了异常处理一目了然。这个能力做出来之后团队里的同学都开始用它来写混沌实验后的复盘报告比截图一堆监控图有说服力多了。我在搭建 Hindsight 的时候最大的感悟就是不要把可观测性简单理解为加几个监控指标。真正有用的可观测性是能在问题发生时让你以一个上帝视角把整个系统的行为脉络捋清楚。后见之明不是天赋而是设计出来的。最后再分享一个个人经验如果你也想搭建类似的事件回放系统不要一开始就瞄准 HLC、分布式因果链这些重概念先做一个单机版、显式埋点、JSON 存储的 MVP。先让它在你的真实业务里跑通一次完整的故障回放你自然就会明白哪些环节需要优化哪些设计是过度工程。这个项目在实测中稳定吃下了每秒十万级事件已经逐步成为团队里排障的必备武器。
阅读完成 · 觉得有帮助?