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

知乎 · · 1101 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。


前言


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

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")
@ResponseBody
public String loadBalancer(String tag){
    Span span = Span.current();      
    //保存 Baggage
	Baggage.current()
	  .toBuilder()
	  .put("app.username", "蒋志伟")
	  .build()
	  .makeCurrent();
......
    ##请求 resource
httpTemplate.getForEntity(APIUrl+"/resource",String.class).getBody();

复制代码


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

@GetMapping("/resource")
@ResponseBody
public 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 标准:

w3.org/TR/trace-context
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")
@ResponseBody
public 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 介绍
github.com/open-telemet
  • Context 介绍
github.com/open-telemet
  • Baggage 介绍
github.com/open-telemet


作者介绍

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

欢迎关注我们的 Github 项目:github.com/open-telemet

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

本文来自:知乎

感谢作者:知乎

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

1101 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传