解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
大石头 authored at 2018-05-15 21:21:05
24.88 KiB
X
# 链路追踪ITracer ## 概述 可观测性是衡量现代应用质量的核心指标之一。NewLife.Core 提供了一套完整的链路追踪规范:`ITracer`/`ISpan`,用于实现轻量级 APM(Application Performance Monitoring)。 与传统 APM 系统不同,NewLife 的链路追踪采用**本地采样+统计**的方式: - 在本地完成数据采样和初步统计 - 仅上报统计数据和少量采样样本 - 极大节省网络传输和存储成本 NewLife 全系列项目(30+ 组件)均内置了 `ITracer` 埋点,开发者可以: 1. 无侵入地获取组件运行指标 2. 接入星尘监控平台实现分布式追踪 3. 基于规范扩展自定义埋点 **Nuget 包**: `NewLife.Core` **源码**: [NewLife.Core/Log/ITracer.cs](https://github.com/NewLifeX/X/blob/master/NewLife.Core/Log/ITracer.cs) --- ## 快速入门 ### 基础用法 最简单的埋点示例: ```csharp using var span = tracer?.NewSpan("操作名称"); ``` 使用 `using` 关键字确保 span 在作用域结束时自动完成,记录耗时和次数。 ### 完整示例 以下是网络接收数据的埋点示例: ```csharp private void Ss_Received(Object? sender, ReceivedEventArgs e) { var ns = (this as INetSession).Host; var tracer = ns?.Tracer; // 创建埋点,记录接收操作 using var span = tracer?.NewSpan($"net:{ns?.Name}:Receive", e.Message); try { OnReceive(e); } catch (Exception ex) { // 标记错误并记录异常信息 span?.SetError(ex, e.Message ?? e.Packet); throw; } } ``` 这个示例演示了: - **创建埋点**:使用动态生成的名称 - **数据标签**:第二个参数 `e.Message` 作为数据标签 - **异常处理**:通过 `SetError` 标记异常埋点 - **自动记录**:`using` 语句自动记录耗时 通过这个埋点,我们可以: - 统计接收数据包的次数 - 记录每次接收的耗时 - 统计异常发生次数 - 查看采样数据和标签 --- ## 核心接口 ### ITracer 接口 性能跟踪器,轻量级 APM 规范的核心接口。 ```csharp public interface ITracer { #region 属性 /// <summary>采样周期。单位秒,默认15秒</summary> Int32 Period { get; set; } /// <summary>最大正常采样数。采样周期内,最多只记录指定数量的正常事件,默认1</summary> Int32 MaxSamples { get; set; } /// <summary>最大异常采样数。采样周期内,最多只记录指定数量的异常事件,默认10</summary> Int32 MaxErrors { get; set; } /// <summary>超时时间。超过该时间时强制采样,单位毫秒,默认5000</summary> Int32 Timeout { get; set; } /// <summary>最大标签长度。超过该长度时将截断,默认1024字符</summary> Int32 MaxTagLength { get; set; } /// <summary>向http/rpc请求注入TraceId的参数名,为空表示不注入,默认W3C标准的traceparent</summary> String? AttachParameter { get; set; } #endregion /// <summary>建立Span构建器</summary> /// <param name="name">操作名称,用于标识不同的埋点类型</param> ISpanBuilder BuildSpan(String name); /// <summary>开始一个Span</summary> /// <param name="name">操作名称,用于标识不同的埋点类型</param> ISpan NewSpan(String name); /// <summary>开始一个Span,指定数据标签</summary> /// <param name="name">操作名称,用于标识不同的埋点类型</param> /// <param name="tag">数据标签,记录关键参数信息</param> ISpan NewSpan(String name, Object? tag); /// <summary>截断所有Span构建器数据,重置集合</summary> ISpanBuilder[] TakeAll(); } ``` #### 关键属性说明 | 属性 | 默认值 | 说明 | |-----|-------|------| | `Period` | 15秒 | 采样周期,定期上报数据的时间间隔 | | `MaxSamples` | 1 | 每个周期内记录的正常采样数,用于绘制调用依赖关系 | | `MaxErrors` | 10 | 每个周期内记录的异常采样数,用于问题诊断 | | `Timeout` | 5000ms | 超时阈值,超过此时间的操作会被强制采样 | | `MaxTagLength` | 1024 | 标签最大长度,超过会被截断 | | `AttachParameter` | "traceparent" | HTTP/RPC 请求中注入 TraceId 的参数名,遵循 W3C 标准 | 这些参数通常由星尘监控中心动态下发调整,无需手动配置。 #### 核心方法 - **`NewSpan(String name)`**: 最常用的方法,创建一个新埋点 - **`NewSpan(String name, Object? tag)`**: 创建埋点并附加数据标签 - **`BuildSpan(String name)`**: 获取或创建 SpanBuilder,用于高级场景 ### ISpan 接口 性能跟踪片段,代表一个具体的埋点实例。 ```csharp public interface ISpan : IDisposable { /// <summary>唯一标识。随线程上下文、Http、Rpc传递,作为内部片段的父级</summary> String Id { get; set; } /// <summary>埋点名。用于标识不同类型的操作</summary> String Name { get; set; } /// <summary>父级片段标识。用于构建调用链</summary> String? ParentId { get; set; } /// <summary>跟踪标识。可用于关联多个片段,建立依赖关系,随线程上下文、Http、Rpc传递</summary> String TraceId { get; set; } /// <summary>开始时间。Unix毫秒时间戳</summary> Int64 StartTime { get; set; } /// <summary>结束时间。Unix毫秒时间戳</summary> Int64 EndTime { get; set; } /// <summary>用户数值。记录数字型标量,如每次数据库操作行数,星尘平台汇总统计</summary> Int64 Value { get; set; } /// <summary>数据标签。记录一些附加数据,如请求参数、响应结果等</summary> String? Tag { get; set; } /// <summary>错误信息。记录异常消息</summary> String? Error { get; set; } /// <summary>设置错误信息,ApiException除外</summary> void SetError(Exception ex, Object? tag = null); /// <summary>设置数据标签。内部根据最大长度截断</summary> void SetTag(Object tag); /// <summary>抛弃埋点,不计入采集</summary> void Abandon(); } ``` #### 关键属性 | 属性 | 类型 | 说明 | |-----|------|------| | `Id` | String | Span 唯一标识,遵循 W3C 标准 | | `ParentId` | String? | 父级 Span ID,用于构建调用树 | | `TraceId` | String | 跟踪链标识,同一调用链中的所有 Span 共享相同 TraceId | | `StartTime` | Int64 | 开始时间(Unix 毫秒) | | `EndTime` | Int64 | 结束时间(Unix 毫秒) | | `Value` | Int64 | 用户数值,可记录业务指标(如数据库行数) | | `Tag` | String? | 数据标签,记录请求参数、响应数据等 | | `Error` | String? | 错误信息 | #### 核心方法 - **`SetError(Exception ex, Object? tag)`**: 标记异常,`ApiException` 类型会被特殊处理 - **`SetTag(Object tag)`**: 设置数据标签,支持多种类型自动序列化 - **`Abandon()`**: 丢弃当前埋点,常用于过滤无效请求(如404扫描)。判定条件不要只看状态码,业务接口主动返回的404有观测价值,应结合“是否命中路由终结点”判断 --- ## 使用最佳实践 ### 1. 注入 ITracer 在 ASP.NET Core 应用中,通过星尘扩展注入: ```csharp using NewLife.Stardust.Extensions; var builder = WebApplication.CreateBuilder(args); // 注入星尘,包含 ITracer 实现 builder.Services.AddStardust(); ``` 详见:[星尘分布式服务平台](https://newlifex.com/blood/stardust) ### 2. 获取 ITracer 实例 有多种方式获取 `ITracer`: ```csharp // 方式1:构造函数注入(推荐) public class MyService { private readonly ITracer _tracer; public MyService(ITracer tracer) { _tracer = tracer; } } // 方式2:属性注入 public class MyService { public ITracer? Tracer { get; set; } } // 方式3:使用全局静态实例 var tracer = DefaultTracer.Instance; ``` ### 3. 创建埋点 #### 基础埋点 ```csharp using var span = _tracer?.NewSpan("MyOperation"); // 业务逻辑 ``` #### 带数据标签的埋点 ```csharp var request = new { UserId = 123, Action = "Query" }; using var span = _tracer?.NewSpan("UserQuery", request); // 业务逻辑 ``` #### 带异常处理的埋点 ```csharp using var span = _tracer?.NewSpan("DatabaseQuery"); try { // 执行数据库查询 var result = await ExecuteQueryAsync(); } catch (Exception ex) { span?.SetError(ex, "查询失败"); throw; } ``` #### 记录业务指标 ```csharp using var span = _tracer?.NewSpan("BatchProcess"); var count = ProcessRecords(); span.Value = count; // 记录处理的记录数 ``` ### 4. 定时任务埋点示例 ```csharp public class DataRetentionService : IHostedService { private readonly ITracer _tracer; private readonly StarServerSetting _setting; private TimerX? _timer; public DataRetentionService(StarServerSetting setting, ITracer tracer) { _setting = setting; _tracer = tracer; } public Task StartAsync(CancellationToken cancellationToken) { // 每600秒执行一次,随机延迟启动 _timer = new TimerX(DoWork, null, DateTime.Today.AddMinutes(Rand.Next(60)), 600 * 1000) { Async = true }; return Task.CompletedTask; } private void DoWork(Object? state) { var set = _setting; if (set.DataRetention <= 0) return; var time = DateTime.Now.AddDays(-set.DataRetention); var time2 = DateTime.Now.AddDays(-set.DataRetention2); var time3 = DateTime.Now.AddDays(-set.DataRetention3); // 创建埋点,记录清理任务 using var span = _tracer?.NewSpan("DataRetention", new { time, time2, time3 }); try { // 删除旧数据 var rs = AppMinuteStat.DeleteBefore(time); XTrace.WriteLine("删除[{0}]之前的AppMinuteStat共:{1:n0}", time, rs); rs = TraceMinuteStat.DeleteBefore(time); XTrace.WriteLine("删除[{0}]之前的TraceMinuteStat共:{1:n0}", time, rs); } catch (Exception ex) { span?.SetError(ex, null); } } public Task StopAsync(CancellationToken cancellationToken) { _timer?.TryDispose(); return Task.CompletedTask; } } ``` ### 5. 过滤无效请求 在 Web 应用中,经常遇到各种扫描请求,可以使用 `Abandon()` 丢弃这些埋点: ```csharp public IActionResult ProcessRequest() { using var span = _tracer?.NewSpan("WebRequest"); // 检测到无效请求(如404扫描) if (IsInvalidScan()) { span?.Abandon(); // 丢弃埋点,不计入统计 return NotFound(); } // 正常处理 return Ok(); } ``` > 判定“无效请求”不要只看状态码:业务接口主动返回的404(如查询的资源不存在)是正常业务行为,埋点有观测价值; > 而扫描探测请求不会命中任何路由终结点。星尘 SDK 的 `TracerMiddleware` 即按「状态码404 且 `ctx.GetEndpoint() == null`」 > 判定,并可通过 `TracerMiddleware.ShouldAbandon` 接管默认规则,其它框架可参照该思路。 `Abandon()` 的作用边界: - **只丢弃当前片段**:同一条链路里由其它跟踪器(`DiagnosticListener`、`OpenTelemetryCollector`、XCode)产生的片段不受影响,需要整链过滤时应在采集端按 TraceId 处理。 - **只省上报不省创建**:埋点对象、请求头标签等已经产生,扫描洪峰下的创建开销依然存在;要在创建前拦截,可让 `ITracer.Resolver` 对探测路径返回空以跳过埋点。 - **星尘档是落地能力**:`DefaultSpan` 会归还片段上下文;OpenTelemetry 档的活动归 SDK 管理,不作为承诺,详见[收口记录](/NewLife/X/Blob/dev/Doc/#收口记录)。 --- ## 分布式链路追踪 ### 跨服务传递 ISpan 提供了扩展方法,支持通过 HTTP/RPC 传递跟踪上下文: ```csharp // HTTP 请求注入 var request = new HttpRequestMessage(HttpMethod.Get, url); span?.Attach(request); // 注入 traceparent 头 await httpClient.SendAsync(request); // RPC 调用注入 var args = new { UserId = 123 }; span?.Attach(args); // 注入跟踪参数 await rpcClient.InvokeAsync("Method", args); ``` 服务端收到请求后,会自动解析 `traceparent` 参数,恢复跟踪上下文。 ### 调用链构建 同一个 `TraceId` 下的所有 Span 会自动形成调用链: ``` TraceId: ac1b1e8617342790000015eb0ea2a6 ├─ DataRetention (父) ├─ SQL:AppMinuteStat.DeleteBefore (子) ├─ SQL:TraceMinuteStat.DeleteBefore (子) └─ SQL:TraceHourStat.DeleteBefore (子) ``` 在星尘监控平台可以查看: - [调用链火焰图](https://star.newlifex.com/trace?id=ac1b1e8617342790000015eb0ea2a6) - [调用链日志视图](https://star.newlifex.com/trace?id=ac1b1e8617342790000015eb0ea2a6&layout=detail) --- ## 监控与分析 ### 查看统计数据 在星尘监控平台可以查看埋点统计: - 调用总次数 - 异常次数 - 平均耗时、最大耗时、最小耗时 - QPS(每秒请求数) 示例:[DataRetention 埋点统计](https://star.newlifex.com/Monitors/traceDayStat?appId=4&itemId=284) ### 采样策略 ITracer 采用智能采样策略: 1. **正常采样**:每个周期内最多保留 `MaxSamples` 个正常样本(默认1个) 2. **异常采样**:每个周期内最多保留 `MaxErrors` 个异常样本(默认10个) 3. **超时采样**:耗时超过 `Timeout` 的操作强制采样 4. **全链路采样**:设置 `TraceFlag` 的调用链全量采样 ### 本地统计机制 每个埋点名对应一个 `SpanBuilder`,用于累加统计: - **Total**: 总次数 - **Errors**: 异常次数 - **Cost**: 总耗时 - **MaxCost**: 最大耗时 - **MinCost**: 最小耗时 每个采样周期结束后,`SpanBuilder` 数据打包上报,然后重置计数器。 --- ## 高级特性 ### 埋点命名规范 建议使用分层命名,便于分类统计: ```csharp // 网络层 tracer.NewSpan("net:{协议}:Receive"); tracer.NewSpan("net:tcp:Send"); // 数据库层 tracer.NewSpan("db:{表名}:Select"); tracer.NewSpan("db:User:Insert"); // 业务层 tracer.NewSpan("biz:{模块}:{操作}"); tracer.NewSpan("biz:Order:Create"); // 外部调用 tracer.NewSpan("http:{服务}:{方法}"); tracer.NewSpan("http:PaymentApi:Pay"); ``` ### 动态参数调整 可以动态调整采样参数: ```csharp var tracer = DefaultTracer.Instance; tracer.Period = 30; // 改为30秒周期 tracer.MaxSamples = 5; // 增加正常采样数 tracer.Timeout = 10000; // 超时阈值改为10秒 tracer.MaxTagLength = 2048; // 标签长度改为2K ``` 通常这些参数由星尘监控中心统一下发。 ### 自定义 ITracer 实现 可以实现自己的 `ITracer`: ```csharp public class CustomTracer : ITracer { public Int32 Period { get; set; } = 15; public Int32 MaxSamples { get; set; } = 1; public Int32 MaxErrors { get; set; } = 10; // ... 实现接口方法 public ISpan NewSpan(String name) { // 自定义埋点创建逻辑 var span = new CustomSpan { Name = name }; span.Start(); return span; } } // 注册自定义实现 DefaultTracer.Instance = new CustomTracer(); ``` --- ## 性能优化 > **接入开销实测数据**:见[《APM埋点性能测试报告》](/NewLife/X/Blob/dev/Doc/性能/APM埋点性能测试报告.md)——不用 APM 为零成本;一次普通埋点约 0.13 µs / 72 B;OpenTelemetry 输出空埋点 235 ns / 672 B。 ### 对象池 `DefaultTracer` 内置了对象池,复用 `ISpanBuilder` 和 `ISpan` 实例: ```csharp public IPool<ISpanBuilder> BuilderPool { get; } public IPool<ISpan> SpanPool { get; } ``` 频繁创建的埋点对象会自动归还对象池,减少 GC 压力。 ### 标签长度控制 对于大型对象,建议控制标签内容: ```csharp // 不推荐:记录整个大对象 span.SetTag(largeObject); // 推荐:只记录关键信息 span.SetTag(new { Id = obj.Id, Name = obj.Name }); ``` ### 条件埋点 对于非关键路径,可以条件性创建埋点: ```csharp ISpan? span = null; if (_tracer != null && IsImportantOperation()) { span = _tracer.NewSpan("ImportantOp"); } try { // 业务逻辑 } finally { span?.Dispose(); } ``` --- ## 异常处理 ### ApiException 特殊处理 业务异常 `ApiException` 不会被标记为错误: ```csharp catch (ApiException aex) { span?.SetError(aex, request); // 记录为正常埋点,Tag中包含业务错误码 } catch (Exception ex) { span?.SetError(ex, request); // 记录为异常埋点,Error中包含异常消息 } ``` ### 异常埋点 每个异常会自动创建独立的异常埋点,便于按异常类型统计: ```csharp // 原始埋点 using var span = tracer.NewSpan("DatabaseQuery"); try { // 抛出异常 throw new TimeoutException("查询超时"); } catch (Exception ex) { span.SetError(ex, query); // 自动创建 "ex:TimeoutException" 埋点 } ``` --- ## 常见问题 ### 1. 埋点数据在星尘平台看不到? 检查以下几点: - 确认已注入星尘扩展:`services.AddStardust()` - 检查网络连接到星尘服务器 - 检查应用在星尘平台是否已注册 - 查看本地日志,确认埋点正常创建 ### 2. 采样数据太少? 调整采样参数: ```csharp tracer.MaxSamples = 10; // 增加正常采样数 tracer.MaxErrors = 50; // 增加异常采样数 tracer.Timeout = 1000; // 降低超时阈值 ``` ### 3. 如何关闭埋点? ```csharp // 方式1:不注入 ITracer // services.AddStardust(); // 注释掉 // 方式2:使用空实现 DefaultTracer.Instance = null; ``` ### 4. 如何查看本地埋点数据? DefaultTracer 会输出到日志: ``` Tracer[DataRetention] Total=10 Errors=0 Speed=0.02tps Cost=1500ms MaxCost=2000ms MinCost=1000ms ``` ### 5. 并发场景下 Span 会乱吗? 不会。`ISpan` 使用 `AsyncLocal<ISpan>` 保存上下文,每个异步流程独立: ```csharp public static AsyncLocal<ISpan?> Current { get; } ``` --- ## 与 OpenTelemetry 的双向适配 OpenTelemetry 是 CNCF 的跨厂商可观测性标准,.NET 上直接建立在 `System.Diagnostics.ActivitySource`/`Activity` 之上。NewLife 的 `ITracer`/`ISpan` 与它是**两套体系**,调用形态高度相似但数据模型不同: | 维度 | NewLife `ITracer`/`ISpan` | OpenTelemetry (.NET) | |---|---|---| | 底层类型 | 自研 `ISpan`/`DefaultSpan` | `ActivitySource`/`Activity` | | 数据粒度 | 按埋点名聚合统计 + 少量样本 | 每个 Span 独立记录,全量调用链 | | 采样 | `MaxSamples`/`MaxErrors`/`Timeout` + `TraceFlag` 强制链路采样 | Head/Tail Sampling + Processor + Exporter | | 上下文载体 | `DefaultSpan.Current` | `Activity.Current` | | 传播 | `traceparent`(已符合 W3C) | `traceparent`(W3C) | | 上报 | 私有 RPC `Trace/Report` → 星尘 | OTLP → Collector/Jaeger/Tempo | 两者可以互相桥接,NewLife.Core 提供两个适配器(**net6.0 及以上**,无新增依赖)。 ### 把 NewLife 埋点输出为 OpenTelemetry `OpenTelemetryTracer` 实现 `ITracer`,内部用 `ActivitySource` 产出标准活动。NewLife 各组件无需任何改造,它们调用的 `Tracer?.NewSpan()` 会原样产出标准活动: ```csharp // 一行启用,本进程所有 NewLife 埋点以 OpenTelemetry 标准输出 OpenTelemetryTracer.Enable(); ``` `Inner` 属性可指向底盘跟踪器,实现双写。此时内层片段会继承活动的 W3C 标识,星尘链路与 OpenTelemetry 链路可以互相对照: ```csharp OpenTelemetryTracer.Enable(new OpenTelemetryTracer { SourceName = "MyApp", Inner = new StarTracer("http://star:5000"), }); ``` 没有任何活动监听者且未设置底盘时,`NewSpan` 立即返回共享的空实现,不产生分配,因此可以安全地长期挂着。 > 注意:`Abandon()` 后活动不再结束,也就不会通知任何采集方,该埋点不会被导出(实测抛弃 10000 次埋点导出 0 条,对照正常埋点 10000 条全部导出)。但活动不停止就无法归还 `Activity.Current`,同一上下文内后续埋点仍会以这个不会导出的片段为父级;需要彻底过滤埋点的场景,应通过采集端的源名过滤或采样处理器实现。详见文末[收口记录](/NewLife/X/Blob/dev/Doc/#收口记录)。 ### 服务端片段继承上游链路 服务端收到请求时,要把上游的 `traceparent` 作为父级,才能串成一条跨进程调用链。星尘等跟踪器的片段标识是可变字段,「先创建片段、再 `Detach(headers)`」即可;而 OpenTelemetry 的活动一旦启动就无法再指定父级,必须在创建时就传入上游上下文。中间件统一用 `NewServerSpan` 屏蔽这一差异: ```csharp using var span = Tracer.NewServerSpan(action, req.Headers); span.Tag = $"{ctx.GetUserHost()} {req.Method} {req.GetRawUrl()}"; ``` OpenTelemetry 只认 W3C 的 `traceparent`(含 NewLife RPC 使用的 `TraceId` 头,其值同为 traceparent 格式);`Request-Id`、`Eagleeye-Traceid` 等历史格式不支持跨进程继承。 ### 把 OpenTelemetry 埋点采集进星尘 很多开源组件原生输出 OpenTelemetry 埋点。`OpenTelemetryCollector` 通过全局 `ActivityListener` 订阅这些活动,转成 `ISpan` 交给目标跟踪器,从而在星尘平台上也能看到第三方组件的调用链: ```csharp var star = new StarTracer("http://star:5000"); var collector = new OpenTelemetryCollector { Tracer = star }.Attach(); // 只采集指定源 var collector2 = new OpenTelemetryCollector { Tracer = star, ExcludeSources = null, IncludeSources = ["StackExchange.Redis", "Npgsql"], }.Attach(); ``` 采集到的活动按“源名:操作名”(如 `StackExchange.Redis:SET`)聚合统计,沿用活动的 W3C 标识并强制采样,因此不会把海量活动逐条上网。 **去重**:同一次操作可能同时被 NewLife 原生埋点、`DiagnosticListener`(Stardust 已内建监听 HttpClient/EF Core/SqlClient/gRPC/MongoDB)以及 `ActivityListener` 采集三次。`ExcludeSources` 默认排除这些已覆盖的源,避免星尘平台上的埋点计数翻倍;也可改用 `IncludeSources` 白名单只采集缺口部分。 Stardust 的 `StarTracer` 已默认挂载该采集器,由 `StarSetting.EnableOpenTelemetry` 控制开关。 --- ## 收口记录 > 已知不处理、不再重复登记的事项。扫描埋点体系前先读本节,避免重复报告。 | 事项 | 位置 | 处置与理由 | |------|------|-----------| | OpenTelemetry 模式下 `Abandon()` 不归还 `Activity.Current` | `OpenTelemetrySpan.Abandon` | **不修**(2026-10 决策)。导出侧本就生效——活动不结束即不通知采集方,实测抛弃 10000 次埋点导出 0 条,对照正常埋点 10000 条全部导出,因此不存在“被抛弃的埋点仍会上报”。残余影响是活动不停止就无法归还 `Activity.Current`:同一异步上下文内后续埋点会以这个不会导出的片段为父级,连续抛弃还会让这条父级链一直被持有(实测 257 B/次)。Web 场景每个请求各自一个异步上下文、链长≈1,影响可忽略;活动生命周期归 OpenTelemetry SDK 管理,库侧不介入。星尘档 `DefaultSpan.Abandon` 已归还上下文(实测驻留由 171 B/次降至 0)。 | | 星尘档 `DefaultSpan.Current` 的 `AsyncLocal` 写入分配 | `DefaultSpan.Start` / `Finish` | **不修**(2026-10-11 决策)。实测单层埋点的分配 **100%** 来自这一次写入:对象池 `Get`/`Return`、构建器字典命中、**值未变化的写入**均为 0 B,而值每变化一次即 72 B(≈40 B 的 `ExecutionContext` + 32 B 的值映射节点)。`ExecutionContext` 不可变,值一变只能新建,属 .NET 模型下限。改 `[ThreadStatic]` 会让埋点无法跨 `await` 传播(异步链路断裂);改为"可变的 `StrongBox` 就地改字段"看似 0 B,但并行分支共享同一盒子会互相覆盖,属静默串包;`Finish` 不还原 `Current` 会让后续埋点挂到已归还对象池的片段上。三者都比分配问题严重,故保留,"每次 `NewSpan` 零分配"在本模型下不可达。两点边界需知:**该值随宿主 `ExecutionContext` 中已有的 `AsyncLocal` 槽数上升**(干净进程 72 B、1 槽 160 B、3 槽约 224 B);**嵌套每层再 +200 B**(2 次上下文写入 144 B + 父片段 `Id` 字符串 56 B,第 2 层另有父 `TraceId` 88 B)。 | --- ## 参考资料 - **星尘监控平台**: https://newlifex.com/blood/stardust - **W3C Trace Context**: https://www.w3.org/TR/trace-context/ - **源码仓库**: https://github.com/NewLifeX/X - **在线文档**: https://newlifex.com/core/tracer --- ## 更新日志 - **2026-10**: 新增 OpenTelemetry 双向适配(`OpenTelemetryTracer` 输出、`OpenTelemetryCollector` 采集);traceId 升级为 W3C 标准的 32 位十六进制;星尘档 `Abandon()` 归还上下文 - **2024-12-16**: 完善文档,补充最佳实践和示例代码 - **2024-08**: 支持 .NET 9.0 - **2023-11**: 增加 `Abandon()` 方法 - **2023-06**: 优化对象池,提升性能 - **2022**: 初始版本发布