可观测性的三大支柱

Posted by Laurence on Wednesday, May 14, 2025

Distributed-Systems-Observability

本文翻译自上述Oreilly出版的电子书《Distributed systems Observability》(By Cindy Sridharan)第四章。

日志、指标和追踪通常被称为可观测性的三大支柱。虽然单纯拥有这些工具并不能直接提升系统的可观测性,但若能深入理解其原理,它们将成为构建更优系统的利器。

事件日志

事件日志是对离散事件不可变的时间戳记录。其形式虽有三类,但本质相同:时间戳+上下文负载。 一共有三种形式:

  • 纯文本:自由格式的文本记录,是最常见的日志形式。
  • 结构化日志:近年来备受推崇,通常以JSON格式输出。
  • 二进制日志:如Protobuf格式日志、MySQL用于复制和时点恢复的binlog、systemd日志,或BSD防火墙pf生成的pflog格式(常作为tcpdump的前端)。

调试系统中罕见或偶发的异常状况,往往需要在极细粒度层面进行分析。事件日志的独特价值在于,它能够为那些被平均值和百分位数掩盖的长尾问题提供富含上下文的深度洞察。因此,事件日志特别适用于揭示分布式系统中组件表现出的突发性、不可预测行为。

复杂分布式系统的故障,很少源于单一组件内的特定事件,而通常涉及高度互联的组件网络中多个潜在触发因素。若仅观察某个时间点离散发生的孤立事件,根本无法确定所有这些诱因。要准确锁定不同触发机制,需要具备以下能力:

  • 从某个系统的高层指标或日志事件表征的症状入手
  • 推演出请求在分布式架构各组件间的完整生命周期
  • 持续追问系统各部分之间的交互细节

除了推断短期存续的单个请求命运外,还需能够推断系统在更长时间维度(相比单个请求生命周期长数个数量级)的整体行为轨迹。追踪(trace)与指标(metric)作为日志之上的抽象层,沿着两个正交维度对信息进行预处理和编码:一个以请求为核心(追踪),另一个以系统为核心(指标)。

日志的优缺点

日志无疑是所有观测数据中最易生成的。日志本质上只是字符串、JSON 数据块或类型化键值对,这种特性使其能够轻松承载任意形式的数据记录。绝大多数编程语言、应用框架和类库都内置了日志支持功能,添加日志语句就像插入 print 语句那样简单直接。在呈现富含本地上下文的高粒度信息方面,日志表现尤为出色——只要查询范围限定在单个服务内的事件追踪。

然而,日志的实用性也止步于此。虽然日志生成看似简单,但各类主流日志库的性能表现却参差不齐。高性能日志库通常能做到极少甚至零内存分配,执行效率极高;但许多语言和框架的默认日志库并非最优实现,这可能导致应用整体性能因日志开销而受损。更关键的是,除非使用RELP等可靠消息传输协议,否则日志消息存在丢失风险——当涉及计费或支付等关键业务场景时,这种可靠性缺陷将带来严重后果。

RELP并非银弹解决方案

RELP是一种使用命令-响应模型的协议(命令和响应称为RELP事务)。RELP客户端发出命令,RELP服务器对这些命令进行响应。

RELP服务器设计用于限制未完成命令的数量,以节省资源。选择使用RELP意味着决定在服务器无法足够快地处理命令时施加背压并阻塞生产者。

虽然这种严格的要求可能适用于每条日志都至关重要或出于审计目的法律要求的情况,但监控和调试很少需要如此严格的保证及其带来的复杂性。

最后,除非日志库支持动态采样,否则过度日志记录可能会对应用程序整体性能产生不利影响。当日志记录不是异步的,并且在将日志行写入磁盘或标准输出时阻塞了请求处理,这种问题会进一步加剧。

采样还是不采样?

为解决日志记录的成本开销,通常建议采用智能采样。采样是从生成的事件日志总量中挑选一小部分进行处理和存储的技术。这部分子集被期望是系统生成事件全集的缩影。

采样并非没有问题。首先,采样数据集的有效性取决于基于哪些键或特征进行采样决策。此外,对于大多数在线服务,需要确定如何动态采样,以便根据传入流量的形状自适应调整采样率。许多对延迟敏感的系统对可用于发出可观测性数据的CPU时间有严格限制。在这种场景下,采样可能被证明是计算成本高昂的。

谈及采样,不能不提能够存储整个数据集摘要的概率数据结构。深入讨论这些技术超出了本文范围,但对感兴趣的读者,O’Reilly提供了很好的资源。

在处理方面,原始日志几乎总是通过Logstash、fluentd、Scribe或Heka等工具进行规范化、过滤和处理,然后存储到Elasticsearch或BigQuery等数据存储中。如果应用程序生成大量日志,则可能需要在Logstash处理之前通过Kafka等消息代理进行进一步缓冲。像BigQuery这样的托管解决方案有配额限制,需注意不能超过。

在存储方面,尽管Elasticsearch是一个出色的搜索引擎,但运行它需要真实的运营成本。即使组织配备了精通Elasticsearch运维的工程师团队,也可能存在其他缺点。例如,在Kibana的图表中经常可以看到流量下降的陡峭曲线,这并不是因为服务流量下降,而是因为Elasticsearch无法跟上数据索引的庞大数据量。即使日志摄取处理对Elasticsearch不是问题,也很少有人能完全掌握Kibana的用户界面,更不用说享受使用它了。

日志处理作为流处理问题

事件数据不仅用于应用程序性能和调试场景,它还是所有分析数据的来源。这些数据从商业智能的角度来看具有巨大价值,企业通常愿意为技术和人员投入成本,以理解这些数据,从而做出更好的产品决策。

有趣的是,企业在商业上希望回答的问题与软件工程师和站点可靠性工程师(SRE)在调试时希望回答的问题有着惊人的相似性。例如,一个对业务重要的查询可能是:

过滤出用户查看某篇文章总次数少于100次的异常国家。

而从调试角度看,查询可能更像是:

  • 过滤出执行超过100次数据库查询的异常页面加载。
  • 仅显示来自法国的页面加载时间超过10秒的记录。

这些查询都依赖于事件。事件是结构化(可选类型化)的键值对。将业务信息与请求生命周期相关的信息(计时器、持续时间等)结合,使得分析工具可以被重新用于可观测性目的。

日志处理非常适合在线分析处理(OLAP)的范畴。从OLAP系统中获得的信息与用于调试、性能分析或系统边缘异常检测的信息并无太大差异。解决Elasticsearch或基于索引的存储系统中摄取延迟问题的一种方法是将日志处理视为流处理问题,通过最小化索引来处理大量数据。

大多数分析管道使用Kafka作为事件总线。将丰富的事件数据发送到Kafka,可以通过KSQL(Kafka的流式SQL引擎)实现对流的实时搜索。

通过为Kafka中的业务事件添加用于可观测性场景的额外计时和其他元数据,可以在重用现有流处理基础设施时获得帮助。这种模式的另一个好处是,数据可以定期从Kafka日志中过期。大多数用于调试的事件数据在生成后仅在较短时间内有价值,而不像业务相关的消息那样需要通过ETL作业进行评估和持久化。当然,这只有在Kafka已是组织核心基础设施的情况下才有意义。仅为了实时日志分析而在技术栈中引入Kafka,尤其是在非JVM环境中或缺乏显著JVM运维专长的团队中,有些过于复杂。

另一种选择是Humio,这是一个托管和本地部署的解决方案,将日志处理视为流处理问题。日志数据可以直接从每台机器流式传输到Humio,无需预聚合。Humio使用复杂的压缩算法高效压缩和检索日志数据。Humio不依赖预先索引,而是允许对事件流数据进行实时复杂查询。由于Humio支持文本日志(绝大多数开发者习惯使用grep的格式),基于读取的临时模式允许用户迭代和交互式查询日志数据。另一个替代方案是Honeycomb,这是一个基于Facebook Scuba的托管解决方案,采取只接受结构化事件的观点,但支持读取时聚合和对数百万事件进行极快的实时查询。

指标

指标是对时间间隔内测量的数字数据的表示。指标可以利用数学建模和预测的力量,推导出系统在当前和未来时间间隔内的行为知识。

由于数字数据针对存储、处理、压缩和检索进行了优化,指标支持更长时间的数据保留和更简单的查询。这使得指标非常适合构建反映历史趋势的仪表板。指标还允许逐渐降低数据分辨率。在一定时间后,数据可以聚合成每日或每周的频率。

现代指标的结构

历史时间序列数据库的一个主要缺点是指标的标识方式不利于探索性分析或过滤。

传统层次化的指标模型,以及早期Graphite版本中缺乏标签或标注,尤其带来了不便。现代监控系统(如Prometheus和较新版本的Graphite)通过指标名称以及称为标签的键值对来表示每个时间序列,从而支持高维度的模型。

在Prometheus中,如图4-1所示,指标通过指标名称和标签共同标识。存储在时间序列中的实际数据称为样本,包含两个部分:一个float64值和一个毫秒精度的的时间戳。 prometheus_metrics_sample

图4-1. Prometheus指标样本

需要注意的是,Prometheus中的指标是不可变的。更改指标名称或添加/删除标签将导致生成新的时间序列。

指标相较于事件日志的优势

大优势在于,指标的传输和存储具有恒定的开销。与日志不同,指标的成本不会随着用户流量或其他可能导致数据激增的系统活动而同步增加。

通过指标,应用程序流量的增加不会显著增加磁盘使用率、处理复杂性、可视化速度和运营成本,而日志则会。指标存储会随着标签值的排列组合增加(例如,当启动更多主机或容器,或添加新服务,或现有服务增加插桩时),但客户端聚合可以确保指标流量不会随用户流量的增加而按比例增长。

注意: Prometheus等系统的客户端库在进程内聚合同一时间序列样本,并在成功抓取时提交到Prometheus服务器(默认每隔几秒抓取一次,可配置)。这与statsd客户端不同,statsd客户端在每次记录指标时都会向statsd守护进程发送一个UDP数据包(导致提交的指标数量与报告的流量成正比增加)。

指标一旦收集,便更易于进行数学、概率和统计变换,如采样、聚合、汇总和相关性分析。这些特性使指标更适合报告系统的整体健康状态。

指标也更适合触发警报,因为对内存中的时间序列数据库运行查询远比对分布式系统(如Elasticsearch)运行查询然后聚合结果以决定是否触发警报更高效、更可靠。当然,仅对内存中的结构化事件数据进行警报查询的系统可能比Elasticsearch略便宜,但运行大型、集群化的内存数据库的运营开销,即使是开源的,对大多数组织来说也不值得,尤其是当有更简单的方法可以获得同样可操作的警报时。指标最适合提供此类信息。

指标的缺点

应用程序日志和指标的最大缺点是它们受限于系统范围,难以理解特定系统之外的情况。当然,指标也可以是请求范围的,但这会导致标签的扩展,从而增加指标存储成本。

对于没有复杂联接的日志,单行日志无法提供请求在系统所有组件中的完整信息。虽然可以构建一个跨地址空间或RPC边界的日志和指标关联系统,但这样的系统需要指标携带唯一标识符(UID)作为标签。

将高基数的值(如UID)用作指标标签可能会使时间序列数据库不堪重负。尽管新的Prometheus存储引擎已优化以处理时间序列的频繁变更,但较长时间范围的查询仍会较慢。Prometheus只是一个例子,所有流行的现有时间序列数据库解决方案在高基数标签下都会性能下降。

在最佳使用场景下,日志和指标为我们提供了单一系统内的全知视角,但仅此而已。虽然这些对于理解单个系统(有状态或无状态)的性能和行为可能足够,但它们不足以理解跨越多个系统的请求生命周期。

分布式追踪是一种解决跨多个系统请求生命周期可视化问题的技术。

分布式追踪

追踪是对一系列因果相关的分布式事件的表示,编码了请求在分布式系统中从开始到结束的完整流程。

追踪是日志的一种表示形式,其数据结构几乎与事件日志相同。单个追踪可以提供请求遍历路径以及请求结构的可见性。请求路径使软件工程师和SRE能够理解请求路径中涉及的不同服务,而请求结构有助于理解请求执行中异步操作的节点和影响。

尽管关于追踪的讨论往往围绕其在微服务环境中的实用性,但可以合理认为,任何足够复杂的应用程序,只要以非平凡方式与网络、磁盘或互斥锁等资源交互或竞争,都可以从追踪提供的优势中受益。

追踪的基本思想很简单:在应用程序、代理、框架、库、运行时、中间件以及请求路径中的任何其他组件中,识别特定点(函数调用、RPC边界或并发段,如线程、协程或队列),这些点表示:

  • 执行流的叉点(操作系统线程或绿色线程);
  • 跨网络或进程边界的跳转或分支。

追踪用于识别每一层完成的工作量,同时通过"先于"语义保留因果关系。图4-2展示了一个请求在分布式系统中的流程。图4-3展示了该请求流程的追踪表示。追踪是由跨度(span)组成的定向无环图(DAG),跨度之间的边称为引用。

reqeust_flow_diagram

图4-2. 示例请求流程图

request_lifecycle_graph

图4-3. 请求生命周期中接触的分布式系统各组件,表示为定向无环图

当请求开始时,会分配一个全局唯一ID,并在请求路径中传播,以便每个插桩点能够插入或丰富元数据,然后将ID传递到请求流中的下一个跳转点。流程中的每个跳转点表示为一个跨度(图4-4)。当执行流到达这些服务的插桩点时,会生成一条记录及其元数据。这些记录通常异步写入磁盘,然后以带外方式提交给收集器,收集器随后可以根据系统中不同部分发出的不同记录重建执行流。

trace_span

图4-4. 以跨度表示的追踪:跨度A是根跨度,跨度B是跨度A的子跨度

收集这些信息并在保留因果关系的情况下重建执行流,以便进行回顾性分析和故障排查,可以更好地理解请求的生命周期。

最重要的是,理解整个请求生命周期使得调试跨多个服务的请求成为可能,以精确定位延迟增加或资源利用率升高的来源。例如,图4-4表明服务C与服务D之间的交互耗时最长。因此,追踪在很大程度上帮助理解"哪个"组件(例如,请求生命周期中哪些系统组件被触及并导致响应变慢?),有时甚至能揭示"为什么"。

分布式追踪的用例多种多样。虽然主要用于服务间依赖分析、分布式性能分析和调试稳定状态问题,但追踪还可以帮助进行费用分摊和容量规划。

ZipkinJaeger是两种最受欢迎的符合OpenTracing规范的开源分布式追踪解决方案。(OpenTracing是一个中立的分布式追踪API规范和插桩库。)

分布式追踪面临的挑战

追踪无疑是现有基础设施中最难以改造的技术,因为要使追踪真正有效,请求路径中的每个组件都需要修改以传播追踪信息。

根据不同人的观点,有人认为请求流中的间隙并不会让追踪的缺点超过优点(因为零星添加追踪被认为比完全没有追踪好,部分追踪有助于从复杂环境中挖掘出知识点滴);也有人认为这些间隙是盲点,会使调试更加困难。

追踪插桩的第二个问题是,仅仅开发者对其代码进行插桩是不够的。许多实际应用的开源框架或库可能需要额外的插桩。在采用多语言架构的场景中,这变得更具挑战性,因为每种语言、框架和具有不同并发模式及保证的线协议都需要协作。实际上,追踪在那些跨公司统一使用核心语言和框架的组织中部署最为成功。

追踪的成本不像日志记录那样灾难性,主要是因为追踪几乎总是通过大量采样来减少运行时开销和存储成本。采样决策可以在以下阶段进行:

  • 在请求开始之前,生成任何追踪之前;
  • 在请求执行全程结束后,所有参与系统记录了追踪;
  • 在请求流的中途,仅下游服务报告追踪。

所有方法都有各自的优缺点,甚至可能需要同时使用。

服务网格:未来的新希望?

虽然追踪的实现一直较为困难,但服务网格的兴起使得集成追踪功能几乎变得毫不费力。服务网格的数据平面在代理层实现追踪和统计收集,这允许将单个服务视为黑盒,同时仍能获得对整个网格的统一且彻底的可观测性。作为网格一部分的应用程序仍需将头部信息转发到网格中的下一个跳转点,但无需额外的插桩。

Lyft通过采用服务网格模式,成功为其所有服务添加了追踪支持,应用程序层唯一需要的更改是转发某些头部信息。这种模式对于以最少代码更改将追踪改造到现有基础设施中非常有用。

小结

日志、指标和追踪各有其独特用途,且互为补充。它们共同为分布式系统的行为提供了最大程度的可见性。例如,以下做法是合理的:

  • 在请求的每个主要入口和出口点设置计数器和日志;
  • 在请求的每个决策点设置日志和追踪。

此外,确保所有三者语义上相关联也是合理的,以便在调试时能够:

  • 通过读取追踪重建代码路径;
  • 从代码路径中的任何单点推导请求或错误比率。

通过采样追踪或事件的示例并与指标关联,可以实现点击指标查看追踪示例,并检查请求在各个系统中的流程。从不同可观测性信号组合中获得的洞察力,成为调试分布式系统的必备条件。

终论

正如我在DigitalOcean负责可观测性团队的朋友布莱恩·诺克斯(Brian Knox)所说:

可观测性团队的目标不是收集日志、指标或追踪,而是建立一种基于事实和反馈的工程文化,并将这种文化推广到整个组织中。

同样,可观测性本身也不仅仅是关于日志、指标或追踪,而是在调试过程中以数据为驱动,并利用反馈来迭代和改进产品。

系统可观测性的价值主要源于其带来的业务和组织价值。快速调试和诊断生产问题不仅能为最终用户提供出色的体验,还为服务的可持续和人性化的运营铺平了道路,包括值班体验。只有当构建系统的工程师优先考虑将可靠性设计到系统中时,可持续的值班才有可能。可靠性并非在值班期间产生。

对许多(如果不是大多数)企业来说,拥有一个良好的警报策略和基于时间序列的"监控"可能就足以实现这些目标。而对其他企业来说,能够调试"针尖找针"类型的问题可能是创造最大价值的关键。

因此,可观测性并非绝对的。根据你的服务需求,选择适合你的可观测性目标。

(全部文章完)