好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

ASP.NET Core整合Zipkin链路跟踪的实现方法

前言

    在日常使用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 - 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文件,一条指令就可以运行起来

<PackageReference Include = "zipkin4net" Version = "1.5.0" /> <PackageReference Include = "zipkin4net.middleware.aspnetcore" Version = "1.5.0" />

其中-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类所在的位置[点击查看源码

dy("nrwz");

查看更多关于ASP.NET Core整合Zipkin链路跟踪的实现方法的详细内容...

  阅读:67次