微服务 - 应用性能监测 · 链路追踪 · 概念规范 · 产品接入 · 方法级追踪 · 创建指标跨度

系列目录
微服务 - 概念 · 应用 · 架构 · 通讯 · 授权 · 跨域 · 限流
微服务 - Consul集群化 · 服务注册 · 健康检测 · 服务发现 · 负载均衡
微服务 - Redis缓存 · 数据结构 · 持久化 · 分布式 · 高并发
微服务 - Nginx网关 · 进程机制 · 限流熔断 · 性能优化 · 动态负载 · 高可用
微服务 - 应用性能监测 · 链路追踪 · 概念规范 · 产品接入 · 方法级追踪 · 创建指标跨度

本文从 [场景/概念/定义],演变的统一规范,再到滋生出的各类开源产品,产品的接入,应用案例等的阐述。

涉及到的组件及版本:.NET 6,OpenTelemetry v1.6,SkyWalking v8.9.1,Jaeger v1.51,Prometheus v2.48

一、概念及定义

1.1 面临的场景

现代互联网服务通常被开发成复杂的、大规模的分布式系统。在分布式系统中,一次外部请求往往需要内部多个模块,多个中间件,多台机器或第三方等相互调用才能完成。在这一完整的调用过程中,有串行执行的,也有并行执行的,有关业务走向的,有关处理能力的等等。

随着微服务和云原生开发的兴起,越来越多应用基于分布式进行开发,但是大型应用拆分为微服务后,服务之间的依赖和调用变得越来越复杂,这些服务是由不同团队开发的软件模块集合构建的,可能使用不同的编程语言,并且可能跨越多个物理设施中的数千台机器,他们之间提供的接口可能不同(RPC/API等)。在这种情况下,我们如何才能快速的确定某次请求都调用了哪些应用?哪些模块?哪些节点?以及它们的先后顺序及各部分的性能如何呢?

1.2 Observability

Observability 可观测性是指如何通过检查系统的运行数据,来了解系统的内部状态。它从各种来源收集和分析数据,以针对环境中运行的应用程序的行为提供详细见解。它可以应用于任何您构建的并希望进行监测的系统。

可观测性很重要的原因在于 [发现],借助可观测性,可让软件工程师、IT、DevOps 和项目团队解读遥测数据。借助仪表板、服务依赖关系图和分布式跟踪等可视化功能,甚至 AI 和机器学习方法,轻松完成。有了合适的可观测性解决方案,就可以了解应用程序、服务和基础架构在跟踪和响应问题方面的表现。让团队评估、监测和改进分布式 IT 系统的性能。甚至可以主动诊断、分析问题,并追溯问题根源。

1.3 应用程序性能监控

Application Performance Monitoring (APM),观测的一种方式,可观测性的子集。APM 解决方案可收集和监测来自各种网站、软件应用程序和服务的遥测数据,并对数据进行分析。是组织快速识别和解决应用程序和代码中任何性能问题的过程。

APM 会使用一套工具和方法来监测和管理软件应用程序的性能。APM 工具一般包括对关键指标的监测,以此来识别和诊断性能瓶颈和问题。一些关键的指标例举:请求率/错误率/响应时间/服务实例数/硬件利用率等。设定各指标的范围(如内存剩余低于20%时),做出响应告警。

如下图监控指标示例:

APM 还可以提供详细的分布式链路追踪和故障排查信息,也是解决以上面临的场景提出来的问题,将每次分布式请求过程中的每个环节的执行情况,按序/耗时/状态/并串型等,很直观的集中展示出来。协助开发人员了解和修复代码中的问题,快速排查及隐患及预估处理能力等。这通常包括告警和报告功能,以让相关方始终了解应用程序的性能。这种多个节点串联起来的请求过程称为 Tracing。

如下图所示的 Trace、Span、Time 关系图:

Tracing 链路追踪是一种用于分析和监视应用程序的方法,尤其是那些使用微服务体系结构构建的分布式的应用程序。一个完整请求链路的追踪(TraceID)用于查出本次请求调用的所有服务/接口/组件等,调用的每个服务/接口/组件等都被称为跨度(Span),用来记录调用顺序,上游跨度(ParenetID)用来记录调用的层级关系。调用时间周期Timestamp,是把请求发出、接收、处理的时间都记录下来。跨度还可以记录一些其它属性信息,比如发起调用服务名称、被调服务名称、返回结果、IP、请求状态、日志、故障等。最后再把拥有相同(TraceID)的跨度(Span)合成一个更大范围的试图,就形成了一个完整的单次请求调用链。
通过以上提到的 [监控] [链路],APM 能够对应用程序的执行情况提供连续不断的详细见解。团队可以利用这些见解,更加积极主动地解决问题,而不是等到客户投诉了才有所行动。APM 有多种用途。例如,团队可以针对用户体验过程中的性能下降设置告警,衡量最新版本的影响,并就哪些地方需要改进做出明智的决策。
总结起来,它可以使得我们发现很多不曾注意到的细节,发现隐蔽的循环依赖,及早的发现问题,定位问题,优化改进,也可以帮助新人理解业务等等。

作者:[Sol·wang] - 博客园,原文出处:https://www.cnblogs.com/Sol-wang/

二、发展及产品

2.1 最初概念

从最初 Google 公司在 Dapper 中提到的 trace、span、annotation 概念:

  • Trace:一次完整的分布式调用跟踪链路;
  • Span:跨服务的一次调用,多个Span组合成一次Trace记录;
  • Annotation:用来记录请求特定事件的相关详细信息及补充;

但 Google-Dapper 并没有开源,它是 Google 内部长期经过打磨后形成的产品,于2010年公布,对外是一篇论文,讲述的是分布式链路追踪的理论和 Dapper 的设计思想。大致由 [植入应用、收集跟踪数据、图形化UI] 三部分组成。后续市场的发展,有很多链路追踪系统也是基于 Dapper 论文的思想和理论为基础的。个人觉得 Google-Dapper 实现了两大方面:监控(发现细微的变化)、跟踪(及时定位并处理)。

2.2 统一规范

|---------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| | 于2016年开始的 OpenTracing 项目得到绝大多数相关团队的认可,为分布式追踪,提供统一的概念、规范和接口。它是一个轻量级的标准化层,并不是功能实现代码,它只是为跟踪数据,用代码定义了一套数据模型,和一套API,是供统一遵循的规范,用于在应用程序中创建和管理这些数据模型。现在大多数链路跟踪产品系统都在尽量兼容遵循 OpenTracing 设计原则。 |

🗂️ OpenTracing 的模型定义

  • Span:基本单元,链路中的每个组件都是一个Span,代表工作流中的一部分,或请求过程中的单个动作。
  • Tags:键值对,用于表示Span,便于查询、筛选分类、理解及跟踪数据。应用于整个跨度。例如表示本节点的主机/响应/端口等。
  • Logs:键值对,可用于捕获应用程序本身的定时日志消息和其他调试或记录信息的输出。日志可用于记录跨度内的特定时刻或事件。
  • Tracer:用户端单次请求所涉及到的所有的 Span 及之间的关系,组合成一个完整的 Span 网,称为一个 Tracer。
  • Carrier:节点间传递数据的模型。跨端点/跨服务等的传递的随行数据,如 trace_id/span_id 等,也可自定义追加数据。

OpenTracing 仅包含 Model 和 API 的定义,不会将产生的数据发送到第三方;需要进一步集成第三方的SDK,发送到第三方并呈现。

🗂️ 热门替代品 OpenTelemetry

|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| | 基于 OpenTracing,新项目 OpenTelemetry 是 OpenTracing + OpenCensus 的产物,简称 OTel。相当于升级版,它是一个,包含一组用于分布式跟踪和监视的工具的集合,支持集成更多的框架和语言。它仅用来生成/收集和导出更多种遥测数据(指标/链路/日志),可将数据发送到任何可观测性后端进行分析。旨在提供一种检测跟踪代码的标准方法,收集有关通过系统的请求流的数据,以帮助分析软件的性能和行为。同样,数据存储和可视化呈现留给其它工具去完成。 |

OpenTelemetry 正在快速成为云原生应用程序领域内主导的可观测性遥测数据标准。如果组织想要做好准备以满足未来的数据需求,而且不想被锁定到某一特定供应商,也不想受限于其既有技术,那么采用 OpenTelemetry 对其至为关键。

看看 OpenTelemetry 官网的自述:High-quality, ubiquitous, and portable telemetry to enable effective observability.

截止本文编写时间,目前在CNCF开源项目中热度长期排名第二。

OpenTelemetry 库集成于项目中,负责[采集/处理/发送]遥测数据给到第三方UI显示,那么遥测数据包含哪些呢???

🗂️ 什么是 Telemetry 遥测数据

经过不断地演变进化,遥测数据分为三大类:

  • Metrics 是系统在一段时间内某方面某度量的持续变化,得到系统及各方面的总负载情况,对于了解系统的整体性能非常有用。
  • Tracing 每次请求所经过的节点,编织成一个树形试图,其中包含依赖/耗时/状态等,这对于了解系统原理及查找系统瓶颈非常有用。
  • Logging 是记录系统行为的离散事件。记录处理过程/错误信息等,是对试图节点的一种扩充,通常用于调试和排查应用程序中的问题。

这三类数据被称为<可观测性的支柱>,能够帮助开发者、DevOps 和 IT 团队理解其系统的行为和性能。

🗂️ OpenTelemetry 的架构和组件

OpenTelemetry 定义了 Metrics/Tracing/Logging 的数据模型,又完成了对这些数据的采集、处理和导出。如何完成这些数据流转,这也是我们接下来重点介绍的部分。

我们可以用 OpenTelemetry 的 ConsoleExporter 方式打印出来,不妨先来看看遥测数据的真容:

OpenTelemetry 库集成在项目上,并将产生的遥测数据开放给第三方,遵循 OpenTelemetry 的第三方提取到数据后,经过后端服务各类聚合计算等,于第三方 UI 中以二维图等呈现结果。详细实现过程关注 [第三章 - 实现原理介绍]。

2.4 市场产品

既然是被市场认可遵循的统一概念、统一规范、统一API,那么会有更多的产品支持或集成。

  • 如:内部已集成的产品 Nginx/Mysql/K8s/ES/Redis/MongoDB/Kafka/MQ 等等
  • 如:可进行数据上报接入的链路追踪产品 Zipkin/Jaeger/SkyWalking/ElasticAPM 等等

对于开发人员来讲,重要的是数据上报接入产品,或者产品自带数据上报,或者侵入式,或者免侵入。

  • 侵入式,要在原项目中为指标日志等做更多的数据埋点(计数或跨度等),对原项目代码变动较大
  • 无浸入比如 SkyWalking,仅配置接入,无需手动做埋点,产品已支持了对譬如CPU/线程/DB的监控等
  • 用 OpenTelemetry 中的模型规范等手动实现数据上报到产品的如 Jaeger,Jaeger接收并计算数据,直观的呈现给用户
  • 产品自带(通常为 Agent 的)SDK,实现自动监控并上报数据到产品的如 SkyWalking, 将 Agent 集成到项目中,上报给后端并UI呈现

近几年,相当多的产品 APM 是基于 OpenTelemetry 之上开发的工具。对于 APM 工具可直接应用即可,通常功能全面,比如自带了非常多的指标监测。对于 OTel 库仅提供基础的 Model 和 API,更多的常用功能需要敲代码完成。直接应用优秀的 APM 工具当然就省时省力。

三、实现原理介绍

本章阐述以 OpenTelemetry 的数据收集与发送的实现,也就是模型和API的用法。主要目的是将重要的部分,单独拿出来说,从概念到实践,用来了解基于 OTel 产品的运行机制。其重要性在于了解了 OTel 的实现过程后,基于 OTel 的产品都可顺理成章的应用对接。

3.1 Metrics 的实现方式

3.1.1 .NET 内置指标

如下拼图示例,所以 OpenTelemetry 只要从以下类中采集数据即可,导出数据给第三方绘制成二维图表呈现。

甚至您可通过 dotnet 命令dotnet-counters查看项目时时运作状况,提供基本的CPU/内存/线程/GC等监控数据,如下图所示:

这也是第三方产品如 SkyWalking-UI 常展示的图表数据。

3.1.2 创建自定义指标

有了以上内置指标,也不能满足我们面对所有业务场景的需求,就需要个性化的新指标来补充。

为了帮助我们理解自定义 Metrics 的运作,在以下案例中,我们借助一些现有产品:OpenTelemetry (data) + Prometheus (UI) 的方式,从代码创建到产生指标数据的过程。

首先,在项目中集成最基本的 OpenTelemetry & Prometheus 库

<ItemGroup>
    <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.6.0" />
    <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.6.0" />
    <PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.6.0-rc.1" />
</ItemGroup>

其次,定义指标类,如下代码示例:

定义的指标名称为 [MY-天气预报]

定义为计数型名称为 [MY-Weather-Forecast-Count]

# 自定义指标:统计天气预报的次数
public class WeatherForecastMetrics {
    /// 发布预报的次数
    private readonly Counter<int> _forecast;
    
    /// 构造
    public WeatherForecastMetrics() {
        var meter = new Meter("MY-天气预报", "0.1.0");
        // ★ 创建一个技术型指标
        _forecast = meter.CreateCounter<int>("MY-Weather-Forecast-Count", description: "统计天气预报的次数");
    }
    
    /// 累计次数
    public void Add(int forecast) {
        _forecast.Add(forecast);
    }
}

指标种类有多种,[ 计数 / 计量 / 柱状 / Up-Down ] 等等。

往下,在控制器中引入计数器,重点为 ★ 部分

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase {

    private static readonly string[] Summaries = new[] {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    /// <summary>
    /// ★ 引入 自定义计数型指标 天气预报指标类
    /// </summary>
    private readonly WeatherForecastMetrics _forecast;
    
    public WeatherForecastController(WeatherForecastMetrics forecast) {
        _forecast = forecast;
    }
    
    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        _forecast.Add(1);    // ★ 方法中加入 预报的计数器,也就是埋点
        
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }).ToArray();
    }
}

最后,在 Program 中做如下服务配置,重点为 ★ 部分

# Program.cs
# ... 省略 ...
// 把我们的 自定义天气预报指标类 先配进去
builder.Services.AddScoped<WeatherForecastMetrics>();
//
// 引入 OpenTelemetry 库
var otel = builder.Services.AddOpenTelemetry();
// 配置服务名称
otel.ConfigureResource(resource => resource.AddService(serviceName: my_serv_name));
// ★ 配置自定义指标 MeterName
otel.WithMetrics(metrics =>
{
    metrics.AddAspNetCoreInstrumentation()
    .AddMeter(WeatherForecastMetrics.MeterName)
    .AddPrometheusExporter();
});
# ... 省略 ...
var app = builder.Build();
// ★ 定义对外获取指标数据的接口
// 默认:/metrics,也可以自定义参数
app.MapPrometheusScrapingEndpoint();
# ... 省略 ...

所以以上总结起来:创建指标计数器 → 埋入方法中 → 运行时收集数据 → 开放取数据的接口。

到这里,既然可以收集到数据,那么我们先 Run 起来项目,手动预报几回天气(请求天气接口),看看是不是已经有了 指标数据。

Prometheus 提供了一个对外可采集 Metrics 数据的接口 /metrics,我们也可以通过这个API来看看返回的数据

巧了,天气预报指标名称和次数赫然在列。(上半部自带指标数据统计的是啥,这里不关心了)

3.1.3 对接 UI 呈现指标数据

本小节,将把上节创建的指标所产生的数据用工具 Prometheus-UI 的方式呈现在我们面前。

部署方式这里测试用 Docker 镜像

docker run -dit --name my-prometheus -p 9090:9090 --restart unless-stopped -e TZ=Asia/Shanghai prom/prometheus:v2.48.0

唯一注意的部分是配置文件里的数据采集地址

# ... 其它省略 ...
scrape_configs:
  # 自定义的采集作业名称
  - job_name: "My-OTel-Prometheus"

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.
    # 配置指标采集地址
    static_configs:
      - targets: ["13.13.1.1:5203"]
# ... 其它省略 ...

Prometheus 的采集地址 对准 项目中 OpenTeletemry 提供的指标数据接口 /metrics,之后使其生效。

好了,浏览器打开 Prometheus 页面 http://{host}:9090,看看我们自定义的指标数据会怎样。。。

巧了,赫然在列

点开右侧列表图标,选择自定义的指标计数器名称,将呈现如上时时图表。

至此,本小节讲述了:.NET 内置指标、自定义指标创建、指标数据的呈现。

3.2 Tracing 的实现方式

同样的,为了帮助我们理解 Tracing,在以下案例中,我们借助一些现有产品:OpenTelemetry (data) + Jaeger (UI) 的方式,从代码创建到产生链路数据,将数据推送到 Jaeger 服务的实现过程。

那么,首先把 Jaeger Run 起来吧,按官网提供的测试环境 Docker 镜像 All-In-One。

docker run -dit --name my-jaeger -p 6831:6831 -p 4317:4317 -p 16686:16686 -e COLLECTOR_OTLP_ENABLED=true --restart unless-stopped jaegertracing/all-in-one:1.51

着重介绍两个端口:数据接收 4317 / 页面访问 16686。

此镜像默认关闭 OpenTelemetry 协议的数据传输,COLLECTOR_OTLP_ENABLED=true要指定开启。

3.2.1 集成内置的组件

在本案例中,我们想在 Tracing 中记录应用执行了 [网络请求/API/DB] 的记录效果。

为此,首先在项目中集成案例需要的库

<ItemGroup>
    <!-- 基础库 -->
    <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.6.0" />
    <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.6.0" />
    <PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.6.0-rc.1" />
    <!-- 为本案例的追加项 OpenTelemetry.Instrumentation.{lib-name} -->
    <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.6.0-rc.1" />
    <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.6.0-rc.1" />
    <PackageReference Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.6.0-beta.3" />
</ItemGroup>

在 Program.cs 中配置 Tracing 的应用

# Program.cs
# ... 省略 ...
// 引入 OpenTelemetry 库
var otel = builder.Services.AddOpenTelemetry();
// 配置服务名称
otel.ConfigureResource(resource => resource.AddService(serviceName: my_serv_name));
// ★ 配置 Tracing
// 启用 Api/Http/SQL,配置链路数据推送(Jaeger)地址
otel.WithTracing(tracing =>
{
    tracing
    .AddAspNetCoreInstrumentation()
    .AddHttpClientInstrumentation()
    .AddSqlClientInstrumentation()
    .AddOtlpExporter(opts => opts.Endpoint = new Uri("http://13.13.1.16:4317"));
});
# ... 省略 ...

在控制器中实现案例的应用场景 [网络请求/DB查询]

# ... 省略 ...
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
    // ★ 模拟 HTTP Request
    HttpClient client = new HttpClient();
    client.GetStringAsync("http://www.baidu.com");
    
    // ★ 模拟 DB 交互
    this.DB_Query();
    
    
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
    }).ToArray();
}
# ... 省略 ...

好了,项目 Run 起来,请求天气API。

浏览器打开 Jaeger 页面 http://{host}:16686

请求天气 API 后,与 HTTP / mssql 三者都已自动组合成链路,可以很直观的看到 Tracing 中每个节点的详细执行情况。OpenTelemetry 在背后负责数据的采集/发送,Jaeger 负责接收数据/页面呈现的效果。

3.2.2 创建自定义跨度

在上节中看到,OpenTelemetry 是针对组件级/服务级的 Tracing,也就是其中的单个节点相当于单个组件或服务,那么服务实例内部的执行情况。。。那如果节点需要更细的颗粒度呢。。。

本小节为了实现方法级的自定义节点跨度,功能类方法中创建节点及附属属性,示例如下:

# 附带 Activity 的功能类
public class My_MessageSend_ActivitySource
{
    // 对外可访问的 活动源对象
    public ActivitySource ActivitySource { get; }
    // 对外可访问的 活动源名称
    internal const string ActivitySourceName = nameof(My_MessageSend_ActivitySource);
    
    private readonly ILogger<My_MessageSend_ActivitySource> _logger;
    public My信息发送_ActivitySource(ILogger<My_MessageSend_ActivitySource> logger) {
        _logger = logger;
        // ★ 创建活动源对象
        this.ActivitySource = new ActivitySource(ActivitySourceName, "0.1.0");
    }
    
    
    // ★ 消息发送方法(集成 Activity)
    public void SendMessage_Activity()   
    {
        using var act = this.ActivitySource.StartActivity("消息发送-节点", ActivityKind.Client);
        
        act?.AddTag("自定义_STATUS", "OK");
        act?.RecordException(new Exception("Exception-测试"));
        act?.AddEvent(new ActivityEvent("自定义事件日志", tags: new ActivityTagsCollection() { new KeyValuePair<string, object?>("单次发送量", "5000笔") }));
        
        
        // 当然,此处也可以再次 .StartActivity 新节点,串行/并行,更小的颗粒度,或叫[逻辑片段节点]
        //using var act_2 = this.ActivitySource.StartActivity("逻辑片段-节点", ActivityKind.Client);
        
        
        _logger.LogInformation("消息已经发送");
    }
}

Program.cs 中的配置

# 引入 自定义活动源
builder.Services.AddScoped<My_MessageSend_ActivitySource>();
#
# ... 省略 ...
#
# Tracing 中配置 活动源
otel.WithTracing(tracing =>
{
    tracing
    // OpenTelemetry (这里节选)自带的支持组件
    // 也就是说,OpenTelemetry 遇到以下组件,会自动创建 Activity 跨度,成为 Tracing 中的节点
    .AddAspNetCoreInstrumentation().AddHttpClientInstrumentation().AddSqlClientInstrumentation()

    // ★ 附加 自定义的活动源
    // 为创建方法级 跨度(Tracing.Span)节点 的案例
    .AddSource(nameof(My_MessageSend_ActivitySource))
        
    // OpenTelemetry 数据发送到的地址
    .AddOtlpExporter(opts => opts.Endpoint = new Uri("http://13.13.1.16:4317"));
});
# ... 省略 ...

控制器中的应用

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    // ... 构造...省略 ...
    
    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        // ★ 调用(已集成 Activity)业务方法
        _mess_actsource.SendMessage_Activity();
        
        
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }).ToArray();
    }
}

同样的,项目 Run 起来,请求天气API。浏览器打开 Jaeger 页面。

OpenTelemetry Tracing 在 Jaeger 中的效果示例图:

假设,我们把每个 Solution 的业务层/数据层等类方法都集成 Activity 的话,那将会是一个非常细的调用过程(Tracing)的呈现。

如果你不想逐个引入 OpenTelemetry.Instrumentation.{lib-name} 这样的组件支持方式

那么 OpenTelemetry.AutoInstrumentation 库包含了更多的组件支持,另如 Redis/MongoDB/WCF/EFCore 等等。
OpenTelemetry dotnet 的现有库

用于扩展 OpenTelemetry 的库、插件、集成和其它可用工具。

官网中查找 https://opentelemetry.io/ecosystem/registry/?language=dotnet&component=all

四、.NET 接入 SkyWalking

本章节的主要目的是为了展示高度自动化产品的快速接入,可呈现的效果,忽略 SkyWalking.NET 是否优秀是否好用。

本章节没有紧密结合官方文档,原因是 SkyAPM.Agent.AspNetCore 为第三方维护发布,so 以下内容依据第三方源码所写,可能会与官网有些许差异,这取决于第三方。

关于链路追踪产品 SkyWalking,它是 JAVA 生态中的重要部分,出自华为员工之手,并非华为产品,Apache 的开源孵化项目。

本章的任务是:在测试项目中集成 SkyWalking 的 APM,将产生的遥测数据传输到 SkyWalking 后端,浏览器访问 UI 地址呈现各数据。

4.1 核心内容

🗂️ 下面来说说它的主要模型构成:

  • 服务 Service:拿微服务来举例,它是整个平台的某个服务,比如 产品服务/订单服务/用户服务等
  • 实例 Instance:某个服务的运行实例,特别是集群化的架构时,某个服务可有多个运行实例来支撑更多的负载
  • 链路 Trace:用户端发起的单次请求,后端的连锁反应涵盖的行为节点,一次完整的请求周期,行为节点用 Span 表示
  • 跨度 Span:相当于链路中的某节点,主要的属性 Id/Name/Time/Parent/Type/Tags/Logs 等体现出节点的分类及周期耗时
  • 片段 Segment:是对 Span 的包装,附加的属性有 ServiceId/InstanceId/TraceId/Reference 等明确了在链路中的归属位置
  • 日志 Logger:是对 Span 的补充,单个 Span 下记录更多的日志信息,通常需要手动透过 Microsoft.Extensions.Logging 完成

🗂️ SkyWalking 的三个核心组件:

  • Agent:(探针)运行在各个服务实例中,负责采集服务实例的 Trace 、Metrics、Logging 数据,然后上报给后端 OAP。
  • OAP:接收 Agent 上报的 Trace、Metrics 等数据进行分析,然后持久化存储;响应 UI 的请求,将持久化的数据返回给 UI。
  • UI:前后端分离,请求后端 OAP,将持久化的数据展示在界面。

4.2 应用案例

4.2.1 部署 OAP / UI

为实现效果,这里测试用 Docker 的方式部署 SkyWalking-OAP / SkyWalking-UI,如下示例:

# 部署后端 OAP 服务
# - 12800:默认 UI 查询后端服务 OAP 的端口
# - 11800:默认 Agent 上报数据到后端服务 OAP 的端口
docker run -dit --name my-sky-oap --restart unless-stopped -p 11800:11800 -p 12800:12800 -e TZ=Asia/Shanghai apache/skywalking-oap-server:8.9.1
# 部署前端 UI 服务
# - 8080:浏览器访问页面的端口
# - SW_OAP_ADDRESS:查询数据指向后端 OAP 的地址
docker run -dit --name my-sky-ui --restart unless-stopped -p 8080:8080 -e TZ=Asia/Shanghai -e SW_OAP_ADDRESS=http://13.13.1.16:12800 apache/skywalking-ui:8.9.1

4.2.2 项目集成

为模拟微服务场景,创建多个API服务,并为每个服务集成名为SkyAPM.Agent.AspNetCore的包,如下图例:

创建 SkyAPM 的配置文件 SkyWalking.json

// 以下配置内容为必须项。其它配置项已有默认值,这里省略
{
    "SkyWalking": {
        "ServiceName": "My-Service-Product",                    // 唯一的服务名称,在 UI 中便于区分识别
        "Logging": { "FilePath": "logs\\skyapm-{Date}.log" },
        "Transport": {
            "gRPC": {
                "Timeout": 10000,
                "ConnectTimeout": 10000,
                "ReportTimeout": 600000,
                "Servers": "13.13.1.16:11800"                    // Agent 上报数据到 OAP 服务的地址,默认:11800
            }
        }
    }
}

在 Program.cs 中完成服务注册

// ...略写...
public class Program
{
    public static void Main(string[] args) {
        // ...略写...
        builder.Services.AddLogging();
        builder.Services.AddSkyAPM(ext => ext.AddAspNetCoreHosting());
        // ...略写...
    }
}

APM 集成完毕,是不是很快。

为模拟服务间的依赖生成链路,服务A 中 HttpClient 的模拟请求,(服务B中的模拟请求省略)如下示例:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using SkyApm.Tracing;

namespace Web.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class IndexController : ControllerBase
    {
        private readonly ILogger _logger;
        public IndexController(ILoggerFactory loggerFactory) {
            _logger = loggerFactory.CreateLogger("首页控制器");
        }


        [HttpGet(Name = "Get")]
        public async Task<string> Get()
        {
            // MSLogger 记录 API 日志
            _logger.LogInformation("API日志 - Skywalking 自动识别 ILogger.");
            
            #region 模拟网络请求
            HttpClient client = new HttpClient();
            // 模拟对 服务B 的网络请求
            var resp = await client.GetAsync("http://localhost:5134/Order");
            _ = await resp.Content.ReadAsStringAsync();
            _logger.LogInformation("Baidu.com Requested");
            // 模拟对 其它服务 的网络请求
            var resp1 = await client.GetAsync("https://cn.bing.com");
            _ = await resp1.Content.ReadAsStringAsync();
            _logger.LogInformation("Bing.com Requested");
            #endregion

            return "Index OK";
        }
    }
}

以上 控制器的方法 Get 中做了三件事: HTTP两次模拟网络请求,MS-Logger 的三笔记录。

把项目 Run 起来,请求项目中的 API 地址后,浏览器打开 SkyWalking-UI 提供的页面,展示效果在下章节介绍。

4.2.3 UI 介绍

从之前 Docker 部署 SkyWalking-UI 服务指定端口 8080 打开页面。

Metrics 各类指标监控仪表盘

Topology 括扑图,链路及某个节点的详细信息

Trace 链路追踪,各服务间各节点的 Tree 结构图,各 Endpoint 详细及日志内容。

Logger 综合日志页面,区分所属的:服务/实例/追踪/类。

来自于 Metrics 的告警与事件,两者也是包含与被包含的关系

4.3 高阶部分

4.3.1 额外部分

🗂️ 来说说 Span 中的 Type

  • Enter:当进入某个服务的首个 Span 时,被认为是当前服务的 Enter Span
  • Exit:某个服务下,最后一个 Span 时,被认为是当前服务的 Exit Span
  • Local:某个服务下,Enter 之后,并且 Exit 之前的 Span 被称为 Local Span

用一张图来说明 SpanType:

🗂️ 来说说 Span 中的 Layer

也就是无侵入方式支持的组件:HTTP / RPC / DB / MQ / CACHE,当服务中含有此五种产品时,将自动生成新的 Span 并记录相关的 Tag,Span 有基本的 Tag,也有各 Layer 私有的 Tag。

比如 HTTP 时出现的 status_code,比如 DB 时出现的 statement(SQL) 等,如下图例:

也可以为 Span 自定义追加 Tag,便于归类搜索具有相同 Tag 的 Span。

🗂️ 来说说 Span 中的 Log

固定格式的信息记录。为区别于 Logger 的日志,个人更愿意把它称为 Runtime 的事件记录。SkyAPM 将运行时的异常自动放入 Span.Log,也可通过当前 Span 追加自定义内容事件。如下示例:

Span.Log 要区别于 MS 中 Logger 的日志

比如上图中的异常是有区别于 MS 中 Logger.LogError 的。前者更多的称为事件,后者更多的称为记录。当然它们看似也有共通型,而在 SkyWalking-UI 中是以不同形式来呈现。SkyWalking 将 MS 中 Logger 类产生的所有信息以独立的日志模块页面呈现。也就是 [跨度信息] 下的 [相关的日志]。

4.3.2 方法级追踪

通常跨组件的链路,也就是 SpanLayer,最小颗粒度只是服务级别的,那某服务内部的 Library 或方法呢,逻辑复杂时,究竟执行了哪些方法及顺序?这就是本章的兴趣和目的(JAVA版可通过配置实现无侵入式的方法级链路,那么.NET版...😓😓😓)

在某个服务中,试图创建一个这样的逻辑层的类方法,用来实现方法级追踪的场景,于控制器中被调用,最终在 Trace 中呈现该方法的节点。以下示例为方法手动单独定义 Segment/Span,以作为 Trace 中的一部分:

using Dapper;
using System.Data;
using System.Data.SqlClient;
using System.Text.Json;
using SkyApm.Common;
using SkyApm.Tracing;
using SkyApm.Tracing.Segments;
using Microsoft.Extensions.Logging;

namespace BLL
{
    public class BUser
    {
        private DAL.DUser _user;
        private readonly ILogger _logger;
        private readonly ITracingContext _tracing;
        private readonly IEntrySegmentContextAccessor _ent_seg;
        /// ★ 为创建新的 Segment(Span) 而引入的 ITracingContext
        /// ★ 为完整串联 Segment(Span) 而引入的 IEntrySegmentContextAccessor
        public BUser(ILoggerFactory loggerFactory, ITracingContext tracing, IEntrySegmentContextAccessor entry_segment, DAL.DUser user)
        {
            _user = user;
            _tracing = tracing;
            _ent_seg = entry_segment;
            _logger = loggerFactory.CreateLogger("BLL业务类");
        }

        public async Task<string> GetInfo()
        {
            string seg_name = $"{typeof(BUser).FullName}.{nameof(GetInfo)}";

            
            /// ★ 创建 Segment/Span,加入到 Trace 试图中,默认此处开始计时
            var new_segment = _tracing.CreateLocalSegmentContext(seg_name);
            _ent_seg.Context = new_segment;
            
            
            await _user.BaseInfo();    // 调用其它类库方法
            
            
            List<SYS_Logs>? query_result = DB_Query(); // 模仿 DB 动作
            _logger.LogInformation($"\r\n{seg_name} - DB Selected"); // ☆ MSLogger日志
            
            
            /// ☆ 为新 Segment (Span) 追加自定义的 Tag
            new_segment.Span.AddTag("产品", "3C产品");
            /// ☆ 为新 Segment (Span) 追加自定义的 Log(我更愿意翻译为[事件记录],为区别于 MSLogger 类)
            new_segment.Span.AddLog(new LogEvent("Result", JsonSerializer.Serialize(query_result)));



            /// ★ 方法终止信号,停止计时
            _tracing.Release(new_segment);


            return JsonSerializer.Serialize(query_result);
        }
        
        
        private List<SYS_Logs> DB_Query() { /*...*/ }    /// 定义 DB 查询方法(省略)
    }
    public class SYS_Logs { /*...*/ }    /// 定义 Model (省略)
}

关注以上代码中 ★ 的部分,是创建新 Segment/Span 的重要步骤。也有非必须的 ☆ 部分。也有不影响理解的部分已省略。

把以上写好的类用起来,甚至引用的类方法BaseInfo()中也同样创建了 Segment/Span,那么。。。再来 Run 下看看效果吧。。。

方法级追踪效果图:

自定义方法级节点呈现:执行耗时、跨度追加的 Tag/Log、执行周期内的 MSLogger 日志内容。

当然,完全可用架构的方式将此封装起来。。。那就更好应用了。不过,新建的 Segment/Span 并不包含在括扑图中。

关于 SpanLayer 支持的组件目前仅有五种,那以上是自定义的节点跨度,并不包含在那五种里面,官方源码值为 Enum 型😂所以发现没,上图中新建方法节点中出现了Unknown🤣...被官方承认,非官方出品的 SkyAPM.Agent.AspNetCore 还有很多提升空间呀。。。

似乎 SkyAPM.Agent.AspNetCore 团队并非有超出现有五种组件之外的意愿。。。或者您有更好的见解 ^_^

4.4 SkyAPM.NET 的劣势

没有对比,就看不到差距。为什么想放弃 SkyWalking.NET???

SkyWalking 是 Java 写的;所以在 Java 方面有更多的支持,官方也支持了更多语言。但 .NET 是第三方写的。如下图介绍:

SkyAPM.NET 仅支持了基础功能,其它可用资源非常有限,比如插件全部是 Java 写的,SkyAPM.NET 不可用。

举个例子:

比如 Java 在类方法头部标注 @Trace,就可完成对方法级的追踪,多方便

比如 Java 通过配置文件即可完成对旧项目无侵入式的类方法追踪,多快捷

比如 ... ...

那么 SkyAPM.NET 能做到哪些???如官网说明:同样是 Agent,左侧 Java 与 右侧 .NET 支持的内容对比

显然,SkyAPM.NET 能做的,也就是上节提到的 SpanLayer 定义的范围,没有更多的个性化扩展便捷支持。当然,这也能满足绝大多数的基本场景,看各自需求咯~~ 或者我在这里有忽略的地方,希望得到大家的补充。。。

4.5 对 OpenTelemetry 的支持

当然是 SkyWalking-OAP 后端接收 OpenTelemetry 数据的支持。

你可以绕过 SkyAPM,将 OpenTelemetry 集成到项目中,麻烦对准后端 OAP 的数据接收地址。。。

参考官网:

Metrics:https://skywalking.apache.org/docs/main/v9.7.0/en/setup/backend/opentelemetry-receiver/

Tracing:https://skywalking.apache.org/docs/main/v9.7.0/en/setup/backend/otlp-trace/

Logging:https://skywalking.apache.org/docs/main/v9.7.0/en/setup/backend/log-otlp/

OpenTelemetry 的方式。。。当然,对于项目工程团队来讲,主要是快捷应用,有现成的全面的强大的 APM,那最好了。

五、这篇文章的创作过程

概念

搜集资料

瞄准 SkyWalking

接触 Elastic APM

对比两者,定位写作方向

发现 SkyWalking 在 .NET 下的劣势

从源头重新了解有关的链路追踪,关注 OpenTracing

捡起 System.Diagnostics 从微软官方下手,OpenTelemetry

基于 OpenTelemetry 实现数据上报,才能体会其中的缘由和奥妙

优秀产品 Helios

未来,是否开启 ElasticAPM 系列???未知的探索

生态,大多属于JAVA🙄🙄🙄

我希望我能讲明白,有遗漏或不妥的地方,望能提出来,拜谢~

相关推荐
Agile.Zhou4 个月前
使用 Microsoft.Extensions.ServiceDiscovery 进行服务发现并调用
microservice
SRETalk4 个月前
2024 年了,IT 运维监控系统都有哪些推荐?
可观测性·observability·开源监控
Sol·wang8 个月前
微服务 - 作业调度 · Hangfire集成式 · 仪表盘 · DolphinScheduler分布式 · 定义流程
microservice