使用zipKin構建NetCore分散式鏈路跟蹤

初夏的陽光丶發表於2021-09-18

在這裡插入圖片描述

本文主要講解使用ZipKin構建NetCore分散式鏈路跟蹤


場景

因為最近公司業務量增加,而專案也需要增大部署數量,K8S中Pod基本都擴容了一倍,新增了若干物理機,部分物理機網路通訊存在問題,導致部分請求打入有問題的物理機時總會出現超時的情況,由於之前系統中沒有使用鏈路跟蹤,導致排查問題比較慢,所以就去研究了市面上的鏈路框架,結果發現了ZipKin這款比較輕量級的鏈路跟蹤框架。


例項程式碼

本文日誌系統採用Exceplesstion
示例程式碼請求鏈路為SimpleZipkin(閘道器服務)--->WebApi(Api服務)--->OrderApi(訂單服務)
首先建立公用類庫,引用以下包(本文以1.5.0版本為例)
如果部署Zipkin使用的是Mysql作為儲存,切記Mysql版本不要高於8.0,Zipkin暫不支援8.0的版本

zipkin4net 
zipkin4net.middleware.aspnetcore

建立ZipKin幫助類

 public static class ZipKinExtensions
    {
        public static IServiceCollection AddZipKin(this IServiceCollection services)
        {
            return services.AddSingleton<HttpDiagnosticSourceObserver >();
        }

        public static IApplicationBuilder UseZipKin(this IApplicationBuilder app, IHostApplicationLifetime lifetime, ILoggerFactory loggerFactory, string serviceName, string zipKinUrl)
        {
            DiagnosticListener.AllListeners.Subscribe(app?.ApplicationServices?.GetService<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;
        }
    }

Exceptionless幫助類

/// <summary>
    /// 日誌擴充套件類
    /// </summary>
    public static class LogHelper
    {
        /// <summary>
        /// 記錄Info日誌
        /// </summary>
        /// <param name="logger"></param>
        /// <param name="message"></param>
        public static void InformationToException(this ILogger logger, string message)
        {
            var tranceId = Trace.Current?.CurrentSpan.TraceId.ToString("x16");
            logger.LogInformation($@"tranceId={tranceId},日誌主體為:{message}");
        }

        /// <summary>
        /// 記錄Debug日誌
        /// </summary>
        /// <param name="logger"></param>
        /// <param name="message"></param>
        public static void DebugToException(this ILogger logger, string message)
        {
            var tranceId = Trace.Current?.CurrentSpan.TraceId.ToString("x16");
            logger.LogDebug($@"tranceId={tranceId},日誌主體為:{message}");
        }

        /// <summary>
        /// 記錄錯誤日誌
        /// </summary>
        /// <param name="logger"></param>
        /// <param name="message"></param>
        public static void ErrorToException(this ILogger logger, string message)
        {
            var tranceId = Trace.Current?.CurrentSpan.TraceId.ToString("x16");
            logger.LogError($@"tranceId={tranceId},日誌主體為:{message}");
        }

        /// <summary>
        /// 記錄追蹤日誌
        /// </summary>
        /// <param name="logger"></param>
        /// <param name="message"></param>
        public static void TraceToException(this ILogger logger, string message)
        {
            var tranceId = Trace.Current?.CurrentSpan.TraceId.ToString("x16");
            logger.LogTrace($@"tranceId={tranceId},日誌主體為:{message}");
        }

        /// <summary>
        /// 記錄警告日誌
        /// </summary>
        /// <param name="logger"></param>
        /// <param name="message"></param>
        public static void WarningToException(this ILogger logger, string message)
        {
            var tranceId = Trace.Current?.CurrentSpan.TraceId.ToString("x16");
            logger.LogWarning($@"tranceId={tranceId},日誌主體為:{message}");
        }
    }

接下來建立SimpleZipkin、WebApi、OrderApi等專案(因為結構一致,所以本文只建立一個),首先引用Exceplesstion

 public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureLogging((hostContext, configLogging) =>
                {
                    configLogging.AddConfiguration(hostContext.Configuration.GetSection("Logging"));
                    configLogging.AddConsole();
                    configLogging.AddDebug();
                    configLogging.AddExceptionless();
                    ExceptionlessClient.Default.Configuration.SetDefaultMinLogLevel(Exceptionless.Logging.LogLevel.Debug);
                    configLogging.SetMinimumLevel(LogLevel.Debug);                   
                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }

接下來在Startup中引入ZipKin

 public void ConfigureServices(IServiceCollection services)
        {
            // 注入Rpc
            //AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
            //services.AddGrpcClient<HelloServer.HelloServerClient>((p, o) =>
            //{
            //    o.Address = new Uri("http://127.0.0.1:3848");
            //});
            //.AddHttpMessageHandler(provider => TracingHandler.WithoutInnerHandler("RpcService"));
            services.AddControllers();
            services.AddZipKin();
            services.AddSingleton<IDiagnosticSource, HttpDiagnosticSourceDemo>();
            services.AddHttpClient("webApi", client => { client.BaseAddress = new Uri($"http://localhost:5001"); });
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "SimpleZipKin", Version = "v1" });
            });
        }


   public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory,
            IHostApplicationLifetime lifetime)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "SimpleZipkin v1"));
            }
            Configuration.GetSection("ExceptionLess").Bind(ExceptionlessClient.Default.Configuration);
            ExceptionlessClient.Default.Configuration.SetDefaultMinLogLevel(Exceptionless.Logging.LogLevel.Debug);
            app.UseZipKin(lifetime, loggerFactory, "SimpleZip", "http://127.0.0.1:9411");//SimpleZip修改為對應的應用名稱,127.0.0.1地址切換為自己的zipkin地址  
            app.UseRouting();

            app.UseAuthorization();
            app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
        }

接下來建立對應的Controller

   [Route("/api/Home")]
    public class HomeController : Controller
    {
        private readonly IHttpClientFactory _httpClientFactory;
        private readonly ILogger _logger;

        /// <summary>
        /// 建構函式
        /// </summary>
        /// <param name="httpClientFactory"></param>
        /// <param name="logger"></param>
        public HomeController(IHttpClientFactory httpClientFactory, ILogger<HomeController> logger)
        {
            _httpClientFactory = httpClientFactory;
            //_helloServerClient = helloServerClient;
            _logger = logger;
        }

        [HttpGet("GetZipKin")]
        public async Task<string> GetZipKin()
        {
            _logger.InformationToException($@"這裡是SimpleZipKinApi");
            var httpClient = _httpClientFactory.CreateClient("webApi");
            var httpResult = await httpClient.GetAsync($"api/order/getorder");
            var result = await httpResult.Content.ReadAsStringAsync();
            return result;
        }    
    }

最後在appsettings.json中加入對應的Exceplesstionless配置

  "ExceptionLess": {
    "ApiKey": "****************************",
    "ServerUrl": "http://127.0.0.1:5000"
  }

OrderApi、WebApi如法炮製,修改對應的請求鏈路資訊

接下來我們使用DiagnosticAdapter做鏈路記載,在公共類庫中建立HttpDiagnosticListener類
DiagnosticSource是Runtime層提供,應用層可以通過它與系統整合、事件日誌、以及效能計數器進行互動。
DiagnosticSource官方介紹:https://docs.microsoft.com/zh-cn/dotnet/api/system.diagnostics.diagnosticsource?view=net-5.0
關於DiagnosticSource設計參考原有yi念之間大佬的文章:https://www.cnblogs.com/wucy/p/13532534.html

public class HttpDiagnosticSourceDemo : IDiagnosticSourceDemo
    {
        public string DiagnosticName => "HttpDiagnosticSourceDemo";

        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("simpleZipKin", 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)
        {
        }
    }

IDiagnosticSourceDemo介面資訊如下:

  public interface IDiagnosticSourceDemo
    {
        string DiagnosticName { get; }
    }

HttpDiagnosticSourceObserver方法如下:

public class HttpDiagnosticSourceObserver : IObserver<DiagnosticListener>
    {
        private IEnumerable<IDiagnosticSourceDemo> _diagnosticSourceDemo;
        public HttpDiagnosticSourceObserver (IEnumerable<IDiagnosticSourceDemo> diagnosticSourceDemo)
        {
            _diagnosticSourceDemo= diagnosticSourceDemo;
        }

        public void OnCompleted()
        {
        }

        public void OnError(Exception error)
        {
        }

        public void OnNext(DiagnosticListener listener)
        {
            var diagnosticSource= _diagnosticSourceDemo.FirstOrDefault(i => i.DiagnosticName == listener.Name);
            if (traceDiagnostic != null)
            {
                //適配訂閱
                listener.SubscribeWithAdapter(diagnosticSource);
            }
        }
    }

最終執行結果如下:
Zipkin為:

Exceplesstion日誌記錄資訊為

通過Exceptionless記錄的環境資訊也能將對應的機器定位

參考

本文引用文章連線如下:
DiagnosticSource官方介紹:https://docs.microsoft.com/zh-cn/dotnet/api/system.diagnostics.diagnosticsource?view=net-5.0
DiagnosticSource參考連線:https://www.cnblogs.com/wucy/p/13532534.html
                   https://sudonull.com/post/3671-Using-the-DiagnosticSource-in-NET-Core-Theory
                   https://andrewlock.net/logging-using-diagnosticsource-in-asp-net-core/
docker部署Zipkin:https://www.cnblogs.com/binz/p/12658020.html
docekr部署exceptionless:https://www.cnblogs.com/edisonchou/p/exceptionless_v5_deployment_introduction.html


如有哪裡講得不是很明白或是有錯誤,歡迎指正
如您喜歡的話不妨點個贊收藏一下吧?

相關文章