查看原文
其他

微服务架构之分布式追踪(.NET 6.0 集成Zipkin)

DotNet 2024-04-12

↓推荐关注↓

前言

在微服务架构中,由于服务之间做了拆分,一次请求往往要涉及多个服务的调用,不同的服务可能由不同的团队开发,使用不同的编程语言,还有可能部署在不同的机器上,分布在不同的数据中心。

当请求出现异常问题时,我们需要知道本次请求调用了哪些服务,又是哪个服务引起的错误,如果靠人肉的方式,到每个服务去查找代码,查看日志,那么无疑是十分痛苦的,这时候分布式追踪系统应运而生。

服务追踪的作用

除了能够快速定位请求失败的原因之外,服务追踪系统还需要记录调用经过的每一条链路上的耗时,这样在我们分析追踪数据时,就可以快速定位系统的瓶颈在哪里,针对耗时较长的链路,做出针对性的优化。

通过追踪系统的链路可视化功能,可以更直观的看到请求的调用链路,及服务之间的依赖关系,我们可以凭此评估链路的调用是否合理,优化链路调用

同时链路追踪系统大多都集成了 生成系统调用关系网络拓扑的功能,它可以将整套系统间的服务调用关系完整的,直观的展示出来,通过网络拓扑可以更好的优化系统架构,找出系统层级间不合理的地方。

服务追踪的原理

要理解服务追踪的原理,首先必须搞懂一些基本概念:traceId、spanId、annonation 等。

traceId:用于标识某一次具体的请求 ID。当用户的请求进入系统后,会在 RPC 调用网络的第一层生成一个全局唯一的 traceId,并且会随着每一层的 RPC 调用,不断往后传递,这样的话通过 traceId 就可以把一次用户请求在系统中调用的路径串联起来。

spanId:用于标识一次 RPC 调用在分布式请求中的位置。当用户的请求进入系统后,处在 RPC 调用网络的第一层 A 时 spanId 初始值是 0,进入下一层 RPC 调用 B 的时候 spanId 是 0.1,继续进入下一层 RPC 调用 C 时 spanId 是 0.1.1,而与 B 处在同一层的 RPC 调用 E 的 spanId 是 0.2,这样的话通过 spanId 就可以定位某一次 RPC 请求在系统调用中所处的位置,以及它的上下游依赖分别是谁。

annotation:用于业务自定义埋点数据,可以是业务感兴趣的想上传到后端的数据,比如一次请求的用户 UID。

简单的总结一下,traceId 是用于串联某一次请求在系统中经过的所有路径,spanId 是用于区分系统不同服务之间调用的先后关系,而 annotation 是用于业务自定义一些自己感兴趣的数据,在上传 traceId 和 spanId 这些基本信息之外,添加一些自己感兴趣的信息。

开源服务追踪系统对比

Google的Dapper,阿里的鹰眼,大众点评的CAT,Twitter的Zipkin,LINE的pinpoint,国产的skywalking。市面上的全链路监控理论模型大多都是借鉴Google Dapper论文,本文重点关注以下两种APM组件:

  • Zipkin :由Twitter公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括:数据的收集、存储、查找和展现。

  • Skywalking :国产的优秀APM组件,是一个对JAVA分布式应用程序集群的业务运行情况进行追踪、告警和分析的系统。

类型zipkinSKYwalking
基本原理拦截请求,发送(HTTP,mq)数据至zipkin服务java探针,字节码增强
接入方式基于linkerd或者sleuth方式,引入配置即可avaagent字节码
支持OpenTracing
颗粒度接口级(类级别)方法级
存储ES,mysql,Cassandra,内存ES,H2,TIDB
agent到collector的协议http,MQhttp,gRPC

对代码无任何侵入,除了性能和对代码的侵入性上 SkyWaking 表现不错外,它还有以下优势,对多语言的支持,组件丰富:目前其支持 Java, .Net Core, PHP, NodeJS, Golang, LUA 语言,组件上也支持dubbo, mysql 等常见组件,

Zipkin的Docker部署

docker run --name zipkin -d -p 9411:9411  openzipkin/zipkin

执行以上命令,zipkin会按照默认的情况将数据存储在内存中,容器重启后,数据就会丢失,可以在开发或测试环境用此种方式,如果是生产环境还是建议将底层存储换成Elasticsearch,可以保证数据的持久化存储。

docker run --name zipkin -d -p 9411:9411 -e STORAGE_TYPE=elasticsearch -e ES_HOSTS=192.168.0.8:9200 openzipkin/zipkin

值得一提的是,底层存储更换成ES后,网络拓扑图将不会自动生成,需要启动下面的容器生成网络拓扑(注意:它是一次性服务,需要定时任务执行此容器,或在需要时手动启动它,才能看到最新的网络拓扑)。

docker run --name zipkin-dependencies --env STORAGE_TYPE=elasticsearch --env ES_HOSTS=192.168.0.8:9200 --env ES_INDEX=zipkin --rm=true -e JAVA_OPTS="-Xmx3550m -Xms3550m" openzipkin/zipkin-dependencies

.NET 6.0 集成Zipkin

首先需要引入如下依赖包

此处使用了 System.Diagnostic.DiagnosticListener 实现对应用程序的监听,具体的订阅原理可以进入链接中的文章去了解和学习。

public interface ITraceDiagnosticListener
{
    string DiagnosticName { get; }
}
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));
    private readonly IConfiguration _configuration;
    private  ZipkinOptions _zipkinOptions = new ZipkinOptions();

    public HttpDiagnosticListener(IConfiguration configuration)
    {
        _configuration = configuration;
        _configuration.GetRequiredSection("Zipkin").Bind(_zipkinOptions);
    }

    [DiagnosticName("System.Net.Http.Request")]
    public void HttpRequest(HttpRequestMessage request)
    {
        clientTrace = new ClientTrace(_zipkinOptions.ServiceName, request.Method.Method);
        if (clientTrace.Trace != null && request != null)
        {
            _injector.Inject(clientTrace.Trace.CurrentSpan, request.Headers);
        }
    }

    [DiagnosticName("System.Net.Http.Response")]
    public void HttpResponse(HttpResponseMessage response)
    {
        if (clientTrace.Trace != null && response != 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)
    {
    }
}
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)
    {
        var traceDiagnostic = _traceDiagnostics.FirstOrDefault(i => i.DiagnosticName == listener.Name);
        if (traceDiagnostic != null)
        {
            //适配订阅
            listener.SubscribeWithAdapter(traceDiagnostic);
        }
    }
}

集成zipkin的注入及中间件添加的方式,可以封装为nuget包的形式,方便其它服务使用。

public class ZipkinOptions
{
    public string ServiceName { getset; }

    public string Url { getset; }

}

public static class ZipkinExtensions
{
    public static IServiceCollection AddZipkin(this IServiceCollection services)
    {
        services.AddSingleton<ITraceDiagnosticListener, HttpDiagnosticListener>();
        return services.AddSingleton<TraceObserver>();
    }

    public static IApplicationBuilder UseZipkin(this IApplicationBuilder app, IHostApplicationLifetime lifetime)
    {
        var configuration = app.ApplicationServices.GetRequiredService<IConfiguration>();
        var zipkinOptions = new ZipkinOptions();
        configuration.GetSection("Zipkin").Bind(zipkinOptions);
        return UseZipkin(app, lifetime, zipkinOptions.ServiceName, zipkinOptions.Url);
    }

    public static IApplicationBuilder UseZipkin(this IApplicationBuilder app, IHostApplicationLifetime lifetime, string serviceName, string zipkinUrl)
    {
        ILoggerFactory loggerFactory = app.ApplicationServices.GetRequiredService<ILoggerFactory>();
        var traceObserver = app.ApplicationServices.GetService<TraceObserver>();
        if (traceObserver != null)
        {
            DiagnosticListener.AllListeners.Subscribe(traceObserver);
        }
        lifetime.ApplicationStarted.Register(() =>
        {
            TraceManager.SamplingRate = 1.0f;//记录数据密度,1.0代表全部记录
            var logger = new TracingLogger(loggerFactory, "zipkin4net");
            var httpSender = new HttpZipkinSender(zipkinUrl, "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());
        app.UseTracing(serviceName);//这边的名字可自定义
        return app;
    }
}

应用启动项添加zipkin中间件,并在配置文件中配置服务名称及zipkin地址。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddZipkin();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();

var app = builder.Build();
app.UseZipkin(app.Lifetime);
app.MapControllers();
app.Run();

可视化展示

访问主机的9411端口,就可以进入zipkin系统。以下是实际的使用效果。

单次调用的链路追踪

网络拓扑

转自:唐磊(Jason)

链接:cnblogs.com/jasonbourne3/p/16628910.html

- EOF -

推荐阅读  点击标题可跳转

开源 .NET 7 + iView 前后端分离通用后台管理系统

EF Code高性能、轻量级分表分库、读写分离开源项目.NET 6.0 开源监控解决方案,支持Redis、Elasticsearch、SqlServer

看完本文有收获?请转发分享给更多人

推荐关注「DotNet」,提升.Net技能 

点赞和在看就是最大的支持❤️

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存