博客/工程

介绍分布式跟踪与节奏,OpenTelemetry Grafana云

2021年9月23日 14分钟

我花了我的大部分职业生涯与各种形式的技术合作,在过去十年左右的时间里,我经常关注建筑、维护和操作健壮、可靠的系统。

这使得我把大量的时间放在研究、评估、和实施不同的解决方案,自动故障检测,监控,和最近,可观测性。bob彩票中奖计划

在我们开始之前:可观测性是什么?可观测性的实践是一个透明系统,或操作系统中,使它可供我们检查和思考通过其输出。

例如,考虑一个交互从用户,导致订单被放置在订单API。如果下单南行,在多大程度上我们能够调试和排除吗?我们能看到并遵循留下的痕迹互动,阅读日志,或分析指标推导出发生了什么事?

在一个典型的单片应用程序中,这将是相当简单的——至少只要我们呆在非生产环境中,通常能够允许我们将调试器附加,甚至放在断点一步通过逻辑作为我们的愿望。在生产中,这将更加困难,因为我们很少(理由)保持启用调试我们释放软件最终用户使用的。

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

巨石和分布式系统

让我们看看一个例子的一个请求会单一的系统。很简单,控制流从一个方法传递到另一个,跨越多个类,因为它对完成旅行。我们得到一个连贯的堆栈跟踪,只添加日志记录,我们至少会得到一个体面的理解发生了什么和为什么。

现在让我们看看相同的系统,但作为一个分布式系统使用microservices实现。这里,每个microservice——或者lambda函数如果你真的把信封——可能是由不同的团队或个人。在此体系结构的组件是松散耦合的,限制只有运行时预期的消息或它们之间的交互。

这种建筑风格也意味着没有总体过程绑定在一起的服务。相反,他们会作为自己的独立的小应用程序,运行甚至复制多个副本,跨多个数据中心,甚至不同的地理区域。

once-straightforward的调试方式现在不再是一个选择。不再有任何明显的每个服务的调用之间的相关性,并试图在一起使用,例如,时间戳或其他元数据将很快变得难以管理。系统中只有10每秒请求,体力劳动将是压倒性的,我们很有可能出错往往。

想象一下,如果我们规模高达每秒10000个请求,或一百万年。如果多个调用相同的方法只有纳秒他们之间发生?它只是不会规模。

那么我们怎么补救呢?这就是可观测性。

可观测性的三大支柱

当人们谈论可观测性,经常听到他们指的可观测性的三大支柱:指标、日志和跟踪。虽然不是神奇地使系统更可观测,以正确的方式,这三个组成一个比较完整的堆栈技术检查系统里想的是什么。

痕迹

在所有三个系统更可观测的重要部分,本文将关注第三个支柱:痕迹。分布式跟踪跟踪,或在这种情况下,是一个为了解决相干在分布式系统的损失。

我们做这个检测系统,使运行时信息作为我们的痕迹。诸如局部作用域变量、堆栈跟踪和日志都添加到跟踪时间戳数据外部分析。

这可能会增加我们的系统的性能开销,但对大多数球队,能够分析系统状态的方便远远超过任何性能损失。

什么时候使用

我们应该总是默认添加分布式跟踪我们的应用程序吗?不一定。如果我们已经能够推断我们的内部系统以令人满意的方式,可能会有其他活动会给团队带来更多的价值或产品。

但是,如果我们现在觉得我们没有办法做任何的追随者,那么分布式跟踪也许正是我们需要:

  • 很容易看到的健康服务系统的一部分。
  • 发现错误和缺陷的根本原因,表面在生产。
  • 发现性能问题和确定在哪里以及为什么会发生。

跟踪的工作原理

所以在实践中这是如何工作的呢?嗯,首先,我们需要一些方法来追踪microservice所说的属于什么痕迹。我们通过添加元数据请求上下文,我们会通过每个后续调用,使他们形成一个连贯的痕迹,然后我们可能搜索和分析使用,例如,一个跟踪或跨度ID。

我希望你仍然清醒。我保证我们很快就会到有趣的地方,但在此之前,我们只需要一点谈论什么构成一个痕迹。

作为跟踪不止一种格式,以及他们如何传播环境中,可能会有轻微的差异构建块的跟踪根据你选择的格式。

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

所以,让我们从顶部开始。我们的任务是建立假设公司DogBook跟踪。我们的一个用户,地板,试图获取列表犬舍的狗住在那里使用我们的API,在这种情况下,/ api / v1 /犬舍

她的请求处理狗窝端点,创建一个根跨度。这个跨度将包含完整的时间要求,端到端。它也将作为一个容器对于所有其他跨越作为请求的一部分创建的。

对于每一个后续的操作,将创建额外的跨度和嵌套在他们的父母。所以,在我们的示例中,创建根跨请求到达api / v1 /犬舍,然后,当我们问的养犬microservice犬舍的列表,将创建另一个跨度。

养犬microservice问microservice为特定的狗狗的名字,将会创建一个三跨,等等,形成了一个因果关系模型。

或许右边的图看起来有点熟悉吗?你可能见过类似的东西。虽然不是完全相同的,大多数现代浏览器做类似的事情,以类似的方式使用它,只有提供的数据聚集在一个不同的方式。

除了可嵌套,每个跨度还拥有仪表数据和时间,也就是使分布式跟踪如此强大。

一个跨度通常捕获以下数据:

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

演示

现在到有趣的东西!为简单起见,我们将用模拟数据替换数据库。然而,我们仍然会将数据逻辑的函数(用自己的跨度),只是为了得到一些更漂亮的跨越。

我们会实现这个在三个不同的microservices JavaScript,都运行在自己的码头工人的容器中。

美元树。├──代理。yaml├──docker-compose。yml├──nginx│└──conf.d│└──违约。参看├──otel-config。yaml└──服务├──狗│├──Dockerfile│├──包。json│└──src│├──指数。js│└──示踪剂。js├──库存│├──Dockerfile│├──包。json│└──src│├──指数。js│└──示踪剂。js└──犬舍├──Dockerfile├──包。json└──src├──数据。js├──指数。js└──tracer.js

tracer.js文件和码头工人文件为所有三个服务是相同的。码头工人的文件,这可能只是暂时的,这就是为什么我们让每个项目一个单独的文件。

#。从节点/服务/ * / Dockerfile:最新WORKDIR /应用程序副本。/包。json包。json运行npm安装副本。/ src src暴露8080 CMD (“npm”、“开始”)
/ / /服务/ * /示踪剂。js const initTracer =要求(jaeger-client) .initTracer;函数createTracer(这是collectorEndpoint){常量配置={这是取样器:{类型:“常量”,参数:1、},记者:{logSpans:真的,collectorEndpoint,},};const选项={记录器:{信息(味精){控制台。日志(“信息”,味精);(msg){},错误控制台。日志(‘错误’,味精);},}};返回initTracer(配置选项);}模块。出口= {createTracer,};

tracer.js当然,我们可以保持一个共享文件在一个独立的实用程序包,但为了简单起见,我刚刚去重复它,以避免复杂的构建。

NGINX将用于这个设置反向代理,允许我们使用容器通过统一接口api,而不是让他们直接向外。

#。/ nginx / conf.d /违约。设计及其上游狗{服务器狗:3000;上游库存}{服务器库存:8080;上游犬舍}{服务器犬舍:8080;}服务器{server_name dogbook;位置~ ^ / api / v1 /狗{proxy_pass http://dogs/ uri is_args args美元美元;~ ^}位置/ api / v1 /库存{proxy_pass http://inventory/ uri is_args args美元美元;~ ^}位置/ api / v1 /犬舍{proxy_pass http://kennels/ uri is_args args美元美元;}}

尽管所有端点可能独立使用,一个我们感兴趣的是为了这个演示/ api / v1 /犬舍,这反过来将消耗的其他api的逻辑。

/ / /服务/犬舍/ src /指数。js const表达=要求(“表达”);const opentracing =要求(“opentracing”);常量数据=需要(' /数据。');const {createTracer} =要求(“。/示踪”);const示踪剂= createTracer (kennels-service, http://collector: 14268 / api /痕迹);const应用=表达();app.use(' / ',异步(点播,res) = > {const父= tracer.extract (opentracing。FORMAT_HTTP_HEADERS req.headers);const跨度= tracer.startSpan(的犬舍。工艺要求的,{childOf:父母}); 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');
/ /,/服务/犬舍/ src /数据。js const axios =要求(“axios”);const opentracing =要求(“opentracing”);函数getKennelName (id、父母、示踪剂){const跨度= tracer.startSpan(的犬舍。get-dog-details’, {childOf:父母});const name =['很棒的狗”,“不是很棒的狗”,“其他一些养犬”][id];span.finish ();返回名称;}异步函数getDogDetails (id、父母、示踪剂){const跨度= tracer.startSpan(的犬舍。get-dog-name’, {childOf:父母});让名字;尽量{让标题= {}; tracer.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, };

让我们试着打开这是怎么回事。我们使用的表达来创建一个简单的HTTP API。目前由一个单端点:根。然后我们检查跟踪请求标头是否这是现有的跟踪,并创建一个新的跨越,包括母公司的跨度ID是否存在。

我们然后执行实际的“业务逻辑”,解决养犬名称的ID和获取养犬的库存和循环,获取每个狗的名字呈现一个接一个。

值得注意的是,在每次调用后续服务data.js,我们将跟踪头确保跟踪保持连贯的即使我们遍历多个单独的服务,所有正在运行的进程。

/ / /服务/库存/指数。js const表达=要求(“表达”);const axios =要求(“axios”);const opentracing =要求(“opentracing”);const {createTracer} =要求(' / tracer.js。');const示踪剂= createTracer(库存服务,http://collector: 14268 / api /痕迹);const应用=表达();app.use(' / ',异步(点播,res) = > {const父= tracer.extract (opentracing。FORMAT_HTTP_HEADERS req.headers);const跨度= tracer.startSpan(库存。过程的,{childOf:父母});const id = req.query.kennelId; span.setTag('inventory.kennel-id', 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

/ / /服务/狗/索引。js const表达=要求(“表达”);const axios =要求(“axios”);const opentracing =要求(“opentracing”);const {createTracer} =要求(' / tracer.js。');const示踪剂= createTracer (dogs-service, http://collector: 14268 / api /痕迹);const应用=表达();app.use(' / ',异步(点播,res) = > {const父= tracer.extract (opentracing。FORMAT_HTTP_HEADERS req.headers);const跨度= tracer.startSpan(“狗。工艺要求的,{childOf:父母}); 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:形象:nginx:最新的港口:——“80:80”——“443:443”卷:-)/ nginx / conf.d: / etc / nginx / conf。d depends_on:犬舍收集器:形象:otel / opentelemetry-collector: 0.23.0命令:“——配置/etc/otel-config。yaml的卷:。/ otel-config.yaml: / etc / otel-config。yaml港口:6831:6831狗:构建:。/服务/狗库存:构建:。/服务/库存犬舍:构建:。/服务/犬舍

我们不会添加任何JavaScript服务端口映射,我们不会直接访问它们,而是通过NGINX反向代理。我们还将建立一个opentelemetry-collector,我们将使用收集跟踪数据并将数据转发给我们的跟踪后端。

接收器:jaeger:协议:thrift_compact: thrift_http:处理器:批处理:出口商:日志:loglevel:调试otlp:端点:tempo-us-central1.grafana.net: 443头:授权:基本< Base64版本的用户名:api key >服务:管道:痕迹:接收器:(jaeger)处理器:(批)出口商:[otlp]

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

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

美元docker-compose了-

然后我们将做一个请求并检查日志跟踪id:

$ curl localhost " localhost / api / v1 /狗舍?id = 2”& &码头工人日志distributed-tracing-demo_kennels_1 […信息报告跨c3d3fa296e838a66: c3d3fa296e838a66:0:1

复制的第一部分跨度ID,c3d3fa296e838a66跟踪ID,并搜索Grafana。

好吧,这其实很酷了!我们能够看到我们前面谈到的因果关系模型,执行时间的百分比是用于每个跨度。

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

如何实现

实际上我们如何实现这个我们现有的项目呢?我完全得到,可能有多个原因为什么是不现实的,至少可以说,实现分布式的跟踪在你当前的项目中。除了要求开发人员积极仪器服务,它需要相当多的努力。

不要绝望!正确它很可能遥不可及,但获得的某个地方远不及耗费时间。JavaScript一样,对于许多流行的语言,Python和Java,可用的许多标准包已经自动仪器!

而不是什么都不做,先给他们一试!也许他们只是你需要追踪的性能问题的困扰你几个月吗?刚刚的时间为每个服务将帮助很大程度上归因于低效的代码!

你很快就会建立一个跟踪的例子的救了你的熏肉,和那些可用的,你就会拥有一个更强大的情况下获得所需要的组织支持继续迭代,建筑仪器,,慢慢地工作你的方式向“步入正轨”!

最简单的方法开始节奏和跟踪Grafana云。我们有免费和付费计划对每一个用例。注册一个免费帐户现在!