™技术博客

apm | 上下文管理器ContextManager

2021年6月14日

Span

Span在分布式追踪系统中一个重要且常用的概念。可从Google Dapper PaperOpenTracing学习更多与Span相关的知识。

Skywalking从2017年开始支持OpenTracingOpenTracing API; Skywalking中Span概念与谷歌Dapper论文 和 OpenTracing类似,但是也做了扩展。
Span有三种类型

  1. Entry Span:代表服务提供者,也是服务端的端点。作为一个APM系统,我们的目标是应用服务器。所以,几乎所有的服务和MQ-消费者都是EntrySpan。
  2. Local Span:代表普通的Java方法,与远程服务无关。不是MQ生产者/消费者,也不是服务(如 HTTP服务)的提供者/消费者
  3. Exit Span:代表一个服务的客户端或者MQ的生产者,在Skywaking早期被命名为LeafSpan。例如,通过JDBC访问DB 和 读取 Redis/Memcached 被归类为 ExitSpan

上下文载体 ContextCarrier

为了实现分布式跟踪,需要绑定跨进程追踪,并且上下文应该在整个过程中传播。这就是ContextCarrier职责。
以下是有关如何在 A->B 分布式调用中使用 ContextCarrier 的步骤。

  1. 在客户端创建一个新的空的ContextCarrier
  2. 调用 ContextManager#createExitSpan 创建一个ExitSpan 或者 使用 ContextManager#inject 初始化 ContextCarrier
  3. ContextCarrier所有信息放到请求头(如 HTTP HEAD)、附件(如 Dubbo PPC框架)或者消息(如 Kafka)中
  4. ContextCarrier通过服务调用传递到服务端
  5. 服务端从对应组件的头部、附件、消息中获取 ContextCarrier 所有内容
  6. 通过ContextManager#createEntrySpan 创建EntrySpan 或者 使用ContextManager#extract 来绑定服务端和客户端

让我们通过 Apache HTTPComponent client 插件 和 Tomcat7 服务端插件演示,步骤如下:

  1. 客户端 Apache HTTPComponent client 插件
    1
    2
    3
    4
    5
    6
    span = ContextManager.createExitSpan("/span/operation/name", contextCarrier, "ip:port");
    CarrierItem next = contextCarrier.items();
    while (next.hasNext()) {
    next = next.next();
    httpRequest.setHeader(next.getHeadKey(), next.getHeadValue());
    }
  2. 服务端Tomcat7 服务器插件
    1
    2
    3
    4
    5
    6
    7
    8
    ContextCarrier contextCarrier = new ContextCarrier();
    CarrierItem next = contextCarrier.items();
    while (next.hasNext()) {
    next = next.next();
    next.setHeadValue(request.getHeader(next.getHeadKey()));
    }

    span = ContextManager.createEntrySpan(“/span/operation/name”, contextCarrier);

    上下文快照ContextSnapshot

    除了跨进程,跨线程也是需要支持的。例如,异步进程(内存中的消息队列)和批处理 在Java中很常见。跨进程和跨线程十分相似,因为都是需要传播上下文, 唯一区别是,跨线程不需要序列化。
    以下是有关跨线程传播的三个步骤:
  • 使用ContextManager#capture() 方法获取 ContextSnapshot 对象
  • 让子线程以任何方式,通过方法参数或由现有参数携带来访问ContextSnapshot
  • 在子线程中使用 ContextManager#continued

核心APIS

上下文管理器ContextManager

ContextManager提供所有主要API

  1. 创建 EntrySpan
    public static AbstractSpan createEntrySpan(String endpointName, ContextCarrier carrier)
    根据操作名称(例如 服务名称、uri) 和 上下文载体ContextCarrier 创建EntrySpan

  2. 创建 LocalSpan
    public static AbstractSpan createLocalSpan(String endpointName)
    根据操作名称(例如 完整的方法前面)创建LocalSpan

  3. 创建 ExitSpan
    public static AbstractSpan createExitSpan(String endpointName, ContextCarrier carrier, String remotePeer)
    根据操作名称(例如 服务名称、uri),上下文载体ContextCarrier 以及对等端(peer)地址(例如 ip+port 或 hostname+port)创建ExitSpan

AbstractSpan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* Set the component id, which defines in {@link ComponentsDefine}
*
* @param component
* @return the span for chaining.
*/
AbstractSpan setComponent(Component component);

AbstractSpan setLayer(SpanLayer layer);

/**
* Set a key:value tag on the Span.
*
* @return this Span instance, for chaining
*/
AbstractSpan tag(String key, String value);

/**
* Record an exception event of the current walltime timestamp.
*
* @param t any subclass of {@link Throwable}, which occurs in this span.
* @return the Span, for chaining
*/
AbstractSpan log(Throwable t);

AbstractSpan errorOccurred();

/**
* Record an event at a specific timestamp.
*
* @param timestamp The explicit timestamp for the log record.
* @param event the events
* @return the Span, for chaining
*/
AbstractSpan log(long timestamp, Map<String, ?> event);

/**
* Sets the string name for the logical operation this span represents.
*
* @return this Span instance, for chaining
*/
AbstractSpan setOperationName(String endpointName);

除了设置操作名称、标签信息和日志之外,还要设置两个属性,即component组件和layer层。 特别是对于 EntrySpan 和 ExitSpan。
SpanLayer是Span的类别,有5个值

  1. UNKNOWN(默认值)
  2. DB
  3. RPC_FRAMEWORK(针对 RPC 框架,非普通的 HTTP 调用)
  4. HTTP
  5. MQ

组件 ID 由 SkyWalking 项目定义和保留。对于组件名称/ID 的扩展,请遵循组件库定义和扩展文档

特殊的 Span Tags

所有标签在跟踪视图都是可用的。 同时,在 OAP 后端分析中,一些特殊的标签或标签组合提供了其他高级功能。

Tag key status_code
该值应该是一个整数。 OAL 实体的响应码对应这个值。

Tag keys db.statementdb.type
db.statement 的值代表数据库语句,如SQL,如果值为空则为[No statement]/+span#operationName。当ExitSpan包含此标签时,OAP 会根据 agent-analyzer/default/maxSlowSQLLength 对慢SQL进行采样。慢SQL的阈值由agent-analyzer/default/slowDBAccessThreshold定义

扩展逻辑端点:Tag key x-le
逻辑端点是一个概念,不代表真正的 RPC 调用,但需要统计信息。 x-le 的值应该是 JSON 格式。 有两种选择:

  1. 定义一个分离的逻辑端点。 提供其自己的端点名称、延迟和状态。 适用于EntrySpan 和 LocalSpan。
    1
    2
    3
    4
    5
    {
    "name": "GraphQL-service",
    "latency": 100,
    "status": true
    }
  2. 声明代表逻辑端点的当前本地跨度。
1
2
3
{
"logic-span": true
}

高级API

异步 Span API

关于Span有一系列的高级API,他们都是在异步场景下使用的。 当Span的Tag,日志和属性(包括结束时间)需要在另一个线程中设置时,你就应该使用这些API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* The span finish at current tracing context, but the current span is still alive, until {@link #asyncFinish}
* called.
*
* This method must be called<br/>
* 1. In original thread(tracing context).
* 2. Current span is active span.
*
* During alive, tags, logs and attributes of the span could be changed, in any thread.
*
* The execution times of {@link #prepareForAsync} and {@link #asyncFinish()} must match.
*
* @return the current span
*/
AbstractSpan prepareForAsync();

/**
* Notify the span, it could be finished.
*
* The execution times of {@link #prepareForAsync} and {@link #asyncFinish()} must match.
*
* @return the current span
*/
AbstractSpan asyncFinish();
  1. 在原始上下文中调用 #prepareForAsync
  2. 将Span传递到其他线程
  3. 在全部操作就绪之后,可在任意线程中调用``#asyncFinish` 结束调用。
  4. 当所有Span的#prepareForAsync完成后,跟踪上下文会结束,并一起被回传到后端服务(根据API执行次数判断)
Tags: apm

扫描二维码,分享此文章