在平常使用ASP.NET Core的开发或学习中,若是有须要使用链路跟踪系统,大多数状况下会优先选择SkyAPM。咱们以前也说过SkyAPM设计确实比较优秀,巧妙的利用DiagnosticSource诊断跟踪日志,能够作到对项目无入侵方式的集成。其实还有一款比较优秀的链路跟踪系统,也能够支持ASP.NET Core,叫Zipkin。它相对于SkyWalking来讲相对轻量级,使用相对来讲比较偏原生的方式,并且支持Http的形式查询和提交链路数据。由于咱们老是但愿能拥有多一种的解决方案方便对比和参考,因此接下来咱们就来学习一下关于Zipkin的使用方式。html
Zipkin是由Twitter开源的一款基于Java语言开发的分布式实时数据追踪系统(Distributed Tracking System),其主要功能是采集来自各个系统的实时监控数据。该系统让开发者可经过一个 Web 前端轻松的收集和分析数据,例如用户每次请求服务的处理时间等,可方便的监测系统中存在的瓶颈。它大体能够分为三个核心概念前端
sr - cs = 请求在网络上的耗时 ss - sr = 服务端处理请求的耗时 cr - ss = 回应在网络上的耗时 cr - cs = 一次调用的总体耗时
关于zipkin概念相关的就介绍这么多,接下来咱们介绍如何部署Zipkin。node
关于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相关文件以后我简化了它的使用方式,最终修改以下git
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文件,一条指令就能够运行起来github
docker-compose -f docker-compose-elasticsearch7.yml up
其中-f是指定文件名称,若是是docker-compose.yml则能够直接忽略文件名称,当shell中出现以下界面而且在浏览器中输入http://localhost:9411/zipkin/出现如图所示,则说明Zikpin启动成功
docker
ZipKin启动成功以后,咱们就能够将程序中的数据采集到Zipkin中去了,我新建了两个ASP.NET Core的程序,一个是OrderApi,另外一个是ProductApi方便能体现出调用链路,其中OrderApi调用ProductApi接口,在两个项目中分别引入Zipkin依赖包shell
<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中配置以下json
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的形式调用则能够采用如下方式bootstrap
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>"));
因为上面说的比较多,并且有一部分关于源码的解读,为了防止由本人文笔有限,给你们带来理解误区,另外一方面也为了更清晰的展现Zipkin的集成方式,我本身作了一套Demo,目录结构以下
ApiGateway为网关项目能够转发针对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更强大,好比应用监控、调用分析、集成方式等。技术并没有好坏之分,适合本身的才是更好的,多一个解决方案,就多一个解决问题的思路,我以为这是对于咱们程序开发人员来讲都应该具有的认知。