Contents

分布式系统的可观察性

1. 简介

在本教程中,我们将讨论可观察性及其在分布式系统中发挥重要作用的原因。我们将介绍构成可观察性的数据类型。这将帮助我们了解从分布式系统收集、存储和分析遥测数据的挑战。

最后,我们将介绍可观察性领域的一些行业标准和流行工具。

2. 什么是可观察性?

让我们切入正题,从正式定义开始!可观察性是仅通过系统的外部输出来衡量系统内部状态的能力

对于像微服务这样的分布式系统 ,这些外部输出基本上被称为遥测数据。它包括机器的资源消耗、机器上运行的应用程序生成的日志等信息。

2.1. 观察数据的类型

我们可以将观察数据分为三类,我们称之为可**观察性的三大支柱:**日志、指标和跟踪。让我们更详细地了解它们。

日志是应用程序在执行代码期间在离散点生成的文本行。通常这些是结构化的,并且通常以不同的严重程度生成。这些很容易生成,但通常会带来性能成本。此外,我们可能需要 Logstash 等其他工具来高效地收集、存储和分析日志。

简而言之,指标是表示为我们在一段时间内**计算或聚合的计数或度量的值。**这些值表示有关虚拟机等系统的一些数据——例如,虚拟机每秒的内存消耗。这些可以来自各种来源,如主机、应用程序和云平台。

跟踪对于单个请求可以流经多个应用程序的分布式系统很重要。当请求流经分布式系统时,跟踪是分布式事件的表示。这些对于定位瓶颈、缺陷或分布式系统中的其他问题等问题非常有帮助。

2.2. 可观察性的好处

首先,我们需要理解为什么我们需要系统中的可观察性。我们大多数人可能都面临着对生产系统上难以理解的行为进行故障排除的挑战。不难理解,我们破坏生产环境的选择是有限的。这几乎让我们去分析系统生成的数据。

可观察性对于调查系统开始偏离其预期状态的情况非常宝贵。完全防止这些情况也非常有用!根据系统生成的可观察数据仔细设置警报可以帮助我们在系统完全失败之前采取补救措施。此外,此类数据为我们提供了重要的分析见解,以调整系统以获得更好的体验。

对可观察性的需求,虽然对任何系统都很重要,但对分布式系统来说是相当重要的。此外,我们的系统可以跨越公共云和私有云以及本地环境。此外,随着时间的推移,它的规模和复杂性不断变化。这通常会出现以前从未预料到的问题。一个高度可观察的系统可以极大地帮助我们处理这种情况。

3. 可观察性与监控

在DevOps 的实践 中,我们经常听到与可观察性相关的监控。那么,这些术语之间有什么区别呢?好吧,它们都有相似的功能,使我们能够保持系统的可靠性。但它们有细微的差别,事实上,它们之间存在联系。我们只能有效地监控一个系统,如果它是可观察的!

监控基本上是指通过一组预定义的指标和日志来观察系统状态的做法。这本质上意味着我们正在观察一组已知的故障。然而,在分布式系统中,有很多动态变化在不断发生。这导致了我们从未寻找过的问题。因此,我们的监控系统可能会漏掉它们。

另一方面,可观察性帮助我们了解系统的内部状态。这使我们能够提出有关系统行为的任意问题。例如,我们可以提出复杂的问题,例如在出现问题时每个服务如何处理请求。随着时间的推移,它可以帮助建立有关系统动态行为的知识。

要理解为什么会这样,我们需要理解基数的概念。基数是指集合中唯一项目的数量。例如,一组用户的社会安全号码将具有比性别更高的基数。要回答有关系统行为的任意问题,我们需要高基数数据。然而,监控通常只处理低基数数据。

4.分布式系统中的可观察性

正如我们之前看到的,可观察性对于复杂的分布式系统特别有用。但是,到底是什么让分布式系统变得复杂,在这样的系统中可观察性的挑战是什么?了解这个问题对于了解过去几年围绕该主题发展起来的工具和平台生态系统非常重要。

在分布式系统中,有很多移动组件动态地改变系统环境。此外,动态可伸缩性意味着在任何时间点都会有不确定数量的服务实例在运行。这使得收集、管理和存储系统输出(如日志和指标)的工作变得困难:

/uploads/distributed_systems_observability/1.jpg

此外,仅仅了解系统应用程序中发生的事情是不够的。例如,问题可能出在网络层或负载均衡器中。然后是数据库、消息传递平台,等等。始终**可以观察到所有这些组件非常重要。**我们必须能够从系统的所有部分收集和集中有意义的数据。

此外,由于多个组件同步或异步地协同工作,因此查明异常源并不容易。例如,很难说系统中的哪个服务导致瓶颈随着性能下降而升级。正如我们之前所见,跟踪在调查此类问题时非常有用。

5. 可观察性的演变

可观察性起源于控制理论,这是应用数学的一个分支,研究使用反馈来影响系统的行为以实现预期目标。我们可以将这一原则应用于多个行业,从工厂到飞机运营。对于软件系统来说,自从像 Twitter 这样的一些社交网站开始大规模运行后,这就变得很流行了。

直到最近几年,大多数软件系统都是单一的,这使得在发生事件时很容易对它们进行推理。监控在指示典型故障场景方面非常有效。此外,通过调试代码来识别问题是很直观的。但是,随着微服务架构和云计算的出现,这很快成为一项艰巨的任务。

随着这种演变的继续,软件系统不再是静态的——它们有许多动态变化的组件。这导致了以前从未预料到的问题。这在应用程序性能管理 (APM) 的保护伞下催生了许多工具,例如AppDynamics  和Dynatrace 。这些工具承诺提供一种更好的方式来理解应用程序代码和系统行为。

尽管这些工具在发展过程中已经走过了漫长的道路,但它们在当时还是相当基于指标的。这使他们无法提供我们所需的有关系统状态的视角。然而,它们是向前迈出的重要一步。今天,我们已经有了一系列工具来解决可观察性的三大支柱。当然,底层组件也需要是可观察的!

6. 亲身体验可观察性

现在我们已经涵盖了足够多的关于可观察性的理论,让我们看看如何将其付诸实践。我们将使用一个简单的基于微服务的分布式系统,我们将在其中使用Java 中的 Spring Boot 开发各个服务。这些服务将使用 REST API 相互同步通信。

让我们看看我们的系统服务:

/uploads/distributed_systems_observability/3.jpg

这是一个相当简单的分布式系统,其中math-service使用由addition-servicemultiplication-service等提供的 API。此外,math-service公开 API 来计算各种公式。我们将跳过创建这些微服务的细节,因为它非常简单。

本练习的重点是识别当今在可观察性背景下可用的最常见标准和流行工具。这个具有可观察性的系统的目标架构如下图所示:

/uploads/distributed_systems_observability/5.jpg

其中许多也处于云原生计算基金会 (CNCF) 的不同阶段,该基金会是一个促进容器技术进步的组织。我们将看到如何在我们的分布式系统中使用其中的一些。

7. 使用 OpenTracing 进行跟踪

我们已经看到跟踪如何提供宝贵的见解,以了解单个请求如何通过分布式系统传播。OpenTracing 是CNCF旗下的孵化项目。它为分布式跟踪提供供应商中立的 API 和工具。这有助于我们向不特定于任何供应商的代码添加检测。

符合 OpenTracing 的可用跟踪器列表正在快速增长。Jaeger 是最受欢迎的跟踪器之一,它也是 CNCF 下的毕业项目。

让我们看看如何在我们的应用程序中将 Jaeger 与 OpenTracing 结合使用:

/uploads/distributed_systems_observability/7.jpg

我们稍后会详细介绍。请注意,还有其他几个选项,如LightStepInstanaSkyWalkingDatadog 。我们可以轻松地在这些跟踪器之间切换,而无需更改我们在代码中添加检测的方式。

7.1. 概念和术语

OpenTracing 中的跟踪由跨度组成。跨度是在分布式系统中完成的单个工作单元。基本上,迹线可以看作跨度的有向无环图 (DAG)。我们将跨度之间的边称为参考。分布式系统中的每个组件都会向跟踪添加一个跨度。跨度包含对其他跨度的引用,这有助于跟踪重新创建请求的生命周期。

我们可以用时间轴或图表可视化轨迹中跨度之间的因果关系:

/uploads/distributed_systems_observability/9.jpg

在这里,我们可以看到OpenTracing 定义的两种类型的引用,“ChildOf”和“FollowsFrom”。这些建立了孩子和父母跨度之间的关系。

OpenTracing 规范定义了跨度捕获的状态:

  • 操作名称
  • 开始时间戳和结束时间戳
  • 一组键值跨度标签
  • 一组键值跨度日志
  • 跨度上下文

标签允许用户定义的注释成为我们用来查询和过滤跟踪数据的范围的一部分。跨度标签适用于整个跨度。同样,日志允许跨度捕获日志消息和其他调试或来自应用程序的信息输出。跨度日志可以应用于跨度内的特定时刻或事件。

最后,SpanContext 将 span 联系在一起。它跨进程边界传输数据。让我们快速浏览一下典型的 SpanContext:

/uploads/distributed_systems_observability/11.jpg

正如我们所见,它主要包括:

  • 依赖于实现的状态,如spanIdtraceId
  • 任何行李物品,它们是跨越流程边界的键值对

7.2. 设置和仪器

我们将从安装Jaeger 开始,这是我们将使用的 OpenTracing 兼容跟踪器。虽然它有几个组件,但我们可以使用一个简单的 Docker 命令安装它们:

docker run -d -p 5775:5775/udp -p 16686:16686 jaegertracing/all-in-one:latest

接下来,我们需要在我们的应用程序中导入必要的依赖项。对于基于 Maven 的应用程序,这就像添加依赖项 一样简单:

<dependency>
    <groupId>io.opentracing.contrib</groupId>
    <artifactId>opentracing-spring-jaeger-web-starter</artifactId>
    <version>3.3.1</version>
</dependency>

对于基于 Spring Boot 的应用程序,我们可以利用第三方提供的这个库。这包括所有必要的依赖项,并提供必要的默认配置来检测 Web 请求/响应并将跟踪发送到 Jaeger。

在应用程序端,我们需要创建一个Tracer

@Bean
public Tracer getTracer() {
    Configuration.SamplerConfiguration samplerConfig = Configuration
      .SamplerConfiguration.fromEnv()
      .withType("const").withParam(1);
    Configuration.ReporterConfiguration reporterConfig = Configuration
      .ReporterConfiguration.fromEnv()
      .withLogSpans(true);
    Configuration config = new Configuration("math-service")
      .withSampler(samplerConfig)
      .withReporter(reporterConfig);
    return config.getTracer();
}

这足以为请求通过的服务生成跨度。如有必要,我们还可以在我们的服务中生成子跨度:

Span span = tracer.buildSpan("my-span").start();
// Some code for which which the span needs to be reported
span.finish();

当我们为复杂的分布式系统分析它们时,这非常简单直观,但非常强大。

7.3. 跟踪分析

Jaeger 带有一个默认可在端口 16686 访问的用户界面。它提供了一种简单的方式来查询、过滤和分析跟踪数据并进行可视化。让我们看一下分布式系统的示例跟踪:

/uploads/distributed_systems_observability/13.jpg

正如我们所见,这是一个由其traceId 标识的特定跟踪的可视化。它清楚地显示了此跟踪中的所有跨度,以及它属于哪个服务以及完成所需时间等详细信息。这可以帮助我们了解在非典型行为的情况下问题可能出在哪里。

8. OpenCensus 指标

OpenCensus 为各种语言提供了 库,使我们能够从我们的应用程序中收集指标和分布式跟踪。它起源于谷歌,但从那时起就被一个不断壮大的社区开发为一个开源项目。OpenCensus 的好处是它可以将数据发送到任何后端进行分析。这使我们能够抽象我们的检测代码,而不是将其耦合到特定的后端。

尽管 OpenCensus 可以同时支持跟踪和指标,但我们仅将其用于示例应用程序中的指标。我们可以使用几个后端。最受欢迎的指标工具之一是Prometheus ,这是一个开源监控解决方案,也是 CNCF 下的毕业项目。让我们看看 Jaeger with OpenCensus 如何与我们的应用程序集成:

/uploads/distributed_systems_observability/15.jpg

虽然 Prometheus 自带用户界面,但是我们可以使用像Grafana 这样与 Prometheus 集成良好的可视化工具。

8.1. 概念和术语

在 OpenCensus 中,度量表示要记录的度量类型。例如,请求有效负载的大小可以是要收集的一种度量。测量是通过测量记录数量后产生的数据点。例如,80 kb 可以是请求负载大小度量的度量。所有度量均按名称、描述和单位标识。

要分析统计数据,我们需要将数据与视图聚合。视图基本上是应用于度量和可选标记**的聚合的耦合。**OpenCensus 支持计数、分布、总和和最后一个值等聚合方法。视图由名称、描述、度量、标签键和聚合组成。多个视图可以使用具有不同聚合的相同度量。

**标签是与记录的测量相关联的键值对数据,**用于提供上下文信息并在分析过程中区分和分组指标。当我们聚合测量以创建指标时,我们可以使用标签作为标签来分解指标。标签也可以作为分布式系统中的请求标头传播。

最后,导出器可以将指标发送到任何能够使用它们的后端。导出器可以根据后端进行更改,而不会对客户端代码产生任何影响。这使得 OpenCensus 在指标收集方面与供应商无关。对于大多数流行的后端,如普罗米修斯,有相当多的出口商提供多种语言。

8.2. 设置和仪器

由于我们将使用 Prometheus 作为后端,因此我们应该首先安装它。使用官方 Docker 镜像可以快速简单地完成此操作。Prometheus 通过在这些目标上抓取指标端点来从受监控的目标收集指标。因此,我们需要在 Prometheus 配置 YAML 文件prometheus.yml中提供详细信息:

scrape_configs:
  - job_name: 'spring_opencensus'
    scrape_interval: 10s
    static_configs:
      - targets: ['localhost:8887', 'localhost:8888', 'localhost:8889']

这是一个基本配置,它告诉 Prometheus 从哪个目标中抓取指标。现在,我们可以使用一个简单的命令启动 Prometheus:

docker run -d -p 9090:9090 -v \
  ./prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus

为了定义自定义指标,我们首先定义一个度量

MeasureDouble M_LATENCY_MS = MeasureDouble
  .create("math-service/latency", "The latency in milliseconds", "ms");

接下来,我们需要为刚刚定义的度量记录一个度量:

StatsRecorder STATS_RECORDER = Stats.getStatsRecorder();
STATS_RECORDER.newMeasureMap()
  .put(M_LATENCY_MS, 17.0)
  .record();

然后,我们需要为我们的度量定义一个聚合和视图,使我们能够将其导出为度量

Aggregation latencyDistribution = Distribution.create(BucketBoundaries.create(
  Arrays.asList(0.0, 25.0, 100.0, 200.0, 400.0, 800.0, 10000.0)));
View view = View.create(
  Name.create("math-service/latency"),
  "The distribution of the latencies",
  M_LATENCY_MS,
  latencyDistribution,
  Collections.singletonList(KEY_METHOD)),
};
ViewManager manager = Stats.getViewManager();
manager.registerView(view);

最后,为了将视图导出到 Prometheus,我们需要创建并注册收集器并将 HTTP 服务器作为守护进程运行:

PrometheusStatsCollector.createAndRegister();
HTTPServer server = new HTTPServer("localhost", 8887, true);

这是一个简单的示例,说明了我们如何将延迟记录为应用程序的度量,并将其作为视图导出到 Prometheus 进行存储和分析。

8.3. 指标分析

OpenCensus 提供称为 zPages 的进程内网页,显示从它们所连接的进程中收集的数据。此外,Prometheus 提供了表达式浏览器,允许我们输入任何表达式并查看其结果。然而,像Grafana 这样的工具提供了更优雅和高效的可视化。

使用官方 Docker 镜像安装 Grafana 非常简单:

docker run -d --name=grafana -p 3000:3000 grafana/grafana

Grafana 支持查询 Prometheus——我们只需要在 Grafana 中添加 Prometheus 作为数据源。然后,我们可以创建一个图表,其中包含用于指标的常规 Prometheus 查询表达式:

/uploads/distributed_systems_observability/17.png

我们可以使用多种图表设置来调整我们的图表。此外,还有几个预构建的 Grafana 仪表板可用于 Prometheus,我们可能会发现它们很有用。

9. 使用 Elastic Stack 记录日志

日志可以提供有关应用程序对事件的反应方式的宝贵见解。不幸的是,在分布式系统中,它被拆分成多个组件。因此,从所有组件收集日志并将它们存储在一个地方以进行有效分析变得很重要。此外,我们需要一个直观的用户界面来高效地查询、过滤和引用日志。

Elastic Stack 基本上是一个日志管理平台,直到最近,它还是三种产品的集合——Elasticsearch、Logstash 和 Kibana (ELK)。

然而,从那时起,Beats 被添加到这个堆栈中以进行高效的数据收集

让我们看看如何在我们的应用程序中使用这些产品:

/uploads/distributed_systems_observability/19.jpg

正如我们所见,在 Java 中,我们可以使用像 SLF4J 这样的简单抽象 和像Logback 这样的记录器来生成日志。我们将在这里跳过这些细节。

Elastic Stack 产品是开源的,由Elastic 维护。这些共同为分布式系统中的日志分析提供了一个引人注目的平台。

9.1. 概念和术语

正如我们所见,Elastic Stack 是多种产品的集合。这些产品中最早的是Elasticseach ,它是一个分布式的、RESTful 的、基于 JSON 的搜索引擎。由于其灵活性和可扩展性,它非常受欢迎。这是导致 Elastic 成立的产品。它基本上基于 Apache Lucene 搜索引擎。

Elasticsearch将索引存储为文档,文档是存储的基本单位。这些是简单的 JSON 对象。我们可以使用类型来细分文档中相似类型的数据。索引是文档的逻辑分区。通常,我们可以将索引水平拆分为碎片以实现可扩展性。此外,我们还可以复制分片以实现容错:

/uploads/distributed_systems_observability/21.jpg

Logstash一个日志聚合器,它从各种输入源收集数据。它还执行不同的转换和增强,并将其传送到输出目的地。由于 Logstash 的占用空间更大,我们有Beats ,它是轻量级数据传送器,我们可以将其作为代理安装在我们的服务器上。最后,Kibana一个在 Elasticsearch 之上工作的可视化层

这些产品共同提供了一个完整的套件来执行日志数据的聚合、处理、存储和分析:

/uploads/distributed_systems_observability/23.jpg

使用这些产品,我们可以为我们的日志数据创建生产级数据管道。但是,很有可能并且在某些情况下也有必要扩展此体系结构以处理大量日志数据。我们可以在Logstash前面放一个像Kafka 这样的缓冲区,防止下游组件压垮它。Elastic Stack 在这方面非常灵活。

9.2. 设置和仪器

正如我们之前所见,Elastic Stack 包含多个产品。当然,我们可以独立安装它们。然而,这很耗时。幸运的是,Elastic 提供了官方 Docker 镜像来简化这一过程。

启动单节点 Elasticsearch 集群就像运行 Docker 命令一样简单:

docker run -p 9200:9200 -p 9300:9300 \
  -e "discovery.type=single-node" \
  docker.elastic.co/elasticsearch/elasticsearch:7.13.0

同样,安装 Kibana 并将其连接到 Elasticsearch 集群也非常简单:

docker run -p 5601:5601 \
  -e "ELASTICSEARCH_HOSTS=http://localhost:9200" \
  docker.elastic.co/kibana/kibana:7.13.0

安装和配置 Logstash 稍微复杂一些,因为我们必须为数据处理提供必要的设置和管道。实现此目的的一种更简单的方法是在官方映像之上创建自定义映像:

FROM docker.elastic.co/logstash/logstash:7.13.0
RUN rm -f /usr/share/logstash/pipeline/logstash.conf
ADD pipeline/ /usr/share/logstash/pipeline/
ADD config/ /usr/share/logstash/config/

让我们看一下与 Elasticsearch 和 Beats 集成的 Logstash 的示例配置文件:

input {
  tcp {
  port => 4560
  codec => json_lines
  }
  beats {
    host => "127.0.0.1"
    port => "5044"
  }
}
output{
  elasticsearch {
  hosts => ["localhost:9200"]
  index => "app-%{+YYYY.MM.dd}"
  document_type => "%{[@metadata][type]}"
  }
  stdout { codec => rubydebug }
}

根据数据源的不同,有多种类型的 Beats 可用。对于我们的示例,我们将使用 Filebeat。安装和配置 Beats 最好借助自定义映像来完成:

FROM docker.elastic.co/beats/filebeat:7.13.0
COPY filebeat.yml /usr/share/filebeat/filebeat.yml
USER root
RUN chown root:filebeat /usr/share/filebeat/filebeat.yml
USER filebeat

让我们看一下Spring Boot 应用程序的示例filebeat.yml

filebeat.inputs:
- type: log
enabled: true
paths:
  - /tmp/math-service.log
output.logstash:
hosts: ["localhost:5044"]

这是对 Elastic Stack 的安装和配置的非常粗略但完整的解释。深入讨论所有细节超出了本教程的范围。

9.3. 日志分析

Kibana 为我们的日志提供了一个非常直观和强大的可视化工具。我们可以通过其默认 URL http://localhost:5601 访问 Kibana 界面。我们可以选择一个可视化并为我们的应用程序创建一个仪表板。 让我们看一个示例仪表板:

/uploads/distributed_systems_observability/25.jpg

Kibana 提供了相当广泛的功能来查询和过滤日志数据。这些超出了本教程的范围。

10. 可观察性的未来

现在,我们已经了解了为什么可观察性是分布式系统的一个关键问题。我们还介绍了一些处理不同类型遥测数据的常用选项,这些选项可以使我们实现可观察性。然而,事实是,将所有零件组装起来还是相当复杂和耗时的。我们必须处理很多不同的产品。

该领域的一项关键进步是OpenTelemetry ,它是 CNCF 中的一个沙盒项目。基本上,OpenTelemetry 是通过仔细合并 OpenTracing 和 OpenCensus项目而形成的。显然,这是有道理的,因为我们只需要处理跟踪和指标的单一抽象。

更重要的是,OpenTelemetry计划支持日志并使它们成为分布式系统的完整可观察性框架。此外,OpenTelemetry 支持多种语言,并与流行的框架和库很好地集成。此外,OpenTelemetry 通过软件桥向后兼容 OpenTracing 和 OpenCensus。

OpenTelemetry 仍在进行中,我们可以期待它在未来几天内成熟。同时,为了减轻我们的痛苦,一些可观察性平台结合了前面讨论的许多产品以提供无缝体验。例如,Logz.io 结合了 ELK、Prometheus 和 Jaeger 的强大功能,提供可扩展的平台即服务。

随着新产品以创新解决方案进入市场,可观察性空间正在快速成熟。例如,Micrometer 为多个监控系统的仪器客户端提供了一个供应商中立的外观。最近,OpenMetrics 发布了其规范,以创建用于大规模传输云原生指标的事实标准。