<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Observability on Coding with Denglei</title>
    <link>https://blog.denglei.me/tags/observability/</link>
    <description>Recent content in Observability on Coding with Denglei</description>
    <generator>Hugo</generator>
    <language>zh-cn</language>
    <lastBuildDate>Thu, 25 Jun 2026 23:06:22 +0800</lastBuildDate>
    <atom:link href="https://blog.denglei.me/tags/observability/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>EF Core 慢查询排查实战：TagWith、OpenTelemetry 与执行计划</title>
      <link>https://blog.denglei.me/posts/efcore-slow-query-tagwith-opentelemetry-execution-plan/</link>
      <pubDate>Tue, 16 Jun 2026 17:04:23 +0800</pubDate>
      <guid>https://blog.denglei.me/posts/efcore-slow-query-tagwith-opentelemetry-execution-plan/</guid>
      <description>&lt;p&gt;EF Core 性能问题里，最折磨人的不是“慢”，而是“慢得没规律”，线上卡，测试又无法复现。&lt;/p&gt;&#xA;&lt;p&gt;很多小D、小W同学都经历过这种现场：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;压测数据很好看&lt;/li&gt;&#xA;&lt;li&gt;数据库 CPU 没打满&lt;/li&gt;&#xA;&lt;li&gt;业务代码看起来也没什么大问题&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;你改了几个 &lt;code&gt;Include&lt;/code&gt;，可能短期有效，但过两周又抖回来。根因往往不是某一行 LINQ 写错，而是整条排查链路没打通。&lt;/p&gt;&#xA;&lt;p&gt;这篇文章就做一件事：给你一套能线上落地的 EF Core 慢查询定位闭环，从应用日志一路追到数据库执行计划，不靠猜。&lt;/p&gt;&#xA;&lt;h2 id=&#34;问题背景为什么本地快线上慢&#34;&gt;问题背景：为什么本地快、线上慢&lt;/h2&gt;&#xA;&lt;p&gt;真实场景：订单列表接口平时 80ms 左右，高峰时段 P95 抬到 1s。你的第一反应是“数据库是不是扛不住了”，但监控显示：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;数据库 CPU 长期低于 50%&lt;/li&gt;&#xA;&lt;li&gt;连接池没有打满&lt;/li&gt;&#xA;&lt;li&gt;磁盘 IO 没明显异常&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;排查下来，又遇到两个组合问题：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;列表查询没有打 SQL 标签，日志里几百条 SQL 根本分不出谁是谁&lt;/li&gt;&#xA;&lt;li&gt;某些筛选条件线上本地索引匹配不同，SQL 文本相同但参数分布不同，执行计划差异很大&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;也就是说，问题不是“不会优化 SQL”，而是“看不见慢点在哪”。&lt;/p&gt;&#xA;&lt;h2 id=&#34;原理解析慢查询定位为什么总是卡在一半&#34;&gt;原理解析：慢查询定位为什么总是卡在一半&lt;/h2&gt;&#xA;&lt;p&gt;EF Core 的查询链路大致是：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;LINQ 表达式翻译成 SQL&lt;/li&gt;&#xA;&lt;li&gt;命令发送到数据库执行&lt;/li&gt;&#xA;&lt;li&gt;结果集回传并在应用层物化&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;很多排查第一步都把 SQL 抓出来看看，忽略了第 2、3 步的上下文信息，比如：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;这条 SQL 是哪个接口触发的&lt;/li&gt;&#xA;&lt;li&gt;这次慢是数据库执行慢，还是返回数据太大导致物化慢&lt;/li&gt;&#xA;&lt;li&gt;慢的是固定 SQL，还是同模板下某些参数更慢&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;要拿到这些信息，最实用的组合就是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;TagWith&lt;/code&gt;：给 SQL 打业务标签&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;OpenTelemetry&lt;/code&gt;：采集耗时、TraceId、SQL 标签并统一上报&lt;/li&gt;&#xA;&lt;li&gt;执行计划：确认索引命中、回表、扫描和 Key Lookup&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;三者合起来，才能形成可服用、可验证的排查闭环。&lt;/p&gt;</description>
    </item>
    <item>
      <title>EF Core 拦截器实战：SaveChangesInterceptor、CommandInterceptor 与审计落地</title>
      <link>https://blog.denglei.me/posts/efcore-interceptors-savechanges-command-audit/</link>
      <pubDate>Tue, 16 Jun 2026 17:04:23 +0800</pubDate>
      <guid>https://blog.denglei.me/posts/efcore-interceptors-savechanges-command-audit/</guid>
      <description>&lt;p&gt;审计不是“给表补几个 CreatedBy 字段”，也不是“在业务方法里顺手记日志”。它本质上是系统级可追溯能力，设计目标是让系统在任何写路径下都能稳定回答四个问题：谁发起、改了什么、何时发生、通过哪条链路触发。&lt;/p&gt;&#xA;&lt;p&gt;真正的难点不在 API 用法，而在系统设计阶段是否把审计定义成基础设施能力。这里聚焦两层落地：&lt;code&gt;SaveChangesInterceptor&lt;/code&gt; 负责实体变更审计，&lt;code&gt;CommandInterceptor&lt;/code&gt; 负责 SQL 执行审计，两者一起组成可观测、可追溯、可审计的最小闭环。&lt;/p&gt;&#xA;&lt;h2 id=&#34;1-问题背景为什么审计必须在系统设计期落地&#34;&gt;1. 问题背景：为什么审计必须在系统设计期落地&lt;/h2&gt;&#xA;&lt;p&gt;如果你刚开始做系统设计，通常会先把功能跑通，再逐步补监控、日志和审计。这条路径很正常，但系统从单一写入口演进到多入口后，审计会出现一些典型断层：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;HTTP 请求有 &lt;code&gt;TraceId&lt;/code&gt;，但数据库变更记录无法关联到具体调用链路。&lt;/li&gt;&#xA;&lt;li&gt;Web 接口有审计字段，批处理、后台任务、集成事件消费链路没有统一审计。&lt;/li&gt;&#xA;&lt;li&gt;SQL 慢查询能看到语句本身，但看不到对应业务场景和调用来源。&lt;/li&gt;&#xA;&lt;li&gt;合规追查时能找到结果，找不到完整过程和责任主体。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这些现象不是某个人“写错了”，而是系统设计阶段还没有建立统一审计模型：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;还没先定义统一的审计契约（身份、时间、来源、关联链路）。&lt;/li&gt;&#xA;&lt;li&gt;写入路径没有统一审计入口，不同调用通道口径自然会分化。&lt;/li&gt;&#xA;&lt;li&gt;变更审计和 SQL 观测没有打通，排障时很难快速复原完整链路。&lt;/li&gt;&#xA;&lt;li&gt;只有开发约定，没有系统级自动机制，随着迭代推进就容易出现漏记。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;这篇文章要解决的核心问题不是“把审计代码写到哪一层”，而是“如何在系统层提供默认生效、可验证、可扩展的审计能力”，并且让这套能力能跟着系统规模一起演进。&lt;/p&gt;&#xA;&lt;h2 id=&#34;2-原理解析先拆职责再落地拦截器&#34;&gt;2. 原理解析：先拆职责，再落地拦截器&lt;/h2&gt;&#xA;&lt;p&gt;如果你是第一次从系统设计角度做审计，最稳妥的方法是先做职责拆分，再做实现落地。这里按“三步法”来理解：先统一实体变更审计，再统一 SQL 观测入口，最后明确拦截器边界。&lt;/p&gt;&#xA;&lt;h3 id=&#34;21-第一步用-savechangesinterceptor-统一实体变更审计&#34;&gt;2.1 第一步：用 SaveChangesInterceptor 统一实体变更审计&lt;/h3&gt;&#xA;&lt;p&gt;&lt;code&gt;SaveChangesInterceptor&lt;/code&gt; 适合处理实体状态相关规则，例如：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;Added&lt;/code&gt; 时补 &lt;code&gt;CreatedAt/CreatedBy&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;Modified&lt;/code&gt; 时补 &lt;code&gt;UpdatedAt/UpdatedBy&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;软删除时把 &lt;code&gt;Deleted&lt;/code&gt; 改写成 &lt;code&gt;Modified + IsDeleted&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这类逻辑和实体状态强相关，放在拦截器里能覆盖所有调用路径，避免每个 Service 重复写一遍。&lt;/p&gt;&#xA;&lt;h3 id=&#34;22-第二步用-commandinterceptor-统一-sql-观测入口&#34;&gt;2.2 第二步：用 CommandInterceptor 统一 SQL 观测入口&lt;/h3&gt;&#xA;&lt;p&gt;&lt;code&gt;DbCommandInterceptor&lt;/code&gt; 适合做 SQL 级别的统一观测：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;慢 SQL 记录&lt;/li&gt;&#xA;&lt;li&gt;SQL 失败统一日志&lt;/li&gt;&#xA;&lt;li&gt;参数快照和调用耗时&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;它不适合做业务决策，但非常适合做“排障入口统一化”。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Serilog &#43; OpenTelemetry：从请求日志到链路追踪的关联落地</title>
      <link>https://blog.denglei.me/posts/serilog-opentelemetry-trace-correlation/</link>
      <pubDate>Tue, 16 Jun 2026 17:04:23 +0800</pubDate>
      <guid>https://blog.denglei.me/posts/serilog-opentelemetry-trace-correlation/</guid>
      <description>&lt;p&gt;这篇直接给落地方案，不再讲结构化日志的背景概念。目标只有一个：在 ASP.NET Core 服务里，把 Serilog 日志和 OpenTelemetry 链路追踪打通，排障时可以从一条错误日志直接跳到完整 Trace。&lt;/p&gt;&#xA;&lt;h2 id=&#34;1-问题背景这篇要交付什么&#34;&gt;1. 问题背景：这篇要交付什么&lt;/h2&gt;&#xA;&lt;p&gt;按下面步骤做完，你会得到一条可执行排障链路：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;日志里稳定带上 &lt;code&gt;TraceId&lt;/code&gt;、&lt;code&gt;SpanId&lt;/code&gt;、&lt;code&gt;RequestPath&lt;/code&gt;、&lt;code&gt;RequestMethod&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;业务日志统一输出 &lt;code&gt;OrderId&lt;/code&gt;、&lt;code&gt;TenantId&lt;/code&gt; 这类主键字段&lt;/li&gt;&#xA;&lt;li&gt;下游 HTTP 调用失败时，日志可直接回跳到同一 &lt;code&gt;TraceId&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;现场排障固定为 3 步：查日志 -&amp;gt; 拿 TraceId -&amp;gt; 打开链路&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;这篇示例默认运行环境：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;.NET 8&lt;/li&gt;&#xA;&lt;li&gt;Serilog&lt;/li&gt;&#xA;&lt;li&gt;OpenTelemetry + OTLP&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;2-原理解析实施前约定与字段契约&#34;&gt;2. 原理解析：实施前约定与字段契约&lt;/h2&gt;&#xA;&lt;p&gt;先把约定定下来，后面配置才不会反复返工。&lt;/p&gt;&#xA;&lt;h3 id=&#34;21-平台层字段必须统一&#34;&gt;2.1 平台层字段（必须统一）&lt;/h3&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;TraceId&lt;/code&gt;：整条请求链路唯一键&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;SpanId&lt;/code&gt;：当前节点唯一键&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;RequestPath&lt;/code&gt;：请求路径&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;RequestMethod&lt;/code&gt;：请求方法&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;StatusCode&lt;/code&gt;：响应码&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;22-业务层字段按场景补充&#34;&gt;2.2 业务层字段（按场景补充）&lt;/h3&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;订单域：&lt;code&gt;OrderId&lt;/code&gt;、&lt;code&gt;CustomerId&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;多租户：&lt;code&gt;TenantId&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;外部依赖：&lt;code&gt;DependencyName&lt;/code&gt;、&lt;code&gt;DownstreamStatusCode&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;23-字段命名规则一次定死&#34;&gt;2.3 字段命名规则（一次定死）&lt;/h3&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;统一使用 PascalCase&lt;/li&gt;&#xA;&lt;li&gt;同义字段只保留一个名字，比如只用 &lt;code&gt;TraceId&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;业务字段保持稳定，不随文案调整&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;3-示例代码按步骤落地&#34;&gt;3. 示例代码：按步骤落地&lt;/h2&gt;&#xA;&lt;h3 id=&#34;31-安装依赖&#34;&gt;3.1 安装依赖&lt;/h3&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;Serilog.AspNetCore&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;Serilog.Sinks.Console&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;Serilog.Enrichers.Environment&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;OpenTelemetry.Extensions.Hosting&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;OpenTelemetry.Instrumentation.AspNetCore&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;OpenTelemetry.Instrumentation.Http&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;OpenTelemetry.Exporter.OpenTelemetryProtocol&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;32-第一步配置-serilog-与-opentelemetry&#34;&gt;3.2 第一步：配置 Serilog 与 OpenTelemetry&lt;/h3&gt;&#xA;&lt;p&gt;把这段放进 &lt;code&gt;Program.cs&lt;/code&gt;，先打通基础链路：&lt;/p&gt;</description>
    </item>
    <item>
      <title>Serilog：从结构化日志认知到 .NET 工程落地</title>
      <link>https://blog.denglei.me/posts/serilog-structured-logging-sink-enricher/</link>
      <pubDate>Tue, 16 Jun 2026 17:04:23 +0800</pubDate>
      <guid>https://blog.denglei.me/posts/serilog-structured-logging-sink-enricher/</guid>
      <description>&lt;h2 id=&#34;问题背景&#34;&gt;问题背景&lt;/h2&gt;&#xA;&lt;p&gt;很多项目不缺日志，缺的是有用的日志。&lt;/p&gt;&#xA;&lt;p&gt;平时接口跑得顺，大家都觉得日志够用。真到线上出问题，日志的短板会一下子暴露出来。&lt;/p&gt;&#xA;&lt;p&gt;比如订单接口偶发超时，日志里只剩这么一句：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Create order failed for customer 1024, cost 3800ms, trace abc123&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这种日志平时看着还行，真到线上排障时基本帮不上太多忙：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;你没法直接按 CustomerId、TraceId、ElapsedMs 做过滤和聚合，只能全文搜索&lt;/li&gt;&#xA;&lt;li&gt;同一个字段今天写 customerId，明天写 userId，后天又换成 client_id，查询条件根本沉淀不下来&lt;/li&gt;&#xA;&lt;li&gt;想统计某类错误的数量、平均耗时、失败占比，往往还得写额外脚本二次清洗&lt;/li&gt;&#xA;&lt;li&gt;日志平台能做的分析能力很弱，因为它拿到的只是一段文本，不是一条可分析的数据&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;问题往往不在于日志打得不够多，而在于日志从一开始就没按可检索、可聚合、可关联的方式去设计。&lt;/p&gt;&#xA;&lt;p&gt;传统文本日志更像写给人看的备注，结构化日志才像写给系统消费的数据。业务一旦进入多人协作、线上排障、统一观测这些阶段，日志就不再只是打出来看一眼，而是排障、审计、告警、指标补充、链路追踪的一部分。走到这一步，结构化日志就不是锦上添花，而是该补的基础课。&lt;/p&gt;&#xA;&lt;h2 id=&#34;原理解析&#34;&gt;原理解析&lt;/h2&gt;&#xA;&lt;h3 id=&#34;什么是结构化日志&#34;&gt;什么是结构化日志&lt;/h3&gt;&#xA;&lt;p&gt;很多人第一次接触结构化日志，会下意识把重点放在 JSON 输出上。其实 JSON 只是表现形式，核心不在日志长什么样，而在日志里的字段有没有明确语义，后续能不能被系统稳定识别。&lt;/p&gt;&#xA;&lt;p&gt;先看两种写法的差异。&lt;/p&gt;&#xA;&lt;p&gt;普通文本日志：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;logger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;LogInformation&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s&#34;&gt;$&amp;#34;Order {order.Id} created for customer {order.CustomerId}, amount {order.Amount}, cost {elapsedMs}ms&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这行代码最终只会产出一段字符串。人类看没问题，但日志平台拿到以后，并不知道哪一段是订单号，哪一段是金额，哪一段是耗时。&lt;/p&gt;&#xA;&lt;p&gt;结构化日志：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;logger&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;LogInformation&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s&#34;&gt;&amp;#34;Order {OrderId} created for customer {CustomerId}, amount {Amount}, cost {ElapsedMs}ms&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;order&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;order&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CustomerId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;order&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;elapsedMs&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;换成这种写法以后，日志框架记录的就不再是一整段普通字符串，而是一个日志事件：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;模板是 Order {OrderId} created for customer {CustomerId}, amount {Amount}, cost {ElapsedMs}ms&lt;/li&gt;&#xA;&lt;li&gt;属性是 OrderId、CustomerId、Amount、ElapsedMs&lt;/li&gt;&#xA;&lt;li&gt;元数据还包括时间、级别、异常、来源、线程、TraceId 这些上下文&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;输出到控制台时，它可以渲染成人能直接看的句子；送到 Elasticsearch、Seq、Loki 或 OpenTelemetry 后端时，它又能以字段化数据的形式被检索和聚合。&lt;/p&gt;</description>
    </item>
    <item>
      <title>从 IApplicationBuilder 到 RequestDelegate：ASP.NET Core 请求管线性能与可观测性</title>
      <link>https://blog.denglei.me/posts/aspnet-core-request-pipeline-performance-observability/</link>
      <pubDate>Tue, 16 Jun 2026 17:04:23 +0800</pubDate>
      <guid>https://blog.denglei.me/posts/aspnet-core-request-pipeline-performance-observability/</guid>
      <description>&lt;p&gt;很多团队做性能优化时，第一反应是改 SQL、加缓存、扩机器。结果接口还是慢，而且慢得不稳定。&lt;/p&gt;&#xA;&lt;p&gt;这类问题里，有一部分根因并不在业务代码，而在请求进入业务之前就已经产生了: 中间件顺序、重复序列化、过重日志、异常处理位置不当，都会把每个请求的固定成本悄悄抬高。&lt;/p&gt;&#xA;&lt;p&gt;这篇文章我们不讲抽象概念，直接从一个真实工程场景出发，拆开 ASP.NET Core 请求管线，回答三个问题:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;请求管线到底是怎么执行的&lt;/li&gt;&#xA;&lt;li&gt;哪些中间件写法会稳定拉低吞吐&lt;/li&gt;&#xA;&lt;li&gt;如何在不牺牲可观测性的前提下，把链路成本控制住&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;1-问题背景-为什么明明-cpu-不高rt-却在抖&#34;&gt;1. 问题背景: 为什么明明 CPU 不高，RT 却在抖&lt;/h2&gt;&#xA;&lt;p&gt;先看一个常见现象:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;峰值时段 P95 从 35ms 涨到 90ms&lt;/li&gt;&#xA;&lt;li&gt;CPU 只到 45%&lt;/li&gt;&#xA;&lt;li&gt;数据库监控正常&lt;/li&gt;&#xA;&lt;li&gt;线程池没有明显爆满&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;像商场收银台排队: 收银员速度没变，库存系统也没卡，但每位顾客在真正结账前都要先填两张表、复印一次小票、走一段绕路。单人多花 10 秒，队伍就会在高峰时段整体失控。&lt;/p&gt;&#xA;&lt;p&gt;在 Web 服务里，这段“真正结账前的绕路”就是请求管线上的固定开销。&lt;/p&gt;&#xA;&lt;p&gt;典型问题包括:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;将高成本日志中间件放在链路最前面，且对所有请求都做完整 Body 记录&lt;/li&gt;&#xA;&lt;li&gt;鉴权、异常处理、路由等中间件顺序错误，导致重复执行或额外分支判断&lt;/li&gt;&#xA;&lt;li&gt;在中间件中做同步阻塞 I/O&lt;/li&gt;&#xA;&lt;li&gt;将一些本该按采样写出的指标，变成了每请求都完整打点&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;2-原理解析-iapplicationbuilder-如何变成-requestdelegate&#34;&gt;2. 原理解析: IApplicationBuilder 如何变成 RequestDelegate&lt;/h2&gt;&#xA;&lt;p&gt;ASP.NET Core 启动时，&lt;code&gt;IApplicationBuilder&lt;/code&gt; 会把你注册的中间件构造成一个 &lt;code&gt;RequestDelegate&lt;/code&gt; 链。&lt;/p&gt;&#xA;&lt;p&gt;关键点只有两个，但经常被忽略:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;中间件按“注册顺序”进入，按“逆序”包裹执行。每个中间件把后续链路作为自己的 next，形成嵌套闭包。&lt;/li&gt;&#xA;&lt;li&gt;任意中间件都可以不调用 &lt;code&gt;next()&lt;/code&gt;，从而短路后续链路。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;一个简化模型如下:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;RequestDelegate&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;app&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;context&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CompletedTask&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;app&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;MiddlewareC&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;app&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;app&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;MiddlewareB&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;app&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;app&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;MiddlewareA&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;app&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 实际执行顺序: A -&amp;gt; B -&amp;gt; C -&amp;gt; Endpoint -&amp;gt; C -&amp;gt; B -&amp;gt; A&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这意味着:&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
