1. 程式人生 > >ASP.NET Core整合Zipkin鏈路跟蹤

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負責展示採集的鏈路資料,及系統之間的依賴關係。 相對來說還是比較清晰的,如果用一張圖表示整體架構的話,大致如下圖所示(圖片來源於網路)![](https://img2020.cnblogs.com/blog/2042116/202009/2042116-20200909215226302-608409456.png)在學習鏈路跟蹤的過程中會設計到相關概念,我們接下來介紹鏈路跟蹤幾個相關的概念 + TranceId,一般一次全域性的請求會有一個唯一的TraceId,用於代表一次唯一的請求。比如我請求了訂單管理系統,而訂單管理系統內部還呼叫了商品管理系統,而商品管理系統還呼叫了快取系統或資料庫系統。但是對全域性或外部來說這是一次請求,所以會有唯一的一個TraceId。 + SpanId,雖然全域性的來說是一次大的請求,但是在這個鏈路中內部間還會發起別的請求,這種內部間的每次請求會生成一個SpanId。 + 如果將整條鏈路串聯起來的話,我們需要記錄全域性的TraceId,代表當前節點的SpanId和發起對當前節點呼叫的的父級ParentId。 然後基於鏈路跟蹤的核心概念,然後介紹一下Zipkin衍生出來了幾個相關概念 + cs:Clent Sent 客戶端發起請求的時間,比如 dubbo 呼叫端開始執行遠端呼叫之前。 + cr:Client Receive 客戶端收到處理完請求的時間。 + ss:Server Receive 服務端處理完邏輯的時間。 + sr:Server Receive 服務端收到呼叫端請求的時間。 ``` 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](https://github.com/openzipkin/zipkin/tree/master/docker),官方使用的方式相對比較複雜,下載下來docker-compose相關檔案之後我簡化了它的使用方式,最終修改如下 ```yml 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檔案,一條指令就可以執行起來 ``` docker-compose -f docker-compose-elasticsearch7.yml up ``` 其中-f是指定檔名稱,如果是docker-compose.yml則可以直接忽略檔名稱,當shell中出現如下介面 並且在瀏覽器中輸入[http://localhost:9411/zipkin/](http://localhost:9411/zipkin/)出現如圖所示,則說明Zikpin啟動成功 ### 整合ASP.NET Core ZipKin啟動成功之後,我們就可以將程式中的資料採集到Zipkin中去了,我新建了兩個ASP.NET Core的程式,一個是OrderApi,另一個是ProductApi方便能體現出呼叫鏈路,其中OrderApi呼叫ProductApi介面,在兩個專案中分別引入Zipkin依賴包 ``` ``` 其中zipkin4net為核心包,zipkin4net.middleware.aspnetcore是整合ASP.NET Core的程式包。然後我們在Startup檔案中新增如下方法 ```cs 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中配置如下 ```cs public void ConfigureServices(IServiceCollection services) { //由於我使用了Nacos作為服務註冊中心 services.AddNacosAspNetCore(Configuration); services.AddScoped(); services.AddHttpClient(ServiceName.ProductService,client=> { client.BaseAddress = new Uri($"http://{ServiceName.ProductService}"); }) .AddHttpMessageHandler() //引入zipkin trace跟蹤httpclient請求,名稱配置當前服務名稱即可 .AddHttpMessageHandler(provider =>TracingHandler.WithoutInnerHandler(Configuration["nacos:ServiceName"])); services.AddControllers(); } ``` 如果是直接是使用HttpClient的形式呼叫則可以採用以下方式 ```cs using (HttpClient client = new HttpClient(new TracingHandler("OrderApi"))) { } ``` 然後我們在OrderApi中寫一段呼叫ProductApi的程式碼 ```cs [Route("orderapi/[controller]")] public class OrderController : ControllerBase { private List orderDtos = new List(); private readonly IHttpClientFactory _clientFactory; public OrderController(IHttpClientFactory clientFactory) { orderDtos.Add(new OrderDto { Id = 1, TotalMoney=222,Address="北京市",Addressee="me",From="淘寶",SendAddress="武漢" }); _clientFactory = clientFactory; } /// /// 獲取訂單詳情介面 ///
/// 訂單id /// [HttpGet("getdetails/{id}")] public async Task 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>(result); return orderDetailDto; } return orderDto; } } ``` 在ProductApi中我們只需要編寫呼叫RegisterZipkinTrace方法即可,和OrderApi一樣,我們就不重複貼上了。因為ProductApi不需要呼叫別的服務,所以可以不必使用整合HttpClient,只需要提供簡單的介面即可 ```cs [Route("productapi/[controller]")] public class ProductController : ControllerBase { private List productDtos = new List(); public ProductController() { productDtos.Add(new ProductDto { Id = 1,Name="酒精",Price=22.5m }); productDtos.Add(new ProductDto { Id = 2, Name = "84消毒液", Price = 19.9m }); } /// /// 獲取所有商品資訊 /// /// [HttpGet("getall")] public IEnumerable 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](https://github.com/openzipkin/zipkin4net),我們找到TracingHandler類所在的位置[[點選檢視原始碼