ASP.NET Core整合Zipkin鏈路跟蹤

前言

    在平常使用ASP.NET Core的開發或學習中,若是有須要使用鏈路跟蹤系統,大多數狀況下會優先選擇SkyAPM。咱們以前也說過SkyAPM設計確實比較優秀,巧妙的利用DiagnosticSource診斷跟蹤日誌,能夠作到對項目無入侵方式的集成。其實還有一款比較優秀的鏈路跟蹤系統,也能夠支持ASP.NET Core,叫Zipkin。它相對於SkyWalking來講相對輕量級,使用相對來講比較偏原生的方式,並且支持Http的形式查詢和提交鏈路數據。由於咱們老是但願能擁有多一種的解決方案方便對比和參考,因此接下來咱們就來學習一下關於Zipkin的使用方式。html

Zipkin簡介

    Zipkin是由Twitter開源的一款基於Java語言開發的分佈式實時數據追蹤系統(Distributed Tracking System),其主要功能是採集來自各個系統的實時監控數據。該系統讓開發者可經過一個 Web 前端輕鬆的收集和分析數據,例如用戶每次請求服務的處理時間等,可方便的監測系統中存在的瓶頸。它大體能夠分爲三個核心概念前端

  • 首先是上報端,它主要經過代碼的形式集成到程序中,用於上報Trace數據到Collector端。
  • Collector負責接收客戶端發送過來的數據,保存到內存或外部存儲系統中,供UI展現。
  • 存儲端能夠是基於zipkin內存徹底不依賴外部存儲的In-Memory形式或依賴外部存儲系統的形式,通常採用外部存儲系統存儲鏈路數據,畢竟內存有限。它可支持的存儲數據庫有MySQL、Cassandra、Elasticsearch。
  • UI負責展現採集的鏈路數據,及系統之間的依賴關係。
    相對來講仍是比較清晰的,若是用一張圖表示總體架構的話,大體以下圖所示(圖片來源於網絡)在學習鏈路跟蹤的過程當中會設計到相關概念,咱們接下來介紹鏈路跟蹤幾個相關的概念
  • 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。node

部署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相關文件以後我簡化了它的使用方式,最終修改以下git

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文件,一條指令就能夠運行起來github

docker-compose -f docker-compose-elasticsearch7.yml up

其中-f是指定文件名稱,若是是docker-compose.yml則能夠直接忽略文件名稱,當shell中出現以下界面而且在瀏覽器中輸入http://localhost:9411/zipkin/出現如圖所示,則說明Zikpin啓動成功docker

整合ASP.NET Core

ZipKin啓動成功以後,咱們就能夠將程序中的數據採集到Zipkin中去了,我新建了兩個ASP.NET Core的程序,一個是OrderApi,另外一個是ProductApi方便能體現出調用鏈路,其中OrderApi調用ProductApi接口,在兩個項目中分別引入Zipkin依賴包shell

<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中配置以下json

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的形式調用則能夠採用如下方式bootstrap

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實現服務的註冊和發現。這個演示Demo我本地是能夠直接運行成功的,若是有下載下來運行不成功的,能夠評論區給我留言。因爲博客園有文件上傳大小的限制,因此我將Demo上傳到了百度網盤中
下載連接: https://pan.baidu.com/s/1jPHyXKV9DAK_oEYQz3xtzA 提取碼: a7u5

總結

    以上就是關於Zipkin以及ASP.NET Core整合Zipkin的所有內容,但願能給你們帶來必定的幫助。若是你有實際須要也能夠繼續自行研究。Zipkin相對於咱們經常使用的Skywalking並且,它的使用方式比較原生,許多操做都須要自行經過代碼操做,而SkyAPM能夠作到對代碼無入侵的方式集成。Skywalking是一款APM(應用性能管理),鏈路跟蹤只是它功能的一部分。而Zipkin是一款專一於鏈路跟蹤的系統,我的感受就鏈路跟蹤這一塊而言,Zipkin更輕量級(若是都使用ES做爲存儲的數據庫的話,Skywalking會生成一堆索引,Zipkin默認是天天建立一個索引),並且鏈路信息檢索、詳情展現、鏈路數據上報形式等相對於Skywalking形式也更豐富一些。可是總體而言Skywalking更強大,好比應用監控、調用分析、集成方式等。技術並沒有好壞之分,適合本身的纔是更好的,多一個解決方案,就多一個解決問題的思路,我以爲這是對於咱們程序開發人員來講都應該具有的認知。

👇歡迎掃碼關注個人公衆號👇
相關文章
相關標籤/搜索