您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關ASP.NET Core實現整合Zipkin鏈路跟蹤的方法,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
Zipkin簡介
Zipkin是由Twitter開源的一款基于Java語言開發的分布式實時數據追蹤系統(Distributed Tracking System),其主要功能是采集來自各個系統的實時監控數據。該系統讓開發者可通過一個 Web 前端輕松的收集和分析數據,例如用戶每次請求服務的處理時間等,可方便的監測系統中存在的瓶頸。它大致可以分為三個核心概念
相對來說還是比較清晰的,如果用一張圖表示整體架構的話,大致如下圖所示(圖片來源于網絡)
在學習鏈路跟蹤的過程中會設計到相關概念,我們接下來介紹鏈路跟蹤幾個相關的概念
sr - cs = 請求在網絡上的耗時 ss - sr = 服務端處理請求的耗時 cr - ss = 回應在網絡上的耗時 cr - cs = 一次調用的整體耗時
關于zipkin概念相關的就介紹這么多,接下來我們介紹如何部署Zipkin。
部署ZipKin
    關于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相關文件之后我簡化了它的使用方式,最終修改如下
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文件,一條指令就可以運行起來
<PackageReference Include="zipkin4net" Version="1.5.0" /> <PackageReference Include="zipkin4net.middleware.aspnetcore" Version="1.5.0" />
其中-f是指定文件名稱,如果是docker-compose.yml則可以直接忽略文件名稱,當shell中出現如下界面
并且在瀏覽器中輸入http://localhost:9411/zipkin/出現如圖所示,則說明Zikpin啟動成功
整合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界面點擊進去可查看鏈路詳情
總結起來核心操作其實就兩個,一個是在發送請求的地方,使用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,目錄結構如下
ApiGateway為網關項目可以轉發針對OrderApi的請求,OrderApi和ProductApi用于模擬業務系統,這三個項目都集成了zipkin4net鏈路跟蹤,他們之間是通過Nacos實現服務的注冊和發現。
關于ASP.NET Core實現整合Zipkin鏈路跟蹤的方法就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。