博客/工程

介绍使用Tempo、OpenTelemetry和Grafana Cloud进行分布式跟踪

2021年9月23日14分钟

我职业生涯的大部分时间都在与各种形式的技术打交道,在过去十年左右的时间里,我一直专注于构建、维护和操作健壮、可靠的系统。

这让我投入了大量的时间来研究、评估和实现自动故障检测、监控以及最近的可观察性的不同解决方案。bob彩票中奖计划

在我们开始之前:什么是可观察性?可观察性是采用一个不透明的系统或系统中的动作,并使我们可以通过其输出来检查和推理。

例如,考虑来自用户的交互,该交互导致通过订单API下订单。如果订单投放出现问题,我们能在多大程度上调试和排除故障?我们是否能够看到并跟踪交互留下的痕迹,阅读日志,或者分析指标来推断发生了什么?

在经典的单片应用程序中,这是相当简单的——至少在非生产环境中是如此,通常允许我们附加调试器,甚至可能放入断点来按我们希望的方式逐步遍历逻辑。在生产中,考虑到我们在发布终端用户使用的软件时很少(并且有充分的理由)保持调试启用,这将更加困难。

虽然这很麻烦,但对于这些单片系统来说效果很好。然而,随着交付速度的增加,系统开始变得越来越分布式,以支持这种新的工作方式,在这种方式中,多个团队将彼此独立地发布系统的各个部分。

巨石vs分布式系统

让我们看一个向单片系统发出请求的示例。它非常直接,控制流从一个方法传递到另一个方法,在完成过程中跨越多个类。我们得到了一致的堆栈跟踪,并且通过只添加日志记录,我们至少可以很好地了解正在发生什么以及为什么发生。

现在让我们看一下相同的系统,但是使用微服务实现为分布式系统。在这里,每个微服务(或者lambda函数,如果你真的在挑战极限的话)都可以由不同的团队或人员交付。此体系结构中的组件是松散耦合的,仅受消息或它们之间的交互的运行时期望的约束。

这种体系结构风格还意味着不存在将服务绑定在一起的总体流程。相反,它们都将作为各自独立的小应用程序运行,甚至可能跨多个副本、跨多个数据中心甚至不同的地理区域进行复制。

曾经简单的调试方式现在不再是一种选择。每个服务的调用之间不再有任何明显的相关性,并且试图使用(例如)时间戳或其他元数据将它们拼凑在一起将很快变得难以管理。在每秒只有10个请求的系统中,人工劳动将是压倒性的,而且我们很可能经常出错。

想象一下,如果我们把它扩大到每秒1万个请求,或者100万个请求。如果对同一方法的多个调用之间只有几纳秒的间隔,该怎么办?它无法扩展。

那么我们该如何补救呢?这就是可观察性的由来。

可观察性的三大支柱

当人们谈论可观察性时,通常会听到他们提到可观察性的三个支柱:度量、日志和跟踪。虽然不能神奇地使系统更易于观察,但以正确的方式配对,这三种方法组成了一个完整的技术堆栈,用于检查系统内部发生的事情。

痕迹

虽然这三个都是使系统更具可观察性的重要组成部分,但本文将重点介绍第三个支柱:跟踪。跟踪,或者在本例中是分布式跟踪,是解决分布式系统中一致性缺失的一种尝试。

我们通过检测我们的系统来做到这一点,使运行时信息作为跟踪的一部分可用。像局部作用域变量、堆栈跟踪和日志都作为时间戳数据添加到跟踪中,以供外部分析。

这可能会给我们的系统增加性能开销,但是对于大多数团队来说,能够分析系统状态的便利性远远超过任何性能损失。

何时使用

我们是否总是默认将分布式跟踪添加到应用程序中?不一定。如果我们已经能够以一种令人满意的方式推理系统的内部结构,那么可能会有其他可用的活动为团队或产品带来更多价值。

然而,如果我们目前觉得我们缺乏完成以下任何一项的方法,那么分布式跟踪可能正是我们所需要的:

  • 可以轻松查看此系统的服务部分的运行状况。
  • 找出生产中出现的错误和缺陷的根本原因。
  • 找出性能问题,并查明它们发生的位置和原因。

跟踪是如何工作的

那么,这在实践中是如何工作的呢?首先,我们需要一些方法来跟踪哪个微服务调用属于哪个跟踪。我们通过向请求上下文添加元数据来实现这一点,我们将在每个后续调用中传递元数据,允许它们形成一致的跟踪,然后我们可以使用跟踪或span ID(例如)进行搜索和分析。

我希望你们都还醒着。我保证我们很快就会讲到有趣的部分,但在此之前,我们只需要稍微讨论一下trace的组成部分。

由于跟踪的格式不止一种,它们传播上下文的方式也不止一种,因此根据您选择的格式,跟踪的构建块可能会有轻微的差异。

对于本文,我们将使用OpenTelemetry项目用于跟踪、上下文传播和导出。一般来说,这些概念也适用于大多数其他跟踪实现。

让我们从头开始。我们的任务是为假设的DogBook公司设置跟踪。我们的一个用户,Floor,正在试图获取一个犬舍列表以及使用我们的API生活在那里的狗,在这种情况下,/ api / v1 /犬舍

她的请求由狗窝端点时,创建根跨度。这个跨度将包含完整请求的端到端计时。它还将用作作为请求一部分创建的所有其他跨度的容器。

对于每个后续操作,将创建额外的跨度并嵌套在它们的父跨度下面。因此,在我们的示例中,根跨度是在请求到达时创建的api / v1 /犬舍,然后,当我们向犬舍微服务请求犬舍列表时,另一个span将被创建。

养犬微服务要求为特定犬舍中的狗的名字提供微服务,将创建第三个跨度,以此类推,形成因果关系模型。

也许右边的图表看起来有点眼熟?你以前可能见过类似的东西。虽然不完全相同,但大多数现代web浏览器都做了类似的事情,并以类似的方式使用它,只是所呈现的数据是以一种略有不同的方式收集的。

除了可嵌套之外,每个跨度还保存检测数据和计时,这就是分布式跟踪如此强大的原因。

单个span通常捕获以下数据:

  • 操作名称。
  • 开始和结束时间戳。
  • 标签,基本上是任意数据的键值对。
  • 一组事件,每个事件是一个三元元组,包含时间戳、名称和一组属性。
  • 父span标识符。
  • 链接到其他因果相关的跨度(通过SpanContext这些相关的跨度)。
  • SpanContext,用于引用一个span。其中包括跟踪ID和span ID等。

演示

现在来看看有趣的东西!为了简单起见,我们将用模拟数据替换数据库。但是,我们仍然将数据逻辑放在它们自己的函数中(使用它们自己的span),只是为了得到一些更漂亮的span。

我们将用JavaScript将其实现为三个不同的微服务,它们都运行在各自的Docker容器中。

$ tree。├──代理。Yaml├──docker-compose。Yml├─nginx│├──conf.d│├──default.conf├──oteli -config。yaml├──services├──dogs│├──Dockerfile│├──package。│├──src│├─index.js│├──inventory│├──Dockerfile│├──package。json│├──src│├─index.js│├──trace .js├──kennels├──Dockerfile├──package。Json├──SRC├─data.js├──index.js├──trace .js

tracer.js所有三个服务的文件和Docker文件是相同的。在Docker文件的情况下,这可能只是暂时的,这就是为什么我们为每个项目保留一个单独的文件。

# ./services/*/Dockerfile FROM节点:最新WORKDIR /app COPY ./package. /json包。RUN npm install COPY ./src src EXPOSE 8080 CMD ["npm", "start"]
// ./services/*/trace .js const initTracer = require('jaeger-client').initTracer;function createTracer(serviceName, collectorEndpoint) {const配置= {serviceName,采样器:{类型:'const',参数:1,},报告器:{logspan: true, collectorEndpoint,},};const options = {logger: {info(msg) {console.log(' info ', msg);},错误(msg) {console.log(' error ', msg);},},};返回initTracer(config, options);}模块。exports = {createTracer,};

tracer.js,我们当然可以将一个共享文件保存在一个单独的实用程序包中,但为了简单起见,我只是复制了它,以避免使构建复杂化。

NGINX将在此设置中用作反向代理,允许我们通过统一的接口使用容器api,而不是直接将它们暴露给外部。

# ./nginx/conf.d/default.conf上游狗{服务器狗:3000;}上游库存{服务器库存:8080;}上游狗窝{服务器狗窝:8080;}服务器{server_name dogbook;Location ~ ^/api/v1/dogs {proxy_pass http://dogs/$uri$is_args$args;} location ~ ^/api/v1/inventory {proxy_pass http://inventory/$uri$is_args$args;} location ~ ^/api/v1/kennels {proxy_pass http://kennels/$uri$is_args$args;}}

虽然所有端点都可以独立地使用,但为了演示起见,我们感兴趣的是/ api / v1 /犬舍,因为这反过来将使用其他两个api作为其逻辑的一部分。

// ./services/kennels/src/index.js const express = require('express');Const opentracing = require('opentracing');Const data = require('./data');const {createTracer} = require('./tracer');const tracer = createTracer('kennel -service', 'http://collector:14268/api/traces');Const app = express();App.use ('/', async (req, res) => {const parent = trace .extract(opentracing. extract)。FORMAT_HTTP_HEADERS req.headers);const span = trace . startspan ('kennels. startspan ')process-request', {childOf: parent});Const id = req.query.id; const name = await data.getKennelName(id, span, tracer); if (!name) { res.status(404); res.send(); span.finish(); return; } const inventory = await data.getInventory(id, span, tracer); let dogs = await Promise.all( inventory.map(async (x) => { const name = await data.getDogDetails(x, span, tracer); return { id: x, name, }; }) ); res.send({ name, dogs }); span.finish(); }); app.listen('8080', '0.0.0.0');
// ./service/kennels/src/data.js const axios = require('axios');Const opentracing = require('opentracing');function getKennelName(id, parent, tracer) {const span = trace . startspan ('kennels. startspan ')get-dog-details', {childOf: parent});Const name = ['awesome kennel', 'not as awesome kennel', 'some other kennel'][id];span.finish ();返回名称;} async函数getDogDetails(id, parent, tracer) {const span = trace . startspan ('kennels. startspan ')get-dog-name', {childOf: parent});让名字;Try {let headers = {};示踪剂。inject(span, opentracing.FORMAT_HTTP_HEADERS, headers); const res = await axios.get(`http://dogs:8080/?id=${id}`, { headers }); name = res.data; } catch (e) { console.log(e); } span.finish(); return name || 'Nameless Dog'; } async function getInventory(id, parent, tracer) { const span = tracer.startSpan('kennels.get-inventory', { childOf: parent }); let result; try { let headers = {}; tracer.inject(span, opentracing.FORMAT_HTTP_HEADERS, headers); const response = await axios.get(`http://inventory:8080/?kennelId=${id}`, { headers, }); result = response.data; } catch (e) { console.log(e); result = []; } span.finish(); return result; } module.exports = { getKennelName, getDogDetails, getInventory, };

让我们来分析一下这里发生了什么。我们正在使用Express创建一个简单的HTTP API。它目前只包含一个端点:根结点。然后,我们检查跟踪标头的请求,以确定它是否是现有跟踪的一部分,并创建一个新的span,包括父span的span ID(如果存在的话)。

然后,我们执行实际的“业务逻辑”,从ID解析狗舍名称,获取狗舍的库存并循环,逐一获取目前每只狗的名称。

值得注意的是,在每次调用时都要从后续的服务开始data.js,我们注入跟踪头,以确保跟踪保持一致,即使我们遍历多个单独的服务,所有服务都运行在自己的进程中。

// ./services/inventory/index.js const express = require('express');Const axios = require('axios');Const opentracing = require('opentracing');const {createTracer} = require('./trace .js');const tracer = createTracer('inventory-service', 'http://collector:14268/api/traces');Const app = express();App.use ('/', async (req, res) => {const parent = trace .extract(opentracing. extract)。FORMAT_HTTP_HEADERS req.headers);const span = trace . startspan('库存。process', {childOf: parent});const id = req.query.kennelId;span.setTag(“库存。kennel-id”、身份证); const ids = await getInventoryByKennelId(id, span); res.send(ids); span.finish(); }); app.listen('8080', '0.0.0.0'); async function getInventoryByKennelId(id, parent) { const inventory = [[0], [1, 2], [3, 4, 5]]; const span = tracer.startSpan('inventory.get-inventory-by-kennel-id', { childOf: parent, }); await new Promise((resolve) => setTimeout(resolve, 100)); span.finish(); return inventory[id]; }

库存而且服务基本上复制相同的行为。为了使跟踪更加有趣,我们还将使用承诺而且setTimeout

// ./services/dogs/index.js const express = require('express');Const axios = require('axios');Const opentracing = require('opentracing');const {createTracer} = require('./trace .js');const tracer = createTracer('dogs-service', 'http://collector:14268/api/traces');Const app = express();App.use ('/', async (req, res) => {const parent = trace .extract(opentracing. extract)。FORMAT_HTTP_HEADERS req.headers);const span = trace . startspan ('dogs. span ')process-request', {childOf: parent});Const id = req.query.id; span.setTag('dogs.id', id); const name = await getDogName(id, span); res.send(name); span.finish(); }); app.listen('8080', '0.0.0.0'); async function getDogName(id, parent) { const names = ['Rufus', 'Rex', 'Dobby', 'Möhre', 'Jack', 'Charlie']; const span = tracer.startSpan('inventory.get-dog-name', { childOf: parent, }); await new Promise((resolve) => setTimeout(resolve, 100)); span.finish(); return names[id]; }

现在我们来试一下。不过,在我们能够做到这一点之前,我们还需要创建一个包含启动所有容器所需配置的组合文件。

版本:'3.1'服务:nginx: image: nginx:最新端口:- '80:80' - '443:443'卷:- ./nginx/conf.d:/etc/nginx/conf. d:/etc/nginx/conf. d:/etc/nginx/conf. d:D depends_on:—kennels collector: image: otel/opentelemetry-collector:0.23.0 command: '——config /etc/otel-config。Yaml '卷:- ./otel-config. Yaml:/etc/otel-config. Yaml:/etc/otel-config. Yaml:/etc/otel-config. Yaml:/etc/otel-config. YamlYaml端口:- 6831:6831狗:构建:./services/狗库存:构建:./services/库存犬舍:构建:./services/犬舍

我们不会为JavaScript服务添加任何端口映射,因为我们不会直接访问它们,而是通过NGINX反向代理。我们还将设置一个opentelementary -collector,用于收集跟踪数据并将其转发到跟踪后端。

receiver: jaeger: protocols: thrift_compact: thrift_http: processors: batch: exporters: logging: loglevel: debug otlp: endpoint: tempo-us-central1.grafana.net:443 headers: authorization: Basic  service: pipelines: traces: receivers: [jaeger] processors: [batch] exporters: [otlp]

当然,您可以使用任何您想要的跟踪后端(和UI),但是在本文中,我们将使用开放源码项目的托管版本节奏在一起Grafana云.这将使我们能够快速、轻松地可视化、分析和探索我们的痕迹。

完成所有配置后,我们将部署堆栈。

$ docker-compose -d

然后我们将执行一个请求,并检查日志中的跟踪id:

$ curl localhost "localhost/api/v1/kennels?Id =2" && docker logs distributed-tracing-demo_kennels_1[…]] INFO报告范围c3d3fa296e838a66:c3d3fa296e838a66:0:1

我们复制张成空间ID的第一部分,c3d3fa296e838a66,这是跟踪ID,并在Grafana中搜索它。

好吧,这其实已经很酷了!我们可以看到前面谈到的因果关系模型,以及在每个跨度中使用的执行时间的百分比。

如果我们向下滚动页面,我们将得到相同跟踪的瀑布视图,并能够进一步深入到细节,并检查像标签、日志等内容。

如何实现

那么我们如何在现有的项目中实现它呢?我完全至少可以这么说,在当前项目中实现分布式跟踪是不切实际的,这可能有多种原因。除了要求开发人员积极地检测服务之外,还需要付出相当大的努力才能做到正确。

不要绝望!现在做正确的事情可能是遥不可及的,但是的某个地方一点都不浪费时间。对于许多流行的语言,如JavaScript、Go、Python和Java,许多标准包的自动插装已经可用!

所以,与其什么都不做,不如试一试!也许它们正是你解决困扰你数月的糟糕表现问题所需要的东西?获取每个服务的时间将极大地帮助确定低效代码!

您将快速构建一个示例库,其中跟踪为您节省了精力,有了这些示例库,您将有一个更强大的案例来获得组织的支持,您需要不断迭代,构建工具,并慢慢地朝着“正确”的方向工作!

开始使用Tempo和跟踪最简单的方法是Grafana云.我们为每个用例提供免费和付费计划。注册一个免费账户现在!