写点什么

ASP.NET Core 整合 Zipkin 链路跟踪

用户头像
yi念之间
关注
发布于: 2021 年 05 月 25 日

前言

    在日常使用 ASP.NET Core 的开发或学习中,如果有需要使用链路跟踪系统,大多数情况下会优先选择 SkyAPM。我们之前也说过 SkyAPM 设计确实比较优秀,巧妙的利用 DiagnosticSource 诊断跟踪日志,可以做到对项目无入侵方式的集成。其实还有一款比较优秀的链路跟踪系统,也可以支持 ASP.NET Core,叫 Zipkin。它相对于 SkyWalking 来说相对轻量级,使用相对来说比较偏原生的方式,而且支持 Http 的形式查询和提交链路数据。因为我们总是希望能拥有多一种的解决方案方便对比和参考,所以接下来我们就来学习一下关于 Zipkin 的使用方式。

Zipkin 简介

    Zipkin 是由 Twitter 开源的一款基于 Java 语言开发的分布式实时数据追踪系统(Distributed Tracking System),其主要功能是采集来自各个系统的实时监控数据。该系统让开发者可通过一个 Web 前端轻松的收集和分析数据,例如用户每次请求服务的处理时间等,可方便的监测系统中存在的瓶颈。它大致可以分为三个核心概念

  • 首先是上报端,它主要通过代码的形式集成到程序中,用于上报 Trace 数据到 Collector 端。

  • Collector 负责接收客户端发送过来的数据,保存到内存或外部存储系统中,供 UI 展示。

  • 存储端可以是基于 zipkin 内存完全不依赖外部存储的 In-Memory 形式或依赖外部存储系统的形式,一般采用外部存储系统存储链路数据,毕竟内存有限。它可支持的存储数据库有 MySQL、Cassandra、Elasticsearch。

  • UI 负责展示采集的链路数据,及系统之间的依赖关系。

相对来说还是比较清晰的,如果用一张图表示整体架构的话,大致如下图所示(图片来源于网络)



在学习链路跟踪的过程中会设计到相关概念,我们接下来介绍链路跟踪几个相关的概念

  • TranceId,一般一次全局的请求会有一个唯一的 TraceId,用于代表一次唯一的请求。比如我请求了订单管理系统,而订单管理系统内部还调用了商品管理系统,而商品管理系统还调用了缓存系统或数据库系统。但是对全局或外部来说这是一次请求,所以会有唯一的一个 TraceId。

  • SpanId,虽然全局的来说是一次大的请求,但是在这个链路中内部间还会发起别的请求,这种内部间的每次请求会生成一个 SpanId。

  • 如果将整条链路串联起来的话,我们需要记录全局的 TraceId,代表当前节点的 SpanId 和发起对当前节点调用的的父级 ParentId。

然后基于链路跟踪的核心概念,然后介绍一下 Zipkin 衍生出来了几个相关概念

  • cs:Clent Sent 客户端发起请求的时间,比如 dubbo 调用端开始执行远程调用之前。

  • cr:Client Receive 客户端收到处理完请求的时间。

  • ss:Server Receive 服务端处理完逻辑的时间。

  • sr:Server Receive 服务端收到调用端请求的时间。

sr - cs = 请求在网络上的耗时ss - sr = 服务端处理请求的耗时cr - ss = 回应在网络上的耗时cr - cs = 一次调用的整体耗时
复制代码

关于 zipkin 概念相关的就介绍这么多,接下来我们介绍如何部署 Zipkin。

部署 ZipKin

    关于 Zipkin 常用的部署方式大概有两种,一种是通过下载安装 JDK,然后运行 zipkin.jar 的方式,另一种是基于 Docker 的方式。为了方便我采用的是基于 Docker 的方式部署,因为采用原生的方式去部署还需要安装 JDK,而且操作相对比较麻烦。咱们上面说过,虽然 Zipkin 可以将链路数据存放到内存中,但是这种操作方式并不实用,实际使用过程中多采用 ElasticSearch 存储链路数据。所以部署的时候需要依赖 Zipkin 和 ElasticSearch,对于这种部署形式采用 docker-compose 的方式就再合适不过了,大家可以在 Zipkin 官方 Github 中找到 docker 的部署方式,地址是https://github.com/openzipkin/zipkin/tree/master/docker,官方使用的方式相对比较复杂,下载下来 docker-compose 相关文件之后我简化了它的使用方式,最终修改如下

version: "3.6"services:  elasticsearch:    # 我使用的是7.5.0版本    image: elasticsearch:7.5.0    container_name: elasticsearch    restart: always    #暴露es端口    ports:      - 9200:9200    environment:      - discovery.type=single-node      - bootstrap.memory_lock=true      #es有内存要求      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"    ulimits:      memlock:        soft: -1        hard: -1    networks:      default:        aliases:          - elasticsearch
zipkin: image: openzipkin/zipkin container_name: zipkin restart: always networks: default: aliases: - zipkin environment: #存储类型为es - STORAGE_TYPE=elasticsearch #es地址 - ES_HOSTS=elasticsearch:9200 ports: - 9411:9411 #依赖es所以在es启动完成后在启动zipkin depends_on: - elasticsearch
复制代码

通过 docker-compose 运行编辑后的 yaml 文件,一条指令就可以运行起来

docker-compose -f docker-compose-elasticsearch7.yml up
复制代码

其中-f 是指定文件名称,如果是 docker-compose.yml 则可以直接忽略文件名称,当 shell 中出现如下界面


并且在浏览器中输入http://localhost:9411/zipkin/出现如图所示,则说明 Zikpin 启动成功



整合 ASP.NET Core

ZipKin 启动成功之后,我们就可以将程序中的数据采集到 Zipkin 中去了,我新建了两个 ASP.NET Core 的程序,一个是 OrderApi,另一个是 ProductApi 方便能体现出调用链路,其中 OrderApi 调用 ProductApi 接口,在两个项目中分别引入 Zipkin 依赖包

<PackageReference Include="zipkin4net" Version="1.5.0" /><PackageReference Include="zipkin4net.middleware.aspnetcore" Version="1.5.0" />
复制代码

其中 zipkin4net 为核心包,zipkin4net.middleware.aspnetcore 是集成 ASP.NET Core 的程序包。然后我们在 Startup 文件中添加如下方法

public void RegisterZipkinTrace(IApplicationBuilder app, ILoggerFactory loggerFactory, IHostApplicationLifetime lifetime){    lifetime.ApplicationStarted.Register(() =>    {        //记录数据密度,1.0代表全部记录        TraceManager.SamplingRate = 1.0f;        //链路日志        var logger = new TracingLogger(loggerFactory, "zipkin4net");        //zipkin服务地址和内容类型        var httpSender = new HttpZipkinSender("http://localhost:9411/", "application/json");        var tracer = new ZipkinTracer(httpSender, new JSONSpanSerializer(), new Statistics());        var consoleTracer = new zipkin4net.Tracers.ConsoleTracer();
TraceManager.RegisterTracer(tracer); TraceManager.RegisterTracer(consoleTracer); TraceManager.Start(logger);
}); //程序停止时停止链路跟踪 lifetime.ApplicationStopped.Register(() => TraceManager.Stop()); //引入zipkin中间件,用于跟踪服务请求,这边的名字可自定义代表当前服务名称 app.UseTracing(Configuration["nacos:ServiceName"]);}
复制代码

然后我们在 Configure 方法中调用 RegisterZipkinTrace 方法即可。由于我们要在 OrderApi 项目中采用 HttpClient 的方式调用 ProductAPI,默认 zipkin4net 是支持采集 HttpClient 发出请求的链路数据(由于在 ProductApi 中我们并不发送 Http 请求,所以可以不用集成一下操作),具体集成形式如下,如果使用的是 HttpClientFactory 的方式,在 ConfigureServices 中配置如下

public void ConfigureServices(IServiceCollection services){    //由于我使用了Nacos作为服务注册中心    services.AddNacosAspNetCore(Configuration);    services.AddScoped<NacosDiscoveryDelegatingHandler>();    services.AddHttpClient(ServiceName.ProductService,client=> {        client.BaseAddress = new Uri($"http://{ServiceName.ProductService}");    })    .AddHttpMessageHandler<NacosDiscoveryDelegatingHandler>()    //引入zipkin trace跟踪httpclient请求,名称配置当前服务名称即可    .AddHttpMessageHandler(provider =>TracingHandler.WithoutInnerHandler(Configuration["nacos:ServiceName"]));    services.AddControllers();}
复制代码

如果是直接是使用 HttpClient 的形式调用则可以采用以下方式

using (HttpClient client = new HttpClient(new TracingHandler("OrderApi"))){}
复制代码

然后我们在 OrderApi 中写一段调用 ProductApi 的代码

[Route("orderapi/[controller]")]public class OrderController : ControllerBase{    private List<OrderDto> orderDtos = new List<OrderDto>();    private readonly IHttpClientFactory _clientFactory;
public OrderController(IHttpClientFactory clientFactory) { orderDtos.Add(new OrderDto { Id = 1, TotalMoney=222,Address="北京市",Addressee="me",From="淘宝",SendAddress="武汉" }); _clientFactory = clientFactory; }
/// <summary> /// 获取订单详情接口 /// </summary> /// <param name="id">订单id</param> /// <returns></returns> [HttpGet("getdetails/{id}")] public async Task<OrderDto> GetOrderDetailsAsync(long id) { OrderDto orderDto = orderDtos.FirstOrDefault(i => i.Id == id); if (orderDto != null) { OrderDetailDto orderDetailDto = new OrderDetailDto { Id = orderDto.Id, TotalMoney = orderDto.TotalMoney, Address = orderDto.Address, Addressee = orderDto.Addressee, From = orderDto.From, SendAddress = orderDto.SendAddress }; //调用ProductApi服务接口 var client = _clientFactory.CreateClient(ServiceName.ProductService); var response = await client.GetAsync($"/productapi/product/getall"); var result = await response.Content.ReadAsStringAsync();
orderDetailDto.Products = JsonConvert.DeserializeObject<List<OrderProductDto>>(result); return orderDetailDto; } return orderDto; }}
复制代码

在 ProductApi 中我们只需要编写调用 RegisterZipkinTrace 方法即可,和 OrderApi 一样,我们就不重复粘贴了。因为 ProductApi 不需要调用别的服务,所以可以不必使用集成 HttpClient,只需要提供简单的接口即可

[Route("productapi/[controller]")]public class ProductController : ControllerBase{    private List<ProductDto> productDtos = new List<ProductDto>();    public ProductController()    {        productDtos.Add(new ProductDto { Id = 1,Name="酒精",Price=22.5m });        productDtos.Add(new ProductDto { Id = 2, Name = "84消毒液", Price = 19.9m });    }
/// <summary> /// 获取所有商品信息 /// </summary> /// <returns></returns> [HttpGet("getall")] public IEnumerable<ProductDto> GetAll() { return productDtos; }}
复制代码

启动这两个项目,调用 OrderApi 的 getdetails 接口,完成后打开 zipkin 界面


点击进去可查看链路详情


总结起来核心操作其实就两个,一个是在发送请求的地方,使用 TracingHandler 记录发起端的链路情况,然后在接收请求的服务端使用 UseTracing 记录来自于客户端请求的链路情况。

改进集成方式

    其实在上面的演示中,我们可以明显的看到明显的不足,就是很多时候其实我们没办法去设置 HttpClient 相关的参数的,很多框架虽然也是使用的 HttpClient 或 HttpClientFactory 相关,但是在外部我们没办法通过自定义的方式去设置他们的相关操作,比如 Ocelot 其实也是使用 HttpClient 相关发起的转发请求,但是对外我们没办法通过我们的程序去设置 HttpClient 的参数。还有就是在.Net Core 中 WebRequest 其实也是对 HttpClient 的封装,但是我们同样没办法在我们的程序中给他们传递类似 TracingHandler 的操作。现在我们从 TracingHandler 源码开始解读看看它的内部到底是如何工作的,zipkin 官方提供的.net core 插件 zipkin4net 的源码位于https://github.com/openzipkin/zipkin4net,我们找到 TracingHandler 类所在的位置[点击查看源码👈],由于 TracingHandler 本身就是 DelegatingHandler 的子类,所以我们主要看 SendAsync 方法,大致抽离出来如下

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken){    Func<HttpRequestMessage, string> _getClientTraceRpc = _getClientTraceRpc = getClientTraceRpc ?? (request => request.Method.ToString());    IInjector<HttpHeaders> _injector = Propagations.B3String.Injector<HttpHeaders>((carrier, key, value) => carrier.Add(key, value));    //记录发起请求客户端链路信息的类是ClientTrace    using (var clientTrace = new ClientTrace(_serviceName, _getClientTraceRpc(request)))    {        if (clientTrace.Trace != null)        {            _injector.Inject(clientTrace.Trace.CurrentSpan, request.Headers);        }
var result = await clientTrace.TracedActionAsync(base.SendAsync(request, cancellationToken)); //AddAnnotation是记录标签信息,我们可以在zipkin链路详情中看到这些标签 if (clientTrace.Trace != null) { //记录请求路径 clientTrace.AddAnnotation(Annotations.Tag(zipkinCoreConstants.HTTP_PATH, result.RequestMessage.RequestUri.LocalPath)); //记录请求的http方法 clientTrace.AddAnnotation(Annotations.Tag(zipkinCoreConstants.HTTP_METHOD, result.RequestMessage.Method.Method)); if (_logHttpHost) { //记录主机 clientTrace.AddAnnotation(Annotations.Tag(zipkinCoreConstants.HTTP_HOST, result.RequestMessage.RequestUri.Host)); } if (!result.IsSuccessStatusCode) { clientTrace.AddAnnotation(Annotations.Tag(zipkinCoreConstants.HTTP_STATUS_CODE, ((int)result.StatusCode).ToString())); } } return result; }}
复制代码

实现方式比较简单,就是借助 ClientTrace 记录一些标签,其他的相关操作都是由 zipkin4net 提供的。我们在之前的文章.Net Core中的诊断日志DiagnosticSource讲解中层说道 HttpClient 底层会有发出诊断日志,我们可以借助这个思路,来对 HttpClient 进行链路跟踪埋点。我们结合 Microsoft.Extensions.DiagnosticAdapter 扩展包定义如下类

public class HttpDiagnosticListener: ITraceDiagnosticListener{    public string DiagnosticName => "HttpHandlerDiagnosticListener";
private ClientTrace clientTrace; private readonly IInjector<HttpHeaders> _injector = Propagations.B3String.Injector<HttpHeaders>((carrier, key, value) => carrier.Add(key, value));
[DiagnosticName("System.Net.Http.Request")] public void HttpRequest(HttpRequestMessage request) { clientTrace = new ClientTrace("apigateway", request.Method.Method); if (clientTrace.Trace != null) { _injector.Inject(clientTrace.Trace.CurrentSpan, request.Headers); } }
[DiagnosticName("System.Net.Http.Response")] public void HttpResponse(HttpResponseMessage response) { if (clientTrace.Trace != null) { clientTrace.AddAnnotation(Annotations.Tag(zipkinCoreConstants.HTTP_PATH, response.RequestMessage.RequestUri.LocalPath)); clientTrace.AddAnnotation(Annotations.Tag(zipkinCoreConstants.HTTP_METHOD, response.RequestMessage.Method.Method)); clientTrace.AddAnnotation(Annotations.Tag(zipkinCoreConstants.HTTP_HOST, response.RequestMessage.RequestUri.Host)); if (!response.IsSuccessStatusCode) { clientTrace.AddAnnotation(Annotations.Tag(zipkinCoreConstants.HTTP_STATUS_CODE, ((int)response.StatusCode).ToString())); } } }
[DiagnosticName("System.Net.Http.Exception")] public void HttpException(HttpRequestMessage request,Exception exception) { }}
复制代码

ITraceDiagnosticListener 是我们方便操作 DiagnosticListener 定义的接口,接口仅包含 DiagnosticName 用来表示 DiagnosticListener 监听的名称,有了这个接口接下来的操作我们会方便许多,接下来我们来看订阅操作的实现。

public class TraceObserver :IObserver<DiagnosticListener>{    private IEnumerable<ITraceDiagnosticListener> _traceDiagnostics;    public TraceObserver(IEnumerable<ITraceDiagnosticListener> traceDiagnostics)    {        _traceDiagnostics = traceDiagnostics;    }
public void OnCompleted() { }
public void OnError(Exception error) { }
public void OnNext(DiagnosticListener listener) { //这样的话我们可以更轻松的扩展其他DiagnosticListener的操作 var traceDiagnostic = _traceDiagnostics.FirstOrDefault(i=>i.DiagnosticName==listener.Name); if (traceDiagnostic!=null) { //适配订阅 listener.SubscribeWithAdapter(traceDiagnostic); } }}
复制代码

通过这种操作我们就无需关心如何将自定义的 DiagnosticListener 订阅类适配到 DiagnosticAdapter 中去,方便我们自定义其他 DiagnosticListener 的订阅类,这样的话我们只需注册自定义的订阅类即可。

services.AddSingleton<TraceObserver>();services.AddSingleton<ITraceDiagnosticListener, HttpDiagnosticListener>();
复制代码

通过这种改进方式,我们可以解决类似 HttpClient 封装到框架中,并且我们我们无法通过外部程序去修改设置的时候。比如我们在架构中引入了 Ocelot 网关,我们就可以采用类似这种方式,在网关层集成 zipkin4net。

自定义埋点

    通过上面我们查看 TracingHandler 的源码我们得知埋点主要是通过 ClientTrace 进行的,它是在发起请求的客户端进行埋点。在服务端埋点的方式我们可以通过 TracingMiddleware 中间件中的源码查看到[点击查看源码👈]叫 ServerTrace。有了 ClientTrace 和 ServerTrace 我们可以非常轻松的实现一次完整的客户端和服务端埋点,只需要通过它们打上一些标签即可。其实它们都是对 Trace 类的封装,我们找到它们的源码进行查看

public class ClientTrace : BaseStandardTrace, IDisposable{    public ClientTrace(string serviceName, string rpc)    {        if (Trace.Current != null)        {            Trace = Trace.Current.Child();        }
Trace.Record(Annotations.ClientSend()); Trace.Record(Annotations.ServiceName(serviceName)); Trace.Record(Annotations.Rpc(rpc)); }
public void Dispose() { Trace.Record(Annotations.ClientRecv()); }}
public class ServerTrace : BaseStandardTrace, IDisposable{ public override Trace Trace { get { return Trace.Current; } }
public ServerTrace(string serviceName, string rpc) { Trace.Record(Annotations.ServerRecv()); Trace.Record(Annotations.ServiceName(serviceName)); Trace.Record(Annotations.Rpc(rpc)); }
public void Dispose() { Trace.Record(Annotations.ServerSend()); }}
复制代码

因此,如果你想通过更原始的方式去记录跟踪日志可以采用如下方式

var trace = Trace.Create();trace.Record(Annotations.ServerRecv());trace.Record(Annotations.ServiceName(serviceName));trace.Record(Annotations.Rpc("GET"));trace.Record(Annotations.ServerSend());trace.Record(Annotations.Tag("http.url", "<url>"));
复制代码

示例 Demo

由于上面说的比较多,而且有一部分关于源码的解读,为了防止由本人文笔有限,给大家带来理解误区,另一方面也为了更清晰的展示 Zipkin 的集成方式,我自己做了一套 Demo,目录结构如下


可以转发针对 OrderApi 的请求,OrderApi 和 ProductApi 用于模拟业务系统,这三个项目都集成了 zipkin4net 链路跟踪,他们之间是通过 Nacos 实现服务的注册和发现。这个演示 Demo 我本地是可以直接运行成功的,如果有下载下来运行不成功的,可以评论区给我留言。Demo 上传到了百度网盘中下载链接: https://pan.baidu.com/s/1jPHyXKV9DAK_oEYQz3xtzA 提取码: a7u5

总结

    以上就是关于 Zipkin 以及 ASP.NET Core 整合 Zipkin 的全部内容,希望能给大家带来一定的帮助。如果你有实际需要也可以继续自行研究。Zipkin 相对于我们常用的 Skywalking 而且,它的使用方式比较原生,许多操作都需要自行通过代码操作,而 SkyAPM 可以做到对代码无入侵的方式集成。Skywalking 是一款 APM(应用性能管理),链路跟踪只是它功能的一部分。而 Zipkin 是一款专注于链路跟踪的系统,个人感觉就链路跟踪这一块而言,Zipkin 更轻量级(如果使用 ES 作为存储数据库的话,Skywalking 默认会生成一堆索引,而 Zipkin 默认是每天创建一个索引),而且链路信息检索、详情展示、链路数据上报形式等相对于 Skywalking 形式也更丰富一些。但是整体而言 Skywalking 更强大,比如应用监控、调用分析、集成方式等。技术并无好坏之分,适合自己的才是更好的,多一个解决方案,就多一个解决问题的思路,我觉得这是对于我们程序开发人员来说都应该具备的认知。

发布于: 2021 年 05 月 25 日阅读数: 232
用户头像

yi念之间

关注

星光不问赶路人,时光不负有心人。 2018.08.22 加入

普通程序员,主攻.net core方向,顺便学习Java和Python。喜欢架构设计,励志成为一名真正的架构师,喜欢研究新技术,喜欢阅读源码。

评论

发布
暂无评论
ASP.NET Core整合Zipkin链路跟踪