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负责展示采集的链路数据,及系统之间的依赖关系。
    相对来说还是比较清晰的,如果用一张图表示整体架构的话,大致如下图所示(图片来源于网络)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记录来自于客户端请求的链路情况。

    改进集成方式

        其实在上面的演示中,我们可以明显的看到明显的不足,就是很多时候其实我们没办法去设置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,目录结构如下

    ASP.NET Core整合Zipkin链路跟踪

    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更强大,比如应用监控、调用分析、集成方式等。技术并无好坏之分,适合自己的才是更好的,多一个解决方案,就多一个解决问题的思路,我觉得这是对于我们程序开发人员来说都应该具备的认知。