写点什么

深入可观测底层:OpenTelemetry 链路传递核心原理

  • 2022-06-22
  • 本文字数:5983 字

    阅读完需:约 20 分钟

深入可观测底层:OpenTelemetry 链路传递核心原理

前言


本文会系统讲解链路传递一些基本概念,同时结合案例讲解链路传递的过程。

Context、Propagator 的概念


我们先看一张在 HTTP 通信下,进行链路传播的例子。



图中是一个 Client 请求 Server 通信,同时 Client 把自己的链路信息传递给 Server 的过程

可以看到,链路信息通过一个叫 TraceContext 对象封装起来,通过 Http Headers

来存取这个对象,最后达到传播的效果,这里面 TraceContext 就是一个 Context 上下文对象。

 

Context 是一种传播机制,它跨越 API 边界,在逻辑执行单元间传递执行范围的值。链路追踪系统在系统进程内部,进程间通过共享进程的 Context 上下文 对象传递链路信息。


刚才 Http Headers 起到了一个传递 Context 载体作用,在链路追踪系统,我们把这样的载体统称为 Propagators。上面传递过程我们用另一张图看看 Propagators 和 Context 的关系:

 


什么是 Propagator?OpenTelemetry 是这样定义的


Cross-cutting concerns send their state to the next process using Propagators,  which are defined as objects used to read and write context data to and from messages exchanged by the applications.


大概意思,横切关注点通过它,在进程间进行状态的传递。这里,又牵涉了一个重要的知识点。

Cross-cutting concerns 的概念


关注点:如果系统按功能划分,关注点就是系统的核心功能。 横切关注点:从整个系统横向维度看到的核心功能。比如日志功能就是横切关注点的一个典型案例。日志功能往往横跨系统中的每个业务模块。图可能比文字来得更形象:



横切关注点(系统基本功能)通过 Propagator 可以传递它的状态给下一个进程,Propagator 是通过应用程序读写上下文数据进行数据的交换。 每个关注点都包含了一组 Propagator Propagator 把每一个横切关注点通过上下文去 Injection 注入和 Extract 提取。

 

链路传递一般通过特定的请求拦截器库来实现,拦截器通过拦截输入和输出请求,利用对 Propagator 的注入和提取操作来实现传递。


Propagator API 用户可以通过检测库来实现 Propagator 操作的 API。

Baggage 链路传递初探


在一个链路追踪过程中,我们一般会有多个 Span 操作, 为了把调用链状态在 Span 中传递下去,期望最终保存下来,比如打入日志、保存到数据库。SpanContext 会封装一个键值对集合,我们叫它Baggage .

Baggage 会在一条追踪链路上的所有 Span 内全局传输。原理是上一个 Span 将 Context 中 Baggage 注入到 Propagator ,Propagator 和目的 Span 联系,从 Propagator 提取出Baggage ,再写入到目的 Span 的 Context 中。


OpenTelemetry 中关于 Baggage 原文定义:

Baggage is used to annotate telemetry, adding context and information to metrics, traces, and logs. It is a set of name/value pairs describing user-defined properties.


通过 Baggage 我们可以实现强大的追踪功能。例如:在用户的手机端添加一个 Baggage 元素,并通过层层服务调用,传递到最后数据库的操作。有了 Baggage,如果我想定位请求中消耗很大的 SQL 语句是来自于具体哪个手机端用户从 Trace 中一看便知。反过来,我们可以知道一个手机端用户它依次做了哪些 SQL 语句查询。这比你登录服务器一台台找前端、后端日志,还要把他们关联起来分析,要容易很多。


方便理解,我们演示 Baggage 效果:


首先,我们在 loadBalancer 请求中加一个 Baggage,loadBalancer 请求了 resource 服务:

@GetMapping("/loadBalancer")@ResponseBodypublic String loadBalancer(String tag){    Span span = Span.current();          //保存 Baggage	Baggage.current()	  .toBuilder()	  .put("app.username", "蒋志伟")	  .build()	  .makeCurrent();......    ##请求 resourcehttpTemplate.getForEntity(APIUrl+"/resource",String.class).getBody();  
复制代码


然后我们从 resource 服务中获取 Baggage 信息,并把它存储到 Span 的 Attributes 中:

@GetMapping("/resource")@ResponseBodypublic String resource(){	String baggage = Baggage.current().getEntryValue("app.username");	Span spanCur = Span.current();     ##获取当前的 Span,把 Baggage 写的 resource		spanCur.setAttribute("app.username",                          "baggage 传递过来的 value: "+baggage);
复制代码


最终,我们从跟踪系统的链路 UI 中点击 resource 这个 Span,找到传递的 Baggage 信息:

图五:展示 Baggage 的传递

 

当然,Baggage 拥有强大功能,也会有很大的消耗。由于 Baggage 的全局传输,每个键值都会被拷贝到每一个本地(local)及远程的子 Span,如果包含的数量量太大,或者元素太多,它将降低系统的吞吐量或增加 RPC 的延迟。

Tag 链路标签和业务监控


我们进行系统链路追踪,除了 Trace 本身自带信息,如果我们还希望添加自己关注的监控。Trace 支持用打标签 Tags 方式来实现。Tags 本质还是 Span 的 Attributes(在 OpenTelemetry 定义中,统称 Attributes。在 Prometheus、Jaeger 里面沿袭 Tags 老的叫法)。

 

打 Tags 的过程,其实就是在 Span 添加我们自定义的 Attributes 信息,这些 Tags 大部分和我们业务息息相关,为了更方便做业务监控、分析业务。

 

我们看一个 Java 打 Tags 的例子:页面定义好了一个 Tag,名字叫"username"。我们输入 Tags 的值,然后把 Tags 通过一个 HTTP 请求发送给付账单的 API,


图六:打 Tags 的演示


 API 获取 Tags 后,把它保存到当前 Span 的 Attribute 中。这个 Span 对应的是代码里面的一个 gateway 方法, 如果不重名 Span 名称,默认使用 gateway 作为 Span 名称:

@GetMapping("/loadBalancer")public String gateway(String tag){   Span Span = Span.current();          ##获取当前 Span,添加 username 的 tag   Span.setAttribute("username", tag);      ...... }   
复制代码


打了 Tags 后,我们可以在跟踪系统搜索 Tag 关键字。通过 Tag 可以快速找到对应的 Trace:


图七:基于 Tags 的搜索

 

可以看到,根据 Tags 的 key 我们可以很方便筛选想要的 Span 信息。实际场景里,我们面临是从成千上万链路中快速定位访问异常的请求。打 Tags 对我们诊断程序非常有帮助。


我们从 Trace 中找到相应的 Span,能看到 Tag 已经注入到 gateway 的 Span 中:


图八:Tags 的用法


 Baggage 和 Span Tags 的区别


  • Baggage 在全局范围内,(伴随业务系统的调用)在所有 Span 间传输数据。Tags 不会进行传输,因为他们不会被子级的 Span 继承;

  • Span 的 Tags 可以用来记录业务相关的数据,并存储于追踪系统中。

Event 事件 监控更细微的链路行为


一个 Span 下可以添加多个事件,事件是相对于 Span 更小的操作粒度。在我们想知道一个 Span 内各种操作,这种操作期望在监控平台能够分析,Event 就可以满足需求。

 

比如请求一个商品结算的 API,这个 API 调用商品结算方法 Count,Count 里面分别做了查询商品库存,核实商品价格,结算总价这几个步骤,商品库存还是由几次数据库的 SQL 查询聚合的结果。如果这个 API 有异常,查看商品结算这个 Span 往往远远不够,在真实的线上场景,我们还需要追踪每个步骤的请求状态和详细日志。


事件和 Span 的数据结构类似,包含的基本属性:

  • 事件名称;

  • 事件时间:它支持由开发者自定义添加时间,让事件时间更精准;

  • 属性 Attributes:和 Span 一样,用来记录事件相关信息。

	@GetMapping("/event")		public String event() {			Span span = Span.current();			span.updateName("创建eventDemo");				// 手动更新Event持续时间			span.addEvent("time.update", System.currentTimeMillis() + 2000, TimeUnit.MILLISECONDS);			// 给Event添加相关信息			Attributes appInfo = Attributes.of(AttributeKey.stringKey("app.id"), "123456",					AttributeKey.stringKey("app.name"), "应用程序demo");			span.addEvent("appinfo.query", appInfo);			logger.info("this is a event");			return buildTraceUrl(span.getSpanContext().getTraceId());		}
复制代码


我们添加 Event 后,从链路中看到 Event 信息记录在 Trace Logs 中。在事件中,我们还能添加相关 Attributes ,记录事件某些信息:



OpenTelemetry 链路数据如何传播  

Carrier 和 Propagator 的关系

在 OpenTelemetry 中,Trace 的传递中有一个核心的概念,叫 Carrier(搬运工具)。它是 Propagator 用来读取 Context 数据的一种媒介 medium,Carrier 数据格式可能是一个字符 Map 或者一个字符数组。


Carrier 充当"搬运" Span 中 SpanContext 工具。例如 OpenTelemetry 中为了把 Trace 的 Span 信息传递下去,在 HTTP 调用场景中,会有 HttpCarrier,在 RPC 的调用场景中会有 RpcCarrier 来搬运 SpanContext。Trace 通过 Carrier 可以把链路追踪状态在进程中、进程间传递。

数据传播基本操作


回顾刚才一些概念,我们完整描述下 OpenTelemetry Span 在传播中有的基本步骤:


  • StartSpan:Trace 在具体操作中自动生成一个 Span

  • Inject 注入: 将 Span 的 SpanContext 写入到 Carrier 的过程


链路数据为了进行网络传输,需要数据进行序列化和反序列化。这个过程 Trace 通过一个负责数据序列化反序列化上下文的 Formatter 接口实现的。例如在 HttpCarrier 使用中通常就会有一个对应的 HttpFormatter。所以 Inject 注入是委托给 Formatter 将 SpanContext 进行序列化写入 Carrier。


Formatter 提供不同场景序列化的数据格式,叫做 Format 描述。比如:


  • TextMap: 基于字符串的 Map 记录 SpanContext 信息,适用 RPC 网络传输

  • HTTP Headers: 方便解析 HTTP Headers 信息,用于 HTTP 传输


一个 Python 程序实现 Inject 注入过程,Formatter 序列化 SpanContext 成 Text Map 格式:

##Trace 生成一个span    tracer = Tracer()    span = tracer.start_span(operation_name='test')    tracer.inject(        span_context=span.context,        format=Format.TEXT_MAP,        carrier=carrier)
复制代码

 

  • Extract 提取: 将 SpanContext 从 Carrier 中 Extract(提取出来):

span_ctx = tracer.extract(format=Format.TEXT_MAP, carrier={})
复制代码


同理,从 Carrier 提取的过程也需要委托 Formatter 将 SpanContext 反序列化。

运行原理


图一:链路数据在 HTTP 传递

 

我们基于 HTTP 通信解释传播原理。由图一,这个过程大致分为两步:


1、发送端将 SpanContext 注入到请求中,相应伪代码实现:

/**** 将 SpanContext 中的 TraceId,SpanId,Baggage 等根据 format 参数注入到请求中(Carrier)** carrier := opentracing.HTTPHeadersCarrier(httpReq.Header)** err := Tracer.Inject(Span.Context(), opentracing.HTTPHeaders, carrier)**/Inject(sm SpanContext, format interface{}, carrier interface{}) error
复制代码


2、接收端从请求中解析出 SpanContext,相应伪代码实现:

// Inject() takes the `sm` SpanContext instance and injects it for// propagation within `carrier`. The actual type of `carrier` depends on/** 根据 format 参数从请求(Carrier)中解析出 SpanContext(包括 TraceId、SpanId、baggage)。** 例如: **  carrier := opentracing.HTTPHeadersCarrier(httpReq.Header)**  clientContext, err := Tracer.Extract(opentracing.HTTPHeaders, carrier)**/Extract(format interface{}, carrier interface{}) (SpanContext, error)
复制代码


Carrier 负责将追踪状态从一个进程"Carry"(搬运)到另一个进程。对于一个 Carrier,如果已经被 Injected,那么它也可以被 Extracted(提取),从而得到一个 SpanContext 实例。这个 SpanContext 代表着被 Injected 到 Carrier 的信息。

 

说到这里,你可能想知道这个 Carrier 在 HTTP 中具体在哪。其实它就保存到 HTTP 的 Headers 中。而且,W3C 组织为 HTTP 支持链路追踪专门在 Headers 中定义了 Trace 标准:

https://www.w3.org/TR/trace-context/#trace-context-http-headers-format

W3C 组织是对网络标准制定的一个非盈利组织,W3C 是万维网联盟的缩写,像 HTML、XHTML、CSS、XML 的标准就是由 W3C 来定制

跨进程间传播数据


数据传播按照场景分为两类:进程内传播、跨进程间传播 Cross-Process-Tracing。


进程内传播是指 Trace 在一个服务内部传递,监控了服务内部相互调用情况,相当比较简单。追踪系统最困难的部分就是在分布式的应用环境下保持追踪的正常工作。任何一个追踪系统,都需要理解多个跨进程调用间的因果关系,无论他们是通过 RPC 框架、发布-订阅机制、通用消息队列、HTTP 请求调用、UDP 传输或者其他传输模式。所以业界谈起 Tracing 技术 往往说的是跨进程间的分布式链路追踪(Distrubute Tracing)。

 

我们用 OpenTelemetry 实践一个 HTTP 通信的 Trace 例子:


这是一个 Java 程序,我们向下游服务发起一个 HTTP 请求。


程序中使用 OpenTelemetry 的 inject 注入,通过 HTTP Headers 把 Trace 传递给 127.0.0.1:8080 的服务。传播前,手动还创建两个想要一块传播的 Attributes:

@GetMapping("/contextR")@ResponseBodypublic String contextR() {	TextMapSetter<HttpURLConnection> setter = new TextMapSetter<HttpURLConnection>() {		@Override		public void set(HttpURLConnection carrier, String key, String value) {			// 我们把上下文放到 HTTP 的 Header			carrier.setRequestProperty(key, value);		}	};	Span spanCur = Span.current();	Span outGoing = tracer.spanBuilder("/resource").setSpanKind(SpanKind.CLIENT).startSpan();	try {		URL url = new URL("http://127.0.0.1:8080/resource");		HttpURLConnection transportLayer = (HttpURLConnection) url.openConnection();		outGoing.setAttribute("http.method", "GET");		outGoing.setAttribute("http.url", url.toString());					// 将当前 Span 的上下文注入到这个 HTTP 请求中		OpenTelemetry.getPropagators().getTextMapPropagator().inject(Context.current(), transportLayer, setter);		// Make outgoing call...
复制代码


运行程序,从监控平台我们看到,Trace 从一个程序成功传递到了 127.0.0.1:8080 的服务:


跨进程传播数据

Github 中文文档


我们 OpenTelemetry Github 社区对链路传播中文介绍,目前还在逐步完善当中。也欢迎大家去贡献更多的中文内容。


  • Propagator 介绍

https://github.com/open-telemetry/docs-cn/blob/main/specification/context/api-propagators.md

  • Context 介绍

https://github.com/open-telemetry/docs-cn/blob/main/specification/context/context.md

  • Baggage 介绍

https://github.com/open-telemetry/docs-cn/blob/main/specification/baggage/api.md

 

作者介绍

蒋志伟,爱好技术的架构师,先后就职于阿里、Qunar、美团,前 pmcaffCTO,目前 OpenTelemetry 中国社区发起人。

欢迎关注我们的 Github 项目:https://github.com/open-telemetry/docs-cn

欢迎大家关注“OpenTelemetry”公众号,这是中国区唯一官方技术公众号。

 

2022-06-22 15:3111881

评论

发布
暂无评论
发现更多内容

书单|互联网企业面试案头书之程序员技术篇

博文视点Broadview

如何通过 Serverless 提高 Java 微服务治理效率?

阿里巴巴云原生

Java Serverless 容器 微服务 云原生

USB2.0 扩展器(一拖四)原理图、PCB,可打样使用

不脱发的程序猿

28天写作 电路设计 USB电路 USB转TTL 3月日更

寻找被遗忘的勇气(九)

Changing Lin

3月日更

为什么我们开发 San 项目时要用 CLI?

百度Geek说

service SLI san command

为什么我们开发 San 项目时要用 CLI?

百度开发者中心

深度分析前端构建工具:Vite2 v.s Snowpack3 v.s. Webpack5

智联大前端

vite webpack 构建工具

京东云新一代自研云服务器 4 月上线;COLING 2020丨面向机器阅读理解的双向认知思维网络

京东科技开发者

人工智能 开发者 云服务器

PT100热电阻温度阻值对应表

不脱发的程序猿

数据分析 28天写作 PT100 3月日更 温度传感器

AI不仅可以把李焕英带回2021,还能告诉你贾玲更像爸爸还是妈妈

京东科技开发者

人工智能 语音识别 语音合成

低代码开发平台解决方案之“金融服务行业”篇

优秀

低代码

从产品经理到产品架构师

博文视点Broadview

面试看这个就够了!最新BAT大厂面试者整理的Android面试题目模板,先收藏了

欢喜学安卓

android 程序员 面试 移动开发

区块链电子合同签署平台,区块链电子存证

13530558032

干货分享丨从MPG 线程模型,探讨Go语言的并发程序

华为云开发者联盟

并发 channel goroutines MPG 线程 Go 语言

AI辅助宫颈癌筛查技术全球居首,守护者的力量来源是?

华为云开发者联盟

AI 华为云 目标检测 宫颈癌

能源管理可视化破冰而出,数字孪生打破传统运维僵局

一只数据鲸鱼

物联网 数据可视化 3D可视化 能源管理 智慧电厂

币宽量化交易软件开发|币宽炒币机器人系统APP开发

系统开发

区块链+版权-助力电子微版权保护

13530558032

BFAI量化交易系统开发|BFAI炒币机器人APP软件开发

系统开发

盘点 HashMap 的实现原理及面试题

老王说编程

Java hashmap HashMap底层原理

中国人工智能,赏花更要寻根

脑极体

Java面试热门技术框架:Spring Security Oauth2.0认证授权

Java架构追梦

Java spring 面试 金三银四跳槽

如何凝聚党员力量?智慧组工系统构架组织部管理平台解决方案

源中瑞-龙先生

解决方案 党员 智慧组工

小鼎量化交易系统开发|小鼎炒币机器人软件APP开发

系统开发

《谷歌是如何运营的》-读书笔记

曦语

读书笔记

如果延迟退休势在必行,区块链如何助力“养老助老”?

旺链科技

产业区块链

【LeetCode】删除字符串中的所有相邻重复项Java题解

Albert

算法 LeetCode 28天写作 3月日更

LinqToExcel.Extend 源码分析 第二波

happlyfox

28天写作 3月日更

当跨国企业女职业经理人遇上创业女 CEO,两者会擦出什么样的火花?

科技新消息

JVM笔记 -- JVM的生命周期介绍

秦怀杂货店

JVM 生命周期

深入可观测底层:OpenTelemetry 链路传递核心原理_语言 & 开发_蒋志伟_InfoQ精选文章