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

2022-11-29 09:01:36

前言

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

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("https://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. https://developer.aliyun.com/article/514488
  2. https://www.cnblogs.com/wucy/p/13642289.html
  3. https://www.cnblogs.com/catcher1994/p/10662999.html

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