• 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,用来完成事件的发布订阅。
    
      "OpenTracing" Version="0.12.1" />
      "Jaeger" Version="1.0.3" />
      "MassTransit" Version="8.0.8" />
      "MassTransit.RabbitMQ" Version="8.0.8" />
    
    

    服务注册

    将服务注册到容器中,设置上报地址,注意此处上报地址是UDP类型,因此在云服务器中开安全组时需要是UDP类型

    builder.Services.AddOpenTracing();
    builder.Services.AddSingleton(serviceProvider =>
    {
        var serviceName = serviceProvider.GetRequiredService().ApplicationName;
    
        var loggerFactory = serviceProvider.GetRequiredService();
        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 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 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,望技术有成后能回来看见自己的脚步

  • 相关阅读:
    hyperf 前置中间件 后置中间件
    PCB如何入门---一些经验与教训
    森林防火与林业资源综合解决方案
    HDLbits exercises 4 (MORE VERILOG FEATURES节选题)
    C#后缀表达式解析计算字符串公式
    CVE-2021-3560 Linux Polkit 权限提升漏洞
    【NSDictionary数组的遍历 Objective-C语言】
    Lambda表达式 ( Java 8新特性 )
    华为数通方向HCIP-DataCom H12-831题库(单选题:61-80)
    6-2 矩阵乘法函数(高教社,《Python编程基础及应用》习题4-11)
  • 原文地址:https://www.cnblogs.com/CKExp/p/16933756.html