本文整理自书籍《SRE 原理与实践》第一部分 概览第1章 介绍不能将碰运气当成战略。——SRE俗语系统管理员模式雇佣系统管理员（sysadmin）运维复杂的计算机系统，是行业内一直以来的普遍做法。传统冲突：研发部门（Dev）关注快速发布新功能；运维部门（Ops）关注避免值班期间发生故障。矛盾根源：绝大部分生产故障由部署变更（新版本、配置修改、流量变化）触发，两个部门的目标本质上互相矛盾。Google的解决之道：SRE核心理念：通过雇佣软件工程师，创作软件系统来维护系统运行，以替代传统模型中的人工操作。团队构成：50%~60%：标准软件工程师。40%~50%：基本满足软件工程师标准，并具备其他技术能力的工程师。SRE特点：对重复性、手工操作有天然的排斥感。有足够的技术能力开发软件系统以替代手工操作。用软件工程的思维和方法论完成系统管理员的任务。终极目标：推动整个系统趋向于无人化运行。经验法则：SRE团队必须将50%的精力花在真实的开发工作上。优势：消除了研发和运维的冲突，促进部门整体水平提高，人员可自由流动，知识共享。DevOps还是SRE？DevOps：是SRE核心理念的普适版，适用于更广范围的组织结构。SRE：是DevOps模型在Google的具体实践，带有特殊的扩展。SRE方法论SRE团队承担以下职责：可用性改进、延迟优化、性能优化、效率优化、变更管理、监控、紧急事务处理、容量规划与管理。确保长期关注研发工作：运维工作限制在50%以内，剩余时间用于研发项目。On-call准则：每8~12小时的轮值期间最多处理2个紧急事件，确保有足够时间正确处理故障、恢复服务并撰写事后报告。事后总结（Postmortem）：包含事故发生、发现、解决的全过程、根本原因、预防或优化的解决方案。原则：对事不对人，目标是发现和堵住漏洞，而非绕过和掩盖。在保障服务SLO的前提下最大化迭代速度：SLO（服务等级目标）：指定服务所提供功能的期望状态。错误预算（Error Budget）：起源：任何产品都不应追求100%可靠（对最终用户而言，99.999%和100%无实质区别）。作用：解决研发团队和SRE团队之间的组织架构冲突。SRE目标从“零事故”转变为共同管理风险，事故成为创新流程中不可避免的环节。管理：只要系统表现高于SLO，研发可继续发版；预算耗尽时，需降低发布速度或优先修复可靠性问题。监控系统原则：不应依赖人分析警报，系统应自动分析，仅当需要用户执行操作时才通知。三类输出：紧急警报（Alert）：需立即执行操作。工单（Ticket）：需执行操作，但非立即。日志（Logging）：平时无人关注，供调试和事后分析使用。应急事件处理可靠性是MTTF（平均失败时间）和MTTR（平均恢复时间）的函数。降低MTTR：通过事先预案并将最佳方法记录在“运维手册（Playbook）”上，可使MTTR降低3倍以上。经过多次演习的on-call工程师比初期几个万能工程师更可靠。变更管理数据：约70%的生产事故由部署变更触发。最佳实践（自动化）：渐进式发布机制（如蓝绿部署、滚动更新）。迅速检测问题。安全快速地回退改动。需求预测和容量规划定义：保障业务有足够的容量和冗余度去服务预测中的未来需求。步骤：准确的自然增长需求预测模型。准确的非自然增长需求来源统计。周期性压力测试，将系统原始资源与业务容量对应起来。主导：SRE应主导容量规划和资源部署过程。资源部署是变更管理与容量规划的结合物，需要小心谨慎地执行。效率与性能责任：SRE负责容量的部署和配置，也必须承担有关利用率的讨论及改进。驱动因素：用户需求（流量）、可用容量、软件的资源使用效率。延迟与容量：负载上升导致延迟升高，延迟升高等同于容量损失。SRE的目标是根据预设的延迟目标部署和维护足够的容量。总结Google SRE代表了对行业现存管理大型复杂服务的最佳实践的一个重要突破。它由“我是一名软件工程师，这是我如何来应付重复劳动的办法”这一简单想法，发展成一套指导思想、方法论、激励方法和独立职业。第2章 Google生产环境：SRE视角本章阐述Google如何将SRE方法论落实在生产环境中，其生产环境本身是一个高度工程化的大系统。硬件数据中心：Google自己设计，从供电、制冷、网络拓扑到计算硬件统一设计。概念区分：物理服务器（Machine）：具体硬件。软件服务（Server）：对外提供功能的软件系统。组织层次：机器 → 机柜（Rack） → 机柜排（Row） → 集群（Cluster） → 数据中心（Datacenter） → 园区（Campus）。硬件故障常态：系统软件将硬件故障对业务的影响隔离掉。管理物理服务器Borg：分布式集群操作系统，负责在集群层面编排任务。职责：接收job、拆分为task、选择物理机、监控、异常重启/迁移。核心思想：任务实例与物理机器无固定对应关系。BNS：名称解析机制，将逻辑名字解析成具体的IP:Port。工程特点：资源声明调度、考虑故障域、资源超限即杀进程。SRE观点：一个缓慢但不断重启的实例，好过一个一直泄漏资源却不重启的实例。存储分层结构：分布式文件服务 → Colossus（集群文件系统） → 数据存储服务（Bigtable、Spanner等）。思路：不同一致性、延迟、吞吐、复制需求，对应不同存储系统，而非一套存储包打天下。网络数据中心内部：自研交换机，Clos拓扑，高带宽（如Jupiter）。数据中心之间：全球骨干网连接，进行带宽承载、路径选择、动态管理、跨区域流量优化。GSLB（全局负载均衡）：贯穿DNS、用户服务、RPC等多个层面，形成分层体系。其他系统软件Chubby（分布式锁服务）：提供类似文件系统的API，处理跨机房一致性。用于主实例选举、存储关键元数据、提供分布式协调能力。监控与警报系统：定期抓取指标，用于报警、版本对比、资源分析和容量规划。软件基础设施目标：最高效地使用硬件基础设施。特征：多线程设计、内置HTTP接口暴露调试信息、大量使用RPC通信（数据格式为Protobuf）。研发环境特点：统一的共享软件仓库、代码评审、分布式编译和测试。结果：研发环境本身也是大规模分布式基础设施的一部分，从写代码起就与生产体系相连。莎士比亚搜索：一个示范服务通过一个全文搜索服务，串起上述基础设施。两部分：批处理：用MapReduce建立索引，写入Bigtable。在线前端：接收用户请求并返回结果。请求链路：DNS查询 → GSLB决策 → Google Frontend（GFE）代理 → 前端服务 → RPC请求后端 → 从Bigtable读取结果 → 返回用户。容量规划：考虑实例数、发布、机器故障、地区流量、成本，设计N+2冗余、跨区域部署和数据副本。总结Google的SRE是建立在一整套统一、工程化的基础设施之上的。第3章 拥抱风险SRE与传统“死守稳定”思维的区别在于主动管理可靠性、成本和创新速度之间的风险。管理风险为什么不是100%可用：边际成本极高。用户对极高可用性感知差别不大。牺牲新功能交付速度。工程资源有限。成本维度：物理资源冗余成本 + 机会成本（工程师做新功能的时间）。度量服务的风险指标：计划外停机时间或用户感知到的不可用程度。两类定义：基于时间：正常运行时间 / (正常运行时间 + 停机时间)。基于请求成功率：成功请求数 / 总请求数（Google更常用，更贴近用户体验）。服务的风险容忍度消费者服务：考虑用户预期、是否影响收入、免费/付费、替代选择、客户类型（企业/个人）。基础设施服务：需满足不同内部客户（如在线低延迟 vs 离线高吞吐）的诉求，通过分层、分池提供不同服务等级。使用错误预算的目的将研发和SRE的争论转为客观数字。构建过程：共同定义SLO → 监控实际水平 → 差值即为剩余错误预算。好处：对齐激励机制。事故不再是“坏事”，而是创新过程中被量化管理的成本。不允许对失败没有度量、边界和约束。第4章 服务质量目标系统解释SLI / SLO / SLA。服务质量术语SLI（服务等级指标）：服务质量属性的可量化表示。如：请求延迟、错误率、吞吐量。SLO（服务等级目标）：针对某个SLI设定的目标值或范围。如：99%的请求在100ms内完成。SLA（服务等级协议）：未达到目标后的后果（如赔偿）。重点：人们常说的SLA，实际讨论的是SLO。指标（SLI）在实践中的应用用户关心什么：不要先看能采集什么，而要先看用户真正依赖什么。指标收集：服务端监控 + 客户端观测（捕捉DNS、浏览器等问题）。汇总：平均值会骗人，应关注分布、百分位、直方图。标准化：将常见SLI模板化，降低理解和配置错误成本。目标（SLO）在实践中的应用定义：必须具体，如“99%的Get RPC在100ms内完成”。选择建议：不要仅依当前状态设目标；保持简单；SLO越少越好但每个都重要；可先宽后严。控制手段：SLO需进入控制回路（监控 → 对比 → 判断 → 执行动作，如扩容、限流、回滚）。建立用户预期：明确SLO可管理用户预期，避免其脑补过高。协议（SLA）在实践中的应用SRE的主要职责是帮助定义可客观测量的指标、评估目标现实性，避免触发惩罚性条款。第5章 减少琐事划定SRE的边界，区分琐事与工程工作。琐事（Toil）的定义运维服务中那些手工、重复、可自动化、战术性、没有长期价值，且会随规模线性增长的工作。特征：手工性、重复性、可自动化、战术性、无持久价值、线性增长。为什么琐事越少越好Google设定目标：SRE至少50%时间花在工程项目上。恶性循环：琐事膨胀 → 无时间做自动化 → 琐事继续增加。什么算作工程工作定义：具有长期战略价值、需要主观判断、能带来持续改进、偏设计和建设的工作。示例：编写自动化工具、构建平台框架、改善可靠性/性能、优化配置管理。琐事繁多是不是一定不好少量好处：带来掌控感、反馈快、压力低。大量坏处：职业发展停滞、士气下滑、角色被误解、人才流失。结论：琐事不可能清零，但必须持续消减。第6章 分布式系统的监控回答什么样的监控体系才适合生产环境。术语定义Monitoring：收集、处理、汇总和展示系统量化数据。White-box monitoring：基于系统内部暴露的状态。Black-box monitoring：从外部用户视角观察行为。Dashboard：展示核心健康指标的页面。Alert：发给某个人或系统的通知。Root cause：修复后可防止同类问题再次发生的根因。为什么要监控观察趋势、版本对比、发现故障、支持调试、容量规划。对SRE最关键的是发现问题并在需要时及时通知。对监控系统设置合理预期原则：监控规则应尽量简单、直接映射故障场景、信噪比高、系统本身可靠易理解。价值在稳定、清晰、可操作，而非“聪明”。现象与原因现象（Symptom）：用户感知到的问题（如网站变慢）。原因（Cause）：导致问题的底层根因（如数据库连接被拒）。准则：报警应优先围绕现象，因为现象更接近用户体验，更能保证报警值得叫醒人。黑盒监控与白盒监控黑盒：擅长证明“用户现在真的受影响了”。白盒：擅长帮助定位“为什么会这样”。关系：现象和原因是相对的，取决于观察层级。4个黄金指标延迟（Latency）：请求耗时。流量（Traffic）：系统承受的负载。错误（Errors）：失败速率。饱和度（Saturation）：资源利用率（应在接近极限前预警）。关于长尾问题平均值无法描述真实用户体验。监控需能看到分位数、直方图和分布。度量指标时采用合适的精度监控本身要有成本意识，非所有指标都需高频率、高精度采集。简化，直到不能再简化持续删除不常用的规则、不展示的数据、未被使用的指标。监控系统本身不能成为运维负担。将上述理念整合起来报警哲学：每条Page都应代表一个真实紧急的问题；收到后必须有可执行动作；报警应少而准；纯机械动作应自动化。监控系统的长期维护监控规则需随软件、负载、架构变化而持续维护。总结健康的On-call体系必须把报警系统控制在一个可承受范围内。监控和报警不是越多越好，而是要为长期稳定服务。第7章 Google的自动化系统的演进Google对自动化的理解：自动化是手段，自治系统才是更高形态。自动化的价值一致性：程序执行比人更稳定可靠。平台性：一次构建，多方受益，暴露平台指标。修复速度更快：显著降低MTTR。行动速度更快：处理人类反应不过来的场景。节省时间：将操作与操作者解耦。自动化对Google SRE的价值Google控制了大部分技术栈，能获取源码，适合将自动化向平台化推进。自动化的应用案例用于新资源创建、服务上下线、部署退役、版本发布、配置修改等。SRE更关心自动化系统的生命周期管理。自动化分类的层次结构纯手工操作。外部维护的系统特定脚本。外部维护的通用自动化系统。内部维护的自动化能力。自治系统（理想形态）：系统自身具备自愈、自调度、自修正能力。让自己脱离工作：自动化所有的东西SRE排斥重复劳动，但需追问：自动化放在哪里，谁来维护，是否与核心系统一致演进。舒缓疼痛：将自动化应用到集群上线中自动化不能与服务脱节，否则流程会重新变慢、变脆弱。使用Prodtest检测不一致情况自动化不仅要做事，还要验证做完后系统是否处于正确状态。幂等地解决不一致情况自动化操作必须考虑幂等性，防止重试导致情况恶化。专业化倾向自动化质量的维度：能力（对不对）、延迟（快不快）、相关性（覆盖主要流程）。以服务为导向的集群上线流程将上线动作本身做成一种服务化能力，从“胶水逻辑”向“平台能力”推进。Borg：仓库规模计算机的诞生Borg改变了抽象层级，管理对象从“某台机器”变成“一整池计算资源”，实现了任务重新调度、自动处理机器故障，是基础设施平台化的一次抽象跃迁。可靠性是最基本的功能自动化也可能出大规模事故。真正好的自动化必须具备：权限控制、审计记录、合理性检查、限速、幂等性、可回退性。自动化是为了更可靠，而非炫技。第8章 发布工程回答如何将变更（最容易制造故障的事）做成可重复、可审计、可加速的工程流程。发布工程师的角色跨领域角色，懂源代码管理、构建系统、自动化工具、测试集成、配置管理，与SRE和研发一起定义发布流程。发布工程哲学自服务模型：每个团队具备自己发布服务的能力，基于统一工具和流程，下放操作权。追求速度：高频发布，每次变更小，测试、调试、回滚更简单，问题定位更清晰。密闭性：构建必须可重现，固定构建工具、依赖库、环境版本。强调策略和流程：关键动作（批准、发版、部署）可控制、可审计。持续构建与部署Rapid系统：承担构建、测试、打包、发版流程编排。构建（Blaze）：定义构建目标和依赖，二进制自描述（时间、版本、标识）。分支：主分支开发，发布分支切出，修复通过cherry-pick同步。测试：源码改动触发测试，发版前对发布分支重新构建和测试。打包：产物放入统一包管理系统，附带哈希、标签、签名。部署：简单服务由Rapid驱动，复杂服务用专门的rollout框架（分阶段、灰度、地域错峰）。发布速度由服务风险决定。配置管理配置是常见事故来源。处理方式包括：配置文件与主分支同步、与二进制同包、单独打包、从外部服务动态读取。每种方式都是在灵活性、可追溯性之间权衡。小结发布工程的核心是让每一次变更都成为可控制、可验证、可回退的工程活动。第9章 简单化没有简单性，就没有真正的可靠性。系统的稳定性与灵活性系统天然是动态的。SRE通过流程和工具在灵活性与稳定性间维持平衡。好的可靠性流程反而能提升灵活性，让错误更快暴露和纠正。乏味是一种美德在生产系统中，“乏味”意味着按预期运行，没有惊喜。必要复杂度：问题本身固有的。意外复杂度：实现方式额外引入的。SRE需持续警惕并消除意外复杂度。我绝对不放弃我的代码无用的代码应被删除，而不是注释掉或加功能开关。注释代码制造噪声，关闭的功能开关是定时炸弹。删除代码本身就是可靠性优化。“负代码行”作为一个指标删除无用代码能降低理解成本、测试成本、缺陷面和资源浪费。最小API小而清晰的API意味着对问题理解更透彻，更易理解、使用、演进和测试。模块化降低变更成本和故障传播范围。模块职责清晰、耦合低，可局部修复、独立演进。扩展到API版本化和数据格式设计（如Protobuf）。发布的简单化小批次发布让每次变更的因果关系更清晰，发布更频繁、更安全。总结稳定不是加更多控制，而是减少不必要复杂度。第10章 基于时间序列数据进行有效报警介绍Google内部监控系统Borgmon的设计思路。Borgmon的起源为适应大规模分布式系统，Borgmon将收集时间序列数据作为第一任务，通过统一规则语言转成图表和报警。应用软件的监控埋点应用需暴露内部状态（如通过/varz接口）。增加监控埋点门槛低：新增变量并暴露即可。监控指标的收集Borgmon根据动态目标列表周期性抓取指标，抓取时间均匀分散。同时生成“合成指标”描述抓取本身是否成功，使监控系统可观察。时间序列数据的存储数据以时间序列（含标签）形式存储在内存数据库，并定期刷到持久化存储。近期数据要快，历史数据要长，可用不同存储层。标签与向量通过标签区分变量名、任务、实例、服务、区域等。支持从单实例到全局的多维度汇总。现代监控系统普遍采用“时间序列+标签”模型。Borg规则计算高层指标（如错误率）通过规则从原始指标推导。底层指标保持通用，业务判断通过规则层表达。报警报警规则为真假判断，需设置最小持续时间以避免抖动。Borgmon将报警交给统一告警管理组件处理去重、合并、路由。监控系统的分片机制采用层级式分片：底层实例抓取指标，上层全局实例抓取汇总值。单点压力可控，汇总逻辑清晰，下钻定位容易。黑盒监控结合白盒（内部指标）和黑盒（外部探针）监控，同时兼顾“是否坏了”和“坏在哪里”。配置文件的维护收集目标和规则分离、支持模板化、规则复用、测试和回归测试。监控系统本身要像软件项目一样被测试和治理。十年之后Borgmon代表了一种成熟监控范式：大规模采集时间序列 → 统一规则汇总分析 → 标签和层级支撑规模 → 模板化和测试保证可维护。第11章 On-call轮值回答如何值班，团队才不会被值班这件事本身拖死。On-call的定位有明确定义的责任人、响应时间、升级路径和恢复目标。值班工程师对恢复结果负责，而非“看过报警”。SRE式On-call和传统Ops的差别传统：故障多就加人，操作多就排班，人扛不住就继续加人。SRE：故障多说明需工程改造，重复操作多说明自动化不够，报警多说明监控设计有问题。On-call是暴露工程缺陷的传感器。50%工程时间原则硬约束：On-call时间 ≤ 25%，其他运维性工作 ≤ 25%，至少50%时间用于工程项目。原因：没有工程时间就修不掉重复问题，值班负担会恶性循环。值班负载的数量平衡轮值频率：不能高到侵蚀正常生活，团队人数要足够覆盖24x7并预留休假和项目时间。Follow-the-sun：全球化服务采用多站点轮值、多时区交接，利用白天覆盖夜间。值班负载的质量平衡关键指标：每次值班期间被打断多少次。经验线：8~12小时窗口内，最好不超过2个真正要介入的紧急事件。影响：一堆“几分钟的小问题”会切碎上下文，降低值班质量。主、副值班模型主On-call：第一时间接警。副On-call：兜底和升级支援。Escalation系统：检查响应情况，必要时升级。避免人力单点故障，减少心理负担。响应时限应与服务的SLO挂钩，值班人的打断成本必须与报警的业务价值匹配。不是所有异常都值得叫醒人。值班人的心理状态高压下人会变笨，易依赖直觉、抓住第一解释、误判因果。值班体系需提供清晰的升级路径、可执行的手册和随时求助的通道。Compensation和可持续性On-call会侵占睡眠、带来压力，健康团队会给予某种形式的补偿，承认这是一项真实负担。报警质量决定值班质量值班过载的根因通常是报警设计太差。理想的报警：收到后知道是否要动作、谁做、不做的后果，并能带出关键上下文。如何缓解值班过载去掉不可操作报警。聚合和去重重复报警。增加最小持续时间抑制尖峰。低优先级事项沉淀为工单。临时让其他团队分担。极端情况下，将支持责任退回开发团队（SRE不无限兜底）。过少的运维压力也会出问题工程师需周期性参与On-call，保持生产环境直觉，否则工程优化易脱离现实。总结On-call必须存在但不能吞掉团队。值班体系可持续的前提，是用工程手段持续消灭值班负担。第12章 有效的故障排查手段将排障从神秘经验拉回到可训练的结构化方法。排障需要的两类能力通用方法论。对具体系统的理解。故障排查的基本闭环观察现象 → 建立假设 → 设计验证 → 排除或确认 → 继续下一轮问题描述越差，排障越慢好的问题报告需包含：期望行为、实际行为、触发时间、复现路径、影响范围。利用历史记录重视历史故障记录，包括上次的排障路径和负结果。保留历史上下文是排障能力的一部分。分诊优先于根因洁癖大故障来临时，优先级是：先恢复服务 → 再保留证据 → 最后做根因分析。常见误区把次级现象当成主要问题。想了一个漂亮但无法验证的理论。过早钻进最复杂的解释。把相关关系误认为因果关系。从信号里找线索指标：看趋势和爆点。日志：看细节和事件序列。分布式追踪：看链路中最慢的一段。Debug页面：看运行期内部状态。真实请求：看用户实际感知。缩小问题空间排障要持续缩小范围：判断是新变更还是旧问题；是客户端、服务端还是依赖层；是单地区还是全局；尝试关闭部分功能看症状是否变化。保留证据事故中的证据（日志、内存状态、配置）会消失，需在可控风险下尽量保留快照。负结果也必须写下来记录查过什么、为什么排除、还剩哪些方向未查，避免重复劳动。App Engine案例案例表明：很像答案的不一定是真相，追踪工具暴露瓶颈但不会自动告诉你根因，复杂解释不天然比简单解释更对。让系统更容易被排障排障能力是系统设计质量的结果。系统应：暴露关键指标、状态可见、请求有唯一追踪ID、变更可追踪、组件边界清晰。第13章 紧急事件响应讲述“真的出事时第一时间怎么做”。第一原则：不要慌稳住节奏，先确认事实，明确当前目标是止血还是定位。紧急响应是平时训练出来的组织真正的质量在事故时暴露：是否有明确流程、值班人是否知道流程、关键人能否快速拉齐、是否习惯记录和演练。事故中先做什么顺序：停止正在放大的伤害 → 恢复用户可见服务 → 再做详细分析。若怀疑某变更，优先暂停、回滚或切回稳定路径。案例一：测试引发的故障不要默认“测试是内部动作所以安全”。测试系统本身需有隔离、限流、快速撤回能力。案例二：配置和发布引发的故障快速确认最近改动，若时间高度重合，先回滚恢复，再确认根因。没有快速回退能力的发布体系是在拿生产做实验。案例三：自动化或流程引发的严重事故自动化系统影响面大、传播快，人可能误以为“正在自愈”。需有紧急停机、手工接管、带外恢复通道。越自动化，越需要人为刹车。共同的成功模式快速识别可疑触发点 → 停止继续变坏 → 找到相关人 → 用降级/切流/回滚恢复 → 将临时经验沉淀为永久修复。事故中要一直记录记录发现时间、谁做了什么、哪步改变了状态、哪些假设已排除。避免回忆不一致、重复尝试、交接混乱。事后别只问“谁干的”每次事故都应成为可传播的经验。训练是为了缩短混乱期，事故处理是组织能力。第14章 紧急事故管理讲多人卷入后，如何将现场从混乱拉回到可管理状态。没有流程时会发生什么大家都在改东西但没人统筹，私聊、开会、另起频道，好心帮忙者增多但变量也增多，沟通成本暴涨，真正信息反而变少。什么时候要“宣布事故”有明显用户影响。需第二个团队一起处理。集中分析很久未收敛。需专门沟通、记录和协调。若1小时左右未解决且影响清楚，应切换至正式事故处理模式。事故指挥官（Incident Commander）不一定是写命令最多的人，但负责：明确当前目标、分配任务、决定人员进出、控制节奏、对优先级拍板。操作负责人（Operations Lead）偏执行面，组织排障和缓解动作，汇报进展，告知方案可行性和风险。是战术层。沟通负责人（Communications Lead）容易被忽略但极其重要。对内统一口径，对外更新状态，挡住无序追问，避免技术骨干被沟通淹没。记录员 / 后勤角色记录时间线、谁在做什么、维护事故文档、准备交接材料。未记录的信息等于不存在。单一控制面使用War Room、IRC、统一会议室，所有关键讨论收敛一处，避免信息分散。维护实时事故文档，写明当前影响、假设、已做/待做动作及责任人。信任被分配的人角色一旦分配，就要让其真正发挥作用。IC不要跳进技术细节，操作负责人不要被多人遥控，沟通负责人不要被绕开。交接必须显式进行交接需明确：当前事实、已排除方向、最优先任务、绝对不要重复的操作、当前风险点。口头说“翻一下聊天记录”等于没有交接。事故管理流程平时也要练可拿中等故障做演练，轮流让不同人担任IC，训练记录和沟通。事故管理是肌肉记忆。总结事故现场不是“更多人同时操作”，而是“更多人按结构化方式协作”。第15章 事后总结：从失败中学习故障处理完，不等于事情结束。为什么必须写PostmortemGoogle体量下事故必然发生。如果不总结，经验无法传播和积累。哪些情况必须写用户可见中断或明显降级。数据丢失或损坏。需On-call人工介入恢复。恢复时间显著过长。监控未发现，靠人发现。一份Postmortem至少要写清什么影响（影响了谁、持续多久、数据风险、临时措施）。发现方式。时间线。缓解动作。根因。后续行动项。根因不是“最近那个失手的人”Blameless原则：默认当事人基于当时信息做判断，真正要修的是系统、流程、工具的漏洞。追问：为什么一个普通失误能穿透到生产？为什么系统没在更早位置拦住？事后总结要写到什么深度反对“人为失误”、“配置错误”等表层答案。需追问：为什么会出现这个配置错误？为什么没被测试挡住？为什么监控没覆盖？协作写作，而不是一个人补作业使用共享文档，相关人补证据、补细节，做正式评审。Postmortem是团队共同构建的事故知识产品。评审时该看什么证据是否充分、时间线是否完整、影响是否写实、根因是否足够深、行动项是否具体、是否值得更广泛传播。行动项必须可执行指向明确问题、有负责人、有截止时间、能验证完成。避免“优化流程”、“加强监控”等空话。奖励好总结，而不是只奖励“没出事”组织要公开支持Blameless、分享优秀Postmortem、鼓励阅读和讨论、奖励写得好的总结和事故处理。Postmortem流程本身也该被优化复盘模板、评审流程、行动项跟进、元数据趋势分析。故障是难免的，但失败若无转化，就白失败了一次。第16章 跟踪故障关注如何把日常故障持续沉淀成可检索、可统计、可改进的生产数据。为什么光有Postmortem还不够Postmortem只覆盖影响特别大的事故。小故障、高频故障、噪音型故障常被忽略。单次事故视角难以发现“全局看很值”的改进项。Escalator：先把“报警有没有人接”管起来职责：跟踪报警响应，在规定时间内无人确认则按路径升级。底座作用显著。Outalator：从“报警”升级到“故障”处理更高层抽象的“故障”。可将多条报警收拢成一个上下文。保存原始报警和处理过程中的邮件往来。支持高亮关键回复。多队列视图很重要SRE常同时处理多个服务报警。多队列时间序列视图能帮值班人重建故障现场，避免信息割裂。“重要回复”机制很实用折叠信息量低的噪音，默认只展开关键信息（如根因猜测、止血动作、变更）。聚合：把“多条报警”还原成“一个故障”目的：将相关报警合并成一个故障对象，降低认知成本。好处：减少重复劳动、避免信息分裂、便于做故障级分析。标签：最值钱的小功能形式：前缀式命名空间，如cause:network、action:rollback。好处：快速记录故障元数据、团队可按自己粒度细化、便于检索和统计趋势。现实：虽有拼写错误等问题，但收益远大于成本。分析：从“记录故障”变成“理解系统”第一层：计数和汇总（每周/每月故障数、哪些服务最吵）。第二层：趋势与对比（报警在变多还是变少、故障密度是否异常）。第三层：语义级分析（哪类基础设施引起最多故障、哪些机制过敏）。不要只看“故障次数”故障多不一定修起来最值，故障少也不一定影响小。统计是分析的入口，不是结论。报告、交接与评审Outalator可生成事件摘要，支持交接。交接最怕只传结论不传上下文，或只传现象不传已做动作。意外收益还可用于记录高权限账号使用、关键周期任务执行等，成为运行期异常事件知识库。总结Postmortem解决“单次事故要学什么”；故障跟踪解决“整体运维到底在被什么反复消耗”。第17章 测试可靠性核心思想：没有亲自验证过的东西，先假设它是坏的。测试为什么直接关系到可靠性对未来可靠性的信心来自：过去的监控数据 + 对未来变更的控制。测试是在证明“变化前后某些关键性质未被破坏”。每多一个有效测试，就少一块未知风险区域。监控给你事实，测试给你前置把关测试的价值在于把Bug拦在生产前。某些Bug的MTTR可因测试变为0。测试、MTTR和MTBF的关系更多有价值的测试 → 更多Bug在发布前被挡住 → 生产低级错误变少 → MTBF拉长 → 团队发布更快。测试越好，改动空间越大测试覆盖度越高，每次改动不确定性越低，团队越敢持续修改系统。
SRE 团队转型与业务接手实践指南本文整理自书籍《SRE 原理与实践》"可靠性管理能力"，聚焦团队转型、新人培养、以及以 SRE 方式接手业务三大核心主题，帮助你理解如何从传统运维团队走向工程化的可靠性团队。主要是和大家分享一下如何培养新人和新人如何学习的一些方法吧！一、为什么要强调团队转型专业的 SRE 团队职责及工作方法与传统运维团队有较大不同。SRE 的使命是提升产品在线持续运行的可靠性/稳定性，很多以传统运维为主的团队都在寻求转型为 SRE 团队的方法。团队转型包括几种类型：培养原先做技术运维的成员转型为 SRE；招聘新人从零开始培养为 SRE；把运维团队整体转型为 SRE 团队，优化团队能力结构，培养和引入可靠性工作所需要的多个方向的工程师。运维工程师与 SRE 的核心区别传统运维工程师偏重运维操作，以满足业务需求为主；而 SRE 以保持和提升系统可靠性为主要职责。两者的岗位职责和工作目标大为不同：维度传统运维工程师SRE核心职责运维操作，满足业务需求保持和提升系统可靠性工作性质偏被动响应较高比重的主动性工作工作方法依赖个人经验手工操作工程化方式改变架构设计能力抓手—感知、反脆弱、保障、快恢故障参与参与处理主导应急协同，参与故障生命周期管理SRE 会使用运维技术，但使用目的是加强可靠性，使用方法是用编程的方式，运维操作只是 SRE 日常工作的一部分。从运维工程师转型为 SRE，既是互联网平台提升可靠性稳定性的要求，也是运维工程师职业发展的需要。平台、团队与个人都面临转型问题，而且必须是体系化的转型——运维工作会逐渐实现自动化，部分工作由产品研发工程师通过系统执行，部分工作交给可靠性工程师来执行，SRE 工程师应进行更多可靠性工程工作。二、运维工程师如何转型为 SRE1. 通过学习提升能力，找到发展方向一个优秀的 SRE 需要具备多方面的能力，以及专业的系统运维技能。如果 SRE 同时具备软件开发能力、产品架构设计能力（初级阶段可以是理解架构的能力）、沟通协调能力、项目管理能力等，将非常有优势。SRE 的能力模型应该是"一专多能"：有自己的专业方向（如软件开发、基础设施、系统架构等）；技术广度同样重要，只有这样才能熟练应用可靠性技术、专业能力和综合能力完成可靠性工作；在专业方向深入耕耘的同时，在其他技术面也要达到一定广度和深度。2. 从度量所负责业务的可靠性开始SRE 和业务方（包括产品、业务负责人、SRE 团队负责人）一起找到业务核心技术指标并进行度量，形成可靠性 SLO。通过 SLO 这个抓手：了解现状、分析风险、暴露问题；驱动产品研发、基础团队、SRE 团队一起改进；开展可靠性工程领域内的各项工程活动（提升感知能力、保障能力、快恢能力等）；把改进后的能力持续反馈出来，分析项目效果；持续分析新的业务现状和风险，逐步建立自己在业务质量和可靠性方面的权威性。3. 对故障生命周期负责SRE 要在故障的整个生命周期内积极主动推动各项可靠性工作，而不仅是参与处理故障。在过程中不断提升对故障发生、发现、定位、处理、总结、改进等环节的理解力和掌控力。三、如何将新人培养为 SRE成为 SRE 需要掌握系统性、累积型的学习方式，也需要有一套成长手册，而非向运维操作方向成长。1. 团队与导师SRE 团队的新人要在各业务运维组中轮训，在小组内安排一位导师负责试用期间的指导工作（一个新人配一位导师）。2. 熟悉业务，学习知识和技术新人进来首先要熟悉业务，了解业务使用的技术，与其他资深 SRE/产品研发工程师沟通，学习业务：熟悉 CMDB、架构文档，了解用户请求是如何进入系统的；从前端、中间、后端到数据库、存储，对整个架构进行反向工程；自己整理文档——每个新人要为团队撰写新文档以及完善现有文档，不仅有利于传承知识，而且有利于激发新人的探索精神；深入研究一个具体的问题，对架构、感知、脆弱性、快恢、保障能力中的某一方面研究透彻；通过统计学、比较思维学会数据运营，学习监控技术，熟悉监控系统。3. 系统性的培训如果没有系统性的培训，新人会在混乱、庞大的信息量和无边的未知信息中惶恐工作，随时提心吊胆、不敢工作、工作效率低，经常找导师咨询问题，不仅成长速度慢，还会影响导师的工作。系统性培训包括以下几个方向：(1) 学习监控认识常见的监控指标，包括 SLI 监控、基础监控（系统、网络）、业务层监控（APM 上报、服务调用、服务日志）、DB/缓存监控。可安排日常基础监控的处理任务帮助新人加深理解。学习常用的监控上报流程：监控程序 → 上报服务 → 存储 → 告警规则 → 展示 → 收取告警。目的是熟悉整体流程及每个组件的监控感知能力，了解如何发现异常。可安排任务：为系统增加一个指标监控、编写监控程序、取出数据告警判断、配置告警、发送并收取告警、在监控系统配置指标视图等。(2) 了解发布变更了解发布平台、变更系统的使用及发布流程，学习执行发布工作、检测发布结果；学习服务的上下线流程（应用构建 → 流量摘除 → 发布 → 流量接入），学习弹性扩容组代码，熟悉弹性过程；新人可在熟悉配置的基础上尝试做些变更（增加监控、增加运维系统新功能、增加小的自动化功能），待掌握后逐步扩展到独立完成升级、扩容等变更动作。(3) 学习产研技术框架及基础设施与平台学习框架的架构原理及日常的问题处理：了解生产的技术架构、高可用设计情况，以及出现问题时框架的处理方式。学习产研开发框架文档、处理框架常见问题、基于框架的故障定位和常规运维操作。熟悉基础设施、基础软件：学习基础设施架构、机房 IDC 信息，熟悉公有云/私有云等云产品和云平台，熟悉运维资源管理、交付流程，学习常用中间件（负载均衡、缓存、队列等）。(4) 学习稳定性保障体系熟悉业务生产技术架构、部署架构、技术流程——清晰每个业务流程的流量路径（接入、流量路由、服务上下游转发等），熟悉每个服务当前的监控项与黄金指标的关系。参与故障处理：阅读故障报告并做简单复盘，学习常规问题排查流程。分为 4 个阶段：导师讲解处理案例新人听 → 导师处理问题新人看 → 新人独立参与简单问题处理 → 高阶过程（故障处理分角色模拟演练、参与真实故障复盘、单独完成故障报告）。4. 个人学习工程师通过学习故障报告可以学习到如何理解系统、如何处理故障的知识。通过学习文档、熟悉集群部署、熟悉架构设计思路可以更好地理解系统如何工作，但这些都只是纸上谈兵；从实际故障中去调查分析系统为什么不能正常工作，是更有效的理解系统的方式。特别是对于新人来说，学习故障报告无疑是快速理解系统的途径，也能让新人快速进入业务稳定性保障角色和状态。四、运维团队转型为 SRE 团队可靠性工程工作表面看是保证业务可靠性、稳定性，一种更好的理解方式是集合各种技术能力，形成一种工程化的方式来保证业务持续稳定运行。目前很多公司的 SRE 团队还是以运维为主，包括业务运维/SRE、基础运维、数据库运维、监控团队等。1. 团队结构SRE 团队需要具备多方面的能力，一个人很难短期内拥有全部技能。SRE 团队要依靠团队的力量：团队内要拥有掌握各种技能的工程师；单个人搞不定的事情，发挥团队其他成员的力量；单个团队搞不定的事情，跨团队协调资源搞定。团队中应该有各方面的专家：管理人员、运维工程师、架构师、SRE 研发工程师、基础组件研发工程师、项目经理、AIOps、数据分析师等角色，并能在特定项目中吸收专业人士参与。2. 在团队内部培养软件工程风气大多传统运维团队习惯于手工解决一个个具体的运维问题——通过登录服务器执行运维操作来分析、排查、解决异常和故障问题。这种工作方式依赖于工程师个人技术经验，效率不高。要培养软件工程风气：鼓励可靠性工程师也具备软件设计开发的能力，把自己同时定位为软件工程师，通过软件工程化的方式解决运维问题；鼓励把实践经验和操作方法通过软件编程形成工具或系统，做成通用型解决方案，解决可靠性相关的架构设计、感知观测、诊断定位、容量、快恢能力问题等；团队要给 SRE/运维工程师留出时间来学习和参与工程开发工作，并提供指导；《SRE：Google 运维解密》中讲到运维工作只能占 SRE 工作时间的50%，另外 50% 要用来开发工具、解决问题；要求 SRE 必须参与工具开发，运维工具开发人员必须参与日常运维工作。3. 建立应急协作的机制要建立机制而不是靠运维工程师的个人经验和能力：充分利用数据、度量和分配可靠性指标对稳定性进行度量，实时感知；确定稳定性目标，主动感知发现和应对，而不是在收到用户投诉或告警时才被动处理；机制应该明确各个岗位要求、能力要求，建立起流程体系。4. 与研发深度协作、参与到系统架构设计中强调稳定性是运维工程师与业务研发工程师的共同责任；可靠性是设计出来的，很多问题属于架构设计问题，不应该由运维工程师靠人力来负责；系统是共同建设的——业务研发团队负责上层系统，运维团队负责基础设施硬件、软件基础设施、基础服务、云服务等；SRE 应该和产品研发人员建立良好互动关系，如参与生产会议、共同承担可靠性工作目标、明确职责分工并共建可靠的系统。5. 团队管理人员为团队找到可靠性的改进方向SRE 有很多方向可以做，团队负责人要为团队指明方向：确定可靠性的改进项目，取得成果并获得 SRE 团队各成员和业务产品研发人员的认可；改进项目是提升系统可靠性的关键工程活动，也是 SRE 的主要工作；可靠性工作方向是阶段性、经常变化的，每个季度每个业务的目标可能都在变化，所以需要团队管理人员有较强的判断能力和把控能力。五、以 SRE 方式接手现有业务接手一个现有业务的可靠性工作是经常出现的场景，如公司内部业务调整、SRE 工程师职责调整、入职新公司接触到的业务等。以下是用 SRE 方式接手业务的过程。第 1 步：了解业务产品、人、背景信息了解业务：以用户视角去了解业务，以开发者视角去了解网站/App 结构、系统结构，初步了解技术原理和流程。了解业务和服务的作用、解决了什么问题，了解业务在公司的重要程度以及目前的可用性。了解人：跟研发负责人沟通，了解与业务相关的人（现有的 SRE/运维工程师、对应的研发团队、研发负责人、测试负责人、产品负责人、运营负责人等），建立沟通渠道，了解可靠性现状。了解背景信息：业务在公司的商业价值、重要程度、老板重视程度。分析可靠性目标：近期是否已经达标，或者有需要提升的目标。了解可靠性现状和业务期望，与产品研发负责人甚至业务负责人沟通，了解核心问题和对稳定性的期望。第 2 步：熟悉业务架构通过阅读文档、业务串讲等方式熟悉业务架构、技术架构：获取架构设计文档、运维文档，初步了解服务应用架构、基础软件等；请研发负责人概要介绍产品、业务架构和逻辑、部署方式、技术栈等；根据架构文档和产研负责人的介绍，进行服务梳理，再回过头理解并消化业务架构、技术架构；验证服务是否符合公司目前的标准部署和运维方式。第 3 步：进一步熟悉部署架构，掌握运维资源通过 CMDB、监控系统、管理后台等梳理现有的软硬件资源（主机、域名、数据库、缓存、云服务等）及其在运维管理系统中的情况；对于运维系统化建设比较薄弱的公司，需要把所有主机、DB、应用等弄清楚，加好权限，自己整理一份列表或核对文档是否正确；如果接维时已有 SRE/运维工程师，可请他们介绍目前的运维工作，了解生产服务现状、主要工作、压力来源，找到可能的导火索、风险点；了解目前的紧急事件排序、紧急事件处理流程，甚至更细节的进程启动方式、监控方法、进程连接关系、部署架构等。第 4 步：熟悉稳定性保障体系，尤其是现有监控查看最近的故障报告、Bug 邮件、工单列表、事后总结等了解最近的故障及产生原因；分析业务稳定性的现状（从故障次数、故障时长、严重程度、各方关注程度等方面），了解大家改进的动力和瓶颈所在；查看与感知能力相关的监控告警的覆盖情况，以及过往故障处理过程中的监控表现情况。第 5 步：处理当前问题梳理业务当前问题，找出影响稳定性的主要问题。根据业务当前阶段（初创、发展、成熟、衰退等），制定不同的可靠性目标和工作策略。第 6 步：推进改进梳理改进事项，列出改进清单，排优先级，选取高价值、低成本的改进项优先落地，逐步推进。六、以 SRE 方式接手新业务对一个新业务来说，SRE 在早期介入会省事很多。如果有机会介入业务早期，SRE 可以从以下工作入手：1. 参与组件选型早期介入有利于推广统一的组件。SRE 对公司的基础设施、基础软件应该是非常清楚的，而很多产品研发人员关注面没有那么广。在使用组件时，建议优先使用公司内部统一的组件，或其他团队使用的较为成熟的组件，而非引入多个不同的组件。2. 参与资源准备与部署架构的设计在早期帮助业务准备基础资源，SRE 可以在项目早期完全熟悉基础资源，也可以在资源准备、采购、交付、部署等流程中进行把关，在早期预防一些可靠性风险；SRE 参与早期的部署架构设计，有利于尽早实现基础设施层的高可用和标准化；在架构评审过程中，如果 SRE 具备架构能力，就能够参与、了解甚至评估业务架构中不合理的地方，在高可用、可靠性、安全性方面提出可靠性的要求，在早期规避风险。七、本章小结SRE 的使命是以工程化的方式保证和提升业务的可靠性。通过结合 SRE 的工作方法和工程能力，可以更高效地建设可靠性。团队转型是基础——从运维操作驱动转向工程化驱动，从个人经验依赖转向机制体系保障；新人培养是传承——通过系统性的培训路径让新人快速进入角色；业务接手是实践——无论是现有业务还是新业务，SRE 都应该以度量驱动、工程化推进的方式开展工作。核心一句话：可靠性是设计出来的，也是工程化建设出来的，不是靠人力堆出来的。
用 ERROR 日志做告警：低成本高 ROI 的兜底监控实践作为服务的研发、运维人员，你会给服务配置哪些告警规则？机器性能相关的？很多人首先想到机器的 CPU、内存、磁盘、网络、IO 相关的指标，虽然在现代微服务架构下，这些指标的重要性下降了，但是这些指标确实比较容易配置，能够给你安全感。但这不是我今天要聊的。服务的SLI指标？如果对 Google SRE 稍微有点了解，大概率会听过 Google 四个黄金指标、RED 这样的方法论，必然不会遗漏 SLI 指标，这也不是我今天要聊的。我想提醒大家的是：服务的ERROR日志数量！很多人遗漏了这个监控告警，原因可能是：日志告警工具缺失日志不规范不好做告警配置已经有了SLI等重要指标了，觉得够用了统计服务的ERROR日志数量并做告警，ROI极高，大道至简，我来讲讲原因。驱动你建立日志中心化收集、ETL、告警链路指标体系相对容易，Prometheus、VictoriaMetrics 等几乎是每个公司的标配，指标的告警也相对容易，所以大家一般都是有的。在指标领域，有问题的通常不是工具，通常是业务研发只在意 CRUD，对程序稳定性、可观测性等关注较少导致的 SLI 缺失。日志通常是后于指标体系的，各个公司的建设通常没那么完备。好在 ELK、Loki、VictoriaLogs 等工具的流行度越来越高，用这些开源项目完全可以把日志体系搭建起来。驱动研发梳理日志级别ERROR日志的告警，通常只需要根据级别过滤，比如告警规则配置（以VictoriaLogs举例）为：_time: 5m AND _stream:{module="a"} AND level:ERROR | stats count(*) error_total | filter error_total:>0只要发现 ERROR 级别的日志，就告警。这会让乱打日志级别的情况得到一定的治理。有些研发不太在意日志级别，有时即便是用户参数错误也会打印 ERROR 日志，这给故障排查带来很多噪音。通常来讲：只有确实影响了服务运行的情况才打印 ERROR 日志。个别用户的问题通常只需要打印 Warning。清晰的日志级别，可以大幅减少排障时的噪音干扰。加速故障定位。要求使用 level:ERROR 这样的字段过滤，要求日志提前做好 ETL，驱动研发人员采用更结构化的日志格式。目前来看，JSON 格式的日志已经成为可观测性领域的共识。指标告警的兜底指标告警很重要，但有时容易漏掉一些问题。使用 ERROR 日志作为兜底，非常关键。因为通常来讲，只要代码逻辑遇到问题，一定会打印 ERROR 日志，所以一般问题都可以通过这一条告警规则命中。
宽事件实践指南采用宽事件（Wide Event）式的监控手段，这一改变缩短了对所有变更的反馈周期，让系统调试变得异常轻松，那些曾让人望而却步的系统，也突然变得易于管理。简单来说，对于系统中的每个工作单元（通常是HTTP请求/响应，但并非绝对），你只需输出一个“事件”，并在其中包含该工作单元相关的所有可收集信息。在遥测领域，“事件”是一个含义宽泛的术语，你也可以根据喜好将其替换为“日志行”或“追踪跨度（span）”，它们在本质上是相同的概念。查里蒂·梅杰斯（Charity Majors）近期将这种方法称为“可观测性2.0”（Observability 2.0）并大力推广，为该概念注入了新的活力，但实际上这并非全新理念。布兰德·利奇（Brandur Leach）早在2016年就在其个人博客中提及“标准日志行”（Canonical Log Lines），2019年又介绍了Stripe公司对该概念的应用。此外，亚马逊云服务（AWS）多年来也一直将其列为最佳实践。好的……大概理解了这个概念，但该如何实践“宽事件”呢？这正是许多开发者感到困惑的地方。从理论上看，这个理念很棒，我们确实应该尝试！但现实是，还有一堆功能等着开发上线，有个bug让我夜不能寐，昨天还冒出30款新的人工智能工具需要学习。而且，到底该从哪里入手？又该添加哪些数据呢？与软件领域的其他事物一样，实现宽事件的方法有很多种。接下来，我将分享一种对我行之有效的实践方案，内容涵盖工具与代码层面的实现思路、一份详尽的属性添加清单，以及对该方法常见质疑的解答。本文将重点聚焦于Web服务场景，但这套思路同样适用于其他各类工作负载。第一步：选择合适的工具我们需要借助工具来为代码添加监控（通过追踪数据或结构化日志行），同时还需要一个接收这些监控数据的平台，以便进行查询和可视化分析。理想的工具应支持快速迭代式的数据查询。我个人推荐使用Honeycomb，不过，只要是基于现代OLAP（在线分析处理）数据库构建的可观测性工具，在紧急情况下都能满足需求，例如：Honeycomb：配备Retriever功能DataDog：配备Husky功能New Relic：配备NRDB功能Baselime：基于ClickHouse构建SigNoz：基于ClickHouse构建Honeycomb、New Relic和DataDog均自行研发了列式OLAP数据存储。如今，随着ClickHouse、InfluxDB IOx、Apache Pinot和DuckDB等工具的普及，各类新的可观测性工具也在不断涌现。若不受环境限制，我强烈建议优先选择OpenTelemetry搭配Honeycomb，这会让后续工作更高效。即便你身处企业环境，且对2010年后出现的技术有所抵触，也可以在紧急情况下使用ElasticSearch这类日志搜索工具。Stripe的博文中还介绍了如何使用Splunk实现类似功能。无论选择哪种工具，都需要熟练掌握以下3种核心技巧，以便筛选事件数据。你运用这些技巧进行迭代查询、挖掘数据价值的速度越快，就越能高效排查问题，深入了解系统的实际运行状态。当可观测性领域的从业者提及“数据切片分析”时，通常指的就是这一系列操作。下文将使用一种自定义的SQL方言来示例查询语句，你可以在所用工具的查询语言中找到对应的实现方式。1. 可视化（Visualizing）人类的身体存在诸多局限，但视觉皮层在识别模式方面能力极强。要充分发挥这一优势，需熟练掌握系统数据的可视化呈现方法，常用的统计函数包括COUNT（计数）、COUNT_DISTINCT（去重计数）、HEATMAP（热力图）、P90（90分位数）、MAX（最大值）、MIN（最小值）以及直方图（Histogram）。请务必熟悉所用工具提供的各类图表功能，勤加练习，提升操作速度。示例：Honeycomb热力图截图示例：Splunk直方图截图2. 分组（Grouping）为宽事件添加的每一个新标注，都会成为一个可用于数据切片的维度。通过GROUP BY语句，我们可以沿着该维度分析数据，验证数据值是否符合预期。GROUP BY instance.id -- 按实例ID分组GROUP BY client.OS, client.version -- 按客户端操作系统和版本分组3. 筛选（Filtering）当锁定某个感兴趣的维度后，通常需要进一步深入分析数据。通过筛选，可将关注点缩小到特定的流量片段，例如某一个端点、某一个IP地址、来自iOS应用的请求，或是开启了特定功能标志的用户请求。WHERE http.route = "/user/account" -- 筛选HTTP路由为“/user/account”的请求WHERE http.route != "/health" -- 排除HTTP路由为“/health”的请求WHERE http.user_agent_header contains "Android" -- 筛选用户代理头包含“Android”的请求第二步：编写中间件提供支持若你使用的是OpenTelemetry SDK，它会自动为请求和响应创建一个包裹式的追踪跨度（span）。在请求处理过程中的任意时刻，都可以通过获取当前活跃的跨度来访问它：let span = opentelemetry.trace.getActiveSpan(); span.setAttributes({ "user_agent.original": c.req.header("User-Agent"), // 设置用户代理原始信息属性 });但需要注意的是，若有代码将你的代码包裹在子跨度中，“当前活跃跨度”会自动切换为这个新的包裹式子跨度！在OpenTelemetry中，目前并没有直接定位到原始“主跨度”的官方方法。不过，我们可以通过一个变通方案解决：在上下文（context）中保存对该特定主跨度的引用，确保随时都能访问到“主包裹跨度”。// 创建一个上下文键，用于在OpenTelemetry上下文中存储主跨度 const MAIN_SPAN_CONTEXT_KEY = createContextKey("main_span_context_key"); function mainSpanMiddleware(req, res, next) { // 获取HTTP监控自动创建的当前活跃跨度 let span = trace.getActiveSpan(); // 获取当前上下文 let ctx = context.active(); // 为主跨度设置固定属性（标识其为主跨度） span.setAttribute("main", true); // OpenTelemetry上下文是不可变的，需创建新上下文并添加主跨度 let newCtx = ctx.setValue(MAIN_SPAN_CONTEXT_KEY, span); // 在新上下文生效的范围内执行后续请求处理逻辑 context.with(newCtx, () => { next(); }); } // 封装一个便捷函数，用于为主跨度添加属性 function setMainSpanAttributes(attributes) { let mainSpan = context.active().getValue(MAIN_SPAN_CONTEXT_KEY); if (mainSpan) { mainSpan.setAttributes(attributes); } }这样一来，添加属性的代码会更加简洁，且能确保属性始终设置在主包裹跨度上：setMainSpanAttributes({ "user.id": "123", // 用户ID "user.type": "enterprise", // 用户类型（企业用户） "user.auth_method": "oauth", // 认证方式（OAuth） });你可以查看这个最小化的运行示例，了解实际效果。在Heroku，我们内部的OpenTelemetry发行版会自动完成上述配置，并为主跨度自动添加尽可能多的标注。若你未使用OpenTelemetry，这份代码片段或许能帮你快速上手，我的上一篇博文也提供了相关逻辑的整合思路。第三步：为“主跨度”添加哪些内容？以及，你计划在宽事件中包含多少维度的数据呢？答案是：越多越好，甚至可以达到数百个维度！维度越丰富，就越能精准地检测和关联各类罕见情况。当你体验到借助丰富上下文调试问题的便捷后，会自然而然地希望在所有场景中都能应用这种方式。我们需要为请求添加各类属性，其数量往往会超出你的预期。轻松列出十几个属性并不难，而在一个监控完善的代码库中，属性数量可达数百个。需要说明的是，下文列出的清单虽已十分详尽，但并非毫无遗漏。OpenTelemetry定义了多组属性名称作为语义规范（Semantic Conventions），也可为属性设计提供参考。在命名时，我会尽可能遵循这些规范。用于筛选的标识规范追踪数据中包含大量跨度，因此需要一套规范来识别和搜索这些“宽事件”。曾有人提议使用“root”（根）或“canon”（标准）作为标识，但我最终选择将这类跨度命名为“main”（主）跨度。属性（Attribute）示例（Examples）描述（Description）maintrue仅在“宽事件”对应的跨度中存在，通常用于包裹请求/响应或后台任务借助这一规范，只需一条查询语句，就能快速了解“某个服务的流量情况”：SELECT COUNT(*) -- 统计请求总数 WHERE main = true -- 筛选主跨度（宽事件） GROUP BY http.route -- 按HTTP路由分组示例：按路由分组的一周流量图表，其中存在异常波动1. 服务元数据（Service metadata）首先，需要添加服务相关的基础信息。此外，还可考虑补充团队归属、团队Slack沟通渠道等元数据，但需注意：若公司频繁进行组织架构调整，这类信息的维护会较为繁琐。至于如何将这些信息与Backstage等服务目录关联，可根据实际需求自行探索。属性（Attribute）示例（Examples）描述（Description）service.nameapi、shoppingcart服务名称service.environmentproduction（生产环境）、staging（预发环境）、development（开发环境）服务运行环境service.teamweb-services、dev-ex服务归属团队，便于故障时确定联系人service.slack_channelweb-services、dev-ex服务相关问题的沟通渠道（Slack频道）查询示例：每个团队负责多少个服务？SELECT COUNT_DISTINCT(service.name) -- 统计唯一服务数量 WHERE service.environment = "production" -- 限定生产环境 GROUP BY service.team -- 按团队分组2. 实例信息（Instance info）当你查看系统负载时，是否曾疑惑“当前负载与服务器配置是否匹配”？以往可能需要切换到其他工具或查阅配置文件才能获取服务器信息，而将这些上下文信息添加到宽事件中，就能在需要时随时查看。属性（Attribute）示例（Examples）描述（Description）instance.id656993bd-40e1-4c76-baff-0e50e158c6eb服务实例的唯一标识IDinstance.memory_mb12336服务实例可用内存大小（单位：MB）instance.cpu_count4、8、196服务实例可用CPU核心数instance.typem6i.xlarge云服务提供商对实例类型的命名（如AWS实例类型）查询示例：哪些服务占用内存最多？它们使用的是哪种实例类型？SELECT service.name, -- 服务名称 instance.memory_mb, -- 实例内存 instance.type -- 实例类型 ORDER BY instance.memory_mb DESC -- 按内存降序排列 GROUP BY service.name, instance.type LIMIT 10 -- 取前10条结果3. 容器/编排环境信息（Orchestration info）无论采用何种系统编排方式，都需确保添加所有相关信息。下文列出了Kubernetes语义规范中的部分示例，供你参考：属性（Attribute）示例（Examples）描述（Description）container.ida3bf90e006b2Docker容器的唯一IDcontainer.namenginx-proxy、wordpress-app容器运行时使用的容器名称k8s.cluster.nameapi-cluster服务所在的Kubernetes集群名称k8s.pod.namenginx-2723453542-065rx服务所在的Kubernetes Pod名称cloud.availability_zoneus-east-1c服务运行所在的可用区（AZ）cloud.regionus-east-1服务运行所在的区域（Region）即便你使用的是PaaS（平台即服务），也能提取到许多有用信息，例如Heroku相关信息：属性（Attribute）示例（Examples）描述（Description）heroku.dynoweb.1、worker.3应用运行时的Dyno标识（来自环境变量DYNO）heroku.dyno_typeweb、workerDyno类型（取DYNO环境变量中“.”之前的部分，便于查询）heroku.dyno_index1、3Dyno序号（取DYNO环境变量中“.”之后的部分，便于查询）heroku.dyno_sizeperformance-m所选Dyno的规格大小heroku.spacemy-private-space应用部署所在的私有空间名称heroku.regionvirginia、oregon应用所在的区域查询示例：当前运行着多少个Dyno？它们属于哪些类型？分别对应哪些服务？SELECT COUNT_DISTINCT(heroku.dyno_index) -- 统计唯一Dyno序号数量 GROUP BY service.name, heroku.dyno_type, instance.type -- 按服务、Dyno类型、实例类型分组4. 构建信息（Build info）在故障排查中，人们常会问“是否刚发布过新版本？”或“发生了哪些变更？”。无需切换到部署工具或查阅GitHub仓库，只需将构建相关数据添加到遥测信息中，即可快速获取答案。将构建系统中的数据传递到生产环境并在运行时可用，可能需要编写不少衔接代码，但在故障排查时，这些信息的价值不可估量。属性（Attribute）示例（Examples）描述（Description）service.versionv123、9731945429d3d083eb78666c565c61bcef39a48f服务版本标识（如版本号或镜像哈希值）service.build.idacd8bb57-fb9f-4b2d-a750-4315e99dac64构建ID（便于故障时审计构建过程）service.build.git_hash6f6466b0e693470729b669f3745358df29f97e8d部署代码的Git提交SHA值（精确定位运行代码版本）service.build.pull_request_urlhttps://github.com/your-company/api-service/pull/121触发部署的合并请求（PR）链接service.build.diff_urlhttps://github.com/your-company/api-service/compare/c9d9380..05e5736新旧部署版本代码对比链接service.build.deployment.at2024-10-14T19:47:38Z部署流程开始的时间戳service.build.deployment.userkeanu.reeves@your-company.com触发构建的认证用户（可能是机器人）service.build.deployment.triggermerge-to-main（合并到主分支）、slack-bot（Slack机器人）、api-request（API请求）、config-change（配置变更）部署触发原因（对部署相关故障排查至关重要）service.build.deployment.age_minutes1、10230部署版本的存活时间（单位：分钟），可快速回答“是否刚发布过新版本”疑问：这些数据会不会存在大量重复？毕竟它们只会在部署时发生变化！（详见“常见质疑”部分解答）查询示例1：近期有哪些系统进行了部署？SELECT service.name, -- 服务名称 MIN(service.build.deployment.age_minutes) as age -- 部署版本的最短存活时间（即最新部署） WHERE service.build.deployment.age_minutes < 20 -- 筛选20分钟内的部署 GROUP BY service.name ORDER BY age ASC -- 按存活时间升序排列（最新部署在前） LIMIT 10查询示例2：上一次部署后，500错误为何突然激增？SELECT COUNT(*) -- 统计请求总数 WHERE service.name = "api-service" AND -- 限定服务名称 main = true -- 筛选宽事件 GROUP BY http.status_code, service.version -- 按HTTP状态码和服务版本分组示例：按HTTP状态码和版本分组的请求图表，v1版本停止服务时500错误激增5. HTTP相关信息（HTTP）大部分HTTP相关属性可通过追踪库的监控功能自动获取，但如果你的组织使用非标准请求头，仍可手动添加更多属性。不要满足于OpenTelemetry默认提供的基础属性！属性（Attribute）示例（Examples）描述（Description）server.addressexample.com、localhost接收请求的HTTP服务器名称url.path/checkout、/account/123/features域名后的URI路径url.schemehttp、httpsURI协议方案（如HTTP或HTTPS）url.queryq=test、ref=####URI的查询参数部分http.request.id79104EXAMPLEB723平台请求ID（如x-request-id、x-amz-request-id）http.request.methodGET、PUT、POST、OPTIONSHTTP请求方法http.request.body_size3495HTTP请求体大小（单位：字节）http.request.header.content-typeapplication/json特定请求头的值（此处以“content-type”为例，可根据服务需求选择重要请求头）http.response.status_code200、404、500HTTP响应状态码http.response.body_size1284、2202009HTTP响应体大小（单位：字节）http.response.header.content-typetext/html特定响应头的值（此处以“content-type”为例，可根据服务需求选择重要响应头）查询示例：SELECT HEATMAP(http.response.body_size), -- 生成响应体大小的热力图 WHERE main = true AND service.name = "api-service" -- 限定服务名称示例：响应体大小热力图，大部分响应体大小处于固定区间，但存在明显异常值，需进一步排查6. 用户代理信息（User-Agent）User-Agent请求头包含丰富信息，无需在后续分析中通过正则表达式解析，应从一开始就将其解析为结构化数据。属性（Attribute）示例（Examples）描述（Description）user_agent.originalMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.3HTTP请求头中User-Agent的原始值user_agent.devicecomputer（电脑）、tablet（平板）、phone（手机）从User-Agent解析出的设备类型user_agent.OSWindows、MacOS从User-Agent解析出的操作系统user_agent.browserChrome、Safari、Firefox从User-Agent解析出的浏览器user_agent.browser_version129、18.0从User-Agent解析出的浏览器版本查询示例：用户使用的浏览器分布情况如何？SELECT COUNT(*) -- 统计请求总数 GROUP BY user_agent.browser, user_agent.browser_version -- 按浏览器和版本分组若你的组织内部有自定义用户代理或约定使用特定请求头，也应将其解析为结构化属性：属性（Attribute）示例（Examples）描述（Description）user_agent.serviceapi-gateway、auth-service在分布式架构中，每个服务需在自定义User-Agent头中包含自身名称和版本user_agent.service_versionv123、6f6466b0e693470729b669f3745358df29f97e8d服务版本（与上述user_agent.service对应）user_agent.appiOS、android若请求来自移动应用，需包含应用类型和版本user_agent.app_versionv123、6f6466b0e693470729b669f3745358df29f97e8d移动应用版本（与上述user_agent.app对应）7. 路由信息（Route info）HTTP相关属性还未完全覆盖！其中最重要的信息之一是请求匹配的API端点。OpenTelemetry SDK通常会自动获取该信息，但并非在所有场景下都能生效。此外，还可考虑提取路由参数和查询参数作为额外属性。属性（Attribute）示例（Examples）描述（Description）http.route/team/{team_id}/user/{user_id}URL路径匹配的路由模式http.route.param.team_id14739、team-name-slug从URL路径中解析出的路由参数值（此处以team_id为例）http.route.query.sort_dirasc与服务响应相关的查询参数（示例：?sort_dir=asc&...中的sort_dir参数）查询示例：SELECT P99(duration_ms) -- 统计请求耗时的99分位数 WHERE main = true AND service.name = "api-service" -- 限定服务名称 GROUP BY http.route -- 按路由分组示例：按路由分组的P99耗时图表，部分路由耗时出现激增，需结合版本信息排查是否由部署导致8. 用户与客户信息（User and customer info）掌握基础属性后，用户与客户信息是最重要的元数据之一。没有任何自动化SDK能精准适配你的用户模型，因此需要手动添加。在实际业务中，单个用户或账户贡献的收入可能占企业总收入的10%以上，且他们的使用模式往往与普通用户存在显著差异。这类高价值用户可能拥有更多子用户、存储更多数据，还可能触发普通用户（如每月支付10美元的用户）不会遇到的限制和边缘场景。因此，必须能够将他们的流量与普通用户的流量区分开来。属性（Attribute）示例（Examples）描述（Description）user.id2147483647、user@example.com用户的唯一标识（若为邮箱，需根据组织政策判断是否可存储到外部服务，避免PII泄露）user.typefree（免费用户）、premium（付费用户）、enterprise（企业用户）、vip（VIP用户）业务层面的用户类型划分（高价值用户需单独追踪）user.auth_methodtoken（令牌）、basic-auth（基础认证）、jwt（JWT认证）、sso-github（GitHub单点登录）用户的系统认证方式user.team.id5387、web-services若存在团队架构，记录用户所属团队IDuser.org.id278、enterprise-name若用户属于企业合约客户，记录其所属企业IDuser.age_days0、637账户创建天数（非用户实际年龄），用于区分新用户与老用户的问题差异（如老用户可能因数据量大触发问题）user.assumedtrue标记是否通过内部方式模拟用户身份进行调试user.assumed_byengineer-3@your-company.com记录模拟用户身份的实际操作人员查询示例：SELECT P99(duration_ms) -- 统计请求耗时的99分位数 WHERE main = true AND service.name = "api-service" -- 限定服务名称 GROUP BY user.type -- 按用户类型分组9. 速率限制信息（Rate limits）无论采用何种速率限制策略，都需添加当前速率限制相关信息。通过这些信息，可快速定位被速率限制的用户案例。属性（Attribute）示例（Examples）描述（Description）ratelimit.limit200000该请求对应的速率限制上限（未来可能为不同用户配置不同限制，需提前记录）ratelimit.remaining130000用户当前剩余的速率限制额度ratelimit.used70000当前速率限制周期内已使用的请求次数ratelimit.reset_at2024-10-14T19:47:38Z速率限制重置时间（若适用）查询示例1：某用户提交了速率限制相关的支持工单，需查看其近期操作：SELECT COUNT(*) -- 统计请求总数 WHERE main = true AND service.name = "api-service" AND -- 限定服务名称 user.id = 5838 -- 限定用户ID GROUP BY http.route -- 按路由分组示例：某用户的请求活动图表，末尾存在大量重复调用同一路由的峰值，为排查提供方向查询示例2：速率限制额度所剩无几的用户，主要在访问哪些路由？这些行为是否存在异常？SELECT COUNT(*) -- 统计请求总数 WHERE main = true AND service.name = "api-service" AND -- 限定服务名称 ratelimit.remaining < 100 -- 筛选剩余额度不足100的请求 GROUP BY http.route -- 按路由分组10. 缓存信息（Caching）对于所有可通过缓存缩短响应时间的代码路径，都需添加缓存是否命中的标识。属性（Attribute）示例（Examples）描述（Description）cache.session_infotrue、false会话信息是否从缓存获取（true为命中，false为重新获取）cache.feature_flagstrue、false用户的功能标志是否从缓存获取（true为命中，false为重新获取）11. 本地化信息（Localization info）用户选择的本地化配置可能是故障的常见诱因，需记录相关信息：属性（Attribute）示例（Examples）描述（Description）localization.language_dirrtl（从右到左）、ltr（从左到右）用户语言的文本排版方向localization.countrymexico（墨西哥）、uk（英国）用户所在国家localization.currencyUSD（美元）、CAD（加拿大元）用户选择的交易货币12. 运行时长信息（Uptime）记录服务处理请求时的运行时长，有助于排查以下几类故障：服务重启后出现的问题服务运行一段时间后触发的内存泄漏服务自动重启时的频繁崩溃问题建议同时记录运行时长的对数（log10），或通过其他方式优化可视化效果。对数形式可在同一图表中清晰展示服务启动初期的状态，避免因长期运行实例（如运行数天的实例）的存在，导致短期数据被压缩到图表底部无法识别。属性（Attribute）示例（Examples）描述（Description）uptime_sec1533服务实例的运行时长（单位：秒），便于可视化查看重启情况uptime_sec_log_103.185运行时长的对数（10为底），可在同一图表中兼顾短期和长期运行实例的展示查询示例：SELECT HEATMAP(uptime_sec), -- 运行时长热力图（原始值） HEATMAP(uptime_sec_log_10) -- 运行时长热力图（对数值） WHERE main = true AND service.name = "api-service" -- 限定服务名称示例：服务崩溃循环时的运行时长热力图，对数形式更易区分异常13. 指标信息（Metrics）这一做法可能存在争议，但我发现在跨度中添加系统处理请求时的状态指标，对调试很有帮助。我们可以每10秒获取一次这些指标并缓存，然后将其添加到该时间段内产生的所有主跨度中。需要说明的是，这种方式捕获的指标在数学严谨性上存在不足。由于仅在有流量时才会记录数据，无法通过它计算出具有严格统计学意义的CPU负载P90值，但在实际调试场景中，这一数据已足够提供初步线索，无需切换到其他工具。尤其当你使用热力图可视化而非进行复杂计算时，效果更佳。不过，不建议基于这类数据设置告警，标准指标仍是告警的理想选择。杰西卡·克尔（Jessica Kerr）近期在Honeycomb博客中也介绍了这种方法。属性（Attribute）示例（Examples）描述（Description）metrics.memory_mb153、2593服务处理请求时的内存使用量（单位：MB）metrics.cpu_load0.57、5.89服务处理请求时的CPU负载（以活跃核心数为单位）metrics.gc_count5390最近观测到的垃圾回收（GC）次数（可为累计值，即服务启动至今的总次数；也可为差值，如最近一分钟的次数，需明确记录统计方式）metrics.gc_pause_time_ms14、325垃圾回收耗时（单位：毫秒，统计方式同上，需明确是累计值还是差值）metrics.go_routines_count3、3000当前运行的Go协程数量metrics.event_loop_latency_ms0、340事件循环等待下一个tick的累计耗时（单位：毫秒），是Node.js应用的重要指标查询示例：请求变慢是否由内存或CPU资源不足导致？SELECT HEATMAP(duration_ms), -- 请求耗时热力图 HEATMAP(metrics.memory_mb), -- 内存使用量热力图 HEATMAP(metrics.cpu_load) -- CPU负载热力图 WHERE main = true AND service.name = "api-service" -- 限定服务名称 GROUP BY instance.id -- 按实例ID分组示例：通过跨度中的指标数据，获取系统运行状态上下文14. 异步请求汇总（Async request summaries）在追踪系统中，异步请求应拥有独立的跨度，但将部分统计信息汇总到主跨度中，仍有助于识别异常值和快速定位关键追踪数据。属性（Attribute）示例（Examples）描述（Description）stats.http_requests_count1、140处理当前请求过程中触发的HTTP请求总数stats.http_requests_duration_ms849上述HTTP请求的累计耗时（单位：毫秒）stats.postgres_query_count7、742处理当前请求过程中触发的PostgreSQL查询总数stats.postgres_query_duration_ms1254上述PostgreSQL查询的累计耗时（单位：毫秒）stats.redis_query_count3、240处理当前请求过程中触发的Redis查询总数stats.redis_query_duration_ms43上述Redis查询的累计耗时（单位：毫秒）stats.twilio_calls_count1、4处理当前请求过程中调用Twilio API的次数（可替换为其他第三方服务）stats.twilio_calls_duration_ms2153上述第三方API调用的累计耗时（单位：毫秒）查询示例：服务对数据库的调用次数是否在合理范围内？SELECT HEATMAP(stats.postgres_query_count) -- PostgreSQL查询次数热力图 WHERE main = true AND service.name = "api-service" -- 限定服务名称示例：单请求数据库查询次数热力图，呈现双峰分布且存在大量查询的异常值疑问：能否不主动添加这些汇总信息，而是通过查询整个追踪数据来聚合获取？（详见“常见质疑”部分解答）15. 采样信息（Sampling）当系统规模扩大，细粒度遥测数据会面临采样难题。运行中的系统可能产生海量数据，工程师通常希望存储并查询所有数据以获得精确结果，但这需要在成本、速度和精度之间权衡。遥测数据与用户交易数据存在本质差异，需以不同思路对待。幸运的是，我们只需获取具有统计显著性的子集数据即可。即使仅采样1/1000的请求，也能为系统整体流量模式提供足够详细的参考。采样是一个深奥的话题，若你刚起步，建议从简单的均匀随机头部采样开始，并为每个跨度记录采样率，以便后续采用更复杂的采样策略。优质工具会根据每个跨度的采样率对计算结果进行加权，无需手动将COUNT结果乘以sample_rate来获取准确值。相关参考文章如下：我最初在《Scuba论文》中了解到这一理念Honeycomb支持按事件设置采样率Cloudflare的Analytics Engine会根据流量自动调整采样率属性（Attribute）示例（Examples）描述（Description）sample_rate1、500采样比例标识，即每N个事件中存储1个，其余丢弃。例如，若采样1%的请求，sample_rate值为10016. 耗时分解（Timings）将请求处理过程拆分为多个关键阶段，并在主跨度中记录每个阶段的耗时，对调试非常有帮助。疑问：这不是子跨度的用途吗？很多工程师初次使用追踪工具时，容易陷入“为所有操作创建子跨度”的误区。实际上，数据结构设计应与查询需求相匹配：子跨度适合展示单个请求的瀑布流耗时，但在跨所有请求进行查询和可视化时会非常繁琐。将耗时信息集中到单个主跨度中，不仅便于查询，还能配合Honeycomb的BubbleUp等功能，快速定位问题（例如某类请求变慢是因为认证环节耗时10秒）。属性（Attribute）示例（Examples）描述（Description）auth.duration_ms52.2、0.2请求处理过程中认证环节的耗时（单位：毫秒）payload_parse.duration_ms22.1、0.1识别服务的核心工作负载，并为其添加耗时记录（此处以请求体解析为例）17. 错误信息（Errors）当遇到错误并需要终止操作时，需在跨度中添加错误相关信息，如错误类型、堆栈跟踪等。我发现一种非常有效的做法：为每个错误抛出位置添加唯一的错误标识（slug）。若该标识在代码库中唯一，只需通过简单搜索就能定位到具体位置，从而快速从仪表盘上的错误峰值定位到代码中抛出错误的具体行数。同时，这一标识也可作为低基数字段，方便使用GROUP BY进行聚合分析。虽然无法覆盖所有可能的错误，但如果某个失败请求缺少exception.slug标识，就说明代码中的错误处理存在遗漏。通过这种方式，能轻松发现未预期的失败场景。if (isNotRecoverable(err)) { // 使用固定字符串作为错误标识，不使用变量或动态生成 // 可通过自定义代码检查规则强制遵循此规范 setErrorAttributes(err, "err-stripe-call-failed-exhausted-retries"); throw err; }属性（Attribute）示例（Examples）描述（Description）errortrue、false标记请求是否失败的专用字段exception.messageCan't convert 'int' object to str（无法将int对象转换为字符串）、undefined is not a function（未定义的函数）错误中的异常信息exception.typeIOError（IO错误）、java.net.ConnectException（连接异常）异常的程序类型exception.stacktraceReferenceError: user is not defined（引用错误：user未定义）、at myFunction (/path/to/file.js:12:2)异常的堆栈跟踪信息（便于定位错误抛出位置）exception.expectedtrue、false标记异常是否为预期内场景（如机器人访问不存在的URL），便于过滤无需关注的不可预防异常exception.slugauth-error（认证错误）、invalid-route（无效路由）、github-api-unavailable（GitHub API不可用）为可预见的错误创建唯一可搜索标识，用于定位代码中的错误抛出位置查询示例1：上周企业用户遇到的错误类型分布如何？哪些错误最频繁？SELECT COUNT_DISTINCT(user.id) -- 统计遇到错误的唯一用户数 WHERE main = true AND service.name = "api-service" AND -- 限定服务名称 user.type = "enterprise" -- 限定企业用户 GROUP BY exception.slug -- 按错误标识分组查询示例2：查找需要优化错误处理的追踪数据（即缺少错误标识的失败请求）：SELECT trace.trace_id -- 追踪ID WHERE main = true AND service.name = "api-service" AND -- 限定服务名称 error = true AND -- 筛选失败请求 exception.slug = NULL -- 筛选缺少错误标识的请求 GROUP BY trace.trace_id18. 功能标志信息（Feature flags）细粒度功能标志是开发者的强大工具，可在生产环境中仅向部分用户或流量开放代码变更。为每个请求添加功能标志信息，能在逐步扩大新代码覆盖范围时，对比新旧代码的运行效果。结合宽事件的全面可视性，即使是复杂的系统迁移也能更可控，让代码发布更有信心。需要注意的是，语义规范中建议将功能标志信息作为事件添加到跨度中。从长期来看，遵循这一标准更有利于获得工具厂商的支持（若该规范进入稳定阶段）。但在当前阶段，我建议同时将功能标志信息添加到主跨度中，以便快速查询。属性（Attribute）示例（Examples）描述（Description）feature_flag.auth_v2true、false当前请求中“auth\_v2”功能标志的启用状态feature_flag.double_write_to_new_dbtrue、false当前请求中“双写新数据库”功能标志的启用状态查询示例：使用新认证流程的用户遇到了哪些错误？与旧流程（对照组）相比情况如何？SELECT COUNT -- 统计错误数量 WHERE main = true AND service.name = "api-service" AND -- 限定服务名称 GROUP BY feature_flag.auth_v2, exception.slug -- 按功能标志状态和错误标识分组19. 关键组件版本（Versions of important things）运行时、框架及核心库的版本信息，能为故障排查提供重要上下文：属性（Attribute）示例（Examples）描述（Description）go.versiongo1.23.2编程语言运行时版本（此处以Go为例）rails.version7.2.1.1核心框架版本（此处以Rails为例，可根据使用的框架替换）postgres.version16.4数据存储服务版本（此处以PostgreSQL为例，可根据使用的数据库替换）查询示例1：Rails曝出安全漏洞，我们的服务正在使用哪些版本的Rails？SELECT COUNT_DISTINCT(service.name) -- 统计使用各版本的唯一服务数 WHERE service.environment = "production" -- 限定生产环境 GROUP BY rails.version -- 按Rails版本分组查询示例2：内存使用量比以往更高，是否与最近的运行时升级有关？两者是否存在关联？SELECT HEATMAP(metrics.memory_mb) -- 内存使用量热力图 WHERE main = true AND service.name = "api-service" -- 限定服务名称 GROUP BY go.version -- 按Go运行时版本分组20. 业务特有信息（Your specific application）这部分是最能体现宽事件价值的地方，因为每个应用都有其独特性或特定业务领域。例如，若你的应用面向牙医，可能需要追踪牙医的专业资质；若涉及物流，可能需要记录包裹所在的仓储位置；若为宠物追踪应用，可能需要记录嵌入式追踪设备的芯片信息。没有任何框架能自动识别你的业务核心关注点并完成监控配置，这需要你根据业务需求手动添加。属性（Attribute）示例（Examples）描述（Description）asset_upload.s3_bucket_paths3://bucket-name/path/to/asset.jpg资源上传的目标S3存储路径（适用于文件上传场景）email_vendor.transaction_id62449c60-b51e-4d5c-8464-49217d91c441第三方邮件服务商的交易ID（便于后续与服务商对接排查问题）vcs_integration.vendorgithub、gitlab、bitbucket版本控制系统集成的服务商（若某类请求因Bitbucket故障失败，可快速定位原因）process_submission.queue_length153、1提交任务时的队列长度（适用于涉及队列的场景）实践注意事项1. 不确定时，优先添加属性当你犹豫“这些数据未来是否有用”时，建议优先添加该属性。额外添加一个属性的边际成本极低；若后续数据量增长过快，相比减少属性、降低采样率，更推荐保留丰富上下文的宽事件并提高采样率。2. 善用热力图Honeycomb的热力图在识别异常值、呈现多峰分布和感知数据整体特征方面表现出色，我希望有更多工具支持这一功能。如今，热力图已成为我开发过程中不可或缺的工具。3. 重视反馈循环修改代码时，同步调整遥测配置，以便观察新代码的运行效果。代码发布后，及时验证结果是否符合预期。必要时，可为某个版本临时添加特定字段，版本稳定后再删除，无需长期保留。紧凑的反馈循环如同自行车加速，既能提升系统稳定性，又能让你更有信心地快速推进开发。4. 语义规范与命名一致性我会尽量遵循语义规范命名属性，但难免存在疏漏——命名本身就是一件困难的事！在团队或跨系统间保持命名一致性同样具有挑战。建议以语义规范为参考，但优先保证数据能顺利输出并获得初步价值，而非严格遵守仍在演进的规范。当数据在组织内部证明其价值后，再投入精力优化一致性。从长期来看，语义规范有助于可观测性工具厂商基于遥测数据提供更深入的分析能力，但这一领域的发展仍处于初期阶段。常见质疑（Frequent Objections）1. 这种方法真的有效吗？我已在数十个生产系统中应用过宽事件，每次这些数据都能在深入理解系统运行状态和排查问题时发挥关键作用，甚至连长期维护系统的工程师都能发现意想不到的情况，例如：某系统90%的流量竟来自单个用户某个工作进程莫名运行着一个月前的旧版本代码某API端点的请求体大小通常为1-2KB，但存在一个边缘场景：某用户的请求体达40MB以上，导致其页面加载时间比P99值还长数分钟为认证中间件添加监控后发现，约20%的请求仍缺少用户信息——原来存在一个已多年未维护的备用认证系统计划废弃的某端点支持A、B、C三种数据格式，但实际流量中从未出现C格式请求，因此可直接移除对C格式的支持2. 我不喜欢这种方式，感觉不对劲如果你现在有这种想法，我建议先尝试5分钟再下结论。当一条日志在终端窗口中折行多次时，很多开发者会本能地产生抵触情绪。例如，传统日志格式看起来更“正常”：[2024-09-18 22:48:32.990] Request started http_path=/v1/charges request_id=req_123 [2024-09-18 22:48:32.991] User authenticated auth_type=api_key key_id=mk_123 user_id=usr_123 [2024-09-18 22:48:32.992] Rate limiting ran rate_allowed=true rate_quota=100 rate_remaining=99 [2024-09-18 22:48:32.998] Charge created charge_id=ch_123 permissions_used=account_write request_id=req_123 [2024-09-18 22:48:32.999] Request finished http_status=200 request_id=req_123而宽事件的日志格式则显得“怪异”：[2024-10-20T14:43:36.851Z] duration_ms=1266.1819686777117 main=true http.ip_address=92.21.101.252 instance.id=api-1 instance.memory_mb=12336 instance.cpu_count=4 instance.type=t3.small http.request.method=GET http.request.path=/api/categories/substantia-trado http.route=/api/categories/:slug http.request.body.size=293364 http.request.header.content_type=application/xml user_agent.original="Mozilla/5.0 (X11; Linux i686 AppleWebKit/535.1.2 (KHTML, like Gecko) Chrome/39.0.826.0 Safari/535.1.2" user_agent.device=phone user_agent.os=Windows user_agent.browser=Edge user_agent.browser_version=3.0 url.scheme=https url.host=api-service.com service.name=api-service service.version=1.0.0 build.id=1234567890 go.version=go1.23.2 rails.version=7.2.1.1 service.environment=production service.team=api-team service.slack_channel=#api-alerts service.build.deployment.at=2024-10-14T19:47:38Z service.build.diff_url=https://github.com/your-company/api-service/compare/c9d9380..05e5736 service.build.pull_request_url=https://github.com/your-company/api-service/pull/123 service.build.git_hash=05e5736 service.build.deployment.user=keanu.reeves@your-company.com service.build.deployment.trigger=manual container.id=1234567890 container.name=api-service-1234567890 cloud.availability_zone=us-east-1 cloud.region=us-east-1 k8s.pod.name=api-service-1234567890 k8s.cluster.name=api-service-cluster feature_flag.auth_v2=true http.response.status_code=401 user.id=Samanta27@gmail.com user.type=vip user.auth_method=sso-google user.team_id=team-1但需明确：宽事件的设计目标是让机器高效读取数据，而非方便人类直接阅读。系统产生的数据量如此庞大，不应浪费宝贵的人力去逐行扫描日志寻找规律——让工具和算法来完成这类工作。3. 这看起来工作量很大若要完全实现本文提及的所有内容，确实需要大量工作。但即便只实现最简单的子集，也能带来显著价值。反之，若不采用宽事件，你需要花费更多精力构建系统的心智模型，通过阅读代码推测问题，却无法验证模型是否与实际系统一致。很多逻辑可封装到组织内部的共享库中，但推动库的 adoption、维护更新以及帮助工程师熟练使用，又会带来新的挑战。未来，我期待有更多 opinionated（ opinionated 指工具或框架有明确设计理念和约束，减少用户决策成本 ）的平台或框架能原生支持宽事件，降低实践门槛。4. 数据量会不会太大？成本会不会很高？Hacker News上有评论称：“这并非Meta（元宇宙）之外无人知晓的理念，只是实施成本太高，尤其是使用第三方工具而非自研工具时，即便采样也难以承受。”首先，建议将宽事件的数据量与你当前每请求的日志量对比——在很多系统中，宽事件反而能减少总体日志量。其次，若为每个请求存储完整宽事件数据，在大规模场景下成本确实可能过高，此时采样就成为关键手段。通过采样，你可以在成本和数据价值之间找到平衡，自主决定投入成本与期望获得的价值。此外，实时OLAP系统的成本正不断降低。过去，Scuba需将所有数据存入内存以实现快速查询；如今，大多数OLAP系统已演进为基于云对象存储的列式文件存储，配合临时计算资源处理查询，成本降低了多个数量级。下一部分将具体展示成本差异。5. 数据存在大量重复，会不会很低效？人类的直觉有时会误导判断，我们通过一个具体示例来分析：我编写了一个脚本[1]，生成包含上述多数属性的NDJSON（换行分隔的JSON）文件，并填充了相对合理的模拟数据。假设某服务每秒处理1000个请求，全天24小时运行，且采样率为1%，那么每月约产生100万条事件数据。生成100万条宽事件示例数据后，文件大小约为1.6GB：http_logs.ndjson 1607.61 MB由于JSON格式中键名会重复出现，若转换为CSV格式，文件大小可减少50%以上：http_logs.csv 674.72 MB而通过gzip压缩后，文件大小大幅缩减，这表明数据实际占用空间远小于直观感受：http_logs.ndjson.gz 101.67 MB采用Parquet、DuckDB等列式存储格式，压缩效果更佳：http_logs.parquet 88.83 MB http_logs.duckdb 80.01 MB列式存储将同一列的数据连续存储，便于采用更高效的压缩策略。例如，若某一列所有值相同，只需存储一次该值；若某一列仅有2-3个不同值，可通过字典编码（dictionary-encoding）进行位压缩，同时还能加快该列的查询速度。示例：DuckDB中全列常量值的压缩示意图、DuckDB中基于字典编码的压缩示意图DuckDB的官方文档对这一机制有详细解读，所有数据仍保持可访问性，且查询便捷高效：❯ duckdb http_logs.duckdb D SELECT COUNT(*) FROM http_logs; -- 统计事件总数 ┌──────────────┐ │ count_star() │ │ int64 │ ├──────────────┤ │ 1000000 │ └──────────────┘ Run Time (s): real 0.002 user 0.002350 sys 0.000946 D SELECT SUM(duration_ms) FROM http_logs; -- 计算总耗时 ┌───────────────────┐ │ sum(duration_ms) │ │ double │ ├───────────────────┤ │ 999938387.7714149 │ └───────────────────┘ Run Time (s): real 0.003 user 0.008020 sys 0.000415此外，还有专门的内存和传输格式可进一步减少内存占用和网络传输量，例如OpenTelemetry正采用Arrow格式作为数据载体，正是出于这一考虑。若想深入了解这一领域，推荐收听关于FDAP技术栈的播客。6. 能否通过关联多个跨度的数据，或查询整个追踪数据来获取这些信息？从技术角度看，这确实可行。例如，Honeycomb已支持根据同一追踪中其他跨度的字段进行筛选，但这种方式属于高阶用法。我们的目标是“让正确的操作更简单”——若查询变得复杂，人们自然会减少查询频率。日常工作中已有太多事务分散精力，因此应尽量简化操作，提升效率。7. 这是否意味着不需要指标（Metrics）了？通常情况下，仍建议生成高层级指标，但所需数量可能会大幅减少。指标的优势在于，当你提前明确需要精确回答某个特定问题时（例如“昨天服务处理了多少请求？”或“上个月CPU使用率如何？”），能快速给出答案。[1] 注：实际上，这段脚本大部分是由Cursor（AI代码编辑器）生成的。原文：https://jeremymorrell.dev/blog/a-practitioners-guide-to-wide-events/
怎么记日志才算专业，SRE应该了解的实践指南
坦白说：很多日志都只是无用信息…[INFO] Starting process … probably. [DEBUG] Made it to line 42 — still alive. [TRACE] Function entered. Leaving soon. [INFO] User clicked a button. Which one? No idea. [WARN] Everything’s fine, just felt like warning you. [DEBUG] Variable x = 7. Might change. Might not. [INFO] Operation completed successfully (we think). [TRACE] Loop iteration #12 of infinite sadness. [DEBUG] Placeholder for meaningful message. [INFO] Shutting down gracefully … except when not.作为开发人员，常常像撒彩纸一样随意记录日志 —— 每个函数的入口、每个变量、每一次系统 “心跳” 都不放过。没过多久，数太字节的无意义日志行就会堆积如山，填满那些没人看的仪表盘。我们向可观测性供应商支付数百万美元，却只是为了存储这些 “垃圾”。每一条无用的日志行都在消耗计算资源、磁盘空间和资金。没有明确目的的日志记录不是可观测性实践，而是 “乱扔垃圾”。即便现在的可观测性平台借助列式存储大幅提升了压缩效率，也没有理由记录所有内容。这种无差别记录仍会让根本原因分析变成 “大海捞针”—— 稀释你真正需要的有效信号，而且你还得为此支付更高成本。我们需要有选择性地记录日志：只记录那些有助于理解系统、调试实际问题或说明业务影响的信息，其他无关内容都不必记录。日志记录的核心原则每一条日志记录都是一个主动选择，而非下意识的操作。如果这条日志无法帮未来的你在凌晨 3 点排查出 bug，那就删掉它。日志记录不是记日记，要做到简洁、清晰且真正有用。在敲下 logger.info（信息级日志记录代码）之前，先停下来问问自己：我之后真的会用 grep（一种文本搜索工具）查找这条日志吗？如果答案是否定的，就删掉它。日志不是叙事流水账，而是证据。它们的存在，是为了在系统出现问题时，告诉你当时系统的运行状态。日志不应被排在可观测性实践的末尾。它们不只是用于验证问题的 “显微镜”，更是用于发现问题的 “地图”。有时候，获取洞见最快的方式，就是直接探索原始文本 —— 通过 grep 搜索、筛选，跟着直觉去挖掘。日志能激发探索欲：它们能展现出指标可能掩盖的细微差别，以及追踪数据无法传递的上下文信息。不要把日志当作最后的求助手段，而应将其视为 “实时输出的真相来源”，从一开始就主动去探索。无上下文，不日志只写 “发生错误”，却不包含输入数据、ID 或系统状态，这样的日志毫无意义。要添加足够的上下文信息，以便还原当时的场景 —— 比如请求 ID、用户 ID、输入参数、操作名称等。如今，借助 OpenTelemetry（一种开源可观测性框架），你可以免费获取追踪 ID（trace ID）和跨度 ID（span ID），一定要善用它们。通过追踪 ID 与追踪数据（甚至指标）关联起来的日志，其价值远胜于孤立的文本行。日志不是可观测性实践中独立的一环，而是根本原因分析的 “收尾章节”。你通过指标触发告警，通过追踪数据展开调查，然后再查看日志，了解到底发生了什么。当日志通过追踪 ID 和跨度 ID 关联起来后，它们就不再是无用信息，而成为了精准、有上下文的证据 —— 直接与单个请求的路径相关联。这才是有明确目的的可观测性实践，而非一堆杂乱无章的文本。结构化日志，而非自由文本自由文本形式的日志早已过时。结构化日志（无论是 JSON、CSV 格式，还是键值对形式）不仅更易于查询，更是数据分析的基础。一旦日志具备了结构化格式，规律便会自然浮现：“这个错误从上周开始激增。”“这个问题大多在事件 X 之后出现。”“这个警告与某次特定部署相关。”日志的未来发展方向，不是逐行阅读单条日志，而是从成千上万条日志中发现整体规律。图片来自网络结构化日志让图表展示变得简单高效。虽然很多可观测性平台支持 “读取时解析 schema”（schema on read），但这种灵活性是有代价的：每一次查询，系统都要逐行扫描并解析原始文本，推断出本该从一开始就存在的结构化格式。这类查询计算成本高、速度慢，而且用户编写查询语句也更困难。预先结构化的日志则能彻底扭转这种低效局面。当你的数据本身就具备结构化格式时，就可以充分利用列式存储和原生聚合功能 —— 在毫秒级时间内完成事件的查询、可视化和关联分析，而无需等待数分钟。明确何时该统计度量，而非一味记录日志并非所有事件都适合写入日志流。有些信息需要更规范的结构和时间维度记录 —— 这正是跨度（span）和指标（metric）的用途。如果你要统计延迟时间、用户流程或分布式系统中的因果关系，就应该生成跨度数据而非日志。跨度能捕捉耗时、上下文以及服务间的关联关系，告诉你 “为什么某个操作变慢或出错”；而日志只能告诉你 “某个问题发生了”。同样的逻辑也适用于指标：将重复出现的日志转化为可告警、可高效聚合的有效信号。如果你发现自己每秒都在记录数百条相同的日志信息，那你就不是在进行可观测性实践，只是在浪费存储资源。对这类信息，只需统计一次、进行汇总，然后让指标和追踪数据来承担主要的分析工作即可。日志级别是给人看的，而非给机器看的日志记录不是个人的调试日记，而是留给未来团队成员的共享资料。每一条日志都应能让他人清晰理解当时发生了什么，无需猜测你的意图。记录日志时，要为下一次故障排查做准备，而非受当下情绪影响。你的日志记录着系统的运行历程，要让这段 “历程” 值得一读。例如：错误（ERROR）：需要人员介入处理。系统出现故障。警告（WARN）：出现意外情况，但系统可正常运行。后续需调查。信息（INFO）：系统的常规运行状态，有记录价值。调试 / 追踪（DEBUG/TRACE）：开发人员临时用于排查问题的信息 —— 不应轻易提交到生产环境。日志记录要审慎。除非确实需要采取行动，否则不要将某条日志标记为 “错误” 级别。滥用 “错误” 级别会让告警机制失去敏感度，导致团队对真正重要的问题视而不见。每个日志级别都应传递明确的意图：哪些问题需要立即修复，哪些需要持续关注，哪些可以忽略。不过，追踪级日志也有其用武之地。例如，在 ClickHouse Cloud（一款云原生数据库）的后台运行中，我们会大量使用追踪级日志，帮助工程师排查性能问题，并为大规模用户提供支持。这是一种经过深思熟虑的例外情况 —— 当你运营着一个实时为数千个工作负载提供服务的分布式数据库时，这种日志级别是必要的。但对于大多数应用而言，如此详细的日志记录并非可观测性实践，只是在制造无用信息。助力高效日志记录的工具现在已有丰富的软件开发工具包（SDK）和强大的过滤工具，无需再 “无差别记录所有日志”，要善用这些工具。现代的 OpenTelemetry Collector SDK（收集器软件开发工具包）能让你精准控制日志记录内容：你可以对代码进行埋点配置，只生成有意义的日志行，然后在数据摄入或收集阶段过滤或丢弃所有无关日志。例如：过滤处理器（filter processor）支持使用 OpenTelemetry 转换语言（OTTL），根据日志的严重级别、资源属性或内容模式等条件，丢弃不需要的日志、指标或追踪数据。如果你的可观测性平台支持，还可以在代理（agent）、收集器网关（collector gateway）或数据摄入阶段进行过滤，确保无用日志不会被写入、存储或建立索引 —— 从而节省计算资源、存储成本和查询成本。如果管理员发现有用户在生成无用日志，可以在数据处理流程中强力过滤这些日志，或者强制推行 “最小日志记录” 策略。即便你使用的是专有平台，它们通常也会提供类似的过滤或摄入控制工具，只是可能不会公开宣传这些功能。有目的地面向日志，否则不如不记录可观测性的关键不在于数据量，而在于清晰度。每一条日志都应凭借其独特价值占据一席之地 —— 即解释那些指标和追踪数据无法传递的信息。没有明确目的的日志记录，只会浪费资金，并掩盖有价值的洞见。日志记录要审慎，要采用结构化格式，要添加上下文信息，要明确何时该统计度量、何时该进行追踪、何时该保持 “沉默”。现代工具让规范的日志记录变得比以往更简单，但最终的自律仍需来自你自身。归根结底，出色的日志记录并非要捕捉所有发生的事情，而是要捕捉关键信息。