Asp.Net Core&Jaeger實現鏈路追蹤

前言

隨著應用愈發複雜,請求的鏈路也愈發複雜,微服務化下,更是使得不同的服務分布在不同的機器,地域,語言也不盡相同。因此需要藉助工具幫助分析,跟蹤,定位請求中出現的若干問題,以此來保障服務治理,鏈路追蹤也就出現了。

OpenTracing協議

OpenTracing是一套分散式追蹤協議,與平台,語言、廠商無關的Trace協議,統一介面,使得開發人員能夠方便的添加或更換更換不同的分散式追蹤系統。

  • 語義規範 : 描述定義的數據模型 Tracer,Sapn 和 SpanContext 等;
  • 語義慣例 : 羅列出 tag 和 logging 操作時,標準的key值;

同樣作為分散式追蹤協議的還有OpenCensus,以及兩者的合併體OpenTelemetry

Jaeger介紹

Jaeger[ˈdʒɛgər]是Uber推出的一款開源分散式追蹤系統,兼容OpenTracing API,已在Uber大規模使用,且已加入CNCF開源組織(Cloud Native Computing Foundation-雲原生計算基金會)。其主要功能是聚合來自各個異構系統的實時監控數據。

圖片

Jager提供了一套完整的追蹤系統包括Jaeger-client、Jaeger-agent、Jaeger-collector、Database和Jaeger-query UI等基本組件。

  1. Jaeger-client:為不同開發語言實現了符合OpenTracing協議的客戶端。
  2. Jaeger-agent:一個監聽在UDP埠上接收鏈路數據的網路守護進程,它從應用程式收集,批處理,並發送給Collector,(也可以沒有這個,client直接上報)。
  3. Jaeger-collector:負責接收Jaeger-client或Jaeger-agent上報的調用鏈路數據,並通過處理管道運行它們,該管道驗證跟蹤、對它們進行索引、執行任何轉換並最終保存到記憶體或外部存儲系統中,供UI展示。
  4. Jaeger-query:查詢服務從存儲中檢索跟蹤並呈現 UI 來顯示它們。

Jaeger安裝

在個人使用或者測試上,Jaeger提供了jaegertracing/all-in-one鏡像,搭建過程十分簡單,數據存儲在記憶體中,但需要注意容器掛了後數據就沒了。

docker run -d -p 6831:6831/udp -p 16686:16686 jaegertracing/all-in-one:latest

創建容器運行後,可以訪問ip:16686查看Jaeger的儀錶面板

Jaeger應用

服務設計

簡化大部分服務設計,整個結構上差不多是如下所示,服務層常見金字塔結構,服務上下游明確,以避免服務間的循環依賴。

圖片

此處建立四個服務以及一個BFF網關層,以滿足服務同步調用,服務間上下游調用,以及服務間事件通訊。

  • JaegerDemo.BFF.Host
  • JaegerDemo.AService.Host
  • JaegerDemo.BService.Host
  • JaegerDemo.CService.Host
  • JaegerDemo.DService.Host

圖片

為這幾個服務設定期望如下

  • 執行Get請求時,從Gateway調用,請求A服務,在同步請求B和C服務,拿到結果組裝後對外返回。
  • 執行Post請求時,從Gateway調用,請求A服務,在發布事件到MQ中,D服務訂閱事件,數據寫入到Sqlite中。

Nuget包引用

  • Jaeger,用來上傳數據到Jaeger。
  • OpenTracing.Contrib.NetCore,基於OpenTracing.Net的增強,用來採集應用數據。
  • MassTransit和MassTransit.RabbitMQ,用來完成事件的發布訂閱。
<ItemGroup>
  <PackageReference Include="OpenTracing" Version="0.12.1" />
  <PackageReference Include="Jaeger" Version="1.0.3" />
  <PackageReference Include="MassTransit" Version="8.0.8" />
  <PackageReference Include="MassTransit.RabbitMQ" Version="8.0.8" />
</ItemGroup>

服務註冊

將服務註冊到容器中,設置上報地址,注意此處上報地址是UDP類型,因此在雲伺服器中開安全組時需要是UDP類型

builder.Services.AddOpenTracing();
builder.Services.AddSingleton<ITracer>(serviceProvider =>
{
    var serviceName = serviceProvider.GetRequiredService<IWebHostEnvironment>().ApplicationName;

    var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
    var sampler = new ConstSampler(sample: true);
    var reporter = new RemoteReporter.Builder()
            .WithLoggerFactory(loggerFactory)
            .WithSender(new UdpSender("xxx.xxx.xxx.xxx", 6831, 0))
            .Build();

    var tracer = new Tracer.Builder(serviceName)
        .WithLoggerFactory(loggerFactory)
        .WithSampler(sampler)
        .WithReporter(reporter)
        .Build();

    GlobalTracer.Register(tracer);

    return tracer;
});

此處我在雲伺服器中開放6831的埠,注意是UDP
圖片

Http請求

在BFF處發起Http調用A服務,以及A服務發起Http調用B和C。

[HttpGet]
public async Task<string> GetAsync()
{
    using var httpClient = _httpClientFactory.CreateClient();
    httpClient.BaseAddress = new Uri("//localhost:7001");

    var aServiceResult = await httpClient.GetStringAsync("/AValue");
    return aServiceResult;
}

請求發送完畢,從Jaeger的儀錶面板查看監控數據,能夠看到一個請求的發起時間,所經過的服務數量、所調用服務的依賴關係、消耗的時長等資訊。整個請求鏈路也就看到了,B和C的同步請求,A和B,A和C的上下游請求也明了。
圖片

Jaeger提供了有向圖描述請求鏈路,來方便理清節點間的通訊邊界,整個請求鏈路也便清晰了。

圖片

事件驅動

在BFF處發起Http調用A服務,以及A服務往RabbitMQ發送集成事件。

[HttpPost]
public async Task<IActionResult> CreateAsync(string value)
{
    var actionName = ControllerContext.ActionDescriptor.DisplayName;
    using var scope = _tracer.BuildSpan(actionName).StartActive(finishSpanOnDispose: true);
    var span = scope.Span.SetTag(Tags.SpanKind, Tags.SpanKindClient);
    var dictionary = new Dictionary<string, string>();
    _tracer.Inject(span.Context, BuiltinFormats.TextMap, new TextMapInjectAdapter(dictionary));

    // Do something
    // ...

    // Send integration event
    await _publishEndpoint.Publish(new ValueCreatedIntegrationEvent()
    {
        Value = value,
        TrackingKeys = dictionary
    });

    return Ok();
}

D服務中消費集成事件,並寫入Sqlite庫中

public async Task Consume(ConsumeContext<ValueCreatedIntegrationEvent> context)
{
    using var scope = TracingExtension.StartServerSpan(_tracer, context.Message.TrackingKeys, "Value created integration event handler");

    var value = context.Message.Value;
    Console.WriteLine($"Value:{value}");
    await _dbContext.ValueAggregates.AddAsync(new ValueAggregate(value));
    await _dbContext.SaveChangesAsync();
}

當請求發送完畢,事件消費完畢後,可以在Jaeger上看到在事件驅動下的鏈路調用過程,以及在調用過程中增加的tags和logs,寫入Sqlite的Sql。
圖片

在原有鏈路結構上,便又多了一個D服務。

圖片

參考

  1. //developer.aliyun.com/article/514488
  2. //www.cnblogs.com/wucy/p/13642289.html
  3. //www.cnblogs.com/catcher1994/p/10662999.html

2022-11-28,望技術有成後能回來看見自己的腳步

Tags: