随着业务的发展,微服务系统会变得越来越大,各个服务之间的调用关系也会日趋复杂。一个WebApi请求,后方可能经历多个微服务以及数据库和MQ操作,在这个调用过程中,可能因为某一个服务节点出现延迟或者失败,而导致整个请求失败,此时极为需要全链路的调用监控。星尘Stardust提供了分布式全链路监控的解决方案。

演示系统:http://star.newlifex.com/,可使用微信、QQ、钉钉等多种第三方登录

典型页面:

应用监控 http://star.newlifex.com/Monitors/appDayStat?appId=5

今日监控 http://star.newlifex.com/Monitors/traceDayStat?appId=5&date=2022-05-11

接口监控 http://star.newlifex.com/Monitors/traceDayStat?appId=5&itemId=322

全局调用链 http://star.newlifex.com/trace?id=ac15452e1649035594650011be21ec

调用关系图 http://star.newlifex.com/graph?id=ac15452e1649035594650011be21ec

链路追踪的价值在于“关联”,终端用户、后端应用、云端组件(数据库、消息等)共同构成了链路追踪的轨迹拓扑大图。这张拓扑覆盖的范围越广,链路追踪能够发挥的价值就越大。而全链路追踪就是覆盖全部关联 IT 系统,能够完整记录用户行为在系统间调用路径与状态的最佳实践方案。

完整的全链路追踪可以为业务带来三大核心价值:端到端问题诊断,系统间依赖梳理,自定义标记透传。

星尘监控功能

星尘分布式全链路监控,主要功能点如下:

  • 功能强大。能够埋点统计调用次数、错误数、耗时等,适用于Web接口、RPC接口、数据库访问、Redis访问、消息队列访问等场景;
  • 简单易用。只有一个服务端和Web控制台,支持多种数据库(MySql/SQLite/Postgresql/SqlServer),免安装,解压后配置数据库连接即可跑起来;
  • 超低投入。计算能力下沉,无需ES等重型数据库,避免了大量的IT基础设施投入,1台2C4G的服务器和1台2C4G的MySql足够支持80多个应用每天4亿多的埋点数据;
  • 多维度分析。丰富的实时计算经验,按照应用、类别、埋点等多个维度进行实时分析,支持月度、每天、小时、5分钟等多种时间刻度,永久保存分析统计数据,主要接口趋势图等同于业务趋势;
  • 监控告警。支持按照应用配置告警阈值和告警机器人(企业微信、钉钉);
  • 业界标准。基于业界标准OpenTracing来设计,跨应用跟踪基于W3C的TraceContext来设计,支持任意语言开发的应用接入,支持不同语言应用系统的链路集成;


(应用监控趋势图)

(跨应用全链路追踪,Android客户端、WebApi、数据库、Redis、消息队列、用户自定义埋点)


调用关系图(多应用多资源方之间的关系)


调用关系环(另一种视角查看应用资源方的关系)


部署星尘服务端

源码:https://github.com/NewLifeX/Stardust

国内:https://gitee.com/NewLifeX/Stardust

可以下载源码,编译StarServer/StarWeb并得到两个输出,标准.NET6.0应用。


StarServer是星尘服务端,默认端口6600,可以通过aspnetcore的urls参数调整端口。服务端以webapi形式接收处理StarAgent星尘代理或者其它星尘客户端的数据请求,其中一部分接口属于监控子系统,接收埋点应用上报的链路监控数据。为提升系统可用性,建议服务端采用双节点部署,业务应用集成星尘客户端时,支持配置逗号分隔的多节点地址,来实现故障转移,例如:“http://star.newlifex.com:6600,http://106.14.11.143:6600


StarWeb是星尘Web管理平台,默认端口5000,可以通过aspnetcore的urls参数调整端口。管理平台是一个基于魔方开发的web后台系统,用于管理查看节点和应用埋点数据。Web管理平台仅用于查看数据和修改配置,无需多节点部署。


星尘支持多种数据库(MySql、SQLite、SqlServer、Oracle、Postgresql),默认SQLite。主要连接名如下:

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "StarServer": "http://127.0.0.1:6600,http://star.newlifex.com:6600",
  "ConnectionStrings": {
    "Stardust": {
      "connectionString": "Data Source=..\\Data\\Stardust.db",
      "providerName": "SQLite"
    },
    "StardustData": {
      "connectionString": "Data Source=..\\Data\\StardustData.db",
      "providerName": "SQLite"
    }
  }
}

实际生产环境中,星尘使用2C4G的MySql数据库,2C4G的服务器,支撑了80多个应用系统的埋点数据,每天共4亿多次调用跟踪。


使用监控系统

星尘系统演示:http://star.newlifex.com

星尘服务端试用版:http://star.newlifex.com:6600/


应用跟踪器

应用监控,应用跟踪器,管理着所有连接到星尘的监控埋点应用,默认自动添加新应用。

  • 启用,如果禁用,星尘将不再接受该应用提交的埋点数据;
  • 采样周期,默认每60秒上传一次采样数据;
  • 最大正常采样数,每个采样周期中,每个埋点选择的采样数据明细,用于建立调用链路;
  • 最大异常采样数,每个采样周期中,每个埋点选择的异常采样数据明细,用于分析系统错误;


应用统计

每个应用每天的总调用数、错误数、平均耗时、最大最小耗时,分类调用数(如接口数、Http请求、数据库、消息队列、Redis缓存、用户自定义埋点)

点击应用名,进入应用每日视图,可以看到该应用在这一天中,每一个操作名/埋点(接口)的调用情况,包括次数、错误数、耗时等。点击这里的种类,可以过滤只查看该类埋点操作的数据,不同种类埋点操作,采用不同颜色显示。

再次点击应用名,可以看到该应用每天的整体调用情况

埋点跟踪统计

点击操作名(埋点/接口),可以查看该埋点操作近90天的每日统计数据,主要有调用次数、错误数、耗时等。上方的“7天”,可以查看该埋点仅7天的每小时统计数据。上方的“24小时”,可以查看该埋点近24小时的每5分钟统计数据,5分钟数据比较多,默认只会保留3天,可以在服务端配置文件中调整。

全链路追踪

每个埋点数据行,都带有“跟踪”链接,可以查看该埋点的某一次调用链路。

如上图,同一个调用链上的多次埋点,具有相同traceId,跟踪视图显示该traceId的前后调用关系,甚至跨多个应用系统,穿越http接口和消息队列。鼠标移到埋点操作名上面,可以看到该埋点的数据标签,或者异常信息。例如,数据库埋点的数据标签就是sql语句,消息队列埋点的数据标签就是消息内容。

链路追踪明细数据默认保存3天,可以在星尘服务端配置文件调整。

异常分析

对于有错误次数的埋点,可以从总次数点击进去,找到错误采样,然后进行跟踪查看。如果有多次错误采样,不方便查找,可以从埋点跟踪统计进入五分钟视图后再找。


应用接入监控系统

微服务系统中的调用采样数据及其庞大,星尘监控通过计算能力下沉来解决这个问题。在业务系统埋点模块内部对埋点数据进行初步聚合,再挑选若干采样数据,在每个采样周期(默认60秒)结束后批量上传到星尘服务端的收集器。收集器落库保存数据后,再次进行聚合,并进行级联统计分析。

任何项目想要接入星尘监控,都需要从nuget中引用 NewLife.Stardust 组件库,实例化StarTracer跟踪器。NewLife.Cube.Core、NewLife.Redis、NewLife.RocketMQ、AntJob等组件都可以从nuget中找到。

星尘监控支持WebApi、HttpClient、Redis、XCode、AntJob等场合的自动埋点追踪,也支持用户自定义埋点。

WebApi应用接入监控

netcore项目在Startup的ConfigureServices中配置初始化 TracerMiddleware.Tracer

public void ConfigureServices(IServiceCollection services)
{
    // APM跟踪器
    var apmServer = "http://star.newlifex.com:6600,http://106.14.11.143:6600";
    var tracer = new StarTracer(apmServer) { Log = XTrace.Log };
    DefaultTracer.Instance = tracer;
    ApiHelper.Tracer = tracer;
    DAL.GlobalTracer = tracer;
    TracerMiddleware.Tracer = tracer;

    // 用于Controller或Service等取得ITracer做自定义埋点
    services.AddSingleton<ITracer>(tracer);

    services.AddControllersWithViews();

    // 引入魔方。仅演示,非必须
    services.AddCube();
}

Configure中加入使用TracerMiddleware中间件

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    var set = Setting.Current;
    if (env.IsDevelopment() || set.Debug)
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseHsts();
    }

    //app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    // 使用TracerMiddleware中间件
    app.UseMiddleware<TracerMiddleware>();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

代码写死或者从配置中心读取星尘服务端StarServer的地址,支持配置逗号分隔的多节点地址,来实现故障转移,例如:“http://star.newlifex.com:6600,http://106.14.11.143:6600”。


services.AddSingleton<ITracer>(tracer) 直接注入跟踪器实例,便于后面集成使用,推荐使用。

DefaultTracer.Instance 是静态属性,用于没有DI的较老代码的接入,不推荐使用。

ApiHelper.Tracer 开放所有HttpClient扩展的埋点追踪。

DAL.GlobalTracer 开放XCode所有数据库访问的埋点追踪。

TracerMiddleware.Tracer 对所有Web请求进行埋点追踪。


这里的TracerMiddleware需要nuget引入魔方 NewLife.Cube.Core 。也可以直接使用以下代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using NewLife;
using NewLife.Log;
using HttpContext = Microsoft.AspNetCore.Http.HttpContext;

namespace WebApi.Middlewares
{
    /// <summary>性能跟踪中间件</summary>
    public class TracerMiddleware
    {
        private readonly RequestDelegate _next;

        /// <summary>跟踪器</summary>
        public static ITracer Tracer { get; set; }

        /// <summary>实例化</summary>
        /// <param name="next"></param>
        public TracerMiddleware(RequestDelegate next)
        {
            _next = next ?? throw new ArgumentNullException(nameof(next));
        }

        /// <summary>调用</summary>
        /// <param name="ctx"></param>
        /// <returns></returns>
        public async Task Invoke(HttpContext ctx)
        {
            // APM跟踪
            //var span = Tracer?.NewSpan(ctx.Request.Path);
            ISpan span = null;
            if (Tracer != null)
            {
                var action = GetAction(ctx);
                if (!action.IsNullOrEmpty())
                {
                    span = Tracer.NewSpan(action);
                    span.Tag = ctx.Connection?.RemoteIpAddress + " " + ctx.Request.GetEncodedUrl();
                    var headers = ctx.Request.Headers.ToDictionary(e => e.Key, e => (Object)e.Value);
                    span.Detach(headers);
                }
            }

            try
            {
                await _next.Invoke(ctx);

                // 根据状态码识别异常
                if (span != null)
                {
                    var code = ctx.Response.StatusCode;
                    if (code >= 400) span.SetError(new HttpRequestException($"Http Error {code} {(HttpStatusCode)code}"), null);
                }
            }
            catch (Exception ex)
            {
                span?.SetError(ex, null);

                throw;
            }
            finally
            {
                span?.Dispose();
            }
        }

        /// <summary>忽略的后缀</summary>
        public static String[] ExcludeSuffixes { get; set; } = new[] {
            ".html", ".htm", ".js", ".css", ".map", ".png", ".jpg", ".gif", ".ico",  // 脚本样式图片
            ".woff", ".woff2", ".svg", ".ttf", ".otf", ".eot"   // 字体
        };

        private static String GetAction(HttpContext ctx)
        {
            var p = ctx.Request.Path + "";
            if (p.EndsWithIgnoreCase(ExcludeSuffixes)) return null;

            var ss = p.Split('/');
            if (ss.Length == 0) return p;

            // 如果是魔方格式,保留3段
            if (ss.Length >= 4 && ss[3].EqualIgnoreCase("detail", "add", "edit")) p = "/" + ss.Take(4).Join("/");

            return p;
        }
    }
}

应用启动时,大概15秒后,以下日志表示接入成功。(需要在Main入口第一行加上 XTrace.UseConsole() )


Web应用跟踪视图如下


消息队列应用接入监控

.NET最爱的Redis消息队列,NewLife.Redis 集成了链路追踪,仅需要在实例化FullRedis对象时,指定Tracer属性。

var redis = new FullRedis { Tracer = tracer, Timeout = 15000, Retry = 5, Log = XTrace.Log };

此外,NewLife.RocketMQNewLife.MQTT 都集成了链路追踪支持。


消息队列跟踪视图如下

数据调度应用接入监控

蚂蚁调度 AntJob 集成了链路追踪,仅需要在实例化调度器时指定Tracer属性。

var set = AntSetting.Current;

var server = _getConfig("antServer");
if (!server.IsNullOrEmpty())
{
    set.Server = server;
    set.Save();
}

// 实例化调度器
var sc = new Scheduler
{
    Tracer = DefaultTracer.Instance,

    // 使用分布式调度引擎替换默认的本地文件调度
    Provider = new NetworkJobProvider
    {
        Server = set.Server,
        AppID = set.AppID,
        Secret = set.Secret,
        Debug = false
    }
};


数据调度应用跟踪视图如下


用户自定义埋点

在关键业务方法内部,我们需要做一些自定义埋点。通过DI注入或者DefaultTracer.Instance拿到ITracer对象,借助NewSpan方法,即可得到一个埋点实例ISpan,参数就是埋点操作名,span开始到释放就是这一次埋点的耗时。

using var span = _tracer?.NewSpan("CreateOrder", orderModel);
try
{
    //todo CreateOrder
}
catch (Exception ex)
{
    span?.SetError(ex, null);
    throw;
}

如上,使用using语法,让span离开作用域时自动Dispose销毁,计算耗时。NewSpan第二个参数是数据标签Tag,如果这一次埋点span有幸成为采样对象送给星尘服务端,那么Tag将会在链路追踪视图里面得以显示(鼠标移到操作名上)。

如果业务代码抛出异常,需要调用SetError方法指定这一次埋点为异常采样,并设置ex异常信息,该信息会送给星尘服务端,用于查看异常详情。

SetError 不是必须的,如果异常时不调用SetError,还是会记入监控统计,只是认为这次调用成功,并且拿不到异常信息。此时有最简化的自定义埋点代码:

using var span = _tracer?.NewSpan("CreateOrder", orderModel);


NewSpan会在进程中建立埋点的父子关系,无需用户处理。而跨应用集成调用链,则需要一些额外操作。

常见注入和提取扩展


调用另一个系统的WebApi时,按照W3C标准,需要在Http请求头中加上 traceparent ,内容是 span.ToString(),格式:00-traceId-spanId-00

spanId是埋点唯一标识,一般是16字符hex编码;

traceId是链路唯一标识,一般是32字符hex编码,具有相同traceId的埋点采样,构成一个完整调用链;

调用方通过span.Attach把span注入到http请求头,接收方从http请求头中解码得到traceId,魔方的TracerMiddleware.cs中有实现该功能。该方案使得不同应用的埋点操作具有相同的链路标识traceId,从而构成一个完整调用链。


NewLife.Redis消息队列的跨系统集成,本质上是在发布消息时,向json集合中注入一个traceparent的字段,消费时读取,从而共用traceId,构成完成调用链。