微服务架构之分布式追踪(.net6集成Zipkin)
在微服务架构中,由于服务之间做了拆分,一次请求往往要涉及多个服务的调用,不同的服务可能由不同的团队开发,使用不同的编程语言,还有可能部署在不同的机器上,分布在不同的数据中心。当请求出现异常问题时,我们需要知道本次请求调用了哪些服务,又是哪个服务引起的错误,如果靠人肉的方式,到每个服务去查找代码,查看日志,那么无疑是十分痛苦的,这时候分布式追踪系统应运而生。
服务追踪的作用
除了能够快速定位请求失败的原因之外,服务追踪系统还需要记录调用经过的每一条链路上的耗时,这样在我们分析追踪数据时,就可以快速定位系统的瓶颈在哪里,针对耗时较长的链路,做出针对性的优化。通过追踪系统的链路可视化功能,可以更直观的看到请求的调用链路,及服务之间的依赖关系,我们可以凭此评估链路的调用是否合理,优化链路调用。同时链路追踪系统大多都集成了 生成系统调用关系网络拓扑的功能,它可以将整套系统间的服务调用关系完整的,直观的展示出来,通过网络拓扑可以更好的优化系统架构,找出系统层级间不合理的地方。
服务追踪的原理
要理解服务追踪的原理,首先必须搞懂一些基本概念: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分布式应用程序集群的业务运行情况进行追踪、告警和分析的系统。
类型 |
zipkin |
SKYwalking |
---|---|---|
基本原理 |
拦截请求,发送(HTTP,mq)数据至zipkin服务 |
java探针,字节码增强 |
接入方式 |
基于linkerd或者sleuth方式,引入配置即可 |
avaagent字节码 |
支持OpenTracing |
是 |
是 |
颗粒度 |
接口级(类级别) |
方法级 |
存储 |
ES,mysql,Cassandra,内存 |
ES,H2,TIDB |
agent到collector的协议 |
http,MQ |
http,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
.net6集成Zipkin
首先需要引入如下依赖包
此处使用了 System.Diagnostic.DiagnosticListener 实现对应用程序的监听,具体的订阅原理可以进入链接中的文章去了解和学习。
1 public interface ITraceDiagnosticListener 2 { 3 string DiagnosticName { get; } 4 }
1 public class HttpDiagnosticListener : ITraceDiagnosticListener 2 { 3 public string DiagnosticName => "HttpHandlerDiagnosticListener"; 4 5 private ClientTrace clientTrace; 6 private readonly IInjector<HttpHeaders> _injector = Propagations.B3String.Injector<HttpHeaders>((carrier, key, value) => carrier.Add(key, value)); 7 private readonly IConfiguration _configuration; 8 private ZipkinOptions _zipkinOptions = new ZipkinOptions(); 9 10 public HttpDiagnosticListener(IConfiguration configuration) 11 { 12 _configuration = configuration; 13 _configuration.GetRequiredSection("Zipkin").Bind(_zipkinOptions); 14 } 15 16 [DiagnosticName("System.Net.Http.Request")] 17 public void HttpRequest(HttpRequestMessage request) 18 { 19 clientTrace = new ClientTrace(_zipkinOptions.ServiceName, request.Method.Method); 20 if (clientTrace.Trace != null && request != null) 21 { 22 _injector.Inject(clientTrace.Trace.CurrentSpan, request.Headers); 23 } 24 } 25 26 [DiagnosticName("System.Net.Http.Response")] 27 public void HttpResponse(HttpResponseMessage response) 28 { 29 if (clientTrace.Trace != null && response != null) 30 { 31 clientTrace.AddAnnotation(Annotations.Tag(zipkinCoreConstants.HTTP_PATH, response.RequestMessage.RequestUri.LocalPath)); 32 clientTrace.AddAnnotation(Annotations.Tag(zipkinCoreConstants.HTTP_METHOD, response.RequestMessage.Method.Method)); 33 clientTrace.AddAnnotation(Annotations.Tag(zipkinCoreConstants.HTTP_HOST, response.RequestMessage.RequestUri.Host)); 34 if (!response.IsSuccessStatusCode) 35 { 36 clientTrace.AddAnnotation(Annotations.Tag(zipkinCoreConstants.HTTP_STATUS_CODE, ((int)response.StatusCode).ToString())); 37 } 38 } 39 } 40 41 [DiagnosticName("System.Net.Http.Exception")] 42 public void HttpException(HttpRequestMessage request, Exception exception) 43 { 44 } 45 }
1 public class TraceObserver : IObserver<DiagnosticListener> 2 { 3 private IEnumerable<ITraceDiagnosticListener> _traceDiagnostics; 4 public TraceObserver(IEnumerable<ITraceDiagnosticListener> traceDiagnostics) 5 { 6 _traceDiagnostics = traceDiagnostics; 7 } 8 9 public void OnCompleted() 10 { 11 } 12 13 public void OnError(Exception error) 14 { 15 } 16 17 public void OnNext(DiagnosticListener listener) 18 { 19 var traceDiagnostic = _traceDiagnostics.FirstOrDefault(i => i.DiagnosticName == listener.Name); 20 if (traceDiagnostic != null) 21 { 22 //适配订阅 23 listener.SubscribeWithAdapter(traceDiagnostic); 24 } 25 } 26 }
1 public class ZipkinOptions 2 { 3 public string ServiceName { get; set; } 4 5 public string Url { get; set; } 6 7 } 8 9 public static class ZipkinExtensions 10 { 11 public static IServiceCollection AddZipkin(this IServiceCollection services) 12 { 13 services.AddSingleton<ITraceDiagnosticListener, HttpDiagnosticListener>(); 14 return services.AddSingleton<TraceObserver>(); 15 } 16 17 public static IApplicationBuilder UseZipkin(this IApplicationBuilder app, IHostApplicationLifetime lifetime) 18 { 19 var configuration = app.ApplicationServices.GetRequiredService<IConfiguration>(); 20 var zipkinOptions = new ZipkinOptions(); 21 configuration.GetSection("Zipkin").Bind(zipkinOptions); 22 return UseZipkin(app, lifetime, zipkinOptions.ServiceName, zipkinOptions.Url); 23 } 24 25 public static IApplicationBuilder UseZipkin(this IApplicationBuilder app, IHostApplicationLifetime lifetime, string serviceName, string zipkinUrl) 26 { 27 ILoggerFactory loggerFactory = app.ApplicationServices.GetRequiredService<ILoggerFactory>(); 28 var traceObserver = app.ApplicationServices.GetService<TraceObserver>(); 29 if (traceObserver != null) 30 { 31 DiagnosticListener.AllListeners.Subscribe(traceObserver); 32 } 33 lifetime.ApplicationStarted.Register(() => 34 { 35 TraceManager.SamplingRate = 1.0f;//记录数据密度,1.0代表全部记录 36 var logger = new TracingLogger(loggerFactory, "zipkin4net"); 37 var httpSender = new HttpZipkinSender(zipkinUrl, "application/json"); 38 39 var tracer = new ZipkinTracer(httpSender, new JSONSpanSerializer(), new Statistics()); 40 var consoleTracer = new zipkin4net.Tracers.ConsoleTracer(); 41 42 43 TraceManager.RegisterTracer(tracer); 44 TraceManager.RegisterTracer(consoleTracer); 45 TraceManager.Start(logger); 46 47 }); 48 lifetime.ApplicationStopped.Register(() => TraceManager.Stop()); 49 app.UseTracing(serviceName);//这边的名字可自定义 50 return app; 51 } 52 }
1 var builder = WebApplication.CreateBuilder(args); 2 builder.Services.AddZipkin(); 3 builder.Services.AddControllers(); 4 builder.Services.AddEndpointsApiExplorer(); 5 6 var app = builder.Build(); 7 app.UseZipkin(app.Lifetime); 8 app.MapControllers(); 9 app.Run();
可视化展示
访问主机的9411端口,就可以进入zipkin系统。以下是实际的使用效果。
单次调用的链路追踪
网络拓扑