网络编程 
首页 > 网络编程 > 浏览文章

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

(编辑:jimmy 日期: 2024/11/27 浏览:3 次 )

前言

"color: rgb(255, 0, 0);">Zipkin简介

"//img.jbzj.com/file_images/article/202009/202009190816411.png" loading="lazy" alt="" />

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

  • 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

"https://github.com/openzipkin/zipkin/tree/master/docker" rel="external nofollow">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中出现如下界面ASP.NET Core整合Zipkin链路跟踪的实现方法

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

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

整合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界面ASP.NET Core整合Zipkin链路跟踪的实现方法点击进去可查看链路详情ASP.NET Core整合Zipkin链路跟踪的实现方法
总结起来核心操作其实就两个,一个是在发送请求的地方,使用TracingHandler记录发起端的链路情况,然后在接收请求的服务端使用UseTracing记录来自于客户端请求的链路情况。

改进集成方式

"https://github.com/openzipkin/zipkin4net" rel="external nofollow">https://github.com/openzipkin/zipkin4net,我们找到TracingHandler类所在的位置[点击查看源码"htmlcode">

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
 Func<HttpRequestMessage, string> _getClientTraceRpc = _getClientTraceRpc = getClientTraceRpc "https://www.cnblogs.com/wucy/p/13532534.html" rel="external nofollow">.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。

自定义埋点

"https://github.com/openzipkin/zipkin4net/blob/master/Src/zipkin4net.middleware.aspnetcore/Src/TracingMiddleware.cs" rel="external nofollow">点击查看源码"htmlcode">

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,目录结构如下
ASP.NET Core整合Zipkin链路跟踪的实现方法ApiGateway为网关项目可以转发针对OrderApi的请求,OrderApi和ProductApi用于模拟业务系统,这三个项目都集成了zipkin4net链路跟踪,他们之间是通过Nacos实现服务的注册和发现。这个演示Demo我本地是可以直接运行成功的,如果有下载下来运行不成功的,可以评论区给我留言。由于博客园有文件上传大小的限制,所以我将Demo上传到了百度网盘中
下载链接:链接: https://pan.baidu.com/s/1LDyoRQehaE0FzedFTC4_Og 提取码: i45x"color: rgb(255, 0, 0);">总结

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

上一篇:使用AJAX实现UTF8编码表单提交到GBK编码脚本无乱码的解决方法
下一篇:asp(vbscript)中自定义函数的默认参数实现代码
友情链接:杰晶网络 DDR爱好者之家 南强小屋 黑松山资源网 白云城资源网 网站地图 SiteMap