SRECon-US2025-笔记-全-

SRECon US2025 笔记(全)

001:十年免费、开放与自动化之路——改善SRE体验

概述

在本节课中,我们将回顾Let's Encrypt项目过去十年的发展历程。我们将了解这个免费、开放、自动化的证书颁发机构(CA)是如何诞生的,它如何解决了早期互联网的安全难题,以及其背后的组织、技术架构和运维哲学。课程将涵盖其历史背景、建立信任的过程、技术栈选择、当前挑战与未来规划,旨在为SRE和开发者提供一个关于如何构建并运维大规模、关键性公共服务的一手案例。


历史背景:为何需要Let's Encrypt?

在Let's Encrypt出现之前,互联网的安全状况与今天截然不同。大约10到15年前,公共Wi-Fi网络极不安全。一个标志性事件是2010年发布的Firefox插件Firesheep,它能轻易窃取同一网络下其他用户的会话Cookie,让登录校园Wi-Fi下的Gmail或Facebook账户变得非常危险。

当时,许多大型网站可能只在登录页面使用HTTPS,但网站的其他部分以及会话Cookie仍以明文传输。人们虽然意识到这个问题,但TLS(传输层安全协议)的普及率远远不够。

TLS本身在当时也存在性能问题。不过,随着新版本TLS的发布、CPU指令集加速以及更高效的加密套件出现,浏览器和大型网站共同努力,性能问题逐渐得到解决。

然而,有一个巨大的障碍始终存在:数字证书。证书难以普及有几个原因。


早期障碍:成本与自动化难题

第一个障碍是成本。虽然单个证书费用不高,但对于需要管理大量证书的场景(例如大学社团为所有成员提供虚拟主机),购买和续费证书在经济和管理上都不现实。

第二个更关键的障碍是自动化缺失。获取和配置证书是一个高度手动的过程:需要邮件沟通、人工验证、手动配置Web服务器并确保证书格式正确。这与当今在托管平台上瞬间自动部署HTTPS网站的能力形成鲜明对比。

当时也有人研究替代Web公钥基础设施(PKI)的方案,例如使用DNSSEC来保护Web,但这些方案推进缓慢。因此,要真正实现全网加密,最可行的方案似乎是创建一个全新的证书颁发机构


项目启动:目标与诞生

Let's Encrypt的目标由此确立:创建一个免费开放完全自动化的CA,以便能深度集成到所有需要证书的系统中。

规划始于2013年。2014年,非营利组织互联网安全研究小组(ISRG) 成立,并开始了软件和基础设施的开发。2015年,Let's Encrypt颁发了第一批证书。

项目初期得到了一批赞助商的支持,他们看到了项目的愿景并助力其启动。

项目启动后发展迅速。从2015年底至今,Let's Encrypt从最初每天颁发少量证书,发展到如今每天颁发数百万张证书。


建立信任:从基础设施到根证书植入

建立并运营一个CA,技术上的挑战只是其一。更重要的是如何成为互联网中受信任的第三方

这个过程分为几个关键步骤:

  1. 初始建设:搭建基础设施,包括服务器、硬件安全模块(HSM)、网络和代码。
  2. 通过审计:遵循CA/浏览器论坛制定的基线要求,通过WebTrust审计,证明其流程和操作符合标准。
  3. 加入根证书计划:说服主要的软件供应商将Let's Encrypt的根证书植入其产品。这包括:
    • Apple(iOS/macOS)
    • Google Chrome
    • Microsoft(Windows)
    • Mozilla(Firefox)
      Mozilla的根证书计划因其完全公开透明而尤为重要,常被其他系统(如Linux发行版)作为参考。
  4. 交叉签名(Cross-Signing):在新根证书被全球所有设备信任之前(这可能需要数年时间),可以通过一个已被广泛信任的CA对Let's Encrypt的中间证书进行“交叉签名”,从而快速获得信任。IdenTrust作为早期赞助商,提供了关键的初始交叉签名。

运营哲学:开放、透明与标准化

成为受信任的CA不仅需要技术,更需要正确的运营哲学。Let's Encrypt通过以下几种方式建立并维持信任:

开源

Let's Encrypt的CA软件Boulder是完全开源的。任何人都可以在GitHub上审查其每一行代码。这促进了与业界的合作,并增强了透明度。

公开事件报告

根据Mozilla根证书计划的要求,Let's Encrypt将所有事件报告公开在Bugzilla上。这种透明度不仅适用于CA生态,有时甚至能推动更广泛的改进(例如,Go语言曾因Let‘s Encrypt报告的一个bug而修改了语言设计)。

标准化协议(ACME)

Let's Encrypt没有仅仅提供一个私有API,而是通过IETF流程标准化了ACME(自动证书管理环境) 协议。这使得整个生态能够蓬勃发展,众多客户端、服务器和防火墙都内置了ACME支持,其他CA也广泛采用了该协议。

单一执行路径

在Let's Encrypt,所有证书(包括测试和生产环境)都只能通过ACME协议颁发。内部人员也没有“后门”命令行工具。这消除了特权路径,简化了合规性与正确性验证。

证书透明度(CT)日志

Let's Encrypt将所有颁发的证书记录到公共的证书透明度(CT) 日志中,并且自身也运营着CT日志。这对SRE和安全团队来说是宝贵的数据源,可用于监控自己基础设施的证书状态。


团队与规模:以小团队驱动大影响

ISRG目前有25名员工,涵盖工程、筹款、传播、财务和管理。其中,只有4名软件开发者全职维护Boulder,以及一个9人的SRE团队

以这样的团队规模支撑每天数百万证书的签发,效率是重中之重。核心思路是:通过自动化构建需要最少人工干预的服务,并严格控制功能范围

这意味着有时必须说“不”:

  • 不支持SMIME或扩展验证(EV)证书。
  • 不提供传统的客户支持。
  • 不填写某些供应商特定的安全审计问卷。

Let's Encrypt的定位是降低HTTPS的准入门槛,而非追求利润或服务所有客户。如果有需求超出其核心使命,可以放心地引导用户使用其他商业CA。

目前,Let's Encrypt的年运营预算约为450万美元,相比初创时期有了显著增长,但服务规模的增长更为巨大。


技术架构揭秘:物理、垂直与内存安全

作为CA,其核心PKI材料受到严格的物理监管要求,因此主要基础设施运行在自有的物理数据中心,而非云端。

  • 基础设施规模:在两个数据中心拥有约3个机架、24台服务器,以及网络、HSM等设备。
  • 云服务辅助:将CRL分发、DDoS防护、日志和指标收集等非核心服务放在CDN和云提供商上。
  • 离线根:根证书密钥存储在完全气隙隔离的离线环境中。

在运维中,一个重要的经验是:硬件相对于人力是便宜的。因此,团队倾向于垂直扩展而非过早进行复杂的水平扩展。例如,数据库使用一个强大的主节点(配备大内存和NVMe闪存)和多个副本,从而避免了管理分布式数据库的复杂性。

应用栈本身是无状态的,采用较为常规的技术组合:Linux、Nomad(编排)、Proxmox(虚拟化)、MariaDB、Salt Stack/Ansible(配置管理)、Prometheus(监控)和Redis(缓存)。

一个特别之处是对内存安全的投入。作为ISRG旗下的项目,Prossimo致力于为关键基础设施提供内存安全的实现。Let's Encrypt正在部署:

  • NTP:使用Rust编写的、内存安全的ntpd-rs替代传统NTP服务,因为时间戳对证书有效性至关重要。
  • DNS:计划用Rust编写的Hickory DNS替代现有解析器,因为域名验证是CA的核心操作之一。
  • TLS/代理:评估Rustls作为TLS库,并关注River(Rust反向代理)的未来发展。

这些投入旨在加固互联网基础设施的关键边界。


核心工具与开发

团队的大部分内部工具和Boulder CA本身都使用Go语言编写。Go在开发效率、性能、内存占用以及与监控、可观测性工具的集成方面取得了良好平衡。


未来规划:改进、简化与演进

上一节我们介绍了Let's Encrypt当前的技术栈,本节我们来看看团队正在着手进行的几项重要改进。

1. 撤销(Revocation)机制的变革

OCSP协议存在隐私和性能问题,且依赖在线查询,导致浏览器常需“失败开放”,使其效果大打折扣。现代浏览器已转向改进的CRL机制。因此,Let's Encrypt已开始逐步取消对OCSP的支持。这对SRE而言是个好消息,因为运维OCSP服务非常繁琐。

同时,为了更优雅地处理大规模证书撤销和续订,Let's Encrypt正在推广ACME协议的续订信息端点(ARI)。该端点能提前通知客户端证书需要续订的时间窗口,而非仅仅在撤销后通知。这既帮助网站管理员避免服务中断,也让CA能平滑管理因大规模撤销引发的续订流量高峰。

2. 停止发送到期提醒邮件

目前Let's Encrypt会在证书到期前30天发送提醒邮件。但这存在诸多问题:邮件可能发送给不再使用的网站;涉及额外的隐私数据管理;且它并非监控网站状态的可靠方式。未来,Let's Encrypt计划减少或停止发送这类邮件,将监控责任交还给网站所有者或更专业的监控服务。

3. 证书透明度(CT)日志的演进

随着证书签发量增长,CT日志服务的读取压力巨大。团队正在开发新的StaCT API,其设计极具SRE思维:可以从S3等静态存储直接提供API服务,读取路径无需自定义代码,且极易被CDN缓存。这将极大提升可扩展性和成本效益。新的实现Sunlight正在开发中。

4. 进一步缩短证书有效期

缩短证书寿命可以减小证书被滥用时的暴露窗口,减轻对撤销机制的依赖。Let's Encrypt计划在今年晚些时候提供可选的有效期仅为6天的证书。虽然这要求用户具备高度自动化和监控能力,但它也将解锁一些新功能,例如颁发IP地址证书,这在之前由于IP地址的易变性和所有权问题而未被允许。


总结与呼吁

本节课我们一起学习了Let's Encrypt如何通过免费、开放和自动化的理念,在过去十年中极大地推动了HTTPS的普及。我们回顾了其历史、建立信任的历程、高效的团队运作模式、独特的技术架构以及面向未来的规划。

作为非营利组织,Let's Encrypt的成功离不开社区的支持。你可以通过以下方式提供帮助:

  1. 推广HTTPS:将你所能影响的非HTTPS网站切换到HTTPS。
  2. 贡献生态:为你开发或维护的产品集成ACME客户端支持,实现自动证书管理。
  3. 财务支持:个人捐赠或寻求企业赞助对项目的持续运营至关重要。

感谢所有让Let's Encrypt成为可能的贡献者、赞助商以及像SREcon这样的社区,它们为项目提供了宝贵的知识和经验。


002:一种SRE的生产环境机器学习监控方法 🚀

概述

在本节课中,我们将探讨在生产环境中监控机器学习(ML)工作负载的必要方法。我们将了解为什么传统的监控手段不足以应对ML模型带来的独特挑战,并学习如何将SRE(站点可靠性工程)的实践与ML运维相结合,构建一个可靠、可观测的ML生产系统。


为什么需要监控机器学习? 🤔

上一节我们介绍了课程主题,本节中我们来看看为什么监控机器学习如此重要。

首先,机器学习模型无法达到100%的准确。正如统计学家乔治·博克斯所言:“所有模型都是错误的,但有些是有用的。” 追求100%的准确度会导致过拟合,且不切实际。现实世界的数据复杂多变,即使今天模型是完美的,明天世界可能已经改变,模型就不再准确。

其次,机器学习模型不具备持久性。一项发表在《自然》杂志上的研究表明,91%的机器学习模型会随着时间的推移而性能下降。模型在部署后不会保持静态,即使初始性能很高,也可能因数据变化而退化。

最后,复杂系统不可能是完全确定性的。复杂系统始终运行在“永久降级模式”中,我们到处都在处理小的故障,通过深度防御来防止大的失败。

总结来说:

  1. 100%准确的ML模型不存在。
  2. 91%的ML模型会性能衰退。
  3. 没有复杂系统是100%弹性的。

因此,我们需要一种专门针对ML的监控方法。


SRE与ML实践者的差异与融合 🔄

上一节我们明确了监控ML的必要性,本节中我们来分析执行监控的两大关键角色:SRE和ML实践者。

SRE的日常工作包括:

  • 深刻理解大规模分布式系统。
  • 构建可扩展、可观测、安全、合规且能抵御流量波动的系统。
  • 系统始终运行在故障模式中,需要处理事故以防止更大问题。

ML实践者的日常工作包括:

  • 深刻理解数学、统计和概率。
  • 开发、评估和优化复杂的算法与模型。
  • 模型从数据中学习,以做出预测和决策,并集成到产品或流程中。

两者都需要与不同团队协作,但存在一个根本差异:SRE的影响是随时间扩散的,其价值体现在长期没有坏事发生;而ML实践者的评估是时间点式的,价值体现在交付一个具体的模型。这种评估方式的差异需要调和,以确保模型在生产中的质量也能持续。


词汇与挑战的鸿沟

两者的日常词汇也存在差异。ML领域关注建模、精确度、回归、训练、测试、噪声、公平性等。虽然有些词(如“性能”)是共通的,但含义可能不同。这种语言上的脱节长期来看代价高昂,我们需要统一语言,确保能相互理解并优先处理相同的事务。

我们面临的共同挑战包括:

  • 规模:一切都在增长(摩尔定律)。
  • 资源短缺:业务快速增长,对资源的需求也飞速增长(霍夫施塔特定律)。
  • 事故与故障模式:双方都要处理,但事故的表现形式可能不同。
  • 跨领域专业教育的缺乏:目前鲜有大学课程同时深入教授SRE和生产级ML运维。
  • 系统可理解性:现代系统(无论是分布式系统还是复杂ML模型)都过于庞大,无法装入一个人脑中(康威定律)。
  • 沟通:沟通中最大的问题是“以为沟通已经发生了”(萧伯纳)。词汇差异会阻碍有效沟通。

因此,我们必须将这两个学科紧密结合起来。


机器学习基础与生产现实 🧠

在深入架构之前,我们先快速回顾机器学习基础及其生产环境下的现实。

机器学习主要分为:

  • 监督学习:使用带标签的数据集作为输入,模型基于数据中的分类进行预测。例如:神经网络、线性回归、决策树。
  • 非监督学习:数据没有标签,首先在数据中寻找模式以形成类别,然后将新数据归类。例如:K均值聚类、主成分分析、层次聚类。

根据伦理AI与机器学习研究所的研究,2024年最流行的ML应用是时间序列预测,其次是大语言模型(LLM)、推荐系统、自然语言处理等。

关键认知是:构建模型只是开始。 模型需要被集成到解决实际问题的复杂分布式系统中。你的故障将不仅是ML故障,也包括所有分布式系统常见的故障。客户不关心你的模型准确率多高,他们关心端到端系统是否工作。因此,你需要监控整个系统。


SRE需求层次在ML中的扩展 🏗️

上一节我们了解了ML的基础,本节我们看看如何将经典的SRE需求层次(金字塔)扩展,以适应ML工作负载。

我们需要一个“ML风味”的金字塔:

  1. 数据管理与质量监控:这是ML监控的基础。数据是模型的燃料,必须监控其收集、质量和统计特征。
  2. 容量规划
  3. 测试与发布流程
  4. 学习与反馈循环
  5. 事故响应:准备好应对ML特有的挑战。
  6. ML监控:监控模型本身。
  7. 所有传统监控层(监控、自动化等)

一个ML项目的生命周期通常包括:定义任务 -> 寻找与探索数据 -> 训练与验证模型 -> 部署 -> 运维(MLOps)。在运维阶段,你需要监控:

  • 业务KPI:传统遥测无法告诉你模型给出的答案是否正确。
  • 数据分布:输入模型的数据、模型输出的数据,以及可能的情况下,客户反馈的真实数据(Actuals)。
  • 部署流程:为ML模型实现有效的CI/CD。

监控复杂数据处理管道 📊

ML系统本质上是复杂的数据处理管道。在构建具体架构前,我们先看看如何监控这类管道。

一个典型管道包括:数据输入(Ingress) -> 转换(可能是ML推理) -> 数据输出(Egress) -> 数据存储。

以下是监控此类管道的要点:

  • 避免孤立视角:不要只查看单个处理环节的原始或处理后的数据,这会失去端到端的可见性。
  • 关注长期趋势:查看绝对和相对性能,单一时点的数据可能无法揭示系统正在缓慢退化。
  • 全面审视数据:除了关注最旧和最新数据,还需注意不同数据类型的处理成本差异,避免误导。
  • 测量端到端延迟
  • 为可理解性添加遥测:系统会不断增长,确保你的监控工具能帮助未来的你理解和解释系统。
  • 理解系统权衡:你的系统是为低延迟还是为数据完整性而优化?例如,交通预测系统可能牺牲完整性以获取最新数据;而金融交易系统则必须保证完整性。

训练阶段的监控 🏋️

模型训练本身也是一个需要投入生产、持续运行的数据管道。

训练流程通常为:数据注入 -> 特征提取 -> 模型训练与验证 -> (循环)。整个过程在一个编排器(Orchestrator)中运行,并保存模型元数据(如训练者、时间、预测快照、模型架构等),以便未来进行模型回滚等决策。

在训练阶段,你需要监控:

  • 数据存储:数据新鲜度、可用性、容量、数据丢失/损坏、模式不匹配。
  • 注入阶段:注入速率、端到端延迟、管道持续时间、丢弃率。
  • 编排器:作业执行、失败与重试、执行时间。
  • ML数据集质量:完整性、正确性、新鲜度、是否存在偏差。
  • 训练过程:训练时长、失败率、训练误差与验证误差、计算资源消耗、生成的模型大小。
  • 模型性能:精确度、召回率、准确率等统计指标,以及模型的计算性能(推理速度)。

设计生产架构与监控策略 🏗️

在开始构建架构前,必须进行设计决策:

  • 明确系统目标:期望的延迟、流量负载。系统是为延迟还是完整性优化?
  • 了解合规要求:隐私、安全、合规与治理要求,进行责任与偏见审查。
  • 决定托管方式
    • 如果需要在边缘设备运行,则托管在客户端。
    • 如果延迟至关重要,可考虑离线或内存托管(模型需较小)。
    • 否则,运行模型即服务

以下是一个参考公式:

if (need_to_run_on_edge):
    host_on_client
elif (latency_is_critical):
    host_offline_or_in_memory  # 模型需小巧
else:
    host_model_as_service

生产架构模式

常见的生产架构模式包括:

  1. 数据库预计算:将预测结果批量计算后存入生产数据库,应用直接查询。适用于数据量不大、变化不极快的场景。
  2. 内存驻留:将模型编译后加载到应用服务器内存,实现极快推理。适用于推理简单、内存充足的情况。
  3. 模型即服务:这是SRE更熟悉的模式。部署一个模型服务,可以同时运行多个模型版本(实验版、生产版),并通过流量路由进行A/B测试和灰度发布,最终将流量导向性能最佳的模型。
  4. 客户端托管:在终端设备上运行模型,更新频率较低。

生产环境监控的四支柱

在生产环境中,监控应围绕四个支柱展开:

  1. 基础设施:如果你拥有底层设施,需监控CPU、内存、磁盘、网络等。
  2. 服务:监控事务率、响应时间、错误率等传统指标。
  3. 数据:除了数据新鲜度和容量,还需监控数据噪声、可变性和分布漂移
  4. 模型:监控其准确率、精确度、召回率,以及预测漂移和不确定性估计。同时,确保为ML模型构建了CI/CD流程,支持实验、测试和生产环境的模型部署与流量管理。

核心挑战:数据分布漂移 📉

分布漂移是ML生命周期中极其重要的一环。你不能脱离数据分布来谈模型准确率。当输入数据的统计分布发生变化时,模型的性能就会下降。

什么是漂移? 它是输入数据行为的改变。例如,一个销售万圣节产品的商店,其数据中“橙色”商品很多;但当圣诞节来临,“金色”和“红色”商品的数据会变多,分布就发生了漂移。

如何工作? 你基于训练数据得到一个分布,并训练模型。在生产中,你持续收集数据,并计算相同特征的分布。将生产数据分布与训练数据分布重叠比较,如果差异显著,就发生了漂移。

如何测量? 可以使用统计方法(如科尔莫戈罗夫-斯米尔诺夫检验、群体稳定性指数、卡方检验等)计算一个漂移数值。你需要为业务定义“差异显著”的阈值,一旦超过,就触发模型重训练。

需要监控哪些数据的漂移?

  • 训练数据:用于训练模型的数据分布。
  • 输入数据:生产环境中流入模型的数据分布。
  • 输出数据:模型产生的预测结果的分布。
  • 实际数据:如果可能,收集客户反馈的真实结果(Actuals),这是评估模型的最直接依据。但有些场景(如预测20年后的情况)可能无法获得实际数据,此时监控输入和输出的分布漂移就至关重要。

可观测性反模式与关键要点 🚫

在实施监控时,需警惕以下常见反模式:

  • 数据囤积:认为收集更多遥测数据总是好的,从不删除旧数据,这反而会降低系统可理解性。
  • 监控与业务目标脱节:只监控CPU使用率,而不监控端到端用户体验。
  • 永不关闭监控:保留所有不再需要的监控和警报,增加噪音。
  • 仪表盘泛滥:创建无数个仪表盘,但无人能全部查看。
  • 对故障模式缺乏共识:过度依赖个别“专家”。
  • 反应式文化:仅在有事故后才添加新监控。
  • 激励错位:将可观测性工作视为“税费”,而非价值。
  • 可观测性英雄:监控成为一个人的工作,知识没有共享。
  • 只见树木不见森林:只监控独立组件,忽略端到端流程。

给SRE的关键要点

  • ML生产管道与任何大规模分布式系统面临相同的挑战,应以相同方式对待和监控。
  • 既要将系统视为整体,也要关注各个部分。成熟的可观测性模式很可能适用。
  • 必须理解ML质量的具体要求,因为传统信号可能无法告诉你模型的好坏。
  • 提升统计学和概率论知识
  • 设计易于理解的系统,降低复杂性。

给ML实践者的关键要点

  • 设计模块化系统,使其易于理解、监控和排查。
  • 深入理解可靠性、安全性、合规性、数据治理等SRE高度关注的要求。
  • 不要将ML模型“扔过墙”给运维团队,你需要成为系统持续运维的一部分。
  • 前瞻性设计,构建能够扩展且不会过快过时的系统。可以通过重训练机制和多模型策略来增强模型的持久性。
  • 理解大规模分布式系统设计原则

推荐阅读

  • 《Reliable Machine Learning》 - Todd Underwood
  • 《Designing Data-Intensive Applications》 - Martin Kleppmann

总结 🎯

本节课中,我们一起学习了如何将SRE的实践应用于生产环境的机器学习监控。我们认识到,由于ML模型无法100%准确、会随时间退化且身处复杂系统,因此必须对其进行专门监控。我们探讨了SRE与ML实践者在目标和语言上的差异,以及如何通过扩展SRE需求层次、监控数据管道和分布漂移来弥合这些差距。最后,我们强调了设计合适架构、避免监控反模式,以及两个团队紧密协作、共同负责的重要性。记住伯纳德·肖的话:“乐观者发明了飞机,悲观者发明了降落伞。” 在构建可靠的ML系统时,我们需要这两种思维。

003:SRE领域的变革者——演进以管理AI基础设施

在本节课中,我们将探讨SRE(站点可靠性工程)原则如何应用于管理AI基础设施,特别是大规模语言模型训练场景。我们将了解AI工作负载与传统微服务的区别,并学习如何调整SRE的五大支柱(可靠性度量、自动化、可观测性、事件响应和容量管理)来应对新的挑战。

背景介绍与挑战

上一节我们介绍了课程概述,本节中我们来看看AI基础设施的具体定义和当前面临的挑战。

AI基础设施,特别是从模型训练的角度来看,其架构与传统分布式系统有显著不同。一个简化的大规模分布式训练架构包含多个计算节点,每个节点负责计算模型权重的一部分,并通过高速网络交换参数,进行前向和反向传播,最终收敛于损失函数。

从基础设施堆栈来看,我们需要管理模型框架以下的所有层面,包括数据中心、存储层(用于存储训练数据和模型数据)、GPU、网络以及运行这些基础设施所需的所有基础管理服务。当然,还有用于编排一切的Kubernetes。

当我们开始进行模型训练时,面临的最大问题之一是处理训练中断。在训练初期,每天甚至每几个小时就会遭遇多次模型训练中断,这迫使团队必须在凌晨起床以恢复训练。这种中断的代价高昂,因为大量昂贵的GPU资产会因此闲置,造成时间和金钱的浪费。

从工程调试的角度看,如今排查训练作业中断需要不同层次的知识。仅仅检查训练作业日志、系统日志或节点级指标已不足以定位问题。例如,在一个包含数千个GPU的训练作业中,GPU利用率曲线可能剧烈波动。通过深入排查,团队可能发现问题的根源仅是一个GPU芯片上的坏光模块,而这可能是由于安装时的灰尘导致的。这类硬件层面的细微问题,是我们以往在微服务领域难以想象的。

与传统微服务领域相比,AI训练领域存在以下关键差异:

  • 在微服务领域,单点故障的影响通常不大,我们拥有成熟的工具(如踢出故障节点或进行流量切换)来应对,且通常对网络带宽要求较低。
  • 在GPU训练领域,单个节点故障的影响范围(爆炸半径)极大,可能导致上千个GPU闲置。恢复或重新初始化的成本很高,加载大型模型可能需要10到15分钟。此外,GPU架构要求极高的网络通信带宽,所有参数交换都在毫秒级内完成。

可靠性度量的演进

上一节我们了解了AI基础设施的独特挑战,本节中我们来看看如何调整SRE的核心——可靠性度量。

在经典的SRE方法论中,我们有五大支柱。那么,在GPU训练领域,它们意味着什么?首先从度量开始。

在微服务中,我们常用服务等级目标(SLO)来度量可靠性。但在GPU训练中,经典的SLO方法论可能需要一些转变。例如,如何度量一个跨越100个节点的训练作业的服务可用性?99%的成功率有意义吗?如果99%的节点成功,但仅一个节点失败,整个作业就失败了。此外,GPU架构中没有RPC调用链,因此也没有成功率SLO或延迟SLO。

因此,我们更倾向于使用“可靠性度量”这个术语。我们提出了两个我们认为能有效指导工程工作的北极星指标:

  1. 训练时间效率:定义为训练作业中有效训练时间与总运行时间的比率。这衡量了实际资源消耗的利用率。
    • 公式训练时间效率 = 有效训练时间 / 总运行时间
  2. 资源供应可用性:从供应链角度衡量资源供应的可用性,即在给定时间窗口内,总可用GPU时间与理论最大GPU时间的比率。
    • 公式资源供应可用性 = 总可用GPU时间 / 理论最大GPU时间

有了这些目标,我们就可以开展SRE实践来改进这些数字。

可观测性:与数学和统计学为友

上一节我们讨论了新的可靠性度量指标,本节中我们将深入探讨可观测性,并看看如何利用数据和数学解决具体问题。

关键挑战在于如何利用海量监控数据快速识别出性能降级的GPU。我们面临三个主要挑战:

  1. 高基数性:数千个GPU,每个GPU暴露数百个指标,数百个作业可能跨越多个集群,标签组合的数量巨大。
  2. 高分辨率:在微服务领域,通常每30秒或每分钟抓取一次数据点可能就足够了。但在GPU领域,要发现GPU通信或传输速率中的突发问题,需要毫秒级的分辨率,这极大地增加了需要存储的监控数据总量。
  3. 粒度问题:在进行任何形式的聚合之前,我们需要保留所有细节,因为你永远不知道需要深入到哪个层级(例如硬件层级)来定位问题。

高基数性和高分辨率导致指标对人类而言难以阅读。我们需要借助统计工具来自动化分析这些数据。

我们有两个假设:

  1. 行内一致性:在训练步骤中,执行相同角色的多个节点应暴露相似的指标。
  2. 时间一致性:跨训练步骤,无论是第一步还是第一千步,所有节点都应表现出相似的行为。

以下是两个用于自动化分析的“武器”:

1. JS散度
JS散度是一种统计差异方法,可以量化不同概率分布之间的相似性。我们将每个GPU的时间序列指标(如GPU利用率)转换为概率分布直方图,然后应用JS散度计算,生成热图。热图中,绿色表示行为相似,红色或黄色则表示可能存在异常。

2. 傅里叶变换
傅里叶变换是一种强大的技术,可以将数据从时域转换到频域。例如,将GPU卡在10毫秒分辨率下的NVLink传输速率时域图(难以阅读)转换到频域后,可以在频谱中看到一个明显的峰值。这个峰值对应的频率代表了训练步骤的正常节奏周期。一旦知道正常节奏,就可以轻松检测出节点何时失步(训练变慢会导致周期变长)。

可观测性:粒度与追踪

上一节我们介绍了用统计方法分析指标,本节中我们来看看当指标不够时,如何通过更细的粒度来定位问题。

有时我们需要更细的粒度。例如,通过可视化跨多个节点的不同操作的执行时间线,我们可以深入理解训练过程。在模型训练中,有两个主要操作需要关注:

  1. 矩阵乘法:GPU擅长快速执行此类计算。
  2. 网络同步:需要在不同GPU节点间广播权重和计算数据。

通过时间线图,我们可以发现计算瓶颈(某个操作持续时间过长)或异常节点(某个节点在每个操作上都延迟)。这种可视化对于确定优化方向至关重要。

如何获得这种粒度?这听起来很像分布式追踪。我们有一个概念验证实现,使用CUDA编程来检测这类操作,获取追踪数据,并将其转换为Chrome浏览器可读取和可视化的格式。这样,我们就能获得一个时间线视图,清晰地展示跨多个GPU同时发生的操作、它们的持续时间以及最重要的是它们如何随时间对齐。

GPU资源管理

上一节我们探讨了如何检测GPU集群中的异常节点,本节中我们来看看如何管理这些昂贵的资源。

除了可用性度量,我们还需要向高层管理者(如CFO)报告GPU资源的利用情况。我们使用桑基图或瀑布图来追踪GPU资源在各种状态间的流转。这种细分至关重要,因为GPU是极其昂贵的资产,值得我们精心“照看”。

以下是GPU资源的典型流转状态:

  1. 总库存:公司拥有的全部GPU。
  2. 已损坏/待维修:部分GPU损坏,等待更换或维修。
  3. 进入Kubernetes集群:剩余的GPU进入集群,但并非立即可调度(可能因驱动更新、负载测试等原因被封锁)。
  4. 可分配:在所有可调度的GPU中,我们可以将训练作业分配其上,但仍存在因资源碎片化导致的闲置。
  5. 活跃使用:在已分配的GPU中,部分正被积极用于GPU计算,部分可能因模型框架未优化而闲置(这是我们希望优化的部分)。

GPU管理策略:驱动更新与基准测试

上一节我们了解了GPU资源的流转状态,本节中我们来看看具体的GPU管理策略,包括驱动更新和主动基准测试。

首先是一个基本问题:在Kubernetes集群中,更新成百上千个节点的GPU驱动,什么策略好?在微服务领域,我们可能采用渐进式滚动更新,先更新1%、5%,逐步增加到100%,并在每个阶段进行测试以确保一切正常。

但在GPU领域,驱动更新可能会中断训练作业。如果使用渐进式滚动更新,可能会在模型训练期间遭遇多次中断,效率低下。事实证明,与AI团队协调,安排一个维护时间窗口,停止训练,然后一次性完成所有驱动的更新,反而更高效。这表明,传统SRE认为对生产环境最佳的做法,在这种场景下可能并非最优。

除了检测和修复降级节点,我们还能做什么预防措施?我们提出了一个主动的节点基准测试协议,在基础设施管理的不同阶段运行不同级别的基准测试。

以下是不同级别的基准测试:

  1. 基础架构测试:检查Pod能否在单个节点上调度,节点是否基本工作正常。
  2. GPU基础测试:测试硬件错误(如ECC错误、PCIe错误)。
  3. 性能测试:测试计算能力(如使用压力测试查看GPU是否满负荷工作)。
  4. 多节点测试:测试一批节点能否彼此高效通信,以在分布式训练策略中表现良好。
  5. 实际模型测试:运行几个小模型训练步骤,查看节点是否已为下一次训练做好准备。

我们需要在以下五个阶段执行这些基准测试:

  • 生产前验证(节点加入集群前)
  • 大型模型训练开始前
  • 维修后阶段
  • 全面诊断阶段
  • 节点空闲时

实现此类基准测试流程的架构并不复杂,基本上是云原生方法:运行一些Operator,设计自定义CRD,采用声明式驱动设计。编写针对不同GPU的基准测试CRD,与节点级Operator协调以选择正确的节点进行测试,并在测试完成后回收所有基准测试Pod。

总结与展望

本节课中我们一起学习了SRE原则在管理AI基础设施中的应用。

关键要点总结如下:

  1. AI工作负载特性:我们首先简要讨论了AI工作负载的特性,认识到其与传统微服务的显著差异,如高成本、持久性、单点故障影响大等。
  2. 可靠性度量:我们重新思考了可靠性度量,发现传统SLO需要转变,并提出了“训练时间效率”和“资源供应可用性”作为北极星指标。
  3. 可观测性:我们选取了可观测性主题,展示了如何借助数学和统计学工具(如JS散度和傅里叶变换)自动化分析海量监控数据,以及如何通过细粒度追踪可视化训练过程。
  4. GPU管理:我们探讨了GPU管理方法,包括资源状态追踪、高效的驱动更新策略以及主动的、多层次的节点基准测试协议。

关于技能组合,传统上我们是容器中心的SRE。但未来,一切将转向以模型为中心,我们将管理模型。SRE手册中的五大支柱仍然适用,但我们需要学习新的工具和知识,如TensorFlow、PyTorch等。对SRE而言,最有价值的技能不是掌握特定工具,而是我们适应并管理新基础设施的转型能力。

最后需要强调的是,尽管AI技术本身(如AIOps)可能在未来帮助我们,但目前我们仍在追求确定性的解决方案。AI领域发展迅速,我们需要保持开放心态,持续学习,以应对未来的新挑战。

004:慢查询的预防与纠正实战

概述

在本教程中,我们将学习如何在一个像Shopify这样高速发展的大型公司中,系统地预防和纠正数据库慢查询问题。我们将从Shopify的实践经验出发,探讨慢查询对系统稳定性的影响,并详细介绍一套结合了自动化监控、开发流程集成和组织保障的实战解决方案。

1:背景与挑战

大家好,我是Kearney,来自印度尼西亚,现居新加坡。这位是Brad,来自澳大利亚墨尔本。我们是Shopify平台SRE团队的一员,这是一个全球分布式团队。我们的工作是与其他工程团队协作,提升平台基础设施的可靠性、真实性和高效性。

首先,我们将简要介绍Shopify及其与SRE工作的关联。我们会回顾在行星级规模下数据库弹性的基础知识。然后,你将了解到我们在像Shopify这样开发速度极快的大公司中,为纠正乃至预防慢查询所发现的机会和构建的解决方案。最后,我们将总结一些可供你实践的行动项。

Shopify的使命是让商业对每个人来说都更好。自2006年以来,全球数百万企业家信任我们来运营他们的业务。一年中最大的购物周末是黑色星期五网络星期一。去年,我们的商家销售额峰值达到每分钟460万美元。我们的商家实现了115亿美元的销售额,较前一年增长24%。我们的基础设施处理着57PB的数据。边缘层的请求速率峰值达到每分钟2.8亿次。我们的数据库总共服务了10.5万亿次查询。

在Shopify内部,有一个名为“基础设施”的部门,其中包含一个我们所属的“弹性”组织。该组织专注于结账、店铺前台和商家管理后台。整个组织的使命是让开发者更容易地实现弹性,并将其作为我们文化的关键部分。我们的团队“平台SRE”就在这里,我们构建工具和流程来提高可靠性。最重要的是,我们遵循“太阳永不落”的原则,7x24小时轮值,负责指挥事故处理,即在“着火”或发生事故时保持冷静和专注。

作为平台出现问题时第一个被通知的团队,我们确保召集所有必要人员来缓解事故,引导讨论,收集相关上下文。事故缓解后,我们会跟进,解决根本原因并进行正式的复盘。我们相信分享经验、工作和想法,甚至记录项目成果(无论好坏)。这与最初的SRE手册强调故事讲述和学习的精神是一致的。这也是我们站在这里分享我们项目的原因。

2:数据库弹性基础

上一节我们介绍了项目背景,本节中我们来看看Shopify为确保数据库弹性所做的历史性投资。当慢查询耗尽数据库容量时,首要想法通常是购买更多容量。以下是短期内扩展数据库的主要方式:

  • 垂直扩展:使用更大的实例,升级存储等。
  • 水平扩展:添加更多实例。例如,如果有只读副本,可以添加更多来分担负载。
  • 配置调优:确保配置已优化以充分利用提供的资源。

然而,这只是短期扩展。除此之外,需要进行长期的弹性投资,因为单个数据库的扩展能力有限。以下是Shopify在过去10到15年间为提升MySQL弹性所做的最重要的六项投资:

  1. 水平分片:将租户分区到不同的数据库实例中。这提供了故障隔离(舱壁模式)和隔离重度用户的能力。缺点是应用层更复杂,且可能需要平衡分片负载。
  2. 服务提取:从核心单体应用中提取出自包含的服务。例如,Shopify将店铺前台渲染的只读部分提取为独立服务,以减轻核心系统负载。
  3. 快速失败机制:使用超时和断路器。当检测到持续的错误条件时,快速失败并将压力返回给应用层。
  4. 使用ProxySQL:作为数据库代理,用于复用连接、替换后端数据库服务器,并提供查询的聚合指标。
  5. 安全的模式迁移:使用类似“LHM”的工具进行零停机、可扩展的模式变更。同时,通过功能标志和针对金丝雀集群的合成流量测试来快速回滚变更。
  6. 赋能开发者:提供关于如何编写高效查询的文档和工具。例如,使用EXPLAIN命令理解数据库内部行为,并提供类似GitLab的交互式工具,让开发者能在生产数据集上实验新查询和索引。

这些构成了我们项目的起点。我们拥有非常坚实的设置,但它并非魔法,问题仍然会出现。这基本上是当今大规模部署数据库的“入场券”。然而,正如Kearney所说,我们仍然观察到大量由慢查询引发的事故。因此,我们项目初期的目标是减少由慢查询引起的事故频率和严重性。计划是通过检查我们的弹性设置,识别平台和流程中的任何缺口。

3:纠正策略:从监控到自动化

上一节我们回顾了数据库弹性的基础,本节中我们来看看当慢查询已经进入生产环境后,如何有效地纠正它。我们优先关注两个主要领域:预防(阻止慢查询被部署并首先引发问题)和纠正(防止其引发事故,即及时检测、缓解和修复生产中发现的问题)。我们先从纠正开始。

现在你遇到了一个慢查询,该怎么办?在深入探讨之前,我们先看一个典型的开发周期:编码、构建、测试、部署,然后进入监控阶段。在最坏的情况下,观察到系统状态降级,进而进入事故处理阶段。

对于一个慢查询事故,其生命周期通常如下:开始时数据库容量耗尽,开始调查负载来源,然后进入缓解阶段,通常需要通过应用防火墙规则、API限流等方式减少负载单位。最后是修复阶段,我们与开发团队合作,理解查询意图并找到性能更优的方案。这是我们处理所有防御措施都未能拦截的事故的通用流程。整个周期耗时很长,是一个刻意严谨的过程,旨在从独特故障中学习并确保不因同一原因失败两次。但当同一模式反复出现时,它的效用就会降低。

我们的目标是找到缩短反馈循环的机会,本质上是在这个图表上“左移”。我们观察到先前系统中的缺口或不足之处有以下几点:

  1. 对事故的依赖:只有在产生影响后才发现问题。
  2. 缺乏所有权和问责制:问题或行动项可能被无限期搁置。
  3. 开发者调试资源碎片化:需要在不同系统间跳转。

接下来,我们看看构建的系统如何解决这些缺口。首先,从高层次概述我们构建了什么:

  1. 数据库层:MySQL服务器会发出慢查询日志(当查询执行时间超过某个可配置阈值时)。
  2. 数据管道:使用开源的Vector可观测性管道读取、预处理日志,并将其同步到内部的可观测性服务Observe
  3. 监控与告警:在Observe中配置监控规则,基于P99执行时间、获取数据大小等字段聚合日志数据。如果查询超过阈值(例如P99超过1秒),则会触发监控。
  4. 自动化处理:触发后,除了向告警通道发送Slack消息,更重要的是会向“慢查询审计服务”发送Webhook。该服务会自动在行动项跟踪器中创建任务,并自动分配给相关团队。它还会存储查询元数据以防止为同一慢查询创建多个未解决的任务。

以下是我们的系统如何解决上述缺口:

  • 解决对事故的依赖:自动化告警创建行动项,移除了团队先前的手动工作。由于我们聚合了每个查询的数据,可以在其演变成全面事故之前捕获慢查询,并优先修复最严重的违规者。
  • 解决缺乏所有权和问责制:自动化分配流程(基于已明确定义且易于检索的表和查询所有权)。工程领导层支持将慢查询修复作为公司重点。如果行动项逾期未处理,作为最后手段,相关团队或组件的代码合并(PR)会被阻止,以此鼓励问责。
  • 解决资源碎片化:构建了一个统一的Grafana仪表板,集中展示重要信息(如统计信息、日志和追踪链接)。在行动项描述中直接提供上下文洞察(如查询来源、控制器名称)。提供关于常见不良模式及解决方法的建议。目标是改善开发者体验。

总结一下纠正措施解决的缺口:对于事故依赖,我们主动检测慢查询并通过自动化扩展纠正流程;对于所有权缺失,我们自动创建并分配行动项,必要时阻止部署;对于资源碎片化,我们提供了统一的上下文信息展示系统。这些纠正措施位于“监控”阶段,我们成功实现了左移。但我们能否进一步左移,在开发周期更早的阶段做些什么呢?接下来,Brad将介绍预防方面。

4:预防策略:在开发流程中左移

上一节我们探讨了如何纠正已出现的慢查询,本节我们来看看如何在问题发生之前进行预防。预防工作的核心是关于部署前的开发者工作流程。我们希望持续在更早的阶段发现问题并左移,减少反馈循环。另一个好处是,这将工作分配回开发者,并保留在开发过程中可能丢失的上下文(等到事故或监控发现问题时,上下文往往已丢失)。

我们构建了一个系统,用于在持续集成(CI)环节捕获慢查询,防止其被部署。它检测测试过程中遇到的新查询(使用Active Record订阅者),保存它们,并自动对新查询运行EXPLAIN分析。目前,我们主要检测全表扫描,这在我们这样的规模下是不可持续的操作。

例如,几周前,一位开发者正在开发一个新的维护任务。代码中有一行类似Location.where(deleted: true).map(&:trusted_id),这会生成一个需要全表扫描的查询。我们如何在开发者合并代码前提醒他们呢?

我们的工作流程如下:

  1. 使用GitHub托管代码。当分支合并到主分支时,我们会运行完整的测试套件。
  2. 将测试运行期间捕获的所有查询提交到我们称为“查询追踪器”的系统。
  3. 下次有新的拉取请求(PR)打开时,在测试PR变更之前,我们会获取主分支中当前已有的所有查询(这些已知查询会被忽略)。
  4. 设置订阅者来捕获所有SQL查询,在测试套件结束时,对未曾见过的新查询运行EXPLAIN
  5. 如果发现任何全表扫描,会再次通过查询追踪器确认是否为全新查询。如果是,系统将在PR上发布评论告警。

告警评论会解释情况,包含我们捕获的堆栈跟踪(以便开发者定位是哪个测试、代码何处调用的),并链接到需要检查的工具。我们的团队也会在Slack上收到通知,以便监控系统性能和误报情况。

那么开发者是如何处理这个通知的呢?最终的代码版本添加了一个WHERE shop_id = ?子句。在Shopify,所有分片表都有shop_id属性,大多数查询都应包含此条件。添加WHERE条件意味着可以使用索引来选取特定行,而无需扫描整个表。此外,查询中使用了SELECT *,但实际只需要trusted_id,通过使用更高效的Rails方法(如pluck)可以避免获取不必要的数据。

这就是我们将慢查询预防集成到开发流程测试阶段的方式。我们还在考虑进一步左移,例如在开发者编写代码时就能检测查询,或者开发IDE插件,在代码提交前就能检查查询。

5:关键要点与行动建议

在上一节中,我们介绍了预防慢查询的具体实践,本节我们将总结从整个项目中提炼出的关键要点和可供你带回团队的行动建议。

以下是一些要点和“家庭作业”,其中一些可能不那么具体,更多是哲学层面的思考和需要牢记的事项:

  1. 减少反馈循环与左移:考虑如何在你的组织中最好地实践这一理念。这能减少浪费的精力(事故停机时间、调查时间、重新收集丢失上下文的时间)。评估在开发生命周期每个阶段的平台和流程弹性。
  2. 构建轻量级工具与自动化:尝试构建轻量级工具和自动化流程,以呈现系统底层正在发生的情况。它甚至可以基于你已经收集的指标(就像我们的慢查询日志早已被收集但未被有效利用)。你可能会对表面之下的发现感到惊讶。
  3. 在自动化的警告与强制执行方面发挥创意:如何让开发者更容易在一开始就做对事情?让做正确的事变得容易,让做错误的事变得困难。因为人为错误不可避免。例如,高亮低效资源使用的可见性,或者像我们那样在必要时阻止部署。这显然需要获得支持以及健康的组织结构,以便能将问题向上推送,并由决策者权衡可靠性与功能工作孰轻孰重。
  4. 通过自动化减少琐碎工作:在这方面保持务实。优先考虑能带来快速胜利的事情。例如,如果还没有为数据库查询设置超时,就设置合理的超时。
  5. 从消耗最多容量的查询入手:使用数据驱动决策。我们实际上会根据读取的行数计算所需的I/O量,如果超过阈值,从第一性原理可知数据库将难以服务该负载。
  6. 专注于不可扩展的解决方案和可持续性:将工作委托和分配回仍保有上下文的开发者,这将减少反馈循环。保留上下文并持续自动化。这是我们主要的收获:必须委托工作并将努力分配回合适的地方。

总结

在本教程中,我们一起学习了Shopify平台SRE团队应对慢查询的实战方法。我们从数据库弹性基础入手,了解了水平分片、服务提取、快速失败等核心概念。接着,深入探讨了纠正策略,即通过自动化监控、告警和行动项管理,在慢查询引发事故前进行干预和修复。然后,我们学习了预防策略,通过将慢查询检测(如全表扫描)集成到CI流程中,在代码合并前就发现问题并反馈给开发者,实现了进一步的“左移”。最后,我们总结了减少反馈循环、构建自动化工具、发挥创意执行、数据驱动决策等关键要点。这套结合了技术方案、流程优化和组织保障的体系,为处理大规模、高速度开发环境下的慢查询问题提供了宝贵的实践蓝图。

005:速度优化之旅 🚀

在本节课中,我们将跟随一位SRE专家,学习如何系统性地诊断和优化一个复杂、不透明且存在性能问题的分布式搜索服务。我们将从零开始,通过科学的方法、严谨的监控和反复的实验,最终显著提升服务性能并降低成本。


背景与挑战

上一节我们介绍了课程的整体目标,本节中我们来看看我们面临的初始状况。

我是Scott Lair。本次分享基于2024年在Figma公司进行的一系列工作。Figma是一家为设计师提供工具的公司。许多产品、网站,甚至我汽车仪表盘的初稿,都可能是在Figma中设计的。就像使用Google Docs一样,用户最终会积累大量草稿、一次性项目和草图,找到所需文件变得困难。因此,文档搜索对Figma用户至关重要。

2024年,我开始与Figma内部的文档搜索团队合作,以解决其延迟、扩展性和可靠性问题。团队对我的加入感到兴奋,因为在我加入Figma之前,我在谷歌担任了17年的SRE。他们理所当然地认为我精通搜索。

然而,事实并非如此。我对此一无所知,从未接触过搜索领域。团队最初的希望很快破灭了。Figma的搜索功能自公司早期就基于Elasticsearch构建,但多年来缺乏维护。它运行在AWS上一个三年旧版本的Elasticsearch上,内部以各种方式不稳定著称,无法为用户提供理想的体验。

公司最近开始为搜索团队增加人手,并计划大力改进。那么,问题具体是什么?通常的问题都有:服务经常很慢,用户有时会遇到数秒的搜索延迟;流量稍有变化,整个服务就可能陷入停滞;我们在搜索上花费了大量资金,但感觉物非所值。最糟糕的是,我们并不真正理解它为何慢、不稳定或昂贵。团队大多数人都是搜索领域的新手,仍在学习。

积极的一面是,我们有一个出色的团队,特别是在搜索相关性方面,这很不寻常。我们有数据科学家在分析一切。我们理解搜索结果一旦呈现,如何服务于用户。

幸运的是,修复工作已经启动。团队开始用AWS上托管的最新版OpenSearch实例替换我们古老的Elasticsearch。OpenSearch是Elasticsearch的一个分支,由亚马逊在Elastic更改许可证后创建。本次分享的内容基本同时适用于两者。

Figma正在引入人员提供帮助,我本应是专家之一。但我对Elasticsearch的了解,可能还不如他们对谷歌搜索的了解多。我对此毫无头绪。团队也明白我远非搜索专家。在最初的介绍之后,我们决定无论如何,有工作要做,开始着手修复。这触及了本次分享的核心:当你几乎不知从何开始时,作为一名SRE,你该如何处理问题?

我决定做一些感觉有点奇怪的事情,因为这类事情我们总认为应该做,但几乎从未真正实践。我最终运用了科学方法,而不是依赖直觉。


第一步:明确现状与科学方法

上一节我们了解了混乱的起点,本节中我们来看看如何用科学方法理清头绪。

这个过程的第一步是试图弄清楚我们知道什么,不知道什么。我们的起点在哪里?我们拥有大量的仪表盘, literally 数百张图表。有些图表有数百条线,因为“越多越好”。我们有告警系统。有客户向我们发送措辞礼貌、设计精良的“投诉邮件”。我们对问题有各种猜测,但没有任何确凿证据。

当然,我们可以直接投入更多的CPU、内存和服务器。也许这会有帮助。但我们甚至无法知道是否真的有效,甚至可能让问题变得更糟。

作为SRE,我们从监控开始。我们试图回答最基本、最核心的问题。当时,我不关心文档摄取延迟、JVM GC统计信息或发布延迟。我只想知道三件事:

  1. 我们每秒处理多少客户搜索?
  2. 搜索耗时多长?
  3. 我们返回了多少错误?

这是任何服务最基本、不复杂的问题。我们在监控中拥有所有这些问题的答案,但它们分散在两个不同的仪表盘上。没有一个答案位于任一仪表盘的首屏,它们甚至在仪表盘上彼此远离。但最糟糕的是,它们都是错误的。

它们必须是错误的,因为我们有多个图表回答每个问题,而它们彼此矛盾,有时相差100倍甚至更多。这就是科学,对吧?

所以第一步是弄清楚这些指标的正确值是什么。我们可以查看所有不同的数据源,找出差异所在。检查我们的测量方法,看看我们实际知道什么。然后筛选出哪些数据最准确、最可能为真,并以此为基础开展工作。

此时,我只想收集一些数据,以了解情况到底有多糟糕。搜索在内部名声不佳,但对我来说大多时候还能工作。我只想了解发生了什么,而不是原因,仅仅是它被使用的频率有多高、速度有多慢、以及何时返回错误。

欢迎来到中学科学课堂和科学方法。我们将收集一些数据,形成一些假设,进行测试,发布一些结果,然后迭代。


数据收集与指标混乱

上一节我们确立了科学方法,本节中我们来看看在数据收集阶段遇到的第一个挑战:指标混乱。

首先,我们收集数据。从搜索速率开始,这似乎很简单:我们每秒进行多少次搜索?

我们拥有来自OpenSearch的数据,显示它为我们执行了多少次搜索。你可能会认为这很准确。难道我们会在它不知情的情况下执行搜索吗?我们还拥有前端的数据,显示有多少查询命中了我们的搜索API。我们控制着代码,这应该相当准确。我们应该能够理解它。

这两个数字相差了500倍。OpenSearch认为它执行的搜索量远高于我们的前端。

好吧,查询速率很难衡量。也许我们从其他地方开始。试试延迟。

OpenSearch说它的平均延迟是8毫秒。平均,真是看待延迟的一个有趣方式。我们拥有许多TB的数据,分布在数百个分片上。在8毫秒内得到答案似乎令人印象深刻且不太可能。但好吧,我们的前端声称P99延迟是一秒。

但我除了调用OpenSearch之外,还做了一堆额外的工作,比如消息检查。我们花了多少时间在那上面?我们没有相关指标。我们甚至不知道这些时间是花在处理上还是调用OpenSearch上。我们所知道的就是,一边是8毫秒,另一边是1000毫秒。科学,对吧?

所以当有疑问时,增加更多检测点。我们添加了一堆额外的跟踪跨度和指标。我们将对OpenSearch API的调用包装在直方图中。我们追踪了一切可能的内容。我们仔细检查了所有现有指标,确保它们确实测量了它们应该测量的东西。

大多数指标是准确的。我们还深入研究了AWS的CloudWatch。30分钟后,我们确定虽然我们的平均查询时间是8毫秒,但AWS实际上知道查询的P99。它是9毫秒,有时是10毫秒。

这并没有帮助我们。与此同时,通过我们的新代码,我们终于知道我们到OpenSearch的出站延迟是200到400毫秒,具体取决于一天中的时间和负载等因素。查看我们的跟踪记录,我们看到绝对最小值是40毫秒。所以OpenSearch最坏情况可能是10毫秒,而我们最好情况是40毫秒。我们是不是不小心在错误的大陆一侧调用了OpenSearch?

因此,我们继续为代码添加更多检测点,一些模式开始出现。我们在夜晚和周末速度很快,在高峰时段很慢。但当我们查看来自OpenSearch的数据时,它真的没有任何相关性。就好像我们测量的不是同一件事。

所以,你需要知道你测量的是什么。我确信此时人群中有一群人在摇头说:“他说得对,他真的不知道Elasticsearch是如何工作的。”但对于其他人,让我带你快速了解一个查询。

我注意到我是被雇来做SRE工作的,而不是图形设计。所以这里的“App”是我们的代码,其余部分是OpenSearch。

我们有一个协调节点,它充当API的前端。我们将发送给它一些东西,然后它会与一堆工作节点通信。每个工作节点在本地磁盘上拥有我们数据的若干分片。

我们的应用程序发送一个API请求,例如“搜索食物”,到协调节点。它进行一些检查和查询优化。然后,由于分片查询被发送到每个工作节点。需要明确的是,出站查询是发送到每个数据分片,而不仅仅是每个节点。如果我们有200个后端节点和200个分片,那么就有200个出站查询,而不是20个。分片以某种模式分布在节点上,并具有一定程度的冗余。

每个工作节点将执行一次搜索,并返回一个答案,例如“来自我的分片的前70个结果”。这些结果返回给协调节点。协调节点获取所有这些结果,对它们进行排序、合并,然后返回去请求关于得分最高的文档的更多信息,比如“是的,你给了我文档ID。现在,你能给我一个可以返回给用户的片段吗?”等等。

这些信息返回后,协调节点打包答案,将响应发送回你的应用程序。

我解释这一切的原因是,我们发现OpenSearch/Elasticsearch的所有指标都是“每个分片”的。我们拥有的来自OpenSearch端的每一个指标都测量的是每个分片的延迟、每个分片的错误、每个分片的查询速率。它不是每个API请求,也不是每个节点,而是每个分片。

协调节点合并部分结果花了多少时间?长尾延迟如何影响事情?是否存在排队延迟?不知道,它不测量这些。

我们肯定做错了。对于一个成熟的产品来说,这似乎令人惊讶。所以我们去阅读OpenSearch的文档。它们很糟糕,完全没有帮助。然后我们阅读了Elasticsearch的文档,好一些,但仍然没有帮助。

我们向AWS支持人员开了一些支持工单。只能说他们不是监控专家。然后我们与AWS的一些OpenSearch开发人员交谈,他们都显得有些躲闪和愧疚,并表示修复这个问题已经在他们的路线图上了。

好吧,既然我们不能真正信任OpenSearch的指标,我们决定必须专注于前端指标:从我们的站点看到了什么。所以我们编写了一些探针来与OpenSearch通信,添加了一些额外的跟踪数据,并积极地将每一个API调用包装在我们的监控中。

慢慢地,我们开始理解事情。我们开始获得自洽的数据,我们有理由相信它是真实的。也许事情没那么糟。哦,我们仍然有一秒的延迟。查询有时仍然会慢到爬行,我们仍然在支付高昂的费用。但这是关键的一点:我们终于可以进行更改,并知道它们是否有效。


建立假设与测试框架

上一节我们终于获得了可靠的数据,本节中我们来看看如何基于数据形成假设并进行测试。

现在我们可以开始修复问题了。是时候开始提问了。我们已经度过了数据收集阶段,开始朝着假设的方向前进。

我们能否优化查询使其运行更快?我们是否一直在做相当于全表扫描的操作,还是它们相对高效?如果我们的P99延迟是400毫秒,而搜索前端API延迟是一秒,其余时间花在哪里了?是否有OpenSearch配置标志和更改可以帮助我们?我们能否通过增加硬件来获益?更改AWS OpenSearch实例类型会有帮助吗?升级到新版本的OpenSearch会有帮助吗?

你会注意到,很多这些问题都是“如果我们改变X,会发生什么”。这有点棘手,因为我们没有一个好的方法来测试。所以问题来了:我们如何针对OpenSearch对一堆测试查询进行基准测试?肯定有工具可以做到这一点。我想知道它叫什么。

哦,看,OpenSearch Benchmark,OpenSearch项目的一部分。我大胆猜测,也许这就是我们正在寻找的,就在GitHub上。它是他们项目的一部分,已经存在很久了,是从一个功能相同但名称不那么明显的Elasticsearch工具分叉出来的。

我花了大约两周时间试图从中获得好的结果。不幸的是,它实际上是一个用于回归测试新OpenSearch构建的工具。对于我真正想用它做的事情来说,它的用途有限。如果你想知道我们遇到了什么问题,可以在之后问我。

在与这个工具半心半意地斗争了几周后,我终于感到非常沮丧,花了一个下午用Go语言写了一个替代品。这真的没那么难。它运行得很好,扩展性更高,给出了更好的结果,能够进行更多测试。它对于进行发布回归测试几乎肯定很糟糕,但对于我们想要的目的来说,它工作得很好。

现在我们有了一个工具。我们可以向OpenSearch实例发送大量查询,并测量它在不同条件下的表现。低查询速率下的延迟是多少?随着负载增加,它的行为如何?它在哪里崩溃?

现在是时候运行一些基准测试斜坡了。我们测量了我们的预发布实例,从最简单的地方开始。我们学到了一些东西,用它来构建一个新的预发布实例,然后是第三个预发布实例。然后,在非高峰时段,我们非常小心地针对生产环境运行了一系列低强度测试,以了解情况到底有多糟糕。

接着,我们构建了一个生产环境的克隆,并运行了数百次测试,改变不同的参数,观察发生了什么。我们建立了一个关于系统实际行为的工作模型,并用它来驱动更多测试。重要的一点是,我们实际上在内部写下了结果。我们测试了这个,我们认为这会有帮助。我们做了这些测试,这是结果。我们在团队中分享这些结果,在Slack上争论,然后第二天一遍又一遍地重复这个过程。

我们更改了配置标志,更改了分片数量,更改了AWS实例类型,尝试了不同版本的OpenSearch。我们预期会有效果的事情,几乎都没有产生我们预期的效果。


关键发现与优化措施

上一节我们建立了测试框架并开始实验,本节中我们来看看通过实验得出的关键发现和具体优化措施。

为了加快速度,我们发现需要更少的数据分片。我们分片太多了,减少这个数量确实对我们有帮助。最初,我们以450种方式绘制了我们的数据图表,认为很多分片能提供良好的吞吐量。但在这种情况下,吞吐量好,延迟却很差。我们只关心延迟。在基准测试中,任何超过大约200个分片都会显著增加延迟。它只是不断上升。

我们猜测是扇出成本、额外的协调节点负载、等待第450个分片响应的长尾效应。在第一轮更改中,我们将分片减少到180个,这带来了不错的提升。为所有这一切选择合适的分片数量有点像一门“黑魔法”,同样,欢迎之后问我。

但是,添加新节点、额外节点并没有帮助。我们实际上只使用了大约10%的CPU。增加更多节点只是移动了空闲线程。

AWS特别建议我们开启的大多数配置选项,实际上要么对我们没有特别的好处,要么有害。例如,他们建议开启并发段搜索,这应该有助于搜索并行化。我们有很多空闲CPU,并行化一些搜索肯定会有帮助。在测试中,它一开始就增加了几个百分点的成本。随着负载增加,它只是变得更糟,从未帮助我们。我们没有发现任何对它有益的工作负载。我们有很多这样的设置,必须全部测试。

不知何故,我们的查询实际上非常好,这让我感到震惊。我最初怀疑这是我们的问题,我们又在做相当于全表扫描的事情。OpenSearch的查询优化器有点神奇,并且喜欢我们的查询。每个分片通常只触及磁盘上大约100个文档。它能够使用我们构建的一堆廉价过滤器来筛选它应该查看的内容,做了正确的事情。这有点神奇,完全不是我预期的结果。

AWS支持人员不断声称每个版本都有巨大的改进。我们从未看到。我们在这里或那里获得了几个百分点的提升,没什么重大的。我们滚动升级到了当时的最新版本,因为它没有任何性能回退,而且我们最终无论如何都必须这样做。

后来我们对向量搜索进行的一些测试在一次升级后确实取得了巨大的胜利。但那是另一天或者另一个房间的话题了。

那么,换成CPU更少、内存更多的节点可能是我们最大的胜利。这是我们或多或少预料到的。这是我们的大问题。当时,AWS有大约139种不同的OpenSearch实例类型可供选择。在排除所有遗留选项后,我们仍然面临一大堆选择。我们想要什么样的CPU与内存比例?我们想要更多、更少但更大的节点,还是更多、更小的节点?我们可以测试这个。

我们能够更改为一个实例类型,它为我们提供了三倍的内存,但只有三分之一的CPU,而每个节点的价格大约只有一半。它实际上明显更快。我们在过程中发现的一件事是,即使是极少量的磁盘读取也会对我们的搜索延迟产生巨大影响。这在历史上本不应令人惊讶,但我们的整个搜索索引必须存在于Linux磁盘缓存中,因为我们默认在EBS上运行OpenSearch存储,而EBS即使使用SSD,延迟也很差。这不是亚马逊或EBS独有的问题。我在谷歌的等效产品上工作过。这是复杂存储层的根本问题。

所以事实证明,向问题投入内存是我们唯一能看到的大而明显的胜利。回顾过去,我们看到的大部分中断至少部分是由缓存未命中问题引起的。东西被加载,缓存命中率下降,总I/O飙升,查询开始花费足够长的时间以至于它们没有在超时前完成。大家都知道这个故事。

我们推出了所有这些更改:更少的分片、不同的实例类型、更新的OpenSearch版本、一些我们知道安全的次要标志调整。如果我们没有建立一个良好的测试框架,我们不可能安全地完成任何这些;如果没有我们在开始时做的所有监控工作,我们也不会知道它是否有效。

这是一个好的开始。我们大致将预算削减了一半。延迟下降了,错误减少了,超时减少了。但我们才刚刚开始。


深入分析与进一步优化

上一节我们通过基础设施调整取得了初步胜利,本节中我们来看看如何深入分析并优化应用层代码。

我们了解到的一件事是,大约三分之一的用户可见搜索延迟实际上来自OpenSearch。根据阿姆达尔定律,如果我们继续加速这一部分,我们能获得的收益是有限的。另外三分之一的时间花在构建查询上,最后三分之一花在验证每个结果的权限上。我们最不希望的就是竞争对手的文档出现在你的搜索结果中。

团队的一些成员仔细研究了我们从这一切中获得的所有数据。我们遇到的一个大性能问题实际上是一个与ActiveRecord相关的、棘手的平台问题,一年多来没有人注意到。当我们修复它时,整个公司的速度提高了20到50毫秒,并且它解锁了我们已实现但出于某种原因从未带来延迟优势的多线程工作。所以这很有趣,我们从一些代码更改中获得了一些额外的性能。

进入这个项目时,我们怀疑我们的问题之一是搜索索引中有太多无用的数据。我们知道其中包含的数据从未真正用于搜索,或者只在极端情况下使用。我们携带了大量额外数据。我们知道这一点,但我们并不真正知道它给我们带来了什么成本,而且没有人愿意盲目地进行复杂的更改。当时,我们无法知道我们对搜索数据所做的任何更改需要数月时间才能真正了解它如何影响用户搜索。

现在,我们处于可以测试其中一些内容的阶段。我们能够从搜索索引中移除争议最小的数据,将其大小减少了50%,这给了我们另一个巨大的速度提升。一切都更快了。更好的是,高峰和非高峰时段的延迟差异开始缩小。一旦数据加载完毕,我们实际上没有进行任何磁盘读取。一切都驻留在内存中。

我们回到绘图板,划掉了所有我们不需要的东西,运行了更多测试,并从搜索索引的大小中又砍掉了90%。里面有一些几乎永远没用的东西。我们能够进一步减少分片数量。一切都很美好。我们大幅削减了搜索节点的数量,从大约60个节点减少到最后的15或20个。只是出现了一个微小的延迟波动。事情开始看起来不错了。

我指的不仅仅是搜索。任何与谷歌人交谈过的人都知道,延迟几乎是一个宗教话题。在这种情况下,迭代速度才是我真正关心的。我们刚开始时,运行实验需要很长时间,启动测试集群需要数周,运行不同的索引测试需要数月。随着时间的推移,我们能够显著加快所有这些速度。所以我们真的可以测试东西,得到答案,并决定做什么,有时在几小时内,有时在几天内,而不是几周或几个月。没有这一点,我们就无法前进。你真的需要能够理解更改的风险和回报。一旦你理解了这一点,你就可以向管理层推销,说服所有人,前进就变得容易了。


经验教训与总结

上一节我们完成了从基础设施到应用代码的全面优化,本节中我们来总结整个过程中的经验教训和关键建议。

并非一切都是美好的。有些事情并不完全像我们预期的那样顺利。我指的不是失败的实验,那是预料之中的。有些我们预期会有帮助的事情却没有效果。奇怪的是,寻求帮助几乎从来没用。我们花了很多时间与一线支持人员交谈,他们又与专家交谈。然后我们与一些专家开会,他们拉来了一群开发人员,我们从他们那里得到了大量建议。但当我们测试时,很少有建议真正适用于我们的情况。

问题是OpenSearch被用于大量不同的用途。我们将其用于文档搜索:我们有一堆文档,请找到最适合用户正在寻找的内容的那些。它被大量用于日志类用途:人们将日志倒入其中,然后在上面进行搜索以试图大海捞针。它也被大量用作跟踪、指标、可观测性平台,很多AI和向量搜索的东西也在那里涌现。它们都有不同的需求。我们发现的大多数旧文档都真正专注于日志和类似日志的搜索。对于那种用途,增加更多分片,增加更多磁盘带宽,它会更快。延迟?你为什么要关心搜索日志的延迟?所以我们得到的很多东西,无论是在网上找到的、搜索到的,还是与开发人员交谈得到的,都是建议增加更多分片、做这个做那个。一旦我们有了数据,这些恰恰是我们最终必须做的反面。

很多新文档都是基于AI的,调优完全不同。对我们来说,最重要的事情是减少分片。我们分片太多了,减少分片,然后尽可能多地投入内存。

一些通用的痛点,作为SRE的强制性吐槽:可能我最大的痛苦来源是别人的仪表盘。人们为错误的问题构建仪表盘,或者至少不是我的问题。我可以像这里的大多数人一样,就什么是好的仪表盘吐槽几个小时。但根本上,监控仪表盘是一种沟通工具。它在那里讲述关于你服务的故事。如果它讲述的是关于你服务的真实故事,那就太好了。故事不应该是“有300件不同的事情在发生,滚动查看一会儿”。故事也不应该是“我们为同一件事选择了35种不同的度量单位”。仅仅专注于SLO,你就可以取得很大进展,这是我们在这里做的第一件事。即使是非正式的,从对用户重要的东西开始。确保你有这些的图表。有哪些次要因素会影响这些?为那些因素准备一些图表。是否有其他因素会影响这些?关键是专注。

另一件每次做都让我惊讶的事情是,监控工具用于实际数据分析时是多么糟糕。试着绘制一下一周内所有实例的查询延迟与磁盘IOPS速率的关系图。你会得到一个散点图或热图。有时你只需要导出数据并使用不同的工具。谷歌的Borgmon和Monarch在这方面很糟糕。Datadog也很糟糕。Grafana大多可以做到。然后是黑盒软件即服务。有时真的很痛苦,但事实就是如此。

所以,用一些具体的建议来总结。如果你正在运行OpenSearch或Elasticsearch,并且你来这里是因为你想从人们那里得到一些关于如何让它更快的建议:

  • 密切关注IOPS以及它从磁盘读取了多少数据。
  • 如果延迟很重要,尽可能保持索引小。
  • 保持分片数量少,并确保分片均匀分布在所有节点上。这就是分片大小调整的“黑魔法”所在之处。

对于所有事情,运行实验,测量结果,写下并分享你的结果。SLO基本上是魔法。它们迫使你写下你关心什么,并与整个团队讨论。这就是全部魔法。它让你专注于重要的事情。仅仅因为它简单,并不意味着它不是魔法。


本节课中我们一起学习了如何系统性地处理一个不透明系统的性能问题。我们从建立可靠的监控和测量开始,运用科学方法形成假设,构建测试框架进行验证,最终通过减少分片、优化硬件配置、清理索引数据和修复应用层问题等一系列措施,显著提升了搜索服务的性能、可靠性和成本效益。关键在于:从用户可感知的指标出发,用数据驱动决策,并通过快速迭代来验证想法。

006:案例研究——一次真实的惊群效应

大家好,下午好,非常感谢大家参加本次演讲。

我的名字是 Nicholas Aroji,很高兴来到这里。

我是彭博社订单与交易捕获团队的 SRE 架构师。

在过去的几年里,我的大部分工作都与性能和可扩展性相关。

我对现有系统进行优化,排查性能问题,并开发和推广性能剖析工具。

今天,我想和大家分享一个关于一个让我非常困扰的问题的故事。

这个故事在结尾处有一个反转,我认为你们会觉得它很有趣。

让我们开始吧,首先谈谈什么是惊群效应问题。

惊群效应是一个性能问题,当多个线程等待同一个事件,并且同时被唤醒时就会发生。

如果只有一个线程能够处理该事件,其他线程就会因空转和不必要的上下文切换而浪费资源。

情况就是这样,有一个事件,多个线程在等待它。

通常是对一个文件描述符的操作。事件触发,所有等待的线程变为可运行状态,但只有第一个被唤醒的线程会处理该事件。

其余线程占用一个 CPU 核心,却发现其他线程抢先处理了事件,然后立即又回去休眠。

如果等待事件的线程数量很大,这可能就是一个问题。

如今这已经不是大问题了,你很少再听到关于惊群效应的讨论。

在过去,典型的情况是,你会有多个线程或任务运行在同一个套接字上。

当新连接到达时,每个线程都会被唤醒,尽管其中只有一个会处理连接。其余的会回去休眠。

当服务开始处理大量活动时,这就成了可扩展性问题。

但内核的改进,比如任务独占唤醒,通过允许等待队列将任务标记为独占处理事件,解决了这个问题。

有了这个改变,只有一个线程会被唤醒,那个特定的问题就解决了。

然后,当 EPOLL 被引入时,等待同一个 EPOLL 文件描述符的多个线程在边缘触发事件发生时都会被唤醒。

边缘触发事件通过只唤醒一个线程解决了这个问题。

在内核 2.6 之前,情况略有不同,当条件变量使用广播通知时,所有等待的线程都会被唤醒,尽管它们中的大多数会立即回去等待伴随的互斥锁。

Futex 中添加了 Requeue 功能,允许只唤醒一个线程,并将其余线程在仍处于休眠状态时移动到互斥锁的等待队列中。

现在仍然有其他方式会触发惊群效应。

但我认为你不再听到它的主要原因,至少是在系统层面,是因为我们大多数人不再编写网络代码了。

这项特权保留给了更了解这些特性的底层库编写者,因此所有这些线程唤醒的雷区都安全地对我们抽象掉了。

顺便说一下,所有这些例子都适用于 Linux,但在其他操作系统中也有类似的场景。

现在,我来讲述一种在现代内核中导致线程同时被唤醒的替代情况。

并且是在遵循系统 API 最佳实践的情况下发生的。它有可能影响成千上万个完全无关的应用程序中的线程。

现在让我们进入故事。

我之前提到过,我工作的一部分是排查性能问题。

所以一位同事找到了我。他告诉我他的服务 unicorn 遇到了性能问题。

我们有服务 unicorn,在处理请求时,它会从数据存储 rainbow 获取数据。

服务调用会记录其持续时间,因此很容易通过日志聚合器浏览和处理它们。

他告诉我平均持续时间接近 80 微秒,这听起来非常合理,对吧?

但我的同事非常细致,除了看平均值,他也关注最慢的实例,我们都应该这样做。

他发现了一些发生频率相当高的、异常离谱的离群值。

最差的延迟高达 20 毫秒。这是一个在短短几小时内持续时间的 P95 直方图。

它剔除了 5% 最快和最慢的实例。

它看起来像你期望的对数正态分布,对吧?

这是包含所有数据的直方图,正如你所见,最差的实例慢至 17.5 毫秒。

这是平均值的 250 倍。这说不通,即使这只占获取操作的很小一部分。这也是一个不能不被理解的问题。

这是真实世界的问题。我们需要弄清楚这个问题的影响。

回到对话。

我的第一个猜测,可能也是你们中许多人的猜测,是这些离群值发生在数据存储 rainbow 极度繁忙的时期。

所以我询问了 CPU 使用率、队列大小、每秒操作数等。但情况并非如此。

问题一直存在。即使在每个层级、每个环境的低活动期也是如此。

我的第二个猜测,可能也是你们的猜测,是当这些离群值发生时,从 rainbow 数据存储获取的记录数量太多或太大。

也不是这个原因。这些离群值发生在获取平均大小的单个记录时。

我的第三个猜测是,也许在观察到这些离群值时,数据存储中正在运行一些后台作业。

某些阻塞了所有查询的东西。但也不是,只有一个后台作业在运行,而且它每天只运行一次。

所以我开始着手排查。我设法通过在一个良好的开发环境中启动一个现有的压力测试来重现这个问题,通常这就成功了一半,对吧?能够可靠地一键重现问题,这很棒。

我尝试的第一件事是采样剖析。现在让我们先思考一下采样剖析,即使你们大多数人都知道它是什么。

采样剖析涉及以给定的频率(例如,每 10 毫秒)获取线程状态的快照,包括其调用栈。

通常有两种方式:基于时间的,使用 setitimer 完成;基于事件的,更准确,但依赖于硬件支持且需要更高的权限。

在剖析结束时,调用栈被聚合,这为你提供了一个关于任务在代码中花费大部分时间的统计模型。

这为你提供了改进性能的线索,对吧?我知道你们可能在想,这个特定问题涉及离群执行时间,而正如我刚才所说,采样剖析通常意味着聚合,这会破坏任何关于离群实例的信息。

所以我只会看到所涉及函数的平均值,对吧?这是真的,但它会简单地向我展示运行这个特定用例时涉及的函数和调用栈,所以它会给我一个很好的起点。

此外,如果问题与扩展性有关,尝试不同类型的负载可能会揭示持续时间增长或缩短的代码路径,尽管我们已经知道这里的问题可能与高流量无关。

另外,我写了一个我很喜欢的小型采样剖析器,当你最喜欢的工具是锤子时,每个问题看起来都像钉子,对吧?

现在,让我告诉你一些关于这个特定剖析器的事情,以及为什么我没有直接使用 perf

这个剖析器同时支持 CPU 内和 CPU 外剖析,意味着它同时获取运行中和休眠中线程的调用栈快照。

这个问题很可能与 I/O 相关,因为它发生在查询数据存储时,对于像这样的 I/O 密集型问题,仅进行 CPU 内剖析不会显示长时间的等待和事件,而这可能就是问题所在。

剖析 I/O 密集型问题与剖析 CPU 或内存密集型问题有很大不同。

我可以用经典的方式通过探测调度器来进行 CPU 外剖析,但这非常具有破坏性,并且会隐藏同样有价值的 CPU 内信息。

更不用说在这个环境中我没有 root 权限。

这个采样器是用户模式或基于时间的,意味着它使用 setitimer 而不是硬件事件,因此它只需要与运行任务相同的权限。

所以我运行剖析器分析 unicorn 服务。如果你想知道为什么我没有剖析 rainbow,那是因为它是一个通用的数据存储,非常稳定,至少一开始剖析它没有意义,尽管它是查询任务。

我看到了这个输出。剖析器生成 Trace Event 格式,可以用 Chrome 浏览器查看。

它和火焰图非常相似,正如你所见,只不过它是倒置的,顶部的函数调用底部的函数。

就像火焰图一样,调用栈火焰越宽,捕获到的样本就越多。

例如,这里我们可以看到程序在函数 fetch_from_rainbow 上花费的时间比在函数 nothing_interesting 上多。

然而,与火焰图不同的是,它按线程分隔信息,正如你所见,每一行是一个线程。它还用不同的颜色区分 CPU 内和 CPU 外的调用栈。

正如你所见,CPU 外是蓝色的,CPU 内是金色的。

这使我能够区分 CPU 密集型代码路径和 I/O 密集型代码路径。

如果我们放大它。

我们会看到这里接收请求,处理请求。正如我们所料,这是花费大部分时间的地方。

沿着栈向下,我们看到 fetch_from_rainbow,这是我们试图优化的函数。我们把它加入嫌疑列表。

如果你继续向下看这个栈,你会看到许多被 fetch_from_rainbow 调用的函数。

play_with_likecolor_main 这样的函数花费的时间最长,所以我查看了它们的源代码,试图找到一些明显的问题,但没有什么突出的。事实上,我查看了嫌疑列表中所有函数的源代码。

我没有发现任何值得注意的地方。

但就像我之前说的,采样的聚合性质没有告诉我关于这些函数中异常执行时间或离群值的任何信息。

所以是时候换个方法,尝试别的东西了。

正如你们所知,有两种主要的性能剖析类型。

虽然还有其他几种类型,但可以说两种最常见的方法是采样和代码插桩。

就像我之前说的,采样获取线程调用栈的快照,聚合它们,并给出时间花费位置的统计模型。

另一方面,插桩是在每个感兴趣的函数或代码块的入口和出口插入代码。

在这两个地方获取时间戳,然后发出这些信息跨度。

然后可以处理时间戳来计算函数调用的延迟。

顺便说一下,我谈论的代码插桩仅指性能剖析的上下文,还有许多其他类型的插桩用于不同的目的。

这可以通过多种方式完成:手动插入日志代码、使用遥测库、在执行前静态修改二进制文件,或者动态修改运行中的任务,这就是我们要做的。

正如你可以想象的,对程序中的每个函数使用它,无论采用哪种方法,都可能非常具有破坏性,以至于无法使用,特别是对于拥有数百万个函数的大型二进制文件。

对于持续时间很短的各种函数,它也没有用处,因为它增加的偏差可能比函数实际持续时间还长。

但如果你有选择地挑选一个可疑函数列表,就像我们拥有的那个,它可以揭示采样剖析隐藏的信息。

让我们看一个例子,看看两种剖析方法提供的信息有何不同。

在顶部,我们有一个采样跟踪。在底部,是同一个程序的插桩跟踪。

我也使用 Trace Event 格式来可视化插桩跟踪,但这次显示不同的信息,这次它只向我展示块,即函数执行的实例,没有调用栈,只有每个函数执行开始和结束的时间。

在这里你可以看到,这个 long_function 的实例在这个时间开始,在那个时间结束。

采样不会告诉你一个函数被调用了多少次,例如,在顶部的采样跟踪中,你看到 quick_functionlong_function 都获得了大量样本,但我们不知道是因为它们在一个循环中被调用了数千次,还是因为它们只运行了一次但时间很长,或者介于两者之间。

插桩会告诉你关于函数每次执行的信息。

插桩还告诉你什么与什么并发运行,以及什么发生在什么之前。

这有助于解决某些类型的问题,比如识别错过的并发机会。

例如,在顶部,你有一个这次用 callgrind 完成的采样跟踪,你在这里看到的可视化来自 KCacheGrind

如果我们看这些跟踪,我们可以看到大部分时间花在一个方便地称为 very_long_call 的函数上。

如果我们只看这个跟踪,我们会很想直接优化 very_long_call 并期望性能提升。

然而,在底部的插桩跟踪中,我们会看到减少 very_long_call 的持续时间根本不会改变服务请求的持续时间。

为什么?因为服务请求直到 short_call3 完成才结束。

插桩向我们展示,在这种情况下,问题在于即使有些函数在并发运行,但也许其中一些在依赖满足后没有立即启动。

例如,假设短调用不依赖于其他函数的结果,它们可以被提升并在服务请求开始时立即执行,然后跟踪看起来会像这样。

看到所有函数都很好地打包到了左边。

现在,如果仍然需要,优化 very_long_call 才有意义。仅靠采样,我们无法发现这一点,而这在实践中经常发生。

特别是对于我们正在解决的案例,因为它提供了关于函数单个执行的信息,它将允许我们定位离群实例,这正是我们需要的。

当时我正在研究这个问题,同时也在开发一个不同的工具,它可以让我在运行的任务中插桩给定的函数列表,并发出它们的开始和结束时间,所以它来得非常及时。

我启动了插桩工具,使用我在上一步用采样器识别出的可疑函数列表。

这就是我们的 unicorn 程序使用代码插桩时的样子。

你可以看到我们采样时看到的相同函数,但现在我们看到每次调用实例,我们看到它们被调用的顺序以及与什么并发。

请注意,插桩跟踪在这个级别没有线程 ID 信息,并发性与操作系统线程无关,一个被调用的函数可能是一个协程,或者运行在一个纤程上。

如果我们缩小并开始向右滚动。

我们看到一些离群值,一些函数花费了异常长的时间,正如预期的那样。

但每次都不是相同的函数,这出乎我的意料。

例如,你可以在这里看到,在我用红色高亮的实例中,函数 color_main 花费的时间比用绿色高亮的实例多得多。

对于函数 peak_treasure 也是如此。看看在我用红色高亮的实例中它花费了多少时间。

所有其他被插桩的函数也发生了同样的情况,如果我们继续滚动,我们会看到它们都有离群值。

这不是我所期望的,因为我假设我正在尝试解决的问题会出现在代码的特定部分?对我来说,每个被插桩的函数似乎都有这种极端的离群值问题,这说不通。

这些是一些被插桩函数的持续时间直方图,正如你所见。

它们每一个都有极端的离群值。尽管它们的平均持续时间不同,但它们都类似于我们之前看到的获取操作的直方图。

所以插桩很棒,但像采样一样,它也隐藏了信息,它只告诉你一个函数何时开始执行以及何时结束。

它没有告诉你中间发生了什么,也许执行线程被抢占了一千次,也许发生了一堆缺页错误,或者谁知道发生了什么其他内核操作。

所以再一次,我们必须改变我们的方法。我们必须回溯。我们回到采样,但我们将以更细粒度的方式处理信息。我们将不聚合它们,而是单独查看每个样本。

我们为什么要这样做?因为我们希望能够准确地看到在这些离群执行发生时,每个线程上正在发生什么。

所以我为我的采样器工具写了一个补丁。来发出细粒度信息,并按每个样本发生的时间逐个样本地显示它。

这就是 unicorn 任务的细粒度跟踪的样子,是的,我也为此使用 Trace Event 格式,同样,显示的信息与其他跟踪类型不同。

每个样本都以其调用栈显示,宽度恒定,位于其被获取的确切时间。

每个样本都在自己的线程通道中。

在这个区域,样本分布非常均匀,正如我们所期望的,因为采样的频率应该是稳定的。

但到了一个离群执行发生的时间,我们在所有线程的样本中看到了一个间隙。

也就是说,在两个连续样本之间,所有线程都有一段异常长的时间。

我不想相信采样器有偏差。

所以我首先假设,无论是什么导致了这些离群值,它都在阻止我的采样器停止线程。

也许线程在一段时间内陷入不可中断的休眠,或者也许它们在核心态遇到了自旋锁。

哦,太好了。

抱歉。也许查询导致了主要缺页错误。

也许有什么东西导致了大量的上下文切换。

我认为其中之一延迟了从我的采样器到被观察任务的信号处理。

我写了一个脚本来计算上下文切换次数,没有发现异常。

我查看了资源监控中报告的缺页错误,没有峰值,至少与这些间隙不匹配。

当我似乎陷入僵局时,我退后一步,决定将我制作的细粒度模式补丁提交到采样器,以便将来在类似问题中使用。

我正在进行冒烟测试以确保我没有破坏任何东西,我将采样器附加到普通的 cat 任务上。

正如你所见 cat 的样本是均匀分布的。但如果你向右滚动,猜猜看?

unicorn 中看到的相同间隙出现了。这令人心碎。

似乎采样器有偏差,我一直以来尝试过的每个任务都在被不规则地采样,而我却不知道。

现在我有两个问题要解决,而不是一个。

我查看了采样器的源代码,寻找导致这种偏差的明显原因,但没有发现。

最终,通过从不同角度反复查看这些跟踪,我终于注意到一些奇怪的事情。

如果我们放大 cat 的细粒度样本跟踪,我们可以看到这些间隙恰好每五秒间隔一次。

这给了我一些希望。采样器可能存在的任何偏差恰好每五秒发生一次,这非常不可能。

所以我至少回到了只有一个问题的状态。

cat 之后,我测试了多个任务,在每个任务上都看到了这些间隙。

所以我假设有什么东西每五秒中断一次每个任务。

为了证明这一点,我写了一个简单的小 Python 脚本。

它所做的就是:在一个循环内,获取一个时间戳,并从前一次迭代的时间戳中减去它,如果差值大于一毫秒(我认为这是合理的,你不会期望连续操作间隔超过一毫秒,即使在解释型语言中),它就打印一行。

所以基本上,它不断测试长时间的执行中断或延迟。

我运行了它,这就是我看到的。每一个都是一次延迟。

如果你看时间戳中的秒数,你会注意到它们都是五的倍数。

所以这些间隙不仅每五秒间隔一次,而且与系统时钟对齐。

这意味着问题与 unicorn 或采样器都无关。正是这些延迟,在五秒标记处运行的任何任务,其延迟都会被夸大。

问题出在这个系统上。

我最初的猜测是有一个邪恶的任务。抱歉,我最初的猜测是有一个邪恶的任务,每五秒产生大量短命的子任务,这些任务驱逐了每个正在运行的任务。

但我不知道下一步该怎么做。我是一个应用程序开发者,所以我大部分时间都在特权用户的领域里。

但命运让我遇到了一位拥有 root 权限的强大朋友,我告诉了他我迄今为止发现的一切,他说,写个 eBPF 脚本,我来为你运行。

对于不熟悉的人来说,eBPF 允许你在内核内运行沙盒程序,并允许你查询其许多内部状态。

太棒了,现在我有能力向内核提问了。

所以我写了一个小 eBPF 脚本,以一定频率采样每个核心。

又是采样。如果有东西在上面运行,它就获取线程 ID 和线程名称,还获取进程 ID 和父进程的进程 ID,你记得我的第一个猜测是某个任务正在创建大量子任务。

所以我想知道父进程。最后,它获取所有这些信息并用时间戳发出。

我获取输出并将其转换为 Trace Event 跟踪,再次显示不同的信息,这是我第四次这样做了,我就是喜欢用 Trace Event 格式进行可视化。

这就是 eBPF 脚本结果的样子。正如你所见,每一行代表一个核心。

每个块是一个恒定宽度的样本,表示当时有一个线程在其上运行。

每个线程被分配一种颜色,以便更容易推理它们。

到目前为止,我们看到的是你所期望的。大多数线程运行很短的时间,然后它们回去休眠,少数线程在核心上花费稍长的时间,但大多数核心在大多数时间是空闲的。

但让我们向右滚动到发生延迟的五秒标记之一,看看发生了什么。

如果你看这个高亮区域,你会看到大量线程在每个核心上执行,当然,这会踢出任何试图在这些核心上做有用工作的线程长达两位数毫秒的时间。

这些线程是我们的罪魁祸首,我们快找到原因了。那么这些线程是什么?

它们是像我最初想的那样,由一个不文明的应用程序创建的短命任务吗?不是。

在查看了线程名称并在源代码中搜索它们之后。

这些线程是由一些库创建的,这些库恰好被许多正在运行的应用程序链接。

所以我用细粒度补丁在链接了这个库的一个任务上运行剖析器。

我想,如果我在这些线程的延迟之前查看样本,我就能找出它们做了什么导致了延迟。

这是其中一个罪魁祸首线程的跟踪。

在中间,你可以看到一次延迟。如果你看延迟前的最后一个调用栈,你可以看到有一个对 tiny_jobs_scheduler_dispatcher 的调用。

我查看了同一跟踪中的其他延迟,然后查看了链接这些库的其他任务中的延迟,它们似乎在延迟期间都在类似的代码上休眠。

那么为什么这个调用会导致延迟?因为它运行周期性作业。在特定时间开始。

在我们继续之前,让我们快速定义一下什么是定时作业调度器,因为它可能有不同的名称,比如定时器调度器、事件管理器,也许还有其他,并且它们有不同的功能集。

定时作业调度器是一个组件或实用程序,允许你安排在未来运行的作业。

功能各不相同,但它通常允许你安排一个作业在从现在起给定时间后运行。

有些支持在特定的开始时间启动。有些允许你安排周期性作业,即每隔一段时间运行一次的作业,有给定的初始延迟。

其他的会强制你重新安排作业以实现周期性。

此外,一些事件循环和线程池也提供类似的功能。

但为什么会有延迟?

在大多数后端主机上,有相当数量的线程使用某种形式的调度、轮询、警报、操作超时、心跳、批处理等。显然,这取决于你的系统做什么。

如果你为这些或其他形式的调度使用定时作业调度器来运行周期性作业,这些作业很可能会有五的倍数的延迟,因为我们大家都喜欢五的倍数。

并且它们可能在整点等时间以相同的时间对齐启动。

你会有大量线程在一天中的相同时间点醒来。

所以经过所有这些铺垫,问题就是这个。

惊群效应可能发生在无关的应用程序中,并且没有任何共同的事件。

它们可以通过使用对齐的、具有公约数的延迟的周期性作业来同步唤醒。

它们不是在等待同一个事件,而是在看同一个时钟。

现在,你们中的一些人可能在想,这不会影响我,我不记得在我的系统上看到过在绝对时间开始的周期性作业,我很确定它们都是在相对时间开始的。

你说得对。如果周期性作业不是以绝对开始时间启动的,那么它们就不会同步,也就没有惊群效应。

如果你没有或很少有这种作业,那很好。但它仍然可能发生。

具有匹配延迟的无关周期性作业可能会在一天中变得同步。这可能性较小,问题也较小,但我既在现实中观察到过,也能够复现它。

假设你有周期性作业 A 和 B,就像我们在这里看到的。它们不同步。但它们有相同的延迟。

一段时间后,它们都再次运行,仍然不同步。但随后发生了一段高活动期,也许有更高优先级的线程。

那么这些线程中的唤醒将一起被延迟。

它们将在高活动期结束时一起醒来。

它们再次休眠相同的延迟,然后它们就同步了,并保持同步,可能直到它们的任务重启。

有趣的是,这段高活动期可能是一个现有的定时惊群效应,而这个惊群效应只是招募了更多线程加入其中,产生滚雪球效应。

所有这些都引出了这些问题。

你的周期性作业真的需要绝对定时器吗?我猜大多数作业(通常是某种轮询)可以使用相对开始定时器。

如果你需要绝对定时器,你能使用不规则的开始时间来防止它们与其他作业对齐从而同步吗?

如果你在轮询某物,你能使用某种订阅机制代替吗?

如果你在处理累积的工作,你能只在有工作要做时才唤醒作业吗?

你能使用质数作为作业延迟或添加随机抖动吗?顺便说一下,我们就是这样解决问题的。

如果你有一个自定义调度器,你能基于单个开始时间而不是前一次唤醒时间来计算所有唤醒时间吗?

你如何监控这个问题?嗯,我们有我之前展示过的检测问题的脚本。你可以使用类似的东西来看看你是否有这个问题。

这个脚本的问题在于它在循环中无休眠运行,所以在它运行的整个时间内会占用 100% 的 CPU 核心。

你可以设置它以最低优先级运行,但从能效角度来看,这仍然是个问题。

所以如果你有这个问题,并且想确保你已经修复了它,我建议采用这种方法。

每天启动几次相同的脚本,每天改变启动时间。

运行一段时间,比如五分钟,显然这些都可以调整,报告任何延迟。

这有几个假设:如果存在一个周期大于五分钟的定时惊群问题,那么它实际上不是问题,我的意思是每五分钟有一次小延迟并不具有足够的破坏性,当然五分钟是可调的。

如果存在一个频率低于五分钟但周期性发生,并且不是一天中大部分时间都发生的定时惊群,那么它一定是由系统活动条件引起的,这是无法避免的。

这只是正常功能的一部分。

任何周期低于五分钟并且每天发生相当长时间的定时惊群最终都会被捕获。

如果你遇到这个问题,我非常想听听你的经验。如果可以,请给我发邮件,如果你愿意,我很乐意匿名处理报告。

这就是我的演讲,再次感谢大家的聆听,希望你们喜欢。

现在我可以试着回答你们可能有的任何问题。

007:SRE大会-2025-美洲-|-srecon-|-分布式-|-缓存-|-OpenTelemetry-|-安全-|-AIOps-p07-P07-Techniques-Netflix-Uses-to-Weather-Significant-Demand-Shifts--BV1TmLDz7EZZ_p7-

课程名称:Netflix应对流量高峰的技术:P07:概述与挑战

在本节课中,我们将要学习Netflix如何应对其全球服务中出现的显著需求波动。Netflix面临的核心挑战在于其全球规模,产品被全球数百万用户在不同时间、通过数千种不同设备类型持续使用。这些设备网络能力各异,分辨率高低不同。

Netflix通过全球技术架构来应对这一挑战。其架构分为控制平面和数据平面。控制平面运行在四个亚马逊区域,负责登录、播放URL获取、个性化推荐等产品核心功能。数据平面称为OpenConnect,是一个全球性的、高度分布式的CDN网络,负责视频流数据传输。OpenConnect在全球ISP中拥有成千上万个接入点。

这种全球性产品导致了高度可变的“每秒启动数”(SPS,即流媒体开始次数),这是Netflix的主要负载指标。流量在不同时间和区域之间存在巨大差异,峰值与谷值之间可达一个数量级。这种大规模的流量转移会导致服务负载发生显著变化。

Netflix的负载高峰主要有三个来源:

  1. 每周故障转移演练:每周进行的区域撤离演练会产生两次负载高峰——一次在接收流量的“救援”区域,一次在恢复服务时原区域承受的巨大负载冲击。
  2. 内容发布:如《怪奇物语》等热门剧集全球同步上线,会驱动用户在同一时间尝试观看,引发全球性的关联观看高峰。
  3. 设备驱动需求:当出现错误时,Netflix设备和用户都会不断重试,这种“重试风暴”可能引发一些最大的负载高峰。

问题的复杂性在于,Netflix的后端并非单一服务器,而是一个复杂的、分层的微服务架构。一个负载高峰会影响整个分布式调用链,因此必须理解负载高峰如何影响这个复杂的调用图。

本节课的故事由三部分组成:我们将讨论流量需求、如何平衡计算资源供给,以及当供需失衡时,我们使用哪些弹性技术来减轻对用户的影响。

课程名称:Netflix应对流量高峰的技术:P07:管理全局流量需求

上一节我们介绍了Netflix面临的负载挑战,本节中我们来看看如何管理全局流量需求。这始于我们的全局架构以及如何在区域之间平衡负载。

理解流量管理需要了解我们的四个区域,它们运行着微服务和数据存储。以下是允许我们在全球范围内灵活调度流量的关键组件:

  • CDN和边缘代理(Zulu):它们协同工作,将用户引导至合适的区域。
  • 全活数据库和缓存复制层:由Apache Cassandra和名为EVCache的分布式缓存系统提供支持。这两项技术是允许我们将用户流量调度到全球任何地方的关键。

当进行流量调度决策时,我们实际上是在延迟和可用性之间进行权衡。我们可以偏向于将用户引导至延迟最低的区域,也可以偏向于负载最轻的区域。在整个课程中你会看到,在为负载高峰做准备时,我们通常会从典型的延迟优先策略,转向可用性优先策略。

让我们通过一个控制平面流量引导的例子来具体说明。假设我们有四个区域,用户试图观看内容,中间有CDN接入点。Netflix的设备会探测网络拓扑,判断是直接连接云端更快,还是通过CDN更快。我们可以利用这些数据,因为负载高的区域通常响应更慢,从而引导用户远离这些区域。

我们甚至可以更进一步,主动引导用户远离特定区域。例如,我们可能希望引导用户远离US-WEST-2区域。其实现原理是DNS。Netflix在自己的OpenConnect CDN上运行权威DNS服务器,我们可以通过这些服务器来调度流量块。递归解析器通过其ISP连接到这些权威DNS服务器,这为我们提供了大约100个控制点。我们可以简单地将流量块移动到不同区域,例如将 api.netflix.com 解析到 useast1.api.netflix.com

这种路由用户的能力对于应对负载高峰至关重要,因为它允许我们结合预测,在负载高峰发生前调整流量形态,也能在预测错误时做出反应。

以下是几种可能的流量形态策略:

  • 典型形态:如果仅优化延迟,不做任何调整,负载分布会呈现区域性的高峰和低谷。
  • 均衡形态:如果我们不确定负载高峰会出现在哪里,可以尝试均衡各区域的流量。
  • 非均衡形态:如果我们有理由相信某个事件会导致特定区域(如US-WEST-2)负载激增,可以主动将流量从该区域移出。

在实践中,我们会在负载高峰发生前的一段时间(ΔT)开始调整流量形态,以最小化区域间的负载差异。当然,负载高峰并不总是按计划发生,我们可能需要在事件发生后重新调整流量形态。关键目标之一是缩短在负载高峰后重新调整流量的延迟,以便我们能快速反应。

然而,仅靠流量调度还不够,因为无论如何调整,在全球范围内移动流量都需要几分钟时间(DNS TTL过期、路由生效等)。在此期间,我们依赖路由层的回退行为。

这建立在我们的技术博客中提到的“优先级负载丢弃”机制上。其核心思想是:Netflix的每个客户端在向云端控制平面发送请求时,都会附带一个优先级。低优先级代表重要工作(如立即播放),高优先级代表不重要工作(如发送日志)。边缘代理Zulu可以有选择地丢弃低优先级工作,以保护高优先级工作。

但如果一个服务因负载高峰而宕机了呢?通常,一个区域被压垮时,并非所有服务都出问题,而是一两个服务在复杂的调用链中承受了意外的负载高峰。在这种情况下,用户请求会被Zulu丢弃。我们意识到,我们还有其他三个云端区域,为什么不降低请求的优先级并将其发送到其他地方呢?

这里有一个关键问题:为什么我们必须降低优先级?这是为了避免级联故障。如果我们不降低优先级,并且没有优先级负载丢弃机制,那么将一个故障区域的所有流量转移到另一个健康区域,可能会导致全局性故障。因此,故障转移能力的关键在于对工作进行优先级划分。

课程名称:Netflix应对流量高峰的技术:P07:管理计算资源供给

仅仅管理流量需求是不够的,我们还必须管理计算资源的供给。本节将探讨如何结合云端现实、进行预测性和反应性扩缩容。

这本质上是让计算机去做困难的事情,因为人类并不擅长管理扩缩容策略,计算机则更擅长。首先,我们需要了解在Netflix我们如何讨论服务容量。历史上,我们使用CPU百分比等指标,但出于多种原因,这不是一个好的思考方式。我们更倾向于使用成功缓冲区和故障缓冲区来讨论。

一个系统在特定负载下运行,存在一个额外的负载量,系统可以成功处理而不违反延迟SLO或抛出错误,这就是成功缓冲区。超过这个缓冲区的负载,我们将丢弃请求,这些就是错误或故障。我们很快就会明白为什么我们非常希望丢弃这些负载。

缓冲区与业务成果紧密相连。你可以通过自动扩缩容或预测性扩缩容来恢复缓冲区,但这需要时间,我们称之为恢复常数。有状态服务通常比无状态服务需要更长的恢复时间。更重要的是,缓冲区的大小也取决于该服务对业务的重要性。例如,对流媒体播放路径至关重要的服务,比那些对个性化推荐非关键的服务,需要更多的缓冲区。

这是一个业务权衡决策。Netflix的许多服务都运行在“较冷”的状态,但这并非偶然,也不是浪费资源,而是我们有意在系统中构建缓冲区,以便在不影响用户体验的情况下处理负载高峰。

一个关键结论是:恢复速度快的服务需要更少的缓冲区。如果服务能更快地扩缩容、更快地恢复缓冲区,就能运行得更高效。

这引出了云端现实第一点:云端资源并非无限,你仍然需要进行容量规划。在Netflix,我们将其分为两大类:

  1. 慢速扩缩容的无状态服务:我们几乎完全预留容量并持续运行。
  2. 快速扩缩容的无状态服务:随着可变负载而动态调整。

你会预留一部分容量,在负载低谷时,这些预留的计算机并非闲置,Netflix会利用它们进行编码等工作。当微服务有需求时,我们再释放这些容量给微服务使用。

但即使这样,你也不会预留全部容量,因为成本极高。因此,基于运行时间和定价,你会使用按需实例或竞价实例。这揭示了云端现实第二点:计算资源的供给是可变的。如果你依赖云提供商提供资源但没有预付费或预运行,就需要考虑哪些类型的实例具有“深度供给”。通常,较新的、性能更好的实例类型,其资源池深度可能不如老旧的实例类型。在采用新实例类型时,我们必须非常小心。

供给的深度也受其他客户行为的影响。例如,在一天中的特定时段或黑色星期五等时期,获取容量可能相对困难。

你的选择有:预留并付费;使用按需预留;通过利用之前建立的缓冲区来处理负载高峰;或者优先将服务器分配给最重要的工作负载(即Tier 0服务)。

问题进一步复杂化,因为计算资源的供给在不同实例类型上每小时都在变化。如果你能灵活选择工作负载的运行环境,能访问更多的计算资源池,你就能访问云提供商更多的容量。例如,如果你严格要求工作负载必须在 i4i.2xlarge 实例上运行,并且没有回退方案,那么当这种特定实例在负载高峰期间出现容量紧缩时,你可能会陷入困境。

这引出了云端现实第三点:当你试图在不同计算机之间保持灵活性时,不同的服务器在性能上存在显著差异。在Netflix,我们使用容量规划库/服务容量建模库来具体理解这些服务器之间的差异。例如,m7a.4xlarge 有16个物理核心,而 m6id.4xlarge 只有8个物理核心,这会显著影响你的缓冲区计算。

让我们看看这是如何影响的。这是一个简单的分析,我们查看了M7A和M7I系列的不同计算机规格,发现故障缓冲区开始的CPU使用率点差异很大。这是因为核心数更多的服务器,在性能曲线进入非线性区域(即“拐点”)之前,可以运行在更高的负载下。我们称这个安全区域为“净空”。Tier 0和Tier 1目标则是我们在故障缓冲区基础上,再预留的线性缓冲区倍数(例如Tier 0可能是2倍缓冲区,Tier 1是1.5倍)。这是一个相当简化的分析,没有考虑服务器数量等因素。但关键在于,如果开发者要求2倍缓冲区,而作为平台方你在不同计算机之间迁移服务,你必须承担不同计算机在不同负载点提供缓冲区的复杂性。

缓冲区也因工作负载而异。例如,一个无状态的Java应用没有像数据库那样的数据压缩、修复或备份等后台任务。因此,你可能会看到两个工作负载在同一类服务器上,一个运行在10% CPU,另一个运行在30% CPU。如果不了解导致这些差异的缓冲区需求,你就无法判断它们是“冷”还是“热”,也许它们正好处于为后台活动预留缓冲区的合适热度。

课程名称:Netflix应对流量高峰的技术:P07:预测与反应性扩缩容

现在我们已经理解了缓冲区,我们可以利用它来预测和预扩缩容我们的服务,以及设置反应性的自动扩缩容策略。

预扩缩容相对直接。如果你提前知道负载高峰即将到来,并且你知道服务运行在线性区域内,你可以线性外推,并在负载到来前将服务器组的指标钉在高位。第一步,我们钉高指标;然后负载到来,顶部发生少量自动扩缩容;最后负载过后,我们再缩容。

这种方法的主要复杂性在于,正如之前提到的,负载高峰对每个服务的影响是不同的。Netflix前门的4倍SPS负载高峰,可能只会在个别服务上引发2倍、3倍或1.5倍的负载高峰。你的调用链在流量和关键性上并不均匀。这一点尤其重要,因为当我们试图分配稀缺的服务器资源时,我们希望确保为Tier 0服务(如流媒体播放)建立缓冲区,并从对业务重要性较低、可用性要求不高的Tier 3或Tier 4服务那里收回计算机。

当然,我们必须问:如果我们预测错了怎么办?据我所知,没有水晶球能完美匹配供给与需求,我们总会出错,总会遇到容量短缺。当我们出错时,就必须通过自动扩缩容来反应。

Netflix是EC2自动扩缩容的重度用户,拥有数万个服务器组随着SPS流量不断自动扩缩容。既然我们理解了缓冲区,就很容易将其转化为自动扩缩容策略。我们以成功缓冲区的起点作为目标追踪点,然后我们在故障缓冲区的起点进行“锤击”。“锤击”是我们喜欢的一个术语,它是一种阶跃扩缩容策略,注入的容量比仅基于利用率数字计算出的要多。一个直观的理解是:CPU利用率本质上是一个受限的指标,10倍、100倍、1000倍的负载高峰都会导致100% CPU。因此,我们需要寻找其他指标来帮助我们应对这种数量级的负载挑战。CPU“锤击”意味着,服务很少运行在成功缓冲区的末端,所以我们只想添加比正常情况下更多的计算机。

但自动扩缩容可能很慢,它有各种延迟来源。我们将其分解为五个主要部分:

  1. 检测问题的时间
  2. 控制平面启动硬件的时间(例如,启动虚拟机)
  3. 内核或系统单元启动应用程序的时间
  4. 应用程序自身启动的时间
  5. 流量收敛的时间

在Netflix,大多数人曾认为控制平面延迟是主要贡献者,认为启动虚拟机很慢。但事实证明,亚马逊的控制平面非常快,这不是问题。真正的问题是检测长尾应用启动。检测负载高峰在我们的配置下需要好几分钟,如果我们主要依赖目标追踪策略,就需要多次扩缩容。长尾应用启动是指那些没有充分理由(有时有正当理由)却需要15分钟才能启动的Tier 0应用。此外,系统启动时间和服务发现也存在一些问题。

对我们来说,这是一个很好的反思机会。我们学到的教训是:几乎每个参与这个项目的人都没有预测到这一点。只有在我们真正开始测量自动扩缩容各组成部分的延迟后,才意识到真正的瓶颈在哪里。

一旦我们理解了问题,就可以解决它。例如,为了解决检测问题,我们使用了EC2高分辨率指标。我们能够根据实际启动延迟自动调整ASG冷却时间,这意味着阶跃策略会更迅速地采取更多行动。最后,我们开始观察每个服务的RPS(每秒请求数)比率,并添加了RPS“锤击”。第三点之所以重要,是因为它提供了负载高峰的完整粒度。例如,你可以区分1000 RPS、10,000 RPS和20,000 RPS的负载高峰,而仅基于CPU扩缩容时,它们都是100%。

这是我们最大的解决方案,我们也优化了其他大部分部分。我们通过深入研究特定服务来减少长尾启动时间,性能工程团队的优秀工程师们也做了一些工作,例如并行启动系统单元以消除systemd启动中的关键阻塞路径。我们从未真正解决服务发现问题,因为我们使用的是轮询的30秒服务发现,对我们来说已经足够快了。

我们优化得有多快?在进行任何调优之前,我们的Tier 0服务如果暴露在10倍负载高峰下,需要8到15分钟才能恢复完全可用性。这对业务来说是不可接受的。改进之后,我们能够将其降低到个位数分钟,即3到4分钟。这些微小的改变,比如改变自动扩缩容的依据、优化冷却时间,实际上能产生显著的影响。我们的业务更能接受3到4分钟的中断,而不是15分钟的中断。所以,这确实有效。

课程名称:Netflix应对流量高峰的技术:P07:弹性技术:负载丢弃与优先级

但是,3到4分钟仍然不够好。在这段时间内,我们的用户会经历什么?我们希望确保拥有弹性技术,在这段时间内为用户提供尽可能好的体验。本节将深入探讨负载丢弃和工作优先级。

当我们预测出错时,我们可以使用负载丢弃。负载丢弃相当直接:你查看目标追踪策略,在成功缓冲区的起点,如果有低优先级工作,就开始丢弃;当深入故障缓冲区(接近90%-95% CPU)时,就开始丢弃一切可能丢弃的请求。

让我们通过一个例子来理解为什么这是一个好主意。想象一条高速公路,上面有一些汽车,代表系统在标称利用率下运行,通行时间是10分钟。当服务开始负载增加时,更多汽车驶上高速,通行时间增加,但我们仍然让所有车都上去。然后我们到达我最喜欢的部分——高速公路开始过载,通行时间越来越长。这就像加州的高速公路,那个限制你进入的交通信号灯。服务与高速公路的主要区别在于,在服务中,我们会直接丢弃请求,而不是延迟它们。

我们这样做的原因是试图避免通行时间趋向无穷大,因为路上发生了连环车祸。当一个服务的利用率达到100%时,你会进入我们所说的拥塞性故障,也称为队列无限增长、系统彻底崩溃,或者我个人最喜欢的说法——“一段非常糟糕的时光”。你想避免这种情况,因为在拥塞性故障期间,成功请求数会降至零。问题是,要让服务恢复处理负载,比它维持在正常QPS水平时需要多得多的计算资源供给。

我们可以看到负载丢弃如何帮助我们远离那种情况。但缓冲区的其余部分呢?事实证明,我们可以通过将成功缓冲区划分为不同的流量优先级来更早地丢弃负载,这样我们就能更早地将对我们不那么重要的负载从服务上卸下。这就是优先级负载丢弃

这是一个丢弃曲线的例子。服务的目标追踪点设在40% CPU。在Netflix,我们允许人们用0到100之间的数字来划分流量优先级,低数字代表重要,就像之前的Zulu例子一样。为了便于指标分析,我们将其分为四个主要桶:尽力而为流量、降级流量、关键流量和批量流量(Netflix中很少)。通过观察X轴(系统负载)和Y轴(我们将丢弃的该流量类别的百分比),你可以看到这自然允许我们存活更长时间,因为我们可以更早地丢弃更多不重要的工作,为真正关键的工作保留CPU周期。

服务所有者如何确定其工作的优先级?一种方式是明确告知我们。通常,Netflix的服务所有者非常清楚哪些URL、哪些路径对产品至关重要,哪些不是。但我们始终有一个后备方案,即直接采用设备声明的优先级。如果服务所有者不想表达对流量重要性的意见,设备已经表达了意见,我们可以将其作为一个相当不错的后备方案。

回到我们的高速公路类比,现在有了优先级划分,它本质上通过允许我们优先决定谁可以上高速来创建了更多缓冲区。我喜欢用的类比是:我要确保救护车能上高速,而开跑车兜风的人可以回家。我们希望救护车在高速上,我们希望用户能够点击播放。

这有效吗?是的,效果非常好。你能看出这张图与之前那张图的区别吗?这张图发生了数量级的流量高峰,而服务在整个过程中持续存活(蓝线),这意味着我们没有进入拥塞性故障。这是巨大的成功。

但I/O密集型工作负载呢?因为正如前一位演讲者提到的,CPU密集型工作负载和I/O密集型工作负载非常不同。我对此的最佳类比是:想象汽车驶上高速,消失一段时间,然后又回到高速。你要避免的是它们全部在同一时间重新出现,导致我们之前讨论过的那种爆炸性拥堵。我知道这个类比有点牵强,但我尽力了。

让我们看看这两张图,试着找出区别。右边是重负载的I/O工作负载,左边负载较轻。区别在于通行时间。重负载的I/O系统更慢。因此,我们实际上可以使用服务级别目标(SLO)利用率作为延迟利用率的代理。在这个例子中,服务有50毫秒的延迟目标,然后我们计算有多少请求比这个目标慢。如果100%的请求都比目标慢,那么该服务就是高负载;如果比例很小,就是低负载。

我们实际上将这种方法用于数据层。我之前提到的那些数据网关,允许开发者基于后端数据库负载来丢弃负载。因此,如果后端数据库变慢,位于该数据库前面的服务就会提前丢弃负载。

举个例子,如果该服务因拥塞而变慢,现在88%的请求都超过延迟目标,即使我们只处于最大负载的5%。这有效吗?是的,有效。这是我们与一个提供文件(非常重要的文件,你可以猜猜是哪种重要文件会提供给我们的边缘)的服务一起运行的测试。我们能够将几个服务器推到超过50 Gbps。我们知道在这个水平上,网卡将开始丢包,从而引入延迟。我们想在这里看到的关键点是:一旦我们开始丢包、出现延迟,我们希望看到右侧的低优先级文件被限流器丢弃。这正是我们观察到的。

课程名称:Netflix应对流量高峰的技术:P07:有状态服务的弹性与缓存策略

我们已经讨论了负载丢弃,但重试呢?很多人认为重试不好。实际上,我们认为只要是在负载丢弃上的重试,就是可以的。因为你发送初始请求的服务器只做了极少的工作,基本上只是丢弃了请求。我们发现这有助于我们从高负载服务器重试到平均负载的服务器。如果你的负载均衡策略明确地做到这一点,那就更好了。底部的数学公式本质上是完全抖动指数退避算法,主要区别在于,因为我们在负载丢弃时重试,所以可以在早期重试上稍微激进一些。除此之外,这是一个相当标准的指数退避算法。

现在我们对无状态服务的弹性有了很好的理解。接下来我想谈谈如何让有状态服务适应这个世界。我们将讨论容量规划,因为我们需要给它们更多缓冲区;以及数据网关,我们用它来将它们原本无界的工作量,转化为易于推理的工作单元,然后我们可以进行负载丢弃等操作。

这涉及到架构图的右侧。Netflix很久以前做出的一个关键决策是:每个微服务都有自己的数据存储,我们不共享微服务之间的存储资源。这会导致更频繁的中断,但与多租户系统相比,这些中断的爆炸半径更小。这对我们来说是正确的业务权衡,但可能不适合你。

由于我们采用了这种单租户模型,我们必须非常小心地进行容量规划。几年前我做了一个演讲,介绍我们如何利用排队论来最优地选择计算机,以尽可能少的成本为我们提供所需的缓冲区。这本质上是一个统计模型,试图模拟我们需要处理多少读操作、写操作,以及它们的大小。当然,一旦工作负载实际运行,你可以直接观察这些指标,但通常我们在应用程序实际启动之前就需要配置数据存储。这允许我们以一种严谨的方式建立之前提到的缓冲区,例如,我们可以给关键数据存储更多缓冲区,而限制非关键数据存储的缓冲区。

那么数据网关层如何帮助我们呢?我们已经对数据存储进行了容量规划,但即使有容量规划,数据存储有时也可能没有足够的容量应对负载高峰。网关的作用是将具有挑战性的API转化为能很好地适应我们之前讨论的弹性技术的东西。网关做的第一件事是将所有读写操作转化为右侧的分块操作或读侧的分页操作。想象一下,如果你的数据库API可以返回无界数量的行,那么你无法真正建立延迟SLO,因为你可能要求数据库返回其整个数据存储。另一方面,如果你能让每个操作返回固定数量的数据(比如几兆字节),写操作也一样,那么你就可以建立延迟SLO,然后测量该SLO的利用率。如果你不这样做,那么当你尝试之前为无状态服务讨论的基于延迟的推测和对冲方法时,你只会不断重试最昂贵的工作,效果非常差。但如果你有网关可以将这些操作转化为分块或分页操作,那么你就可以利用我们之前讨论的同一套弹性技术工具包。因此,网关首先将你的数据存储转化为增量API。

然后,它们还可以帮助你使其具有幂等性。这一点至关重要,因为重试和对冲严重依赖于能够安全地重复操作。很多数据库本身并不具有幂等性。例如,如果你在Postgres上执行一个操作,向一列写入一个值,然后在一段时间后与其他写入并发地重试该写入,你可能会损坏数据——那个对冲写入可能会覆盖你之前写入的数据。因此,网关的作用是使用我们称为“幂等令牌”的东西,为所有操作提供统一的幂等层。然后,为每个支持它们的数据存储实现这些抽象层的代码,会实现这种幂等性。例如,对于Cassandra,技术是“最后写入获胜”;对于事务性数据存储,你可能使用版本戳并事务性地推进它。

当你将增量和幂等性结合起来时,你就得到了可重试性。这对于处理负载高峰至关重要,因为处理负载高峰的一部分是知道系统何时变慢。我们之前讨论过基于延迟的丢弃,这些技术结合起来让我们拥有这种“对冲前时间”曲线。我希望你从这张图中带走的关键点是:你可以为你的业务选择任何对冲策略,但关键是,当客户端观察到的延迟变得非常高时,我们会停止所有对冲。这是我们使用的一种弹性技术,试图在出现大负载高峰时挽救系统,我们不想通过重试所有昂贵的工作而使情况恶化。但在正常操作中,这些对冲有助于我们尽可能严格地保持SLO。

让我们把它们放在一起。这是一个真实世界的负载高峰,Netflix一个关键值抽象层上出现了2倍负载高峰。我们看到负载翻倍。立即,我们的服务器开始使用内置的并发限制器和CPU限制器丢弃负载。然后,客户端对冲和指数退避重试减轻了对用户的影响(顶部的图大部分是绿色的,这很好)。最后,自动扩缩容在几分钟内自我修复了系统,并且没有唤醒任何人类。计算机为我们做到了这一点——这就是我们的目标:我们希望这些弹性技术协同工作,这样我就不必被叫醒,我们的值班工程师也不必被叫醒。

如果你对这些抽象感兴趣,我们写了一系列关于如何在Netflix抽象存储的文章,比如我们的键值服务和时序服务。贯穿所有这些文章的一个共同点是,我讨论的这些弹性技术都内置其中——你无法使用一个没有将工作分解为小的、可增量重试、并能持续取得进展的数据层。

最后我想提到的是,与一些行业参与者相反,我们实际上非常喜欢使用缓存来提高可用性。我想用两个例子来说明。第一个是,我们大量使用进程内服务缓存。这里的要点是,你经常听到人们说“我想缓存我的数据库”。在Netflix,数据库并不承担大部分繁重工作,服务才是——服务与各种数据源通信,组合它们,这相当昂贵。因此,如果我们在服务前面放置一个缓存,当服务有变更端点调用时使其失效,然后将大多数读操作托管在缓存中,我们基本上就能用Memcached替换那些Java服务。谁用过Memcached?是的,它能比Java服务处理多得多的负载高峰。因此,我们利用这种能力,通过简单地防止这些负载高峰击中服务本身,来应对服务上数量级的负载高峰,相反,它们击中了这些能以低成本处理高负载的缓存层。

但这里有一个问题:你必须开始将那个缓存视为本质上的Tier 0服务,它基本上就是你的服务。在Netflix,我们花了很多时间让我们的缓存可以全局复制,可以从副本填充,这样我们的命中率始终保持极高。如果你好奇,可以查看我同事Privy和Triam关于全局缓存的演讲。此外,服务必须愿意接受最终一致性。显然,需要强事务边界操作的工作负载不太适合这种模型。幸运的是,大多数人并不知道推荐给他们的电影行是否正确。

最后,我们完全缓存我们能缓存的一切。这使用了一个名为Hollow的开源项目。这里的想法是:如果负载高峰根本不击中数据库呢?相反,我们做的是:我们获取数据存储的快照。例如,我们在媒体数据存储中大量使用这种方法,比如哪些内容可以在哪个国家、哪种电视上观看。这些数据被快照到S3,然后拉入我们的视频元数据服务的内存中。我们的视频元数据服务是一个无状态服务,它从S3拉取状态快照,但这没关系,它可以像任何其他服务一样自动扩缩容。这样做的好处是,你只需要在VMS服务上处理负载高峰,而不必同时在其数据库上处理负载高峰。在Netflix,我们大量使用这种方法来处理我们称之为中小型数据集的情况,即数据量在10GB以下的数据集,事实证明这涵盖了大量的数据。

课程名称:Netflix应对流量高峰的技术:P07:测试与总结

当然,我想在这里以提醒结束:我讨论的许多技术可能不适合你的环境。我强烈鼓励你持续测试。如果你认为其中一种技术会有帮助,我鼓励你运行测试。在Netflix,我们使用一个测试金字塔,从底层的标准单元测试、属性测试、模糊测试和负载测试,一直到我们实际上将Netflix的所有流量引导到一个区域,看看哪些我们不知道的缓冲区开始耗尽的情况。我们还进行自动扩缩容挤压测试,我们选取单个服务,用10倍负载高峰冲击它,并断言诸如“自动扩缩容是否能在四五分钟内恢复服务的成功缓冲区”等属性。因此,我讨论的这些技术确实需要被测试并持续重新测试。

这里有一个负载测试的例子,我们经常这样做:我们有一些工作负载(在这个例子中,是0到10,000次操作/秒,1到4KB数据大小),我们使用一个基准测试工具,将其作为系统测试植入,然后我们基本上测量它在哪里崩溃。我今天讨论的每一项技术,都是通过这种风格的负载测试来验证的。这就是我们知道可以安全地开始推广到生产环境的原因。

总之,如果你能运用我们今天讨论的一些技术,仔细管理你的流量需求并用供给来平衡它——比如流量整形、回退、优先级划分,做出艰难的业务权衡(有时慢比宕机好,有时过时数据比宕机好)——希望至少其中一点是你可以带回自己的组织并应用到你的业务中的。谢谢,我很乐意回答任何问题。

总结

本节课中我们一起学习了Netflix如何应对其全球服务中出现的显著需求波动。我们首先了解了Netflix面临的全球规模挑战和负载高峰的主要来源。接着,我们深入探讨了如何通过DNS流量调度和优先级负载丢弃来管理全局流量需求。然后,我们分析了管理计算资源供给的复杂性,包括容量规划、缓冲区概念以及预测性与反应性扩缩容策略。我们还详细介绍了负载丢弃、工作优先级、对冲与重试等关键弹性技术,以及如何将这些技术应用于有状态服务和通过缓存策略提升可用性。最后,我们强调了持续测试对于验证和确保这些技术有效性的重要性。通过综合运用流量管理、资源供给平衡和弹性技术,Netflix能够在面对巨大负载波动时保持服务的高可用性,并将对用户的影响降至最低。

008:可靠、高效地处理日志

在本教程中,我们将学习如何使用 Fluent Bit 构建可靠且高性能的日志处理管道。我们将从日志的生命周期开始,探讨可能遇到的问题,并深入了解 Fluent Bit 的配置选项、性能优化技巧以及监控方法,帮助你确保日志数据能够完整、及时地送达目的地。

日志的现状与挑战

上一节我们介绍了本教程的目标,本节中我们来看看为什么需要像 Fluent Bit 这样的工具来处理日志。

日志无处不在,它们是系统可观测性的基石。然而,现代云原生环境中的日志处理面临三大挑战:

  • 来源多样:应用程序、第三方组件、基础设施(如 Kubernetes)都会产生日志,且格式不统一。
  • 体量激增:日志数据量通常以每年 250% 的速度增长,带来巨大的存储和成本压力。
  • 用途广泛:SRE、开发人员、安全团队等不同角色都需要访问日志,但需求可能不同。

这导致日志管理变得复杂,数据可能分散在不同的系统中,难以形成统一、可操作的视图。

Fluent Bit 简介:云原生可观测性管道

了解了挑战之后,我们来看看解决方案。Fluent Bit 是一个开源的、高性能的日志、指标和追踪数据收集、处理与转发工具。

其核心定义是:Fluent Bit 使你能够从任何来源收集事件数据(日志),通过过滤器进行丰富处理,并发送到任何目的地。

它的处理流程可以概括为以下几个阶段:

  1. 输入:从各种来源(如文件、标准输出、HTTP)收集数据。
  2. 解析:将原始数据转换为 Fluent Bit 的内部事件格式。
  3. 过滤:对事件进行修改、丰富(如添加 Kubernetes 元数据)或路由。
  4. 缓冲:在内存或文件系统中暂存数据,以应对背压。
  5. 路由与输出:将处理后的事件发送到指定的目的地(如 Elasticsearch、Datadog、S3 等)。

Fluent Bit 通常以三种模式部署:

  • 边车代理:在 Pod 内与应用容器一起运行,专门处理该容器的日志。
  • 守护进程集代理:在 Kubernetes 每个节点上运行,收集该节点上所有 Pod 的日志和系统指标。
  • 聚合器/转发器:作为中心节点,接收来自多个代理或服务器的数据,进行聚合后再转发。

云原生日志的生命周期

现在,让我们跟随一条日志,看看它在 Kubernetes 环境中是如何被 Fluent Bit 处理的。假设 Fluent Bit 以 DaemonSet 形式运行。

以下是日志经历的典型阶段:

  1. 应用记录:应用(如 Nginx)将日志行写入标准输出。
    192.168.1.1 - - [10/Oct/2025:13:55:36 +0000] "GET /api/data HTTP/1.1" 200 1234
    
  2. 容器运行时捕获:容器运行时(如 containerd)将标准输出转换为 CRI 日志格式,并写入文件。此格式添加了时间戳、流信息等元数据。
  3. Fluent Bit 采集:Fluent Bit 的 tail 输入插件跟踪(tail)日志文件,并应用内置的解析器(如 cri)来识别日志格式。
  4. 内部事件转换:Fluent Bit 将日志行解析为其内部事件格式,包含时间戳和供内部使用的元数据。
  5. 元数据丰富:通过 kubernetes 过滤器,Fluent Bit 查询 Kubernetes API,获取 Pod 名称、命名空间、标签等信息,并将其添加到日志记录中,使其具备上下文。
  6. 进一步处理与输出:日志可以被其他过滤器处理(如删除字段、脱敏),最终根据配置的路由规则被批量发送到指定的输出目的地。

可能出错的事情:四大“灾难”

在理想路径之外,我们需要为异常情况做好准备。日志在 Fluent Bit 处理过程中可能遇到以下问题:

以下是四种需要避免的“灾难”:

  • 丢弃:日志永久丢失,这是最坏的情况。
  • 延迟:日志未能及时送达,影响实时监控和告警。
  • 重复:由于重试机制,同一批日志可能被发送多次。
  • 失真:日志格式解析错误或处理不当,导致信息无法理解。

这些问题通常由背压引起,即下游处理能力不足导致上游数据堆积。背压可能源于:

  • 资源不足(CPU、内存、网络带宽)。
  • 数据源产生日志过快。
  • 处理管道中存在瓶颈(如过多串行过滤器)。
  • 输出目的地故障或响应缓慢。

配置 Fluent Bit 以实现可靠性与性能

了解了问题和原因后,本节我们来看看如何通过配置来规避风险。虽然默认配置适用于简单场景,但为了应对生产环境的严苛要求,我们需要进行调优。

配置验证与测试

在部署前进行验证可以避免许多问题。

以下是几个有用的验证工具:

  • Dry Run:使用 fluent-bit --dry-run 命令可以快速检查配置文件语法。
  • 自定义解析器测试:使用 Rubular 等在线工具测试和调试你的正则表达式解析规则。
  • Expect 插件:在 CI/CD 管道或本地测试中使用此插件,验证数据在管道每个阶段的转换是否符合预期。

缓冲与流量控制

合理的缓冲设置是可靠性的关键。

以下是针对 tail 输入插件的重要缓冲配置:

  • Buffer_Chunk_Size:设置每个内存块的大小,默认 32KB。对于冗长的日志行,可能需要调大。
  • Buffer_Max_Size:设置单个文件可读入缓冲区的最大尺寸。
  • Skip_Long_Lines:设置为 On,避免过长的日志行阻塞整个处理队列。
  • Mem_Buf_Limit:为每个输入插件设置内存缓冲区上限,防止内存耗尽。
  • 文件系统缓冲:启用 storage.type filesystem,当内存缓冲区满时,将数据暂存到磁盘,提供更强的可靠性保障。

以下是输出阶段的控制选项:

  • Throttle 过滤器:控制发送到目的地的日志速率,防止冲垮下游服务。配置示例定义了 Rate(事件数/周期)和 Window(时间间隔)。
  • 调度器与重试:配置 Retry_Limit(最大重试次数)、Retry_Backoff(退避基准时间)等参数,采用指数退避策略处理输出失败。

性能优化技巧

为了充分发挥 Fluent Bit 的性能,可以考虑以下高级功能。

以下是提升性能的主要手段:

  • 多线程:启用 workersMultithreading,让输入、输出和处理器在独立线程中运行,显著提升吞吐量。注意:如果目的地严格要求日志顺序,则需谨慎使用输出多线程。
  • 处理器:将复杂的、串联的过滤器逻辑转换为处理器,并附加到输入插件上。处理器在数据进入管道初期执行,可以减少过滤阶段的拥堵。
  • 流处理:使用 Fluent Bit 的流处理引擎,通过类 SQL 语句对流动的数据进行实时查询、聚合和分析,满足复杂的实时分析需求。

监控 Fluent Bit 自身

一个处理日志的系统,其自身的健康状态更需要被监控。Fluent Bit 提供了丰富的自监控指标。

Fluent Bit 会暴露 Prometheus 格式的指标,帮助你回答以下问题:

  • 运行状态:运行了多久?何时启动?经历了多少次热重载?
  • 数据处理量:各输入、过滤器、输出插件处理了多少记录、数据块和字节?
  • 缓冲状态:内存和文件系统中分别有多少 up(就绪)、down(存储)和 busy(处理中)的数据块?
  • 输出状态:到每个目的地的连接数、活跃连接数、重试次数是多少?

重要提示:监控指标比 Fluent Bit 自身的日志更可靠地反映数据是否被丢弃,因为重试过程中的错误日志可能产生误导。

诊断工具

当出现问题时,可以使用内置工具进行深度诊断。

以下是三个关键的内置诊断工具:

  • 健康检查端点:配置 HTTP 健康检查,基于错误和重试次数判断服务状态。
  • 内省信号:向 Fluent Bit 进程发送 SIGCONT 信号,它会生成一份详细的内部状态报告,涵盖任务、数据块和存储层情况。
  • Tap 功能:类似于追踪,可以展示单个日志记录在 Fluent Bit 内部流经各个处理阶段的详细路径和时间,用于深度调试。

总结与社区资源

本节课中,我们一起学习了如何利用 Fluent Bit 构建可靠、高效的日志管道。我们从日志的挑战出发,深入探讨了 Fluent Bit 的架构、日志的生命周期、可能遇到的问题及其解决方案,并详细介绍了配置优化、性能调优和监控诊断的方法。

要掌握 Fluent Bit 的强大功能,需要理解其内部架构。希望本教程为你播下了种子,激发你进一步探索。

你可以通过以下方式加入社区并获取更多资源:

  • Slack 频道:在 Fluent 社区 Slack 中交流。
  • 文档与博客:访问 Fluent Bit 官方文档
  • 《Fluent Bit for Kubernetes》电子书:获取结构化学习指南。

感谢 Fluent Bit 社区的所有贡献者和用户。

009:我们的 OpenTelemetry 之旅 🚀

概述

在本教程中,我们将跟随 ThousandEyes 公司的 SRE 团队,学习他们如何在一个复杂的分布式系统中引入并应用分布式追踪技术。我们将了解他们在没有追踪时所面临的挑战,探索 OpenTelemetry 如何帮助他们构建统一的追踪管道,并学习他们推动技术落地的宝贵经验。无论你是初学者还是有一定经验的开发者,这篇教程都将为你提供一个清晰、实用的分布式追踪入门指南。


第一章:背景与挑战

上一节我们概述了本次学习之旅。本节中,我们来看看 ThousandEyes 平台的基本情况,以及他们在引入分布式追踪之前所面临的困境。

我们的应用程序、基础设施都是分布式的。单体服务器的时代已经过去。一个请求可能会穿越许多不同的技术栈、SaaS 平台,甚至不同的云提供商。

ThousandEyes 构建了一个网络保障平台,帮助客户可视化、理解并检测其自身网络及互联网中的问题。因此,我们必须能够理解平台底层发生的情况。随着公司发展,我们向客户交付了越来越多的服务,请求在基础设施中穿行的路径也变得异常复杂。

我们内部虽然重视可观测性,但希望借助分布式追踪更进一步。

技术栈概览

以下是平台涉及的主要技术:

  • 编程语言:大量 Java,以及一些 Kotlin、Go、C++、Python 和 Rust。
  • 部署平台:主要部署在 AWS 的 Kubernetes 上,跨越三个核心区域和一些灾备区域。
  • 数据流:大量使用 Kafka 进行数据传输。
  • 数据存储:包括 MySQL、ClickHouse、Elasticsearch 和 MongoDB。
  • 服务网格:使用 Istio 进行服务间通信管理。

Istio 服务网格为我们带来了许多好处,包括 gRPC 负载均衡、服务发现能力,以及大量的可观测性数据。我们甚至一度“淹没”在 Prometheus 指标中。这让我们开始更好地理解服务间的交互关系。

下图展示了我们 Web 应用命名空间中服务间的互连情况(由 Kiali 生成)。你可以看到,它看起来相当混乱,有许多移动的部分和服务相互连接。试想一下调试问题时的情景:两个服务相互连接,但我们真的能分辨出是在何种情况下、针对哪个请求或用户操作吗?

我们意识到了 Istio 的分布式追踪功能,但尚未探索。这成为我们开启分布式追踪之旅的动机之一。


第二章:没有分布式追踪时的故障排查

上一节我们介绍了平台的复杂性。本节中,我们通过一个具体场景,来看看在没有分布式追踪时,故障排查是怎样的体验。

想象一个清晨,警报响了。客户请求收到大量 5XX 错误。我们打开电脑检查指标。很好,我们看到了下降趋势,知道服务 Web 请求的某个环节出了问题。然而,指标缺乏深度上下文。它们擅长识别趋势和提醒问题,但缺乏细节。

接下来,响应人员可能会开始查看日志。访问日志在可用性问题中很有用,我们知道哪些端点失败了。但是,这个请求下游有哪些服务?是单个的 Agent View 服务有问题,还是 Scheduled Test 服务有问题,或者是 Timeline 服务?为什么返回了 500?

此时,响应人员需要检查更多的仪表盘和日志,尝试在脑海中构建图像。他们需要消化大量与当前请求不直接相关的信息,尝试确定服务所有权和请求流,并可能联系其他响应人员。这个过程非常困难且令人沮丧。

面临的挑战

以下是当时遇到的主要挑战:

  • 手动关联:在众多仪表盘中搜寻,从 300 个图表中寻找异常、峰值和谷值,试图为故障和问题请求的路径构建假设。这非常耗时,且可能误入歧途。
  • 缺乏上下文:日志量巨大,存在“大海捞针”的问题。除非日志特别详细,否则它们往往缺乏请求流的上下文。指标是高度聚合的,没有问题的具体细节。如果真有细节,又可能面临高基数问题。响应人员很难识别请求背后涉及的具体服务。
  • 知识孤岛:我们总是依赖那个最了解服务的“专家”。当他不在时怎么办?随着团队增长和重组,这将成为更大的挑战,我们不能依赖运气。
  • 人员疲劳:响应人员反复处理同类警报和流程,常常难以解决,只会感到疲惫。另一个副作用是,我们可能会在没有确认的情况下,将怀疑涉及的服务负责人拉入事件中,从而分散他们为客户构建新功能的精力。

所有这些因素都导致了问题与事件解决速度的放缓。


第三章:分布式追踪带来的改变

上一节我们看到了没有追踪时的混乱。本节中,我们来看看引入分布式追踪后,同样的故障排查场景有何不同。

我们有了相同的访问日志,但现在日志中包含了 trace ID。这是我们做的一项工作:确保信号混合,这确实能帮助人们找到追踪链路。

让我们深入查看这些具体请求的追踪详情。这里有很多信息。首先,我们看到请求从 Nginx Ingress Controller 开始。从日志中我们也能知道这点。但我们还能看到它经过了 API Gateway,各种 Istio 代理拦截了请求,最终到达了 Agent View Service。

我们甚至看到它调用了 Data Access Scheduled Test Service。问题似乎就出在这里。让我们进一步深入查看这个跨度(Span):“get timeline data for metrics method”。进一步查看,我们看到那里有一个异常,甚至可以展开查看完整的堆栈跟踪。

现在,团队获得的信息比最初的访问日志多得多。他们可以精确定位问题发生在测试数据未找到时。在这个案例中,他们能够确定 UI 即使在测试被禁用时,仍然在错误地调用测试数据。他们通过提交 PR 更新 UI 来缓解问题,随后也更新了后端,确保 API 在测试未找到时也返回 404。

我们之前也通过其他错误报告工具获取异常,但有了追踪,我们能够直接从日志一路追踪到端到端的流程,了解涉及哪些服务,并在上下文中查看异常。


第四章:上下文传播与 OpenTelemetry 方案

上一节我们看到了追踪的强大。本节中,我们来探讨实现追踪的核心概念——上下文传播,并介绍我们基于 OpenTelemetry 的解决方案。

理解上下文传播

上下文传播是分布式追踪的关键。请求流中的第一个服务需要启动一个跨度上下文(Span Context)对象。它会确定一个 trace ID,以及自身操作的 span ID。对于根服务,没有父 span ID。这里通常还会做出采样决策。

当服务 A 向服务 B 发起请求时,它会注入头部信息(在 HTTP 情况下是 HTTP 头部)。服务 B 会提取这些头部,并启动自己的跨度上下文对象。这个对象会识别出来自服务 A 的父 span ID,然后为其自身操作创建新的 span ID 和跨度上下文对象,依此类推,服务 C 也是如此。

我们可以看到,trace ID 在整个链路中保持不变,而 span ID 则在整个追踪中维持着跨度之间的链接。这对于维持追踪的顺序和结构至关重要。跨度会来自我们基础设施的各个角落。采样决策通常也随上下文一起传递。在大多数情况下,第一个服务通过头部采样做出决策。当然,还有更复杂的采样策略,但我们目前保持简单。

我们的请求流与挑战

我们的典型请求流是怎样的?我们是否需要更新所有应用程序的代码来维护这个上下文?

让我们先看一个典型请求流。用户请求某个 Agent 的测试数据。首先,请求通过 Istio 代理到达我们的 Nginx Ingress Controller。Istio 服务网格会拦截所有进出请求。然后,请求会调用 Agent View Service,再到达 Data Access Service,后者会调用其他服务和数据库来完成请求。

Istio 正在拦截你的请求。我们最初天真地想:能否在不更改应用程序代码以传播上下文的情况下,仅仅通过启用 Istio 的追踪功能,就从应用程序中获取追踪信息并理解这些请求流?当我们推广追踪时,经常被问到:Istio 不能替我们做这些吗?

遗憾的是,不能。请求到达 Nginx Ingress Controller,Istio 启动一个追踪,创建带有 trace ID 的跨度上下文对象。但 Nginx 会将头部传递给下一个服务。所以,Istio 代理和 Agent View Service 会收到请求,并从 Ingress Controller 提取追踪上下文。然而,当 Agent View 应用向 Data Access Service 发起请求时,除非它包含了追踪上下文头部,否则 Istio 会启动一个新的追踪。Istio 不是“读心术”。不更改代码,我们最终会得到断开的追踪。

这看起来会是这样:我们有一个追踪对应请求流的第一部分(Ingress Controller 和 Agent View)。当 Agent View 向 Data Access Service 发出请求时,这个追踪就断开了,除非它传递头部以传播上下文。这不理想。之前提到的问题是手动关联,所以我们需要保持追踪的连贯性。

我们必须弄清楚如何在应用程序和基础设施组件(如 Istio 和 Nginx)中保持上下文的传播。这时,OpenTelemetry 走到了前台。

引入 OpenTelemetry

在深入我们的管道细节之前,我先为不熟悉的人介绍一下 OpenTelemetry。

在 OpenTelemetry 出现之前,分布式追踪相对是专有的、端到端的。供应商之间没有一致的追踪格式或协议共享。如果你想从供应商 X 切换到供应商 Y,就必须从代码中剥离旧的插桩(Instrumentation)并替换成新的。这非常耗时,而你本可以用这些时间构建新功能。

OpenTelemetry 出现了,它提供了一个供应商中立的插桩、收集和导出遥测数据的框架。它提供了一组 API、SDK 和工具,用于插桩、生成、收集和导出日志、追踪和指标。还有一些关于事件和性能分析的新工作,我们也在密切关注。

它有一个标准化格式:OpenTelemetry 线路协议(OTLP)。但也支持多种格式的接收和导出。这一点尤其引起了我们的兴趣:我们如何收集所有数据呢?

我们引入了 OpenTelemetry 世界的“瑞士军刀”——OpenTelemetry Collector。它提供了一个供应商无关的解决方案,用于收集、处理和导出遥测信号。这样,我们可以接收不同格式的遥测数据。对我们来说,这意味着 OTLP 和 Zipkin。我们当时评估了基础设施,Istio 不支持 OpenTelemetry 格式(尽管后来增加了支持,最近甚至获得了 HTTP 支持,我们正在考虑迁移)。因此,我们将 Collector 配置为同时接收 OTLP 和 Zipkin。这允许我们从代码中的 OTel Agent 收集 OTLP 信号,并从 Istio 和 Nginx 等基础设施组件收集 Zipkin 格式的数据。

然后,我们也可以导出到多个后端。刚开始时,我们尝试了许多不同的后端。通过 OTel Collector,我们可以确保相同的追踪最终出现在各种不同的后端中,并可以并排比较完全相同的追踪。我们根据用户界面、搜索和查询语言灵活性、API 集成等因素对它们进行评估。

部署架构

在部署 OTel Collector 方面,一种方法是使用 Agent 模式,将其作为 Sidecar 与应用程序一起部署。另一种方法是将其集中部署在集群中,即网关模式。我们决定集中部署,因为这意味着当我们更改配置变量(如更改追踪后端或后端参数)时,不需要重启或重新配置所有应用程序。同时,我们可以在中心位置过滤跨度,通过资源处理器添加资源属性(如环境级别详情)。如果未来我们转向尾部采样,由于已经集中部署,我们可以过滤所有数据。

我们如何部署这个 Collector 呢?这里有一个好主意,尤其是在 Kubernetes 中操作时:OpenTelemetry Kubernetes Operator。我们用它来管理我们的 OTel Collector。我们定义一个 OpenTelemetry Collector 自定义资源,在其中定义接收器、处理器和导出器配置。然后,Operator 会在 Kubernetes 服务后面部署多个 Collector 副本,甚至处理诸如通过 HPA 管理自动扩缩容等事情。这是快速启动和运行 OTel Collector 的好方法。

但真正的魔力来自于注入的自动插桩。为了传播至关重要的上下文并从应用程序中获取丰富的遥测数据,我们需要插桩。通过 Operator,我们定义一个 Instrumentation 资源,它将环境变量注入到 Pod 中,并允许我们按服务在自定义资源中定义 Agent 配置。

在服务启动时,一个 Init 容器被注入到 Pod 中,它会挂载 OpenTelemetry Java Agent。同时,在服务启动时将其注入。这意味着团队无需直接将库添加到代码中,就能获得相当不错的插桩水平。

我们的 Instrumentation 资源看起来像这样:

apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: my-instrumentation
spec:
  propagators:
    - tracecontext
    - b3multi
  sampler:
    type: parentbased_traceidratio
    argument: "0.01"
  java:
    image: otel/java-agent:latest

敏锐的读者会注意到,我们的传播器(propagators)同时包含了 tracecontextb3multi。我们基于 Zipkin 的基础设施(Istio 和 Nginx)需要 b3multi 格式来在整个链路中传播头部。因此,虽然我们以两种格式发送追踪,但需要配置 OTel Agent 不仅为未来传播 tracecontext,还要支持 Zipkin 的 b3multi 格式,以兼容 Istio 和 Nginx。

我们还使用 Kubernetes 对象元数据来构建一些 OTel 环境变量,特别是服务名称,以匹配 Istio 的服务名称,保持一致性,避免出现服务名称变体和重复。插桩还会向 Pod 启动添加大量与 Kubernetes 相关的元数据环境变量,包括 Pod 名称、命名空间、节点名称等。这对于故障排查、与日志和指标关联,或者查看来自特定 Pod 的追踪非常有用。

整体架构

那么整体架构是什么样的呢?我们有一个相同的请求流:请求到达 Ingress Controller,Istio 启动追踪上下文,将请求发送给 Agent View Service,上下文作为 b3multi 头部包含在内。Agent View Service 向 Data Access Service 发起请求,也发送 b3multi 格式的追踪上下文。来自 Nginx 和 Istio 代理的追踪跨度是 Zipkin 格式的。应用程序中的自动 Agent(通过注入的插桩添加)使用的是 OTLP 格式。但由于我们全程使用 b3multi 来保持追踪上下文和 trace ID 一致,传播不会中断,我们的追踪保持连贯。

所有数据在 OTel Collector 处被转换为 OTLP 格式,然后导出到我们的后端(目前是 Grafana Tempo)。如果你深入追踪领域,可能会发现旧的软件和基础设施不支持最新的 OTLP 和 W3C 标准。但好处是,OTel Collector 很可能支持你需要的标准,并能将其转换为 OTLP。它支持许多不同供应商和格式。


第五章:推动采用与未来展望

上一节我们构建了追踪管道。本节中,我们来看看下一个挑战:如何推动团队采用这项技术,并展望未来的发展方向。

一旦我们的管道启动并运行,下一个挑战就是采用。如何鼓励使用它?关于采用,我认为有两方面:服务需要被插桩,以获得重要的上下文传播和服务操作详情,这需要尽可能简单。另一方面是人员,他们需要在数据就位后开始使用这个系统。

插桩策略

我们意识到有两种主要的插桩类型:自动插桩和手动插桩。

OpenTelemetry 的自动插桩 Agent 为多种语言提供支持。当使用通用库时,它们能原生地插桩应用程序的常见功能,如 HTTP、RPC、数据库和消息调用。这也能确保在这些操作跨越多个服务时进行上下文传播。我们的大多数服务是 Java,可以快速受益于自动插桩。

然后是手动插桩。工程师需要将 OTel API 或 SDK 添加到代码中,然后需要手动使用这些 SDK 来提取和注入追踪头部,并为他们的代码操作创建跨度。这提供了更多的灵活性和细节,但也带来了开发时的障碍。最终,我们一些使用 C++、Go、Rust 的服务必须使用这种方式。但我们希望首先专注于自动插桩。

我提到了注入的自动插桩。这是我们真正投入的地方。我们可以在几分钟内将 OTel Java Agent 添加到服务中,并快速从追踪中获得价值。这可能是目前主要的插桩方法,我们已有 100 多个服务通过这种方式进行了插桩。

鼓励插桩

有了这些选项,我们如何鼓励插桩呢?

首先,我们专注于一个核心服务——我们的主 Web 应用程序。我们与 Web 平台团队合作,为我们的 Web 应用和 API 添加了注入的插桩。这为我们提供了一个在整个组织内被认可的核心示例。许多人都使用过这个主 Web UI,很多人也为它贡献过代码。这有助于我们鼓励其他应用程序进行追踪。

有一个团队实际上有 50 多个服务,全部是 Java,在一个单体仓库中。通过一些巧妙的定制化补丁,我们能够相对快速地为他们所有的服务进行插桩。

然后是丰富的文档。ThousandEyes 是一个全球性组织,工程师能够异步地插桩他们的代码并掌握分布式追踪非常重要。因此,我们的文档解释了管道架构,提供了详细的插桩说明,并引用了 OpenTelemetry 官方文档,还提供了使用 Operator 和注入插桩来插桩服务的复制粘贴和自助服务示例。

ThousandEyes 有很好的技术分享文化。技术分享允许工程师分享他们发现的有趣技术、新功能以及构建方式,也包括像这样的平台变更。一旦我们有了像 Web 应用这样的核心示例和一些演示,我们就可以向整个组织展示,并有文档供人们参考以便开始。我们准备并发表了一场技术分享,涵盖了分布式追踪概述、管道工作原理以及如何插桩你的服务,重点介绍了注入的自动插桩。这引发了很多问题,一些非常热情的团队立即开始插桩他们的服务。

然后是生产就绪检查清单。让旧代码更改和插桩总是更困难,但我们不想进一步增加技术债务。因此,我们将插桩要求加入了我们的生产就绪检查清单中。

鼓励使用

一旦我们获得了所有数据,人们需要开始使用系统才能真正从中获得洞察。

我们做的其中一件事是,在有了核心示例并更新了 Web 应用程序后,我们更新了 Web 应用程序的应急预案。这确实给了响应人员一个提醒:追踪是存在的。尤其是在事件发生的紧张时刻,他们可能会忘记。预案中还包含了定制化的查询。这样,如果他们还不熟悉查询语言,可以帮助他们理解并找到诸如 5XX 错误或高延迟等问题,他们只需点击链接,追踪信息就会呈现在他们面前,他们会记住它。

然后是混合信号。旧习惯很难改掉,习惯于检查日志的响应人员可能仍然会专注于日志。因此,我们从第一天起就将 trace ID 添加到了 Istio 和 Nginx 的访问日志中。OTel Java Agent 通常也会将 trace ID 添加到日志中。我们发现许多用户会直接从日志跳转到追踪。他们在日志中看到 trace ID,就会想:“好吧,让我看看这下面发生了什么。”

还有一个通用的追踪探索仪表盘。我们意识到人们可能需要一些时间来学习查询语言。我们希望人们一旦插桩了他们的服务,就能快速探索涉及他们应用程序的追踪。通过这个仪表盘,他们可以去点击他们的服务,它会运行一些查询,比如查找高延迟追踪、查找有错误的追踪,然后查找一些跨度多的追踪(因为它们往往更有趣)。这允许人们在插桩后立即探索工具,并可以使用这些查询作为示例来构建自己的查询。

然后是事件演练日。在 ThousandEyes,我们通过运行模拟事件来培训新的事件响应人员。在一些场景中,我们使用追踪,并且应急预案与 Web 应用程序的相同,这帮助人们熟悉可观测性工具。实际上,在响应过程中可能没用上,但我们可以在事后复盘时使用它,帮助人们熟悉。塑造新面孔更容易。如果他们从一开始就看到价值,希望他们会更频繁地使用它。

然后就是一般性地推广追踪,思考“我们能用追踪解决这个问题吗?”或者在事件中帮助他人。我和我的团队有时会关注线上问题,如果我们认为追踪可能有助于解决问题,或者问题与 Web 应用程序有关,我们会去帮忙,帮助人们构建查询,并给予一些小小的提醒。我们甚至在事件中途为某个服务添加过自动插桩。

另一个奇怪的用例是 Jenkins。有一个团队的构建特别慢,他们无法确定原因,构建有很多并行分支和大量步骤。我们为 Jenkins 添加了追踪。他们可以将整个构建可视化为一个追踪,并立即看到耗时最长的部分。结果发现是读取外部文件多花了大约一到两分钟。他们将文件嵌入到 Jenkins 作业中,立即为每次构建节省了这一到两分钟。

未来工作

接下来是推广使用和采用,但我们的旅程还没有真正结束。当然,科技领域的事物需要持续维护,我们还有更多工作要做。

以下是一些未来的想法:

我们仍然相对较新于分布式追踪,所以需要插桩更多服务。采用率在增长,但仍有“潜伏者”。Istio 拦截所有请求带来的一个好处是,我们可以追踪哪些服务尚未插桩。我们需要关注这些服务。

接下来是 Exemplars。我提到了日志中的 trace ID,我们的用户使用的一个关键途径是从日志跳转到追踪。然而,如果能使用 Exemplars 会更好。通过 Exemplars,我们可以在图表上拥有数据点。例如,在那个可用性下降的图表上,有一个数据点,点击它,你就可以看到确切的上下文和一个示例场景,一个导致该可用性下降和 500 错误的示例追踪。想象一下,你能多快地从警报跳转到完整上下文。

还有更多:前端的 OpenTelemetry。我们的数字体验越来越多地在浏览器中,用户与 JavaScript 的交互也越来越多。我们一直在探索为前端元素添加追踪能力,以真正理解从浏览器到后端代码的完整用户体验。

然后是一些高级采样策略。目前,我们在预发布环境中采样 100%,这对于调试预发布版本甚至复现问题非常棒。但在生产环境中,我们大多采用 1% 采样,以保持数据量较低。大多数时候这没问题,但有时我们得不到足够的数据,或者错过了某些请求。同时,我们仍然保留了许多不感兴趣的数据,例如大量成功的交互。因此,我们想要探索头部采样:在客户端采样 100%,然后在 Collector 中收集所有追踪,再做出决策,例如对包含错误的追踪采样 100%(或稍低),对高延迟的追踪采样,甚至对具有特定操作的追踪以更高百分比采样。


总结

本节课中,我们一起学习了 ThousandEyes 公司实施分布式追踪的完整旅程。

我们需要认识到,系统和组织的复杂性将持续增长。我们的可观测性工具可以帮助克服这一点。我们可以不断改进工具,以打破故障排查的障碍。分布式追踪被证明是帮助我们更快解决问题的有效方法之一。

但如果没有 OpenTelemetry,我们无法做到这一点,或者至少旅程会更加艰难且缺乏未来保障。对我们来说,其标准化和开箱即用的功能,尤其是集成多种标准、测试多个后端以及未来相对轻松切换的能力,至关重要。

在采用方面,这总是一个障碍。但我们发现,尽可能简化流程、通过详细文档降低门槛以便人们异步操作,以及通过技术分享进行现场演示、鼓励采用并让人们对此感到兴奋,这些方法都很有帮助。注入的自动插桩确实是“救星”,它真正帮助我们让团队快速启动并运行,将插桩融入他们的代码。

当然,在让人们使用系统方面,只需尝试通过应急预案将追踪呈现在人们面前,给予他们小小的提醒,并混合信号(如在日志中添加 trace ID),确保他们在查看其他信号时能遇到它,从而可以直接从日志跳转到追踪。

010:使用统计技术自动检测游戏崩溃问题

在本教程中,我们将学习如何利用统计技术,在海量日志数据中自动识别导致云游戏体验崩溃的关键问题。我们将从问题背景出发,逐步介绍数据预处理、特征提取、模型评估到最终应用的全过程。


背景与挑战

我的团队来自OpenConnect,这是Netflix自建的内容分发网络。我们负责视频点播、直播流媒体和云游戏内容分发的可靠性、可扩展性和用户体验质量。

我们习惯于监控延迟、比特率和丢包等指标。然而,随着我们开始支持云游戏,确保良好的用户体验变得复杂得多。

云游戏允许用户在大多数电视和电脑上玩游戏,无需下载游戏或拥有强大的硬件。用户将手机作为控制器连接到我们的服务器,电视作为显示器。这意味着用户的输入从控制器传到服务器,服务器渲染游戏,再将帧发送回电视,本质上是一种实时流传输。

云游戏带来了独特的挑战。即使我们完美地交付了内容(延迟、比特率等指标都很好),用户仍可能体验极差。这是因为游戏本身可能“崩溃”了——我们可能正在传输黑屏、错误面板或循环内容。

一个具体案例是,我们最受欢迎的游戏上线后,在特定情况下,部分国家的用户连续三周无法正常游戏。问题的指示器只是一条日志:NGP profile response T name unknown。然而,检测它花了很长时间,原因有二:第一,没有强烈的“崩溃”信号;第二,导致问题的日志是“大海捞针”。

上一节我们介绍了云游戏监控面临的新挑战,本节中我们来看看我们如何定义“问题会话”。

定义问题信号

为了更有效地检测问题,我们需要将用户在崩溃会话中表现出的行为,与会话中出现的潜在问题日志关联起来,从而同时确认会话已崩溃且日志与问题相关。

存在多个指示会话崩溃的指标,但都很微弱。以下是这些指标及其局限性:

  • 游戏时长:如果游戏对用户崩溃,他们可能不会玩很久。但用户也可能因为不喜欢游戏而早早退出,或者尽管游戏不工作也耐心等待。
  • 调查评分:我们在游戏退出时显示调查,提供评分和自由反馈。低分可能意味着游戏崩溃,但也可能只是用户不喜欢游戏。
  • 比特率:如果传输的是黑屏等内容,比特率可能会下降。但有些错误(如循环内容)可能不会导致比特率大幅降低。
  • 点击率:当游戏崩溃时,每秒点击次数可能会减少。

对于这个项目,我们需要一个即使有噪声也足够好的单一指标。我们选择了游戏时长少于两分钟作为用户不满意的信号(无论是因为游戏问题还是单纯不喜欢游戏)。

同时,我们需要一个与问题相关的指示器,因此我们转向了日志。我们每天有数百万条错误日志,需要将它们聚合以减少基数并增强信号。

上一节我们确定了核心指标,本节中我们来看看如何处理海量的日志数据。

日志处理与降维

我们每天有海量日志(过去30天有约2000万条独特日志,总计1亿条)。我们需要将独特的日志条目聚合,以减少基数并增强从日志行出现中获得的信号。

我们通过以下流程实现:

  1. 选择与过滤:选择包含“异常”、“错误”和“警告”的程序输出,并排除已知与问题无关的高频日志。
  2. 预处理:对于许多错误日志,我们不需要完整的堆栈跟踪,因此可以分割并取第一行。此外,随机数、IP地址、URL等功能上通常不区分日志,因此我们用正则表达式替换它们。
    • 例如,将包含唯一任务ID的日志 Error processing task 12345 替换为 Error processing task [num],从而将大量独特日志合并为一条。
  3. 向量化:使用句子转换器将日志文本转换为向量(嵌入),这些数字数组能捕捉文本的语义。
    • 这可以通过几行代码实现,例如使用 sentence-transformers 库。
    from sentence_transformers import SentenceTransformer
    model = SentenceTransformer('all-MiniLM-L6-v2')
    log_embeddings = model.encode(log_lines)
    
  4. 聚类:使用聚类算法(如DBSCAN)将相似的日志向量聚在一起,使得语义相似的日志被视作同一类。
    • 这可以合并那些通过预处理无法合并的日志,例如各种“错误区域代码未找到”的变体。

通过这一系列步骤,我们将超过2000万条独特日志减少到135个日志簇(进一步将部分独特日志组合成26个集群)。

上一节我们成功将日志数据降维,本节中我们来看看如何量化日志与游戏崩溃之间的关联强度。

关联性量化:精确率、召回率与F1分数

现在我们有了一组基数较小的处理后的日志,可以评估它们与用户游戏时长过短之间的关联紧密程度。我们借用分类模型性能分析的三个概念:精确率召回率F1分数

具体做法是:将会话出现特定日志作为预测标签,将游戏时长少于两分钟(我们定义的问题信号)作为真实标签,然后看它们之间的对应关系如何。

  • 精确率:在所有预测为阳性(即出现该日志)的会话中,真实为阳性(即游戏时长<2分钟)的比例。
    • 公式:精确率 = 真阳性 / (真阳性 + 假阳性)
    • 可以理解为:出现某条日志的会话中,看起来崩溃的会话占多少百分比
  • 召回率:在所有真实为阳性的会话中,被正确预测为阳性的比例。
    • 公式:召回率 = 真阳性 / (真阳性 + 假阴性)
    • 可以理解为:所有看起来崩溃的会话中,包含某条日志的会话占多少百分比
  • F1分数:精确率和召回率的调和平均数,用于平衡两者。
    • 公式:F1 = 2 * (精确率 * 召回率) / (精确率 + 召回率)
    • 它平衡了“问题需要多普遍我们才能注意到”以及“日志对用户游戏时长过短应有多强的预测性”。

与问题相关的日志示例

  • 真阳性高,假阳性和假阴性低。
  • 结果:精确率83%,召回率67%,F1分数0.74。
  • 这意味着:83%出现该日志的会话看起来崩溃了;67%的短游戏时长会话出现了该日志。

与问题无关的日志示例

  • 真阳性低,假阳性和假阴性高。
  • 结果:精确率10%,召回率20%,F1分数0.13。

上一节我们学习了评估关联性的指标,本节中我们回到最初的案例,看这些方法如何应用。

案例回顾与应用

让我们回到最初那个持续三周的事件。如果按会话事件数绘制图表,与问题相关的日志(绿色)完全淹没在高频、不重要的错误日志中。

然而,当我们按F1分数排序时,该日志跃居榜首,表明它与游戏崩溃密切相关且影响广泛。进一步查看其精确率超过80%,意味着80%出现该日志的用户在两分钟内退出。

需要注意的是,这不是一个完美的指标,因为有些用户会耐心等待或中途做其他事。正是这种噪声特性,迫使我们建立了这一整套程序。

目前,这套程序已在我们所有游戏和平台上每小时运行一次,成功检测到了真实问题(其中一些已升级为事件),并促使我们调查小规模问题(尤其是通过精确率视角的仪表板)。

未来,我们计划:

  1. 加快告警流程。
  2. 使用“中途退出时间”指标,检测并非在游戏开始,而是在特定关卡或交互后发生的崩溃问题。
  3. 更广泛地与合作伙伴合作,改进测试,获取更清晰的信号等。

总结

本节课中我们一起学习了如何利用统计技术解决云游戏场景下的复杂监控问题。核心要点如下:

  1. 新领域带来新挑战:云游戏等新领域会引入看似棘手的监控难题。
  2. 统计方法使难题可解:面对海量、高基数的数据(如日志),统计方法可以提供可行的解决方案。
  3. 工具易于集成:所使用的统计工具和概念(如向量化、聚类、精确率/召回率)都有现成的库和模型,只需少量代码即可集成到工作流中,尽管调优和选择本身可能更具挑战性。

通过定义关键指标(短游戏时长)、处理海量日志(预处理、向量化、聚类)以及量化关联性(精确率、召回率、F1分数),我们构建了一个能够自动检测游戏崩溃问题的系统,显著提升了问题发现和解决的效率。

011:SRE大会-2025-美洲-|-srecon-|-分布式-|-缓存-|-OpenTelemetry-|-安全-|-AIOps-p11-P11-Mapping-a-Better-Future-with-STPA--BV1TmLDz7EZZ_p11-

课程概述:SRE与系统理论过程分析(STPA) 🧭

在本节课中,我们将要学习一种名为系统理论过程分析(STPA) 的方法论。STPA由麻省理工学院的Nancy Leveson博士和John Thomas博士开创,它帮助我们在系统构建之前,就识别出可能导致故障的设计缺陷。我们将通过一个Google Maps路况更新系统的真实案例,了解STPA如何超越传统的可靠性实践,预防那些并非由单一组件故障引起的系统级损失。


章节1:传统SRE方法的局限性 ⚠️

作为站点可靠性工程师(SRE),我们的目标是预测甚至预防尽可能多的系统中断。然而,SRE团队通常只占工程总人数的一小部分,我们面临着现实的资源约束。那么,我们如何才能有效地预测和预防所有可能的故障呢?

传统的SRE实践主要关注由组件故障引起的损失。例如,我们会问:“当我的服务崩溃时,客户会受到什么影响?”或者“当后端返回500错误时会发生什么?”。

但是,存在一整类系统损失,它们并非源于任何单一组件的故障,而是源于组件之间的交互。对于这类系统级损失,SRE和软件工程师往往缺乏有效的应对工具。

为了说明这一点,让我们看一个John Thomas博士使用的例子:黄油刀。

  • 黄油刀本身是可靠的。它很少会断裂,在切割黄油时能安全地履行其功能。
  • 然而,其安全性取决于上下文。如果一个孩子将黄油刀插入电源插座,情况就变得不安全了。
  • 那么,是什么“失败”了?刀是可靠的(金属导电),插座是可靠的(提供电力),孩子也是“可靠的”(如果你给他一把刀,他很可能会去插插座)。
  • 这里的损失源于刀、孩子和插座之间的交互,而非任何单一组件的故障。

上一节我们介绍了传统方法在处理系统交互问题上的不足,本节中我们来看看一个具体的系统故障案例。


章节2:一个令人困惑的系统中断案例 🚧

假设我负责一个处理并发布Google Maps上道路封闭信息的系统。这个系统包含多个环节:数据摄取、处理、验证、上传到数据库、进一步传播,最后提供服务。

一天早晨,我收到了手动告警,因为许多用户被导航到了一条正在举行游行的封闭街道上。这绝不应该发生。我检查了仪表盘,发现没有违反任何SLO(服务等级目标),所有服务都显示为绿色,每个服务都按预期工作。

那么,故障是如何发生的?

经过深入调查,我发现尽管每个组件都按设计工作,但服务之间的交互配置有误。关键服务对其他系统部分的行为做出了错误的假设。为了修复这个问题,开发者和我需要花费两个月的时间来重新设计和调整这个系统。

这个案例引出了核心问题:为什么在一切看似正常的情况下,系统依然会发生中断?答案在于,传统的可靠性解决方案(如冗余或组件级质量改进)无法预防所有类型的损失。


章节3:引入STPA:系统安全性的新视角 🔍

在Google Maps SRE团队,我们采用了一种较新的方法论——系统理论过程分析(STPA)。我们坚信,STPA填补了我们在管理系统方式上的必要空白。

STPA的核心思想是:在软件系统构建之前,就揭示它们将如何被“破坏”。这能为我们节省时间和金钱,并最终帮助我们的用户。

STPA分析包含四个关键步骤:

  1. 定义分析目的:划定系统与环境的边界,明确系统目标、不可接受的损失以及危险状态。
  2. 建立控制结构模型:这与数据流图完全不同。控制结构模型描述的是控制和反馈回路,包含了系统中各部分之间的非线性关系。
    • 控制结构 != 数据流图
  3. 分析控制结构,识别不安全控制行为:找出在何种条件下,控制指令会导致系统进入危险状态。
  4. 识别损失场景:找出可能导致系统中断的具体、可实现的例子。

接下来,让我们将这些步骤应用到之前提到的道路封闭系统中。


章节4:应用STPA分析道路封闭系统 🗺️

首先,我们进行步骤1:定义分析目的

  • 目标:同步Google Maps与第三方道路封闭信息的状态。
  • 损失:不可接受的结果。例如:用户被导航至封闭道路。
  • 危险:导致损失的系统状态。即:Google Maps与第三方道路封闭信息不同步
  • 约束:为防止危险和损失,系统必须满足的条件。
    • 主要约束:必须始终保持同步(即危险状态的反面)。
    • 次要约束:如果进入危险状态,必须能够快速检测并缓解。这正是SRE在可观测性、灾难恢复等方面投入大部分精力的地方。

完成了目标定义,我们进入步骤2:建立控制结构模型

系统的总体目标是确保Google Maps包含第三方封闭信息的最新状态。

  • 底层受控过程:Google Maps的状态。
  • 上一层道路封闭处理器:负责同步Google Maps状态,通过向Google Maps发送“创建”或“删除”指令来实现。
  • 再上一层快照差异分析器:负责告诉处理器具体要添加或删除哪些封闭信息。它接收第三方快照作为输入。
  • 顶层人类(工程团队):他们拥有处理器和差异分析器的逻辑,并在系统不同步时收到告警。人类始终是系统的一部分。

这个控制结构揭示了关键点:差异分析器做出决策时,依赖的反馈是第三方快照,但它并没有直接获得关于Google Maps当前状态的反馈


章节5:发现致命的设计缺陷 💥

现在进入步骤3:分析不安全控制行为。一个不安全的行为是:当Google Maps上不存在某个道路封闭时,差异分析器没有创建该封闭信息。

那么,这如何发生?差异分析器如何知道Google Maps是否不同步?回顾之前的系统描述,差异分析器的心智模型假设:上一个快照等同于Google Maps的当前状态

但这个假设总是成立吗?让我们看看它如何被打破。

假设系统需要创建“25街封闭”的信息。但如果这个“创建”操作失败了呢?(例如,数据库提交失败、消息未发送、快照之间存在竞争条件等)。那么,Google Maps就不会应用这个封闭,系统进入危险状态。

接下来,当第三个快照到达时,系统会比较快照3和快照2。由于“25街封闭”在两个快照中都存在,差异分析器认为没有变化,因此永远不会重新尝试发布该封闭信息

至此,我们发现了设计缺陷:系统依赖于一个可能错误的假设。即使所有组件都按设计运行,系统也会持续更新其他封闭信息,而永远不会重试丢失的“25街封闭”,最终导致与真实世界完全脱节。

几天或几周后,大量用户因被导航至封闭道路而感到不满,我收到了告警。此时系统已处于损失状态。


章节6:STPA带来的改变与巨大收益 🚀

前面我们详细分析了一个由设计缺陷导致的潜在故障。如果我将这个有缺陷的设计投入生产,会发生以下情况:

  1. 系统上线,道路封闭信息开始从Google Maps上消失。
  2. 几小时、几天或几周后,用户受到影响,我收到告警。
  3. 我们花费一周时间试图找出哪些封闭信息丢失,但仍不知道根本原因。
  4. 再花两周找到这个设计缺陷。
  5. 最后用六周时间启动一个新系统(新设计直接从Google Maps读取状态,这在事后看来是显而易见的方案)。

然而,上述中断实际上从未发生。因为我们在构建系统之前就使用了STPA进行分析。我们通过STPA发现了这个设计缺陷,并在设计文档中通过重写几个段落就修复了它。具体来说,新设计将Google Maps的状态反馈直接提供给快照差异分析器

不仅如此,通过STPA,我们总共发现了七个重大设计缺陷,包括操作顺序无效、数据处理竞争条件、同步状态反馈不足、易出错的数据源接入流程等。

对于一个已经由各系统技术负责人审核通过的设计来说,这是一个很长的缺陷列表。为什么传统评审方法会遗漏这些?因为软件开发主要基于“快乐路径”,默认系统状态是安全的。而当思考“不快乐路径”时,我们缺乏系统性地审视系统安全性的工具,只能依赖在庞大状态空间中的随机探索,这通常导致我们选择忽略这些路径。

设计阶段应用STPA,识别和修复这七个缺陷的总成本极低:仅26小时,平均每个缺陷不到4小时。修复设计问题的成本随着产品成熟度呈指数级增长。因此,在新服务和新系统的早期阶段与STPA团队合作最为有效。

在Google,当我们将STPA应用于成熟系统时,如果发现安全漏洞,可能会遇到工程团队的阻力,因为修改成熟系统的成本很高。但是,当我们将STPA应用于系统设计时,工程合作伙伴非常乐于与我们合作。现在,他们甚至会主动来找我们进行STPA分析,因为我们在他们构建系统之前就帮助修复问题,这对他们来说成本要低得多。


课程总结 📝

本节课中,我们一起学习了系统理论过程分析(STPA)的核心价值。

  • 我们认识到,传统可靠性方法对由组件交互和错误假设引发的设计缺陷是盲目的。
  • 我们通常只能等待系统中断,然后被动地修复,这成本极高,并会打乱产品路线图。
  • STPA提供了一种在系统实现之前,就系统性地识别和预防此类问题的方法。
  • 设计阶段应用STPA,能够以极低的成本(如案例中的26小时发现7个缺陷)解决问题,避免未来高昂的故障修复和业务影响成本。
  • 这使工程师能够提前解决根本问题,成为效率倍增的“1000倍工程师”。

如果你想学习更多关于STPA的知识,可以参考麻省理工学院(STPA的发源地)发布的相关资料,Google也开始发布如何应用STPA的资源。

(演讲结束,进入问答环节)

012:SRE大会-2025-美洲-|-srecon-|-分布式-|-缓存-|-OpenTelemetry-|-安全-|-AIOps-p12-P12-Is-the-S-in-SRE-for-“Security”--BV1TmLDz7EZZ_p12-

概述 📋

在本教程中,我们将探讨SRE(站点可靠性工程)与安全(Security)之间的深刻联系。我们将通过数据分析、实际案例和核心概念,阐述为何安全性能与软件交付及可靠性性能高度相关,并介绍如何将SRE的实践与思维模式应用于安全领域,以实现整体技术性能的提升。

课程内容:1:安全与可靠性——共同的基石

我的目标是让安全变得不那么可怕,并使其像结构工程或机械工程中的安全性一样,成为技术工程不可或缺的一部分。这是一个关于安全与SRE如何重叠的故事。

我的祖父是一名飞行员,他飞行时总是使用检查清单。这启发我阅读了《清单宣言》一书。书中,外科医生阿图·葛文德讲述了如何将航空业的检查清单理念应用到外科手术中。这让我思考:我们能否将这种方法应用于网络安全?

这引导我进入了安全科学领域。安全科学家埃里克·霍兰格尔提出了“安全第二”的概念。他认为,传统上我们通过“没有坏事发生”来定义安全成功,但这无法形成科学。真正的安全科学需要关注全部结果范围,包括积极结果和正常结果,并将安全重新定义为“安全地工作”。

改变结果有两种策略。一种是约束性能,这会同时减少负面和正面结果。另一种更好的策略是提升整体性能,将绩效曲线向右移动。这样不仅能减少负面结果,还能增加正面结果。这种思维转变意味着安全或SRE工作不应被视为成本,而应被视为能带来更好结果的投资。

安全性能的核心在于:当系统暴露于威胁、危险或故障时,其表现如何。我将通过数据研究表明,网络安全性能与软件交付及可靠性性能高度相关。

课程内容:2:数据揭示的关联性

上一节我们介绍了安全性能的思维转变,本节中我们来看看支持这一观点的三项关键研究。

以下是来自2019年的三项不同研究,它们从不同角度证实了这种关联:

  1. 2019年DORA《加速DevOps状态报告》:这项研究表明,可靠性、性能和安全性的度量指标倾向于同步移动。采用DevOps实践的团队在提升部署频率的同时,也降低了变更失败率并缩短了服务恢复时间。
  2. 2019年Veracode《软件安全状态报告》:该报告发现,代码扫描频率与漏洞修复速度存在强关联。每年仅扫描1-3次的团队,修复50%的漏洞需要近一年时间;而每天扫描(约300次/年)的团队,修复漏洞仅需数天。这不仅是测试更快,更与DevOps团队的整体快速迭代和修复能力相关。
  3. 2019年《软件供应链状态》研究:对Java Maven仓库的分析显示,保持安全的最佳方式是保持依赖项更新,而不仅仅是关注安全更新。频繁更新依赖项的项目更安全。核心公式是:频繁更新 → 更快纳入安全补丁 → 更安全的状态

我个人践行这一理念:在开始任何新功能开发前,先更新所有依赖项并修复由此产生的问题。定期这样做,破坏通常很小,同时我也不必专门担心安全问题。

课程内容:3:安全与SRE的重叠模式

基于以上学习和数据,我认识到安全性能与广义的技术性能存在大量重叠,并总结出三种模式:

  • 模式一:安全性能完全包含在通用性能之内。例如,系统维护活动(如配置管理、打补丁)既是核心SRE工作,也是最有效的安全控制措施。
  • 模式二:安全与通用性能部分重叠。两者共享某些核心能力,但各自也有独特领域。
  • 模式三:安全处理的是全新或未知领域。此时通用技术实践尚未覆盖,例如早期的软件供应链攻击。

本教程将主要探讨模式一和模式二,特别是它们如何应用于SRE。

一项2024年的元分析研究指出了最有效的安全控制措施,前两名是:

  1. 攻击面管理(即配置和资产管理)
  2. 补丁更新节奏(即软件更新和依赖管理)

这两者本质上都是系统维护活动,是SRE的核心职能。因此,提升安全最有效的方法之一就是做好SRE的日常工作。真正的挑战往往不是“不知道做什么”,而是“没有时间和资源去做维护”,因为功能开发通常优先级更高。

课程内容:4:核心重叠能力详解

上一节我们看到了安全与SRE在维护工作上的重叠,本节我们来深入探讨几个关键的重叠能力领域。

以下是四个核心的重叠能力领域:

  1. 可观测性:安全团队和SRE团队都需要洞察系统内部状态。SRE更关注性能与可用性指标,而安全团队更关注异常行为。但两者都基于相同的数据源和基础设施。市场趋势也显示,许多顶级安全信息与事件管理工具实际上是通用可观测性平台增加了安全功能。使用统一平台可以节省成本,并且通常能更好地扩展。
  2. 事件响应与事后调查:安全事件和系统故障事件在影响和频率上有所不同。安全事件通常影响更大(可达百亿级)、频率更低(数年一次);而系统故障更频繁、单次影响相对较小(百万级)。这导致响应模式不同:安全事件响应周期长,可能涉及深度取证和威胁狩猎;SRE事件响应要求快速恢复,事后复盘节奏更快。但两者都需要事件协调、日志分析和根因定位技能。双方合作能带来互补视角:安全团队擅长“大海捞针”找异常,SRE团队擅长快速协调资源。
  3. 测试:SRE和安全团队都关注“非快乐路径”。SRE思考“出错时怎么办”(错误、故障),安全团队思考“有人故意破坏时怎么办”(攻击)。测试方法广泛重叠,包括:静态代码分析、混沌工程、形式化方法、使用内存安全语言、参数化SQL查询,甚至记录系统设计假设。自动化测试对两者都至关重要。一个关键理念是:Bug就是Bug。无论是安全漏洞还是可靠性缺陷,快速发现并修复都能让系统更健壮、更安全。
  4. 安全水平目标:我在此提出 SLO 的概念。SRE使用服务水平目标作为承诺机制:当可用性低于目标时,承诺将资源从功能开发转向可靠性建设。SLO思路类似:预先确定可接受的安全风险水平,当风险超过此水平时,承诺将资源转向安全加固。挑战在于,安全很难用“每年允许一次入侵”来度量。因此,SLO应使用前瞻性、与成功相关的指标,例如:
    • 每个端点的漏洞数量
    • 对互联网开放的TCP端口数量
    • 未使用多因素认证的IAM比例
    • 孤儿账户比例
    • 登录失败/成功率

早期漏洞管理实践可视为一种原始的SLO:当漏洞报告过长时,基础设施负责人会决定暂停新功能开发以修复漏洞。这实际上就是SLO在起作用。

课程内容:5:实践建议与总结

基于以上讨论,以下是三个核心实践建议:

  1. SRE工作支撑核心安全性能:做好配置管理、资产清点、依赖更新和补丁安装等日常维护工作,是提升安全性的最有效手段之一。
  2. 扩展SRE能力以支持安全,反之亦然:在事件响应、风险管理和可观测性等方面,鼓励两个团队协作。他们独特的视角和技能可以互补。
  3. 安全与SRE协同并进:这两个领域拥有大量重叠且互补的技能组合。共同协作比各自为战能走得更远。

总结 🎯

本节课中我们一起学习了SRE与安全之间的紧密联系。我们通过数据看到,提升软件交付和可靠性性能会直接促进安全性能。两者在可观测性、事件响应、测试和维护等核心能力上高度重叠。通过采用SRE的实践,如定义SLO,并将安全视为整体技术性能的一部分,我们可以更有效、更主动地管理安全风险,最终构建出更可靠、更安全的系统。

013: 关于内存的常见误解

概述

在本节课中,我们将探讨程序员和系统管理员在内存管理方面常见的误解。内存是系统可靠性和性能的核心,但许多关于其工作原理的直觉可能是错误的。我们将从基础概念开始,逐步深入到Linux内核的内存管理策略、现代资源控制机制,以及如何正确监控和优化内存使用。

1:内存分配的真实情况

我的名字是Christown。我是一名Linux内核工程师。我主要在内核的内存管理子系统上工作,特别是资源控制和隔离方面。我也是systemd项目的维护者。我的工作重点是思考如何让Linux更可靠、更具可扩展性。

今天,我想谈谈程序员和系统管理员在内存方面常见的误解。内存是一个基础话题,但关于它的讨论常常充满情绪,例如关于交换空间的争论。在我与SRE和软件工程师合作的15年里,我看到了很多关于内存实际工作原理的误解。希望本次分享能帮助你更新一些概念,并获得一些优化生产系统的新思路。

让我们从一个基础例子开始。请看以下C代码:

char *ptr = malloc(1 * 1024 * 1024); // 分配1MB内存
strcpy(ptr, “hello”); // 写入字符串
printf(“%s\n”, ptr); // 打印
free(ptr); // 释放内存

这个程序看似简单:分配1MB内存,写入字符串,打印,然后释放。现在,请思考一个问题:malloc调用在何时真正分配了物理内存?malloc的契约到底是什么?

如果我告诉你,malloc这个函数名虽然暗示“内存分配”,但它实际上与物理内存分配毫无关系,你会怎么想?

那么,这一系列事件究竟是如何展开的呢?

2:Linux内存管理的目标

首先,为了确保我们在同一层面理解,我们需要了解Linux内存管理的高层目标和非目标。

以下是几个主要目标:

  1. 最大化系统资源利用率:我们希望尽可能利用所有可用内存。这不仅意味着让内存可访问,还要确保应用程序有合适的系统调用来有效使用它。理想情况下,系统内存应被持续使用,空闲空间通常会被磁盘缓冲区、文件系统缓存等填充。
  2. 确保内存访问安全:如果一个程序试图侵入另一个进程的内存且没有权限,我们必须阻止。这主要由CPU硬件支持,但内核需要根据CPU的异常报告采取行动。
  3. 自身资源高效:内存管理代码必须非常快。完美的内存跟踪如果导致系统变慢,就毫无意义。因此,内核代码中充满了近似计算和权衡,性能至关重要。
  4. 对应用程序透明:绝大多数内存管理活动不应涉及应用程序。效率优化工作需要在应用程序无感知的情况下透明地进行。这要求我们重新思考应用程序“看到”内存的方式。

上一节我们介绍了内存管理的基本目标,本节中我们来看看Linux为实现这些目标采用的一个关键策略。

3:超量承诺与按需分页

Linux为了最大化内存利用率,采用了一种称为超量承诺的策略。

超量承诺就像一张信用卡:每次你申请内存,系统都会答应你。事实上,系统甚至乐于给你比整个系统物理内存总量更多的内存。为什么会允许这样?

这与另外两个概念紧密相关:按需分页交换

  • 按需分页:我们不会预先分配物理内存,只有在应用程序实际使用某块内存时,才为其保留物理内存。
  • 交换:我们允许将一些已加载的内存卸载到更慢的存储设备(如磁盘)上。

这一切都基于一个事实:程序分配的内存数量,是预测其实际使用情况的非常糟糕的代理。事实上,程序分配的大量内存要么从未使用,要么极少使用。

将这些页面一直保留在物理内存中会非常浪费,因为系统无法将这些内存用于其他用途。因此,我们的目标是通过将申请内存的行为与实际物理分配解耦,来提高系统整体的内存利用率。

一种典型的反对观点认为,内存管理应该是应用程序开发者的责任,内核不应插手。然而,多年来我们经历的所有安全漏洞都证明,人类非常不擅长管理内存。因此,许多项目转向了像Rust这样对内存访问有高度抽象的语言,甚至是现代C++。当然,还有解释型语言,你对其内存完全无法控制。

超量承诺确实有其缺点。例如,如果所有程序都同时要求使用它们申请的所有内存,那么系统就会陷入困境。不过,这些问题可以在内核和用户空间层面得到缓解,我们稍后会讨论一些方法。

了解了内存分配的策略后,我们来看看Linux在底层是如何实现这些概念的。

4:虚拟内存与缺页异常

Linux在底层通过虚拟内存来实现上述机制。

虚拟内存是内核和CPU共同实现的一种抽象层。例如,系统可能“虚拟地”给了应用程序一些内存,但从未为其保留物理内存。每个进程在Linux上都有自己的虚拟地址空间。在这个空间内,程序看到的内存就像普通内存一样,但它并不真正知道背后是什么,甚至可能根本没有物理内存支持。

一个虚拟内存地址不一定由RAM支持,它同样可以由交换空间支持。当访问时,我们需要将其提升到主内存。它也可能根本没有映射,就像按需分页的情况,我们需要逐步将页面调入内存。

当然,这并不能阻止你在自己程序内访问有效但错误的内存区域。我们只能阻止你干扰其他程序,或者对内存进行无权限的操作。

我们将超量承诺和按需分页这两个概念混合在了一起。按需分页就是我们之前讨论的:直到被“需求”时,才实际分配物理内存。这种需求通常以缺页异常的形式出现。

缺页异常这个名字听起来很吓人,它实际上是CPU发来的一个消息:“有人试图访问一块内存,但我完全不知道他们在说什么。” 这意味着CPU没有从该虚拟页面到任何物理RAM的映射。因此,内核要么需要分配一个新的物理页帧并建立映射,要么判定这是一个无效地址访问。

以下是典型的工作流程:

  1. 用户空间应用程序通过库函数(如malloc)请求内存。这些函数内部可能使用sbrkmmap等系统调用来增加虚拟内存空间。关键点:这些操作只增加虚拟内存空间,完全不涉及物理RAM
  2. 稍后,当进程实际去使用这块内存(解引用虚拟地址)时,CPU因无映射而产生缺页异常。
  3. Linux内核发现这块内存尚未放入主存,于是分配一个新的物理页帧,并建立正确的映射。
  4. 如果需要从后备设备(如交换空间)加载数据,内核会先将数据复制到内存,然后恢复程序执行。整个过程对程序是透明的

这里需要记住的关键点是:像malloc这样的函数与实际分配内存是脱节的。程序调用malloc时并没有分配真实内存,它们只是获得了将来可以使用的选项。这其中的间接交互非常多,因此在推理应用程序的内存语义和生命周期时必须非常小心。

既然malloc可能“说谎”,那么free呢?它总该释放内存了吧?

5:释放内存的真相

Chris,你告诉我调用malloc时它在说谎,它说给了我内存但实际上还没给。但至少当我调用free时,它总该把内存释放掉吧?他们不会对我说两次谎,对吧?

当然会。他们是Linux内核开发者,当然会。

请看这个简单程序:它分配1MB,触及每个页面,然后释放,最后等待。如果你运行这个程序并在free之后检查其驻留集大小,你期望它会降到0,对吗?很可能不会。实际上,更可能发生以下三种情况之一,每一种都令人困惑:

以下是可能发生的情况:

  1. 毫无变化:RSS保持1MB。这是因为分配器只是将你的内存放到了某个空闲列表上,没有归还给操作系统。像Google的TCMalloc这样的分配器通常会这样做,以避免昂贵的系统调用。
  2. 部分下降:RSS下降了一些,但不是全部。分配器比较慷慨,归还了部分页面给OS,但保留了一些,心想“也许你以后还会用到”。这在Glibc的mallocjemalloc中更常见。
  3. 不降反增(我最喜欢的情况):释放内存后,RSS反而增加了。这是因为free操作本身可能需要分配内部元数据,或者分配器为了重组其内存池(例如对抗内存碎片)而触及了更多页面或页表。

关键点free是给你使用的内存分配器的一个信号,而不是给操作系统的信号。它的意思是“我不再需要这个了”,而不是“请立即归还”。分配器决定如何处理它。这就像在聚会上说“我要走了”,然后挥手告别。你可能真走了,也可能三小时后被发现还在后面玩。你的声明对最终结果可能毫无影响。

这就是为什么在生产环境中监控内存泄漏如此棘手和繁琐。一个可能释放了所有内存的进程,实际上可能并没有释放内存。现代分配器为此做了许多不同的权衡。这也是为什么在现代系统上测量RSS相当棘手的原因之一。

我们讨论了内存分配和释放的复杂性,但内存访问本身也存在巨大误解,这源于它的名字。

6:随机存取内存并不“随机”

我看到观众席中有我不喜欢的人,一位会议组织者Dan。Dan,你能帮我个忙,读一下这张幻灯片吗?

“随机存取存储器是一种电子计算机存储器,可以以任何顺序读取和更改,通常用于存储工作数据和机器代码。随机存取存储器设备允许数据项的读取或写入时间几乎相同,而与数据在存储器内的物理位置无关。”

我不敢相信你会对在场的女士们和先生们撒谎,在这种场合说这样的假话。你真应该感到羞愧,Dan。

让我们来看一个大型二维数组的两种遍历方式。

// 列优先遍历 (慢)
for (int j = 0; j < COLS; j++) {
    for (int i = 0; i < ROWS; i++) {
        access(array[i][j]);
    }
}

// 行优先遍历 (快)
for (int i = 0; i < ROWS; i++) {
    for (int j = 0; j < COLS; j++) {
        access(array[i][j]);
    }
}

你可能会想,我最终都要访问这个二维数组中的所有元素,对吧?那么为什么第二种方式快了50倍

因为尽管我们尊敬的同事和维基百科那么说,随机存取存储器实际上非常不擅长“随机”访问内存

原因之一是DRAM是按行组织的,其操作由CAS预充电周期控制。

  • RAS:行地址选通,告诉RAM激活哪一行。
  • CAS:列地址选通,在已激活的行中选择哪一列。
  • 预充电:准备切换到一个新行。

关键点在于:你可以在一个激活的行上执行多次列操作,然后才需要下一次预充电。在列优先遍历中,每次访问都需要预充电、激活新行、选择列。在行优先遍历中,我们连续多次访问同一行,只需要一次RAS,然后是一连串CAS操作。RAS和预充电操作相比CAS操作极其昂贵。

行优先访问也更利于缓存利用。缓存行是CPU缓存和内存之间数据传输的最小单位(x86-64上通常是64字节)。我们以连续对齐的块读取它。如果随机读取,我们每次都可能要读入一整条缓存线然后丢弃。而顺序读取则可以持续利用同一条缓存线。

顺序读取更快的另一个原因是虚拟内存被划分为固定大小的页(x86上通常是4KB)。转译后备缓冲器是虚拟地址到物理地址的缓存,通常容量有限。如果我们在地址空间中跳跃寻找页面,就需要不断换入换出TLB条目。顺序访问则不需要。

大多数现代编译器还会自动对第二种版本进行向量化,使用SIMD指令。但需要明白,性能提升的大部分原因并非SIMD,而是RAS/CAS和预充电。

你可能注意到我没提预取。预取是CPU自动检测这种顺序访问并提前加载数据。在这个特定例子中,它帮助不大。本幻灯片的主要目的是提醒你,在编写代码时,请记住:尽管名字叫“随机存取内存”,但无论是RAM还是CPU,实际上都非常不擅长真正的随机访问。

理解了内存访问的模式后,我们还需要了解Linux如何看待不同类型的内存。

7:内存类型与RSS的局限性

理解Linux内存管理的另一个关键是,Linux从语义上区分不同类型的内存。

例如:

  • 匿名内存:顾名思义,没有后备存储。这是程序运行期间通过malloc等分配的内存。
  • 缓存和缓冲区:现在它们统一为统一页面缓存。但如果你问大多数Linux管理员,他们会说页面缓存/缓冲区是可回收的,可以随时释放。

问题在于,“可回收”并不意味着“可以立即释放”。可回收意味着如果你真的非常需要,并且在某些条件下,也许回收会被允许。但这不意味着“哦,上帝,发生了坏事,我现在就允许你回收”。它不在乎你的紧急需求。

例如,如果某个应用程序正在频繁读写某个文件,我们不太可能直接丢弃那个文件的缓存。因此,虽然在某些情况下它可以被轻易释放,但并非总是如此。这导致人们不可避免地会问:“为什么我的应用程序内存不足?我明明有大量的缓冲区和缓存。” 很可能那些缓存是必需的。

缓存可能是必需的,这也是为什么驻留集大小这个人们喜爱且无处不在的指标是不靠谱的

RSS将大量注意力集中在少数几种内存类型上(匿名内存和映射文件内存),但我们忘记了,许多工作负载离开大量的缓冲区和缓存根本无法运行。

我们测量RSS是因为它容易测量,而不是因为它是一个好的度量标准。

因此,当有人问你“你的应用程序实际使用了多少内存”时,除非你做过压缩测试直到性能下降,否则很难给出准确答案。在Facebook的一个案例中,一个团队多年来一直认为他们的服务在每个机器上的内存占用是100-150MB,但通过我们将在本课讨论的指标,他们发现实际占用接近2GB。这是一个巨大的认知差距。

这也是为什么在现代资源控制中,我们通常一起限制所有类型的内存,而不是只限制匿名内存。因为如果只限制匿名内存而忽略页面缓存,应用程序仍然可以轻易地突破限制。

既然RSS有局限性,那么现代系统是如何进行资源控制的呢?

8:Cgroups与内存保护

现代内存管理的一个关键构建块是Cgroups

Cgroups是一种内核机制,用于平衡和控制机器上共享的资源,如内存、CPU、IO等。它本质上是由用户定义的一组进程,并施加一组资源限制。通常,你会为某个服务(如一个守护进程)创建一个cgroup。

如果你操作过容器,你可能已经接触过它们。所有现代容器运行时都使用cgroups。这是因为cgroups解决了传统Unix内存管理(如ulimit)长期存在的许多问题和限制。

Cgroups已经存在14年了,变化很大。最值得注意的是,大约10年前(4.5内核),我们发布了cgroup v2。在cgroup v2中,我们一起限制所有内存memory.max文件不仅限制RSS,而是限制包括缓存、缓冲区、套接字内存、为应用程序分配的内核内存等在内的所有内容。这与过去每个进程限制或cgroup v1只限制部分内存类型相比,是一个重大变化。

表面上看,这应该工作得很好,也确实如此。它设置了cgroup允许消耗的最大内存量。但问题是,如何用它来构建一个可靠的系统?

假设你有几个切片。在systemd术语中,切片是层次结构中某类应用程序的子部分。例如,在best-effort.slice中,你会放置尽力而为的服务;在workload.slice中,放置你真正想在机器上运行的应用。

尽力而为的应用程序可能是配置管理或指标收集。它们很好,但如果在机器完全过载时不运行,也可以接受。而工作负载则包含你真正需要在这台机器上运行的应用,例如Web服务器上的HHVM或Nginx,数据库服务器上的MySQL。如果不运行这些,这台机器就失去了意义。

一个典型的、负责任的系统管理员可能会这样想:我担心某个尽力而为的应用发生内存泄漏,可能会拖垮整个机器。这很糟糕。所以我最好在best-effort.slice上设置内存限制。但里面有些东西其实挺重要的,我得确保它不会影响其他部分……如此循环,直到没人能说清这台机器应该如何工作为止。

不可避免地,其中一个的内存使用量会增长一点,然后你就必须思考如何进行资源分配。如果你在像Facebook或Google这样的公司工作,有成千上万的服务和团队,情况会无限恶化。

我们的最终目标只是保持workload.slice中的东西运行。如果我们能直接编码这个目标呢?比如这样:

workload.slice: memory.low = 20G

memory.low是过去50年Unix内存控制方式的一个根本性改变。其理念不是试图通过给每个应用套上紧身衣来控制内存,而是声明我们的主要工作负载需要多少内存才能运行,然后让系统去解决其余问题

那么这是如何工作的呢?这一切都基于回收。回收是尝试释放一些页面的过程。memory.low会挂钩到内核的回收基础设施,并执行所谓的保护。例如,如果你将memory.low设置为20G,那么只要你使用的内存低于20G,我们通常会豁免你被回收内存。这意味着我们倾向于从其他所有人那里回收内存。只有出现全局性严重短缺时,我们才会动用你的“储备”。

这听起来简单,但实现起来相当复杂。例如,当系统上多个保护相互竞争时,我们必须决定如何处理。我们还需要处理从父cgroup到子cgroup的保护分配,特别是当子cgroup相互竞争时。

使用这种内存保护方式的一个真正好处是,它是工作守恒的,或者至少更接近工作守恒。因为它不是预留。只要你不影响他人,你可以使用任意多的内存。只有当受保护的应用程序需要使用它时,我们才会强制执行。这对临时性的内存峰值和内存构成变化更加宽容。

我们现在主要依靠memory.lowmemory.min来进行内存保护。限制仍然有适用场景,但通常更有限。

这种保护机制自然地将我们引向了另一个相关话题:交换空间。

9:交换空间的真正作用

几年前,我写了一篇名为《In Defence of Swap》的博客文章。我写这篇文章是因为我们刚刚经历了一次新闻推送服务中断,我调查后发现,如果当时启用了交换空间,这次中断很可能不会发生

如果你没读过这篇博客,我强烈推荐。很多人认为交换空间在如今内存充足的时代已经无关紧要。这种想法很奇怪,因为交换和内存并不是完全可互换的概念。对于交换擅长的事情,你很难通过其他方式获得;而对于它不擅长的事情,你通常可以缓解。

这些讨论通常围绕着对交换空间用途的误解。交换空间几乎与“紧急内存”或“备用内存”或“更慢的内存”这些概念毫无关系

你也会在网上看到有人说:“我不使用交换空间,因为那样内存就会变成磁盘IO。”但这显然没有道理。无论你是否使用交换空间,内存都可能变成磁盘IO。因为如果我们不能换出匿名页面,我们就会去驱逐文件缓存。现在你必须从磁盘获取文件缓存数据。这甚至更糟,因为可供选择的页面池更小了。

这些误解严重损害了交换空间的声誉,并导致了“如果你有很多可用内存,交换空间就没用”的错误观念。

那么,如果它不是扩展RAM的机制,它是什么?

  1. 允许回收本应锁定的内存类型:它为原本会锁定在RAM中的内存(匿名内存)提供了后备存储。
  2. 允许更渐进地增加内存争用:如果没有交换空间,内存耗尽会变得非常快速和二元化。有了交换空间,争用可以更平缓地上升。
  3. 保持适度的内存压力以促进高效利用:就像我们编译时常用make -j $(nproc+1)一样,我们希望在内存上也保持一点正压力,以确保我们使用了所有可用资源,同时又不影响系统延迟太多。如果没有交换空间,这几乎不可能做到,因为内存争用的增加会极其迅速且难以判断。

交换空间确实有其权衡,我在文章中详细讨论了。但总的来说,在系统中启用它是一个非常积极的事情。

我们讨论了如何控制内存,但首先我们需要知道如何正确测量它。你如何查看Linux进程的内存使用情况?

10:超越RSS:正确测量内存使用

谁想告诉我如何在Linux中查看进程的内存使用情况?有人说top,我爱你。top之类的工具往往只测量一种类型的内存。它们有不同的列显示不同内容,但呈现给你的主要数字通常是RSS或PSS。

关键点:它们不知道你可能正在使用的任何缓存。你可能会想,缓存只是可选的,对吧?问题在于,对于任何足够复杂的应用程序,答案几乎总是“不”。

以Chrome为例。Chrome的二进制代码段超过130MB。我们将其代码加载到内存中,是逐步进行的,作为页面缓存的一部分。但是,如果我们想执行某部分代码,那块缓存就不是我可以随时回收的。我需要它在内存中,否则程序无法运行。

对于程序执行期间显式加载的文件的缓存也是如此。最终,这些页面必须进入主内存才能被访问。

在cgroup v2中,我们有一个名为memory.current的文件,它测量cgroup的当前内存使用情况,包括缓存、缓冲区等所有内容。那么问题解决了,对吧?

当我告诉人们不要测量RSS时,一个常见的反应是:“好吧,那我测量memory.current。”memory.current顾名思义,显示cgroup当前使用的所有内存。但了解这意味着什么以及区别在哪里非常重要。

我们不再只谈论RSS,而是包括该应用程序使用的所有类型的内存,如缓存、缓冲区、套接字内存等。所有这些都在memory.current中。这样做是对的,因为如果不这样,我们就无法防止滥用系统资源。但同时,它也带来了一个非常不同的推理范式。

理解为什么推理memory.current比看起来更复杂,需要回到为什么我们行业最初倾向于使用RSS作为度量标准。

人们选择RSS作为指标,是因为它相当稳定,通常相对静态,不会大幅波动,很容易为其设定一个限制,并且晚上看着这个指标会让你感觉良好,即使它实际上什么也没做。

memory.current则恰恰相反,它告诉你真相,但人们完全不知道如何处理真相。例如,如果你设置了一个8GB的内存限制,而你的应用程序在夜间运行,系统没有内存压力。memory.current的值会是多少?它也会是8GB。为什么?因为我们已经用各种好东西填满了它——缓冲区、缓存等等。这并不是说你需要8GB,而是我们给了你8GB,因为没人向我们要。那么为什么不给你呢?

所以,如果没有压力迫使其收缩,我们如何判断在任何给定时间真正需要的内存量是多少?

11:使用PSI与Semaphore寻找内存工作集

让我们看一个内核编译的例子。在没有限制的情况下,这个内核编译的峰值memory.current超过800MB。在cgroup v2中,我们有一个可调参数叫memory.high,它会从cgroup回收内存,直到其回落到某个阈值以下。现在,没有限制时,构建大约需要4分钟,占用约800MB。当我们应用600MB限制时,它花费的时间几乎完全相同,但可用内存减少了25%。即使设置为400MB,也只用了一半内存,总时间只增加了3秒,这是很好的权衡。但如果我降到300MB,构建永远无法完成。

我们知道这个过程需要300MB到400MB之间的内存才能以合理的性能运行。但找到这个确切的临界点是一个非常繁琐的试错过程。而且这种试错只适用于像本例这样高度均匀的工作,不适用于像Web服务器这样的场景。因此,要获得准确的数字,我们需要不同的方法。

Semaphore是一个自包含工具,它使用cgroup v2的压力失速信息系统来找出你的应用程序自然需要多少内存。

PSI是我们为cgroup v2开发的新技术之一,用于确定特定资源是否被过度使用。以前内核中从未有过这样的指标。我们有很多相关指标,如内存使用量、页面缓存/缓冲区使用量、页面扫描率等。但即使有这些指标,也很难区分系统的有效使用和过度使用。

在PSI中,我们使用这些指标来测量系统上线程因内存相关工作而停滞的时间比例。例如,pressure=0.16意味着在某个时间窗口内,有0.16%的时间我在做一些如果我有更多内存就不必做的工作

这可能包括等待内存锁、被限制、等待回收完成,甚至是等待一些与内存相关的IO,如将页面内容换入页面缓存或从交换空间换入。本质上,它是在说:“如果我有更多这种资源,我可以运行得快0.16%。”

使用PSI和memory.high,Semaphore对cgroup施加刚好足够的内存压力,以驱逐对工作负载性能不必要的冷内存页面。它基本上是一个积分控制器,动态适应峰值和低谷。例如,Web服务器请求增多时,我们扩展;请求减少时,我们收缩。

它提供的是应用程序随时间变化的内存工作集概况。因此,它可以用来回答“这个东西实际需要多少内存来运行?”这个问题。

在本例中,我们发现答案大约是340MB。为了得到这个结果,我们不断压低应用程序的内存限制,直到开始看到问题或压力,然后回退到刚好足够的程度。

为什么有人想这样做?我们为什么不直接让应用程序自由运行?正如我之前提到的,在可观测性方面,我们曾有一个团队多年来认为他们只用了100-150MB内存,正是通过这种方法,他们意识到实际上每个机器需要2GB。我们用这种方法发现了RSS测量无法发现的回归和泄漏。

我们还用它来分摊内存短缺并提前应对。当一台机器已经严重争用时,快速释放内存可能很困难,因为资源已经非常稀缺。我们可以通过在情况还好的时候就采取行动,来避免这些情况。

这些功能的结合意味着Semaphore是我们进行工作负载堆叠的关键部分,因为它不仅准确反映了工作负载当前的需求,还允许我们随时间监控并调整堆叠限制。

总结

本节课我们一起学习了关于内存管理的多个核心概念和常见误解。

我们从一个简单的malloc/free例子开始,揭示了内存分配与物理RAM分配的脱节,以及free并不立即向OS归还内存的真相。我们探讨了Linux内存管理的目标,包括最大化利用率、安全性、高效性和对应用的透明性。

我们深入了解了超量承诺按需分页策略,以及虚拟内存缺页异常如何协同工作。我们纠正了关于“随机存取内存”的误解,强调了顺序访问的重要性。

我们指出了RSS指标的局限性,它忽略了缓存等关键内存类型。接着,我们介绍了现代资源控制的核心——Cgroups,特别是cgroup v2如何统一限制所有内存类型,以及memory.low保护机制如何提供更灵活、工作守恒的内存保障。

我们为交换空间正名,解释了它作为匿名内存后备存储、平缓内存争用的重要作用,而非仅仅是“慢速内存”。最后,我们探讨了如何正确测量内存,引入了PSI指标和Semaphore工具,它们能帮助我们找到应用程序真实的内存工作集,实现更精准的资源管理和堆叠。

希望本课程为你提供了理解和推理生产环境中服务内存复杂性的工具与技术。设计、调试和优化时,请记住这些概念。

014:关于SRE值班的“九个糟糕”与改进之路

在本节课中,我们将一起探讨SRE(站点可靠性工程)领域一个普遍存在但常被忽视的痛点:值班(On-Call)。我们将基于一项广泛的行业调查和研究,揭示当前值班实践中普遍存在的九个核心问题,并探讨如何通过团队协作和流程改进来应对这些挑战,最终目标是让值班工作变得可持续且不那么令人痛苦。

研究背景与方法

上一节我们概述了值班带来的普遍困扰,本节中我们来看看支撑本次讨论的研究基础。为了获得行业层面的洞察,而非仅基于个人经验,我们进行了一项综合研究。

以下是研究采用的方法:

  • 文献综述:我们回顾了大量学术论文和行业文章,以理解现有的理论和最佳实践。
  • 问卷调查:我们设计并发布了一份包含65个问题的详细调查,旨在收集一线工程师的真实体验和感受。

本次调查的受访者背景具有代表性:平均拥有18年行业经验,在当前公司工作约4年,通常身处500人以上的中大型组织,其值班轮换团队规模约为5-10人。这为我们提供了讨论行业现状的可靠背景。

核心发现:“九个糟糕” (The No-Good 9)

基于研究数据,我们总结出当前值班体系普遍存在的九个主要问题。我们将其称为“九个糟糕”,以便记忆和讨论。接下来,我们将逐一审视其中几个关键问题。

1. 入职与培训缺失

在调查涉及的所有值班相关话题中,入职与培训是满意度最低的一项,平均评分仅为2.35星(满分4星)。高达34%的受访者认为其培训比标准水平更差。

这意味着,许多工程师在未接受充分培训的情况下就被推上了值班岗位。他们可能不了解系统、不知道如何处理警报、不清楚升级路径。正如一位受访者尖锐指出的:“我们从不培训新团队成员,我们只是失去他们。” 这种缺失直接导致了值班时的无助感和压力。

2. 仅限被动改进

大多数组织的值班改进策略是纯粹被动反应式的。问题出现了,才去修复它。缺乏前瞻性的规划、度量和投资。值班成了运维的“杂物抽屉”——所有不知道该如何处理的事情都被扔了进来。

这种模式无法持续,因为系统熵增是自然规律,如果只被动响应而不主动优化,值班负担只会越来越重。少数表现较好的团队有一个共同点:他们会定期(每周或每月)召开会议,专门讨论值班相关问题和改进措施。

3. 缺乏自主权与清晰流程

工程师常常感到没有自主权去解决他们值班时遇到的问题。20%的受访者表示他们不能或不愿更新警报规则。

造成这种情况的原因可能包括:没有操作权限、只能将问题升级给他人、利益相关者阻挠变更等。此外,流程本身也常常缺失或过于复杂。34%的团队甚至不监控其值班工具和实践是否有效。关于服务等级协议(SLA)、升级路径、部署跟踪的信息也常常模糊不清,这加剧了值班时的混乱和焦虑。

4. 交接流程不受支持

值班交接是传递上下文、确保连续性的关键环节,但往往被忽视。28%的人表示他们从不指望能得到交接,11%的人只是有时能得到。所谓的“交接”有时只是一句“给你,晚安”。

研究表明,交接期间信息的复杂性和不确定性会影响工程师对系统当前状态的信心。有效的、有互动的口头交接远比单向阅读交接清单更有价值。

5. 值班安排与焦虑

值班安排本身也是一个压力源。虽然排班频率(2.85星)和日程管理(2.83星)的评分不算最低,但53%的受访者表示他们有时或总是对即将到来的值班感到焦虑。

这种焦虑在缺乏培训、没有自主权、流程混乱的背景下是完全可以理解的。灵活的排班、对夜间被叫醒次数的关注,以及为生活事件留出调整空间,都能有效缓解这部分压力。

6. 责任过载与隐形工作

这是最核心的问题之一:值班工程师通常责任过载。他们不仅在值班,还在同时进行多项其他工作。

以下是值班期间工程师常需并行处理的其他任务:

  • 处理非值班触发的故障(75%)
  • 参加会议(75%)
  • 处理内外部支持请求
  • 帮助团队成员
  • 修复缺陷、进行功能开发等“实际工作”

当工作超载时,人体会自然进入“效率与彻底性权衡”模式。调查显示,工程师们最常见的应对策略是:

  • 砍掉低优先级任务(通常是那些有趣或主动性的工作)
  • 向经理或团队寻求支持(将负担分摊给团队)
  • 推迟截止日期

这引出了一个关键矛盾:值班工作至关重要且耗费大量精力(每年可能占用6-8周),但它却常常不被视为“实际工作”,在绩效评估和晋升中得不到认可。这形成了一个“值班厄运循环”:业务压力 > 值班健康被忽视 > 工作超载 > 倦怠与失误 > 压力持续 > 人员流失。

影响:团队在弥补组织的不足

上述“九个糟糕”的后果是严重的。74%的受访工程师表示,他们曾因值班责任经历过工作超载、职业倦怠或两者兼有。

然而,在一片黯淡中,我们发现了一线光明:团队协作。尽管组织层面支持不足,但团队内部的支持度很高。队友支持评分达3.33星,经理支持评分达3.15星。83%的人可以信赖队友帮助排查问题,87%的人认为经理会提供帮助。

这揭示了一个深刻的事实:是团队中的人在共同努力,以弥补组织在值班体系上的缺陷。 他们相互支持、分摊负担,才使系统得以运转。这既是感人的,也说明了问题根源在于系统层面,而非个人。

行动建议:从“适当实践”开始

既然我们指出了问题,并看到了团队的努力,那么该如何改进呢?我们提倡关注“适当实践”而非空泛的“最佳实践”,并从小而可持续的习惯入手,而非追求颠覆式变革。

以下是针对部分“糟糕”问题的具体建议:

针对复杂/缺失的流程:

  • 建立和维护值班手册(Runbooks)。
  • 实施指标度量,跟踪值班负载、唤醒次数等。
  • 完善发布工程实践,例如避免在无人支持时部署。
  • 明确并沟通升级策略。
  • 投资可观测性工具。
  • 建立风险管理和事后复盘文化,不仅针对故障,也针对值班周期。

针对培训与入职缺失:

  • 将培训正式化并公开进行。
  • 提供正式和非正式的学习机会,如影子跟随(Shadowing)。
  • 培训内容不仅包括技术知识,还应涵盖协调协作流程:如何升级?遇到故障时该如何沟通?

针对值班安排:

  • 尽量保持排班灵活性。
  • 提前规划,避开组织重大活动期。
  • 监控并设法减少夜间唤醒次数。
  • 体谅并照顾团队成员的个人生活安排。

针对交接流程:

  • 鼓励并规范交接。最有效的是有互动的口头交接。
  • 让接棒者主导交接对话,提出他们关心的问题。
  • 确保交接是双向对话,而非单向汇报。

一个核心原则: 警惕并清理“创可贴式修复”。在凌晨3点为了快速解决问题而采取的临时方案,不应成为系统的长期依赖。必须为工程师留出时间,将这些临时修复转化为稳健的解决方案。

总结与未来行动

本节课中,我们一起深入探讨了SRE值班实践中普遍存在的系统性困境——“九个糟糕”,包括培训缺失、被动改进、缺乏自主权、交接不力、责任过载等。我们看到,这些问题导致了广泛的工作超载和职业倦怠,而一线团队正在用自己的协作默默弥补组织的不足。

改变这一现状需要行业共同努力。我们不应满足于现状,而应追求一种更好的状态。为此,我们倡议建立更广泛的行业对话,分享“适当实践”,持续测量值班的影响,并共同探索如何构建更健康、可持续的值班文化。

改变始于认知,进步源于行动。希望本次讨论能为你和你的团队提供一个起点,开始审视并改善你们的值班体验。


注:本教程内容整理自SREcon Americas 2025大会演讲《“On-Call Is Ruining My Life” and Other Tales about Holding the Pager as an SRE》,旨在提炼核心观点,以更易理解的方式呈现给初学者。所有研究发现和引述均基于原演讲者及其团队进行的调查与研究。

016:SRE大会-2025-美洲-|-SREcon-|-分布式-|-缓存-|-OpenTelemetry-|-安全-|-AIOps-p16-P16-The-Perverse-Incentives-of-Reliability--BV1TmLDz7EZZ_p16-

在本节课中,我们将要学习关于SRE(站点可靠性工程)领域中的一个核心挑战:扭曲的可靠性激励。我们将探讨为什么传统的“避免停机”目标可能是一个陷阱,以及如何通过调整策略和激励来更有效地工作。课程内容基于一次SRE大会的演讲,旨在帮助初学者理解复杂环境下的可靠性工作。

课程概述:扭曲的可靠性激励

当前,世界变得前所未有的复杂。人工智能的兴起带来了混乱。SRE团队通常资金不足、工作过度,并且在一个“用更少资源做更多事”的世界里,我们作为成本中心,很难维持已取得的可靠性成果。低问责制和高指责氛围越来越普遍,甚至可能产生一种“无法做得更好”的无力感。

章节1:问题的核心——无人关心的正常运行时间

我从未对一次会议演讲如此兴奋。我花了很大精力准备这次分享。

首先,快速举手示意一下。有多少人认为,归根结底,SRE的工作就是不惜一切代价避免停机?这是一个陷阱。我知道你们可能这么想,但你们也都知道这是个陷阱。你们太聪明了。

我是凯蒂·怀尔德。我是那种无法阻止自己冲向火场的人。我有时觉得我们成为SRE是因为这份工作比纵火犯收入更高。

这里有一个小免责声明:本演讲内容与任何在世或已故的人物或实际事件的相似之处纯属巧合。

我是一个“动词”型的人。我的动词是“冲入火场”。事实上,我学习过宏观经济学,现在我对一切事物都如此复杂和混乱感到非常兴奋。

好了。你们可能都认出了我们最喜爱的SRE正能量视频的开场。如果不认识,这里有一个链接。但当前的宏观经济背景是,世界比以往任何时候都更复杂。人工智能的“硬度”正在引发混乱。没人在乎SRE。我们像往常一样资金不足。我们过度工作,这是我们自己造成的,但我们停不下来。我们试图用更少的资源做更多的事。很难维持我们已经取得的可靠性成果。我们非常担心情况会变得更糟。我们可能是对的,因为我们通常都是对的。低问责制、高指责的氛围变得越来越普遍。这是你们告诉我的,我也看到了。还有一种冷漠感,一种“也许不可能做得更好”的感觉。这很艰难,因为在一个资源稀缺、追求“用更少资源做更多事”的世界里,我们是一个成本中心。在一个也痴迷于尽可能快地添加生成式人工智能的世界里,作为成本中心很难。那么,我们该怎么办?

章节2:扭曲的激励与“旁观者效应”

所以,在我想到“伦纳德·伯恩”这个标题之前,我演讲的标题谈到了我们拥有的激励、支配我们所在组织的激励,以及它们为何不好(技术术语是“扭曲的”)。为什么会这样?问题是,在停机发生之前,没人在乎正常运行时间。没人会说:“干得好,SRE,系统正常运行,做得好。”也许你的团队会,但……你们在招人吗?他们只有在没有正常运行时间时才会注意到、才会在乎,然后他们会生气,并且这是你的错,因为你搞砸了。

你所做工作的后果是严重延迟的。你正在做的事情,将在未来一段时间内逐步提高基线可靠性。未来还没到来,因为时间就是这样运作的。这对你来说很困难。应用程序开发人员(App Devs)有强烈的动机去避免这整个戏剧性场面。我们稍后会讨论原因。另一件事是,SRE们大多在事情真正搞砸时才被看到。这是巧合吗?如果你知道如何向经理解释虚假相关性,请来和我谈谈。我不知道怎么做。是的,所以你只在情况非常糟糕时看到我们。所以,是的,这不太好。

所以,不仅仅是埃隆·马斯克,我们在整个行业的董事会和Zoom会议室里都能看到这张幻灯片。

我认为最令人担忧的部分是,关于这是否是个好主意的共识仍然存在分歧。很多人没有把这些事情联系起来。我不知道为什么,但这就是我们生活的世界的事实。所以,这就是我们的处境。这就是长期运作的方式。凯恩斯有句名言:“长期来看,我们都死了。”他绝对可能是在谈论SRE。事实上,我们长期都会死,他只是不在乎。他会说:“但我会死,所以我反正也不会在乎。”另一件事是关于应用程序开发人员。为什么你们不做得更好?为什么不来帮我?生产环境宕机了,你的小部件不工作了,而他们只是“啦啦啦”地走开。有人会说:“哦,我看到我的文本写着‘别人会行动’,别人会……我们就假装我没说过。”我的意思是,我们知道如果情况真的很糟,值班的SRE会被呼叫,对吧?这通常是真的,事情就是这样运作的。因此,大多数理性的人会非常努力地远离任何类型的事件。这在技术上被称为 “旁观者效应” ,这是对人类本性的一个基本发现。只要人数超过三个左右,它就会适用。就像“你要帮他吗?也许应该有人帮他?”这就是人类的工作方式。

所以,这些不帮助你的应用程序开发人员,他们只是对自身激励做出反应的理性人类。他们的激励是逃离火场。他们只是在这么做。

章节3:SRE的困境——为何我们与众不同

除了你。谁把你弄成这样了?是的。因为你不是旁观者。我的意思是,你在这里,在这个房间里。你在听我讲,你在听伍兹博士讲,因为你就是不能眼睁睁看着一切烧毁。你会说:“我们必须做点什么。”也许,也许你最终自己做了很多,你知道这有毒,但你忍不住要拯救这一天,毕竟,我们了解这些应用程序。

那么,我们该怎么办?

我曾经历过,我有那件T恤。非常字面意义上的。他们给我寄了一件T恤,以纪念我未能创建一个可扩展的运维团队。就像在说:“干得好,你仍然自己在处理事件。”我心想:“靠。我失败得太惨了。我需要做点什么。”好吧,那么“如何不成为我”可以作为本次演讲的另一个标题。我会告诉你几个我做过的事情,它们主要是浪费了我自己的大量时间和精力。

章节4:传统方法的陷阱——名词思维与安全网

首先,我们应该做点什么。系统地,我们不应该接听每一个告警页面。凯蒂,我们应该建立一个能建立系统的系统,等等,等等。当然,我们从哪里开始呢?我们衡量变化,对吧?我们需要了解我们各项举措的投资回报率。所以我们需要知道我们所有的指标是什么,我们需要某种非常强大的测量工具,因为我们希望这是准确的。然后我们就去做,然后我们就能够确定优先级,比如该做什么。好吧,兔子好像在问:“你在下面还好吗?”因为问题是:你不需要确切知道你的平均修复时间(MTTR)是多少,就能知道它很糟糕。我们可以把它标为红色。通常,我们大致知道什么是不好的。这同时也是你作为一个“名词”型人的表现,我们知道这不好。我们稍后会读论文,但我们暂且相信这是不好的。好的,好的,所以我明白了。我陷入了分析瘫痪。让我,让我行动起来。有很多故障,其中大多数是由变更引起的。我们知道这一点。这对所有事情来说都是微不足道的真理。所以,太好了。我将防止故障影响到生产环境。

我的意思是,好吧,兔子戴上了安全帽。这很好。防止故障影响到生产环境并不是一件坏事。而这就是问题所在。你们都是非常聪明的人,不会做明显愚蠢的事情。你们做的是应该做的好事。但你们只是不应该去做。这就是你们的问题。因为你可以花费大量的时间和精力来制作你的巨型安全网,而应用程序开发人员就像在走钢丝(他们自己并不知道),而你却在制作安全网。你设置了“绿色即推送”,你拥有所有的自动化,你在预生产环境运行合成流量,你设置了邪恶的预演环境。你做得很好。这很可爱。但问题在于,你没有足够的人手。这种“名词”型SRE方法的问题是,你将需要一个至少三倍于当前规模的SRE团队。我知道你尝试过,因为我是你,我也尝试过。你告诉过领导层和其他所有人,他们应该关心。就像“我们应该做这个,你应该关心,因为我关心。”很好,他们可能不在乎。此外,你还做了很多幻灯片,说明SRE是多么重要,如果我们有安全网来接住我们那些走钢丝的应用程序开发人员,那么他们犯更多错误也没关系,这很重要,所以我们需要10倍多的SRE和更多的预算,多得多。你还写过宣言,说应用程序开发人员也应该关心,应该做得更好,还有,如果你们能别写那么烂的软件,也许有时测试一下,那就太好了。是的,我也尝试了所有这些事情,这就是我最终得到那件T恤的原因。

我逐渐意识到:停机本身并不是问题。你可能对防止停机感到非常兴奋,因为这很有趣。但停机不是问题。正如伍兹博士所说,混乱是不可避免的,而且“龙”并没有变得更温顺,对吧?此外,停机是一个名词。这不是我定的规则。停机不是我们的问题。长时间的停机才是我们的问题。现在你可能会说这两者是一回事,你在咬文嚼字,有什么区别?区别在于:存在一个机会窗口,一个宽限期。多长的停机时间是可以接受的?是我的网络问题吗?哦,我这里信号不太好。让我重新运行那个任务,那个窗口。我们知道这个窗口。“是我吗?”那个“X网站宕机了吗?谷歌一下”的窗口,对吧?

你可能不介意那张幻灯片。我对这次演讲本身就有停机容忍窗口,对吧?所以,事情是这样的:如果他们没注意到,那就没关系。对于你的系统,存在某个持续时间的停机是可以接受的。那是一段没人会在乎的时间,因为他们正试图弄清楚是他们自己的问题还是你的问题,你处于那种“这是谁的问题”的模糊地带。好的,这就是我们必须利用的。这就是答案。你有一个某种类型的停机容忍窗口。所以你必须找到这个数字。如果你做高频交易,我帮不了你。这次演讲不适合你,希望他们能为所有人提供充足的SRE资金。对于其他所有人,都有一个窗口,也许是几秒钟,也许是几个小时,也许是几天,也许是某种每周运行一次的报表任务,只要它在一周内运行就没事,对吧?但每个人都有那个窗口。

你需要知道它是什么。现在,这不是你的错误预算。这非常重要,对吧?错误预算基本上是说,你知道,一次44分钟的故障和44次1分钟的故障,对于99.9%的可用性来说是一样的,因为数学,因为它确实是一样的,对吧?是的,这是真的。这也是一个错误。这是我们跌倒的地方。停机容忍窗口是实现无限正常运行时间的方法。因为假设你的用户没有注意到一分钟的停机,那对他们来说,一分钟和零分钟是一样的,因为他们没注意到,就等于没发生。所以你有0除以44,数学上是无穷大。现在,这就是我实现无限正常运行时间的一个秘密技巧。

永远不要让停机时间超过他们能注意到的时间。基本上,就像这样,是的,如果网站宕机了但没人注意到,我们就没事。我发现当销售人员谈论100%的正常运行时间时,这曾经让我发疯。我会想:“你想要什么?星际故障转移吗?你根本不懂你在说什么。”现在我想:“哦!你只是希望停机时间短到客户不会对你尖叫。我们也许能做到。”这也比星际故障转移便宜得多。所以,嘿,赢了。好吧。所以你的使命是:确保你的缓解时间、你的服务恢复时间在你的停机容忍窗口之内。我想指出,这不是一个静态值,这是一个你必须不断平衡的方程。这是一件非常“动词”的事情,就像你必须不断做这件事。那个停机容忍窗口,是一个不断演变的情况。你知道,在拨号上网的时代,它相当长,因为你会想:“是我的电话线被拔掉了吗?是我的猫……哦,比如,你知道,朱迪可能正在打电话。”它曾经更长,现在变得更短了,这是一个不断演变的情况。但你必须不断地平衡这个方程,这就是你努力的方向。没有一个静态的最终状态会告诉你“恭喜你,你获得了奖杯”。你只需要继续前进。赢得这场游戏是一个过程,而不是一个结果,因为这是一场不会结束的游戏。你选择玩下去。

章节5:可行的缓解策略——动词思维实践

但这其实是个好消息,因为我们实际上有很多技术。比如,回滚。我们可以再试一次。所以我不打算深入探讨所有你可以投资的通用缓解措施。我的观点是:你应该优先考虑通用的缓解措施,而不是所有其他我浪费生命去做的事情。你应该做得更好,你不应该成为我。所以,回滚。同样,你需要不断确保这确实有效,你必须定期进行回滚。如果你不这样做,你就没有回滚。有趣的事实是,你不想在需要的时候才发现它不起作用。

你可能从我的口音听出我不是本地人。即使我回家,每个人也都这么问我。我来自南非,我们在无法满足需求时,非常擅长降低服务质量。所以我们有这种全国性的“减载”做法,当电网无法供电或我们无法向电网提供足够的电力时,我们就会“减载”。这很棒。令人惊奇的是,我们选择对象,选择时间,我们努力做到公平,我们尽量不对医院这样做。我的意思是,如果南非政府都能想明白,你也能想明白。你应该进行负载卸载。我现在住在加拿大,所以我做公开演讲时,法律上要求我必须提一下蒂姆·霍顿斯,否则我可能会失去公民身份。所以,是的,就像直接扩容,让它更大,直接砸钱。这并不免费,它很昂贵,但可能值得。但同样,这就像一个正在运行的实践。如果你不经常进行扩容和缩容,你会遇到各种各样的问题,你会知道的。就像水流过管道,你不能只把一根管子变大,然后指望不会混乱,你必须经常练习这个。但是,是的,就让它更大。在你弄清楚发生了什么之前,直接砸钱直到问题解决。这是你应该经常做的事情。

最后,你应该在你的工具箱里有这些东西。你应该能够阻止一个用户或一个查询,你应该能够隔离,你应该能够引流,你应该能够做所有这些事情,以便快速缓解情况。同样,你要确保你正在这样做。“用进废退”是这里的基本思想。你不能只是把它设置好一次,然后就说“我完成了,这是我的漂亮名词”。因为当你需要它时,它不会在那里为你服务。你必须不断检查它,确保它有效。

章节6:获取资源与盟友——利用贪婪与恐惧

好的,你可能会说,是的,我们可以做这些项目。但是凯蒂,我们仍然有问题。我们仍然需要钱。说实话,我们也需要爱。实际上有一个爱尔兰乐队叫“爱与金钱”。你需要这些家伙,他们的音乐不怎么样。但我的意思是,是的,你需要保持有酬就业,而不是像我一样失去理智。我理解这一点,这当然是真的。你可以做很多伟大的工作,但如果你被解雇了,那就不会发生。所以是的,你仍然需要钱,你仍然需要爱,而且你值得拥有。我们如何得到它?好吧,有一个方法。人类行为的方式是贪婪和恐惧。这是我们两个主要的驱动力。我们想远离坏东西,走向好东西,对吧?当这两者起作用时,我们就会移动,从坏处移向好处。这就是我们所做的。这就是进化。所以,贪婪和恐惧,我们如何利用这个来为我们自己获得爱和金钱呢?这稍微取决于你是在企业对企业(B2B)的世界,还是在消费者对软件即服务(B2C SaaS)的世界。你的贪婪和恐惧目标有点不同。但如果你在企业世界,你想和你的企业销售团队以及法务部门交朋友。因为企业销售是关于我们如何达成和续签这些支付大量资金的企业交易,同时不被起诉。现在,律师们担心:“我们会被起诉吗?有人说了起诉吗?我们应该……等等,让我们锁定这个。”所以你要确保你正在与你的销售团队交朋友。他们理解“企业就绪”、“卓越运营”等等,所有这些我们都可以销售的东西,人们会为此付费。现在他们就像:“哦,这很令人兴奋。我们喜欢这些SRE人。”而法务部门会说:“嗯,我们能在服务积分上节省多少钱?什么是主服务协议(MSA),它说了什么?如果我们因为没做到而被起诉会发生什么?”哦,现在他们非常担心。现在他们就像:“哦,我的天哪。”而你就像:“我们完全可能被起诉,因为SRE就是我和我的朋友在一个悲惨的法庭上。”不管怎样,现在他们就像跑向CEO:“哦,救命,好吧。”

而在一个消费者对客户(B2C)的SaaS公司,你通常没有庞大的销售团队和庞大的法务团队。但你有产品经理。你的产品经理真的很想确保功能小部件有参与度,并且当结账功能出问题时他们不会亏钱。这些实际上都是可靠性指标。我们只需要做一些翻译。你还有支持团队,他们会说:“拜托,当一切都坏了的时候,所有这些工单太可怕了。”所以你和他们交朋友。然后事情是这样的:你不需要自己去要钱。你那些有点贪婪、有点焦虑的朋友会为你争取到。

章节7:改变开发人员文化——从逃离到参与

如果你像我一样,你可能会想:“但是那些应用程序开发人员呢?”因为如果他们只是继续制造混乱而不在乎,那一切都毫无意义。这也是真的。你可以做很多事,你可以有很多盟友,但你一个人不够。你仍然需要更广泛的可靠性文化,需要从事件中学习,你需要编写代码的人在乎。但他们有动机逃跑,记得吗?因为那就像一场火,他们不想靠近,因为他们是理性的。我们(SRE)是“坏了”的,他们在逃跑。我们如何让他们跑向火场?好吧,事实上,我找到了两个技巧,我非常兴奋。第一个是 “侦探刷” 。他们是工程师。他们是好奇心很强的人。当东西坏了,就像“是谁干的?”就像“Wasm代理插件发生了什么?我们用了Wasm。我想在场。”这确实有效,非常令人兴奋。另外,有人说“简历开发”吗?因为如果你能指出,理解一个系统如何崩溃是理解系统设计总体上的好方法,而这是你职业阶梯的一个组成部分。如果你能帮他们把这些点连起来,突然之间,他们会感兴趣得多。

第二个是:你必须赢得人们的注意力。你可以强迫他们给你时间,但你不能强迫某人给你他们的注意力。就像尊重一样,必须赢得它。所以你必须像这样:“我们是SRE。每个人都想和我在一起,想成为我,想成为我。”你必须体现这种氛围。那么我们怎么做呢?第一件事是保持友善,即使你真的告诉过他们是这样,即使他们在凌晨三点呼叫你,让你像读睡前故事一样给他们读手册,即使他们不友善,你也要友善。第二件事是,找点乐子。我真的很喜欢在我的事件复盘会议上播放音乐,我试着把歌曲和事件配对起来。人们试着猜事件中发生了什么,以及这怎么会和碧昂丝有关。这很有趣,我发现在这种“燃烧”的情况下我享受自己,而这种快乐是具有传染性的,不知何故,更多的人会跑向火场。

所以,谢谢你们。你们也很有趣。

总结

在本节课中,我们一起学习了SRE领域中的“扭曲激励”问题。我们认识到,单纯追求“零停机”是一个陷阱,真正的目标应该是将服务恢复时间控制在用户能容忍的窗口内。我们探讨了如何从“名词”思维转向“动词”思维,通过实践回滚、负载卸载和流量控制等通用缓解策略来动态平衡可靠性。我们还学习了如何利用组织内的“贪婪与恐惧”(如销售、法务、产品、支持团队的诉求)来为SRE工作争取资源和盟友。最后,我们讨论了如何通过激发好奇心(侦探刷)和营造积极、有趣的氛围来改变开发人员文化,让他们从事件的“旁观者”转变为积极的参与者。记住,赢得可靠性是一场持续的过程,而非一个静态的终点。

017:如何在风暴中成熟你的数据架构——Bluesky的生存故事 🚀

在本节课中,我们将跟随Bluesky团队的Jazz,回顾他们在11天内用户量激增10倍的极端压力下,如何应对一系列复杂的系统故障,并逐步成熟其数据架构。这是一个关于韧性、快速决策和架构演进的真实案例。

1. 背景与挑战:风暴来临前的平静 🌊

大家好,我是Jazz,来自Bluesky团队。我们是一个约21人的团队。大约两年前我加入时,用户量约为10万。如今,我们的用户已超过3300万,在极短的时间内经历了爆炸式增长。

我们的系统运行在裸金属服务器上,这意味着容量基本固定。那么,如何应对固定容量下10倍的增长呢?

2. 风暴时间线:识别四种故障模式 ⚡

在本次超大规模事件中,我们经历了四种故障模式。理解这些模式是解决问题的第一步。

以下是四种故障模式及其特点:

  1. 可控软件故障:自己编写的软件出现问题。相对容易修复,只需部署更好的代码。
  2. 不可控软件故障:第三方或开源软件出现问题。修复难度大,需要联系供应商或深入研究。
  3. 可控硬件故障:自有硬件(如磁盘、节点)故障。虽然麻烦,但可以自行更换解决。
  4. 不可控硬件故障:外部依赖的硬件(如运营商光纤)故障。最令人无奈,通常只能等待供应商解决。

我们的流量峰值从每天5000次请求/秒飙升至超过50000次请求/秒。低谷时的流量也超过了历史峰值,这意味着我们全天24小时都处于高压状态,没有安全的维护窗口。

3. 实战案例一:外部硬件故障与失败的容灾 🌐

我们首先遭遇的是不可控硬件故障:数据中心的光纤被切断。当时我们在该设施是单线接入,无法快速切换流量,导致约50%的用户无法访问。

我们决定将流量故障转移到另一个数据中心。然而,这次转移并不顺利。

转移后约五分钟,我们的数据库缓存被彻底击穿。热分片(存储热门数据的分片)的负载翻倍,而我们的缓存架构无法应对这种突发压力,导致所有用户都受到影响。我们不得不回滚操作。

我们学到的教训是:在事故中,不做决定有时比做错决定更糟。果断很重要,但必须权衡“不作为”的成本与“尝试”的风险。我们之前在小规模下测试过容灾,但在10倍流量下这是第一次,结果我们学到了艰难的一课。

对于此类外部故障,我们能做的是:联系供应商、向用户致歉、更新状态页。更重要的是,优先实现BGP配置的双线接入,让冗余系统真正发挥作用。

4. 实战案例二:不可控软件故障与数据库自限流 🗄️

在读取路径上,我们遇到了由开源数据库引起的不可控软件故障。一个数据库节点崩溃后,重新加入集群时引发了长达30分钟的严重性能下降。

我们当时缺乏足够的可见性来理解数据库内部发生了什么。我们收集日志、核心转储,在Github上搜索类似问题,在恐慌中试图找出原因。

问题根源是:节点崩溃时尝试生成一个700GB的核心转储失败。重启后,它需要追赶上千万条写入操作。虽然我们使用了NVMe硬盘,但数据库错误地认为自己可用的IOPS(每秒输入输出操作次数)很少,从而进行了激进的自我限流。这导致整个集群超时,而不仅仅是该节点有问题。

此外,当时我们只有一个大型数据库集群承载所有工作负载,其中一个高写入负载的服务成为了“吵闹的邻居”,加剧了问题。最终,我们在一个非常详细的监控面板中发现了自限流的迹象,并通过调整配置告知数据库真实的IOPS能力解决了问题。

5. 实战案例三:可控硬件故障与“消防水管”过载 🔥

在写入路径上,我们遇到了可控硬件故障。Bluesky有一个公共事件流(Firehose),允许外部订阅。当写入吞吐量增加时,广播这些事件带来了巨大的扩展性挑战。

我们的“中继”进程负责聚合和广播事件流。它从单机单进程运行,在流量激增下不断崩溃,导致网络上的写入延迟高达8-10秒。

问题根源是硬件层面的

  1. 磁盘空间耗尽:数据压缩例程跟不上写入速度,磁盘被填满。
  2. NVMe延迟飙升:持续高强度的写入耗尽了SSD的预留空间。垃圾回收器无法跟上,导致写入延迟从几百微秒暴增至几十毫秒。公式上可以理解为:
    实际写入延迟 = 基础延迟 + 垃圾回收排队延迟
    当垃圾回收跟不上时,排队延迟急剧增加。
  3. 文件系统损坏:在机器生命末期,我们甚至遭遇了罕见的XFS文件系统损坏。

系统负载从接近0飙升至4000,这是机器“极度痛苦”的信号。

我们的应对策略是多管齐下

  1. 切换至低负载架构:启用一个已搁置8个月的“非归档中继”方案,减少磁盘I/O。
  2. 卸载服务子组件:将广播流量分流到另一台机器。
  3. 启用热备硬件:用备用机器承载新服务。
  4. 团队分工协作:将不同任务分给不同成员,在2-3小时内并行解决。

6. 实战案例四:内部软件故障与“衔尾蛇”循环 🐍

我们还制造了一个“衔尾蛇”式的内部软件故障。由于配置错误,我们部署的代理将公共事件流流量错误地引向了新建的内部流量卸载服务,而这个服务又连接回自身,形成了一个循环。

这导致外部订阅者在数小时内收到大量重复事件,而错过了真实事件。这起事故由疲惫创可贴式修复叠加导致。在高压下,我们进行了许多手动修复且未及时纳入代码管理,导致配置混乱和人为错误。

事后我们:修复代理配置、为消费者重放丢失的事件、向开发者社区致歉,并决定自动化未来的代理部署流程,要求至少两人审查。同时认识到,团队疲惫时,自查和交叉检查至关重要。

7. 实战案例五:可控软件故障与名人缓存策略 ✨

我们遇到了典型的“贾斯汀·比伯问题”:极少数名人账号占据了绝大部分流量。我们的分片数据库虽然水平扩展,但每个数据分区只由三个核心服务。当所有人都查询同一个名人时,这三个核心就会过载,拖累整个数据库的P99延迟。

我们的解决方案是引入动态缓存

  1. 每个数据进程维护一个内存缓存。
  2. Redis每30秒同步一次缓存配置(即“谁是热门用户”列表),而非缓存数据本身。
  3. 各进程根据此配置管理自己的缓存。

代码逻辑大致如下:

# 伪代码示例:数据进程定期从Redis获取热门用户列表
def update_cache_config():
    hot_actors = redis.get(‘hot_actors_list‘)  # 获取配置,非数据
    local_cache.set_policy(hot_actors)         # 更新本地缓存策略

部署该策略后,数据库查询P99延迟从100毫秒降至10毫秒,峰值查询吞吐量降低了50%,效果显著。这个案例告诉我们,数据的访问模式极大程度上决定了何种优化策略最有效。

8. 其他挑战与速战速决 🛠️

在11天的风暴中,我们还快速应对了其他问题:

  • DNS限流:公共DNS服务(如1.1.1.1)对查询速率有限制(约5000次/秒)。我们通过搭建本地DNS解析器并请求供应商提升限额来解决。
  • 搜索API被滥用:一个帮助用户迁移的Chrome扩展程序疯狂调用我们的搜索API,形同DDoS攻击。我们联系开发者修复了扩展。
  • 服务资源竞争:算法推荐服务与Postgres数据库同机部署,导致内存竞争。这促使我们将工作负载隔离到专属机器。

9. 总结与反思:如何恢复并变得更强大 🌈

本节课中,我们一起回顾了Bluesky在极端增长压力下面临的多重挑战。我们从这些实战中学到了:

  1. 冗余不是摆设:确保冗余系统(如网络双线)真正可用,并提前测试容灾方案。
  2. 可见性是关键:深入监控系统内部指标(如数据库自限流),以便快速定位根因。
  3. 理解硬件极限:了解所用硬件(如SSD的预留空间)的物理限制,设计与之匹配的架构。
  4. 避免创可贴式修复:手动变更需及时回滚至代码库,防止配置漂移和“衔尾蛇”错误。
  5. 针对数据模式优化:像名人缓存策略那样,根据实际查询形状制定最有效的解决方案。
  6. 团队与流程:在危机中分工协作,并在事后自动化流程、加强审查,为下一次压力做好准备。

最终,我们通过增加硬件、完善架构和团队协作度过了难关。这段经历虽然充满压力,但也极大地提升了我们的系统韧性和应对危机的能力。记住,系统总会出问题,但我们可以选择如何应对并从中成长。

018:与视障事件响应者协作的最佳实践

在本教程中,我们将学习如何与视障或盲人SRE(站点可靠性工程师)进行有效协作。我们将探讨如何调整与计算机的交互方式、改进团队沟通,并确保工具和流程对所有人都可访问。这些实践不仅能帮助视障同事,也能提升整个团队的效率和协作质量。


课程1:不同的视角与适应

上一节我们介绍了本课程的目标,本节中我们来看看视障工程师能为团队带来的独特价值。

作为视障人士,我们一直在适应环境。这种成长经历使我们天生擅长处理音频信息、进行多任务处理,并能敏锐地捕捉语音中的细微差别。在事件响应团队中,这些技能非常宝贵,因为我们能够“听”出问题的不同方面,从而提供独特的视角。

拥有多元化的团队,你自然能获得这些不同的视角。


课程2:与计算机交互——输入

上一节我们了解了视障同事的独特优势,本节中我们来看看他们如何与计算机进行交互,首先从输入开始。

对于视障用户,高效的输入方式至关重要。

以下是几种主要的输入方法:

  • 盲打:在早期学习盲打是一项关键技能,它能确保用户将双手保持在键盘上,提高输入效率。
  • 语音输入:在移动设备或特定场景下,语音输入是常用的辅助方式。
  • 键盘替代方案:许多操作依赖键盘快捷键和等效命令,而非鼠标,因为鼠标操作对屏幕阅读器不友好。

核心概念是保持双手在键盘上,并使用键盘快捷键(如 Ctrl+S 保存)来提高效率。


课程3:与计算机交互——输出

上一节我们讨论了输入方式,本节中我们转向输出,重点介绍屏幕阅读器。

屏幕阅读器是一个“监视”其他应用程序(如桌面程序或网页)的软件。它通过API或DOM获取信息,并将其转换为语音或盲文输出。

为了让屏幕阅读器有效工作,应用程序必须与之正确通信。如果通信失败,屏幕阅读器要么沉默,要么猜测屏幕内容,这通常会导致不理想的结果。


课程4:演示1——使用屏幕阅读器进行终端操作

现在,让我们通过一个实际演示来看看屏幕阅读器如何辅助技术工作。以下演示将展示在终端中使用屏幕阅读器完成Git操作。

我将使用Windows终端和屏幕阅读器。对于输入,我使用盲打。对于输出,我主要依靠语音反馈,必要时用盲文显示器核对。

# 检查Git状态
git status
# 添加文件
git add demo/*
# 提交更改
git commit -m “报告更新”
# 推送更改
git push

在演示中,屏幕阅读器会朗读命令、输出结果和错误信息(如输入fit而非git时的提示),使我能够像视觉正常的工程师一样完成工作。这凸显了工具(此处是终端和Git)通过屏幕阅读器提供清晰、准确信息的重要性。


课程5:演示2——使用盲文显示器进行编码

上一节我们看到了语音输出的例子,本节中我们体验另一种输出方式:盲文显示器。

盲文显示器是一种硬件设备,能以盲文形式实时显示屏幕内容。对于编码等需要精确核对字符(如括号、缩进)的技术工作,它不可或缺。

在演示中,我使用盲文显示器在Visual Studio Code中编写一个Ansible playbook的YAML文件。

---
- name: 默认Web更新
  hosts: web_servers
  become: true
  tasks:
    - name: 确保Nginx最新
      apt:
        name: nginx
        state: latest

通过盲文显示器,我可以逐字检查代码,确保缩进(在YAML中至关重要)和语法正确无误。同时,屏幕阅读器的语音反馈与盲文输出同步,验证了开发环境(VSCode)与辅助技术之间的良好通信。


课程6:改进沟通——核心原则

前面的演示展示了个人如何与工具交互。现在,我们来探讨如何改进人与人之间、以及人与技术之间的沟通。

对于视障SRE而言,改进沟通的核心在于调整你的沟通方式。目标不仅是让屏幕阅读器能获取信息,更是要确保它提供的是有用的输出

一个历史教训是:仅通过自动化工具(如过去的“Bobby”测试)确保“可访问”,而不进行真实用户测试,可能会产生像所有图片都标注为“alt text”这样毫无用处的页面。因此,进行屏幕阅读器和最终用户测试至关重要。


课程7:改进沟通——具体实践(爬行阶段)

“爬行”阶段指的是基础的、必要的适应性调整。以下是几个关键实践:

在屏幕共享时,请勿仅说“错误在屏幕上”。请将关键文本粘贴到聊天窗口。

  • 良好示例:“数据库似乎出了问题。我把错误信息贴到聊天框里了。”
  • 不良示例:“数据库报错了,信息在屏幕上。”

发送截图时,请务必描述截图内容。更好的做法是附带文字说明。

  • 虽然OCR技术可以识别截图文字,但直接提供文字描述更可靠、更高效。

注意“反向可访问性”问题。例如,如果文本和背景色对比度太低,任何人都难以阅读。

  • 我曾提交一份技术文档,因为前景色和背景色相同,导致同事一开始以为页面是空的。

沟通是双向的。如果你发现我的摄像头角度不对,或者我提供了难以访问的内容,请直接告诉我。


课程8:改进沟通——不要单独使用颜色

使用颜色传达信息时,切勿仅依赖颜色本身。

大约8%的男性和0.5%的女性患有某种形式的色盲(如红绿色盲)。如果仪表盘仅用红色表示故障,视障或色盲用户将无法识别。

解决方案是提供额外的标识符。

以下是一个不良的服务状态列表示例(对屏幕阅读器而言):

服务器名        状态
node1.example.com
node2.example.com

(屏幕阅读器可能只读出“状态,空白列2”,无法传达“故障”信息。)

改进后的示例如下:

服务器名        状态
node1.example.com  !
node2.example.com

通过添加感叹号等符号,并在团队内约定其含义,所有成员都能理解状态信息。


课程9:改进沟通——处理图表数据(行走阶段)

“行走”阶段涉及更复杂的场景,例如处理仪表盘中的图表。

对于屏幕阅读器用户,一个复杂的折线图或柱状图可能只是一系列没有上下文的数据点朗读,认知负荷极大。

解决方案包括:

  • 确保数据可用:提供图表的表格化数据视图或可下载的数据集(如CSV文件)。
  • 发送智能通知:与其让人在大量数据中寻找异常,不如让监控工具分析、总结并直接发出通知:“Randy,服务器X在Y时间出现CPU尖峰,请查看。”

这不仅帮助了视障工程师,也减轻了所有团队成员的认知负荷,是多元化团队协作带来共同优化的典型例子。


课程10:总结与资源

在本课程中,我们一起学习了与视障SRE协作的最佳实践。

爬行(基础调整):我们通过额外沟通来弥合差距,例如在屏幕共享时进行语言描述、解释截图内容,并避免单独使用颜色来传达信息。

行走(优化协作):我们确保图表数据可通过表格或下载方式获取。更进一步,我们优化工具,使其能够分析、总结、优先处理数据,并以可读格式(如清晰的通知)呈现给所有人。这提升了整个团队的效率。

奔跑(持续学习):如果你不确定该怎么做,最好的方法是直接询问。开放、双向的对话是包容性文化的基石。

一些有用的资源:

  • IBM企业可访问性页面:提供丰富的资源和检查工具。
  • Web内容可访问性指南(WCAG):为网页与辅助技术通信提供了国际标准。
  • 社区与交流:可以通过会议Slack、RSA Slack或电子邮件与我联系,共同探讨解决方案。

感谢你的时间。包容性设计让每个人都能更好地奔跑。

019:实际进行跨事件分析 🚀

在本节课中,我们将探讨如何超越对单个事件的回顾,通过跨事件分析来获取宏观洞察,从而推动组织层面的系统性改进。我们将学习如何构建一个可持续的、大规模的事件学习计划。


概述:从单个事件学习到跨事件分析 📈

上一节我们介绍了从单个事件中学习的重要性。本节中,我们来看看如何将这种学习规模化,即进行跨事件分析。跨事件分析旨在从大量事件回顾中识别模式和主题,从而发现更深层次的系统性问题和改进机会。

为什么传统的度量标准不够?🤔

有多种方式可以衡量一年中的事件。你可以统计事件总数、产生的行动项总数,或者事件持续的总时长。但这些数字本身缺乏上下文,无法提供有意义的洞察。它们无法告诉我们事件背后的原因、影响以及如何系统性预防。

我们真正需要的是建立在大量个体事件回顾基础上的洞察。我们希望从这些事件集合中识别出带有上下文的模式,以突出改进机会。

什么是“从事件中学习”?📖

“从事件中学习”是一种深入理解事件如何从不同视角发生的方法。其重点不在于行动项或为满足合同要求而填写的报告。它旨在全面理解事件的成因。

以下是进行事件回顾的典型步骤:

  • 第一步:识别数据。确定涉及的人员、沟通渠道(如Zoom、Slack)、相关的代码提交或支持工单。
  • 第二步:准备并召开回顾会议。基于已有数据,邀请相关人员,创建叙事时间线,并召开协作式会议,让每个人分享经历。
  • 第三步:总结并分享发现。将最终发现整理成他人能理解的格式,根据受众(如领导层或技术团队)调整内容细节。

这种方法帮助我们获得超越表面原因(如“发现一个Bug”)的深刻理解,例如发现组织合并后的权限遗留问题,从而采取能惠及整个团队的行动。

规模化分析的挑战与机遇 ⚙️

将单个事件分析的做法规模化会面临诸多挑战。不同组织的做法可能不同,但通常包括:为所有重大事件进行回顾、建立可比较的结构化流程、以及进行足够多的分析以发现共性主题。

然而,规模化是困难的。原因如下:

  • 耗时耗力:创建叙事时间线、协调会议、撰写报告需要大量时间。
  • 技能差异:工程师可能不擅长收集定性数据、进行数据分析或向非技术人员有效呈现发现。
  • 数据不完美:叙事报告难以进行数据查询和跨事件比较。

Inova的成功实践:我们的故事 🏆

在Inova,我们建立了一个可持续的跨事件分析计划。我们有一个专职团队,负责对所有重大和敏感事件进行无责难的学习回顾。我们定期(季度、年度)进行宏观分析,并向组织提出改进建议。关键点在于,我们让工程、运营、市场、法务等多个部门都参与并认同这个过程。

我们通过以下方式实现了这一目标:

  1. 证明价值:我们主动参与讨论,展示工作带来的直接改进。
  2. 持续改进:我们不断回顾并调整程序,以适应组织和技术的演变。
  3. 发挥创造力并推动边界

实践指南:如何发挥创造力?🎨

在回顾流程上创新

没有固定规则。你可以决定回顾哪些事件、由谁主持、准备时长等。我们的创新包括:

  • 集中化分析团队:为确保质量,我们组建了专职分析团队,而非让产品团队兼顾。
  • 一体化会议:我们将时间线回顾、因素分析和后续步骤讨论合并到一个会议中。
  • 扩大参与范围:邀请工程以外的部门(如市场、法务)参与,这有助于理解事件的全方位影响并促进团队协作。

在事件报告(产出物)上创新

我们的报告存储在Jira中,混合了定量和定性字段:

  • 定量字段:如时间戳、影响分类,便于查询和初步分析。
  • 定性字段:如叙事时间线、影响描述、人员决策依据,提供深度上下文。

这种“带上下文的指标”方法,为深入分析奠定了基础。

在分析方法上创新

我们的分析从专注于数字演变为富有洞见的对话:

  • 月度会议:与产品领导讨论具体事件的学习点和优先事项。
  • 季度/年度回顾:展示数据、提出顶层发现和项目建议。

分析时,我们采用“蔬菜与肉酱”法:既提供指标(蔬菜),也提供上下文(肉酱)。例如:

  • 分析受影响最大的客户旅程,并深挖其背后原因(如供应商不稳定)。
  • 分析贡献因素的共性,例如在节假日是否出现更多容量问题。
  • 分析行动项的重复主题,是否可以催生更大的倡议(如建立SLO季度计划)。

分析师思维速成指南 🧠

即使不是专业分析师,你也可以培养相关思维:

  1. 保持好奇,而非评判:询问事情如何运作,思考当事人面临的约束条件。
  2. 关注社会技术系统:不仅考虑技术故障,还要思考流程、工具、团队沟通、人员培训和组织变动的影响。
  3. 将数据视为方向性指标:数据本身没有意义,上下文决定一切。给领导呈现MTTR时,务必附上背景说明。
  4. 与人交谈:从他们的视角理解经历,避免基于数据的错误假设。

经验与教训:成功模式与失败反模式 ⚖️

成功经验

  1. 避免成为“行动项工厂”:确保行动项的所有者认同并拥有它,允许取消无意义的行动项。我们引入了“建议”与“承诺”的区分,以跟踪那些单次事件中难以优先但反复出现的重要改进。
  2. 促进透明与跨团队学习:通过定期分享会,让不同团队了解其他部门的情况并相互学习。例如,关于功能标志工具的决策后来催生了相关的流程改进,各团队得以分享经验。

失败教训

  1. MTTR等聚合指标的局限性:平均解决时间(MTTR)本身信息量有限,因为它无法区分10分钟的事件和40小时的事件。我们通过提供上下文(例如,安全事件因涉及新团队和流程而耗时更长)来补充这些指标,并逐渐减少对其的依赖。
  2. 避免指标被博弈:事件总数、内部原因事件数等指标曾引起领导关注,但很快被博弈(例如,通过制造大量短暂“闪烁”来降低MTTR)。我们通过引入“未遂事件”(near misses,我们称为“挽救”)分析和区分安全事件流程,为这些数字增添了有意义的上下文,从而发现了更多学习机会。

如何开始?你的第一步 🚶

你不需要一个完整的团队才能开始:

  • 从小处着手:选择一个小组或一个季度,先提升单个事件回顾的质量。
  • 让他人参与:在召开回顾会议前,与参会者沟通议程;在分享分析前,先征求技术主管或经理的意见。
  • 积累成功:小的胜利会吸引更多的支持和资源。
  • 这不是冲刺,而是马拉松:不要试图单挑CTO或指责同事。争取盟友,循序渐进地推动变革。

总结与行动号召 🌟

本节课中,我们一起学习了如何从对单个事件的学习,演进到进行有意义的跨事件分析。关键在于为数据赋予上下文创新你的流程与工具,并让更广泛的组织成员参与进来。通过构建一个专注于学习、所有权和持续改进的文化,你可以规模化地从事件中学习,并真正推动有意义的改变。

如果你想深入探讨,欢迎加入“韧性软件基金会”(Resilience and Software Foundation)的社区。现在,就去尝试实践这些步骤吧!


020:运行DRP桌面演练

概述

在本教程中,我们将学习如何规划和执行灾难恢复计划的桌面演练。我们将以密歇根大学文学、科学与艺术学院技术服务中心的真实案例为基础,介绍从发现问题、制定计划、执行演练到评估改进的完整流程。目标是帮助你理解如何通过结构化的演练,提升团队对灾难恢复流程的熟悉度,并发现计划中的潜在缺陷。


背景与问题

上一节我们概述了课程目标,本节中我们来看看促使我们进行演练的背景和遇到的问题。

我们的团队负责为学院提供超过70项基础服务。我们自2008年起就制定了灾难恢复计划,但在实践中发现了以下问题:

  • 范围狭窄:大多数计划只考虑自然灾害导致数据中心损毁的情况,忽略了服务或组件级别的故障。
  • 恢复步骤模糊:许多计划的恢复步骤仅限于“购买新硬件、重装系统、从备份恢复”,缺乏详细指导。
  • 文档缺失或过时:部分服务没有构建指南,或者指南未随系统变更而更新,知识仅存在于个别成员脑中。
  • 计划不完整:并非所有服务都有对应的灾难恢复计划。
  • 缺乏一致性:由于计划模板不统一,不同作者会删除自认为不必要的部分,导致计划间差异巨大。
  • 测试有效性存疑:年度测试可能只是个人的思维实验,缺乏团队协作和验证。人员流动也导致服务负责人变更,最初的假设和知识可能丢失。

我们希望通过演练达到以下目标:统一团队的认知基线、识别服务上下层的所有依赖关系、建立一致的计划编写和测试方法。


灾难恢复计划应包含的内容

在了解了存在的问题后,本节我们来看看一份合格的灾难恢复计划具体应包含哪些核心要素。我们的计划模板包含以下部分:

  • 服务描述:提供服务的高级概述。实施恢复的人可能不是该服务的专家。
  • 依赖项列表:包括硬件(主机名、IP、型号/序列号、CPU/内存/磁盘、位置)、软件(特定版本要求)、配置信息(服务账户、数据库用户等)。
  • 安全与设施:物理和电子安全访问控制。
  • 备份信息:明确指出备份存储的位置和解决方案。
  • 操作信息:测试频率、最小运行标准(例如,单节点只读)与完全运行标准的定义。
  • 文档位置关键: 不要将文档内嵌在计划中,但必须提供指向构建指南、架构图等文档的明确链接或路径。
  • 计划任务:例如 cron 作业,包括运行时间、执行账户及备份方法。
  • 联系人信息:服务负责人、技术负责人、需要通知的客户或合作伙伴、硬件/软件供应商紧急联系人。
  • 服务级别与影响:如果存在服务级别协议,请链接。否则,需说明最大可接受停机时间以及超出时限的业务影响。

桌面演练的设计与执行

明确了计划内容后,本节我们将焦点转向如何设计和执行有效的桌面演练。

我们为整个15人团队组织了引导式桌面演练,针对三项服务进行:虚拟机管理程序基础设施(基础服务)、网络托管环境(应用服务)、校内数据中心(物理位置)。

以下是我们的方法:

  • 角色:我们简化了正式角色。服务经理和技术负责人扮演自己,其他成员扮演中央IT、学院办公室等外部依赖方。多人兼任观察员和数据记录员。
  • 形式:协作讨论,引导员(我)不隐藏信息,也不通过掷骰决定行动成败,目标是共同找到恢复路径。
  • 未做的部分:我们没有模拟发送真实通知(邮件、状态页),也未撰写正式的事后报告,以降低复杂性。

具体演练场景:

  1. 虚拟机环境入侵:攻击者侵入集群中的一个节点,进而可能访问所有节点和底层存储(包含虚拟机镜像和备份)。如果中央IT的备份也已过期,后果将是灾难性的,需要从零开始重建一切。
  2. 网络服务器入侵:攻击者侵入托管了136个网站的核心服务器。由于访问受限,破坏被控制在单台服务器内。恢复工作包括重建服务器、从备份恢复网站、更换所有密码和证书。
  3. 数据中心物理入侵:讨论窃取、破坏、为掩盖盗窃而破坏等多种可能性。演练揭示了一些非技术约束,例如某研究设备根据资助条款必须存放在特定机柜,灾后迁移需要法律部门介入。

演练成果与衡量标准

执行演练后,我们需要评估其成效。本节我们回顾最初的目标,并查看参与者的反馈来衡量成功与否。

我们通过调查问卷(1-5分)收集反馈,问题包括:演练进行得如何?最喜欢/最不喜欢的部分?如何改进?

2024年2月首次演练结果:

  • 平均分:4.3
  • 积极反馈:形式有吸引力;增进了对计划和个人思考过程的了解;圆桌讨论和头脑风暴有帮助;通过讨论别人的计划,发现了自己计划中的漏洞。
  • 改进点:讨论有时会陷入细节(“钻进兔子洞”);问题定义可以更清晰;首次尝试纳入重大事件沟通流程显得多余,后续已移除。

关键反馈引用

“这次演练令人恐惧。” — 这恰恰是演练的目的之一,即真实地感受灾难的压力。

2024年3月第二次演练结果:

  • 平均分提升至:4.7
  • 超过一半参与者认为两次演练效果相当或第二次更好。
  • 积极反馈:主题更集中;专注于技术行动而非沟通,效果更好。
  • 持续改进点:仍需控制讨论深度;部分人认为范围有时过窄。

衡量标准达成情况:

  • 提升了对计划实施的理解?
  • 考虑了需要在学院/大学层面进行的增、删、改?
  • 发现了更多漏洞和盲点?
  • 制定了填补漏洞的计划?

后续行动与关键要点

根据演练的发现和反馈,我们制定了后续步骤。本节将介绍这些行动,并总结一些供你参考的关键要点。

后续行动:

  1. 修订计划:参与者根据新认知更新其灾难恢复计划、构建指南、架构图和设计文档。
  2. 明确通用依赖:团队一致同意,即使有些依赖看似明显(如电力、冷却、核心网络、DNS、中央认证、文档存储),也应在计划中列出。
  3. 实施改进
    • 确保所有服务器磁盘静态数据加密
    • 在资产清单中添加缺失的软件许可证。
    • 研究并实施虚拟机亲和性/反亲和性规则
    • 更新监控项。
    • 鼓励加入大学级的灾难恢复实践社区。
  4. 固化流程:引导员分析结果,团队决定每年2月/3月进行演练,并开发培训模块以培养更多引导员。

供你参考的关键要点:

  • 做最坏打算:假设需要从零开始重建且没有备份
  • 文档至关重要:维护最新的构建指南和架构图。考虑使用变更日志来记录与指南的偏差。
  • 备份验证:确保备份了正确的内容,并定期验证其可恢复性。
  • 测试深度:根据风险选择测试方式——思维实验、桌面演练、部分恢复测试或完整重建。可以混合使用,例如每年做桌面演练,每3-5年做一次完整重建。
  • 资源准备:如果计划中引用了外部资源(如Wiki),确保在恢复环境也能访问,或将其PDF副本附加到计划中。

总结

在本教程中,我们一起学习了运行灾难恢复计划桌面演练的全过程。我们从识别现有计划的不足出发,探讨了完整DRP应包含的要素,详细介绍了如何设计并执行针对不同服务场景的桌面演练。通过分析参与者反馈来衡量演练成效,并根据发现制定了具体的后续改进措施。最后,我们总结了一系列在编写和测试自身灾难恢复计划时的关键考量。记住,演练的目的不是追求完美,而是暴露问题、统一认知、并为真实的灾难做好准备。

021:一种治理方法 🧠

在本教程中,我们将学习如何通过治理方法来优化机器学习训练基础设施。我们将探讨容量管理、可观测性、治理框架的设计与实施,以及如何将这些实践与业务投资回报率联系起来。课程内容基于Meta公司的实践经验,旨在为生产工程师和SRE提供可操作的见解。


容量管理与挑战 ⚙️

机器学习训练硬件非常昂贵且稀缺。获取和部署这些硬件需要大量时间和配套基础设施。为了最大化投资回报率,必须确保硬件在其生命周期内获得最大效用。

模型开发过程本身也带来挑战。与请求-响应服务不同,机器学习开发更具实验性和迭代性,反馈周期很长。由于反馈周期长,我们需要尽早考虑可靠性和容量使用问题。此外,结果的可调试性有限,因此需要确保在启动阶段就尽可能正确。

在大型环境中,我们会面临异构硬件环境、区域限制、内部产品竞争以及不同使用阶段(构思、测试、生产)的资源分配问题。要充分利用整个计算集群,需要全面的可观测性。

可观测性与治理基础 📊

要实现有效治理,首先需要出色的归因和指标。没有这些,很难为业务取得正确的结果。

归因可以有不同范围,从公司级到产品组甚至子组。目标是尽可能细化归因,以支持治理。治理初期可能由人工主导,但最终目标是实现自动化治理。

归因示例

  • 基础设施:硬件类型、区域。
  • 训练栈:使用的软件栈。
  • 业务上下文:模型名称、项目、模型类型(业务目的)、训练阶段。

指标示例

  • 成本:GPU小时数、网络带宽、存储。
  • 效率:每秒样本数、GPU利用率、SM利用率(GPU寄存器级)。

通过收集这些属性和指标,并采取治理方法,我们可以更好地将基础设施使用与业务投资回报率联系起来。

训练工作负载治理框架 🏗️

上一节我们介绍了治理所需的基础数据,本节中我们来看看如何构建一个治理框架。治理可以应用于多个领域,例如模型组合管理、资源访问控制和项目优先级排序。本案例将深入探讨训练工作负载治理,即管理启动并使用ML训练容量的作业。

训练工作负载治理可分为几个层次:

  1. 归因治理:这是基础层,为作业提供目的信息(如关联的项目、模型类型、开发阶段)。这些属性是可观测性的关键,也是构建其他治理工具的基础。
  2. 容量治理:通过配额检查,确保作业使用正确的容量池(例如,高优先级池提供低延迟保证)。
  3. 可靠性治理:防止作业因使用已弃用功能或不兼容的硬件配置而失败,这既影响容量效率,也影响开发人员生产力。
  4. 效率治理:在容量有限时,根据作业的业务上下文(如项目紧急程度、收入影响)进行精细化的优先级排序,并推广使用更高效的训练技术。

为了达成这些治理目标,我们构建了一个治理框架,并设定了以下核心要求:

  • 通用性:框架可用于训练工作负载及其他治理领域。
  • 分布式与层次化:支持Meta大型组织的分层级、分范围的规则管理。
  • 多治理点一致性:在作业提交链路的不同节点(如笔记本、编排器、中央调度器)实施一致且灵活的规则。
  • 强大的规则表达能力:允许团队利用其专有数据源和业务状态,制定丰富、敏捷的治理规则。
  • 高可靠性:框架本身需可靠且维护开销低。

框架设计与关键技术 🔧

我们设计的解决方案将治理业务逻辑与具体执行框架分离,并将其集中管理。然后在所有关键通道(尤其是中央调度器)部署客户端,以确保治理覆盖的全面性和早期问题检测。

以下是该解决方案的一些关键设计细节:

  • 层次化规则结构:规则像树一样组织,与公司职责结构对应。每个策略节点负责提供特定功能,并决定下一个执行的策略节点。这提供了所需的隔离性和层次化治理结构。
    # 概念性伪代码,表示策略节点决策流程
    def evaluate_policy_node(job, context):
        if not check_company_level_rule(job):
            return "REJECT", "违反公司级规则"
        elif not check_product_group_rule(job, context):
            return "REJECT", "违反产品组规则"
        else:
            return "PROCEED", determine_priority_and_pool(job)
    
  • 团队自主与责任:各团队可以独立推出并拥有其策略,同时负责处理策略失败、维护指标和告警。
  • 安全隔离:框架提供基于模板的方法来构建策略,并在编写、运行时隔离上下文,确保错误只会影响负责该策略的团队。
  • 可靠性保障
    • 服务设置为 故障开放,以保障作业提交不受框架自身故障影响。
    • 策略上线前经过 影子模式 多日观察,预知其影响。
    • 提供框架级的 紧急熔断机制
  • 沟通与渐进式推行:治理涉及大量人力协作。在阻止违规作业前,必须进行充分沟通,提供明确警告和修复指南,以获取社区支持,避免摩擦。治理策略的推行是缓慢而结构化的过程。

关键成果与经验总结 🎯

通过实施该治理框架,我们取得了多项成果:

  • 实现了对关键业务属性(如模型类型)的 100%覆盖,获得了清晰的成本洞察以支持投资决策。
  • 在容量紧张时,实现了工作负载的 高效优先级排序
  • 显著提升了 开发人员生产力,阻止了大量将在生产环境中失败的作业。

我们从实践中总结出以下核心经验:

  1. 治理推行是艰难的:沟通至关重要,这是一个以人为本的缓慢过程,框架的目标是结构化前进并最小化损害,而非追求速度。
  2. 治理是有限使用的工具:自动化才是最佳方式。能够通过自动化实现的目标,就不应使用治理。治理应与自动化相辅相成。
  3. 治理需与激励结构对齐:如果治理规则与业务目标存在深层冲突,导致“打破规则反而收益更大”,那么该治理注定会失败并引发摩擦。
  4. 不同治理领域需协同工作:模型开发、训练、A/B测试、推理管线等各个环节的治理故事必须与更大的业务叙事保持一致,否则会出现低效和漏洞。

治理的价值与推广 🚀

治理的最佳状态是轻量级的,它通过推动可观测性来实现目标。可观测性使我们能够看清投资流向和效果,从而获得实现业务目标和SRE目标的杠杆。治理是达到目的的手段。

生产工程师和SRE非常适合推动这项工作,因为:

  • 全局视野:PE擅长审视开发过程的整体图景,使其更可重复、可靠和高效。
  • 可靠性核心:治理本质上是可靠性驱动的,这与SRE的核心关切一致。
  • 可观测性驱动:PE善于跨整个生态系统构建可观测性,而不仅仅是关注特定产品切片。
  • 解决方案构建:我们擅长构建解决复杂挑战的解决方案,推动系统向更自动化、更可调试、更可理解的方向发展。

即使在小规模环境中,也可以开始思考如何引入这些实践:开始跟踪关键属性和指标,并利用它们为业务创造更大价值。


本节课中我们一起学习了如何通过构建系统的治理框架来优化机器学习训练基础设施。我们从容量挑战和可观测性基础出发,深入探讨了一个分布式、层次化治理框架的设计、实施与可靠性保障,并总结了关键的成功经验和推广价值。治理的核心在于通过可观测性赋能业务,在保障可靠性与效率的同时,推动整个ML开发流程向更自动化、更高效的方向演进。

022:超越顺序——异步流水线可观测性与告警方案

概述

在本节课中,我们将学习如何为异步应用程序定义服务级别目标,以确保其得到有效监控,从而为用户提供一致的体验。我们将分享一个包含三个主要步骤的“配方”:准备指标、定义SLO与仪表盘、以及维护覆盖与合规性。

1:准备原料——收集应用与指标

上一节我们介绍了本教程的目标,本节中我们来看看实现可观测性的第一步:准备必要的“原料”。

异步应用程序在我们的组织中并不罕见。许多用例并不需要立即响应用户请求,例如收集请求后进行“发射后不管”的处理,只需在结果就绪时再关注即可。这是基础设施中非常常见的模式。

以下是构建可观测性“配方”所需的核心要素:

  • 异步应用程序:这是我们的主要“食材”。需要明确其工作流程与状态。
  • 指标:没有指标就无法实现任何可观测性。这是基础。
  • 仪表盘与可视化:在指标之上,需要创建可视化的仪表盘,这自然意味着需要查询能力。
  • 有效事件与良好事件的定义:在定义SLO之前,需要明确什么事件是“有效的”,什么事件是“良好的”。
  • 服务级别指标:在良好事件与有效事件的定义之上,才能构建SLI。

为了确保我们对“异步应用程序”有清晰的理解,这里以eBay的商品推荐系统为例进行说明。

该系统的工作流程如下:

  1. 生产者:用户通过UI或API购买商品或添加商品到关注列表。这些是同步服务,但它们会生成一条消息(或称“事件”)并将其放入一个队列(种子商品队列)。事件负载包含用户ID和商品ID。
  2. 队列:作为生产者与消费者之间的缓冲区。
  3. 异步排序服务:这是我们要监控的主要消费者应用。它从队列中消费事件。
  4. 外部调用:消费者会调用机器学习推荐API。由于网络瞬态问题,调用可能失败。
  5. 重试队列:平台支持重试队列。如果消费事件失败,事件会被自动加入重试队列。消费者可以从主队列或重试队列读取事件。
  6. 已排序商品数据库:处理完成后,结果(例如,针对某个用户ID的推荐商品ID列表)被写入该数据库,供其他服务(如推荐商品页面)后续使用。

2:烹煮与调制——从指标到SLO与仪表盘

上一节我们了解了异步应用的工作流程,本节中我们来看看如何为其定义服务级别指标。

以下是一些可能的SLI示例(非详尽列表):

  • 可用性:衡量成功处理事件的比例。
  • 延迟:衡量从事件产生到被成功处理所需的时间。
  • 新鲜度:衡量处理结果(如推荐商品)的时效性。
  • 质量:衡量处理结果(如推荐列表的准确性或完整性)的好坏。
  • 吞吐量:衡量单位时间内处理的事件数量。

在我们的实践中,我们主要关注可用性延迟这两个SLI。原因在于其关键性:如果能有效测量可用性和延迟,可以在很大程度上推导出其他三个指标。例如,可以将低质量结果定义为失败响应;新鲜度和吞吐量也可以从延迟中推导(处理时间越长,结果越不新鲜,吞吐量也越低)。

定义可用性SLI

每个进入异步应用的事件都有三种可能状态:成功放弃重试。其中,重试是瞬态,所有事件最终都会变为成功放弃。我们使用Prometheus计数器来记录事件状态。

以下是定义“良好事件”与“有效事件”的Prometheus查询示例:

  • 良好事件(成功)sum(rate(consumed_item_count{consumer="ranking", event="rank_item", status="success"}[1m]))
  • 有效事件(总数)sum(rate(consumed_item_count{consumer="ranking", event="rank_item", status=~"success|abandoned"}[1m]))

可用性SLI即为:良好事件数 / 有效事件数

定义延迟SLI

我们使用Prometheus直方图来计算延迟。延迟的计算涵盖整个流水线:

  1. 事件在队列中的等待时间。
  2. 事件在重试队列中的等待时间(如果发生重试)。
  3. 消费者处理事件的时间。
    将所有阶段的耗时相加,得到总延迟并存入直方图。

基于此,定义延迟SLI的Prometheus查询如下:

  • 良好事件(延迟达标)sum(rate(consumed_item_latency_bucket{consumer="ranking", event="rank_item", le="10000"}[1m]))
    • 这里 le="10000" 表示延迟小于等于10秒的事件被认为是“良好的”。
  • 有效事件(总数)sum(rate(consumed_item_latency_bucket{consumer="ranking", event="rank_item", le="+Inf"}[1m]))

延迟SLI即为:延迟达标的事件数 / 总事件数

构建监控仪表盘

收集了指标和SLI后,是时候用自定义仪表盘来“调味”了。仪表盘面板可以包括:

  • 事件状态(可用性指标)
  • 消费者积压
  • 重试队列积压
  • 生产者到消费者延迟(P99延迟)

这些面板可用于日常SRE工作流。但更高效的方式是在此基础上引入SLO和告警系统。

3:装盘点缀——维护覆盖、合规性与进阶实践

上一节我们完成了SLO的定义和仪表盘的构建,本节我们将探讨如何维护SLO的覆盖与合规性,并介绍一些进阶实践。

SLO与告警

SLO是服务级别目标,代表了对客户期望的内部承诺。它设定了SLI在一段时间内的目标。例如:“99%的事件应在30天内于10秒内被处理”。其中,99%是SLO目标,30天是SLO周期,10秒是SLI指标(延迟)。

为了避免告警疲劳,我们采用基于错误预算消耗率的告警策略。

  • 错误预算:在服务超出合规性之前,允许累积的错误数量。公式为:错误预算 = 1 - SLO目标。例如,99.9%的可用性目标对应0.1%的错误预算。
  • 消耗率:服务消耗错误预算的速度相对于SLO周期的比率。消耗率为1表示预算将在30天周期内平稳耗尽;消耗率为2表示将在15天内耗尽。

我们采用多窗口多消耗率告警来减少误报。例如,可以组合设置:

  • 关键告警:在1小时窗口内消耗率达到14.4时触发(对应消耗2%的月错误预算)。
  • 警告告警:在2小时窗口内消耗率达到3时触发。

利用SLO指标

除了基础面板,我们还可以 harness SLO指标来获得更深入的洞察,例如:

  • 近7天性能:展示服务在过去7天的SLI表现趋势。
  • SLI成功率:直接展示可用性或延迟SLI的成功率。
  • 月度错误预算消耗率:展示30天周期内错误预算的消耗情况。
  • 基于错误预算的每日SLO表现:显示每日分配的错误预算以及服务当日的实际消耗对比。

这些可视化能清晰展示服务何时、因何(如部署)接近或违反SLO,帮助团队量化变更或事件对用户的影响。

进阶点缀建议

为使您的异步可观测性从基础升级为“ gourmet 体验”,建议尝试以下“点缀”:

  1. SLO分类助手与AIOps:利用AI处理初步告警,为工程师提供必要的上下文,实现“厨房预热”。
  2. 将SLO集成到CI/CD流水线:当SLO超出合规性或故障调查未在一定时间内解决时,自动阻止发布滚动。
  3. 定义覆盖与合规性标准:为整个组织制定策略,确保所有服务都满足可用性目标,并定义SLO应覆盖的流量比例和整体合规性要求。

实践收益

通过实施SLO,我们能够:

  • 在收到告警后,快速通过SLO仪表盘定位问题(如下游依赖部署)。
  • 根据影响评估,决定是自动回滚、通知产品负责人,还是进一步优化。
  • 量化特定变更或功能对用户的影响(如2.1%的收入影响),从而提升系统韧性并预防未来问题。

总结

本节课中我们一起学习了为异步流水线构建可观测性与告警的完整“配方”。我们从挑选最新鲜的指标开始,到烹制出有意义的SLO和仪表盘,最后讨论了如何装盘点缀以实现覆盖与合规性。伟大的可观测性如同一顿均衡的美食,需要正确的原料、恰当的技术和一丝创造力。通过实施能直接代表客户体验的异步流水线SLO,您将获得可操作的见解和可量化的影响,从而增强站点可靠性与客户满意度。请将这些技巧带回您的“厨房”,根据您的独特环境调整“调味”,烹制出让客户回头再次享用的可靠性大餐。

023:处理史上最大规模的域名迁移

概述

在本教程中,我们将跟随Squarespace工程师的视角,深入了解他们如何成功完成对Google Domains的收购与迁移。这是一次涉及超过900万个域名、数百万工作区席位,并需要在10个月内完成技术、产品和业务全面升级的复杂工程挑战。我们将学习域名系统的基础知识、大规模迁移的架构设计、性能优化策略以及如何在不影响用户体验的前提下完成如此庞大的系统整合。

域名基础知识 🧠

在深入迁移细节之前,我们需要理解一些域名领域的核心概念。这对于理解后续的技术挑战至关重要。

一个域名主要由两部分组成:点号左边的部分和点号右边的部分。

  • 顶级域名:位于点号右侧的部分,简称 TLD。例如 .com.net,或国家地区代码如 .uk.au,以及一些新潮的域名如 .shop.blog
  • 二级域名:位于点号左侧的部分,简称 SLD。这是客户在购买域名时可以自由选择的部分。将SLD与TLD组合,就得到了完整的域名。

接下来,我们快速了解域名生态中的四个关键角色,即“域名四R”。

以下是域名生态系统中的四个核心角色:

  1. 注册局:负责运营特定TLD的公司。例如,Verisign公司运营 .com.net 域名。全球所有的 .com 域名都归该公司管理。
  2. 注册商:注册局不直接向消费者销售域名。注册商与多个注册局合作,向消费者销售其TLD。当您去一家公司购买域名时,您接触的就是注册商。注册商受ICANN(互联网名称与数字地址分配机构)监管,需要遵守诸多规定,如运行WHOIS服务器、确保没有商标侵权、防止域名被用于网络钓鱼和欺骗等。
  3. 经销商:如果一个公司想销售域名,但不想承担注册商的合规责任,它可以成为经销商。经销商通过注册商提供的API在其平台上销售域名,而注册商仍然是背后的实际服务提供者。
  4. 注册人:即最终购买和使用域名的客户。注册人可以从经销商或注册商处购买域名。

迁移背景:收购的挑战 🎯

上一节我们介绍了域名的基础概念,本节中我们来看看这次收购项目启动时,Squarespace面临的起点和挑战。

故事始于2023年。当时的Squarespace作为一个域名提供商,形态还很初级。我们主要是一个网站建设平台,域名是作为网站服务的附属产品提供给客户的。

这意味着在Squarespace内部,域名被视为网站的子产品。客户管理域名需要进入其网站的设置页面。作为注册商,我们也非常年轻,仅支持 .com.net 两个TLD的直营销售,其他TLD均通过经销商模式销售。我们的客户也大多是新手用户,他们只希望域名能与网站配合工作,最多再购买一个邮箱,并不使用域名转发、邮件转发等高级功能。

团队小,产品也小。我们当时只有大约8名工程师、1名经理和1名产品经理。尽管我们系统内管理的域名数量已经不少,但我们的目标仅仅是服务好网站建设客户,并未刻意寻求扩张域名业务本身。

2023年4月,我从当时的CTO那里得知了可能收购Google Domains的消息,并且公司需要我来领导这次收购。在与当时的域名团队经理沟通后,我们克服了最初的震惊,开始思考Squarespace需要进行哪些改变才能确保这次收购成功。交易尚未公布,我们有一些准备时间。我们开始与全公司的首席工程师和资深工程师合作,因为我们知道,根据即将引入的新业务,我们的数据量或服务吞吐量可能会翻倍、三倍、四倍,甚至五倍。我们开始进行负载测试,修复代码瓶颈,并扩容部分数据库集群。

2023年6月,收购消息正式公布。当我们开始与Google Domains团队合作时,我们才真正了解到这次收购的具体内容:

  1. 域名:我们将转移超过900万个域名,这会使我们管理的域名数量翻两番。这些域名还附带了Google Workspace席位,约有100万个席位随之转移,这使我们管理的Workspace席位数量也翻了三倍。
  2. 经销商业务:Google Domains本身也是一家经销商。Google Workspace和Google Cloud会向其客户提供域名,并通过Google Domains的API来完成服务。
  3. 前端流量:Google Domains的前端网站将进行调整,用户无法再通过其注册新域名,这部分流量将被重定向到Squarespace。我们需要思考如何向这批新客户展示自己。

当时是6月,我们尚不清楚交易何时完成,但知道律师需要工作几个月。我们明确的是,一旦交易完成,我们将有10个月的不可变更时间线来完成所有相关工作流。

除了上述三点,这促使我们思考Squarespace内部需要在产品、系统等方面做出哪些其他改变。因为这不仅仅是转移域名,我们更希望随域名而来的客户能够留在Squarespace。

目标差距:我们需要成为什么?⚡️

我们了解了Squarespace域名业务的起点,现在来看看Google Domains是什么样子,以及我们需要追赶的目标。

客户选择Google Domains,是因为它是一个为各类域名客户(新手和高级用户)提供强大工具的域名提供商。这意味着Google支持许多Squarespace当时不具备的功能,例如域名和邮件转发、DNS GLUE记录等。同时,它也是一个成熟得多的注册商,通过其注册商支持超过300个TLD,而我们只支持2个。因为是Google,其系统内的域名数量是我们的五倍。

这带来了三个核心挑战:

  1. Squarespace需要成为一个支持所有类型客户的域名提供商。
  2. 我们需要构建一个用于经销域名销售的API。
  3. 我们需要打造一个以域名为中心的产品体验。

具体来说,这意味着:

  • 我们的注册商需要从支持2个TLD扩展到支持超过360个TLD
  • 我们需要支持Google Domains提供的所有功能,不能让客户失去他们依赖的功能。
  • 我们需要构建一个独立的、以域名为中心的产品。之前域名功能被隐藏在网站设置里,但对于来自Google的客户,他们是将我们视为新的域名提供商,而非网站提供商。我们需要一个能让他们管理甚至购买更多新域名的平台。

因此,虽然这听起来像是一次迁移,但实际上此次收购包含了六项并行的工作

  1. 构建将域名从Google迁移到我们的迁移引擎
  2. 构建支持Google Workspace和Cloud等大型提供商的经销商API
  3. 构建一个全新的前端网站,以建立Squarespace Domains品牌。
  4. 在我们的注册商系统中实现功能对等
  5. 在我们的产品中实现功能对等
  6. 在Squarespace内部定义独立的域名产品并建立品牌。

迁移引擎架构 🏗️

所有工作都在并行推进,但真正的“房间里的大象”是域名迁移本身。本节中,我们来看看迁移引擎是如何设计和工作的。

显然,Google和Squarespace需要紧密合作,因为我们的系统将开始通信。最终的方案是构建一个流式API。Squarespace提供一组REST API,接收Google告知的“已准备好迁移”的域名。我们在自己这边完成所有设置后,会通知Google。Google最后确认并清理其系统,将域名所有权正式移交给我们。

在Squarespace侧,我们决定构建一个事件驱动架构,因为许多系统需要并行设置。我们的迁移服务本质上是一个编排器,它在Squarespace的多个服务间进行协调:账户服务(需要为新客户创建账户)、多个域名服务(注册商、产品、DNS服务等)、网站服务、计费服务等。所有这些都尽可能并行发生。

审计日志记录是其中关键的一环。在迁移系统时,我们需要高度确信操作正确无误。我们需要能够核对Google发送的信息是否最终正确落地到我们的系统中,确保必要的功能已正确设置,客户没有丢失关键功能。同时,我们也需要为未来可能出现的客户疑问提供核查依据。

我们的一大目标是最小化对客户的影响,不希望他们失去对域名的访问权限超过必要时间。这意味着我们在后台进行大量准备工作。Google告知域名就绪后,我们开始设置。一旦耗时较长的步骤(如创建账户、设置账单信息、在域名系统中设置基础信息)完成,如果期间域名信息未变,我们就继续完成最终迁移。此时,域名会在Google侧被锁定,客户无法再更改任何信息。如果在我们的配置期间客户进行了更改,我们会重新开始流程。最终,我们将锁定窗口压缩到了10秒。在这10秒内,客户实际上是从Google Domains被切换到了Squarespace Domains。

实际上,这次迁移不仅仅是更换域名提供商。一个域名包含多个部分,因此发生了三次迁移

  1. 域名提供商从Google迁移到Squarespace。
  2. DNS提供商迁移(多数客户使用Cloud DNS)。
  3. 账单提供商从Google Payments迁移到Squarespace。

对于DNS迁移,我们与Cloud DNS团队合作,将DNS提供商账户直接从Google转移到Squarespace。在迁移开始前的一段时间内,双方都有权限访问DNS区域,之后切断Google的连接。

对于账单迁移,Squarespace使用Stripe作为计费提供商。为了安全,我们促成了Google的计费系统直接将账单信息传递给Stripe,然后Stripe向我们提供一个令牌,用于向这些客户收费。这样避免了敏感信息流经我们的系统。

迁移就绪与推进策略 📈

在上一节我们了解了迁移引擎的架构,本节中我们来看看如何判断一个域名“准备就绪”,以及我们如何规划和推进整个迁移过程。

我们通过“队列”的概念来控制迁移节奏。每个队列就像一组开关或闸门,对应一个功能是否就绪。每当一个功能准备就绪,我们就打开一个闸门。只有当一个域名的所有功能条件都满足时,它才能通过所有闸门,流入迁移引擎,最终被迁移。

这比听起来更复杂。要使一个域名就绪或一个功能可用,需要两件事同时完成:

  1. 我们的产品需要构建出该功能,服务要能支持它。
  2. 我们的迁移引擎需要知道如何设置该功能。如果设置错误,即使支持了也无法工作。

域名不仅仅是域名,它附带许多“功能”。我们需要支持的功能包括:

  • TLD支持:如果我们的注册商不支持 .uk,显然无法迁移 .uk 域名。
  • 产品体验:例如,我们需要为拥有多个域名但没有网站的客户设计管理体验。
  • 高级功能:如DNSSEC、域名转发、邮件转发、附加的Workspace等。

所有这些工作都在并行进行。大约在项目进行到第7个月(2024年4月),我们进行了一次检查点评估。此时倒计时还剩90天,但情况不容乐观。

查看迁移跟踪器,有两条线:

  • 蓝线:表示我们有资格迁移的域名数量。当时只有约120万个域名符合条件,距离900多万的目标很远。
  • 黄线:表示实际已迁移的域名数量。当时不到50万。

我们面临两个问题:

  1. 需要让更多域名尽快符合迁移条件。
  2. 如果域名符合条件,我们需要大幅提高迁移吞吐量。

我们决定采取两步走策略

  1. 保护迁移团队:给他们几周时间专注于修复瓶颈、优化速度,而不施加“更快”的压力。迁移以现有速度继续进行。
  2. 让所有功能团队全速前进:制定计划,按功能能解锁的域名数量排序,优先开发能解锁大量域名的功能。

通过与Google Domains团队深入分析,我们发现没有捷径。为了迁移绝大多数域名,我们必须构建所有功能,包括那些使用量很少的“长尾功能”。我们制定了计划:先实现能覆盖约700万个域名的功能(左列),再实现覆盖剩余200多万直至900多万域名的功能(右列)。

根据甘特图规划,要到5月底6月初才能让所有域名符合条件。这给了我们大约一个月的时间来完成全部迁移。假设我们速度足够快,时间还算充裕。

性能优化:让引擎飞起来 🚀

我们有了推进策略,但迁移引擎本身的速度是关键瓶颈。本节中,我们看看如何通过基础优化大幅提升迁移吞吐量。

在2024年4月,我们的迁移速度大约是 3.2个域名/秒。以这个速度计算,迁移100万个域名需要每天运行18小时,连续5个工作日。要迁移900万个域名,则需要连续运行9周。在6月初才开始全力迁移的情况下,我们没有9周的容错时间。我们必须更快。

好消息是,我们不需要什么特殊技巧,只需回归基础优化。我们构建的是事件驱动架构,因此首先检查了Kafka配置:分区数量是否合理?是否充分利用了分区?消费者数量是否足够?我们发现有时分区涌入大量数据,但消费者处理不过来,于是进行了扩容。

另一个例子是关于域名转发。我们测试时使用的域名通常只有5-10条转发规则。但实际迁移中,我们发现有些客户有数百甚至数千条规则。最初,迁移引擎为每条规则单独调用一次API,导致大量延迟。我们开始对请求进行批量处理,批量发送给API,并进一步探索批量读写数据库。

我们还做了显而易见的事:仔细检查了所有数据库的索引,看是否有缺失。由于涉及众多服务,这是一项跨团队协作的工程。

仅仅通过这些看似基础但当时被忽略的优化,我们在三周内将吞吐量提升到了 9个域名/秒。这使得迁移100万个域名只需每天运行10小时,连续3个工作日。全部迁移工作预计可在三周半内完成。这让我们信心大增。

我们甚至一度将峰值提升到了 12个域名/秒,这意味着我们有能力在一天内迁移100万个域名

并行挑战:经销商API与最终成果 🏁

在迁移引擎飞速运转的同时,另一项重大挑战——经销商API也在同步推进。本节我们来看看这方面的进展以及整个项目的最终成果。

我们在2024年4月初启动了经销商API,并逐步接收来自Google Workspace和Google Cloud的流量。这非常令人兴奋,因为我们直接嵌入了Google Workspace的结账流程中。

根据协议,我们需要遵守与Google Workspace之间的SLA(服务等级协议),要求是三个9(99.9%) 的可用性。这对我们来说要求很高,最初三个月我们并未达标。原因有几个:首先,API是从零构建的,我们有很多需要学习的地方;其次,域名业务,特别是注册商服务,是一个高度依赖外部系统的分布式系统。我们依赖的注册局会有维护窗口和计划外中断,这些都影响我们的SLA。我们必须构建更具弹性的架构来满足要求。从9月开始,我们成功达到了目标。

10个月的期限到了,7月来临。回顾我们的两步走策略:

  • 蓝线(符合条件域名):我们非常有效地每周都让更多域名符合条件,直到5月底,所有域名都具备了迁移资格。
  • 黄线(实际迁移域名):可以看到,在6月初的蓝色区域,迁移活动非常密集。特别是在6月的第二周,我们在一周内迁移了250万个域名。值得注意的是,在收购之前,Squarespace总共才管理着200万个域名。我们一周的迁移量就超过了原有总量,且没有对系统造成任何中断或对客户产生影响。

最终,我们成功迁移了100%的域名(除了少数使用动态DNS等复杂功能、我们因时间不足而无法无损迁移的客户)。我们迁移了超过900万个域名,为经销商API实现了三个9的SLA,并在平台上支持了超过360个TLD。

总结

在本教程中,我们一起学习了Squarespace团队如何应对史上最大规模的域名迁移工程。我们从域名基础知识入手,了解了收购带来的多重挑战:包括技术架构上需要构建事件驱动的迁移引擎和经销商API,产品上需要实现数百项功能对等并打造独立品牌,以及工程上如何通过优化Kafka配置、批量处理、数据库索引等基础手段,将迁移吞吐量提升数倍,最终在紧迫的时间内成功完成了超过900万个域名的平滑迁移,并实现了高标准的服务可用性。这次成功的核心在于跨公司团队的紧密协作、清晰的阶段性策略以及对“最小化客户影响”这一核心目标的不懈追求。

024:驯服野兽——理解并驾驭HTTP反向代理的力量 🦁

在本节课中,我们将学习HTTP反向代理的核心概念、其优势与挑战,并探讨如何通过有效的工具和策略来管理和调试它们,以确保系统的稳定性和可观测性。

概述

HTTP反向代理是现代网络架构中的关键组件,它位于客户端和后端服务之间,处理HTTP请求和响应。它们功能强大,但也可能引入复杂性。本节将深入探讨其工作原理、常见陷阱以及最佳实践。

关于演讲者与Varnish

首先,简单介绍一下我自己。我是一名拥有15-20年经验的开发者,目前担任解决方案工程师。我的工作重点是帮助SRE解决他们的问题。我主要使用C语言,并且偏好Arch Linux。

接下来,谈谈Varnish。这是一个始于2006年的开源项目,非常“老派”。我们的首席架构师是FreeBSD内核开发者,我们每周仍在IRC上交流。Varnish最初是作为Squid的替代缓存引擎而设计的,虽然Squid至今仍在,但Varnish在功能上已经大大扩展。与Nginx等工具不同,Varnish通过提供一种编程语言(VCL)来配置,而非简单的开关,这赋予了用户完全的控制权。此外,我认为我们拥有最好的日志系统。

什么是HTTP反向代理?

当我们谈论HTTP反向代理时,指的是一个广泛的范畴。它涵盖了从HTTP/0.9到HTTP/3(QUIC)的协议。常见的术语如“第7层负载均衡器”、“Kubernetes Ingress”、“API网关”等,本质上都属于HTTP反向代理。

我们可以将其视为一个通用实体,可能是一台服务器、一个容器、一个集群,甚至是像CDN这样的大规模服务。它接收客户端的HTTP请求,为了生成响应,会向后端(或称源站)发起零次或多次HTTP请求。这可以用于内容组合、重试等目的。

HTTP反向代理的优势 🚀

我们不太关心底层的以太网或电缆,我们关心的是在机器上能看到的层面。在应用层,我们有各种构建在HTTP之上的API(视频、存储、DNS等)。HTTP本身是一个简单、基于文本的请求-响应协议,易于理解,并且几乎所有编程语言都能轻松构建HTTP反向代理。

正因为如此,HTTP反向代理能做很多事情。以下是几个基于Varnish的例子(其他系统也类似):

  • 动态压缩:如果后端存储的内容未压缩,Varnish可以即时压缩后再缓存,节省内存/磁盘空间和CPU。
    // VCL示例:在收到后端响应头时触发压缩
    sub vcl_backend_response {
        if (beresp.http.content-type ~ "text") {
            set beresp.do_gzip = true;
        }
    }
    
  • 速率限制:轻松实现API限流,例如限制每10秒15次请求。
    // VCL示例:使用模块进行速率限制
    import ratecounter;
    if (ratecounter.is_denied(client.ip, 15, 10s)) {
        return (synth(429, "Too Many Requests"));
    }
    
  • 内容重写:在测试环境中,可以使用正则表达式替换HTML中的链接。

HTTP反向代理的挑战与陷阱 ⚠️

然而,HTTP反向代理也有不少问题。网络就像洋葱,有层次,而且会让你流泪。应用层协议(绿色部分)是大家共识且运作良好的;越往上走(红色部分),情况就越混乱。关键点是:要到达上层,必须经过下层。

这带来了一些陷阱:

  1. IP地址欺骗:一个常见的配置是只允许特定IP(如localhost)进行缓存清除(PURGE)。但如果前面还有另一个反向代理(如Nginx),并且它使用自己的本地IP向后端发送请求,那么任何通过该Nginx的请求都将被授权清除缓存。解决方案是让前置代理将真实的客户端IP放入一个头部(如X-Real-IP),然后让Varnish根据这个头部进行判断。
  2. 透明缓存限制:我们无法“透明地”缓存像YouTube、Facebook这样的网站。因为要看到HTTP内容并进行缓存,必须经过TLS层。而你无法冒充这些网站,因为你没有它们的证书链。
  3. 工具抽象泄漏:像curl这样的工具会在不同命令中做出细微但重要的差异。所有的抽象都是不完美的,HTTP在近40年的历史中,一直不善于清晰分离数据与逻辑,加上遗留实现,使得问题非常复杂。
  4. 调试歧义:在调试REST API时,你可能会遇到HTTP状态码是200(成功),但JSON消息体却返回错误信息的情况。这时,双方可能都没错,只是没有明确是在哪个层面(传输层 vs. 应用层)进行沟通。
  5. 下层协议中断:一个真实案例中,客户在Varnish前使用了F5负载均衡器。某些用户请求会失败,Varnish日志显示F5在收到大约4KB的头部数据后丢弃了连接。问题是,F5自身没有相关日志。Varnish实际上只是暴露了早已存在的问题(当Varnish不存在时,浏览器根本收不到任何错误响应)。这说明了由于上层(HTTP)问题导致下层(TCP/IP)连接被破坏的情况。

如何应对挑战:工具与策略 🛠️

上一节我们看到了HTTP反向代理可能带来的问题,本节中我们来看看如何利用工具和策略来有效管理和调试它们。

首先,一个强大的工具是 varnish-gather。这是一个通用的Shell脚本,能运行数百条命令来收集机器的完整状态信息,并打包发送以供离线分析。它能极大减少排查问题所需的来回沟通时间。即使你已有完善的仪表盘和告警,varnish-gather所收集的数据广度也往往是独特的。

那么,具体如何应对这些“尖刺”呢?

  1. 深入定位问题:当收到“我的网页不工作”这样的报告时,要不断向下钻取。具体是页面中的哪个对象?哪次传输失败了?将问题范围缩小到可以构建的特定请求上。你不需要了解所有协议细节,但要知道它们的存在,并善用搜索工具。
  2. 善用协议调试工具
    • 对于HTTP,curl -v(详细模式)是一个极佳的工具。它可以展示DNS、TLS和HTTP的完整过程。参数--connect-to允许你强制将请求发送到特定IP,而不影响TLS分析。
  3. 利用分层日志:Varnish的日志系统记录一切,但本身不做处理。它将事件日志记录到内存的环形缓冲区中,由其他工具读取和格式化。这提供了巨大的信息量,就像一个事件日记,记录了请求/响应的每一步状态变化。
    • varnishlog:提供最详细、最即时的信息,适合深度调试。
    • varnishlog -j:输出JSON格式的日志,便于集成到可观测性平台。
    • varnishncsa:生成类似Apache NCSA格式的单行日志,适用于计费、审计等需要明确格式的场景。
    • 指标(Metrics):从另一个维度讲述相同的故事。

关键点:你不需要永久记录所有内容。可以根据目的进行采样和设置不同的保留策略。允许数据存在重叠是可以的,随着时间的推移,重叠部分会自然衰减。重要的是要有针对性。

  1. 前后端关联:Varnish的一个强大之处在于它能同时记录客户端请求和后端请求。一次客户端HTTP请求通常对应两次HTTP事务。如果前后端日志不一致(如之前F5的例子),猜猜谁会背锅?关联前后端日志至关重要。
  2. 实施追踪(Tracing):在现代分布式系统中,一次客户端请求可能会触发无数下游HTTP调用。追踪(Tracing) 因此变得非常重要。你不需要等到完全实现OpenTelemetry才开始。即使只是在边缘的第一个反向代理中生成并传递一个UUID头部,也能让你在日志中关联整个调用链。可以参考W3C的Trace Context规范。请注意,网络设备(电缆、交换机)通常不参与追踪,因此需要两端都报告才能拼凑出完整的网络路径。

Varnish与OpenTelemetry实践 🌐

最后,我想展示一下Varnish与OpenTelemetry的集成。在一个演示环境中,客户端请求Varnish,Varnish会向两个不同的后端发起请求以组合内容,并可能进行重试。虽然基础设施看起来简单,但实际的调用链(瀑布图)可能相当复杂。

通过集成OpenTelemetry,Varnish能够生成详细的追踪跨度(Span),并在Grafana等工具中可视化。这使我们能够清晰地看到Varnish内部的所有操作(包括重试、后端调用),而不仅仅是最终结果。这种深度可观测性比一个只简单报告“收到请求”的黑盒服务器要有价值得多,它赋予了运维人员处理信息的能力。

总结

本节课中,我们一起学习了HTTP反向代理的核心概念。我们认识到它既是功能强大的工具(可用于缓存、限流、压缩等),也可能引入复杂性(如IP欺骗、调试困难、抽象泄漏)。为了有效驾驭它,我们需要:

  • 使用像varnish-gathercurl -v这样的专业工具进行深度调试。
  • 建立分层、有目的的日志记录策略,并关联前后端日志。
  • 积极实施追踪(即使是简单的UUID传递),以获得分布式系统下的全链路可见性。
  • 理解网络分层模型,并意识到问题可能跨层出现。

通过结合正确的工具、清晰的策略和对系统层次的深刻理解,我们就能“驯服”HTTP反向代理这头“野兽”,充分发挥其威力,同时确保系统的稳定与可观测性。

025:混沌实验——数据中心压力测试 🧪

在本教程中,我们将学习如何在一个大型金融机构(USAA)中,从零开始构建并持续演进一套数据中心级别的混沌实验体系。我们将涵盖其技术实现、监控策略、自动化流程,以及克服文化、人员和流程障碍的关键经验。

历史背景与演进之路 🕰️

上一节我们介绍了本教程的概述,本节中我们来看看混沌实验在USAA的起源。这一切始于多年前,当时团队的目标是从单一数据中心模型迁移到双活数据中心架构,以提升系统韧性。研究发现,80%被迫执行灾难恢复计划的企业会在五年内破产,这凸显了主动验证系统韧性的重要性。

最初的测试并非针对整个数据中心,而是从一个名为WebSphere的Java中间件应用服务器开始。团队从最简单的单元开始测试,逐步扩大规模。

以下是测试规模逐步扩大的过程:

  • 单JVM测试:从单个数据中心的单个JVM(Java虚拟机)开始,观察其对响应时间、CPU等指标的影响。这有助于应用级别的容量规划。
  • 单节点测试:随着信心增强,测试扩展到整台服务器节点故障,这有助于验证虚拟化策略和应对物理服务器故障。
  • 多节点/单Cell测试:更激进地移除一个Cell(通常由多个节点组成)中的多个节点,以测试局部容量损失下的系统行为。
  • 50%容量测试:最终,团队能够安全地移除单个数据中心内一半的计算容量。

一个重要的经验是:始终寻找并记录测试预期目标之外的其他收益。例如,这些测试数据为后续按业务线重构应用部署架构提供了宝贵的容量规划依据。

全数据中心故障转移测试 🏗️

上一节我们回顾了测试的演进历程,本节中我们来看看如何将测试规模扩大到整个数据中心。为了实现将全部用户流量切换到单一数据中心的“疏散”测试,需要进行一些工程化改造。

核心机制是通过内容分发网络(CDN)的流量钉扎功能,逻辑上将用户分组并引导至指定数据中心。测试采用渐进式策略,从单个用户组开始,逐步增加范围和信心。

测试范围不仅包括外部用户流量,也必须涵盖内部系统间流量。对于非双活架构的系统,测试验证了其客户端在跨地理距离调用时能否处理增加的延迟。这种“疏散”模式在实际发生重大故障时,是快速隔离问题、进行诊断和恢复的有效手段。

需要注意的是,对于主备架构的系统,我们不在混沌测试中对其进行故障转移,而是要求其团队自行安排定期的故障转移测试。有趣的是,许多团队会利用我们进行数据中心级测试的窗口期,同步执行他们的故障转移演练,以模拟最坏情况。

工程化与自动化 🤖

上一节我们介绍了全数据中心测试的模型,本节中我们来看看如何通过工程化和自动化来安全、高效地执行它。在SRE团队成立并接手该流程后,首要任务就是消除手动操作带来的繁琐和人为错误风险。

我们构建了一个完整的CI/CD流水线来编排所有测试任务。流水线集成了关键的安全与回滚机制。

以下是自动化流水线的核心组成部分:

  • 预检查:最初检查计算和中间件层的容量与服务状态。后来升级为检查服务等级目标(SLO)的燃烧率警报,确保测试开始时没有活跃的短期警报。
  • 自动化执行:按计划执行流量切换等一系列步骤。
  • 自动化回滚:如果触发了预设的SLO燃烧率警报,测试会自动回滚,通常在几分钟内完成。
  • 手动回滚按钮:作为安全网,提供“紧急停止”按钮,用于在自动化未触发时手动回滚。
  • 应急预案:为每个自动化步骤准备了应急预案,以便在失败时快速联系对应团队手动处理。

我们坚持只使用SLO燃烧率警报作为自动化回滚的触发条件,而非普通的服务等级指标(SLI)警报。这一决策在当时有力地推动了SLO监控模式在全公司的采纳。设置回滚标志的服务所有者必须承诺:修复问题,并将触发的警报升级为事件进行处理。

监控策略的演进 📊

上一节我们探讨了自动化执行,本节中我们来看看如何监控这些高风险的实验。监控策略随着测试范围的扩大而不断演进。

最初,我们关注JVM级别的指标(CPU、堆内存、GC、响应时间)。当SRE团队开始负责全数据中心测试时,为了消除领导层的顾虑并确保测试可控,我们构建了一个关键服务全景监控仪表板,作为“单一事实来源”。尽管这并非典型的SRE实践(SRE更倡导由开发团队负责监控),但在当时是必要的。

如今,我们的监控重心已转移到SLO燃烧率警报上。在测试期间,我们会密切关注这些警报的触发情况。正常时可能只有零星警报,但一旦出现底层问题,就会引发“圣诞树效应”——大量依赖服务同时告警。

我们进一步将监控数据整合到内部的状态页中,为业务伙伴提供其服务组合运行状况的快速视图。未来的目标是向外部用户公开状态页。

文化、人员与流程挑战 👥

上一节我们介绍了技术层面的监控,本节中我们来看看实施此类测试所面临的最大挑战:非技术因素。我们的大部分时间和精力都花在了克服文化、人员和流程的障碍上。

首要建议是:与现有问题管理流程对接。早期我们依赖个人关系和“交易”来推动问题解决,导致问题积压失控。后来通过与问题管理团队协作,并利用自动化将常见优化项(如JVM参数调优)嵌入标准部署流程,才得以有效管理。

必须利用一切可能的杠杆来处理发现的问题。在我们的文化中,可用性是驱动行动的首要指标。我们通过指出未解决的问题如何影响可用性目标来推动修复。考虑到每周有2000-3000次变更,任何延迟测试都可能积累风险。

大型问题常常会阻碍测试的进行。应对策略包括:将触发回滚的问题正式纳入事件和问题管理流程;推动跨团队协作与问责制,而非指责;将问题及其状态定期呈报给领导层;在测试因故长期无法执行时,利用“事后复盘”会议指出测试可能预防的故障。

用数据而非情绪说服他人至关重要。例如,通过分析变更量与测试间隔的关联,展示长时间不测试与故障增加的相关性。我们为每次测试都保留事后复盘记录,建立了一个可搜索的“经验库”,用于发现“冒烟”(潜在问题)的迹象。

赋能与知识传递 🧠

上一节我们讨论了处理问题和推动文化的策略,本节中我们来看看如何通过赋能团队来扩大影响、减少知识缺口。早期,SRE团队深度参与每个问题的诊断和修复方案制定,负担沉重。

为了将应用级监控和韧性建设的知识普及,我们启动了一个“结对观测”项目。在测试期间,邀请应用团队与SRE坐在一起,分享监控技巧和“部落知识”。这项投入回报丰厚,极大地提升了工程社区的参与度和热情。

一个成功案例是帮助一个内部短链接服务团队。他们无法在测试环境复现性能瓶颈。通过共同设计混沌实验、增加合成负载,我们发现了其数据库连接未使用连接池,几乎导致端口耗尽的严重缺陷。这次提前发现避免了可能影响数十个关键应用的生产事故。

此外,我们还创建并分享了一个容器化的本地性能测试样板项目。开发者可以在本地工作站运行完整的监控栈和性能测试场景,并将其集成到自己的CI/CD流水线中。该项目还指导开发者如何获取和分析Java堆转储以定位瓶颈。这套工具显著提升了团队自主进行性能测试和调优的能力。

克服恐惧、不确定性和怀疑 🛡️

上一节我们探讨了赋能团队,本节中我们来看看实施混沌实验的头号障碍:FUD。对抗FUD所花费的精力一度超过测试本身的工程工作。

领导层教育至关重要。获得最高管理层(如CIO)的理解和支持,是抵御各方质疑的基石。

持续用数据说话。当遇到“这太冒险了”或“我的应用很特殊”等质疑时,要求对方提供具体数据来支撑其观点,往往能使担忧消散。有趣的是,业务部门有时会从反对变为主动要求测试,例如在预期业务高峰(如飓风季)前来验证系统承载力。

展示价值证明。我们保存了每次测试的复盘记录和问题追踪数据,可以列举无数个“避免了重大故障”的案例。我们还引入了一个韧性信心指数,该指数会根据上次成功测试时间等因素计算。当指数下降时,人们反而会开始呼吁进行测试。

SLO自动回滚机制成为了“安全毯”,极大地减轻了团队的压力,也增加了各方对测试安全的信心。但也要注意,它可能被某些团队当作“拐杖”,从而容忍更高的风险或降低质量门槛,这是我们正在关注的问题。

利用未来的高风险活动作为推动力。例如,在进行大规模生命周期升级(如操作系统迁移)时,负责该升级的领导层反而会成为混沌测试的积极倡导者,因为他们需要“数据中心的救生艇”作为保障。

未来展望 🚀

上一节我们分享了克服阻力的经验,本节中我们来看看混沌实验的未来发展方向。经过超过15年的演进,这项实验已被视为“常规操作”,这是一个巨大的文化胜利。

历史证明,这种从小到大的混沌实验顺序是有效的,它成功帮助我们度过了一次真实的数据中心中断事件。近期一次测试中暴露的问题,甚至揭示了一个存在多年、每次生产部署都会发生但被忽略的缺陷。

未来,我们希望:

  1. 应用级故障转移测试发展,并引入企业级混沌工程平台,让各团队能自主规划执行隔离的混沌实验。
  2. 测试日程更加灵活,覆盖多个日期,甚至包括业务高峰日,以验证任何情况下的单数据中心运行能力。
  3. 将测试范围扩展到混合云/多云环境,例如测试某个SaaS服务或公有云区域故障的影响。

总结 📝

本节课中我们一起学习了在USAA构建数据中心级混沌实验体系的完整历程。我们从技术层面了解了测试的演进、自动化流水线的构建以及监控策略的升级。更重要的是,我们深入探讨了在实施过程中遇到的文化、人员与流程挑战,以及通过领导层支持、数据驱动、赋能团队和建立安全网等策略来克服恐惧、不确定性和怀疑的有效方法。

最终要义是:混沌实验的价值无法估量,但它的实施无法一蹴而就。它需要激情、坚持,以及灵活运用技术与非技术手段来持续推动。希望我们的经验能为你的混沌工程之旅提供有益的参考。

026:以玩家为中心的可用性度量——Riot Games如何改变其文化

概述

在本节课中,我们将学习Riot Games如何从一个缺乏统一可用性度量标准的公司,转变为建立一套以玩家体验为核心的可用性度量体系。我们将了解他们面临的挑战、设计的解决方案,以及这一变革如何重塑了公司的文化和响应机制。


章节 1:背景与挑战

上一节我们概述了课程内容,本节中我们来看看Riot Games在2020-2021年面临的困境。

Riot Games是一家以《英雄联盟》、《无畏契约》等竞技游戏闻名的多游戏工作室。其核心文化是“玩家至上”。然而,在2020年疫情初期,玩家数量激增,公司从单一游戏转向多游戏运营,运维团队压力巨大,出现了严重的倦怠问题。

一个根本性的问题是:Riot Games运行了超过10年,却没有一个统一的、标准的游戏可用性度量数字。公司领导层无法获得关于某天、某周或某月可用性的一致报告。对于一个信奉“玩家至上”的公司来说,这是一个严重的问题。

这种“玩家至上”的文化,在事件管理过程中反而成了一种阻碍。虽然能轻易召集30人处理问题,但很难确保召集的是“正确的”30人。公司的“完全责任制”文化导致团队只专注于自己负责的服务,而非整个游戏体验。这造成了一种“合理推诿”的环境,没有人对玩家的整体游戏体验负责。

因此,Riot Games的CTO给新成立的运维团队设定了一个目标:在12个月内,定义并实施一个全公司认可的可用性度量标准。这个标准必须得到游戏高管领导的认可,他们必须相信这个数字,并认为它能影响其产品。


章节 2:放弃技术方案,回归基本原则

上一节我们介绍了Riot面临的度量挑战,本节中我们来看看他们为何放弃了纯粹的技术解决方案。

团队最初的想法是建立一个技术仪表板,展示所有800多个服务的红绿状态,并按依赖关系汇总到游戏层面。然而,这条路被证明行不通。

原因在于

  1. 度量标准不统一:由于“完全责任制”,每个服务都以不同的方式度量其可用性。
  2. 改造成本过高:统一所有服务的度量方式需要改造整个技术栈,预计需要两年时间才能达到80%的采用率,而团队只有12个月。
  3. 不符合玩家视角:玩家不关心单个服务是否宕机。他们关心的是能否在想要的时候,以想要的方式与游戏互动。

因此,团队决定放弃从技术栈入手的方案,回归基本原则,利用现有资源快速构建度量体系。

以下是他们拥有的关键资产

  • 强烈的责任感文化:可以加以利用。
  • 告警系统:虽然嘈杂,但能发现问题。
  • 出色的事件报告:团队擅长记录和追踪事件。
  • 玩家支持团队:作为后备,能快速反馈玩家问题。
  • 卓越的实时指标:特别是实时并发玩家数(CCU) 数据,这是公司业务的核心,所有人都信任。

团队意识到,或许可以利用事件期间捕获的数据来构建SLO,因为他们确切地知道在事件发生时服务出现了故障。


章节 3:建立共同语言:事件优先级

上一节我们确定了利用现有数据构建度量的方向,本节中我们来看看建立共同沟通框架的第一步——定义事件优先级。

在“玩家至上”的文化中,生产环境的任何问题都可能被视为紧急事件。团队需要一种共同语言来讨论影响,而不是争论技术分类。

他们摒弃了基于服务“关键/非关键”的分类方式,因为这种分类常引发“我非关键为何被呼叫”的争论,耽误故障恢复。

新的优先级体系完全基于对玩家的影响程度

以下是新的优先级定义:

  • 优先级 1(P1):影响 50% 或以上玩家基数。这被视为严重失败。
  • 优先级 2(P2):影响 15% 至 50% 的玩家基数。
  • 优先级 3(P3):影响 1% 至 15% 的玩家基数。
  • 优先级 4(P4):影响 少于 1% 的玩家基数。

这种分类方式将对话焦点从“哪个服务坏了”转移到“多少玩家受到了影响”,为整个公司提供了讨论问题严重性的通用框架。


章节 4:定义玩家体验类别

上一节我们统一了事件严重性的衡量标准,本节中我们进一步细化,定义玩家体验的具体问题类别。

仅知道影响了多少玩家还不够,还需要知道影响了玩家的“什么”体验。团队将所有游戏问题归纳为三个核心类别,每个类别下再细分:

以下是三个核心玩家体验类别及其子类

  1. 连接游戏:包括登录、更新游戏。如果失败,玩家根本无法进入游戏。
  2. 商业交易
    • 支付处理:使用信用卡购买虚拟货币。
    • 内容获取:使用虚拟货币购买皮肤等内容。
  3. 核心游戏体验(最复杂,共识为6个子类):
    • 匹配:与其他玩家组队。
    • 游戏进行:游戏内体验,如断线、崩溃。
    • 终局奖励:游戏结束后获取奖励。
    • 聊天与语音:玩家间的交流。
    • 组队:与朋友一起游戏。
    • 库存/账户信息:读取玩家的账户数据和资产。

通过结合优先级(影响范围)体验类别(影响内容),团队能够用简洁的语言描述任何事件。例如,“韩国、大洋洲和欧洲《无畏契约》的匹配和库存读取出现P1事件,持续60分钟”,这足以让了解此框架的人明白问题的严重性。


章节 5:灵感来源与度量公式设计

上一节我们建立了描述问题的语言,本节中我们来看看如何将这些数据转化为一个可信的可用性度量数字。

团队需要设计一个SLO(服务水平目标)。传统的SLO基于错误率或延迟,但Riot缺乏统一的技术栈来实现这一点。他们从电力公司获得了灵感:电力公司通过“用户供电分钟数”来度量可靠性,少数用户短时间停电不会影响整体SLO,但大规模停电就会。

Riot拥有类似的实时数据:并发玩家数(CCU)。团队提出了一个创新概念:服务玩家分钟数

度量公式的核心思想如下

  1. 总服务玩家分钟数(预期):将每个区域、每个游戏每分钟的CCU相加,得到一个月内预期服务的总玩家分钟数。这通过计算滚动平均值来预估任何时间点应服务的玩家数。
    • 总预期分钟数 = Σ(所有区域所有游戏每分钟的CCU)
  2. 受影响玩家分钟数:根据事件报告,估算在事件持续期间,受影响的玩家比例所对应的玩家分钟数。
    • 受影响分钟数 = 事件持续时间 × 受影响玩家比例 × 该时段预期CCU
  3. 可用性百分比
    • 可用性 = (总预期分钟数 - 受影响分钟数) / 总预期分钟数 × 100%

这个公式的优势在于:

  • 加权影响:高峰时段的事件比低谷时段影响更大。
  • 区域平衡:大区的事件比小区影响更大。
  • 基于可信数据:CCU数据是公司业务的基石,无人质疑。
  • 玩家视角:直接反映了玩家体验的聚合质量。

章节 6:目标设定与报告呈现

上一节我们设计了度量公式,本节中我们来看看如何设定目标并将结果呈现给组织。

设定一个现实的目标至关重要。由于从未测量过,团队不知道实际数字。设定一个明知无法达到的目标(如99.99%)会让人放弃。经过讨论,他们设定了第一个目标:99%。对于服务着每月超2000亿玩家分钟数的公司,即使1%的缺失也意味着巨大的影响。

月度报告的核心是一份“热力图”,以游戏分片为横轴,玩家体验类别为纵轴,直观展示哪些区域、哪些功能出现了问题。报告还包含一个执行摘要,汇总全局可用性百分比,并指出哪些分片未达标。

例如,一份报告可能显示《英雄联盟》全球可用性为98.97%,16个分片中有13个达标,问题集中在其中3个分片。这种呈现方式让领导层一目了然地看到问题所在,并基于此采取行动。


章节 7:实施、推广与文化影响

上一节我们看到了报告的形式,本节中我们来看看这个方案如何被推广及其带来的深远文化影响。

在12个月期限的第9个月,团队已经能够可靠地生成报告。接下来的挑战是让全公司接受它。

推广的关键步骤

  1. 获得CTO支持:CTO亲自参与,向各游戏高管团队传达此度量标准的重要性,并暗示其将与绩效挂钩。
  2. 与所有技术负责人沟通:向50-100个开发团队的负责人解释,可用性报告将不再基于他们各自的服务度量,而是基于玩家体验。
  3. 与目标挂钩:从2022年开始,每位游戏负责人的关键绩效目标(OKR)中都包含“达到99%可用性”。

实施后的显著成果

  • 文化转变:讨论焦点从“谁的服务故障”转向“玩家的什么体验受损”。
  • 运维团队士气飙升:团队看到了自己的工作和建议能驱动公司资源投入解决问题,士气调查分数从极低跃升至接近满分。
  • 成立SRE团队:由于度量体系成功,CTO批准了预算,正式建立SRE团队。
  • 组织规模扩大:根据康威定律,更多专注于 resiliency(弹性)的工程团队被划归运维部门管辖。
  • 问题负责人制度:游戏产品负责人开始任命“负责人”来专项处理登录、匹配等特定体验领域的问题。

章节 8:成功的关键要素

上一节我们回顾了变革带来的积极影响,本节中我们来总结一下,如果你想在自己的公司推行类似的变革,需要哪些关键要素。

演讲者被问及最多的问题是:为什么这个方案能成功?需要什么前提条件?

以下是成功的关键要素

  1. 高层强制授权:必须有一位有足够权威的高层领导(如CTO)自上而下地推动,并能够将目标与激励机制(如奖金、绩效)挂钩。没有这个,任何报告都只是空谈。
  2. 基于可信的核心数据:度量必须建立在公司内部公认准确、无可争议的数据之上。对Riot来说,就是CCU数据。
  3. 具备能力和信誉的团队:执行团队必须深入了解公司业务、数据陷阱和组织内的人际关系。空降的外部专家很难获得信任。
  4. 允许试错的氛围:这不是“不成功便成仁”的任务。需要有“如果这个方法不行,我们就尝试另一个”的心态。
  5. 清晰的玩家视角:整个度量体系和沟通语言必须围绕用户/客户体验构建,让所有角色(工程师、产品经理、高管)都能直观理解。

总结

在本节课中,我们一起学习了Riot Games如何通过聚焦“玩家体验”,利用现有的、可信的并发玩家数据,构建了一套独特的可用性度量体系。他们放弃了统一技术栈的艰难道路,转而从定义共同语言(事件优先级、体验类别)入手,设计了以“服务玩家分钟数”为核心的SLO公式。这一变革不仅产出了一个可信的度量数字,更重要的是,它彻底改变了公司讨论和响应可用性问题的文化,提升了团队士气,并驱动了更有效的资源投入。其成功的关键在于高层的强力支持、对核心业务数据的巧妙利用,以及一个深谙公司内部运作的、值得信赖的推行团队。

027:请还我网线!AWS中的网络限制剖析

概述

在本教程中,我们将深入探讨AWS(亚马逊云科技)中网络性能的各类限制。我们将从基础的网络带宽和每秒数据包数限制讲起,逐步深入到连接跟踪、数据包分片等高级主题。通过学习,您将理解这些限制如何影响您的云上应用,以及如何监控、诊断和优化网络性能,确保您的服务在面对突发流量时依然稳定可靠。


网络带宽与数据包速率限制

上一节我们概述了课程内容,本节中我们来看看AWS网络最基础的两个限制:带宽(Gbps)和每秒数据包数(PPS)。

AWS实例的网络性能受两个关键指标限制:

  1. 带宽限制:以吉比特每秒(Gbps)衡量,例如15 Gbps。
  2. 数据包速率限制:以每秒数据包数(PPS)衡量,例如125万PPS。

这两个限制共同作用。带宽限制是硬性上限,无论数据包大小如何,吞吐量都不会超过此值。数据包速率限制则是一个隐含限制,其数值通常被设定为:在使用1500字节标准数据包时,刚好能达到标称的带宽上限。

核心公式

理论最大吞吐量 (Gbps) = min(标称带宽, 实际PPS * 平均数据包大小 * 8 / 10^9)

这意味着,如果您的应用发送的数据包平均尺寸较小,即使未达到带宽上限,也可能触及PPS限制,从而导致实际吞吐量远低于预期。例如,一个标称15 Gbps的实例,在处理平均500字节的小数据包时,实际吞吐量可能只有约5 Gbps。

以下是AWS不同实例系列的典型网络规格对比:

  • 通用型实例:网络带宽相对较低,例如m7g系列最大为30 Gbps。
  • 计算优化型实例:通常提供更高的网络限制。
  • 网络增强型实例:名称中带有“n”,专为高网络吞吐量设计,可提供高达200 Gbps的带宽。

监控网络限制

了解了限制的存在后,我们需要知道如何监控它们。AWS提供了专门的指标来指示实例是否触及了网络限制。

自2021年起,AWS引入了网络“额度”指标。当实例的网络流量超过其允许的限制时,相应的计数器就会增加。这些指标包括:

  • BandwidthAllowanceExceeded(带宽额度超限)
  • PacketRateAllowanceExceeded(数据包速率额度超限)

重要提示:这些指标仅表示数据包在Nitro虚拟网卡的队列中曾被延迟(排队),并不直接等同于丢包。数据包可能随后被成功发送,也可能被丢弃。

获取这些指标需要一些配置工作,因为它们并非默认在CloudWatch中提供。您需要通过实例操作系统内的工具来收集:

  1. 它们来源于弹性网络适配器驱动。
  2. 可以使用ethtool命令读取。
  3. 若想集成到Prometheus或CloudWatch,需配置对应的代理(如Node Exporter的ethtool收集器或CloudWatch代理)。

监控建议:由于网络流量可能存在持续仅数秒的“微突发”,即使1分钟粒度的监控也可能无法捕捉到这些瞬时峰值。因此,看到额度超限计数器增加,但平均带宽利用率却很低的情况是可能发生的。


实战案例:微突发与S3上传

理论需要结合实际。我们曾遇到一个案例:一个c6i.xlarge实例的网络BandwidthAllowanceExceeded计数器每隔5分钟规律性增长,但该实例的平均带宽使用率仅为15 Mbps,远低于其12.5 Gbps的极限。

经过排查,发现问题根源在于一个每5分钟运行一次的9MB S3文件上传任务。虽然数据量不大,但该上传在26毫秒内完成,这相当于一个短暂的微突发,瞬时速率推算约为2.7 Gbps。

我们与AWS支持的沟通揭示了关键点:

  • 根本原因:数据包经过互联网网关,其MTU被限制为1500字节。小数据包、高瞬时速率触发了PPS限制下的微突发。
  • 尝试与结论
    • 启用VPC端点并使用巨型帧未能解决问题。
    • PacketRateAllowanceExceeded计数器为0,说明不是平均PPS超限。
    • 这证实了限制是在微秒级粒度上实施的,微突发足以触发排队。

经验总结

  1. 限制的实施粒度非常细。
  2. AllowanceExceeded计数器增加不等于丢包,可能只是排队。
  3. 必须监控应用行为,确保其能容忍潜在的数据包丢失。
  4. 带宽额度是共享资源,即使有额度积分,也无法保证随时可用。

应对带宽与PPS限制的策略

当遭遇网络限制时,有哪些可行的解决方案呢?以下是经过验证的策略列表:

  • 垂直扩展:升级到更大的实例类型,通常能获得更高的网络带宽配额。
  • 选用网络增强型实例:为网络密集型工作负载选择名称带“n”的实例系列。
  • 升级实例代次:例如,从第6代升级到第7代实例,可能以相近成本获得20-25%的网络性能提升。
  • 使用E2实例带宽权重:在最新代次实例上,可以重新分配网络与EBS存储的吞吐量权重。
  • 启用巨型帧:在VPC内、通过直连或传输网关的通信中,将MTU设置为9001字节,减少数据包数量,提升有效吞吐量。
  • 平滑流量,避免微突发:通过应用程序配置、TCP参数调优或流量控制工具来平缓流量峰值。

数据包分片的陷阱

上一节我们讨论了常规流量限制,本节中我们来看看一个特殊的性能杀手:数据包分片

当IP数据包大小超过路径上的MTU时,就会被分片。在AWS中,分片数据包无法享受Nitro网卡的硬件加速,而是由CPU处理,性能大幅下降。

关键发现

  • 入向分片:遵循较慢的“标准”数据包速率,但该速率随实例规模增大而提升。
  • 出向分片:存在一个固定的、极低的限制(约1024 PPS),且不随实例规模扩大而增加

这对于使用VPN、隧道封装协议的应用(如我们的物联网核心网)影响巨大,因为封装会增加数据包大小,容易导致分片。

应对分片问题的策略

  1. 避免分片:确保隧道接口的MTU设置正确,让分片发生在内层数据包,对外层透明。
  2. 启用Path MTU Discovery:确保实例能发送“需要分片”的ICMP错误,帮助通信对端调整数据包大小。
  3. 配置TCP MSS钳位:自动调整TCP连接的最大段大小,避免分片。
  4. 利用传输网关分片:如果架构允许,在传输网关上配置巨型帧,并让网关负责分片,减轻实例压力。
  5. 启用“分片绕过”:使用最新的ENA驱动,可以解除1024 PPS的出向分片限制,但需注意这会占用Nitro CPU资源。

连接跟踪的限制与应对

网络限制不仅关乎吞吐量,还关乎连接状态。本节我们探讨连接跟踪的限制。

安全组是状态化的防火墙,它需要维护一个连接跟踪表来记录所有活动连接的状态。这个表有大小限制,但AWS并未公开此限制的具体数值。

当连接跟踪表满时,新建立的连接可能会失败,导致SSH登录、DNS查询、甚至AWS Systems Manager访问异常。

监控连接跟踪
AWS提供了两个相关指标:

  • ConntrackAllowanceExceeded:当连接因跟踪表满被拒绝时增加。
  • ConntrackAllowanceAvailable:显示跟踪表的剩余容量。

重要提示:这些指标需要较新版本的ENA驱动才能支持。例如,Ubuntu 24.04 LTS自带的驱动可能不支持,需要自行编译升级。

应对连接跟踪限制的策略

  1. 禁用连接跟踪:如果安全组规则允许所有出站流量(目标为0.0.0.0/0),并且有对应的入站规则,那么该安全组上的连接跟踪可以被禁用。注意:这会降低安全层级,需谨慎评估。
  2. 垂直扩展:使用更大的实例,连接跟踪表容量通常也更大。
  3. 调整连接空闲超时:减少网络接口上连接跟踪条目的默认空闲超时时间(默认为5天),加速旧条目的回收。
  4. 降低流量的基数:通过架构优化,减少AWS网络需要区分的独立连接数量。例如,在我们的案例中,通过引入通用封装,将众多客户流汇聚到更少的隧道中。

总结

在本教程中,我们一起深入学习了AWS云中复杂而重要的网络限制体系。

我们首先认识到,限制的存在是为了隔离租户,保护彼此。我们系统性地探讨了带宽与PPS限制、如何监控它们,并通过微突发的案例理解了限制实施的细粒度。接着,我们剖析了数据包分片这一性能陷阱及其独特的出向限制。最后,我们深入研究了连接跟踪的限制、监控方法以及包括禁用跟踪在内的多种应对策略。

关键收获在于:

  • 文档是朋友:AWS文档在不断改进,定期阅读至关重要。
  • 监控需谨慎:提供的指标很有用,但需要正确理解和配置,有时仍需深入排查。
  • 架构是关键:许多限制可以通过优化应用架构(如平滑流量、减少分片、降低连接基数)来缓解或规避。
  • 持续学习:AWS平台在不断演进,新的实例类型、驱动功能和优化可能悄然发布,保持技术敏感度是必要的。

云网络看似抽象,但其限制实实在在影响着应用的性能与成本。希望本教程能为您在AWS上构建稳健、高效的应用程序提供一份实用的指南。

028:语义约定与如何避免可观测性中断

概述

在本节课中,我们将学习 OpenTelemetry 语义约定的重要性,以及如何通过工具和最佳实践来避免因语义约定变更而导致的可观测性中断。我们将介绍 OpenTelemetry Weaver 工具和 Schema 处理器,它们能帮助团队在开发和部署过程中保持可观测性数据的稳定性和一致性。

1:OpenTelemetry 与语义约定简介

大家好,我是 Keil,是 TI 的一名高级工程师,同时也是 OpenTelemetry 项目内部多个项目的合作伙伴。另一位是 Dinesh,是 Datadog 的高级工程师,也是 Datadog 内部 OpenTelemetry 团队的创始成员。

我们团队与 OpenTelemetry 社区以及 Datadog 的内部团队合作,确保使用 OpenTelemetry 的客户拥有出色的产品体验。

我们将要讨论的问题,由这里的图表表示,是我们的客户在生产环境中遇到的可观测性中断事件。这个事件促使我们以通用的方式解决这个问题,而我们将在社区内构建的解决方案正是本次演讲要介绍的内容。

首先,让我们花点时间看看第一张图。我们都在使用 CI/CD 管道来自动化、受控地部署应用程序的新版本。我们有一些方法来确保降低部署可能立即导致故障的风险。

但通常缺少的是一套控制措施,以确保你的可观测性后端、你的可观测性栈不会间接地、或者因为某些原因(例如重命名了一个指标、新增了一组属性或某些指标中消失了特定属性)而中断,从而导致现有的仪表盘、告警或查询不再按预期工作或完全失效。

本次演讲的目标是尝试通过我们在 OpenTelemetry 项目中定义的一套工具和约定来解决这个问题。

对于不完全熟悉 OpenTelemetry 的人,让我们从这个项目的快速介绍开始。它是第二个 CNCF 项目,是一个供应商中立的开源可观测性框架,框架内包含多个组件。

我们有检测系统,也称为多种语言的客户端 SDK。我们还有一个名为 OpenTelemetry Collector 的组件,可以说是一个遥测网关。它基本上是一个解决方案,用于连接被观测的复杂系统与你为基础设施运行的一组可观测性后端。

一个非常常见的场景是,你拥有多个微服务,可能使用不同类型的检测。如果你很幸运,可能只使用 OpenTelemetry;或者如果你有一些遗留的微服务,它们可能在使用其他解决方案。因此,Collector 的第一个好处是进行一些适配,包括主题和协议方面的适配。然后,Collector 将能够将这些统一的遥测信号表示发送到各种类型的可观测性后端。

因此,OpenTelemetry 的第一个好处是确保你从问题源收集所有信息,可以统一遥测数据并对其进行一些数据处理,然后将其路由到适当的遥测后端。

我们还有第二个重要的项目,名为“语义约定”。这个项目的目的是定义通用的属性集和信号(这里的信号指的是指标、事件、日志和追踪)。这个第二个项目“语义约定”的目标是简化操作,确保在不同的服务、负责实现和设计这些服务的不同团队中,人们使用相同的语义约定、相同的属性名称,以避免大量的复杂性。

在后端方面,对于 SRE 或任何想要使用遥测信息的人来说,经常看到某个特定团队使用类似 HTTP_UNDERSCORE_REQUEST_TIME 这样的名称来命名指标,而另一个团队使用 HTTP.DOT.REQUEST.DOT.TIME。如何解决这种命名混乱?这就是我们在 OpenTelemetry 项目中正在实施的一套工具和技术的目的。

2:OpenTelemetry 语义约定目录

解决这个问题的第一个要素是我们称之为“OpenTelemetry 语义约定目录”的东西。它是一个由多个特别兴趣小组维护的开放目录。目前有 9 个小组,他们正在定义遥测定义,目标是确保跨可观测性工具的一致性和互操作性。

如右图所示,这是一个指标的定义。它包括名称、描述、单位、一组属性,并且还附带了仪器的规范,说明我们报告的是哪种类型的指标。

这个开放目录是这些指标、事件、跨度等定义的集合。其目标领域是让各个服务和实现它们的团队采用这些语义约定,以减少这些信号命名出现偏差的风险。

但这并非没有挑战。我们有 9 个特别兴趣小组,他们都在独立工作,目前产生了大约 900 个属性和信号,组织在 74 个领域中,而且这个规模每天都在增长。因此,目标是我们如何帮助这些团队以自动化、安全的方式维护这些内容。我们还将看到,我们为解决这个特定问题而建立的自动化工具也可以被任何企业、任何供应商用来创建他们自己的自定义语义约定目录,并且他们可以使用相同类型的工具来确保相同的保证。

3:生产环境中的典型问题

我们在生产环境中经历的问题,可能也是你们遇到的问题,是一系列典型问题。你部署了一个新版本的软件,它运行良好,但检测实际上并不完全符合预期,你破坏了一些告警和仪表盘,这不一定能立即轻易发现。

另一组问题我之前提到过,有些团队使用略微不同的版本来命名相同的概念、相同的指标、相同的匹配属性。如何确保人们朝着同一个方向收敛?

我们还谈到缺乏文档或文档不一定非常准确的问题。我们如何确保提高这个小组或你的企业上游产生的遥测目录的质量?你如何确保自定义指标、自定义标签等得到适当记录?因此,我们需要某种控制来实现这一点。

最后,在生产中花费大量时间并不少见,因为某些指标或属性缺失、不完整或没有完全按预期工作。同样,我们如何确保解决这个问题?

4:Weaver 工具介绍

我们正在创建的工具基本上首先能够检测目录中的任何破坏性变更。我们还控制升级和降级,Dinesh 将讨论一种新型处理器,可以部署在 Collector 中,以确保如果你有时间升级后端,你能够降级信号的版本。

我们还能够识别与命名约定的偏差,因此我们可以确保例如所有下划线都是未经授权的。这类策略是可配置的,并由这个 OpenTelemetry Weaver 项目支持。

我们还确保所有必填字段都正确定义,并且我们能够生成 Markdown 格式的文档,我们还可以扩展系统以创建其他格式的文档。

这是一个尚未完全可用的新功能,但我们希望通过这个 Weaver 项目实现的目标是,我们真的想做与软件开发生命周期中已经存在的相同类型的控制,在那里你有单元测试和覆盖率机制来基本测试系统的各个元素,并且你已经覆盖了,比如说,80% 的代码。这是一个很好的迹象,表明你有足够的测试来信任你的系统和相应的部署。因此,我们希望为检测获得相同类型的测试和覆盖率。

因此,Weaver 不仅可以作为从规范检查和生成代码及文档的工具,还可以直接在你的 CI/CD 管道中使用。作为一个 OTLP 监听器,即一个 OpenTelemetry 监听器,你所有的单元测试或集成测试都可以将遥测数据发送到这个系统,该系统将根据你指定的注册表检查接收到的每个信号的合规性。然后最后,你会得到结果,例如存在差距,或者我们没有覆盖 70% 或 80%,这取决于你想要达到的阈值。这就是我们想要支持的新型能力。

这个工具名为 Weaver,它是一个 CLI 工具,也可作为 Docker 镜像使用。它支持多个命令。

5:Weaver 命令详解

我们有一个 resolve 命令。它基本上生成这组分散的 YAML 文件的聚合版本。

有一个 weaver check 命令,我将在本幻灯片之后详细介绍。它用于应用策略,以确保你遵循最佳实践。

我们有一个 generate 命令来生成 Markdown 文档,但也生成当前由 OpenTelemetry 支持的各种客户端 SDK。至少是与语义约定相关的部分。因此,所有将表示属性或表示指标或事件的语言结构都将通过这些命令生成,并且它已经在 OpenTelemetry 门户中到位。

最近,我们添加了一个选项来计算同一注册表不同版本之间的差异,这就是我们将与 Dinesh 将要介绍的 Schema 处理器一起使用的东西。

好的,这是对当前支持的默认 OpenTelemetry 策略的非常快速的介绍,我不想在那里花太多时间。例如,我们确保永远不会删除元素,确保如果某个东西是稳定的,它不会回到实验开发模式,诸如此类。我们显然也会检查命名约定,看它是否遵循我们期望的模式。

现在,更深入地描述一下 Weaver 能够实现什么。如前所述,Weaver 带有多个命令。这是一个描述,是 weaver registry check 的概述。实际上,Weaver 是一个可扩展的工具,你可以通过添加新模板、新策略来轻松扩展 Weaver,而无需重新编译整个系统。

它是如何工作的?假设你想检查你的注册表是否遵循一些最佳实践。你将提供注册表作为输入参数,并且这个注册表将遵循某些步骤。在这个图的顶部,我们将应用我们之前提到的解析前策略,然后我们将解析注册表。解析注册表意味着我们基本上解析所有引用和所有扩展机制,在这个过程结束时,我们得到一个单一的大规模注册表,其中所有内容都正确已知,没有引用,也无需理解事物是如何被覆盖的。

然后我们有第二阶段的策略,即解析后策略。对于每个阶段,你都可以提供自己的策略,我们将看到这是如何轻松实现的。

在你提供两个注册表的情况下,一个是已发布的注册表版本,另一个是你想用于系统的新版本,我们有第三阶段的策略,用于比较并确保我们遵循我们想要应用的兼容性规则,以确保我们不破坏任何东西。

这个命令的输出是,如果存在违规列表,将直接显示在你的 CI/CD 管道中,你可以用它来了解需要修复注册表的哪些地方。

这是一个策略的例子。显然,我们提供了很多默认策略。所以如果你不想进行这种程度的定制,也没关系。

我们基本上重用了我认为相对知名的策略语言 Rego,它是由 Open Policy Agent 引入的,这是一个在 Kubernetes 中广泛使用的知名产品。因此,我们基本上提供了一种用这种 Rego 策略扩展 Weaver 的方法。

下一个命令是 weaver generategenerate 命令的下一个参数基本上是一组模板的名称。

我们有一些默认模板。一个名为 markdown,其他一些则带有相应客户端 SDK 的语言名称。因此,如果你想生成 Java、C#、Python 等的客户端 SDK,你只需在 generate 后面加上语言名称,就可以生成客户端 SDK。它的工作方式是,我们有一个基于 Go 的模板集合。

这些模板集合被命名。其中一个集合是 markdown,另一个将如我所说,针对不同语言有不同的名称。你可以使用这个系统来生成其他东西。如果你想生成 HTML 或其他任何你想从注册表派生的工件类型,这已经完全可行。如果你已经在使用其中一种,你也可以将这种系统集成到你自己的企业目录中。

这是关于 Weaver 的。今天我想描述的最后一类命令,我们还有其他命令,但这是最重要的一个。最后一个是我们称之为 weaver registry diff 的命令。这个命令再次接受两个注册表,即同一注册表的两个版本。我们有一种自动化的方式来比较它们并计算差异。

这种差异是可以被像 Schema 处理器这样的东西使用的工件,以执行我们想要应用的升级或降级转换,以避免破坏任何东西,例如,在后端维护特定版本。Dinesh 将更多地讨论这一点。但如果你想要创建迁移指南,也可以使用它。例如,假设你是一个供应商。就我自己而言,在 TI,我们做负载均衡器、IaaS 和其他类似的安全导向产品,所以我们可以使用这种工具来生成与特定产品对应的遥测目录,当我们有这个产品的新版本时,我们可以生成迁移指南。

这种命令可以自动生成迁移指南,因此不再存在人为错误或忘记某些事情的可能性,因为它是完全自动化的。

6:OpenTelemetry Collector 与 Schema 处理器

谢谢,Keil。现在我们已经了解了语义约定是如何演变的。让我们来谈谈 Collector,这是 OpenTelemetry 的核心组件。OpenTelemetry Collector 是一个供应商中立的代理,允许我们收集、转换和处理所有类型的遥测信号。它适用于数据管道,具有接收器、处理器和导出器,你可以用来构建管道。

让我暂停一下,我想看看有多少人在生产环境中运行 OpenTelemetry Collector。请举手,哇,大约 30%。好的。

让我们看一个简单的 Collector 管道示例。我们有三个服务正在生成追踪。我们有一个管道,使用 send_traces 将追踪发送到可观测性后端,它对追踪进行采样并发送它们。另一个管道使用 span_metrics_connector 来计算 RED 指标,并将其也发送到后端。我们使用这种设置,以便可以在未采样的数据上计算 RED 指标。你想知道每秒请求数,而不进行采样。

在这个例子中,我们使用 span_metrics_connector 并计算四个维度上的 RED 指标,即 http.methodstatus_codedeployment.environmentservice.name,这是我们想要的典型 RED 指标维度。

这一切看起来都很好,运行得很顺利。谁能告诉我,当我们定义这个管道时,我们做出了什么假设?

没有。好的,我认为,在不知不觉中,我们通过使用属性名称定义了管道。我们与语义约定绑定在一起,因为正如我们所看到的,语义约定可以演变,可能以不同的方式发生。这对于任何处理器都是如此。任何时候你在 Collector 中使用某个处理器和一个属性名称,你就是在定义一个管道到一个特定的语义版本,这是人们通常不会假设的,但这就是管道的本质。

那么在这里,我们有一个场景,维护服务 B 的团队决定升级他们的 OpenTelemetry SDK 版本。在这种情况下,当他们更新 OpenTelemetry SDK 版本时,他们也隐式地更新了他们的语义约定。因此,在这种情况下,他们正在使用 1.27 的语义约定产生新的遥测数据。

在这两个版本之间发生的变化是,环境名称 deployment.environment 被重命名为 deployment.environment.name。因此,由于这个变化,RED 指标不再在环境维度上计算。这就是我们的客户遇到的情况。因此,其中一个团队进行了更新,并且在不知不觉中,他们破坏了他们的仪表盘。他们破坏了他们的告警,并且在生产中处于盲目状态。我们花了很长时间才追踪到问题,发现问题是语义约定导致的。

我们在这里描述的情况并非孤立事件。当 HTTP 语义约定稳定下来时,这个问题再次出现,当时发生的情况是 http.method 被重命名为 http.request.method,这也破坏了客户环境。他们不再看到这些的 RED 指标。

作为管理 OpenTelemetry Collector 管道的 SRE,我们希望避免这类情况。我们无法控制哪些团队更新他们的软件,哪些团队将产生新的语义约定并破坏所有的仪表盘和告警。这正是我们想要解决的问题。

我们建议通过使用 Schema 处理器来解决这个问题。

Schema 处理器将传入的遥测数据转换为目标模式版本。在这个例子中,我们使用目标模式版本 1.26。

这使我们能够定义特定于目标版本的管道配置。

通过使用 Schema 处理器作为 Collector 管道中的第一个处理器,我们可以转换传入的信号以匹配目标版本。

Schema 处理器能够进行这些转换,原因有两点。首先,传入的 OTLP 负载使用一个名为 schema_url 的可选字段定义了负载的语义版本。因此,传入的负载有一个模式 URL,它告诉传入的语义约定版本。其次,它使用一个遥测模式转换文件,这是 weaver diff 命令的输出。因此,Weaver 能够判断这两个语义约定之间存在差异。这就是转换规则。它作为 Schema 处理器的输入。基于这些,Schema 处理器执行向上转换或向下转换,因为在这个例子中,它执行的是向下转换,但也可能存在需要向上转换的场景。

通过在管道的最开始转换数据,我们能够保护所有经过 Schema 处理器的遥测数据。

这意味着各个服务、Collector 设置和遥测设置现在解耦了,之前存在的紧密耦合不再存在。

现在,如果你运行这个例子,服务 B 的 1.27 语义约定产生了 deployment.environment.name,Schema 处理器会将该属性重命名为 deployment.environment,这样就不会再破坏 RED 指标了,因为 span_metrics_connector 仍然能够使用相同的名称。

7:当前方案的局限性与未来工作

这个解决方案看起来都很好,但它总是有局限性。

Schema 处理器正处于积极开发阶段。我很高兴地宣布,它将成为下周发布的 Collector 核心版本的一部分,我认为这很棒。我们付出了很多努力才达到这一步。

第二个更大的问题是,模式 URL 是整个负载中的一个可选字段,这意味着 SDK 并不强制要求设置它,所以有时需要用户自己确保设置了模式 URL。

如果负载中没有模式 URL,Schema 处理器就不知道是否需要执行向上或向下转换。因此,它只是忽略负载并将其发送下去进行处理,这不是我们想要的。所以仍然有一些事情我们想要避免。

如果变更不是客观的,例如合并指标名称或拆分指标,Schema 处理器无法以编程方式处理任何事情,这些是我们仍然无法以编程方式处理的事情,我们仍在研究这些问题。目前,我们使用的是模式文件格式 1.0,它不能帮助我们处理复杂的转换。

不幸的是,我们仍然在手动更新,因为这个文件是在 OpenTelemetry 的早期阶段定义的,我们仍然在手动更新,没有检查机制,所以存在一些错误风险,因此我们也希望自动化这一点。

8:社区的未来工作方向

有了这些,让我看看我们在社区中正在进行的下一步工作。

我们的管道中有很多事情要做,这是一个很长的列表。我想谈的第一件事是,我们希望扩展 Weaver 以支持多注册表,我们相信这将是最常见的用例。像我们这样的可观测性供应商,Datadog,想要定义他们自己的语义约定,每个 OpenTelemetry 用户也有他们自己的语义约定。因此,我们希望 Weaver 工具能够处理多个注册表。

显然,另一件事是我们正在努力开发一种新的模式文件格式。我们希望它是自包含的注册表,允许我们进行向上和向下转换。

社区里有很多关于这个主题的讨论,关于格式、我们想要处理哪种转换、如何定义转换以及使用什么语言。我认为那里发生了很多有趣的事情。

正如我所说,Schema 处理器刚刚发布,所以它仍处于 alpha 状态,我们希望稳定 Schema 处理器,确保它可用于生产,并且我们可以推荐每个人使用并在 Collector 管道中定义它。我认为那里仍然有令人兴奋的工作。

我们看到的场景,我认为我们需要问自己的一个有趣问题是,如何在开发阶段避免和捕捉语义约定偏差的场景。这就是我们相信 Weaver 实时检查将有所帮助的地方。因此,我们想要的是,我们为代码编写测试,但我们是否为应用程序产生的遥测编写测试?我认为答案很可能是否定的,因为我们假设遥测总是存在的。

因此,这是我们希望鼓励每个人思考的模式优先方法,人们也为遥测编写测试,例如定义你的应用程序产生什么样的遥测,然后使用 Weaver 进行测试和合规性测试,这样你就可以编写一个单元测试,将单元测试输入 Weaver,Weaver 将产生测试输出是否符合注册表的结果,是否缺少某些东西或其他任何问题。

我跳过了一件事,那就是类型安全的客户端 SDK。我们正在考虑与 proto 相同的思路,我们希望获得属性和所有内容的类型安全的 SDK,这样人们就知道每个属性是什么类型。现在,它是任何字符串,没有属性类型。所以人们不知道持续时间是毫秒还是秒,或者它是字符串还是数字,这是缺失的东西。我们希望避免这类情况,而不是留给猜测。

正如我所说,我想鼓励每个人都成为这段旅程的一部分。我们正处于这段旅程的早期阶段,社区正在通过大量讨论和问题来推进工作。

我已经链接了两个 Slack 频道,这是我们目前最活跃的地方,以及所有的 GitHub 仓库,如果你能来测试我们的工具,给我们关于错误或功能请求的反馈,告诉我们如何帮助减少你自己的日常工作负担。

总结

在本节课中,我们一起学习了 OpenTelemetry 语义约定的重要性,以及如何通过 Weaver 工具和 Schema 处理器来管理和避免因语义约定变更导致的可观测性中断。我们探讨了生产环境中常见的问题、Weaver 工具的功能与命令、Collector 中 Schema 处理器的工作原理,以及社区未来的发展方向。希望这些知识能帮助你构建更稳定、可靠的可观测性系统。

029:通过变点检测阻止性能退化

概述

在本节课中,我们将学习如何在一个大规模、高波动的分布式系统中,利用变点检测技术来识别和预防性能退化。我们将以彭博社的Tickupplant市场数据平台为例,探讨其团队如何构建一个实用的变点检测产品,以应对低延迟这一核心产品特性所面临的挑战。


什么是变点?🔍

在开始之前,我们需要明确一个基础定义。如果你有一个描述系统行为的指标或时间序列,变点本质上是指该时间序列发生突然且持续变化的时刻。你通常需要关注它,调查它,并了解系统发生了什么变化。

明确了这一点后,我们来谈谈构建变点检测产品的经验。


为什么变点检测对我们至关重要?🏢

为了理解变点检测为何与我们相关,首先需要了解Tickupplant是什么,以及我们在彭博社市场数据组织中的定位。

如图所示,Tickupplant负责向彭博社的下游应用实时(或其他方式)提供市场数据。因此,如果你是彭博社的客户,通过终端、企业API服务或批量数据产品查看市场数据,你很可能就是Tickupplant的客户。

作为彭博社市场数据的“共同祖先”,Tickupplant的性能将直接反映在彭博社整体市场数据产品的性能上。对于SRE而言,有时存在一种看法,即故障会获得大量关注,但当一切顺利时,工作成果往往被视为理所当然。然而,在延迟方面,情况对我们略有不同,特别是因为低延迟是我们产品的一个特性

我们的客户非常关心低延迟,因为这最终关系到他们的成功。既然客户关心它,并且Tickupplant的延迟会影响彭博社的延迟,那么在任何时间点,了解我们正在进行的工作是让彭博社的性能变得更好还是更差,对我们的团队来说都至关重要。


延迟问题的挑战是什么?🌪️

那么,是什么让延迟,特别是对我们和我们的产品而言,成为一个具有挑战性的问题?答案在于,在一个像我们这样的大规模分布式系统中,最终影响延迟的因素具有多样性

从这里的可视化图表可以看出,我们处理的是高容量、极度波动的数据。更复杂的是,我们无法完全控制甚至可靠地预测那些最终导致延迟的因素。这可能是世界各地的新闻事件,也可能是客户兴趣和市场子集的不断变化。除此之外,这些高波动时期通常也是客户对延迟更加敏感的时期。

为了说明这种波动性和数据量,在我们制作这张幻灯片并获得批准后的两周内,峰值已不再是4000亿。我们现在看到的当前峰值是每天4900亿条消息

最终,所有这些因素意味着并非所有延迟都是可以避免的。我们必须接受一定程度的延迟。


我们如何应对?🛡️

那么,我们实际上选择怎么做呢?我们选择更好地预防那些我们可以控制的延迟影响客户

例如,核心版本发布等事项,我们对此有控制权。因此,如果我们能足够早地标记出性能退化,我们就能在它最终影响客户及其性能之前,控制并回滚它。

当我们开始思考如何实际解决这个问题时,挑战又回到了我们所讨论系统的广度和规模上。我们不可能依赖理解客户行为的每一个细微差别及其对市场的兴趣,并设计出能完美反映这些情况的性能测试。

因此,我们选择依赖统计方法,这使我们能够将正在处理的这个相当复杂的系统,视为一个简单得多的黑箱。


我们的行动历程 🚀

那么,我们实际上为此做了什么呢?有一段时间,我们并没有太多实质性的行动。我们思考了很多,认识到了它的必要性,但直到问题找上门来,我们才被迫采取行动。

我们看到性能下降席卷了数据中心中的许多机器。我们所有的工程师都明显看出这是个问题,但尽管有很多人关注,却没有人能准确指出问题是从何时开始的。它似乎与代码发布或功能开关没有任何关联,也没有人取得任何进展。

直到我们部署了一个非常基础的变点检测算法,并能够获取每台机器在每个变点前后的元数据,我们才得以解决问题。情况是这样的:

  • 背景:这里展示的是机器随时间的“工作时间”。
  • 定义工作时间 是我们处理一个数据单元(如之前看到的市场消息图表中的一条行情)所需的时间。
  • 重要性:工作时间之所以重要有两个原因。第一,它类似于一个激活函数或悬崖函数。如果我们的工作时间变得过高,我们的延迟可能会无限制地爆炸性增长且无法恢复。第二,它基本与负载无关。无论市场是极度波动、我们收到大量数据,还是处于更典型的状态,工作时间大致相同。

我们看到的情况是:这台机器被添加到了一个特定的配置文件中,但直到几天后机器因维护重启时,变化才真正发生。随后,由于完全独立的原因,这个变更又被来回回滚和重新应用,每一次也都有其各自的几天延迟。

这看起来很简单,部分是因为我选择了最简单的图表来说明。但当你考虑到我们拥有的数百万指标、奇怪的发布模式以及这种巨大的“怪异”性时,仅通过查看Grafana图表,人类几乎无法处理这个问题。


现实数据的复杂性 📊

不幸的是,我们的很多数据并不像上面那样干净,很多看起来更像这样。这里展示的是同一事件在三个不同环境中的情况,我将逐一讲解以说明我的意思。

  1. 环境一:这是延迟,越低越好。可以看到它相对一致,有一些尖峰,一个较大的尖峰。虽然这不好,我们不喜欢,但如果我们要发布这段代码,这并不代表存在生存风险。
  2. 环境二:这是相反的情况。确实有一个大的尖峰,但延迟保持在高位,从未完全恢复到基线水平。如果这代表了软件的性能,并且它被发布到我们所有的机器上,那可能构成生存威胁。
  3. 环境三:不幸的是,对于算法来说,这是更常见的情况,即更加模糊不清。根据你获得的数据点数量、时间尺度、幅度大小以及你对系统的了解,你可能会说这里、这里或那里可能是一个变点。当你查看比较不同变点检测方法的研究时,会发现即使是专家之间也存在显著的标注不一致性。

我们选择的算法 🤖

因此,在选择作为我们产品基础的初始算法时,我们希望在拥有良好性能评估指标之外,还希望它标记出的内容符合我们对“变点”的预期。为此,我们选择了贝叶斯在线变点检测

简单来说(这里做了大量简化),它的工作原理是:当算法读取你的时间序列时,它会形成对数据分布应该是什么样的一个概念。然后,当它看到足够多似乎来自不同分布的数据点时,它就会将其标记为一个变点。


产品化过程中的挑战 ⚙️

我想花更多时间谈谈将这项技术作为产品带给团队时所面临的实际挑战。我将跳过那些预期的挑战,其中很多坦率地说,是一个团队首次进行数据科学项目时会预料到的,比如跟踪标注者间一致性、对实验和数据进行版本控制等。

我想花更多时间谈谈那些意想不到的挑战

讽刺的是,第一个挑战是预期内的变化。我们的算法非常擅长检测市场开盘和收盘、机器维护期。如果你的日历不工作,它甚至能告诉你周末到了。这很棒。但问题是,虽然这些都是变点,算法是对的,但它们并不是我们真正关心或感兴趣的东西。

根据我的经验,当我观察那些计算是否满足SLO的团队或内部异常检测系统时(在一些外部项目中也见过),很多人处理这类预期异常或预期变点的方法是创建一个或多个配置文件,基本上是说“我想忽略这段时间”。这最终会变得极其繁琐。我认为人们低估了保持这些配置文件更新和可用所需的工作量。例如,我们关心数百个市场,它们都有自己的开盘和收盘时间,都有可能在每年不同日期发生的假期,时区是另一个问题。我们曾为一个内部SLO做过类似的事情,多次因为各种夏令时和时区情况而出现问题,最终就像打地鼠游戏一样。

我们在这里的做法是,几乎原则上刻意避免使用这类配置文件。我们更多地专注于对数据进行后处理和前处理,以确保对这些预期变化更具弹性。例如,与其为每个市场、每个序列设置配置文件来排除特定时间点,不如将很多工作推到查询层(在我们的案例中是Graphite),以获取我们知道是“干净”的序列。


分类诊断视角的挑战 🧐

我想谈的另一个问题是从分类诊断的角度来看。这是我们最初的用户界面,我认为对于一个SRE团队来说看起来还不错。

不幸的是,这里有12000行数据,而这还是我们发布后不久的情况。是的,其中一些是误报,但也有很多是我们没有真正考虑太多、想当然的用户体验问题。例如,你用稍微不同的参数运行相同的分析,现在你复制或近乎复制了很多变点;你在不同日期运行相同的分析,由于我们有滚动的保留窗口,你会得到略有不同的变点;还有像底部这两行这样非常简单的情况,它们非常相似,是两个非常相似的变点,发生在同一个集群,只是相隔四个小时。主要区别是它们发生在两个不同的节点上。从诊断角度来看,这重要吗?是的。但从每周执行此任务的分类诊断人员角度来看,这与我所提到的其他一切叠加起来,会让人非常不知所措。


我们的解决方案:注重产品体验 🎯

最终,我们的解决方案是在提供良好的体验和产品上投入比人们预期的、听起来非常算法化和数据科学化的项目更多的精力

例如:

  • 我们为分析运行添加了标签,以便查找我们关心的特定分析片段。
  • 我们增加了将变点标记为真阳性或假阳性的能力,以建立更好的反馈循环并进行改进。
  • 我们还添加了许多此处未描绘的过滤选项。

总结与建议 📝

总的来说,如果你的系统有很多可能的变化方式且非常嘈杂,使得简单的基于规则的系统无法工作,特别是当你的系统规模非常大、责任分散,并且不符合SRE的理想状态(即并非所有东西都按你希望的方式进行了检测,并非每个人都按你希望的方式进行测试),当你必须接受现状时,我推荐使用这样的统计系统,无论是用于异常检测还是变点检测。

我特别要强调,与人们通常的预期相比,你可能会花费多得多的时间来改进产品的可用性,而不是实际的算法部分

我还推荐去年Ivan Happen关于类似问题的演讲,他从不同的角度处理了类似的问题。我认为拥有另一个视角是很好的。

如果你想制作一个市场开盘/收盘检测器,请联系我们,我们很乐意回答你的问题。如果你想在彭博社工作,我们有一个二维码。

谢谢。


本节课中我们一起学习了:在一个高波动、大规模分布式系统中应用变点检测的必要性、挑战与实践方法。我们了解了变点的定义,探讨了延迟问题的复杂性,看到了贝叶斯在线变点检测算法的应用,并深刻认识到,将算法转化为有效的产品,其成功往往更依赖于出色的用户体验和工程化实践,而不仅仅是算法本身的先进性。

030:化流程为产品

在本教程中,我们将学习如何将复杂、手动的运维流程转化为自动化、自助式的产品。我们将探讨自动化背后的核心原则,分析流程的通用构建模块,并重点介绍如何通过卓越的用户体验设计来驱动产品的采用。最终目标是创建出不仅功能强大,而且深受用户喜爱的自助服务平台。

流程自动化的演进与挑战

在深入具体方法之前,我们先回顾一下自动化的发展历程。最初,自动化是出于必要,我们编写Shell脚本、Cron作业来处理重复任务。随着复杂度增长,我们开始使用版本控制、Python、Puppet、Ansible、Terraform等工具,以一致且可靠的方式实现自动化。

许多工程师的职业生涯都建立在掌握这些工具之上。后来,这一旅程引领我们走向Go、Kubernetes CRD,并开始开发自己的软件来实现自动化。代码的核心目标始终不变:让事情以编程方式、自动、可预测且一致地发生。我们知道,可靠性是自动化与一致性的结合。

起初,我认为只要自动化技术流水线,就能解决大部分问题,使事情变得可重复、可预测和声明式。但无论你的CI/CD流水线多么完善,告警系统多么出色,你可能仍然在处理手动文件传输,或者在海量负载均衡器日志中寻找几天前故障的根本原因,并手动提交工单。

从流程到产品的思维转变

当我六年前加入公司时,我发现有一支高技能的工程师团队,致力于通过出色的自动化来简化流程。然而,有些流程过于复杂,涉及太多人员和手动环节,无法完全自动化。随着时间的推移,这些流程成为我们改进和发展的重大障碍。

这促使我思考:如果我将这个具体问题——手动流程问题——视为一个需要解决的产品问题,会怎样?我能否开发一个产品或技术来解决它?这个问题将你置于一个不同的角度:你不再是问题的一部分,不再受现有边界的限制。你跳出框架,审视它,并试图找到解决方案。

我想,如果让流程本身成为产品会怎样?如果我的SRE团队可以专注于创建代表他们行动的工具,供他人使用,从而将宝贵的时间重新投入到真正需要关注的领域,而不是创建另一个特殊环境、部署另一个服务或配置更多资源,那会是什么样子?

从手动流程的维度思考,我们是人与技术之间的中介。那么,为什么不能将这种中介委托给我们能够构建的技术,然后成为这项技术的提供者呢?我相信技术,相信它可以解决任何问题,包括这个具体问题。因此,我们决定将其视为一个工程问题,而工程问题需要工程方法。

流程自动化的通用构建模块

自动化成功的关键不仅在于编写脚本或代码,更在于将流程分解为根本上可重用的构建模块。如果我们分析大多数运维工作流,它们都可以归结为五个通用步骤:

以下是构成任何流程的五个核心构建模块:

  1. 询问/输入:我们需要获取输入数据。
  2. 执行动作:我们想要执行某个操作,例如重启、部署、发送邮件等。
  3. 审批流程:作为一个守门员,无论审批是程序化的还是手动的,它都是自动化流程中可以使用的一个模块。
  4. 等待:等待的能力允许我们进行异步处理。我们可以开始某项工作,等待输入,等待相关人员返回,执行另一个流程,然后恢复操作。
  5. 报告状态:我们需要知道流程的状态。

从高层次看,任何手动流程都可以通过以特定方式排列这些构建模块来构建或重建。细节可能不同,收集数据的方式可能各异,审批流程可能变化,但基本原则保持一致。

理论实践:域名掩码自动化案例

有了构建模块的理论基础,我可以按顺序排列它们来自动化流程,并希望看到理论付诸实践。我们解决的第一个流程是自动化所谓的“掩码域名”。

为了给不熟悉我们业务的听众一些背景,我们运营于客户体验和用户体验领域,通过不同渠道收集来自用户的信号。其中一个信号是客户反馈。从技术层面看,这些反馈被发送到我们的域名。但有些客户不希望提及我们的域名,他们希望反馈在其自己的域名下发送。这是一个非常简单的需求。

但在运营层面,由于依赖关系,这曾是一场噩梦。这个过程始于客户联系专业服务团队,要求设置域名掩码。专业服务团队会为SRE团队创建一个工单。然后,SRE生成证书签名请求,将其发回给专业服务团队。专业服务团队将其转发给客户签署,客户再将证书发回。专业服务团队接着将其转发给SRE进行验证和安装。SRE会创建一个DNS记录,并要求客户指向该记录。最后,专业服务团队通知客户更新DNS,一切开始运作。

这个过程极其简单直接,但由于相互依赖、涉及人员以及客户方既定的程序,我们一直以同样的低效方式重复它。

现在,让我们看看如何使用之前提到的构建模块重新安排这个流程:

以下是使用构建模块重构后的域名掩码流程:

  1. 输入:客户发起掩码域名请求。我们需要向专业服务团队询问客户详情。
  2. 动作:创建工单用于审计跟踪。
  3. 动作:同时创建证书签名请求和DNS记录。
  4. 等待:等待专业服务团队返回已签署的证书。
  5. 审批/验证:验证证书,确保其与生成证书签名请求时创建的私钥匹配。(此处作为守门员,旨在建立对流程的信任,但技术上可实现自动化)。
  6. 动作:安装提供的证书,并通知专业服务团队客户可以执行DNS更新。

本质上,这是完全相同的流程和步骤,只是以更有条理的方式进行了排列。但为了让其工作,为了让专业服务团队能够启动并遵循此流程,我们必须将它们全部整合在一起,创建一个系统和一个界面。我们做到了。

自动化之外的挑战:用户体验至关重要

然而,令人惊讶的是,人们并不兴奋,我们仍然收到大量工单,尽管系统在功能上非常完善。我们是技术人员,当我们构建东西时,我们考虑的是技术细节,为技术人员构建。我们完全可以创建一个接收API并返回JSON的服务。如果你愿意,可以使用curl与之对话。

即使我们构建了用户界面,它看起来也像这样(注:指演讲中展示的原始界面),说实话,我当时很自豪。但它仍然需要非常长的文档页面,让人们理解他们应该做什么、先做什么、下一步做什么、何时继续。令人惊讶的是,人们不喜欢阅读冗长的文档。

我们持续收到工单,因为存在一个“捷径”。如果某件事需要阅读文档,而另一件事是捷径,人们总是会选择捷径。但对我们来说,这个Jira工单根本不是捷径,因为它需要我们持续在Jira中工作、更新内容和跟踪进度。

于是,我们有了另一个启示:我们理解到,仅有自动化是不够的。人们必须愿意使用它。我再次戴上初创公司创始人的思维帽子,问自己:如果我要开发一个我希望人们使用的产品,它会是什么样子?一个好的、畅销的产品是什么样子?

当我谈到产品时,想想苹果、沃尔沃或奔驰。当你打开新iPhone时,你立刻明白如何使用它。你坐进一辆奔驰,每个控制装置都如你所料。苹果永远不会要求客户SSH到他们的手机来启用蓝牙。奔驰不会要求你带扳手来启动车辆。沃尔沃不会提供五页长的文档教你如何换挡。他们期望它“直接能用”。

这些品牌有某种共同点,我希望我的产品也有同样的感觉。我希望它直观、有吸引力且易于使用。我的产品需要一个出色的界面,但作为SRE,我们如何知道什么是出色的界面呢?

打造卓越用户体验的设计原则

为了回答这个问题,我们咨询了产品专业人士的朋友和同事,并学习了大量用户体验设计原则。我们专注于客户体验,并将同样的理念应用于内部,应用于我们的自动化产品。我们确定了以下对我们需求绝对最佳的设计指南,希望你们也能发现它们有用。

以下是打造用户喜爱的自动化产品的七项关键设计原则:

  1. 清晰优于复杂:减少认知负荷,使交互显而易见。不要显示任何非必要信息。例如,在我们的内部监控系统中,底部只提供你正在寻找的确切信息,用清晰的大拇指图标指示证书状态。
  2. 简约而强大:提供简单的默认选项,但也允许在需要时进行高级控制。应用此原则,我们最初将冗长的侧边栏缩减为几个按钮和切换开关,并用图标替代文字,因为图标更直观。
  3. 渐进式披露:不要一次性用所有选项淹没用户。理想情况下,应根据用户之前的选择动态显示相关选项。我们通过JSON配置文件定义相关步骤、前后关系来实现这种表单配置。
  4. 一致性:保持相同的用户界面模式和设计语言,确保当用户使用新系统或现有系统演进时,仍然感到熟悉和自在。
  5. 容错与恢复:如果用户输入了错误信息,不要让他重启整个流程或刷新页面。提供返回并修复之前错误的方法。我们发现“下一步/上一步”箭头在网页表单中最实用、最直观。
  6. 可访问性与包容性:这里指的是针对不同技能水平的人群。你希望你的系统对技术高手和零基础的新员工都可用。可以通过“简约而强大”的原则来实现:为入门用户展示最简界面,同时提供一个“高级模式”切换来暴露更多功能。
  7. 视觉反馈:当系统执行操作时,让用户知道它正在工作,而不是卡住了。加载时显示旋转图标,遇到错误时显示有意义的错误信息。错误信息应具有可操作性,告诉用户问题出在哪里以及下一步该做什么,而不是简单的“服务器遇到意外错误”。

实现成功的三步算法

理解了这些原则后,我们将其应用到现有系统中。结果,采用率飙升,人们开始使用系统。我们随后将成功经验复制到下一个、再下一个流程。

后来反思这个过程时,我们清楚地认识到,真正的挑战并非来自技术,而是来自人员、流程和采用率。你可以解决技术挑战,轻松实现自动化,使其成为自助服务,但如果不方便,人们就不会使用它,你的所有工作都可能付诸东流。

但有一种方法:你希望让人们爱上你所做的事情。你希望它吸引人且简单,让他们希望所有事情都像你做的那样好。这样你才能赢得用户的心和思想。

我将这个演讲命名为“Per Aspera ad Productum”(历经艰辛,终成产品)。但这个过程不必艰难。前路已明,我很高兴能在此与大家分享这些想法。

最后,我想分享一个我们现在使用的、帮助我们重构旧工作方式的算法。它非常简单、明显,但非常强大。

以下是实现流程产品化的三步算法:

  1. 理解

    • 理解流程边界:明确你试图自动化的流程的确切起点和终点。手动流程的问题之一在于它们不一定有明确的起点和终点。
    • 理解你的用户:了解谁是你的用户和受益者(他们可能不同)。与目标受众沟通,理解他们的真实需求,围绕满足这些真实需求来构建产品,而不仅仅是为了让你和你的团队受益。
    • 理解限制:考虑隐私、数据治理、安全等所有我们熟悉的运营限制。
  2. 实施

    • 就是去做。任何适合你开发产品的方法都行。
  3. 改进

    • 改进产品的唯一方法是收集反馈,并基于反馈采取行动。
    • 重要提示:避免变通方案。变通方案不是功能,它们容易成为部落知识,并且由于其性质而难以维护。如果用户提出请求,不要给他们变通方案,要使其正式化。
    • 管理产品路线图:产品路线图与运营路线图略有不同,你需要管理缺陷、功能请求和技术债务。
    • 使组件可重用:我们都理解可重用性的含义。

总结

本节课中,我们一起学习了如何将手动运维流程转化为成功的自助服务产品。我们从自动化演进的历史出发,认识到仅靠技术自动化不足以驱动采用。核心在于进行思维转变,将流程视为待开发的产品。

我们分析了任何流程都可分解为输入、动作、审批、等待、报告这五个通用构建模块。通过一个域名掩码的实际案例,我们看到了如何用这些模块重构流程。

然而,真正的突破来自于关注用户体验。我们介绍了七项关键设计原则:清晰优于复杂、简约而强大、渐进式披露、一致性、容错与恢复、可访问性与包容性、视觉反馈。应用这些原则能显著提升产品的采用率。

最后,我们总结了一个实用的三步算法:理解、实施、改进,用于系统地重构旧有工作方式。

请记住,如果要从本课中带走一样东西,那就是这个公式:卓越的自动化 + 出色的用户体验 = 人们喜爱的自助服务

031:关键的事件管理指标 🎯

在本节课中,我们将探讨如何有效衡量事件管理流程的成功与否。我们将分析传统指标的局限性,并学习如何构建一套更全面、更能反映真实情况且能避免不良激励的指标体系。


大家好,我是Jamie,Datadog的高级SRE。我刚刚结束了为期六个月的突发事件管理团队临时经理任期。这个团队负责定义事件管理和值班流程与实践,并衡量这些计划在全公司的成功程度。

我是Laura,Datadog的高级工程师,我的工作范围广泛,核心是确保我们的系统可靠且具有韧性。这自然包括确保我们的事件管理流程和工具运行良好,将客户受意外事件影响的程度降至最低。

在这次演讲中,我将扮演一个新角色:Datadog的一位新上任的工程经理,正准备在事件管理领域有所作为。

欢迎来到Datadog。Datadog虽然不是巨头公司,但规模相当大,并且拥有自下而上的工程文化。这意味着Datadog的任何工程师在某个时刻都可能参与处理事件,每个人都受过培训,能够胜任事件响应者或事件指挥官的角色,并且需要参与撰写事后分析报告。我们有一个专门的团队来构建可持续的事件流程,并持续评估我们的事件管理和值班状态。

那么,高管们如何知道雇佣这个事件管理团队是值得的呢?我们需要了解我们在控制事件方面做得如何,以及团队为改进业务所做的工作是否物有所值。我们希望在公司层面需要重大调整之前,就能发现问题。同时,我也希望获得丰厚的奖金。

那么,如何用高管能理解的语言证明我们做得很好呢?

我知道,我们应该衡量事件的平均恢复时间。如果我们的响应有效,就能更好地恢复。所以,MTTR下降意味着我们有良好的事件响应。

这是一个容易统计的指标,但它究竟为组织带来了什么价值呢?我认为它描绘了一幅不准确且过于简化的可靠性图景。事件是不断变化的,单一指标不足以捕捉这种复杂性。它也不是一个稳健的汇总统计量。Shaan Devidovich在一篇出色的论文中做过统计分析,指出事件数量的变化和事件数据的标准差变化,意味着MTTR的变化几乎可以确定是数据中的噪音,你并没有测量到对组织有用或可操作的东西。

不仅如此,它还会产生不良的激励。降低MTTR最简单的方法就是让同一个事件反复发生,每次响应都越来越熟练,直到能非常快地关闭它。但这对于事件管理项目来说并不是一件有用的事。

在你提出之前,我想说,平均确认时间和其他类似的MTTR变体也好不到哪里去。它们同样带有自身的不良激励问题,并且和MTTR一样不稳健。

好吧,我们不想让同一个事件反复发生。那么我们应该衡量事件数量。如果我们工作做得好,事件就会越来越少。这说得通,我们希望系统故障更少,通过减少事件来实现。

我能看到其中的逻辑,但事件数量的变化究竟意味着什么?事件数量的增减往往是相关性而非因果关系,可能受到业务周期性、节假日或代码冻结期的影响。此外,它也会带来不良激励,鼓励人们不将真正需要响应的事件上报,让低严重性事件变成“有问题的Bug”,最终导致你失去对实际故障的可见性,因为它阻碍了人们参与你的流程。

那么变更失败率呢?它类似于事件数量,但根据系统规模和变更速度进行了标准化。

但什么是“变更”?什么是“失败”?这很难定义。它也无法很好地反映问题是如何相互关联的,以及变更如何随着时间的推移从看似无关的提交中累积起来。

但人们需要认真跟进事后分析,所以他们需要有很多行动项。行动项越多越好。

如果“行动项越多越好”,就会鼓励人们制定低价值的行动项,尤其是当它与“衡量行动项完成度”的想法结合时。我们可以在团队层面衡量这个,比如周环比趋势。

但为什么不是小时环比呢?如果只针对单个团队及其事件,数据变化太大,周环比无法提供可操作的数据。

我们希望事件不那么糟糕。我们可以用事件严重性来衡量吗?这有助于减少客户遭受的严重中断次数。

我同意,让事件不那么糟糕是好事。但如果我们试图计算高严重性事件的数量,就会鼓励人们人为降低事件严重性,甚至可能不上报本该是事件的事情。

你这是在为难我。为什么不衡量团队资历水平呢?我们知道资深工程师更优秀,所以他们只有在事情真的出问题时才会犯错。

我不一定更优秀。而且我认为这可能会导致团队之间相互对立,甚至可能形成一种指责文化。其下游影响可能相当糟糕。

Jamie,你让我明白了,衡量事件成功真的很困难。事件很复杂,变化多端,涉及大量人际互动,我们确实需要认真思考我们正在建立的激励机制。

但请理解我的立场。业务部门确实想知道我们是否有效。作为团队,我们也应该想知道。我们想知道我们做的事情是否有用且重要。

所以,我们不能只说“相信我们”,或者对我们的实践是否有效、是否让事情变得更好一无所知。

我完全同意。让我们采取工程化的方法来解决这个问题。让我们尝试将问题分解为更小的问题,将事件响应和值班本身视为目标。我们可以同意,无论我们做什么,系统都会出故障。无论工程师多么熟练,故障总会发生。

因此,如果我们在响应方面是有效的,并且我们的事件管理计划因此是有效的,那么哪些事情会是真实的?我们如何衡量它们来证明我们正在朝着目标前进?

那么,有效的事件管理。这是一个好问题。作为负责值班的“多管闲事者”,我的目标是什么?

对于值班本身,我想知道我们的工程师擅长此道。我想知道值班的工程师能及时响应,知道如何使用工具,并且工具运行良好。我想知道他们对此负责,流程能按我们的要求执行,即尽快通知并让具备解决问题知识的人参与进来。我想知道他们将其视为最高优先级的中断。

没有人会在自己的值班期间失联或无法处理事件。

当然。关于工程师认真对待值班并负责这一点,在Datadog,我们相信这在文化上是普遍真实的。这是我们觉得不需要特别去衡量的事情。这也是一种信任的衡量,我们信任我们的工程师和文化,当有人做了不符合既定值班流程的事情时,他们会告诉同事或经理。我们经常分享期望等。同时,重要的是要提醒自己,收集数据不是免费的。在这种情况下,如果我们觉得不需要衡量,也许就不应该花费成本去衡量。

另外,关于人们如何与值班工具互动,我认为值班工具在不需要时应该是隐形的。混乱和额外的工作负担对值班来说是毒药。你不想因为人们试图使用你提供的系统而给他们带来额外的工作负担。因此,在量化这一点时,我们可以考虑系统的直观性。我们可以衡量人们的无心之失。如果有人有多种方式可以呼叫某人,而有人使用了错误的方式,这对你来说就是一个信号。同样,如果有人使用错误的统计分析或工具来跟踪告警,这也是有帮助的。你也可以跟踪发送到Slack但从未被确认的告警数量。如果你确实需要衡量严肃性和责任感,你也可以衡量响应告警的时间,但正如我之前所说,在Datadog我们觉得不需要这样做。

好的,这几乎就像是我们值班工具的产品成功指标

我还想知道,我们不会因为值班而让工程师精疲力竭。当呼叫人们时,他们编写新功能的时间和精力就会减少,而新功能是我获得丰厚奖金的基础。我也相信,经理也是人,希望善待他们的员工。所以,可能我也不想让我的员工精疲力竭。我想知道我们对工程师提出的值班要求,是业务真正需要的最低合理要求,并且在人类长期可持续承受的范围之内。值班对团队来说是一个持续的成本,我们应该知道我们正在最小化这个成本,并考虑防止倦怠。我真的想知道我们没有让值班的负担变得不公平。

当你问一个团队“你们的值班轮换公平吗?”时,他们会回答“是”,因为感觉工作不公的人会更快地精疲力竭。

因此,为了让值班的负面影响最小化,我认为我们可以衡量值班的糟糕程度,特别是在它是否会影响个人生活并导致倦怠方面。我们都知道被寻呼机吵醒很糟糕。那么,一个人有多少个夜班(包括连续夜班)?在这些夜班期间,他们收到了多少个夜间告警?我们是否发现这些负担分布不均?

说到实际收到告警,一个人在一个班次平均收到多少告警?工程师在一个小时内最多收到多少告警?这可以衡量一个班次的通常糟糕程度和最糟糕情况。

但就影响个人生活而言,他们连续值班多少天?这些班次有多长?在任何给定时间段内,他们总共值班多长时间?正如Corey周二所说,这种影响因人而异,在我们的团队内部以及我们合作的团队内部,考虑这种影响的表现形式以及我们如何根据个人生活中的不同责任来最小化它,是非常重要的。

就轮换的公平性和防止人员倦怠而言,并非所有事情都可以量化,尤其是感受方面。但我们可以量化一部分。例如,我们可以分析周末和夜班的分布情况。我们知道周末班或夜班会影响到个人生活,因此我们可以考虑这些班次在轮换中是否分配均匀,在共享轮换的团队之间是否分配均匀。

我认为我们可以处理这些数据。我可以把这些数据带给高管看,并指出那些随着时间推移而改善的趋势线。我喜欢看到改善的趋势。

接下来,我们来看看你可能认为是团队核心使命的部分:在工程师被呼叫之后,有时会遇到真正的事件。什么让一个事件变得“好”?没有事件是好的,但什么让一个事件比另一个更好?

我六个月前在SREcon Dublin看到一个非常棒的演讲,叫《土拨鼠之日》,他们用AI研究了事件响应。作为经理,我很喜欢。那里的结论是,最好的响应者是那些擅长协调和沟通的人。

基于你关于最好的值班工具应该是隐形的观点,我们的事件流程和工具也应该是隐形的。我们能衡量这一点吗,Jamie?衡量我们的工程师是否在有效沟通,是否拉入了需要的人,并在这些人之间保持协调?

我们可以衡量。是的。就有效沟通而言,我们可以考虑对Datadog处理事件的最新功能和流程的采用情况。在这方面我们有些优势,因为我们使用自己的事件管理平台。所以我们可以检查人们是否在使用我们开发并提供的最新功能,比如事件工作屏幕,这在几年前帮助我们管理了一次严重事件。

我们还可以衡量来自专门轮换(为帮助值班人员而设立)的人员被拉入高严重性事件的频率,例如专门处理安全事件的人员、负责大规模事件协调的人员。

就事件工具的隐形性而言,如果你有一条“铺好的路”,这实际上很容易衡量。既然我们的团队为整个公司定义工具和流程,我们可以从覆盖范围的角度思考:人们是否在创建自己的事件自动化来弥补我们留下的空白?还是我们的工具足以满足他们的需求?就隐形性而言,人们知道你提供事件自动化的服务名称吗?他们可能不应该知道。或者他们是否惊讶地发现它是由你的公司构建的,因为它已经无缝集成到他们已有的工作流程中?

还要考虑其中的情感维度。事件的很多方面取决于人们的感受以及他们在响应时的舒适度。因此,即使你的工具和流程非常有效,仍然可能有其他因素让人感到沮丧,导致糟糕的事件体验。我们都知道人们会在愤怒时填写调查问卷,所以创建一个让人们可以在最沮丧时提供反馈的调查机会。这样你不仅可以了解平均体验,还可以了解体验中的异常值。

这种反馈也可以来自对事件期间聊天记录或事后分析进行LLM情感分析。我们在这方面也取得了一些成功。

那么,我知道你告诉我人们认真对待他们的寻呼机。团队是否按照我的定义,以适当的严肃性对待事件?我们是否看到这样的情况:团队在问题得到缓解后就离开了?我们是否没有足够频繁地向利益相关者提供更新?或者可能将事件流程用于并非真正紧急的事情?比如团队因为无法将问题纳入其他团队的OKR而使用事件流程?

我认为我们可以在一定程度上衡量严肃性。从概念上讲,我们可以将事件视为一个状态机。这个状态机的起始状态是影响开始,终止状态是我们跟进完所有行动项、完全缓解所有影响并且事件关闭。

基于这种思维模式,我们可以思考事件在状态机中的滞留位置。我们不希望事件在缓解后没有解决的情况下长期滞留,除非Slack频道中有持续的更新。考虑到状态机的概念,当进入后期阶段时,考虑衡量跟进事件所需数据的完整性也很重要。例如,你希望人们在事件中填写的所有跟踪元数据是否都已记录?对于更严重或影响更大的事件,这些数据是否记录得更一致?这可能是应该的。

还要考虑团队即使在非强制要求时,是否选择撰写事后分析报告。如果一个团队在其范围内发生了一个他们认为有趣的事件,如果有合适的条件,他们会乐于撰写事后分析报告。这也是事件严肃性的一个标志,表明我们确实在反思,即使公司没有强制要求。

至于将事件用于不需要事件响应的事情,比如将问题归咎于其他团队的OKR,这背后往往也存在组织断层线。这有点模糊,很难量化或对组织断层线进行变点检测。但通过跟进事后分析、阅读组织中产生的报告并寻找危险信号是有用的。因为最终,总会有人沮丧到说出“是那个团队干的”之类的话。在你的事后分析中寻找指责性、寻找这类危险信号,可以帮助指出组织断层线所在,以便你投入时间和精力从结构上解决这些问题。

好的,我希望我们擅长处理事件,以快速、彻底地解决客户问题。你告诉我衡量时间无法告诉我们这一点,无论我多么希望它能。

那么,我们找一个有效的代理指标。我们可以假设,感觉准备充分且对响应感觉良好的工程师,是“我们尽了最大努力”或“我们很好地解决了事件”的良好代理指标。这是一个假设,但我可以在高管面前为其辩护。

那么,对于响应事件的工程师,他们是否感到舒适、准备充分并准备好响应?事后他们的自我批评如何?他们是否提到因为技能不足、准备不充分,或者担心系统可能宕机或被指责,而难以跟上或专注于技术挑战?

这是个好问题。正如其他演讲者指出的,进入事件时的感受非常重要。如果人们感到舒适和准备充分,那么他们响应起来就会容易得多。你可以通过量化来衡量一部分,但像我在这里说的许多其他事情一样,你也需要用上下文来限定。

例如,你可以量化流程中角色的使用情况。如果你使用角色,你应该有办法跟踪这一点。如果使用Datadog,你可以通过事件应用免费获得这个功能,并进行衡量,然后检查角色是否在应该出现的地方出现。例如,我们的高严重性事件是否由事件指挥官指挥?最高严重性事件是否由专门的事件指挥官指挥?

同样,你也可以阅读事后分析报告,寻找困惑的危险信号。如果人们感到困惑且没有准备好响应,他们会在事后分析中写出来,尤其是当响应者同时也是报告撰写者时。但这有点模糊,你需要做一些定性分析。在你进行如何运行事件和值班的培训之前和之后做这个分析真的很有帮助。你们都有关于如何运行事件和值班的培训,对吧?

此外,通过调查和在第一次事件后,你可以与某人再次沟通,看看情况如何。有很多方法可以做到这一点。但我认为,关于事后自我批评的想法也很重要。你提到了这一点,说当人们担心被指责时,可能难以专注于技术挑战。

无责文化始于政策层面,但确实需要许多不同领导者的认同才能形成一个无责的组织。进行检查以确保组织的某些部分没有形成指责性做法是很有用的。为此,拥有一个配备人员来掌握事件脉搏(直接或间接)并开始寻找这些危险信号的团队非常方便。如果有什么不对劲,就跟进它。用Pico的无限智慧来说就是:如果你闻到了,就告诉我们。

好的。那么,事件处理的最后一部分:我们与客户的沟通如何?通常我们发现,发生事件对声誉和客户信任的损害,比发生事件但沟通不畅要小。那么,我们是否及时地以客户能理解的语言告知他们问题?

谢天谢地,这很容易量化,因为你已经知道在哪里与客户沟通。有专门的公共和私人渠道与客户沟通。因此,你可以衡量在这些渠道中发布通信的时间。同时,留意客户对已发布内容的反馈,因为在你分析事件并试图尽可能有效地沟通时,你很可能会听到一些关于你所提供更新的反馈。如果你有专门的客户沟通团队,也从他们那里获取一些定性反馈。

好的,还有一件事。我们有一个志愿者升级轮换,对吧?实际上不是SRE团队的成员,而是来自全公司的工程师。他们会被升级到更严重的事件中,以运行协调并帮助推动解决。

这个轮换状态良好吗?人员配备充足吗?是否难以找到志愿者?轮换上的人是否准备好响应?他们只会被呼叫处理真正可怕的事件。他们对事件工具和技术感到舒适吗?他们能胜任事件指挥官的角色吗?反过来,这个轮换是否得到了公司其他部门的信任?当他们出现提供帮助时,这是一件好事还是坏事?

我认为这种认知非常重要,尤其是对于被定位为专家的轮换。在紧急情况下建立信任非常困难。你可以在紧急情况下巩固已经建立的信任,但从一个不熟悉你专家团队的团队从零开始建立信任不是一个好主意。因此,最好主动建立这些桥梁,并通过培训和类似方式让大家了解谁是专家,以及如何以不困难的方式与他们互动。

就量化该轮换的健康状况而言,我认为我们也可以做到。你可以考虑轮换上的人数、他们的平均任期。我们发现对于这个特定的轮换,12到18个月是一个相当好的时间段。同时,也要关注轮换内部交接的一致性。如果他们是专家,并且处理高严重性事件,我们需要确保所有上下文都能在班次之间正确传递。

此外,留意工程师对该协调轮换价值的直接反馈,因为如果感觉不对劲,工程师会提供反馈。

好的,那么我们来谈谈事件之后。如果我把事件看作一个朋友曾向我描述的投资——你被迫预先支付了大部分成本——那么我想知道我是否从这笔被迫进行的投资中获得了最大回报。

那么,我们是否正确地分析了发生的事情并修复了发现的问题?我们是否足够快地进行分析以防止重复发生?这意味着我们需要在分析上进行真正的工程投入,而不仅仅是勾选复选框,并且我们要看到行动上的进展,而不是反复折腾。比如,我不希望在一个事件中看到一个行动项是扩容,而在下一个事件中又看到缩容。

是的,行动项非常重要,有很多方法可以衡量。希望这不需要在橘子上写结构工程方程,但无论对你的工程组织有效的方法是什么。确保分析充分,这在你有事后分析报告时非常有帮助,因为你可以衡量报告的完整性和长度,以确定是否进行了全面分析,并确保这种分析在你的职级体系中。如果你的职级体系中没有撰写事后分析和值班,那么昨天就需要加上。所有严重事件都应该撰写事后分析报告,如果没有,你真的应该知道为什么没有反思那个严重事件。同时,无论是否快速完成,都应尽快开始撰写事后分析报告。我们在内部有一个流程,要求事件关闭后一定时间内开始撰写事后分析报告,强烈建议你也这样做,这样你可以衡量与该标准的偏差。

但同样,思考团队是否对其事后分析和事件使用团队内审查流程也很有帮助。作为一个团队,我们为公司其他小组提供了一个结构化的流程,让他们进行自己的去中心化事后分析审查。这个流程在使用吗?如果没用,团队是否报告这很有价值?

但你也提到了低反复性和投资于高质量修复和行动项的想法。所以我们不衡量事后分析报告中的行动项数量,原因如前所述。但我们确实跟踪行动项完成的速度。这方面没有静态目标,因为有些修复在我们的系统中非常深入,有些则较浅。但跟踪需要多长时间非常重要。

我们寻找团队定义短期和长期行动项,以确保分析不仅仅关注快速修复,而是也关注可以长期进行的事情。我们还监控重复事件,并跟踪每个事件的促成因素类别,观察它们随时间的使用情况,看看我们的模式如何变化。根据我的经验,对重复事件的分析最好手动完成。这是工程工作,让工程师寻找他们以前见过的模式非常有帮助,特别是如果你有一个中央小组一直在阅读大量事后分析报告的话。

我们尝试过用LLM做这个,但还没有取得好的结果。你是说它们不能做所有事情。

好的,我们快到了。我明白如何再次使用这些指标来讲述一个故事,证明我们的事件管理是良好且有效的。但我也想衡量我们为客户构建的东西是否也在变得更好,对吧?我们在改进它们,而不是原地踏步。

Jamie,根据你告诉我的,似乎如果我们做对了,MTTR应该会随着时间的推移而上升,因为我们的事件变得更加复杂,需要更多的东西以新的、有趣的方式发生故障才能构成一个事件。我知道我们仍然无法衡量平均时间,那仍然行不通。但我们能否衡量事件复杂性呢?

是的,我认为我们今天已经讨论了几种衡量事件复杂性不同方面的指标,但我们也可以更直接地衡量一些东西。一个是事后分析报告的长度。更多的反思通常表明有很多复杂性需要梳理。你也可以检查参与事件响应的工程师数量。

还有事件中涉及的角色数量(在基于角色的事件响应流程中)。事件中涉及的角色越多,事件就越复杂,因为它需要更多具备特定技能的专业人员加入这些事件。你还可以检查事件是否需要来自升级轮换的多个成员。这也涉及到事件的长度,所以如果你要衡量这一点,请进行标准化。但如果一个事件需要某人从一个专门轮换交接给该轮换中的另一个人,或者拉入该专门轮换的多个成员,这是一个很好的指标,表明你正在处理一个非常复杂的事件。

你也可以为此使用LLM,这非常令人兴奋。这是对事件聊天频道、事后分析报告或事件产生的任何工件进行情感分析的好地方,你可以做一些分析来确定这些事件的复杂程度。

好的。我在这里提出的问题已经很大程度上暗示了这一点:在很大程度上,我们构建的所有这些指标的客户是我们的内部高管。

那么,这个客户满意吗?他们是否从我们这里获得了所需的信息?大概是关于他们负责的系统的可靠性状态、问题出现的地方等方面的可见性,以便他们在认为有必要时介入。我们是否提供了这些?

是的,既然高管也是客户,我们可以考虑与高管现有的接触点是什么,以及如何优化这些接触点以确保我们也获得反馈,因为高管的时间非常有限。如果你已经有诸如事件季度审查或事件管理反馈会议之类的事情,那么利用这些就很好。我们在Datadog做的一件事是每月发布事件摘要供高管阅读。对你的组织来说可能周期不同,但检查这些摘要的浏览量很有帮助。如果你使用Confluence,那个小眼睛图标就在那里,这有助于了解人们是否真的在阅读、消化和理解这些信息,或者你是否需要在其中一次会议中进行额外的核对。

好的,这给了我们很多指标来向高管展示我们的事件管理流程是成功且运行良好的。

但是,对于那些注意到这并不等同于衡量客户可靠性的高管,我该怎么说呢?我们对事件管理流程的满意度只是我们“有效解决客户问题”的一个代理指标。

我们涵盖了很多基于我们相信该流程能让我们良好恢复来衡量事件流程是否有效的方法,但这与“产品对客户可靠”不是一回事。

那么,我们如何考虑衡量客户体验呢?大家跟我一起说:SLOs

是的,所以如果你想衡量客户的可靠性,请直接衡量。不要使用事件——事件是你处理异常和例外事件的内部流程——作为客户体验的代理,因为正常的客户体验不会被最坏情况所捕捉。

好的,我们实际上已经这样做了。我们俩作为一个团队,并与我们的管理层进行了这样的对话,既讨论了衡量成功的必要性,也讨论了作为团队我们应该如何衡量自己的进展和公司的进展。

进展如何呢?

情况各有不同。随着时间的推移,它通常是有效的,但这是一个你必须进行不止一次的对话。你需要首先与任何有决策权的人就你将衡量的价值观和指标达成初步一致。你需要有管理层相信,衡量事件管理流程和人们的满意度是一个好主意,而不是直接试图构建一个汇总统计量。有些人会抵制。无论你想进行多少次这样的对话,都会有新人加入组织。因此,最好与他们进行一次理解性的对话,了解他们试图实现什么、他们想了解组织的什么,然后解释他们建议的实施方式为什么不符合他们的目标,并尝试为他们构建一个符合目标的方案。这永远不会简单。正如我所说,这是一个持续的对话。即使在你不得不暂时衡量错误指标的情况下,你也可以从你这边保持对话的开放性。想想马拉松,而不是赢得争论或冲刺。

在你持续迭代的过程中,尽早并经常获得对这些指标的反馈非常重要。一旦你为团队定义了成功指标,特别是如果你有一个运行事件管理的团队,其成功由这些指标定义,那么改变和移动这些成功指标就非常困难。因此,在正式做出改变之前,确保所有相关人员以及工作将受此影响的每个人都达成强烈一致非常重要。否则,你可能会被视为一个将事情强加给开发团队的“SRE团队”,这只会重新筑起高墙。

因此,像我之前说的那样,建立一个结构化的反馈流程也很有帮助,创建一个让人们可以在愤怒时填写的调查问卷,但同时也要确保在你与他们合作构建这些指标的整个过程中,人们都能给你反馈。这在一个拥有自下而上文化的大型组织中尤其重要。

但我认为在整个过程中具有挑战性的一点是,基于这些事件指标,获得你需要纠正的趋势的可见性。因为指标太多了,我们正在捕捉组织可靠性的一个非常复杂的图景。这很棘手,我们还没有完全解决这个问题。我们定期检查仪表板,以了解趋势是什么,理解什么是“正常”,以便发现与正常的偏差,建立心智模型,以便了解我们正在看什么以及它可能产生什么影响,在采取行动之前了解每个指标对人们日常生活实际意味着什么。

就像实际的软件系统一样,现实中你无法提前预测所有可能出错的事情。因此,我们很多对新兴问题的持续跟踪和意识,只是通过保持参与、了解正在发生的事情、加入发生的事件、与人交谈、定期让人们有机会来抱怨等方式完成的。

通过这种方式,我们可以看到一些新出现的令人担忧的模式。我们把这些带到团队中,说我们认为需要跟进。我们设计一些跟进措施,进行一些对话。然后停止看到那个模式。

在“你的事件流程是否有效”这个问题上,记住元数据和汇总统计量——特别是如果你通过要求每个团队填写一堆复选框等方式廉价地收集它们——不能替代分析,这一点非常重要。我知道你可能在这次会议上已经听过这个了。但是,深入挖掘并在上下文中理解模式和问题的分析,是无可替代的。汇总统计量不会帮助你。如果值得理解正在发生的事情,就值得真正去理解,而不只是做一个汇总。

当然。我们涵盖了很多内容。虽然你的组织可能不应该衡量与我们完全相同的东西,因为你的组织肯定与我们不同,但这里总结了一些我们在内部密切关注以衡量我们事件管理团队成功与否的事项。很高兴我们让这张幻灯片对手机友好。

当然,这张幻灯片背后有一整篇RFC,包含了我们无法在35分钟内塞进去的更多信息、背景和细节。请考虑你的组织面临的挑战,并将其视为一个需要根据你面临的约束条件来解决的工程问题。

我知道你们总是拍照,但请不要把那个列表当作“这就是答案”。像大多数工程事物一样,构建正确的东西将取决于你的组织、需求、要求、背景和文化。我们想强调的是,将你的事件视为一个内部协调流程,并直接衡量该协调是否成功,与客户端的表现分开,这是有意义的。衡量我们是否妥善处理事件,涉及到定义“妥善处理事件”意味着什么,并理解每个事件都将是独特而特殊的。对“好事情”取平均值没有帮助。

与利益相关者,特别是你的高管合作,围绕这种复杂性以及他们理解事件管理流程的目标建立共同愿景,是构建真正能让事情变得更好、或者至少能给你真正洞察力和可见性的指标的关键,而不是在你的组织中产生浅薄的优化行为、博弈和无意义的工作,这是一个真实的风险。

说到高管,如果你像Laura的角色一样,正在寻找一些东西给你的高管看,以证明我们正在做正确的事情并且持续改进,这里有一个对高管友好的流程图。你可以参考这个来为你自己的事件管理流程构建衡量标准,并请注意这是一个循环。一旦你完成整个过程,你会一遍又一遍地重复它,因为你的组织将不断变化和改进。正如昨天演讲的观点所说,这是一个动词,你需要持续重新评估你的事件管理流程,以保持最佳状态。

谢谢大家。

032:构建全自动硬件SKU选择系统以优化成本

在本教程中,我们将学习如何为 Apache Pinot 构建一个全自动的硬件SKU选择系统,以优化其服务成本。我们将从问题背景出发,逐步讲解系统的设计目标、核心组件、数据收集方法、推荐算法以及无影响迁移策略,最终展示该系统的实际成效。

概述:为何需要多SKU硬件?

大家好,我是 Dino。我们在 LinkedIn 负责 Pinot 项目。Sabrina 现在在 Netflix,但她之前在 LinkedIn 工作。

Pinot 是一个开源的分布式 OLAP 数据存储。OLAP 代表在线分析处理,主要用于对海量数据进行切片、切块和分析,处理规模通常在 TB 到数百 TB 级别。Pinot 的架构包含一个代理路由层和一组承载数据段的服务节点层,它通过下推 SQL 查询来处理大量数据。

本次分享将主要聚焦于服务节点层面。

Pinot 在 LinkedIn 支持多种不同的用例。例如,面向会员的分析功能“谁查看了你的个人资料”;企业级产品如展示人才在全国流动情况的“人才智能生态系统”;以及内部业务洞察工具。这些用例具有不同的查询模式和延迟预期。

与许多 SQL 产品不同,Pinot 中每个查询的成本差异可能非常大。我们的系统规模庞大:数十万 QPS、数万个服务节点、数千个代理节点以及数万个 Pinot 表。

最初,Pinot 部署在 LinkedIn 的通用应用节点上,只有一种主机规格。后来我们增加了 SSD。但随着时间推移,我们面临一个问题:不同的用例需要不同类型的硬件资源。有些是 CPU 密集型用例,有些是内存密集型用例,有些则是存储密集型或 I/O 吞吐量密集型用例。继续使用“一种规格适应所有场景”的策略会导致硬件资源浪费。例如,如果一个用例需要大量磁盘,其 CPU 可能就会闲置。

因此,我们的目标是:在避免引入大量运维负担的前提下,支持多种硬件SKU,并将其集成到生态系统中,以管理这个庞大的主机集群。我们预计这样做可以节省大量成本,初步目标是降低约20%的成本。另一个理由是,从历史经验看,主机规模越大(核心数越多、内存越大),单位成本通常越低。

设计目标与非目标

上一节我们介绍了引入多SKU硬件的背景,本节中我们来看看项目的具体设计目标。

我们的目标包括:

  • 实现约20%的成本节省。
  • 最终支持任意SKU,以适应未来可能出现的新硬件。
  • 在将租户和表在不同硬件间迁移时,不产生性能影响。
  • 实现整个过程的全自动化,避免引入运维负担。

我们的指导原则是: 最小化每个新SKU带来的额外工作。

我们的非目标包括:

  • 不追求提高单台主机的利用率。只要达成成本节省目标,即使利用率仍然较低,我们也认为项目是成功的。

系统架构概览

为了实现上述目标,我们构建了一个包含多个组件的系统。

以下是系统的主要组成部分:

  1. 编排器:负责管理整个流程。
  2. 生产指标与存储:收集关于主机性能的深度指标。
  3. 成本估算与SKU推荐引擎:了解我们支持的SKU及其硬件规格,并给出推荐。
  4. 迁移器:负责将生产环境中的租户和表在不同SKU的主机间进行无影响迁移。

接下来,我们将深入探讨每个部分的设计细节。

方法论:如何为集群选择最佳SKU?

在深入技术细节之前,让我们从高层次思考如何解决这个问题。本质上,我们是在尝试为现有集群构建一个使用新SKU的新集群,通过应用新的硬件维度来使其更具成本效益。

我们需要回答两个核心问题:

  1. 我们需要定义一组指标并系统地收集它们,以分析旧集群的资源利用率。
  2. 基于此分析,我们需要构建某种启发式方法或模型,将这种利用率映射到新的SKU和维度上,从而实现成本优化。

核心指标收集:CPU、内存与存储

要回答第一个问题,我们需要在三个维度收集指标:CPU、内存和存储。

CPU 指标收集

对于CPU,我们的方法是:查看服务于某个用例的给定集群上的CPU核心总数,如果其总利用率低于某个阈值,我们就考虑缩减核心数量。我们将这个阈值设定为60%。这意味着如果总CPU利用率低于60%,我们将缩减核心数,以获得合理的利用率,同时为有机增长保留约40%的余量。

目前,我们并未考虑CPU频率、IPC等更细微的因素。但如果未来要开发更精细的预测模型,可能会将这些因素纳入。

在缩减核心数后,我们仍希望集群能够承受99.99% 的流量,以确保良好的用户体验。

由于Pinot的查询延迟非常低(数十毫秒),我们需要以非常高的频率收集CPU利用率数据——每5毫秒一次。但同时,我们不希望这影响系统性能。经过研究,我们发现 Linux Performance Co-Pilot 非常有用,它能以低开销进行指标收集和发布。

内存指标收集

Pinot的内存使用由三部分组成:

  1. Java堆内存:由Java内存分配和GC算法管理。
  2. 堆外缓冲区:我们使用Java缓冲区库手动分配和管理的缓冲区。
  3. Linux页面缓存:我们使用mmap将数据块从磁盘映射到内存,并通过mmap读取。这部分由Linux系统管理,并体现为Linux页面缓存。

其中,绿色和蓝色部分(Java堆和堆外缓冲区)的总和会反映为 RSS内存。我们收集RSS数据,并在此基础上增加1.5倍的余量作为最终的内存使用量估算。

对于内存映射部分,由于Linux页面缓存的惰性淘汰机制,很难确切知道哪些页面被实际使用。因此,我们通过对RSS内存应用一个系数来为这部分也预留余量。

存储指标收集

存储方面需要考虑两点。首先,需要确保主机上有足够的稳态存储容量来存放所有数据段。

其次,更复杂的是I/O性能。当实际使用这些数据段或将其mmap到内存并开始读取时,会发生分页操作。尽管我们使用高性能NVMe SSD,但分页过程仍受限于SSD的速度,即吞吐量和IOPS。

因此,在实践中,我们像捕获CPU指标一样,需要捕获所有的突发I/O模式(吞吐量和IOPS),以确保在上传新数据段或服务查询时,不会发生资源争用或颠簸,并且带宽对两种操作都足够。

为此,我们也使用 PCP 来捕获NVMe SSD的细粒度IOPS和吞吐量指标。

数据处理与压缩:T-Digest算法

为了使这个过程真正自动化,我们在Pinot生态系统内设计了一个高效的工作流。

一个巨大的挑战是:由于我们以极高的频率(如5-15毫秒)收集指标,且集群规模庞大,数据量会非常巨大。

我们做的优化是:不存储所有时间序列数据,因为我们真正需要的只是百分位数数据。我们以分布的视角来看待数据。为此,我们利用了 T-Digest算法

具体做法是:在一个固定的时间窗口(例如一小时)内,收集了该窗口内所有高频CPU利用率数据后,我们查看其分布。T-Digest算法会选取一定数量(如100到10000个)的数据点作为整个分布的代表点。这是对分布的一种有损压缩。

我们对此进行了充分的实验,并精心选择了参数,使得误差范围非常小。

在每个主机上,我们进行PCP收集。在固定时间窗口后,将数据转换为这些T-Digest点。这些点被序列化为字节并发布到Kafka主题,最终摄入到Pinot表中。

我们使用这种压缩后的分布视图来进行推荐,也就是之前提到的百分位数计算。

T-Digest的一个优点是,我们可以在Pinot内部动态查询和合并它们。Pinot有一个percentileTDigest函数,可以接收一列原始的T-Digest字节数据。给定一个时间窗口和一些过滤条件(例如,你想查看过去X天或某个集群的CPU利用率百分位数),你可以实时编写一个Pinot查询,并指定任何百分位数(如99.99或99.999),它会给出非常准确的估算。

这极大地减少了为如此大规模集群存储多维度指标数据所需的存储空间,并且数据管理、摄入和查询都在Pinot内部实现。

SKU推荐算法:线性规划问题

在获得了集群的资源利用率数据(即我们测量的各种百分位数)之后,用于为给定集群找出最佳SKU和规模的启发式方法就相对直接了。

想象你有一个大的SKU列表。每个SKU都有其核心数、内存大小,并且可以挂载一定数量的磁盘。每个磁盘也有其容量、IOPS和吞吐量。这些是硬件的容量。

我们的做法是:对于这个集群,我们希望配置 X 台此类SKU的主机和 Y 块磁盘,并使磁盘均匀地挂载到所有机器上。

对于每台主机,我们会预留一部分核心供操作系统使用,并为操作系统和守护进程保留一些内存缓冲区。在此之后,我们需要确保集群的总容量(等式左侧)大于服务表所需的实际资源使用量(等式右侧)。资源使用量就是我们在真实集群上测量的那些百分位数。

现在,这个问题就变成了一个线性规划问题:在满足服务特定百分位流量所需的所有容量约束的前提下,最小化使用此类SKU和磁盘的总成本

无影响迁移策略

现在,我们已经了解了容量估算和SKU选择算法的工作原理。下一步就是获取输出结果(目标SKU),并执行迁移。

我们希望在迁移现有SKU时做到无影响。这意味着用户不应察觉到任何停机时间,其性能也不应出现任何下降。

我们实现这一点的关键方法是:同时在新SKU和旧SKU上运行集群,使集群暂时拥有双倍容量。

具体迁移步骤如下:

  1. 分配新主机:在目标SKU上分配新主机。
  2. 添加表/租户:将表和租户添加到新SKU,并给新主机打上标签。此时,表将在双倍容量下运行。
  3. 流量分流:在路由层,大约50%的流量会流向旧SKU,另外50%流向新SKU。
  4. 第一轮评估:评估查询性能(如P95延迟),并查看内存使用、CPU利用率等其他指标,确保一切正常且在预期内。
  5. 禁用旧副本:如果评估通过,则在路由层对旧主机执行“软禁用”。我们并不立即删除数据,只是停止将查询路由到旧SKU。此后,100%的负载将流向运行在目标SKU上的新集群。
  6. 第二轮评估:进入另一轮评估,查看在100%流量下的性能指标。
  7. 完成或回滚:如果一切正常,则完成整个评估,并清理旧主机。如果任何一轮评估出现问题(如查询性能变差),则立即重新启用旧主机,自动回滚,并通知工程团队进行调查。

通过这种方式,我们实现了SKU的无停机迁移。

新表智能配置与SKU感知

迁移现有表之后,我们还需要考虑如何在新表配置中纳入对新SKU的考量。

在Pinot,我们有一个智能配置系统,它早在SKU迁移项目之前就已存在。这个系统接收用户提供的输入,如表特征(列基数、模式)、预期QPS、查询模式、延迟SLO和存储需求,然后将其转换为配置表所需的容量,并自动执行。

这个过程包含两个步骤:

  1. 初始配置:配置新集群,用户可以开始逐步增加QPS。
  2. 扩容评估循环:系统在较低负载下观察表的表现,推断其在达到全负载时所需的容量,并判断是否需要扩容。

我们对这两个步骤都进行了SKU感知改造:

  • 在第一步,我们会选择成本最低且能满足其生产查询需求的最佳初始SKU
  • 在第二步的反馈循环中,我们也会考虑多种SKU,根据集群当前使用的SKU进行评估。

此外,当表在生产环境中进入稳定状态后,用户可能会要求增加QPS或数据保留期(即需要更多容量)。我们还有一个余量估算系统,用于查看其现有集群的余量,判断在给定集群规模下是否能支持更多的QPS。在此过程中,我们也加入了SKU智能,使评估能够考虑集群中可用的内存大小、磁盘大小等因素。

通过以上三点,我们使现有的智能配置系统能够与多SKU协同工作。

便捷的基准测试工具

为了让一切变得更好,我们还有一个非常方便的基准测试工具,供我们团队和客户团队使用。

这个基准测试工具的目的是:用户可以启动一个类生产环境的集群,使用真实的查询和QPS进行测试,以验证集群是否能支持此类用例。

在此基准测试服务中,我们也提供了SKU选择功能。除了设置QPS和查询模式外,我们还提供了一个自助服务界面,让他们可以选择所需的服务器和容量。我们提供了一个SKU池供他们使用,这也方便我们在项目过程中进行验证。通过这个流程,他们能够使用真实的查询测试多种SKU。

项目成果与总结

最后,我们取得了一些非常令人印象深刻的成果。

我们超额完成了目标:仅将20%的集群迁移到混合SKU,就使总服务成本降低了35%。去年,我们将未来硬件预测的总成本降低了57%

随着更多SKU加入我们的备用资源池,我们可以通过智能配置流程,轻松支持更多用例和 onboarding 新客户。

最后需要说明的是,这个项目的成功离不开整个团队的努力,包括未到场的 Florence、Som、Shirayu 和 Saar 等成员。

本节课中我们一起学习了:

  1. 问题背景:单一硬件SKU无法满足多样化Pinot用例需求,导致资源浪费。
  2. 系统目标:实现自动化、无影响的多SKU支持,以节约成本。
  3. 核心方法:通过高频指标收集(使用PCP和T-Digest算法)分析资源利用率,并将其建模为线性规划问题,以推荐成本最优的SKU组合。
  4. 关键流程:设计了双集群并行运行、分步评估的无影响迁移策略,并对新表配置和扩容流程进行了SKU感知改造。
  5. 最终成果:显著降低了服务成本和未来硬件采购预算,并建立了可扩展的自动化硬件管理框架。

033:日交易数十亿美元时的生产工程 🏦

在本教程中,我们将学习在高频交易环境中,如何构建和维护高可靠性的生产系统。我们将通过分析真实世界的重大事故,探讨监控、告警、事件响应以及团队文化等核心概念,以确保在日交易额巨大的金融业务中避免灾难性损失。

引言与背景

大家好,我是 Pedro,来自 Jane Street 公司。我们是一家在全球市场交易金融产品的自营交易公司。本次分享将概述我们在此类环境中如何进行生产工程,以及我们如何看待可靠性问题。

在深入技术细节之前,我想先讲述一个真实发生的事件。

事故案例分析:Knight Capital 事件

2012年8月1日,当时美国最大的交易公司之一 Knight Capital 试图将更新版本的交易系统部署到生产环境。

在旧版本系统中,有一段代码已闲置数年且未得到妥善维护,其中包含一些错误。由于启用该段代码的指令未被使用,这些错误无关紧要。然而,在新版本中,他们重新利用了该指令来指向一段希望启用的新代码。

升级当天,其交易系统套件中的一个系统未能成功升级,仍停留在旧版本。当东部时间上午9:30市场开盘后,这个未升级的系统开始执行那段包含错误、已被弃用且无人理解的旧代码。

在接下来的45分钟内,该公司损失了4.6亿美元,对纽约证券交易所的股价造成了重大干扰,交易了超过3.9亿股公司股票,并积累了数十亿美元的非意向头寸。事故发生后,Knight Capital 的股价暴跌,大约四个月后被竞争对手收购。

虽然事后看来,这似乎是明显的软件工程失误,但其中也包含一些诚实的错误,例如遗留死代码或重新利用配置字段以避免复杂的版本迁移。然而,从生产工程的角度,我们可以得出几个关键教训:

  1. 交易极其危险:在45分钟内轻易损失数亿美元,相当于每分钟烧掉超过1000万美元。
  2. 监控系统必须健壮且冗余:Knight Capital 并非没有监控,但由于交易量激增,其监控系统本身开始落后,无法跟上。在你最需要监控时,它可能会失效。
  3. 告警必须清晰且可操作:市场开盘前,关于那个未升级系统发出了97封告警邮件。如此多的噪音告警可能导致人员脱敏,从而忽略了真正需要采取行动的信号。
  4. 必须授权支持团队采取行动:如果支持团队能在5分钟内意识到问题的严重性并有权采取措施(如关闭交易),损失可能仍然巨大,但或许不至于摧毁公司。

这些教训深刻影响了我们在 Jane Street 构建系统和实践的方式。

交易环境与技术栈概览

接下来,我们看看交易环境在高层次上是如何运作的。

一个交易者想要在市场上交易,例如纳斯达克,大致涉及三个逻辑组件:

  1. 市场数据:交易者需要了解市场动态,即实时行情数据,用于驱动交易系统或供交易员分析,也可用于历史回测。
  2. 订单录入系统:交易者决定下单后,需要一种方式将订单发送到交易所。这包括下单、改单、撤单以及接收成交回报。
  3. 头寸与簿记:需要跟踪实际交易的头寸,用于会计、监管报告等目的。作为交易公司,我们有责任遵守严格的合规标准。

本次分享将主要聚焦于中间的订单录入系统,因为它是连接内部系统与外部世界的边界,也是防御问题的最后一道防线和感知外部问题的第一道防线。

订单是什么?

一个订单是指令,用于以特定价格、特定数量买入或卖出某种金融工具。其基本元素包括:

  • 方向:买入 (BUY) 或卖出 (SELL)
  • 数量:交易多少单位
  • 代码:金融工具的标识符
  • 价格:交易价格
  • 货币:计价货币

想象一下这些元素中任何一个出错:方向反转、数量错误、货币单位弄错,都可能导致灾难性的交易错误。

另一个警示故事:日本“胖手指”事件

2005年12月8日,日本一家大型券商的交易员试图发送一个订单:以60万日元的价格买入1股J-Com公司的股票。

不幸的是,他输错了指令,变成了:以1日元的价格卖出60万股J-Com股票。

这一个打字错误,最终导致该公司损失超过270亿日元(约合当时的2.23亿美元),并导致日经225指数因此单笔订单下跌近2%。更糟糕的是,交易员试图撤单,却因东京证券交易所系统的故障而未能成功。

这个故事的启示是:

  • 每一笔订单都至关重要
  • 交易所等外部世界的问题也会直接影响你的交易能力

这些真实事件驱动着我们内部进行改进,有时我们称之为“事件驱动开发”。

技术问题分类:故障与中断

在技术层面,我们面临的问题大致分为两类:

  1. 故障:订单中的错误可能立即转化为灾难性损失。在自动化交易中,小错误会以远超人类的速度迅速累积成巨大损失。
  2. 中断:故障可能导致中断,而我们对故障的响应(如关闭交易)本身也会造成中断。

交易中断的后果不仅仅是机会成本损失,还包括:

  • 关系损害:依赖我们进行交易的交易所和机构对手方,在我们中断时可能转向竞争对手。
  • 监管关注:作为大型市场参与者,中断会给整个系统带来压力,可能招致监管机构的审查、罚款甚至暂停交易资格。
  • 增加风险与成本:如果交易策略持有大量头寸并计划在未来对冲,交易中断会阻止对冲操作,从而主动增加公司风险。

此外,一个关键点是:高影响、高风险场景通常与系统高负载正相关。市场繁忙时,系统负载更高(更容易出问题),同时头寸更大、交易价值更高(出问题成本更高),形成了一个负反馈循环。

交易领域的独特性

交易环境与其他科技公司相比有一些独特之处:

  1. 时间依赖性:市场有明确的开盘和收盘时间(如纽交所9:30-16:00)。这意味着:

    • 开盘前有一个“无影响缓冲期”,可以修复问题。
    • 开盘和收盘时段价值最高,也最需要确保系统稳定。
    • 虽然有些市场(如衍生品、加密货币)交易时间更长,但股票市场目前仍遵循此模式。
  2. 技术微调与低延迟:我们采用托管服务,将服务器部署在离交易所最近的数据中心,甚至同一个机柜内。这是因为光速限制下,几纳秒的延迟差异在高速竞争性市场中至关重要。这要求我们使用大量定制硬件和自研软件,优化部署。

  3. 业务高度复杂:金融产品、不同市场(美国、欧洲、亚洲)的规则差异巨大。我们的用户(交易员)是领域专家。事件响应高度依赖于具体的交易上下文。有时,部分恢复(如恢复20%功能)可能实现90%的价值,支持人员需要具备业务知识来做出高质量决策。

监控策略:技术健康与交易影响

监控在这种环境中不是可选项,而是必需品。我们宁愿关闭大部分交易系统,也不会在没有监控的情况下运行。

我们将监控大致分为两类:

  1. 技术健康监控:关注软件和硬件的运行状态(内存、CPU、网络延迟、丢包率、订单发送能力等)。这主要由工程师负责。
  2. 交易影响监控:关注系统是否发送了预期的订单、累积的盈亏、市场对交易的响应等。这主要由交易员负责。

两者对于评估是否即将犯下大错都至关重要。本次分享主要聚焦技术健康监控。

订单流与监控挑战

在订单录入系统中,基本的消息流包括:发送订单、取消订单、接收成交回报(包括确认、拒绝、成交)。

基于SLO的监控在此面临挑战:

  • 可用性SLO:任何停机都可能立即需要关注并引发监管审查,简单的“99.99%”可用性指标不够。
  • 错误率SLO:虽然比可用性稍好,但任何订单被拒绝都可能影响关键时刻的操作能力。
  • 延迟SLO:平均或高分位延迟是问题的前兆。但尾延迟在高速交易中同样关键,因为策略可能建立在微秒级响应的假设上。

我们的方法:基于事件的告警

对于实盘交易监控,我们更多地依赖基于事件的告警。前提是这些告警必须是高质量的、非噪音的、且始终可操作的。

这种方式的好处包括:

  • 高细节度:直接了解发生了什么错误。
  • 快速定位:告警可直接链接到引发错误的代码位置。
  • 快速响应:为支持团队提供最快的事故反应能力。

我们仍然使用基于指标的告警来观察长期健康趋势(如内存使用增长),但它们不用于触发需要立即关注的交易事件。

支持轮值与事件响应流程

以支持美国交易所订单流连接的纽约工程师为例:

  • 交易时间:9:30 - 16:00。
  • 支持班次:通常分为9:00-13:00和13:00-17:00两班,跟随太阳周期与全球办公室协作。
  • 频率:工程师大约每4-6周轮值一次。在非事件处理期间,他们可以进行项目工作。

当遇到事件时,标准流程如下:

  1. 评估影响:确定发生了什么、影响范围(哪个交易台、哪种工具、哪个市场)。
  2. 评估严重性:估算损失金额,通常需要与交易员协作。
  3. 寻求专家:如果无法自行解决,立即寻求主题专家的帮助。
  4. 沟通与行动:大声沟通你的行动,因为任何操作本身都可能带来风险。
  5. 修复与迭代:实施修复,并重复直到问题解决。

一个虚构的响应示例

  1. 9:30:市场开盘。我们观察到仅在纳斯达克交易所出现订单拒绝。
  2. 初步结论:影响仅限于该交易所。进一步发现,拒绝都来自一个特定的交易系统,且只涉及部分美国ETF。
  3. 联系交易台:确认该交易台的所有订单都无法发出。
  4. 主动暂停交易:立即暂停该系统的交易功能,防止更多错误订单发出或造成网络拥堵。同时通过内部广播系统通知所有交易用户。
  5. 深入调查:发现该系统最近刚升级了新版本,消息格式有变更。
  6. 解决方案:与专家讨论后,决定回滚该系统。
  7. 恢复:大约5分钟后,系统恢复在线。

从发现事件到暂停交易缓解影响可能只需几分钟,再到完全解决可能在十分钟内。事后需要进行复盘,分析总成本、变更流程、能否通过灰度发布更早发现问题等。

团队文化:无责复盘与心理安全

当出现问题时,很可能有人犯了错误。我们坚信在进行复盘时,应采用无责文化

我们努力营造一种隐藏错误比犯错更糟糕的文化。金融史上充满了“流氓交易员”的例子,他们为了掩盖一个小错误,进行更冒险的交易,最终导致巨额损失甚至公司破产。

在我们公司,当你犯错时,重要的是能够举手说:“我搞砸了,我们一起修复它。”十有八九,严重问题的根源不是单个人的失误,而是流程上的缺陷,是系统允许这个错误发生。

这种文化需要反复灌输和实践。它对于建立心理安全、鼓励透明沟通至关重要。

生产工程项目与工具

最后,我们看看生产工程师会构建哪些工具来提升能力:

  1. 连通性监控仪表盘:一站式查看所有交易所和交易伙伴的连接状态、交易金额、消息速率、延迟等信息。更重要的是,能够直接从该界面执行操作,如连接、断开或暂停特定交易所的交易。
  2. 性能基准测试工具:由于使用大量定制硬件,我们需要工具在特定硬件上自动定义和运行任意测试,确保系统性能符合预期。
  3. 全局交易暂停服务:集中管理哪些交易功能被启用或禁用,并清晰记录原因。当交易员询问为何无法下单时,可以快速给出解释并提供更多详情链接。支持团队也能从此界面高效控制交易状态。

总结与核心原则

在本教程中,我们一起学习了在高频交易这一价值决策以分秒计的业务中,如何构建可靠的生产工程体系。

核心原则总结如下:

  • 快速正确响应至关重要:我们通过使用可操作的、基于事件的告警来实现快速响应,这些告警必须提供高细节度信息。
  • 监控系统是关键且必须最健壮:我们视监控系统为关键基础设施,其可靠性必须最高。我们宁愿先关闭交易系统,也不愿在监控失效的情况下运行。
  • 支持人员需要业务知识:赋能支持团队,让他们掌握足够的信息以做出高质量的现场决策,减少不必要的升级耗时。
  • 授权支持团队在紧急情况下采取行动:支持工程师应被授权在感到风险时做出决策,而不必总是等待升级。

在这样一个每分钟都可能损失千万美元的行业里,这些原则是保障生存与成功的基石。

034:系统思维与“中毒”系统 🧠💀

在本节课中,我们将探讨在系统监控和运维中引入人工智能(AI)工具后,可能面临的独特挑战。我们将学习“中毒系统”的概念,理解AI作为工具的局限性,并讨论如何运用系统思维和韧性工程原则,在复杂且可能具有误导性的环境中保持有效运维。


引言:当系统变得“有毒”

大家好,我是Dan Deep。今天我们将讨论“系统思维与中毒系统”。

我是Niberley基金会的研究员,同时也是CNG基金会Defaf和Hard He工作组的成员。这位是Cindy,她是一名软件工程师,拥有丰富的SRE实践经验。

我们经常需要处理故障系统。但今天,我们要讨论的是处理那些不仅“坏了”,而且可能具有“误导性”的系统。想象一下,当你查看监控面板和日志时,根本原因被层层自动化所掩盖。现在,再把AI引入这个混合体。

AI承诺帮助我们更好地理解系统、检测异常甚至预测故障。但如果AI是基于不完整或被“污染”的数据进行训练的呢?我们常听说“垃圾进,垃圾出”。但当运维系统开始依赖AI时,我们得到的是黄金,还是介于两者之间的东西?这引出了我们的第一个核心观点。


AI是一种有缺陷的理解工具 🤖⚠️

上一节我们提出了AI可能不可靠的观点。本节中,我们来看看为什么AI本质上是一个有缺陷的理解工具。

AI驱动的监控和调试工具并不能真正理解系统。它们只是基于历史数据寻找模式。在SRE领域,我们使用工具来监控和优化系统,但功能强大的工具也带来了新挑战:它们不仅反映现实,也在塑造现实,有时甚至会误导我们。

AI模型从数据中学习。如果数据本身有问题——无论是由于错误、不一致还是人为操纵——那么AI得出的结论也将是错误的。当我们盲目相信这些有缺陷的AI输出时,就会做出错误的决策。

核心公式有缺陷的训练数据 -> 有缺陷的模型 -> 有缺陷的决策

AI并不理解它所要照看的系统。它只是基于数据模式进行概率性猜测,而非真正理解因果关系。

在本节接下来的部分,我们将探讨三个突显AI局限性的关键问题。

以下是三个主要问题:

  1. 数据污染:AI依赖历史数据。如果数据不准确、不完整、有噪声或有偏差,模型就会学到错误的东西。数据污染可能是无意的(例如,本应标记为“正常”的系统状态被错误标记为“异常”),也可能是有意的恶意攻击。一旦数据被污染,基于此训练的AI模型就会输出错误结果,而这些错误结果又可能成为后续模型的训练数据,形成恶性循环。更危险的是,如果AI系统从未见过真正的故障,它可能会假设这种故障根本不存在,从而错过真实的关键故障。
  2. AI偏见:AI模型会强化训练数据中的模式,而不会质疑这些模式是否应该被使用,或者是否公平地代表了整体情况。由于AI从历史事件中学习,它并不总能捕捉到现实世界的全部复杂性。例如,一个AI驱动的修复系统可能会优先采用过去的解决方案,即使该方案是错误的或次优的(比如因为CPU飙升就建议重启服务,而重启可能导致更严重的全面中断)。AI也可能低估新型故障的重要性,因为它没有针对新基础设施(例如服务网格)进行训练。
  3. AI相关的技能退化:这是一个正在发生的概念。当人类过度依赖自动化工具时,可能会失去某些高级技能和机构知识。这就像一个小镇上的手语逐渐被遗忘一样。在SRE领域,如果我们盲目遵循AI的票单而不加思考,我们可能会丧失关键的系统性思维和故障排查能力。

总而言之,AI不是一个可靠的决策者。它是一个需要持续学习和人工监督的工具。讽刺的是,它有时非但没有简化故障排除,反而增加了复杂性。


实践中的挑战:当AI“自信地”犯错 🧪

上一节我们从理论层面探讨了AI的局限性。现在,我们通过一些实际例子,看看这些挑战在现实中是如何体现的。

让我从一个故事开始。有一次,我需要使用一个团队都不太熟悉的工具来完成一项任务。我向同事求助,他给了我一个命令行参数建议。这个参数看起来完美地解决了我所有问题,这让我觉得“太巧了”。结果执行时,系统提示“无法识别的标志”。我回头问同事,才发现这个建议是他从AI聊天工具那里得到的未经核实的输出。这引出了一个深刻的问题:我们为什么会盲目相信从互联网(或AI)上得到的任何东西?

以下是几个具体的案例:

  • 案例一:缺失文档的困境:在Google Cloud环境中,一项新的“软删除保留策略”没有官方的Python SDK文档。我向AI工具求助,它“自信地”给出了多种方法,但这些方法循环往复,均不奏效,浪费了大量时间。最终,我通过查阅源代码和测试用例,找到了正确的API调用方式,再让AI基于此生成代码,才成功解决问题。这说明AI在缺乏训练数据的领域能力有限,需要人类的引导和验证。
  • 案例二:隐藏的偏见与错误假设:如果你让AI生成一张“用左手写字的人”的图片,它很可能会生成用右手写字的人像。另一个例子是签证查询:AI可能错误地断言“持有美国签证的印度公民不能在加拿大转机”,而实际上这是可以的。你需要不断纠正它。在系统层面,我们有一个Cassandra集群通常运行在90%的内存使用率(这是其设计使然,性能正常)。但引入的AI系统却断定其“性能糟糕”,因为它基于“高内存使用率等于问题”的通用假设。
  • 案例三:脱离上下文的“优化”:我们曾让一位新同事优化一个Elasticsearch系统。他使用AI工具生成了非常详细、看似专业的优化方案。然而,他并不了解业务背景:这个系统已被标记为“仅维持生命”,目标是将每月成本从12,000美元降至4,000美元,且不能进行新的开发或重大维护。我采取的方案是调整索引策略(例如,将日索引合并为月索引,再合并为年索引),在不改动应用逻辑的前提下显著降低成本。而AI工具在没有此上下文的情况下,给出的建议很可能是“过度优化”或方向错误的。

这些例子表明,AI驱动的可观测性、告警和事件响应工具仍在演进。AI的建议正变得越来越难以挑战,因为许多组织依赖自动化决策。但正如我们所说,你需要的是“人在回路中”,而非完全自主的系统。


解决方案:系统思维与韧性工程 🛡️🔧

面对可能“中毒”的系统和有缺陷的AI工具,我们该如何应对?本节我们将探讨如何运用系统思维和韧性工程来构建韧性。

我们做的很多事情都关乎系统思维,其核心在于弄清楚如何做不显而易见的事,如何处理复杂事物并使其可运作。但当系统本身“坏了”、“有毒”时,你该如何进行系统思考?

这让我想起电影《公主新娘》中的一个场景:主角面对两杯毒酒时说:“不要犯傻,我两杯都下了毒。” 关键在于,与其试图避免所有错误,不如变得非常擅长“喝下毒药”。在一个不完美的世界里,没有完美的系统。AI也好,中毒也罢,重点在于变得善于在问题中前行。

一个韧性系统是“社会技术性”的,它包含人、工具、应用程序、计算机以及它们之间的交互。韧性不仅仅关乎正常运行时间或MTTR。韧性是关于能够利用现有条件,在系统发生问题时仍能持续推进的能力。

我们从不盲目信任自己的代码,也不会毫无保留地信任同事的代码。我们会验证、检查、思考。我们对工具也应如此。工具本身需要具备韧性,而我们使用工具的过程和方法也需要具备韧性。

人类理解世界的方式很特别:我们观察环境,并构建工具来帮助我们理解环境。然后,我们迭代这个过程:学习工具、根据自身上下文调整工具、改进工作流程。这就是工具的进化,也是我们传授系统思维和故障排查的方式(例如,“遇到问题先检查DNS”这样的流程图)。

现在,如果工具(AI)本身有点模糊、不稳定、不可预测,我们只需将这一特性纳入我们的流程和思维中。以“黄金信号”为例,传统的信号是延迟、流量、错误和饱和度。在AI时代,这些信号可能变形为令牌速率、上下文丢弃、模型运行状况等。但万变不离其宗。形式不同,本质相同。

系统思维从来都是混乱且人性化的。你为了理解事物而构建的工具总是摇摇晃晃、脆弱的。你的系统越复杂,你就越需要能够解释你的工具如何工作。这个过程一直都很混乱,将来也是如此。

这里有一个著名的讽刺,即“自动化的悖论”(一篇1970年代的论文):你自动化得越多,系统越好,但运行它所需的人却需要懂得更多、更精深。就像从可以手动修理的简单汽车,进化到需要大量专业设备和知识的现代汽车。事物越复杂,工程师就需要越能适应并与之共事。

关键在于,你必须将整个系统作为一个整体来思考。


总结与启示:培养更好的工程师 🧑‍🔬🚀

在本节课中,我们一起学习了在SRE实践中引入AI工具后可能面临的“中毒系统”挑战。

我们探讨了AI作为理解工具的三大缺陷:数据污染、AI偏见和技能退化。我们通过实际案例看到,AI会“自信地”犯错,尤其是在缺乏上下文或训练数据的领域。最后,我们指出,解决方案在于回归系统思维和韧性工程的核心——接受不完美,将AI视为需要人类监督和引导的工具,并持续培养工程师深入理解系统和批判性思考的能力。

让我用一个关于“999美元的故事”来结束。一艘船的引擎坏了,许多专家都无法修复。最后请来一位老师傅,他用锤子在一个地方敲了两下,引擎就恢复了。他开出的账单是:敲一下值1美元,知道敲哪里值999美元。

这个故事对SRE工程的启示在于:未来不在于构建更好的锤子(工具),而在于培养知道敲哪里的更好工程师。面对“中毒”的系统和AI,深厚的系统知识、批判性思维和适应能力,才是我们最宝贵的资产。


035:应对DevOps团队过载的实用指南 🚀

在本节课中,我们将要学习如何识别和应对软件团队中常见的工作过载状态。我们将探讨过载产生的原因,并介绍一些实用的策略来改善这种情况,帮助团队更高效、更从容地工作。


引言:无处不在的过载状态

我是Alex Wise。本次演讲的主题是“没有时间做完所有事”,主要讨论软件团队中那些工作多得无法及时完成的情况。我们将思考这种情况为何会发生,并希望提供一些可以带回家的技巧,或许能让情况变得更好。

我在社交媒体上很活跃,喜欢与人交流。我长期专注于可靠性的软件工程师工作,经历过从初创公司到大型企业的各种环境。

这是我的狗Clover。它也喜欢响应事件——当然,仅限于食物掉在地上的时候。在我工作过的所有地方,我经常看到或亲身经历团队面临的工作量超出我们实际能及时完成的情况,我们不得不就质量、时间安排、周末或夜间加班等问题做出艰难的权衡。

文献中将这种状态称为“过载”,而我更习惯称之为“忙碌”。

如果你曾在软件团队中经历过忙碌,请快速鼓掌示意。是的,我认为这非常普遍。这种状态不仅令人不适和沮丧,如果持续时间过长,还会产生有害的影响。

以去年的CrowdStrike事件为例,它指出了我们常见的生产压力:交付的压力、质量与修复之间的艰难权衡、中断性工作等等。过载状态非常普遍,并且长期处于其中可能相当糟糕。

因此,我希望能深入探讨其发生的原因,并找出一些改善的方法。


理论基石:适应性宇宙与过载响应

我首先尝试在网络上寻找答案,但未能找到立竿见影的解决方案。于是,我决定反思自己作为软件工程师的经验。

经过思考,我得出了关于世界的两个基本真理,我称之为“Alex的真理”:

  1. 资源是有限的:我们的时间、金钱、精力和注意力都是有限的。
  2. 变化是持续的:世界在不断变化和移动,我们必须适应和响应。

后来我发现,这实际上与David Woods博士的“优雅可扩展性理论”不谋而合。该理论认为,随着世界的变化,我们必须适应,并且不可避免地会触及时间、资源、精力和注意力的极限,从而感到不适和过载。

这个理论帮助我们理解,当讨论技术债务或造成实际影响的故障时,我们不应首先指责团队,而应抱有同理心。因为根据适应性宇宙的观点,系统有时会自然地处于过载状态。这也论证了我们需要高效利用资源的重要性,以便在需要适应变化时,能更快速地摆脱过载。

然而,这个理论没有完全解答我的疑问:如何区分过载带来的痛苦和其他原因造成的痛苦?

Woods博士在《实践认知系统工程》一书中提供了一个框架,阐述了系统面对过载时的四种可能响应方式:

  1. 卸载:不做某些工作。
  2. 降低彻底性:草率完成工作。
  3. 时间转移:稍后再做。
  4. 招募资源:引入帮助。

通常,后两种(时间转移和招募资源)能在我们的系统中产生更积极的结果,但它们需要预见性、规划和资源成本。

这个框架同样适用于由人和机器组成的“联合认知系统”,例如你的软件交付团队和他们处理的JIRA工作队列。如果我们反复听到“为什么这个变更没提交?”或“我们不是说几个月前就修复这个吗?”,这可能表明团队正在用过载的方式响应。同样,当团队使用积极的、基于计划的响应方式时,我们应该予以庆祝。


深入分析:知识衰减与系统僵化

为了理解我们自身的行为是否会加剧或改善过载,我建立了一个简单的软件团队模拟模型。

我们模拟一个由4名工程师维护的单一服务。第一年编写15万行代码,之后不再添加新功能,但会模拟人员流失。我们引入Bug:每X行代码会产生一个Bug,修复时间取决于代码作者是否在职。

我们测量的是每年用于修复固定数量Bug所花费的小时数。模拟结果曲线显示:

  • 第0年:上下文清晰,修复迅速。
  • 第1-3年:随着原始作者离开,修复相同数量Bug所需时间逐年增加,团队明显感到过载压力。
  • 第4-6年:时间消耗稳定在高位,系统陷入一种僵化但可预测的“混乱”状态。

如果我们将每年的修复时间上限设定为第0年的水平(例如1500小时),那么被修复的Bug数量会大幅下降,而Bug积压则会无限增长,形成“无尽的待办事项清单”。

我将这种现象称为知识衰减:随着对系统认知的退化,我们适应变化世界的能力、系统的弹性都会受到影响,事物趋于僵化。鉴于软件构建的经济性,我们遭遇这种情况的频率可能超出预期。

那么,如何应对知识衰减?

1. 保留员工:尽可能保留员工是超级能力,但并非总能实现。
2. 在编写代码时共享知识:我们之前的模型假设每行代码只有一个作者。如果采用结对编程,让多人同时拥有代码上下文,结果会大不相同。模拟显示,结对编程能显著延缓知识衰减的速度,直到所有原始作者都离开。
3. 主动进行系统迁移:如果我们在第4年决定重写一半的代码库(例如将旧架构迁移到Kafka),修复时间会大幅下降,相当于“重置了时钟”。这让我想起了Will Larson关于“技术债务唯一可扩展的修复方法是迁移”的论述。

主动迁移需要锻炼一系列组织能力:

  • 识别迁移目标:问“如果我们明天重写这个,它需要做什么?”,而不是“这个怎么工作?”。
  • 建立共识:决定具体做什么(例如,迁移到Kafka还是更换数据库)。
  • 降低风险:分阶段、小步快跑地进行,减少对客户的影响。
  • 向业务方推销:不能只做简单的投入产出分析,需要构建让业务方能理解的变革叙事。

4. 打破“谁碰谁负责”的文化:在受知识衰减困扰的系统中,容易形成“谁碰了出问题的代码谁就有责任”的文化,导致无人愿意接触晦涩难懂的部分。这会使系统知识加速流失。
应对方法包括:

  • 明确所有权(必要但不充分)。
  • 举办午餐学习会,鼓励分享晦涩系统的知识。
  • 调整奖励机制,表彰那些梳理和理解遗留系统的贡献。
  • 资深工程师带领新人一起探索代码库,重建认知脚手架。

流程优化:队列理论与在制品限制

那个不断增长的待办事项清单也让我想起了Donald Reinertsen的《流》一书。他将软件团队建模为数学队列(如M/M/1队列),并得出了两个对本次演讲至关重要的见解:

1. 不要使容量饱和
当团队利用率达到100%时,吞吐量会骤降,工作完成时间的变异性(不可预测性)会急剧上升。实际上,当利用率达到约75%时,这些负面影响就开始显现。因此,让团队避免过载不仅有文化或安全上的理由,也有强大的经济学理由——过载的团队表现会更差。

2. 批次大小很重要
大型工作批次(例如更换整个公司的身份提供商项目)会带来指数级增长的交货时间、更高的变异性,并且具有自我强化的特性——一个团队的大批次工作会导致依赖它的其他团队也形成大批次,问题在整个组织中传播。

为了应对这些问题,Reinertsen和其他人提出了在制品限制的概念。设定团队同时进行的工作数量上限,有助于改善那1%的、无故耗时极长的“尾部延迟”工作。设定WIP限制的方法有多种:

  • Reinertsen方法:设为平均WIP水平的两倍。
  • 约束理论:针对最大瓶颈设置全局速率限制。
  • 避免利用率陷阱:任务数少于“处理者”(工程师)数量。

过载不仅发生在团队层面,也发生在组织层面。John Cutler的“组织在制品”关系图展示了WIP上升如何导致吞吐量下降、学习速度变慢、士气低落、错过截止日期,进而引发仓促招聘,最终又反馈回WIP的上升。这个关系图也表明,组织层面的过载也会表现出“卸载”、“招募资源”等响应模式。


总结与回顾

本节课中,我们一起学习了如何理解和应对DevOps团队的工作过载状态。

我们首先从David Woods博士的理论出发,理解了过载是系统在有限资源下适应持续变化的自然状态,并学会了识别系统对过载的四种响应方式。

接着,我们通过模拟发现了“知识衰减”现象——即系统认知随时间退化导致维护成本飙升。对抗知识衰减的策略包括:尽可能保留员工、在编码时通过结对编程等方式共享上下文、以及主动规划和执行系统迁移以刷新团队知识。

然后,我们借鉴了Donald Reinertsen的队列理论,认识到让团队容量饱和(利用率100%)会严重损害效率,而缩小工作批次大小设置合理的在制品限制是优化流程、缓解过载感的关键。

最后,我们看到过载问题会在组织层面形成复杂反馈循环,需要系统性的而不仅仅是英雄式的干预。

希望这些思路和策略能帮助你更好地诊断团队的过载状态,并采取有效措施,带领团队走向更可持续、更高效的工作节奏。

036:SRE大会-2025-美洲-|-srecon-|-分布式-|-缓存-|-OpenTelemetry-|-安全-|-AIOps-p36-P36-Securing-Distributed-Cache---Achieving-Secure-by-Default-with-Key-Challenges--BV1TmLDz7EZZ_p36-

课程概述:Netflix分布式缓存安全实践 🛡️

在本教程中,我们将跟随Netflix的工程师团队,深入探讨他们如何保障全球范围内每秒处理数百万请求的分布式缓存系统(EVCache)的安全,同时维持亚毫秒级的延迟。我们将学习其安全架构的核心组件、面临的挑战、真实事故分析以及从中汲取的最佳实践。


章节 1:EVCache 系统简介 🏗️

我的名字是Aash Steve Boyyle。我是Netflix分布式缓存平台的高级软件工程师。

与我一起的还有Shiam和Sam。我们共同管理着分布式集群,确保其安全运行,为流媒体、游戏、直播和广告平台每秒处理数百万次请求。

今天,我们将带您了解我们如何在保障这一切安全的同时,在全球范围内为每秒数百万次请求维持亚毫秒级的延迟。

让我们开始这次旅程。在这次旅程中,我还会分享一些关键挑战和见解,包括我们遇到的事件、如何处理它们,以及一些我希望大家都能带回去的经验教训。

左边是一个熟悉的界面。这是您访问Netflix.com时打开的页面。

右边是使这一切成为可能的调用关系图。

Netflix采用高度微服务化的架构。

有大量的微服务在协同工作,支撑着您在Netflix所做的每一件事。

例如,这个主页对于Netflix的每一位订阅者都是独一无二的。

为了实现这一点,有预计算的微服务和页面渲染微服务在发挥作用。

几乎所有这些微服务都处理着海量的数据,包括离线和在线事务处理。

在这些微服务链的最末端是数据访问层,它们由我们的分布式缓存EVCache在瞬间提供支持,这也是我们将要讨论的内容。

那么,EVCache到底是什么?EVCache是一个分布式、分片且复制的键值存储。

它是在Memcached之上内部开发的。

左边展示了一个非常简单的部署,显示了一个单写入器设置,应用程序可以存在于每个可用区。

默认情况下,我们优先使用本地区域的调用。但在故障回退的情况下,应用程序也可以跨区域访问。

EVCache支持区域内和全局复制。这意味着我们可以跨一个或多个可用区复制数据,也可以跨区域复制。

EVCache能够抵御故障。我们可以容忍一个节点宕机、一个可用区宕机,甚至整个区域宕机,这得益于我们健壮且有弹性的基础设施。

EVCache客户端是拓扑感知的。它确切地知道一个键应该存储在何处,并直接与该键所在的节点通信。在发生故障时,它知道如何回退到不同区域进行重试。

我们的整个基础设施是线性可扩展的,取决于客户端每秒的读写工作负载。

我们拥有健壮的缓存形成基础设施,确保当出现故障时,它们能够热启动并准备好供客户端使用。

看看我们运营的集群概览。EVCache在四个不同的区域运行:美国东部1区、美国西部2区、欧洲西部1区和美国东部2区。

我们运行着200个集群,涵盖22000台服务器,每秒处理4亿次操作,存储着2万亿个数据项,占用14.3PB的存储空间。

现在,我们将向您展示我们如何平稳地运行这一切。

让我们深入了解一下从客户端开始的示例数据流。

这是一个非常简单的流程,客户端访问Memcached,Memcached在高层上由监听器和工作线程组成。

监听器分配一个工作线程来处理请求,然后该工作线程访问Memcached存储。

存储部分我将在接下来的幻灯片中更深入地讲解,我们会运行哪些类型的存储,然后将请求返回给客户端。

这是Memcached存储的第一个版本,纯内存版本。在这种情况下,工作线程纯粹将键及其值存储在内存中。所有内容都驻留在内存中。

现在,这是一个更高效的设置,即Memcached的双混合模式设置。在这种情况下,热键是那些被频繁访问的键。

它们及其值纯粹驻留在内存中,而冷键或者说长尾数据,这些值被刷新到EX存储,即Memcached的基于磁盘的版本。

请记住,即使是那些值的键,为了快速访问和查找,它们仍然在内存中。


章节 2:客户端安全实现 🔐

现在您看到了大量数据通过不同渠道流动。这就引出了一个问题:这一切都安全吗?在安全至上的时代,对于每个Alice和Bob,我们都知道总有一个Eve或黑客在等待窃听和嗅探数据包。

那么我们如何安全地运行它?这就是TLS或传输中加密发挥作用的地方。

这是我们的集群如何安全运行的一个非常宏观的概述。在左边,您从客户端开始,客户端与内置SSL缓冲区的Memcached建立一个双向TLS通道,而Memcached又使用OpenSSL结构与Memcached工作线程通信。

现在我们将深入探讨客户端和服务器端的这些细节,讨论客户端如何融入这个图景。首先,我将邀请我的同事Shera,他将为您深入概述客户端方面。

谢谢,Akaash,很棒的营销策略。我也给大家五分钟时间,或许您可以再次拿起手机注册Netflix。Aash对EVCache做了很好的介绍。正如大家所知,EVCache有两个组件:EVCache客户端和EVCache服务器。

在接下来的10到15分钟里,我将从客户端的角度讨论安全方面。Sam将从服务器的角度讨论安全方面。

我的演讲将快速介绍什么是EVC客户端,它的最重要特性是什么,然后深入安全方面。让我们开始吧。

什么是EVC客户端?它是一个开源库。可以在Github上找到,如果您还没有,请去查看。EVC客户端用于从EVC服务器获取或设置数据。这里的EVC服务器是一组缓存节点。

EVC客户端最重要的特性是什么?它是一个“厚客户端”。厚客户端意味着所有优秀的分布式系统逻辑都存在于EVC客户端中。例如,可用性。区域内和跨区域的所有数据复制都是由EVC客户端完成的。

正如Akaash提到的,它是拓扑感知的,这意味着EVC客户端知道整个服务器拓扑,并且确切地知道从哪个服务器读取数据或向哪个节点写入数据。现在您可能会问一个问题:拥有厚客户端有很多优缺点,但您可能会问为什么选择厚客户端?实际上,这是默认设计的。

我们想要整个分布式系统逻辑要么存在于服务器端,要么存在于客户端。但我们更倾向于放在客户端,因为我们希望将EVC服务器上的内存专门用于获取和设置操作,以实现极高的性能。例如,对于每秒4亿次操作,我们希望服务器尽可能高性能。这就是我们将所有逻辑移到客户端的主要原因。

现在您可能会问,现在有这样一个厚重的客户端,这意味着设置必须尽可能高效。这里的效率是什么意思?例如,正如我之前提到的,EVC客户端知道整个服务器拓扑,这意味着我在这里举一个随机例子:一个拥有10个节点的EVCache集群在一个可用区中,客户端管理10个连接。如果我们进行复制,我们尝试至少有三个副本在区域内可用,这意味着客户端几乎有30个连接。对于整个设置,它必须尽可能高效。

我们在这里做的是,我们使用非阻塞I/O与Memcached节点通信,这样每个操作我们不需要等待每个操作完成。每个线程可以同时处理多个连接。

接下来是负载均衡。这又与我刚才提到的非常相关。正如我们提到的,EVCache客户端知道向哪里写入数据以及从哪里读取数据。整个负载均衡逻辑,我们基于键使用一致性哈希,并将数据路由到特定的服务器节点。整个负载均衡逻辑都存在于EVCache客户端中。

安全连接。让我们给安全连接一个特别的关注。

在深入安全连接之前,让我们从数据流开始。EVC客户端帮助我们实现从客户端到服务器的无缝且安全的数据移动。

这意味着它确保数据完整性,并防止未经授权的访问。我们通过SSL和TLS来实现。我们确保数据在传输过程中被加密,这样它就不能被拦截或窃听,从而保护数据。

数据防御。EVCache客户端采用全面的数据安全技术来保护数据。

如果您了解开源的EVC客户端,您可能会发现那里没有任何安全实现。原因是我们使用自己的安全实现,并且我认为将其开源没有太大意义。

因此,当我们在Netflix的不同应用程序中部署EVC客户端时,我们拥有开源的EVC客户端,并在其周围有一个包装器,其中包含所有安全实现。我们将整个库作为一个整体部署到所有应用程序中。

很好,现在让我们谈谈客户端在SSL/TLS加密中的作用。我们拥有所有这些安全措施的原因实际上是为了加密数据,使其永远不会被拦截,并且只能由授权方解密。

数据完整性再次至关重要,例如,您写入的内容就是接收到的内容,EVC客户端维护这种数据完整性。

认证。每当我们谈论认证时,我们主要关注服务器端,但客户端在SSL和TLS中也扮演着重要角色,因为它获取安全信息,传输所有安全信息,并且必须以非常高效的方式进行。它将安全信息传输给服务器。同样,我不会详细介绍每种情况的实现,我只是强调客户端在所有这三种场景中也扮演着同等重要的角色。但Sam将非常详细地概述我们如何做每一种情况。

很好,现在让我们谈谈,正如我之前提到的,安全实现是非常内部的,我们使用内部实现。这就是我们不开源的原因。因此,在这张幻灯片和未来的幻灯片中,我已经将内部实现拿掉,只采用了基本概念来说明:您有一个客户端,如何保护它,以及如何大规模部署它。以及您需要具备哪些要素。Netflix大量使用Java,这就是为什么我在这里以Java为例。但对于其他编程语言,构造或概念是相同的。

那么第一步实际上是去保护连接。这里需要两个步骤:拥有一个SSL上下文和SSL引擎。SSL上下文是什么?可以将其视为一个工厂,用于产生所有安全连接。正如我之前向您提到的,EVC客户端管理或维护与每个节点的连接,这意味着,例如,在我之前的例子中,假设有30个连接,这意味着所有30个连接都必须是安全的。您如何确保这些连接安全或一致地产生这些安全连接?就是通过SSL上下文。SSL引擎是什么?实际上,我们这样做的原因是为了去加密数据。SSL引擎负责数据的加密。

现在我们建立了连接。如何通过TLS 1.2和1.3初始化这个过程?我们两者都启用。我们同时启用的原因是为了支持最新的加密协议以及改进的握手过程。

初始化过程中的另一个重要步骤是性能如何。正如我所说,我们每秒大约进行4亿次操作。我们不希望这些客户端驻留在客户端应用程序上,我们不希望成为客户端的瓶颈。我们希望这些库尽可能高性能。因此,我们使用非阻塞I/O。正如我之前提到的,我们不希望操作等待。我们希望处理多个连接,并且认为一个线程管理多个连接比每个连接都有自己的线程更容易管理,在大规模下管理后者会很困难。因此,套接字连接配置为阻塞模式为false。

现在,您可能会问一个问题。您刚刚说客户端管理所有连接。连接故障怎么办?是的,连接故障随时发生。例如,间歇性网络故障也可能导致连接故障,服务器宕机也会导致连接故障。我们如何很好地管理这些故障?非常简单,我们去呼叫值班人员,这意味着没有一个EVC工程师或值班人员能整夜安睡,我们将24小时待命,因为故障随时可能发生。

我们在这里做的是一个两步过程。第一步是我们尝试自动修复。例如,我们尝试修复这些连接故障。如果不行,那么我们必须呼叫值班人员。让我们从如何进行自动修复开始。我们有一个连接池管理器,它会启动一个新线程,并确保监控每个连接的健康状况。例如,如果任何这些连接出现故障,那么我们会将其放入重试队列。

并尝试查看它是否是间歇性故障。在某些情况下,我只是在这里举个例子。如果服务器完全没有响应,那么这意味着所有这些重试也不会起作用。如果超过某个阈值,我们必须触发值班呼叫,或者值班人员来了解问题的性质。可能是客户端问题,可能是服务器问题,可能是网络问题,然后采取适当的行动。因此,第一步总是自动修复,这解决了几乎90%或95%的用例,只有5%的情况会触发值班呼叫。

资源管理,正如我再次提到的,这与连接故障非常相关。想象一下,您有一个运行在客户端应用程序上的EVC客户端,如果您没有清理资源,这意味着存在泄漏的通道。这里会发生什么?这些泄漏的通道将使用客户端的CPU和资源,这将降低客户端的性能。从客户端的角度来看,他们只是使用了您的库,但即使这不是EVC客户端或EVC的一部分,仅仅因为存在这些泄漏的通道,他们也会遇到性能下降,因为这些是运行并保护这些连接的后台线程。因此,第一步是确保我们在这里可以做什么,再次是主动检测这些泄漏的通道并清理它们。

我们如何做到这一点?再次,我以Java为例。在Java中,我确信你们大多数人都知道,有选择器和选择键。选择器负责监控每个通道,即套接字通道,以确保它们准备好进行读取或写入。选择键可以看作是每个向选择器注册的独立通道。我们遍历所有选择器和选择键,确保它们准备好进行读取或写入。如果没有,那么最好主动清理它们。

然后,正如我之前提到的,将其放入重试队列,看看它们是否能够重新连接。

如果我在设计客户端库,我强烈建议将资源管理视为P0优先级,否则在实际生产环境中调试这个问题将极其困难,并且实际上会影响客户端性能。

很好,那么重连策略。现在考虑一种情况,我刚刚提到整个分布式系统逻辑都存在于客户端而不是服务器端,我们随时都在扩展集群。我们不希望这些连接都建立在客户端上。想象一个场景,我们去扩展集群,我们必须要求客户端重新部署以再次保护连接,对吧?

对于一个集群来说,这没问题。对于两个集群来说,这也没问题。但在整个Netflix范围内使用EVCache时,我们就必须不断在Slack上工作,与这些客户端沟通进行重新部署。我认为这对我们的时间来说并不高效。因此,我们在这里做的是,这两个事件应该完全独立。再次,我们使用相同的EVC连接池管理器来定期监控这些连接。它不仅检查连接,还检查是否有新连接可用,因为客户端知道整个服务器拓扑。对于新连接,它会建立新连接;例如,如果节点宕机,它会断开那些连接,清理那些连接,所有这些都由EVC连接池管理器完成。因此,重连策略非常独立,并且也会为客户端节省大量时间。

我们这样做是为了确保数据安全。然后我们使用我们的SSL引擎,正如我之前提到的,每当我们建立连接时,就去加密数据。正如您所见,SSL_engine.wrap 用于加密数据,因此只有授权方才能解密它。

很好,现在我们继续部署了代码。代码运行得很好。我们继续部署了它。但要注意的一点是,单元测试、集成测试总是会在生产环境中出现问题,对吧?因此,最好有指标,特别是对于我们的分布式系统,最好有指标,特别是对于这些安全功能,实现起来很困难,因为整个工作流程或整个业务逻辑都在事务中发生,这意味着您需要理解数据包,需要理解CPU性能分析,而且调试过程有时也会变得非常困难。

为了主动发现问题,我强烈建议设置大量的指标和监控。我们确实有30到40个指标,但在这种情况下最重要的指标是活动连接数、空闲连接数、连接延迟。我不打算详细解释,因为名称已经说明了:错误率和SSL握手失败。我想强调一下SSL握手失败。例如,一个新客户端尝试访问一个它未被授权的EVCache集群。

从客户端的角度来看,我们在Netflix内部,我们仍然可以去授权,我们仍然可以访问任何我们想要的EVCache集群,对吧?但这取决于平台工程团队来保护其数据。在这种情况下,我们做的是,我们收到所有警报,例如,如果一个客户端尝试访问它未被授权的EVCache集群,我们会收到页面警报甚至Slack警报,说有一个新客户端正在尝试授权。然后在这种情况下,我们做的是,我们主动联系客户端,询问他们的业务用例。例如,如果业务用例有理由访问那些敏感数据或任何安全数据,那么我们去添加所有适当的安全信息,以便这些客户端随后可以访问它。如果没有,我们必须传达,作为平台工程师,我们必须告诉所有安全措施,以使我们的数据尽可能安全。

现在,我让Sam来谈谈服务器端。谢谢您的介绍。


章节 3:服务器端认证与授权 🔑

大家好,我是Sam。我将谈谈EVCache服务器如何对请求进行认证和授权。那么,EVCache服务器如何确保不良行为者或不应访问某些敏感信息的服务或用户被拒之门外,并且他们的请求被拒绝?

之前提到客户端使用SSL/TLS与服务器建立连接。服务器也做同样的事情,它使用TLS。在一个称为相互TLS的过程中,客户端和服务器交换它们的证书。

因此,服务器接收客户端证书,客户端接收服务器证书,并且它们验证对方是否如其所声称的身份。通过这个过程,它们可以开始通信,并且彼此认证。

这些证书由证书服务颁发,这是一个受信任的机构。这样,客户端可以信任服务器,服务器可以信任客户端。

现在,让我们谈谈授权。在整个Netflix,我们有很多不同的服务、很多用户,以及很多不同的缓存。每个缓存包含不同的信息。因此,一个缓存中的信息可能是敏感的,而另一个缓存中的信息可能不敏感。因此,每个缓存将有不同的读取策略、不同的写入策略,哪些应用程序可以访问这个缓存,哪些用户可以访问这个缓存,对于每个缓存都是不同的。因此,当客户端尝试向某个EVCache服务器发送请求,要求获取信息时,服务器如何知道是否允许这个特定客户端访问这些信息?

Netflix有一个授权服务。基本上,这个授权服务包含每个不同服务器、每个不同缓存集群的所有不同策略。它知道,好的,这个用户被允许读取这个缓存,这个应用程序被允许写入这个缓存。因此,它将所有这些策略推送到相关的服务器和授权代理上。

因此,代理现在知道了。客户端A被允许。客户端B不被允许。这是一个例子,对吧?代理驻留在每个EVCache服务器上,然后这就是Memcached如何知道的方式。

回到那个例子,我们有一个客户端试图访问这些信息,Memcached收到这个请求,并向本地代理发出请求:嘿,这个客户端要求这些信息,我可以吗,将信息交给这个客户端安全吗?代理会说“是”或“否”。如果说是,Memcached将用相应的信息响应;如果说否,Memcached将终止连接。未经授权的应用程序将无法访问那些敏感信息。

但是我们如何知道,Memcached如何知道最初是谁发送的请求?因为这是Memcached为了询问代理谁被允许访问该服务器而必须知道的关键信息。

它并不是请求文本的一部分。一个简单但错误的解决方案是让客户端在请求中附带这个信息。Beth在请求某些信息时说,顺便说一下,我是Beth,并要求Memcached返回一些敏感信息。Memcached去询问授权代理。授权代理说,好的,是的,Beth被允许访问这些信息。好的,一切都好,对吧?

这是Lupin。Lupin是一个神偷。现在Lupin假装是Beth,也向Memcached发送相同的请求。如果Memcached不知情,它只会询问授权代理:嘿,Beth被允许访问这个吗?授权代理回答是。然后Memcached会返回敏感信息,Lupin就会得到他不应该得到的数据。

为了防止这种情况,我们可以查看客户端证书内部。还记得之前服务器和客户端如何交换证书吗?现在服务器可以访问客户端证书,并且证书中有信息可以帮助我们识别客户端是谁。

让我们看看证书内部,它采用非常标准的X.509格式。我们可以看到有有效期等信息,有颁发者信息,颁发时间,还有签名。这里要关注的关键是扩展部分。

每次Netflix颁发这些证书时,它还会包含一个Protobuf消息,其中包含有关客户端的信息。因此,在这个消息中,我们可以看到应用程序名称在那个Protobuf消息中。这意味着服务器可以去读取那个客户端证书并读取那个应用程序名称。这就是服务器将知道这是谁的方式。

好的,现在Beth将她的证书发送给服务器,并包含她的应用程序名称,它会显示Beth。那么问题解决了,对吧?

现在,安静。我们这里有另一个未经授权的应用程序。这个应用程序实际上试图通过更改其证书的某些字段来假装是Beth,声称他们是Beth。

如果Memcached不安全,它会读取这个字段,并认为Beth正在发出这个请求,然后它会返回敏感信息。

现在我们可以转而查看客户端证书内部的证书签名。

这个签名是通过获取所有字段(包括扩展部分)计算出来的,然后使用哈希算法计算哈希值,再用受信任机构颁发的私钥加密该哈希值。

一旦计算出该签名,服务器就可以访问该签名,然后尝试使用同一受信任机构颁发的公钥解密它。然后服务器继续计算所有不同字段的自己的哈希值。

如果哈希值匹配,这意味着客户端和服务器计算出了相同的哈希值,并且所有内容都匹配,没有字段被更改。因此,在这种情况下,如果某个字段被更改,那么服务器知道哈希值不同,它将拒绝该请求。

这样,EVCache服务器中的数据就安全了。

我们实际上按连接进行认证和授权,而不是按请求,因为每个连接可以有许多请求,我们希望减少服务器负载。这样,我们就不必像按请求那样频繁地进行认证和授权。

让我们看看表示每个连接的结构体。这里有一些有趣的字段。我们可以看到有一个“已认证”字段,表示此连接是否经过认证。连接内部还存在SSL上下文。

我们在这里添加了两个字段。一个是此连接是否被授权,另一个字段是此连接上次被授权的时间。

为什么我们需要最后一个字段?因为对此集群的授权实际上可以随时被撤销。因此,在她的访问权限被撤销的情况下,我们不希望该连接始终认为Beth被允许访问此服务器。因此,如果该访问权限被撤销,授权服务会将更新后的策略推送给授权代理,然后代理会通知。

现在,Memcached会每隔一段时间询问代理:嘿,这个连接仍然有效吗?这个连接仍然被授权吗?如果不是,那么Memcached将终止该连接。

因此,如果Beth不再被授权访问,即使她之前被授权,她也不再能访问敏感数据。

我们还有内部指标,显示哪些应用程序正在尝试访问哪些集群。我们还可以知道它们被授予访问权限的频率。在这种情况下,我们可以看到客户端A、客户端B和客户端C已被授予访问权限。

如果某些客户端被拒绝访问,那么它会呼叫值班工程师进行调查。

每隔一段时间,服务器证书必须刷新。必须轮换。在这种情况下,Memcached仍然有一个过时的服务器证书,我们会有一个定时任务每隔一段时间运行一次,以确保Memcached更新其内部服务器证书,这样当Memcached与客户端通信时,客户端将收到更新的服务器证书,然后它将能够信任服务器。

现在我将交给Akash。


章节 4:真实事故分析与经验教训 🚨

非常感谢Sam和Shira为我们的观众提供了对基础设施的深入见解。我希望这能让您更深入地了解我们如何安全地运行我们的集群,以及其中涉及的内容。

但是,与所有分布式系统和基础设施一样,事情并不总是沿着顺利的路径发展。事情随时可能出错,随时可能失败。我在这里为您深入分析一个我们遇到的实际场景。

这是我们一直做的常规工作流程:扩展我们的EVCache。我们根据客户需求或一年中的不同季节随时扩展我们的集群。

在这种情况下,新集群Y上线。这里的Y可以是一个新的实例系列,也可以是更高的节点数量,或两者兼有。

一旦这个新集群预热完成,它准备好为客户端请求服务,并开始接受读取请求,然后“砰”的一声,EVCache值班人员收到了警报。

可能出了什么问题?这就是我们开始忙乱的时候。我们开始查看一些仪表板。

这是我们看到的第一件事:从客户端侧有明显的写入延迟飙升的证据。

与所有高性能平台或基础设施团队一样,我们做了什么?我们责怪客户端。

总是客户端的错,对吧?在这种情况下,我们从客户端日志中得到了一个相当明显的日志行,表明并发限制被触发,并且它们正在主动失败。

不幸的是,在与客户端团队深入探讨并进行广泛讨论后,我们发现这实际上是一个相当误导性的线索,这种情况经常发生,并且发生在EVC请求层之下,因此这不可能对任何延迟产生影响。

我们继续我们的探索之旅,查看了其他几个部分。您可以看到GC暂停时间增加了,读取重试次数大幅上升。

现在,这些重试更多是一种结果,因为如果延迟上升,客户端可以随时重试,正如Shira提到的,在这种情况下这很正常。

这是我们转变调查方向的地方,查看了不同之处,隔离了堆栈。查看新旧部署之间的差异。

您可以看到它在两个关键方面有所不同:构建版本更高,并且设置类型不同。正如我之前提到的,我们运行两种不同类型的Memcached。第一种是内存中的,当我们扩展时,我们切换到了EX存储。这就引出了一个问题:我们为什么要这样做?我们这样做取决于客户端不断变化的需求。如果我们看到他们能从分布式Memcached提供的效率中获益更多,我们会尝试将他们放在分布式Memcached上,特别是当数据量很大时。

这时,我们将注意力转向了硬件系列,并在这里取得了突破。我们进行了两个不同的实验。在第一个实验中,我们部署了一个内存版本,构建版本更高,但实例系列与我们已有的不同。我们看到延迟与我们一直以来的水平基本一致。

在第二个实验中,我们尝试了不同的实例系列,但使用了EX存储版本或基于磁盘的Memcached版本。延迟立即再次开始上升。这几乎告诉我们,我们在Memcached磁盘版本的配置上搞砸了。

这时我们退后一步,深入使用了性能分析的老工具集,并获取了CPU转储。

您可以清楚地看到,有一个“获取授权策略”调用,消耗了超过50%的CPU周期。

这绝不应该发生。很明显,如果Memcached总是忙于获取授权策略,它怎么会有CPU周期来服务客户端请求呢?那么为什么会发生这种情况?

我带您看一个小图,展示在正常情况下的流程是什么样的:客户端发出一个获取或设置调用,工作线程从证书存储中获取证书,并将连接标记为已授权。正如我提到的,我们在连接级别进行授权。这是一切顺利的情况。

在这种情况下,客户端仍在进行获取调用。但工作线程没有去证书存储,而是去获取一个配置,该配置告诉工作线程是查看策略存储还是证书存储。它被设置为策略存储。这就是调用来源的地方,每个请求都会发生这个调用。然后获取策略,然后将连接标记为已授权。因此,客户端仍然得到服务,但大量的CPU周期浪费在了那个“获取授权策略”调用上。

这就引出了一个问题:什么可能改变了?事实证明,当我们升级到更高的构建版本,即n+1构建时,我们改变了获取此配置的关键方式。

这个配置过去是机器镜像的一部分,但随着升级,这个配置被移到了一个可配置的属性中,该属性被意外地设置为true。

这个简单的配置更改在客户端中引发了混乱。

这给我们带来了可以从这个特定事件中吸取的经验教训,以及安全运行集群的最佳实践。

首要的是始终保持部署的一致性。想象一下我们运行着200个集群。如果它们都处于不同的配置中,那将是一团糟,您很难理清哪个集群运行在哪个配置下。如何管理?如何回滚?因此,始终保持您的构建版本一致。

拥有健壮的配置管理也很重要,这样您的构建会自动告诉您:嘿,我正在升级,这些是随此特定构建升级而来的配置更改。

在这种情况下保持冷静和坚持不懈很重要。例如,您看到我们在这次调查中走了几条不同的路径,但都走进了死胡同。是第n+1条路径将引导您走向成功。

CPU性能分析是一个很好的工具。使用它。它已经存在很久了,我认为它会一直存在。它总是有助于了解系统级别实际发生了什么,并且对您的系统拥有这种上下文很重要。

第四点很有趣,我真的很喜欢这一点。始终以迭代调试为目标来开发您的技术栈。想象一下,在这种情况下,如果我们的技术栈难以部署,我们可能需要几天时间来运行实验和隔离问题。但由于我们有非常快的部署周转时间,我们能够在几个小时内完成。

一如既往,对所有有问题的代码部分设置指标很重要。我认为这是SRE大会的主题,尽可能在所有地方设置指标,以便您能主动收到警报,而不是依赖被动发现。

在安全时代,使用最新协议比以往任何时候都更重要。学术界的研究进展非常快。保持对安全的关注并实施强密码套件非常重要,以便您免受恶意攻击。

那么,当我们安全地运行集群时,我们做了哪些权衡?正如您在这里看到的,在几乎所有关键的客户端指标上,启用授权与不启用授权相比,我们付出了5%的性能代价。

这就引出了一个问题:值得吗?如果我告诉您,不这样做的后果是客户数据不安全,并且EVCache值班人员会有许多不眠之夜。

我想在场的每个人都会同意,始终值得尽可能保护客户数据的安全。

即使这意味着更高的CPU利用率。我想我们都可以通过运行一个留有一些余量的集群来应对,同时仍然保持客户数据的安全。

说到这里,我想把时间留给大家提问,非常感谢大家的聆听。


课程总结 📝

在本节课中,我们一起学习了Netflix如何保障其大规模分布式缓存系统EVCache的安全。我们从系统架构概述开始,了解了EVCache作为分布式键值存储的基本原理。接着,我们深入探讨了客户端和服务器端的安全实现细节,包括SSL/TLS加密、相互认证、基于证书的授权以及连接级别的安全策略管理。通过一个真实的线上事故案例,我们分析了配置错误如何导致性能问题,并从中总结了保持部署一致性、重视配置管理、善用性能分析工具、设计易于调试的系统以及全面监控等宝贵的最佳实践。最后,我们认识到,在分布式系统安全与性能之间取得平衡是可能的,保护用户数据安全永远是首要任务。

037:从“宠物”到“牲畜”——一种经济高效的Elasticsearch扩展架构 🐄

概述

在本教程中,我们将学习如何将一个传统的、难以扩展的“宠物式”Elasticsearch架构,改造为现代的、可无限扩展的“牲畜式”架构。我们将深入探讨其核心设计思想、具体实现方案以及带来的显著收益,包括成本节约、性能提升和运维简化。

章节 1:问题起源与挑战

1.1 从MVP到规模化困境

每个系统都始于一个最小可行产品。起初,日志通过UDP发送到服务器,使用grep等工具处理,一切运行良好。随着公司成长,我们引入了Elasticsearch,它提供了便捷的仪表板和搜索功能,说服团队迁移至此非常容易。

然而,当系统规模开始成倍增长时,问题出现了。我们首先尝试纵向扩展,使用更大的机器。但这很快达到硬件极限。接着,我们转向横向扩展,添加更多节点,但发现Elasticsearch集群本身存在扩展上限。

1.2 集群扩展的固有瓶颈

Elasticsearch架构依赖于一个主节点来协调集群状态,例如索引和分片信息。当集群节点数量增长时,节点间的通信开销会急剧增加。通常,在默认配置下,一个集群的节点数不建议超过200个。虽然有些公司通过调整大量内存参数等方式将集群扩展到700个节点以上,但这是一种非常脆弱且非标准的“宠物式”运维方法。

1.3 “多集群”方案及其弊端

为了突破单集群限制,一个自然的想法是创建多个Elasticsearch集群,并将不同的客户或数据路由到不同的集群。这带来了两个核心路径的问题:

搜索路径问题
Elasticsearch提供了跨集群搜索功能,可以将搜索请求透明地重定向到后端其他集群。但这使得第一个集群变得极其关键,因为它不仅存储客户数据,还存储了Kibana配置、告警、仪表板等所有元数据。如果这个集群故障,所有集群都将无法访问。

写入路径问题
在从数据中心迁移到云的过程中,由于时间和业务压力,我们简单地将原有的“宠物”服务器集群原样搬到了云上,并在前面放置了AWS负载均衡器。负载均衡器仅能基于HTTP头或路径进行简单路由。这导致了一系列问题:

  • 需要手动配置每一个数据写入端,告知它们应该写入哪个集群,变更耗时数周。
  • AWS负载均衡器无法有效感知集群健康状态。即使集群处于黄色或红色不健康状态,它仍然会返回HTTP 200状态码,导致流量持续涌入问题集群。
  • 各集群硬件配置、负载不均,存在热点问题(例如90%的数据都写入第一个集群)。
  • 运维复杂度爆炸:命名规范不一、部署模型多样、告警频繁,且难以在不影响客户的情况下进行变更。

章节 2:设计理念:“牲畜” vs “宠物”

上一节我们看到了传统“宠物式”架构在扩展时遇到的种种困境。本节中,我们将引入一个根本性的设计范式转变。

“牲畜 vs 宠物”是一个源自2016年的比喻,用于阐释两种不同的运维哲学:

  • 宠物式:给服务器起个性化的名字(如whiskydexte),像对待宠物一样精心照料。每台服务器都是独一无二、不可替代的。一旦宕机,需要立即修复,整个服务可能因此中断。
  • 牲畜式:服务器没有名字,只有编号(如web-01web-02)。它们被视作可替代的、一次性的资源。任何服务器都可以随时被丢弃和替换,而不会影响整体服务。监控的是服务层面的指标,而非单个服务器的健康。

我们的目标是将Elasticsearch架构从“宠物”转变为“牲畜”。这意味着:

  • 服务器可随时替换,无需人工干预。
  • 设计面向未来10年的扩展性。
  • 分离数据层和展现层。
  • 支持不同的服务等级协议(如“尽力而为”的集群)。
  • 追求成本效益,不绑定特定硬件。
  • 实现命名标准化和自愈能力。

章节 3:核心架构设计

基于“牲畜”理念,我们设计了以下核心架构组件。

3.1 智能负载均衡与健康检查

我们在所有集群前端部署了一个智能负载均衡器(非AWS ALB)。它的核心职责是:

  • 基于权重的路由:根据后端集群的健康状态动态分配流量。
  • 深度健康检查:不再信任HTTP 200状态码。我们实现了一个TCP健康检查服务,其灵感来源于Brendan Gregg的USE方法(Utilization, Saturation, Errors)。

该健康检查服务通过一个公式为每个集群计算一个健康分数(0-100),负载均衡器根据此分数调整流量权重。

健康分数计算公式

score = (100 - utilization_p90) * (100 - saturation) * (100 - errors) / 10000
  • utilization_p90:例如CPU使用率的P90值,避免单节点异常影响整体判断。
  • saturation:饱和度指标,如线程池队列深度。
  • errors:错误率,例如磁盘即将写满的状态。

此外,我们直接查询Elasticsearch集群状态(绿色/黄色/红色)。如果集群状态为红色(只读或丢失主分片),则将其健康分数直接置为0,负载均衡器将不再向其发送流量。黄色和绿色状态则参与上述公式计算。

3.2 管理集群与数据集群分离

我们彻底分离了管理功能和数据存储:

  • 管理集群:一个独立、轻量的Elasticsearch集群。它不存储任何真实的客户数据,只负责:
    • 托管Kibana(统一的访问入口)。
    • 配置跨集群搜索。
    • 存储所有告警、仪表板等元数据。
    • 运行健康检查服务。
    • 作为审计服务的聚合点。
  • 数据集群:多个完全同构的、纯粹的“牲畜”集群。每个集群配置、硬件规格完全一致,只负责存储和查询数据。它们对上层应用透明,甚至不知道自己存的是哪些客户的数据。

3.3 整体架构视图

以下是架构的核心视图:

客户 (写入/查询)
        |
        v
[ 智能负载均衡器 (基于健康分数路由) ]
        |
        |---------------------------------------|
        |                                       |
        v                                       v
[ 管理集群 ]                          [ 数据集群组 1 (集群A, 集群B...)]
(Kibana, 元数据, 健康检查)                 [ 数据集群组 2 (集群C, 集群D...)]
        |                                       |
        |--------跨集群搜索---------------------|

通过这种设计,我们实现了:

  • 无限水平扩展:可以在每个数据集群组内不断添加新的、同构的数据集群。
  • 故障隔离:单个数据集群故障不影响全局。
  • 透明运维:扩容、迁移、升级对用户完全透明。
  • 统一入口:用户通过统一的管理集群访问所有数据。

章节 4:实施效果与收益

架构改造后,我们获得了显著的、可量化的收益。

4.1 性能提升

  • 查询性能:一个关键查询的耗时在优化后大幅下降。
  • 写入延迟:索引延迟(日志从产生到可搜索的时间)降低了近4倍。下图展示了Kafka中待消费日志量的变化,迁移后积压几乎消失。

4.2 成本优化

“牲畜”架构允许我们采用更经济的资源配置策略,我们称之为 “牲畜率0”

  • 旧思路(宠物):为每个Elasticsearch节点配置一块大型、昂贵的磁盘(如2TB SSD)。
  • 新思路(牲畜):为每个节点配置一块小磁盘(如500GB),但创建更多节点。将数据分散到更多更便宜的节点上。

成果

  • 最大集群的计算成本降低了17%
  • 一个中型集群的计算成本降低了惊人的57%
  • 通过进一步将SSD替换为更便宜的GP3磁盘并进行更多“牲畜率0”优化,预计未来可节省50% 到 82% 的成本。

4.3 运维简化

  • 部署与迁移时间:将一个新集群投入生产或迁移一个大型集群的时间,从原来的14周缩短到5天。最大集群的迁移仅用了2天,且对用户零感知。
  • 配置统一:所有数据集群配置一致,降低了认知负荷和运维错误。
  • 赋能实验:可以轻松创建镜像集群来测试新硬件、新配置或识别问题查询,而不会影响生产环境。

章节 5:增强工具:查询审计服务

在“牲畜”架构下,集群规模庞大,我们需要更强大的工具来洞察问题。Elasticsearch自带的慢日志和安全日志信息分散,难以关联。为此,我们构建了一个轻量级的查询审计服务

实现方式

  1. 在每个Elasticsearch Pod中部署一个Sidecar容器。
  2. Sidecar通过UDP收集该节点的慢查询日志和安全日志。
  3. 所有Sidecar将日志发送到管理集群中的一个聚合服务。
  4. 聚合服务将海量分散的日志聚合成按用户、查询等维度统计的视图,并写入管理集群。

价值

  • 客户问题定位:当客户抱怨查询慢时,我们可以从审计仪表板中直接看到该查询的详细性能指标,快速判断是集群问题还是查询本身问题。
  • 集群洞察:获得全局视图,例如:可缓存查询的比例、各时段查询负载、问题用户识别等。
  • 未来优化:为实施查询限流、缓存以及阻止恶意查询提供了数据基础。

总结

在本教程中,我们一起学习了如何通过从“宠物式”到“牲畜式”的架构转变,解决大规模Elasticsearch部署的扩展性、成本和运维难题。核心要点包括:

  1. 理念转变:将服务器视为可丢弃、可替换的“牲畜”,而非需要精心呵护的“宠物”。
  2. 架构核心:通过智能负载均衡(基于深度健康检查)和管理/数据集群分离,构建了一个透明、无限扩展的层次结构。
  3. 显著收益:实现了性能提升(4倍索引速度)、成本大幅降低(最高57%)和运维极度简化(迁移时间从周降到天)。
  4. 工具赋能:构建查询审计服务,为大规模集群下的问题诊断和优化提供了关键洞察。

这种架构不仅适用于Elasticsearch,其“牲畜化”、池化、基于健康状态路由的核心思想,可以广泛应用于其他需要大规模、高可用的分布式系统设计中。

038:每年百万构建,仅需一页告警——内部服务运维实践

在本教程中,我们将学习一个SRE团队如何通过一系列文化、策略和技术决策,实现每年处理超过百万次产品构建,同时将告警唤醒次数降至极低水平的实践。我们将重点探讨团队规模缩减背景下的运维策略、自动化升级以及服务稳定性与团队健康的平衡。

大家好。我是Ka,来自澳大利亚。我是一家名为Octopus Deploy的公司的SRE,我们开发持续部署工具。如果你对此感兴趣,可以在octopus.com找到我们。我所在的内部工具团队负责为我们的产品提供构建、测试和部署基础设施。标题中的“百万构建”指的是我们的产品在一个日历年内,在我们团队提供的平台上完成的构建次数。

现在,我需要说明一下。你看到标题上那个带星号的小脚注了吗?我在这里取巧了一点。那“一页告警”指的是一年内真正需要唤醒某人的告警。对我而言,我关心的是被唤醒,我不喜欢这样。虽然还有其他告警,但只有一个是真正需要唤醒人的。

首先,简单介绍一下Octopus Deploy的工程文化。大多数工程团队都需要参与值班。团队大致分为三类。

第一类是负责外部服务值班的团队。有人属于这类团队吗?你们为顾客使用的服务值班。

第二类是像我们这样为内部服务值班的团队。有人吗?好的,有一些。

第三类则是“秘密”的第三种团队,他们不为任何服务值班。这里有人属于这类吗?没有。那一定很惬意。是吗?

看来大家的视角很丰富,这很好。我所在的团队,我认为我们拥有从GitHub开始,一直到我们营销网站后端那个展示发布说明、提供产品版本(甚至回溯到V1及更早版本)下载链接的应用之间的所有环节。

在深入核心内容之前,我们需要提供一些背景信息。我们需要回到过去,看看我所在团队的初创时期。

这大约是我加入公司一年后。我开始真正站稳脚跟,我们当时有这些构建基础设施和测试在运行,但它们像是“环境”的一部分,没有明确的负责人。

我认为这可能是合理的,这反映了公司从一个技术创始人有机成长到当时约70人的状态。我和其他一些人认为,CI系统可能需要一个明确的负责人。我们认为,我们遇到了一些问题,或许应该开始认真管理它。

于是,我们找到了获取系统管理权限的途径,并接手了它。从那以后的几年里,我们接管了越来越多的流水线环节。

为了成为好的管理者,我们从基础工作开始,比如修补运行这些服务的主机操作系统。我们可能想这么做,但结果呢?哎呀。

我们当时全是手动操作,临时应对,没有任何自动化。事实证明,当构建系统宕机六小时时,软件工程师们会不高兴。我也不高兴。

因此,我们很自然地想到,好吧,这很痛苦。我们或许应该在一天结束或开始时做这类工作,以减轻公司其他同事的负担。但这有一个问题。

那就是,在我的团队里,没有人签约在常规工作时间之外例行工作。所以它们才叫“工作时间”。这存在团队成员倦怠甚至离职的风险。我们想改变这一点。

接下来,我将花些时间讲述我们做出的一些决定,这些决定事后看来帮助我们实现了现在的状态:运行这些服务,而无需在一周的大多数晚上熬夜。

我要从一件可能有点令人失望的事情开始,如果你希望找到一个可以直接解决问题的工具的话。你必须与人沟通,抱歉。我知道这对我们中的一些人来说很难。没有软件层面的“修复方案”,尽管我们稍后会讨论一些技术选择。

我们面临的问题主要围绕时区。我们是一个远程优先的组织,早在2020年之前就支持远程工作,但工程团队的“重心”在澳大利亚东海岸。尽管众所周知,那里仍然有不止一个时区(感谢夏令时)。但我们也有同事在新西兰和其他地方。

我们进行了很多艰难的来回沟通,比如:每隔几周让某些服务停机几小时可以吗?最终我们从大多数人那里得到的答案是:可以。除非我们正在尝试发布一个长期支持版本,并且同时有很多变更发生。否则的话,是的,没问题。

如果你看过Katie Wes关于“无限正常运行时间技巧”的演讲,这有点像我们实现它的方式。我们说,好吧,让我们制定一些关于何时允许服务不可用的规则。于是我们写了一份支持政策。

我们在政策中写道:好的,各位,我们承诺在正常工作时间内安排一个人,负责保持服务正常运行,主动监控生产环境状况,并处理所有传入的运维工作。

作为交换条件,我们制定了告警策略。既然我们都在工作时间内完成这些工作,我们也会参与值班。但只有当你需要我们的工具来解决你的故障时,才发出告警。

所以,如果你只是碰巧在遥远时区工作,只是想完成一次构建,但系统不工作,不行,你得等到明天。但如果我们有一个安全补丁需要在夜间发布,是的,绝对要告警。这就是我们存在的意义。

这个策略效果很好。我们现在为响应时间设定了SLA。不过,我不想跑题太远,我知道这个缩写对某些人来说可能有点敏感,你可能在过去有过不好的体验。还有其他敏感的缩写。

在这整个时期,我第一次学到了第三个缩写:RIF。不知道这是什么意思的人,你们很幸运。这是“人员编制缩减”的委婉说法,指的是裁员。

当我的团队忙于建立这些良好的文化和政策时,公司正接近我们每次SREcon都必须提到的动态安全模型中的“经济失败”边界。如果你不知道这是什么,可以去看看Andrew Hatch去年的演讲,他讲得比我详细得多。

总的来说,我们可以说,有影响力的人认为公司在三个边界内移动:经济失败、工作负载失败、性能失败。他们认为我们正走向经济失败。

Octopus作为一家公司实际上没有债务融资,非常关注盈利能力。因此,他们看到利润率下降,说我们需要采取一些激烈措施。

所以实际情况是这样的。我提到所有这些,是因为RIF影响了我们的选择。因为我们的团队缩减到了三个人。

分布在非常遥远的地方,比如墨尔本。还有墨尔本和卧龙岗(靠近悉尼)。非常不错的澳大利亚地方。这当然不是维持一个可持续的7x24小时值班计划所需的七八个人。

于是,减少重复性劳动(Toil Reduction)成为了重点。我们已经在做这件事,但现在真的需要加速了。我们需要确保能够处理所有必要的工作。

我们做出的一个技术选择是拥抱云服务。我的意思是,在基本所有合适的地方,我们都选择使用云供应商提供的最高抽象级别的服务。

一个很好的例子是数据库。我的团队运行着一堆来自第三方的不同应用,有些用MySQL,有些用PostgreSQL,有些用Microsoft SQL Server。我们只有三个人,没有时间深入掌握所有这些技术的运维知识。

所以我们依赖云供应商,他们说:好的,你只需要给我们一个漂亮的API来创建和管理这些东西,而补丁、备份和高可用性由他们负责。

说到高可用性。我们做出的另一个选择是,即使感觉不必要,也选择以高可用模式或复制模式(或两者兼有)部署服务。我知道你会说,Ka,那拜占庭故障模式呢?是的,这是个风险。

但我们更倾向于接受这个风险,而不是因为单个可用区断网(这确实发生过)而导致我们的服务直接下线。公平地说,我们团队中运行的复制状态服务并不多,所以这确实是一个容易做出的选择。

你可能还会想,Ka,所有这些不花钱吗?是的,确实花钱。不过,我很幸运,我们有一个领导团队,他们认识到尽管公司整体情况如此,我们仍需要投入一些资金,以避免留下的人员倦怠。

这个时期经常被提及的一句话是:要有成本意识,但不要害怕成本。因此,我的团队需要做预算预测和成本建模,并随时间推移对此负责,但我们没有被给定一个上限数字并被要求围绕它设计。我们可以构建我们认为需要的东西,告知他们,他们会相应地进行规划。

我们做出的第三个,可能更具争议的选择是,基本上自动升级所有东西。不是盲目地自动升级,但至少建立系统,让我们知道何时需要升级。

我指的是最广泛意义上的升级:Terraform提供商版本、应用程序本身、它们的依赖项,凡是互联网上有版本流的东西,我们都升级。

我们的合理默认做法是,至少建立一个流水线,在有可用更新时提议变更。对于某些东西,我们选择直接应用更新,看看是否着火,因为我们对其可用性不那么在意。对于另一些,我们有测试或其他类型的关卡来阻止变更发布。还有一些,我们将这些活动安排在周末,这样如果没有问题,就不会造成太大损害。

实现这一点并不容易,无论是在政治上还是技术上。我们第一次这样做时相当困难。但我们加入了很多安全措施,并且随着我们了解这些系统如何故障,我们总结出了很多适合我们的良好模式。

我想指出,我们并不使用预览版构建,我们不是那么前沿,但我们喜欢尽可能保持最新。

话虽如此,有时周一你进入家庭办公室,新西兰的软件工程师会不高兴,因为构建系统在周末自动升级后宕机了。这确实发生过,那次对话很痛苦。但事后看来,在那之前已经有大约100周我们顺利完成了升级,什么都没发生。我宁愿选择后者。

而不是必须手动计划这些服务的月度、季度或年度大爆炸式升级。

以上所有这些,就是实际上,截至去年(实际上本周刚更新,去年有150万次构建),我们团队仍然只有一次告警唤醒。大多数利益相关者对现状相当满意。

当然也存在一些缺点。第一个听起来可能有点像,你知道,当你参加面试时他们问“你最大的缺点是什么”,而你回答“我工作太努力了”。我保证这是真的。

随着我们开始增长(在RIF之后),我们的系统变得过于稳定,无法产生那种让人们理解系统如何故障的自然故障。我们最终不得不投入大量精力,通过阅读复盘报告或只是进行知识分享会等方式,明确地让新人了解过去故障的情况。我们的团队还没有足够的能力进行故障演练或混沌工程之类的事情。我们现在正在努力。这并不是说我们没有故障,只是不足以依赖它们作为团队新成员的学习来源。

另一个挑战是,公司现在正在全球范围内增长。因此,我们的团队需要挑战“澳大利亚东海岸的工作时间”仍然适合作为值班支持与日间支持之间界限的假设,因为我们在新西兰有人,在澳大利亚四个时区有人,现在工程团队在欧洲也有人。我们该怎么办?我不知道。这并不明确。我知道我们不能实行“跟随太阳”模式。我们的团队不够大,覆盖的地方不够多。

在结束之前,我想回到这个缩写:MTBF(平均故障间隔时间)。它让我思考,也许,我们知道这个指标并不完美,但也许我们可以考虑“平均告警间隔时间”。你多久收到一次告警?多久被唤醒一次?我们的大约是每12个月一次,持续了大约三年。但这算好吗?算坏吗?我不太确定。我认为非常低并不理想。我认为我们需要一些关于系统运行状况的最低限度反馈。系统是否在边缘发出吱吱声?我不知道。所以,如果你是工具供应商,这里有一个可以构建的新图表。

非常感谢大家的时间。我需要感谢我的团队为这次演讲做出的贡献,给我时间来到这里,并审阅这次演讲。我需要感谢我的伴侣,她现在在家带着三个孩子,我才能在这里。

如果这些内容引发了你的思考,你想讨论一下,我会在这里。我在Slack上,你可以通过这些社交媒体联系我。我想我还有一些时间回答问题。

谢谢。


总结

在本节课中,我们一起学习了Octopus Deploy内部工具团队如何通过制定清晰的支持与告警政策、拥抱云服务的高抽象层级、实施自动化的渐进式升级策略,在团队规模较小且分布广泛的情况下,实现了高吞吐量(每年百万级构建)与极低运维负担(每年仅一次唤醒告警)的平衡。核心经验在于:通过文化沟通明确期望,利用技术选择降低运维复杂度,并在成本与稳定性、自动化与可控性之间做出明智的权衡。这些实践为资源有限的团队管理关键内部服务提供了有价值的参考。

039:如何在紧迫时间内高质量、有风格地实现多云部署

概述

在本节课中,我们将学习一个真实的SRE团队如何在时间紧迫、资源有限且缺乏目标云平台(AWS)经验的情况下,成功构建并交付一个高质量的多云Kubernetes平台。我们将重点分析他们采用的独特协作方法“Jam Sessions”,以及支撑项目成功的关键软技能和组织因素。

项目背景与挑战

上一节我们介绍了课程的整体目标,本节中我们来看看项目启动时面临的初始困境。

我们的团队管理着一个基于GCP和GKE构建的托管Kubernetes平台。该平台集成了ArgoCD、Helm、Prometheus、Grafana、L7负载均衡器和密钥管理器等工具,并为租户提供支持。

一个周五,老板提出了新需求:在五周内将平台扩展到AWS,以支持一项即将发布的服务。团队核心成员共10人,都是GCP专家,但对AWS一无所知。此外,新集群还需满足比现有集群更高的合规标准,并要求在开始工作前提交完整的系统架构图和工作分配计划。

策略调整与沟通

面对“未知领域”与“预先详细规划”的矛盾,以及紧迫的时间线,团队必须调整策略。

以下是团队为破局所采取的关键沟通行动:

  1. 与管理层沟通:请求明确需求细节,并尝试推动部分限制条件的调整。
  2. 与产品负责人沟通:确认发布时限的实际灵活性,发现其比最初印象中更为宽松。
  3. 与产品工程师沟通:建立直接联系,了解他们的具体需求,并告知他们将由平台团队负责基础设施,减轻了他们的负担。
  4. 与合规部门沟通:请求指派专人进行快速决策审批,以实现迭代式开发的同时满足合规要求。

团队主动承担了前期的合规文档工作(例如基于AWS文档绘制网络架构图进行预审),这一举动也鼓励了其他成员参与进来,共同分担。

核心协作方法:Jam Sessions

在开放了与各关键干系人的沟通渠道后,团队内部需要一种高效的协作模式来攻克技术难题。我们借鉴了团队另一小组进行云迁移时采用的“Jam Sessions”模式。

Jam Sessions是一种专注于实际工作的会议,而非状态同步会。团队每周进行两次,每次两小时。会议内容非常灵活。

以下是Jam Sessions可能涵盖的主题类型:

  • 决策制定:针对某个技术方案进行讨论并做出决定。
  • 代码审查:集体Review Pull Request。
  • 求助解困:帮助成员解决当前遇到的技术阻塞。
  • 知识分享:分享近期学到的新知识或对AWS的新理解。

会议初期,主持人(通常是笔者)会主动分享自己遇到的困难或一知半解的地方,例如多次分享“这是我对AWS目前的理解,但我仍然很困惑”,以此营造一种心理安全的氛围,鼓励大家承认不足、积极求助。

Jam Sessions的优化演进

最初的Jam Sessions缺乏焦点,甚至会出现开场时面面相觑的尴尬。团队随后进行了优化。

首先,每次会议开始时,会先通过在线协作文档或聊天线程收集并确定议程。其次,由于这是非正式工作会议,团队开始邀请“客人”参与,例如遇到部署问题的产品工程师,或需要共同做出合规决策的合规部门同事。

在会议中,团队会自然地根据兴趣和专长分成子小组,并行处理不同议题(如一人去审查PR,另一组去解决网络问题)。这种自愿组合的方式确保了知识在团队内流转,而非集中在个别人手中。

针对“每周四小时深度会议占用过多专注时间”的反馈,团队进一步优化了流程:在会议当天上午,大家在一个共享线程中发布话题并投票。会议议程则按“客人的话题优先 -> 广泛感兴趣的话题 -> 小众专业话题”的顺序安排,后者通常会形成子小组讨论。

项目成果与成功因素总结

本节课我们一起学习了这个多云扩展项目的完整历程。最终,团队在约六个月时间内交付了生产级的AWS平台,虽然超出了最初五周的要求,但通过灵活的沟通(例如让产品工程师先在成熟的GCP平台上测试),满足了产品方的实际节奏需求。该AWS集群已稳定运行超过一年,无重大事故,且相关知识已在团队内充分共享,足以支撑值班轮换。

回顾项目,Jam Sessions这种协作模式之所以成功,关键在于团队具备心理安全,成员能坦然说“我不懂”或“我需要帮助”。此外,领导层提供的自主权支持、团队在招聘和日常工作中注重的情商,都是不可或缺的基石。

对个人而言,项目的成功不仅依赖于技术能力,更依赖于软技能:凝聚团队、有效沟通、敢于说“我不知道,我们一起解决”以及接纳更好的他人意见。这些技能对职业生涯的各个方面都至关重要。

最后,如果读者也想尝试Jam Sessions,一个小建议是:使用带有喷水海豚动画的会议软件,或许能为会议增添一丝轻松趣味。


总结:本节课中,我们一起学习了一个SRE团队通过建立心理安全、采用灵活的Jam Sessions协作模式、并积极进行内外部沟通,在时间压力和技术陌生的情况下,成功实现高质量多云平台部署的实战案例。核心收获在于认识到软技能、团队协作与开放沟通在解决复杂工程问题中的决定性作用。

040:缓解电子交易中的大规模系统性故障 🛡️

在本教程中,我们将探讨电子交易系统面临的风险,并学习如何通过架构设计来预防和缓解大规模系统性故障。我们将从电子交易的特殊性开始,分析潜在风险,并最终提出具体的架构缓解策略。

概述 📋

电子交易系统运行在严格的时间窗口和监管环境下,任何故障都可能带来巨大的财务、监管和声誉风险。本节课程将引导你理解这些风险,并学习如何通过前瞻性的系统设计来构建更具韧性的交易平台。

电子交易的特殊性与风险 ⏰

电子交易系统严重受限于其交易的市场,因此交易时间至关重要。虽然部分市场全天候交易,但我们处理的大多数市场交易时间窗口相当狭窄。因此,尽管系统可能需要全天候可用以接收客户订单,但事故的严重性会因一天中的时间不同而产生巨大差异。

从图表中可以看到,以伦敦和美国交易所为例,如果事故发生在凌晨2点,不会造成太大问题。但如果事故发生在12小时后,我们将面临非常糟糕的一天。

现实世界中的事故案例 📉

我们可以从现实世界的一个事故案例中理解风险。2023年12月,伦敦证券交易所发生硬件故障,导致其系统性能下降。虽然性能下降未达到自动故障转移的阈值,但仍造成市场在一天内两次被迫停止交易。此事立即成为新闻焦点。

这对我们意味着什么?我们可能面临多种风险。

电子交易系统面临的三大风险 💸

以下是电子交易系统主要面临的三大类风险:

  1. 财务风险:并非所有订单的风险都相同。订单规模差异巨大,一个非常大的订单,即使其返回给客户的价格仅有微小变动,也可能对损益产生巨大影响。
  2. 监管风险:我们处于高度监管的行业,任何问题都会迅速受到全球监管机构的审查。如果他们发现不合规之处,将导致制裁和罚款,后果严重。
  3. 声誉风险:这是一个竞争非常激烈的行业。正如我们从伦敦证券交易所事件所见,一旦出现问题,会迅速成为新闻,没有人希望自己的公司因出错而登上新闻。

保障系统持续运行的标准措施 🏗️

为了保持系统持续运行,我们采取了一些标准措施。我们拥有多个地理位置的数据中心,运行多个实例以实现容量扩展,并隔离不同业务单元。我们定期测试所有站点,确保不会在出现问题时不知如何应对。在部署软件时,我们采用分阶段部署策略,逐步建立信心。通过先在试点环境、部分生产环境进行测试,确保测试了所有不同的用例,避免突然发现一个影响所有业务流的问题。

然而,这并非故事的终点。事情仍然可能出错。系统中总会有问题暴露出来。这些系统非常复杂,包含数千万行代码。因此,其中一些问题可能在数年里都未被察觉,尤其是那些奇怪的边缘情况。我们知道,越不频繁出现的问题,几乎肯定是最严重的问题。

潜在的系统性风险类型 🔍

那么,这些风险具体有哪些类型呢?

  • 潜伏缺陷:一个存在已久的缺陷,在它出现之前我们无从知晓。
  • 毒丸消息:我们从多个来源获取外部数据流。如果某个消息中有我们处理不当的内容,并且该消息在整个平台广播,突然间,系统的多个部分就会发生故障。
  • 相互依赖问题:随着系统从单体架构演进,服务间相互调用日益增多。我们如何处理这些不同组件相互依赖的事实?我们面临级联故障的风险。我们如何处理它们在不同时间运行不同版本的问题?
  • 环境变化:这可能是平台中其他人所做的更改,也可能是外部世界的变化。市场对新闻反应迅速,波动性会急剧上升。今年我们已经多次看到这种情况。所有这些不同因素都可能引发问题。

问题场景模拟与挑战 🐛

这样一个问题可能是什么样子?假设我们有一个缺陷。它不知何故绕过了我们所有的控制措施,通过了代码审查、回归测试、自动化测试,并逐步部署到整个平台,然后在那里潜伏了数年。直到某件事发生,才有人知道它的存在。可能是市场数据量激增,超出我们以往所见;可能是我们从未见过的新消息类型;也可能只是平台中的某些变化,现在我们触发了那个缺陷。

对于不熟悉相互依赖问题的人,也可以从Knight Capital的事件中看到这一点。不同版本的组件相互通信时表现出不同的行为。结果,当一个新的功能开关被打开时,他们突然开始向市场发送完全失控的订单。

当这种情况发生,我们遭遇事故时,我们试图找出影响范围以及如何修复。如果幸运,可能只是惹恼某些人,我们可以轻松应对。但我们不可能总是那么幸运。有时,这可能会中断交易。正如我们所说,这涉及大量资金和巨大的监管压力。这些问题有时已经存在多年。

我们该如何调试?引入那个缺陷的开发者可能已经不在公司了。这些都不会是快速修复。

架构层面的缓解策略 🧱

那么,我们能做些什么来缓解这种情况?从根本上说,我们可以构建更多的测试,可以提高编码水平。但解决这个问题的最佳方式,我们必须审视最根本的东西,即我们系统的架构。我们如何设计才能降低此类风险?显然,如果我们事后才做这件事,修复成本会很高,正如金字塔的大小所反映的。在后期更改架构,你会非常困难。

从架构角度,我们可以考虑以下几点:

减少爆炸半径

部署服务时,很容易想到:让我们部署一个通用的、可在多种不同情况下使用的服务。但当某个地方出错时,它们会同时全部宕机。这不是一个好局面。我们可以引入这些服务的不同变体。每个变体只包含特定用例所需的功能。这样,当问题发生时,它只影响使用该部分功能的具体区域。当然,这意味着我们向系统中引入了额外的复杂性。我们现在必须管理所有这些不同的版本和组件,理解我们在做什么。这是一种权衡。

核心功能隔离

这些是复杂的系统。我们不是将它们构建为单体,而是有不同的组件。在构建时,我们可以让系统核心与某些其他功能隔离开来。这样,如果某些功能模块出现问题,只有那部分停止工作,系统实现优雅降级。系统的其余部分,主要部分,在我们的案例中即交易能力,可以继续工作。显然,我们希望能在过程中修复那些其他部分,功能得以恢复,用户满意。但至少,我们能够管理我们所做事情的核心。

备用系统切换

即使有了以上所有措施,仍然存在系统核心功能本身发生故障的风险。在这种情况下,你可能拥有另一个与我们所做事情有部分重叠的系统。我们有多个不同的业务线在进行交易。交易能力在不同系统间是共通的。因此,也许我们可以使用其中一个其他系统来缓解故障。显然,你必须拥有不同的系统。这也带来了风险:如果我们从未这样做过怎么办?如果我们从未使用过这个系统怎么办?你不能只是扔给某人一个新系统,然后希望他们能够使用它。因此,你必须定期演练你在这里所做的一切。这个策略相当极端,可以想象,成本也相当高昂。

总结 🎯

本节课中,我们一起学习了电子交易系统面临的独特挑战和系统性风险。我们分析了财务、监管和声誉三大风险,并探讨了标准运维措施的局限性。

最重要的是,我们深入研究了三种关键的架构缓解策略:

  1. 减少爆炸半径:通过服务变体隔离故障影响范围。
  2. 核心功能隔离:实现系统优雅降级,保障核心交易能力。
  3. 备用系统切换:在极端情况下,通过演练过的备用系统维持业务。

这些问题并不简单,没有让它们消失的捷径,也没有银弹。我们无法挥动魔杖就让一切正常运转。我们需要在架构上进行投资。我们需要在设计和构建时就预期到事情会出错。我们不能仅仅接受“我构建的系统很健壮,一切都会好”的想法。我们需要以最小化风险的方式来构建系统。但同时,我们需要接受当这些系统被使用时,它们必须正常工作,并且我们需要在此过程中持续验证。

041:利用服务发现劫持模拟依赖故障

概述

在本节课中,我们将学习如何通过“劫持”服务发现机制,来更安全、便捷地实施混沌工程演练(Game Day),模拟微服务依赖故障,从而提升系统的整体韧性。我们将重点介绍三种不同的劫持方法。

什么是混沌工程演练?🧪

混沌工程演练的核心是主动模拟生产环境中的故障,以验证系统在压力下的行为和韧性。在Shopify,我们通过创建“韧性矩阵”来规划演练。该矩阵列出了服务的所有关键流程,以及针对每个流程可能发生的故障类型和预期的系统反应。

以下是规划演练的基本步骤:

  1. 识别服务的关键业务流程。
  2. 列出每个流程可能依赖的组件及其潜在故障模式。
  3. 定义针对每种故障,系统应有的表现(如:服务降级、优雅失败、优先级处理等)。
  4. 执行故障注入,观察并验证实际结果是否符合预期。

许多团队在第一步就会遇到挑战,因为他们并不完全清楚服务的所有依赖关系。因此,仅完成韧性矩阵的创建本身就是一个极具价值的练习。

故障注入的挑战与思路 🎯

上一节我们介绍了混沌演练的基本概念,本节中我们来看看实施故障注入时遇到的具体挑战。当团队准备好韧性矩阵后,下一个问题是如何实际地“关闭”依赖以模拟故障。

直接关闭生产依赖服务是危险且具有破坏性的。理想情况下,我们希望演练对团队而言是低摩擦、可插拔且安全的。从客户端应用的视角来看,依赖“不可用”这个结果是相同的,无论是因为服务本身宕机、网络问题还是其他原因。因此,我们可以通过代理层来模拟这种不可用状态,而无需真正中断依赖服务。

我们主要使用两种故障注入代理:

  • Toxiproxy:一个由Shopify开发的开源代理。它的优势在于可以与测试代码深度集成,非常适合在持续集成流水线中使用。你可以编写测试,通过Toxiproxy对依赖注入延迟或错误,然后进行断言验证。
  • Envoy:一个功能强大的服务网格代理。如果你的生产环境已经使用了Envoy,那么你可以直接在其配置中添加故障注入规则,而无需引入新的组件。

核心挑战:流量重定向 🔄

介绍了可用的工具后,我们面临一个核心问题:如何让服务的流量自动经过我们设置的故障注入代理?最直接的方法是修改应用程序的代码或配置,将目标地址从真实的依赖改为代理的地址。

例如,将连接数据库的代码从:

// 原始代码,连接至真实数据库
client, err := sql.Open(“mysql”, “db-primary:3306”)

修改为:

// 修改后代码,连接至故障注入代理
client, err := sql.Open(“mysql”, “toxiproxy:3306”)

然而,要求团队为了每次演练都修改代码、构建新镜像并部署,这引入了巨大的风险和不便。这违背了我们“低摩擦、可插拔”的目标,会导致团队减少演练频率。

为此,我们开发了 Game Day Buddy,一个Kubernetes Operator。团队只需提交一个简单的配置文件,即可自动完成整个故障注入的搭建。

一个配置示例如下:

apiVersion: chaos.shopify.io/v1
kind: GameDay
metadata:
  name: simulate-redis-latency
spec:
  targetDeployment: “checkout-service”
  dependency: “redis-cache.production.svc.cluster.local”
  toxic:
    type: “latency”
    attributes:
      latency: 1000 # 注入1000毫秒延迟

Game Day Buddy会根据配置自动创建Toxiproxy实例,并设法将checkout-serviceredis-cache的请求重定向到Toxiproxy。那么,它是如何实现流量重定向的呢?这就引出了我们今天的主题——服务发现劫持。

服务发现劫持的三种模式 🛠️

Game Day Buddy在底层支持三种劫持服务发现的模式,这三种模式也概括了此类技术的通用思路:本地劫持、全局劫持和途中劫持。接下来,我们将通过具体例子逐一讲解。

模式一:本地劫持(修改Hosts)

这是最直接的方法。在Linux系统中,应用程序在发起网络请求前,通常会先查询本地的/etc/hosts文件来解析域名。Kubernetes提供了hostAliases字段,允许我们为Pod动态添加主机名映射。

Game Day Buddy的工作流程如下:

  1. 在目标Pod所在的Namespace中部署一个Toxiproxy实例。
  2. 通过Kubernetes API,向目标Pod的Deployment添加hostAliases,将依赖服务的域名指向Toxiproxy的IP地址。
  3. 配置Toxiproxy,将其上游设置为真实的依赖服务。

这样,当应用程序尝试访问依赖时,会被/etc/hosts重定向到Toxiproxy,再由Toxiproxy转发给真实服务,从而在中间层注入故障。这种方法简单有效,但有一个致命缺陷:如果应用程序在域名后加了一个点(例如 redis.),表示这是一个完全限定域名,系统将跳过/etc/hosts直接查询DNS,导致劫持失效。

模式二:全局劫持(修改DNS)

当本地劫持失效时,一个更激进的想法是直接修改全局的DNS服务器。例如,在集群的CoreDNS中增加一条规则,将所有对redis服务的查询都解析到Toxiproxy的地址。

公式可以表示为:

DNS_Query(“redis.service”) -> Resolve_To(Toxiproxy_IP)

这种方法威力巨大,但影响范围太广,会影响到集群内所有查询该域名的服务,缺乏隔离性。因此,我们仅在对隔离的测试集群或特定的命名空间内进行演练时,才会谨慎使用这种方法。

模式三:途中劫持(拦截发现协议)

对于使用ZooKeeper、Etcd等专门做服务发现的系统,上述两种方法可能都不适用。服务启动时并不知道依赖的具体地址,而是通过查询这些发现服务来动态获取。

以ZooKeeper为例,客户端与服务发现的交互流程复杂:

  1. 客户端连接已知的ZooKeeper地址。
  2. 客户端列出(list)可用的服务节点(如缓存分区、数据库实例)。
  3. 客户端获取(get)某个特定节点的详细信息,其中包含了该服务的真实连接地址(IP和端口)。
  4. 客户端使用获取到的地址连接真实服务。

为了劫持这种模式,我们开发了 Zoo Creeper。它是一个透明的代理,部署在客户端和ZooKeeper之间。

其工作原理是:

  1. 对于普通的list请求,Zoo Creeper直接转发,不做修改。
  2. 当客户端发起关键的get请求时,Zoo Creeper会拦截这个请求。
  3. 它将请求转发给真实的ZooKeeper,并获取到真实的节点信息。
  4. 在将信息返回给客户端之前,Zoo Creeper动态创建一个Toxiproxy实例,并将节点信息中的真实地址替换为Toxiproxy的地址。
  5. 客户端拿到修改后的信息,便会去连接Toxiproxy,从而实现了故障注入。

Zoo Creeper的优点是高度灵活和精准:

  • 协议透明:它完整实现了ZooKeeper的通信协议(这本身是一项挑战,因为协议并未完全公开)。
  • 插件化设计:节点信息可能是各种编码格式(如Protocol Buffers)。Zoo Creeper通过插件机制支持解析和修改不同格式的内容,使其能适配多种服务发现协议。
  • 精准控制:可以精确劫持某一个特定依赖,也可以根据规则批量劫持(例如,“劫持所有位于us-central1区域的数据库”)。

总结 🎓

本节课中,我们一起学习了如何通过劫持服务发现来赋能混沌工程演练。

首先,我们明确了混沌演练对于构建韧性系统的重要性,即使是从创建韧性矩阵这样的桌面练习开始也大有裨益。为了让团队能频繁、安全地进行真实故障注入,提供便捷的工具至关重要。

我们遇到的核心障碍是如何将流量无缝导入故障注入代理。为此,我们深入探讨了三种劫持服务发现的模式:

  1. 本地劫持:通过修改Pod的hostAliases实现,简单但可能被应用优化绕过。
  2. 全局劫持:通过修改DNS实现,影响范围大,适用于隔离环境。
  3. 途中劫持:通过代理拦截并修改服务发现协议(如ZooKeeper)的响应,实现最为灵活和精准的控制,代表工具是Zoo Creeper。

通过将这些模式封装在 Game Day Buddy 这样的工具中,我们使得开发团队能够以声明式、低风险的方式运行演练,真正实现了“可插拔”的混沌工程,从而持续提升系统面对真实故障时的韧性。

042:云环境中的网络流数据

在本教程中,我们将学习如何利用网络流数据来理解和管理云环境中服务间的通信。我们将从实际问题出发,介绍网络流的基本概念,并展示如何通过构建流量图和拓扑模型,来回答关于流量模式、容量规划和风险控制的关键问题。

动机:为什么需要网络流数据?🚀

上一节我们介绍了课程目标,本节中我们来看看为什么需要关注网络流数据。

Slack的一位前首席工程师在调查服务迁移时提出了一个关键问题:如果我们移动服务,我们有多少流量?流量去向何方?我们交付这些流量的能力是否面临风险?

理解服务如何通信至关重要。对于Slack而言,这个问题在当时很难回答,因为我们的服务交付方式非常不统一。我们的工作负载混合运行在EC2、EKS和容器上。服务之间既通过像Envoy这样的服务网格通信,也有完全不参与网格、直接使用控制台或DNS的主机。要找到这些服务间通信的共同点相当混乱。

这个问题不仅限于服务本身。我们所处环境中的网络基础设施(如负载均衡器和网关)也存在有限的约束,例如每秒数据包数或带宽。我们同样不希望这些组件过载。

现状:传统监控的局限性📊

上一节我们了解了问题的复杂性,本节中我们来看看传统监控方法的不足。

以下是当时我们网络监控的现状:我们通过Prometheus的node_exporter等工具获取了单个主机的统计数据。这些网络统计数据是未分化的,它们能捕获主机流量的总量,但无法告诉你流量的去向。它们没有方向性,也无法告诉你这台主机正在与哪个服务通信。在Slack,我们很少关心单台主机的流量,我们需要额外的属性来进行分组分析。

理论基础:流网络与图论🔍

上一节我们看到了传统方法的局限,本节中我们将引入一个经典的图论概念来建模问题。

这种情况让我想起了一个经典的图论问题。我们需要回顾一下大学知识,即“流网络”的定义。图由顶点和边组成,流网络是图的一种特殊情况。它是一个有向图,包含顶点和边,还有一个描述每条边容量的函数。流是一个满足边约束条件的映射,需要满足两个条件:

  1. 可行性条件:一条边上的流量不能为负,也不能超过该边的容量。可以将其类比为城市间的单向公路网络。
  2. 流量守恒条件:流入一个顶点的流量总和必须等于流出该顶点的流量总和,除非该顶点是流的源点或汇点。

为了更直观地理解,请看下图。我们可以将边的宽度想象成道路的容量。我们添加一个红色流,其流量大约是容量的一半,它从源点A经过C、D到达汇点G(有时我们称之为路径)。然后我们添加一个蓝色流,它消耗了这条路径上的全部剩余容量。此时,如果我们想从B到G,就无法再走经过C和D的路径,因为容量已耗尽,必须选择替代路径。这就是流网络定义的基础。

应用:将云架构建模为图🗺️

上一节我们介绍了流网络的理论,本节中我们将其应用到更熟悉的云架构场景中。

让我们看一个大致标准的AWS基础设施图。从左到右,左边可以看作是边缘区域,来自互联网的请求首先到达负载均衡堆栈,经过一个自动伸缩组,落在运行代理功能的EC2实例上。如果请求无法由边缘区域的代理或缓存层本地处理,它会继续传送到流量或中转网关,再传回我们的中央处理区域,经过另一套类似的负载均衡器和实例处理堆栈(例如我们的Web应用堆栈)最终处理请求。

我们可以将这个高级架构模型转换成一个图,清晰地标出所有依赖关系。例如,一个可用区依赖于其上游区域;从架构图可知,网络负载均衡器被限制在单个可用区内;下游的自动伸缩组依赖于NLB,依此类推,直到EC2实例。

现在我们已经用图描述了拓扑结构,接下来需要回答关于流量流的问题:它们的规模有多大?源点和汇点是什么?你所关注的基础设施(可能是云基础设施,也可能是物理设施)的拓扑结构是怎样的?核心问题是:该拓扑图的容量能否满足我们的流量需求?

数据收集:NetFlow/sFlow技术📡

上一节我们建立了拓扑模型,本节中我们来看看如何获取描述流量源、汇和规模的数据。

我们采用了一项历史悠久的技术:sFlow。它有一个RFC定义。本质上,它使用经典的五元组(源IP、目的IP、源端口、目的端口、协议类型,如TCP或UDP)作为哈希键。我们在一段时间间隔内维护几组计数器:匹配该哈希的采样数据包数量,以及这些数据包有效载荷的总和。这使我们能够描述流的源和汇(即源和目的IP地址),而有效载荷的总和则成为我们流的规模。

然而,仅凭这些导出数据,我们仍然缺少完整的流路径信息,因为我们无法基于此理解网络拓扑。

实现:在Slack的构建方案⚙️

上一节我们了解了数据来源,本节中我们看看Slack是如何具体实现的。

在Slack,我们运行一个开源的sFlow代理(任何人都可以使用),部署在每台主机上。我们从特定的隧道接口采样(稍后会详细说明),收集统计数据并发送到基于PMAC的集中式服务。PMAC也是一个开源服务,它包含一个sFlow监听器(即sFlow记账守护进程),从整个集群收集sFlow日志,将其写入磁盘。然后,系统以五分钟为间隔,解析本地磁盘上的日志,并根据我们感兴趣的分组进行注解。我们可以利用sFlow日志中的IP地址,查询其他数据源(如Chef基础设施或Kubernetes基础设施),从而将流量映射并归因到具体的服务、可用区或区域。这样,我们就可以从故障域的角度,用真正有意义的组来注解这些数据。

完成日志注解后,我们将其作为一组Parquet文件写入S3,其文件格式与AWS VPC流日志完全相同。然后,我们将这些数据传递给一个名为Kentik的第三方工具进行进一步分析(稍后讨论)。当然,你也可以运行自己的数据仓库来查询这些数据。

关于搭建成本的一些快速统计:这大约花费了我一个季度一半的时间,其中大部分时间用于将代理滚动部署到整个集群。系统自2024年1月以来一直运行,非常稳定,维护成本极低。

拓扑发现:利用现有工具🧩

上一节我们完成了流量数据的收集和注解,本节中我们解决如何获取网络拓扑信息的问题。

在实现拓扑理解方面,我们借助了Kentik产品。它在底层使用了一系列AWS EC2和网络API的Describe调用来构建拓扑图并理解所有依赖关系。当然,你也可以自己实现这部分功能。

这里简要说明一下为什么不直接使用AWS VPC流日志。在Slack,有几个因素使其具有挑战性:

  1. 我们运行一个全加密的隧道接口,所有流量在传输过程中都是加密的。对云供应商而言,他们只能看到流向VPN监听服务端口的加密流量,难以进行有效归因。我们需要在加密前捕获流量。
  2. 我们运行专用的入站和出站代理堆栈,愿意放弃那部分流量的细粒度信息。
  3. 最重要的是成本:我们描述的这套系统,其月度运行成本比使用VPC流日志低三个数量级(即便宜1000%)。同时,我们还能按照自己关心的故障域对日志进行分解,从而进行更有用的分析。

用例:数据驱动的决策💡

上一节我们构建了完整的系统,本节中我们探讨几个具体的应用场景。

以下是一些重要的用例:

1. 成本分摊与优化
我们的基础设施团队希望将成本分摊回服务所有者。例如,一个网络负载均衡器被服务A和服务B用来访问服务C。通过理解拓扑中的流量,我们发现服务A发送的流量是服务B的两倍。因此,我们可以建立一个成本分摊机制,让服务A承担该负载均衡器三分之二的成本,服务B承担剩余的三分之一。此外,对于跨可用区与可用区内流量的成本差异,现在我们可以基于实际数据与服务所有者进行讨论,权衡流量模式与成本,或许能促使他们重新设计应用以实现更经济的流量流向。

2. 流量迁移验证
在进行VPC对等连接迁移到中转网关网状网络等操作时,迁移过程的每个中间步骤都可能担心流量丢失或路径无效。基于流量需求数据,并将其转化为图论问题后,我们可以利用现有算法进行流量分析,以确定之前讨论的“可行性条件”是否仍能得到满足。

3. 故障影响模拟与容量规划
我们可以更进一步,不局限于迁移场景。回到我们的大型拓扑图,我们可以用它进行类似的分析:模拟移除一个节点(例如整个可用区),然后应用仍然存在的流量,检查是否仍能满足需求。这样,我们可以清楚地看到图形是否被分割成两部分,并评估剩余可用区的容量是否充足。我们还可以迭代检查图中每个节点和每条边被移除时的情况,以确保不过度配置资源,避免为不存在的请求浪费资金。

总结🎯

本节课中我们一起学习了如何利用网络流数据来洞察和管理云环境。

总结一下,要启用网络流数据分析,你需要:

  1. 生成能提供源和汇信息的日志。
  2. 对你的网络拓扑进行建模。
  3. 理解拓扑内的转发规则,以确定流量的路径。

最终,你可以基于这些信息做出明智的业务决策,真正地在无需实际挪动流量的情况下,界定和管控风险。


043:OLTP SQL数据库查询追踪与评分 🛠️

在本教程中,我们将学习如何通过客户端查询追踪和预检评分,来主动与被动地管理大规模OLTP SQL数据库的使用,从而提升系统可靠性和性能。


概述 📋

在Datas公司,由于产品的高速增长,我们管理着数千个跨区域、跨云、多引擎的数据库实例。这些实例具有多样且快速变化的数据库使用模式,并采用多租户架构。数据库并非万能,不当的使用模式(如查询分布变化、优化器选择次优执行计划)常常导致事故。传统的数据库监控工具(如Percona、MySQL Performance Schema)主要从服务器端提供聚合视图,但在处理多租户环境下的使用相关事故时,缺乏客户端上下文信息,这限制了根因分析的效率。

因此,我们发展了一套结合被动响应(事故处理)和主动预防(代码合并前拦截)的解决方案。被动响应方面,我们通过客户端查询追踪,在事故发生时快速定位问题租户或查询;主动预防方面,我们在CI/CD环节引入查询评分机制,阻止不良模式进入生产环境。


挑战与现有工具的局限 ⚠️

上一节我们概述了大规模数据库管理面临的复杂性。本节中,我们来看看具体挑战和传统工具的不足。

我们面临的核心挑战包括:

  • 管理数千个跨区域、跨云、多引擎的数据库实例。
  • 应对多样且快速变化的数据库使用模式。
  • 在多租户架构下,一个租户的昂贵查询可能影响共享同一物理数据库的所有其他租户。

现有工具(如Percona Monitoring and Management, MySQL Performance Schema)非常出色,它们提供了查询级别的资源消耗聚合视图。然而,在处理数据库使用相关的事故时,它们通常不够用,因为其视角主要集中在数据库服务器端

缺失的关键是客户端上下文。 这对于多租户环境尤为重要。例如,当多个租户共享一个数据库,且其中一个租户发出昂贵查询导致整体负载升高时,从服务器监控只能看到全局负载上升,难以直接定位到具体的责任租户。


解决方案一:客户端查询追踪 🔍

认识到客户端上下文的重要性后,我们引入了客户端查询追踪。这为事故响应提供了丰富的维度信息。

通过客户端查询追踪,我们可以在查询中携带自定义标识(如租户ID、API名称),从而能够按这些自定义维度进行聚合分析。如下图所示,X轴是时间,Y轴是数据库耗时,色块按自定义ID切片。我们可以清晰地看到是哪一个色块(即哪一个租户或API)对数据库时间的增长贡献最大。

[示意图:一个随时间变化的堆叠面积图,显示不同自定义ID对数据库耗时的贡献。其中一个色块在事故期间显著凸起。]

凭借这些信息,我们可以在事故期间快速采取行动(如对问题租户进行限流),并在事后修复有问题的查询。这种灵活性为我们提供了宝贵的洞察,并在处理数据库使用相关事故时被证明非常有效。


从被动到主动:预合并查询评分 ✅

虽然查询追踪在事故响应中很有用,但这毕竟是一种事后补救。我们的目标是防止新的不良模式进入生产环境。因此,我们将目光投向了开发流程的更早阶段——代码预合并的CI环境。

我们的理念是偏向可靠而非性能。我们认为某些查询模式根本不应该被提交。以下是我们不希望出现的几类查询:

以下是我们在CI环境中通过查询评分主动禁止的一些不良模式:

  1. 不可预测的查询:如果查询的执行计划不可预测(例如,优化器可能选择次优索引),我们应该通过强制使用索引或调整查询结构来固定执行计划。
  2. 过于复杂的查询:这通常意味着将业务逻辑嵌入了SQL中,这些逻辑应该卸载到应用程序层处理。
  3. 可被重写为更高效形式的低效查询:它们能产生相同结果,但消耗更多资源。
  4. 没有超时限制的查询:可能导致长时间挂起。

为此,我们在数据库客户端的CI流程中引入了基于规则的查询评分机制。它会扫描CI环境中的所有查询,对不良模式发出早期警告,从而防止反模式被合并到代码库中。


查询评分实战示例 🚫

让我们通过几个具体例子,看看查询评分如何拦截危险查询。

示例一:危险的 SELECT ... FOR UPDATE
假设有如下查询和表结构:

-- 被评分为不良的查询
SELECT * FROM orders FOR UPDATE WHERE a=1 AND b=2 AND d=3 LIMIT 1;

-- 表结构
CREATE TABLE orders (
    id INT PRIMARY KEY,
    a INT,
    b INT,
    c INT,
    d INT,
    KEY idx_ab (a, b)
);

问题:该查询在ab列上有索引,但d列没有。如果匹配a=1 AND b=2的行有20万条,而满足d=3的行只有0条,数据库为了找到这“不存在”的一行,需要扫描并锁定这20万行(尽管有LIMIT 1)。这曾导致数据库性能下降600倍。
修复:对于SELECT ... FOR UPDATE,应始终使用点查(即通过完整覆盖主键或唯一索引来精确锁定)。应修改查询,确保WHERE条件能利用合适的索引进行精确查找。

示例二:不可预测的索引选择

-- 被评分为不良的查询
UPDATE users SET status='inactive' WHERE b=1 AND c=2;

-- 表结构
CREATE TABLE users (
    id INT PRIMARY KEY,
    b INT,
    c INT,
    status VARCHAR(10),
    KEY idx_b (b),
    KEY idx_c (c)
);

问题:此UPDATE语句的WHERE条件涉及bc列,数据库优化器可能选择索引idx_bidx_c。在生产流量下,优化器可能突然切换索引,而新索引的性能未经测试,存在风险。在一次真实事故中,类似的查询导致优化器选择了全表扫描(1亿行),期间所有写操作均失败。
教训:不要完全信任优化器。
修复:创建一个复合索引(b, c),或使用索引提示明确告诉优化器使用哪个索引。

查询评分还会检查其他模式,例如:DELETE/UPDATE操作必须与索引和LIMIT结合使用、禁止全表扫描和全索引扫描、禁止无索引查询等。


演进:统一的数据库使用评分卡 📊

数据库使用不仅关乎查询,还涉及流量、表结构(Schema)和数据分布,它们紧密耦合。因此,我们的下一步是构建一个统一的数据库使用评分卡

我们整合了本教程中涵盖的所有组件:

  1. 全链路追踪:追踪查询从预合并到生产环境、从数据库客户端到数据库服务器的全过程。
  2. 模式(Schema)追踪:将同样的理念应用于数据库表结构,从设计到上线全程跟踪。

一个数据库所有者可以获取其数据库的综合评分。评分维度包括:

  • 流量:查询是否没有设置超时?是否从未在CI中测试过却在生产运行?
  • 查询:查询是否没有命名(难以追踪)?是否需要检查过多行才能返回一行结果(资源消耗过大)?查询成功率是否过低?
  • 表结构(Schema):检查多项规则,例如:
    • 表是否没有主键?
    • 是否有过多或重复的索引?
    • 是否使用了无边界、可能随机暴增的列类型?
    • 读写放大比是否异常(可能表明数据模型设计不佳)?

此外,我们还在尝试将成本、数据分布和数据库弹性等更多维度纳入评分体系。

数据库使用评分卡大致如下图所示,每个数据库都会有一个总分,并分解到流量、查询、表结构等不同维度。

[示意图:一个评分卡面板,显示数据库名称、总体健康分数,以及流量、查询、Schema等子项的分数和详情。]

所有这些评分卡数据以及客户端查询追踪功能,都构建在Databricks的产品之上。作为Databricks的客户,你也可以快速构建类似的解决方案。


总结 🎯

本节课中,我们一起学习了如何通过结合被动响应和主动预防两种策略,来管理大规模OLTP SQL数据库的使用。

  • 被动响应:通过客户端查询追踪,在发生事故时提供丰富的客户端上下文(如租户ID、API),帮助我们快速定位根因,这已被证明非常有效。
  • 主动预防:通过数据库使用评分卡,在代码预合并的CI阶段就执行数据库最佳实践,从流量、表结构和查询等多个维度进行约束。

这两者共同为我们的数据库最佳实践提供了保障,使我们能够通过这套机制有效管理上百个服务团队。

044:将移动端纳入可靠性视图 🚀

概述

在本节课中,我们将探讨如何将移动端数据纳入系统可靠性工程实践中。我们将了解为什么仅关注后端服务不足以反映真实的用户体验,并学习如何通过移动端可观测性来构建更完整的系统视图。


为什么需要移动端可观测性?🤔

上一节我们概述了课程目标,本节中我们来看看为什么移动端数据至关重要。

你是否见过这样的系统视图?一个非常简化的系统视图可能如下所示:可靠性团队关注他们的服务、运行时间、各个组件如何通信、传入的请求和传出的响应。服务可能具有高达99.999%的可用性,一切都在服务等级目标内。这似乎意味着每个人都很满意。

但是,这里缺少了什么?当我们谈论“每个人”时,很可能指的是“人”,即用户。在每一个额外的“9”背后,可能都有一位用户正盯着手机,感到沮丧。你是否在了解他们是否满意?

因为用户体验发生在用户所在的地方。它可能是用户无法完成的支付,或是无法点赞邻居家小狗的照片。用户感知到的体验发生在客户端,尤其是移动设备上。这就是用户体验。如果你没有提供出色的用户体验,怎么能说你的技术栈对用户是成功的呢?

当我们谈论好的或坏的用户体验,或反映这些体验的服务等级目标时,它们并非在服务层被体验。用户体验有点像你整个系统的倒影,因为它发生在客户端。那个客户端可能是系统中每一个服务、数据库、组件、管道、牙签和胶带的最终结果,但你必须在那个系统的末端取得成功,因为那是用户所在的地方。

因此,存在大量你可能尚未收集的信息。不仅仅是按钮点击和滚动,还有一系列与客户端特定问题相关的交互,这些问题影响着个人和用户与系统的关系。这是一整盘复杂的问题。


一个餐厅的比喻 🍽️

让我们从一个小故事开始。

想象一下,你是一名SRE,热爱计算机,但你也喜欢烹饪。如果你开一家餐厅会怎样?你通过服务顾客来赚钱。你的背景是计算机,所以你要确保质量保持在高水平。你会测量各种指标,观察厨房的运作,因为美味的食物加上快速的准备意味着满意的顾客,对吗?

但是,如果顾客入座有延迟,等待很长时间才能有桌子呢?如果服务员态度粗鲁,不以顾客为导向呢?如果酒水定价过高,人们只能选择质量较低、更便宜的酒呢?那么,你会得到这样的评价:“太慢了,服务差,酒水糟糕,再也不来了,一星。”而你的食物可能很美味,上菜也很快。这怎么可能呢?

满意的食客需要的不仅仅是好食物。满意的食客需要良好的整体体验。将这一点转换到我们的世界:满意的用户需要良好的体验,而不仅仅是服务器端的快速处理。


移动端特有的故障点 📱

上一节我们通过比喻说明了整体体验的重要性,本节中我们来看看移动端特有的挑战。

工作流程从设备开始,也在设备上结束,这中间有大量的失败空间。你的网络请求在发送和接收时可能在后端出现问题。

如果请求从未被你的后端接收到呢?这可能发生。应用可能在发送请求前崩溃。可能存在网络问题,比如DNS问题,或者用户走进了电梯,信号中断了。或者只是应用中的一个错误,导致请求在客户端队列中等待发送,却从未发出。你可能对此一无所知。

但有时你会知道失败,比如请求被延迟了。你知道它失败了,但你知道延迟是否发生在客户端吗?也许只是发送请求花了很长时间。如果客户端存在拥塞呢?杂乱的SDK占用了所有连接,繁忙的后台进程占用了所有CPU核心,导致你的网络请求无法发出。用户体验会很差。

用户可能已经花费数分钟填写了大量表单和操作,才发出一个请求。但你对此并不知情,而用户可能已经感到恼火了。应用中也可能存在错误,因为这种情况时有发生。有时由于优先级难以处理,请求只是排在队列后面。

如果你的服务器记录了200状态码,连接关闭,一切正常,成功,对吗?但如果应用在处理响应时出错了呢?这可能发生吗?反序列化失败经常发生。客户端处理响应可能很慢:反序列化、注入数据库、再取出、重新加载,可能还需要加载一些大图片。整个过程可能花了10秒钟。请注意,在应用上,我们以秒为单位测量延迟。所以,感谢你在后端节省了100毫秒,但用户等待了10秒。

同样,应用中的错误也可能导致问题:我收到了响应,但就是不想显示它,因为界面被折叠了,无法滚动。所以很遗憾,你的用户没有得到满足,而你的后端却说:“给你,成功了。”仪表盘上到处都是绿色。

不幸的是,如果移动应用是用户正在使用的界面,那么在用户数据中心之外存在大量的故障点。


移动环境的挑战 🌍

上一节我们列举了具体的故障点,本节中我们深入探讨移动环境本身的复杂性。

因为移动环境是一个充满敌意的环境,我们工作的所有东西都是不可预测且充满陷阱的。例如,我之前提到的那些错误。我找到了,15分钟就修复了。很好,部署了。但还要等几天进行QA测试,然后进入Alpha、Beta版。终于可以发布了。发布到应用商店,等待审核,这可能需要三天。然后,终于发布了。等等,不,只是1%的灰度发布,确保没有崩溃。再等24小时。24小时后,错误修复发布了。但是,只有当用户下载了更新,修复才算真正发布。而且,更新的长尾效应非常长,不是所有人都会立即更新。所以,如果你在15分钟内修复了一个错误,可能需要15天才能覆盖到大部分用户。

运行时环境也是不可预测的。有各种制造商生产的各种设备,运行着各种版本的Android或iOS。你能获得多少资源,RAM、CPU周期,取决于操作系统愿意给你多少。如果你走进电梯,没有信号了,请求就失败了。你以为它会工作,它也确实在工作,直到它不工作了。这是因为你无法预测你将在什么环境下运行。

最后,用户对性能的期望差异巨大。我使用一部快速的手机和快速的网络。如果响应时间是两秒,那还可以接受。但如果你用的是一部十年前传下来的旧手机,并且因为身处农场而使用2G网络呢?那么,如果你按下一个按钮,手机旋转了大约20秒然后显示成功,你可能会觉得:“天哪,这工作得真好。”因此,应用性能的好坏取决于观察者,它不是一个绝对的数字。你不能简单地画一条线说这是好性能,那是坏性能。它比那要微妙和复杂得多。


解决方案:从移动端收集数据 📊

让我们回到我们的故事。你意识到经营餐厅的前厅部分很困难,你更愿意和计算机打交道。让我们转向一个食品配送应用。现在,厨房就是你的数据中心,那是你进行所有检测和测量的地方。

如果在数据中心之外出现问题怎么办?那么,你会失去业务。因为作为一家餐厅,如果你的应用对我来说不能用,我就会使用其他应用,去其他餐厅。最隐蔽的是,你甚至不知道你失去了业务,因为你不知道有人曾试图使用你的应用来订购食物,因为你没有收集数据中心之外的任何信息。

一个快速的解决办法是什么?是的,当然。快速解决办法就是开始检测应用。添加任何检测点。我确信你为产品团队设置了类似分析的工具。所以,就从那里获取一些数据。任何数据都可以。请开始从外部获取一些信息。即使不是超级详细,它至少能为你提供额外的背景信息,告诉你谁在尝试使用你的应用,以及可能是在什么环境下使用的。

从小处着手。但理想情况下,你想要什么?你希望应用像后端一样可观测。这意味着遥测数据。我谈论的不仅仅是设备上操作的速度和日志级别,我谈论的是以用户为中心的遥测数据。你想了解用户的想法和行为,所有带来良好用户体验的性能指标,以及最终的结果:它是否成功了?用户是否在你的应用中停留了足够长的时间以获得价值?遥测数据必须以用户为中心,而不是以机器为中心。

在此基础上,你还需要添加上下文。这就是魔法发生的地方。你为遥测数据添加上下文,就像为强大的进攻线添加了明星球员,你就能在超级碗中创造奇迹。你得到的是真正的移动可观测性,能够回答关于你应用的问题,使用的是你之前未曾预料到的数据。


行动起来:拥抱移动可观测性 🚀

关于这一切,可观测性并不新鲜,对吧?它已经被使用了很长时间,用于了解应用程序、将系统链接在一起、做出关键决策,基于日志聚合已有二十年了。这很困难,但并不新鲜。

关于前端应用的信息也不新鲜。在任何前端实践中,从一开始,产品团队就总是想要了解应用的信息。他们总是想知道发生了什么,用户做了什么。但开发人员并没有真正有机会去问“为什么”。所以,客户端可观测性的目标是了解用户的“为什么”:发生了什么以及为什么会发生。

但让我们友善一点。这对移动团队来说是新的。在移动领域,没有使用遥测数据来了解应用程序的传统。因为他们为什么需要呢?他们通常只被告知有崩溃发生,有人在应用商店留下了差评,或者CEO不喜欢某个功能。所以他们是反应式的。当你不与最终用户共情时,在生产环境中测量事物是困难的;但当你共情时,如果你还没有锻炼过这块“肌肉”,那就更困难了。移动开发人员还没有让这块“肌肉”工作起来。所以,帮助他们。

移动和前端的遥测数据可以看起来和后端一模一样,只是目前还没有。让你的团队开始使用OpenTelemetry从应用中记录一些东西。找出你应用中最重要的部分何时失败。因为当你在Otel中记录一些东西时,它会看起来像你在系统其他部分收集的其他遥测数据。数据形态会非常相似,只是会包含移动端特定的信息。所以,与系统的其他部分共享这些数据和上下文,将它们全部链接在一起。

然后,教他们如何锻炼你已经熟悉的那块“肌肉”:询问关于系统的问题。你可能不熟悉用户交互的性能数据,但你熟悉在整个系统中关联信息。所以,问一些问题:为什么我无法结账?为什么日本的用户无法登录?应用商店的这些评论是真的吗?还是我们更了解情况?

如果有一个要点你应该记住,那就是:为你自己获取一些“Molly”。移动可观测性非常重要,所以去获取一些。或者更具体地说,不是所有的“Molly”,而是移动可观测性。但“Molly”更容易记住。所以,去获取一些“Molly”吧。


总结

本节课中,我们一起学习了将移动端纳入系统可靠性视图的重要性。我们了解到,仅关注后端指标无法反映真实的用户体验,因为大量的故障和性能问题发生在客户端。我们探讨了移动环境的独特挑战,如碎片化的设备、不可预测的网络和用户期望的差异。最后,我们强调了通过以用户为中心的遥测数据和OpenTelemetry等工具实现移动端可观测性的必要性,并鼓励团队开始收集和分析移动端数据,以构建更完整、更准确的系统可靠性视图。

045:从HAR到OpenTelemetry追踪

概述

在本节课中,我们将学习如何将浏览器网络活动记录(HAR)转换为OpenTelemetry追踪数据,从而提升Web应用的可观测性。我们将介绍OpenTelemetry、HAR文件以及ThousandEyes平台,并详细讲解转换的架构、过程与实际应用。


课程内容

1. 介绍OpenTelemetry 🧩

上一节我们了解了课程主题,本节我们来认识核心工具OpenTelemetry。

当您听到OpenTelemetry时,可能会想到可观测性。它是一个供应商中立的开源框架,用于管理遥测数据。您可以收集来自服务和基础设施的所有遥测数据,进行处理,然后导出到不同的可观测性后端平台。

我的定义是:它是一个开源的可观测性框架,用于管理遥测数据。今天我们将专注于追踪(Traces),但它也包含指标(Metrics)、日志(Logs)等信号,并且正在讨论性能剖析(Profiling)。

谈到OpenTelemetry就不能不提Collector。它就像是遥测数据的主要支柱。Collector允许我们以多种不同的协议和格式接收数据,例如Prometheus、Jaeger、OTLP(OpenTelemetry标准格式)。您也可以查询Prometheus或数据库,甚至从文件中读取数据。然后,您可以在此处理数据:丰富数据、过滤数据。最后,您可以将数据导出到您的可观测性平台,或者写入文件。这完全由您配置。社区已经创建了许多现成的组件,但您也可以构建自己的组件,这并不难,我鼓励大家都尝试一下。

2. 认识HAR文件 🔍

上一节我们介绍了OpenTelemetry,本节中我们来看看什么是HAR文件。

有多少人知道HAR或HTTP存档文件是什么?有多少人在过去一周或一个月内使用过它?可能没有。HAR通常是我们用于在本地调试网页的工具。当出现问题时,支持团队可能会要求您:“请发送您的HAR文件,以便我查看发生了什么。”

HAR文件包含了您网页的所有交互信息,即执行的所有请求。它通常是JSON格式,并包含敏感数据,如Cookie、带有电子邮件或密码的请求头等,请务必注意。

HAR文件通常看起来像这样:它是一个JSON格式的文件,包含了加载页面时发出的所有请求信息。您会看到HTTP版本、请求方法、响应代码、URL等数据,这些都非常有用。

3. 了解ThousandEyes平台 🌐

现在,让我们将目光转向ThousandEyes。

ThousandEyes是一个用于监控网络的应用。我们监控全球的网络,在全球拥有云代理节点,以及可以在您自己基础设施中部署的企业代理节点,确保您的网络拓扑正常工作。

它是实时的,因为谁希望首先从客户那里得知中断消息呢?这里有一张图片,展示了当前全球的中断情况,例如Monday.com、微软在比利时的问题,您可以看到这个工具多么有用。

现在,让我们重点介绍ThousandEyes中的页面加载测试。

页面加载测试用于模拟网页加载页面时的行为。在这里,我们可以找出页面加载时间、加载完成情况以及加载页面所执行的所有请求。

实际上,HAR就在这里。您在那里看到的是我们以瀑布图形式展示的HAR,但这些数据仅存在于ThousandEyes内部。如果您想在外部使用它,比如在Grafana或Honeycomb中,您需要将其转换为OpenTelemetry追踪,这就是我们今天要做的事情。

4. 转换过程:从HAR到追踪 ⚙️

上一节我们了解了数据来源,本节中我们来看看核心的转换过程。

转换过程如下:每个HTTP请求将被转换为追踪中的一个跨度(Span)。

这个跨度将包含以下字段:

  • 名称:包含请求方法和目标地址。
  • 类型:始终是“客户端”,因为我们是向服务器、向网页发出请求的一方。
  • 开始时间和结束时间:这是理解性能问题的关键。
  • 状态:如果成功则为“OK”,如果出现错误则为“Failed”。
  • TraceID和SpanID:由我们自动生成。

本周有一个关于OpenTelemetry语义约定的会议,非常有用。我们使用语义约定是因为它能赋予数据额外的含义。

以下是跨度属性的示例:

  • http.request.method
  • server.address
  • server.port
  • http.url.full
  • http.response.status_code

如果我们都遵循相同的规则,语义约定将非常有用,并为我们的数据增添额外含义。这就是为什么任何可观测性后端在接收数据时(例如IBM),都能理解这是一个具有以下字段的HTTP请求。

您可能还会想到请求头。OpenTelemetry将其视为可选项。原因是它们可能包含敏感数据,存在泄露风险。您可能想到数据脱敏,但这也可能失败。在ThousandEyes,我们不会流式传输这些请求头,主要是出于敏感数据风险的考虑,而且它们可能非常庞大。我们真正感兴趣的是理解性能问题,而不是发送或接收了哪些请求头。

最后,让我们谈谈资源属性。

资源属性是一种标识产生遥测数据的资源的方式。在这里,我们将资源标识为ThousandEyes测试和运行该网络测试的代理。我们将包含您的ThousandEyes账户ID、测试类型、名称和ID,以及运行测试的代理信息。我们还将包含代理名称、代理位置、代理类型等信息,以及一个永久链接。这在调试问题时非常有用:您不仅可以识别问题,还可以通过永久链接跳转到ThousandEyes,以获取有关该网络问题的完整概览。

我们已经知道追踪的样子,让我们看一个例子。对于资源属性,所有跨度都会包含。对于每个跨度,我们将有时间、名称、跨度属性等。这只是JSON格式,但当您意识到它在您的可观测性后端平台上多么有用时,您就会需要它。

5. 系统架构 🏗️

现在让我们深入了解架构。我希望它不会太复杂,但您能理解我们是如何使用它的。

在ThousandEyes,我们的代理生成所有测试数据,这些数据包含HAR。然后,数据被发送到我们的追踪Collector。我们使用OpenTelemetry Collector,实际上,追踪Collector会处理所有数据,并将其发送到集成Collector。在集成Collector中,我们进行路由:决定哪些数据点去往哪个可观测性后端。用户决定特定测试的所有遥测数据去往哪个后端,他们创建集成来建立这种连接。

让我们放大一点看。数据从Kafka接收,我们将其从HAR转换为OpenTelemetry追踪。然后,数据进入处理器,我们过滤掉不会被发送到任何地方的数据。请记住,ThousandEyes运行着成千上万个测试,数百万个代理,因此我们需要过滤掉不需要的数据。在属性处理器中,我们会丰富数据。我们最初只接收测试ID,但我们还需要知道测试站点、测试名称、代理位置等信息。这些信息对于了解问题是仅发生在旧金山,还是西班牙也有问题,至关重要。之后,出于性能原因,我们会将数据批量处理。然后,我们将其导出到集成Collector。

集成Collector将处理这些数据点。处理时,一个数据点可能会被发送到两个可观测性后端,以实现冗余,或者因为您正在进行迁移,或者任何其他原因。我们需要复制该数据点,并添加一个新的属性:集成ID。

在OTLP路由(OTLP导出器的一个扩展)中,我们决定:这个数据点属于那个集成,它将去往那个可观测性后端,比如Grafana;另一个去往Honeycomb;还有一个去往IBM Instana。您会意识到我们需要路由这些数据。

回顾一下,我们在Collector中还有扩展的概念。扩展是可以通过Collector所有管道访问的组件。这里我们有健康检查(在Kubernetes中,您需要知道Collector何时准备就绪)、存储扩展(我们从元数据服务获取所有集成、测试、代理的数据,并存储在缓存中)。我们还有Pub/Sub系统,我们在此发布数据,并从其他组件接收数据。例如,每当数据点被过滤掉时,我们就会向这个Pub/Sub系统发送事件,然后在指标扩展中,我们接收该事件并递增计数器。我们还有Sentry,用于捕获所有的panic或Go错误。指标扩展则用于监控我们自己的可观测性系统,我们了解关于数据过滤、接收、发送和导出等的所有指标。

我想强调一点,我忘了说明为什么我们有两个Collector而不是一个。首先,当然是因为它们属于不同的业务领域:第一个是追踪领域,负责处理追踪数据和转换;第二个是集成领域。其次,它们的扩展规模不同。追踪Collector的扩展取决于我们从测试中接收的数据量,而集成Collector的扩展取决于用户创建了多少集成。

6. 演示:实际应用 🎬

让我们进入演示环节,看看实际效果。

我们将创建一个ThousandEyes测试,该测试将访问Cisco.com,加载Cisco.com网页。这个测试将为我们提供加载Cisco.com页面所需的时间、完成错误(如超时),当然还有我们的HAR。HAR包含加载该网页所需的所有请求,但这些数据只存在于ThousandEyes内部。如果您需要将这些数据用于其他地方,您无法直接获取。这就是我们使用OpenTelemetry追踪的原因。

现在,我们将创建一个集成来决定:来自该测试的数据,我想发送到Jaeger。我进入OpenTelemetry集成,选择我的目标,选择类型为“追踪”,选择测试,然后创建它。现在,所有数据都将流向Jaeger。

在Jaeger中,您可以可视化所有追踪。一个追踪看起来会是这样:所有请求将作为不同的跨度出现。对于每个请求,您将拥有我们之前提到的信息:资源属性(标识ThousandEyes)和跨度属性(标识特定请求,如响应代码和HTTP方法)。所有这些信息都会显示在那里。

我们还可以查看单个跨度的截图,并逐一检查。在文本中,我们将看到,如前所述,请求方法(例如GET)、响应代码(200)、我们访问的服务器地址、服务器端口、类型和URL。对于每个单独的请求,我们都会有这些信息。然后,对于进程标签(在Jaeger中这样称呼),也就是资源属性,它标识了ThousandEyes:您的账户、数据模型版本、永久链接、您的代理、您的测试,所有这些信息都在那里。

这很酷,但我们如何利用它呢?每个可观测性平台都有自己的优势。我只是用Jaeger做演示,但在这里我们可以做这样的事情:例如,只筛选出有错误的追踪(即页面加载出错的追踪),然后您会看到所有有错误的追踪,这多么有用!然后您可以深入查看,发现加载Cisco.com时出现了错误请求,就在那个特定的跨度、那个特定的请求中。现在,您将看到您的客户实际面临的情况。

另一个用例是时间分析:为什么有些页面加载需要5秒,而另一些需要7秒?可观测性平台的另一个优点是您可以比较两个追踪。您可以说:“这个追踪花了5秒,而另一个花了7秒,差异很大。为什么会这样?” 然后您可以进行比较,发现一个追踪有210个跨度,而另一个有245个。为什么它们不同?现在您可以逐一检查,理解为什么这些请求在我们的系统中花费的时间不同。

7. 经验总结与回顾 📝

我想以这个项目的经验教训来结束本次演示。

关于追踪,每个人都在谈论微服务以及它们如何相互连接,但还有许多其他用例。这只是另一个用例:我们可以将HAR转换为OpenTelemetry追踪并进行调试。

关于OpenTelemetry,有一个庞大的社区在努力使其变得更好,他们非常乐于助人。您可以向他们提问,可以在GitHub上创建问题,我们甚至为我们发现的一些问题做出了贡献。

关于Collector,之前有一个会议讨论过它,它是一个成熟的产品。开始时创建处理器、接收器可能很困难,但一旦您创建了一些,它们就能完美地协同工作。它运行得非常好,我真心鼓励大家都去尝试。

最后但同样重要的是,我们最初的关键信息是什么?我们希望改进对系统的监控,我们希望识别错误,我们希望更深入地了解我们的Web应用。


总结

本节课中,我们一起学习了如何通过将HAR文件转换为OpenTelemetry追踪来增强浏览器可观测性。我们介绍了OpenTelemetry框架、HAR文件格式、ThousandEyes平台的作用,并深入探讨了转换的架构、数据处理流程以及在实际监控、调试和性能优化中的应用价值。通过利用OpenTelemetry的标准化和强大社区,我们可以更有效地监控Web应用性能,快速定位问题。

046:理论构建与实践 🏗️

在本节课中,我们将学习如何将技术债务视为一种理论构建和实践的过程。我们将探讨技术债务的本质,并学习如何运用隐喻来更好地理解和沟通它,从而制定有效的应对策略。


概述

技术债务是软件开发中一个普遍且令人头疼的问题。我们常常感觉它阻碍了我们的工作,却又难以向他人解释清楚其重要性和紧迫性。本次分享将不会提供“一个神奇的技巧”来消除技术债务,而是专注于如何构建我们自己对技术债务的理解模型,并利用这种理解来更有效地沟通和行动。


演讲者背景与视角

我是Yvonne Lamb,一名工程师。我既尝试解决技术债务,也常被同事视为能解决技术债务的人。我的经验主要来自规模在40到400人之间的科技初创公司,而非大型科技企业。

我的个人视角受到两项爱好的影响:赛艇和直言不讳的性格。作为赛艇手和舵手,我学会了在无法直接沟通的情况下与他人协调。而“灾难性的直率”则让我习惯于清晰地表达复杂想法。这些经历塑造了我看待协作和沟通的方式。


技术债务:一个难以言说的问题

我们很容易在非正式场合(比如会议酒吧)对技术债务高谈阔论。然而,当我们需要向有决策权的人解释“为什么需要解决它”时,却常常语塞。我们可能会陷入两种不理想的反应:

  1. 简单地说“因为它很糟糕”。
  2. 或者,陷入技术细节的泥潭,让听众感到困惑。

这两种方式都难以促成有效的对话和行动。问题的核心在于,我们常常跳过“自己先想明白”这一步,直接试图用他人(如业务方)的语言去填充一个“表格”,而缺乏一个能让自己信服的、清晰的思考框架。


技术债务概念的起源

技术债务(Tech Debt)这个概念通常被认为是由Ward Cunningham在1992年提出的。当时他正在为一个金融服务公司开发Smalltalk项目,并深受《我们赖以生存的隐喻》一书的影响。

他使用“债务”这个金融隐喻,是为了向非技术人员解释:为了快速发布产品而编写“不够完美”的代码(举债)可以加速开发,但之后必须通过重构来“偿还”这笔债务,否则将持续支付“利息”——即未来每次修改时都要付出的额外精力。

一个更宽泛的定义来自Jean Kim:技术债务是当你下次想去修改某些东西时,所感受到的所有阻碍。这一定义捕捉了技术债务带来的挫败感,但可能过于强调“过去做错了什么”。实际上,很多技术债务只是随着时间和需求变化自然累积的,当初的决策在彼时可能是合理的。


作为社会概念的技术债务

技术债务是一个“社会概念”——在一个社群(如工程师群体)中,我们大致理解它指的是什么,并共享一种“这是个问题”的共识。然而,它并非一个具有精确操作定义的魔法词汇。不同组织、不同团队对“什么是技术债务”以及“哪些需要优先处理”的看法可能大相径庭。

因此,我们不能仅仅抛出“技术债务”这个词就期望获得资源和支持。我们需要做“准备工作”:深入理解特定的技术债务,并找到一种能向不同受众有效传达其影响和解决方案的方式。


一个新的思考隐喻:家务劳动

我主张使用家务劳动作为思考技术债务的隐喻。这不一定是你最终用于说服他人的那个比喻,但它可以是一个强大的内部思考工具,帮助你理清利害关系、评估各种方案。

为什么是家务劳动?因为技术债务和家务劳动都具有基础设施的特性。社会学家Susan Leigh Star总结了基础设施的几个关键属性,它们同样适用于技术债务和家务:

  1. 嵌入式:嵌入在其他结构、社会安排中。
  2. 透明性:在正常运作时不可见,只在“故障”时显现。
  3. 范围广:其影响超越单一地点或事件。
  4. 通过实践学习:成为某个实践社群的一员后才会掌握。
  5. 路径依赖:建立在已有的实践和决策之上。
  6. 通过标准/约定与其他设施联结
  7. 模块化修复:通常被一点一点地修复,而非一次性重建。

金融隐喻(债务)在某些场景(如金融科技公司)可能更直接有力,因为它有成熟的记录、法规等基础设施支撑。而技术债务则缺乏这种清晰的“记账”体系。家务劳动隐喻则更贴近我们维护复杂、持续运行的系统的日常体验。


案例解析:从家务看技术债务

以下是几个通过家务劳动类比来理解技术债务具体形态的例子。

1. 更换马桶:人们需要看到积极的变化

家务故事:我家有一个老式马桶,不节水,但我们总觉得“没有好时机”更换它。直到侧下水道破裂,不得不进行大规模维修。在工程尾声,我们决定让工人顺便安装一个新马桶。虽然主要工程是修下水道,但新马桶这个“看得见的改进”让巨大的花费感觉更值得。

技术债务故事:在Chef,我们团队的首要项目是重建标准化构建管道。然而,内部用户最关心的是“能获得对外发布的软件包仓库”。虽然包仓库并非我们工作的核心目标,但意识到这一点后,我们将其作为项目的重要成果来沟通和交付,这极大地提升了内部客户的满意度和耐心。

核心要点

  • 进行基础设施或技术债务相关工作时,为内部客户找到一个他们能感知的“胜利点”至关重要。
  • 这并不意味着要做额外的工作来制造“新功能”,而是找到你正在做的工作中,那些能直接改善他人体验的部分,并清晰地传达出去。

2. 冰箱与路径依赖:有些决定早已注定

家务故事:老冰箱坏了,我们发现现代美式冰箱体积太大,无法通过1947年老房子的门。最终只能选择一款昂贵的进口澳洲冰箱,它甚至太高,迫使我们拆除了橱柜。安装的便利性被多年前的房屋结构决策所限制。

技术债务故事:我们继承了一个难用的软件包仓库。交接时得知,当初选择它仅仅是因为“它用Helm图表部署很方便”。后来才发现它本质上是为镜像仓库设计的,删除包极其困难。等我们接手时,所有关键的技术选型决策都已无法更改。

核心要点

  • 许多技术债务的根源在于早期的、具有路径依赖性的决策。
  • 意识到这一点后,我们可以更主动地记录和说明我们创建的服务的设计意图和边界,防止后人将其误用于不合适的场景。

3. 奇怪的壁橱:我们必须一次性全部修复吗?

家务故事:我家有一个80年代私自扩建的壁橱,位置不佳、保温差、电路危险。完全重建它是一个大工程。我们的策略是:在进行其他房屋维修时,顺带解决与之相关的问题,比如修补屋顶、增加防水层。我们持续关注这个问题,并寻找模块化修复的机会。

技术债务故事:在Chef,我们需要将构建环境从老旧、耗能的Solaris机器迁移到新硬件。一个关键问题是:新硬件生成的二进制文件能否向后兼容到更旧的Solaris 10 U1?没人知道是否有客户还在用这个版本。我们兵分两路:一路研究技术可行性,另一路(我负责)竭力调查“是否真有此需求”。这个过程揭示了公司内部在客户使用情况追踪上的诸多信息盲点。

核心要点

  • 面对复杂的技术债务,要持续追问两个问题:我们真的必须做这件事吗? 以及 我们能做到吗?
  • 保持对“问题空间”的关注——即理解用户及其上下文的全貌,而不仅仅是表面需求。
  • 技术债务的修复往往是渐进和模块化的,而非一蹴而就。

4. 香料架:为事物找到归属地

家务故事:我不需要完美的香料架。我的解决方案是把香料放在橱柜门上的罐子里,顶部贴上标签。它不完美,但每种香料都有固定的、易于找到的位置。关键在于“为物品建立一个家”。

技术债务故事:Etsy的“Deployinator”系统。他们并没有先构建一个完美的部署系统和UI,而是先建立了一个前端(UI)和后端(引擎)的清晰边界。前端满足了工程师日常部署的急需,而后端的复杂重构则可以在此基础上稳步进行。

核心要点

  • 处理混乱(无论是杂物还是代码)的有效方法不是追求完美,而是为事物建立清晰的归属和接口
  • 通过定义清晰的边界和接口,可以将庞大、混乱的问题分解为可管理的部分,并优先解决阻碍当前工作的部分。

一个反面教材:如果不与相关方沟通并达成共识,就制定一个庞大的、一蹴而就的技术债务清偿计划,结果可能像一只被剃光毛的猫——看似解决了问题,实则带来了新的痛苦和不适。不要这样做。


总结:理论作为解放性实践

本节课中,我们一起探讨了技术债务的本质,并引入了家务劳动作为思考它的一个丰富隐喻。我们通过多个案例看到,技术债务与家务劳动一样,具有基础设施的特性:嵌入式、透明、范围广、路径依赖。

使用这样一个隐喻,不是为了直接说服你的项目经理或老板,而是为了帮助你自身进行理论构建。它为你提供了更多“把手”,让你能更清晰地分析特定技术债务的利害关系、可能的解决路径以及沟通策略。

技术债务的讨论常常伴随着强烈的情绪,这某种程度上是对抗熵增(系统趋向混乱的自然规律)的挫败感。拥有一个自己的理论模型,就像数学家面对混沌数据时一样,能让我们在无序中看到模式,在起点上重获认知的快乐。

最后,引用剧作家汤姆·斯托帕德《阿卡迪亚》中的台词:“未来是混乱的……当你以为你知道的一切几乎都是错的时候,这是活着的最好时代。” 拥抱这种复杂性,用我们构建的理论来指导实践,正是管理技术债务的核心。


本节课中我们一起学习了:

  1. 技术债务沟通的难点在于缺乏有效的个人思考框架。
  2. Ward Cunningham提出的“债务”隐喻有其特定背景。
  3. 技术债务是一个需要具体化的“社会概念”。
  4. 家务劳动是一个有用的思考隐喻,因为它和技术债务共享基础设施的诸多属性。
  5. 通过多个类比案例(马桶/冰箱/壁橱/香料架),我们学习了如何分析技术债务的 stakes、路径依赖、修复策略和优先级。
  6. 最终目标是构建个人化的理论模型,以更清晰、更有力地进行沟通和决策,将应对技术债务视为一种“解放性实践”。

047:AIOps - 证明它!致SRE AI产品供应商的一封公开信 📝

在本节课中,我们将学习如何批判性地评估那些声称能为SRE(站点可靠性工程)工作带来变革的AI产品。我们将探讨如何区分营销炒作与实际价值,并学习一套实用的提问框架,以确保引入的自动化工具能真正增强团队能力,而非削弱它。


感谢精彩的介绍。也感谢Fred的参与。

现场有多少人看到这个主题时心想:“天啊,又是关于AI的演讲。”没关系,这是一个安全的空间,你可以不喜欢它。我最初也持怀疑态度,这是我的默认立场。但在AI这件事上,我经历了一段漫长的旅程。

我知道我不是这里唯一有这种感受的人。市场上充斥着大量炒作。对我们这类人而言,我们的超能力之一就是对炒作极为反感。但在炒作之下,现实是,这是一种新型计算机的诞生,许多事情正在改变,这也是我认为它如此重要的原因。

我们需要随之做出一些改变。这次演讲的起源,是《Sssri Weekly》的Lex Neva与我谈论他每天如何收到供应商推销其AI SRE产品的宣传,这些宣传读起来就像广告。Lex会回复他们,要求对方“拿证据给我看”,但之后就再也没收到回音。他写了一封精彩的公开信,我鼓励大家去读一读。链接已附上。他基本上是说,赢得SRE信任的方式是提供实例和数据。让我知道你也了解这个产品可能如何失败。同样,他再也没有收到回复。

在这篇博文发布后不久,Lex就去Nvidia工作了。所以现在他赚了很多钱,拥有很多GPU。这就是为什么我亲爱的朋友Fred今天和我一起做这个演讲。

我上网查看了所有AI产品的宣传和它们做出的承诺。我看到诸如“这将成为你的新SRE伙伴”、“它能回答所有问题、审查代码、主动监控和预警”、“自适应学习和零维护机制,减少救火工作”、“快速自动根因定位”、“全面理解应用、找到根因、建议修复、生成事件后分析”、“AI SRE将从监督工作转向非监督工作,最终自主完成大部分工作”等说法。

这很有趣,对吧?正如记者们所说,这其中有“部分真实”,或者像我们说的,是“大话”。当我和Lex讨论这些时,我开始产生一些回忆。我意识到,在Honeycomb的前三四年,我一直打开着John Allspaw的一篇名为《致监控和告警公司的公开信》的博文标签页。其中写道:“这永远是一个未解决的问题。不要告诉我你的软件将如何为我解决它。告诉我你正在努力打造一个产品,它将作为出色的团队成员加入我的团队。告诉我你正在构建的东西将如何让我的人变得更好。”这显然对我们Honeycomb正在构建的一切产生了巨大启发。但今天,十有八九,它依然能引起共鸣。我真的很想看到一封致所有AI SRE和AIOps公司的公开信,因为它太有共鸣了。

这并不是说十全十美,但十有八九,当你听到人们感到焦虑和不安时,对我来说,一个非常有用的测试方法是将“AI”替换为“自动化”。因为很多东西实际上并不新鲜,它只是自织布机发明以来我们一直在应对的问题的加速版本。

有趣的是,进行这种替换后,我们在实践中部署自动化方面拥有丰富的经验。因此,有一系列问题我喜欢问自己,我认为每次面对新的自动化任务时,我们都应该问自己这些问题,这套问题对于AI智能体(尤其是那些号称“根因即服务”的)同样非常有效。

关键提问框架 🔍

上一节我们提到了用“自动化”的视角来审视AI炒作。本节中,我们来看看一套具体的问题框架,帮助你在评估AI工具时保持清醒。

以下是评估AI工具时需要问的几个核心问题:

  1. 你是在监督,还是在被增强?
    我们都见过类似特斯拉自动驾驶汽车的模式,你需要把手放在方向盘上,否则它就会停止工作。你的工作是确保汽车不犯错,但人们不喜欢这样。网上有视频显示,人们用橙子或水瓶卡住方向盘,然后就可以在汽车自动驾驶时看电影。我们知道这种监督模式效果不佳。无论是特斯拉自动驾驶、飞机,还是80年代Bainbridge发表的“自动化的讽刺”,甚至当你只是看着代码说“是、否、是、否”时,你都是监督者,被排除在主动工作之外。这一切都回到了所谓的“Fitts列表”概念,即人类擅长一件事,机器擅长另一件事,所以让机器做它擅长的,让人做他擅长的,但这通常让你处于监督的位置,拥有机器所不具备的判断力。这是错误的。我们寻找的应该更像一个团队合作的联合系统。机器应该赋予人类更多能力,而人类则引导机器走向正确的方向,但我们都被困在同一个系统中。一个带有文字的笔记本也是这个系统的一部分。这是我们做决策、沟通和工作的方式,人们需要参与其中以保持对现状的了解。

  2. 你在循环中的哪个位置?
    你在AI前面还是后面,结果会不同。如果你在AI前面,你会得到与在AI后面监督不同的结果。如果一个三人团队做决策,他们的工作方式会与单人不同。如果你在团队中加入一个AI,情况又会不同。当有人告诉你“有人参与循环”时,问问这个“人”在循环的哪个位置。否则,你可能只是决策的替罪羊,而不是真正与AI组成团队的一部分。

  3. 它基于哪些视角?
    每个智能体都有一个有限的认知范围。David Woods讨论过这一点。存在局限性。如果你的AI只接入系统的可观测性数据和组件数据,它永远不会给出诸如“如何重组你的团队结构”这样的建议。它不了解你的团队。因此,这些是固有的限制。同样,它在操作上也会受到限制,就像你可以限制系统中用户的权限一样。它将不了解你的社会技术系统中的主要部分,它会忽略其中的许多部分。这带来的风险之一是,如果你拥有的东西被设计成看起来和行为都像一个智能体(一个你可以与之交谈并完成所有事情的人),但它只有有限工具的能力,那么你就拥有了错误的接口。这就是为什么这里的图片是用PlayStation 5手柄骑自行车。我们经常被推销的就是这种东西:拥有工具的能力,却披着与人交谈的接口外衣,这极具误导性,并包含固有风险。

  4. 它是让你越用越好,还是越用越差?
    另一个问题是,它兑现承诺的能力如何?没有什么东西能完全兑现承诺。这就是营销和工程是两个非常不同的部门的原因。即使你需要它完美,它也不会完美。事件响应中一个常见的比喻是“英雄”,即那些习惯于进行紧急救援的人。他们对系统了如指掌,最终成为所有紧急情况中非常关键的单点瓶颈。他们不能去度假,不能结婚度蜜月。问问我在夏威夷海滩上凌晨3点接到CTO电话是什么感觉,就因为MongoDB起不来。这对人来说是一个巨大的反模式,对AI智能体来说也将同样严重。如果你的智能体正在处理某些类型的问题并处理所有问题,它们是否会变成“英雄”?如果它们崩溃或行为不当,而你的员工不再知道如何调试,你该怎么办?

  5. 谁最终负责?
    当你使用某种技术解决方案时,存在增强人还是增强机器的角度。当你增强机器时,通常发生的情况是,基本常见操作以更快的速度完成,你可以用相同的努力做更多事情。而当你增强人时,通常是你将最具挑战性的任务变得容易得多,这会产生不同的影响。因为如果你总是只增强机器,而它们开始崩溃,那么你将永远跟不上节奏。如果你增强的是人,你就有更广泛的事情可以做、可以处理。

  6. 它允许你更好地探索吗?
    我喜欢在拿到关于运维的工具时问:它是否让我能更好地探索?我能否看到所有丰富的信息,只是更轻松地浏览它们,获得完整的视角和上下文?或者它也许告诉我应该看哪里?它显示所有数据,但有一个小亮点引导我的视线,我仍然能看到周围的情况,但它将我的注意力引向某个更受限制的地方。它过滤掉你不想看到的东西,只显示“相关信息”。现在,如果我想质疑这些信息,就需要我自己费力地去别处查看,因为它没有显示出来。在某些情况下,它会更加受限。它会告诉你“发生了某事,你也许应该执行这个操作:重启、安装升级、更改配置”。现在,我甚至更多地脱离了上下文,我所能判断的只是自动化为我选择的这条狭窄路径。在某些情况下,你会得到最糟糕的部分:它直接告诉你“做这件事”。没有其他选择。我在这里只是自动化执行的手。

  7. 谁在做适应?
    这样做的问题是,它引导你走上一条非常具体的路径。但“谁在做适应”决定了谁对结果负责。当出现问题时,谁从事件中学到了东西?谁进行修复?AI做错了事,谁去善后修复?是AI自己修复自己吗?谁设置新的防护栏?甚至谁决定现在发生了事件、发生了错误?谁能改变“什么是事件”的定义,认为“这是新常态,可以接受”?当你的信息发生变化时,谁来做适应?有趣的是,如果你把这两列放在一起看,在左边,你可能被置于一条没有自主权和所有权的路径上,你被给予一系列后果,然后被告知要对其负责,但你却没有机会做正确的事情。因此,当你拿到自动化工具时,必须认真思考这一点。


总的来说,如果我要给供应商写一个“太长不看”的总结,那就是这三个关键词:加入扩展扎根

  • 加入:我希望你打造一个团队协作者。这是你在广告中宣传的。它需要能够像一个团队成员那样行动。如果它不能像一个团队成员那样行动,那就做正确的事,只成为你实际能够成为的工具。不要在你只有工具能力时,却伪装成一个人的样子。在我看来,这就是为什么我们看到很多人像交易“魔法咒语”一样交易提示词。人们试图迫使AI做他们想要的特定事情,因为它们只有工具的能力,而我们正试图让它们足够可预测,以便在非常特定的上下文中使用它们。这将是挑战的一部分。非确定性是奇妙的,但我们仍在努力控制它,因为一个你可以预测的工具并不是一个好工具,它只是一个可以……制造混乱的东西。

  • 扩展:赋予现有操作者更多能力,集成到工作流中,给予他们新的能力或改进他们。

  • 扎根:再次强调,这些模式是已知的。对它们的研究有很长的历史,即使AI有很多新的做事方式,它们从根本上说往往是相似的模式。因此,你需要能够表明你已经探索并思考过这些问题,而不是在我的组织里拿我做实验,让我来承担你“瞎搞”的后果。

是的,所以Fred和我开始准备这个演讲,后来他们把我们升级成了闭幕主题演讲,这对我们来说非常好。但我们开始思考……


实际上,供应商可能根本不在乎我们为他们整理的这些好建议。或者说,我们并不指望他们会在乎。但我们指望你们在乎。我认为,这个房间里的人有一种道德权威,这种权威来自于愿意在事情变糟时承担责任。当演讲中提到“负责的人是做适应的人”时,这句话击中了我。当你愿意做困难的事情、冲向火场、在半夜被叫醒时,你就积累了这种道德权威。这并不是说我们都崇尚“英雄文化”,但我认为,做困难事情的人有权要求组织以某种方式改变。

长期以来,我们一直是风险的守护者。我思考着这个角色在过去的变化,从我刚入行时开始——我可能是最后一代运维人员之一,会在半夜接到电话,叫出租车去机房,在MySQL主库宕机时手动打开电源开关,因为我们没有远程操作。我当时想,好吧,这就是我余生要做的事了。惊喜!我认为我们的角色归根结底是:我们是复杂软件系统的风险守护者和引导者。

而AI就是一堆非常复杂的软件系统,我们比以往任何时候都更需要。有前所未有的风险需要评估和防范。

但要真正迎接这个时刻,我认为我们也必须进化。谈论风险管理时,不能不承认对于大多数公司来说,它们面临的最大、持续存在的生存风险不是创新不足,就是速度不够快。你可能会耗尽可靠性的预算,但也可能会耗尽银行账户里的钱。是的,压力不仅仅是技术上的,它们也是财务上的、领导力上的、组织上的。妥善处理风险就是能够处理这些权衡,而不是固执地坚持某一个立场。很容易变成那种“说不的部门”。我想如果我们自己没有这样做过,也许在我们工作的地方也感受过安全团队带来的这种感觉,而这通常是我们不想成为的模式,也是安全团队自己不想成为的模式。我真的觉得,这是运维部门像濒危物种一样的主要原因,因为它们被视为“说不的部门”,被视为成本中心。而当你拥有技术团队时,我们所有人都在创造价值。如果你纯粹是一个成本中心,那可不是一个好位置。

这是一个巨变的时代,而有变化的地方就有机会。事情变化得太快了,伙计们。两年前,生成式AI刚刚出现,当时我们很多人很容易把它斥为“花哨的自动补全”,因为它当时就是那样。现在它已经不是了。说实话,任何对未来一两年以上做出自信预测的人都是在胡说八道。他们越自信,你就越不应该听他们的。

我真的理解大家对这种淘金热感到疲惫,我也是。AI似乎正在入侵每一个领域,没有什么能免疫。我参加过Honeycomb的销售电话,当我们提到AI时,人们的眼神就变得呆滞,好像在说:“哦,你们也来这套。”我对这些话题已经有点麻木了,就像每个人都觉得必须在一些废话上贴上AI标签一样。我告诉Fred,现在每当我看到“AI”和“下一个功能”放在一起,它只是告诉我这是“阿尔法质量”的,因为一旦某样东西变得可靠、好用,他们就不再叫它AI了。没人会说“Gmail AI垃圾邮件过滤器”,它们已经AI了20年,但……是的,对我来说,这与一个观点相符:如果你的AI尊重其限制并且好用,它就变成了一个工具。垃圾邮件过滤就是一个工具。它是AI技术,但受到了很好的约束。它只做这一件事。它有了一个名字,就不再是“AI”了,因为我们知道如何使用它,并且它被很好地工具化了。

是的,有一些有趣的事情,比如非确定性进入一个基于确定性的软件工程学科,这非常有趣。API是什么,如果你甚至不能依赖答案?这非常有趣。

我没有答案。我所知道的是,未来正在被创造,而未来需要像我们这样的人:批评者、反对者、总是思考事情会如何崩溃的人。但为了参与对话,我们需要欣赏其中的机会,我们需要克服我们有时总是看它如何失败的倾向,并超越这一点。即使你不相信,即使你不想让它成真,但有钱人认为这是真的。

我现在处于一个非常奇怪的位置,介于工程师、经理、高管、董事会成员和风险投资家之间,而他们看待世界的方式与我们截然不同。风险投资家现在认为,世界分为“前AI公司”和“后LLM公司”。下次我们去融资时,我们将与那些在LLM之后成立、只用五个人就能做同样多事情的初创公司进行效率比较。即使我们认为这不是真的,人们也会戴着怀疑的帽子看着我们。

说实话,如果要从我的职业生涯中吸取一个教训,那就是:每当你被要求做一件你觉得没有意义的事情时,提出这个要求的人很可能正面临着一系列你不知道或不了解的压力和期望。这也许是一个机会,去同理那种“我们不知道压力是什么”的处境。这就是我们在事件复盘中学到的,当发生奇怪且完全愚蠢的事情时,你被告知要“关注责任归属”,并思考“在当时什么让它显得合理”。所以,如果你被要求以一种没有意义的方式应用AI,有可能你的高管、你的上级正在应对类似的压力,然后你可以成为一个盟友,以一种不那么愚蠢的方式来做这件事,因为这有时就是一个愚蠢的要求,你只需要以恰当的方式表达出来。

这在以前也是如此。我曾在一家公司工作,工程总监过来说:“我们有六个月时间从AWS迁移到GCP。”基础设施团队的反应是:“这太他妈蠢了。我们不干。”然后大约四个月,他们都在消极抵抗:“我们不干。就是不干。你可以开除整个团队。你卡着没法迁移。谁在乎呢。”到了第四个月,总监终于过来说:“我们有资金,但前提是迁移。”然后基础设施团队说:“他妈的终于来了。”六周就完成了。

这引出了一个推论:拜托,伙计们。我也有一个惊人相似的故事,大约十年前在Parse。在Parse,很多时候我觉得他们就是让我做一些没有意义的事情。所以我大部分时间都在和我的老板斗争。如果他当时告诉我:“我们真的很害怕如果Parse找不到出路,Facebook会关掉它。”我可能会说:“我可以帮忙。”而不是一路和他斗争到底。我知道透明度可能很难,但我真的认为这是值得的。就像Fred说的,我们一小时前还在疯狂排练,他说:“SRE是一个高语境角色,我们在语境中茁壮成长,我们可以被信任。”有太多敏感数据和敏感的事情了。所以,让我们展现出来,展现我们可以被信任,但然后我们也期待一些他妈的透明度。如果人们不能信任你,告诉你真实的情况,那么他们可能不配得到你的劳动。

我们擅长的一件事是学习。学习需要学习的东西,弄清楚它是什么。我最近读了Annieella的一篇文章,真的推荐大家看看,叫做《软件工程师的身份危机》。它真的说出了我们许多人的情感反应,我们这些将职业生涯和大部分生活建立在成为建设者、工匠、运维人员之上的人。你知道,成为一名监督者的感觉并不那么好。我一直很喜欢这篇文章,直到最后,最后她说:“工程师-经理的钟摆为我们展示了一条可能的道路。”然后我想,好吧,我现在更喜欢它了。

如果技术史告诉我们什么,那就是我们花费一生建立和掌握的技能,如果扩展到拥抱新事物,仍然具有相关性。作为SRE,要成为SRE,就要做最坏的打算。这是他们的巨大优势,也可能成为我们的失败。你知道,我经常开一个玩笑,说运维工程师通常不创办公司,因为你需要能够暂时搁置怀疑,去设想一个比现状好得多的东西,而我恰恰没有这种能力。这就像宇宙的意外,而Christine(注:可能指联合创始人)一直说:“嗯,也许这能行。”而我则说:“这里有10种它行不通的方式。”

总的来说,这对于整个AI热潮是一个巨大的平衡:一些人追逐巨大的回报,而我们看到巨大的风险。我们将始终关注风险,因为这就是我们的工作;而高管或创始人将关注回报,因为这就是他们的工作。在某个时刻,如果你想恰当地在两者之间进行沟通,你必须理解他们,以便能够站在他们的立场上,这样才有希望他们也站在你的立场上。当然,这可能在任一方失败,但至少如果我们不从一开始就说“我退出,我绝不碰这个”,我们就给了自己一个机会。需要做一些努力去理解他们的情况,以便能够进行对话,从而对它在你的业务中如何实施有一定的控制和引导能力。

是的。这就是为什么我想……今天的世界有很多恐惧和焦虑,理由很充分。但我想挑战在座的每一位,稍微思考一下我们想在这个行业的未来中扮演什么角色。

不要只是读一堆关于风险和复杂认知因素的白皮书,然后坐回去假设它永远不会成功。不要只是以批评开头,然后就此打住。至少试着理解为什么有些人如此兴奋。看在上帝的份上,不要让你自己的身份与“AI失败”捆绑在一起。我真的看到有人这样做。如果你发现自己希望它失败,因为AI成功的想法让你充满恐惧和焦虑,我挑战你把它变成好奇心。

是的,当我看到这张幻灯片时,我只是看到它被投票否决了,我想:“去他妈的,拜托。”我不喜欢这个。这很蠢,我不喜欢。我知道这会走向何方,结局不会好。但是,公司里有一群工程师正在尝试它,并且有点相信它。再一次,如果我直接告诉他们“你们在做蠢事,你们是不称职的工程师,他们不相信这个,这只会以眼泪告终”,这就像在事件中一样,思考“为什么这对他们来说有意义?”如果我想有任何机会让他们正确使用这个工具,我必须能够理解他们来自哪里,他们面临什么压力。对于初级工程师来说,情况很严峻,他们只是觉得“我必须比以前更快地写代码,否则我会失去我的房子”。是的,我仍然认为他们那样写代码有点蠢。我不喜欢。我不会那样做。这不是我的工作方式。但我能理解他们来自哪里,因为我在职业生涯中处于不同的位置,拥有不同级别的权威和不同的支持。我也想尝试一下,看看它会如何搞砸,并和他们一起走过这个过程。在某个时刻,就是这样,这就是人们的工作方式。你必须理解他们如何工作,才能为他们设计出合适的东西。你必须要有那种给予和接受。这种对话对于获得好的解决方案是必要的。如果你只是把操作员或新手当作……他们不会得到好的结果。如果你认为工程师更……他们也不会得到好的结果。所以,是的,我喜欢那张倒过来的脸。它安慰了我。但这不是……必须尝试一下,也许只是为了更透彻地讨厌它。我会写博客的。

最后,你对AI及其对艺术家、环境的影响有道德或伦理上的反对或疑虑吗?我也有。我认为,正因为如此,更要成为专家,或者至少近距离接触这些东西。你无法通过退出集体行动问题来帮助解决它们。之前有一句很棒的话,我不记得是谁说的了,但就像“你不能通过跳到火车前面来阻止它”。如果你想让你的批评被听到,它需要是有根据的批评。社会上充满了从外部扔石头的人。我们对他们免疫,我们可以耸耸肩,把他们当作“喷子”。来自内部的批评才能切中要害。

所以,结论是:SRE们做最坏的打算,但仍然努力争取最好的结果。这是悲观主义和实用主义的结合。


有什么问题吗?


本节课中,我们一起学习了如何以SRE的视角批判性地审视AIOps产品。我们回顾了营销承诺与现实之间的差距,并掌握了一套关键的提问框架,用于评估AI工具是增强团队还是削弱团队。核心在于理解工具在“人机协作循环”中的位置、其固有的局限性以及最终的责任归属。记住,我们的角色是复杂系统风险的守护者,在AI时代,这一角色比以往任何时候都更重要。面对变化,我们既要保持对风险的清醒认知,也要保持开放和学习的心态,以便能够引导技术向增强人类能力、创造真正价值的方向发展。

posted @ 2026-03-29 09:25  布客飞龙II  阅读(18)  评论(0)    收藏  举报