.NET 项目集成可观测性:从架构思考到落地实践

本文基于一个真实的.NET 9项目集成可观测性栈的经历,分享从"为什么做"到"怎么落地"的完整过程。


一、为什么需要集成可观测性?

1.1 触发点:一个"慢"接口

项目中有一个业务接口,涉及多表关联、缓存查询、外部服务调用,业务联链条很长。上线后用户反馈"慢",但具体慢在哪一步?是数据库查询?是外部调用?还是序列化?靠日志很难定位,本地复现也困难。

我的诉求很简单:想上好监控,做到心里有数。

  • 接口整体耗时多少?哪些步骤最耗时?
  • 系统运行时的QPS、错误率、内存、CPU是什么水平?
  • 出问题时能快速定位,而不是靠猜。

这就是我们集成可观测性的起点------不是为了"高大上",而是为了解决一个具体的问题。


二、可观测性架构设计

此次集成,用到的核心组件有4个,OpenTelemetry,Prometheus,jaeger,grafana,在这之前,日志的部分我们已经集成到了ELK,所以整个可观测性的架构设计图如下

组件 作用 说明
OpenTelemetry Collector 数据中枢 接收应用推来的 OTLP 数据,分发给后端
Prometheus 指标存储 存 QPS、延迟、内存等时序数据
Jaeger 链路追踪 存每次请求的完整调用链
Grafana 可视化 把Prometheus和Jaeger的数据做成图表
ElasticSearch 日志存储 通过Logstash将系统产生的各类日志传输存储

注意:本篇后续不过多涉及ELK相关内容,这里点出来是因为和可观测性组件产生了交集。此外,本文演示的相关中间件的部署环境都是基于Windows,大家部署时可以根据实际情况选择部署环境。

*2.1 部署架构

我这里的数据可观测组件部署情况是,只在1台中心服务器上部署了Jaeger,Prometheus和Grafana,而所有应用节点服务器都通过Otel将数据推送到中心服务器,也就是webapi的trace/metric全部汇聚到中心服务器,只提供一个面板地址,同一排查。

这套架构在我这里的场景验证是有效的,适合中小型项目。

2.2 OpenTelemetry Collector:数据中枢

Collector是整个栈的核心枢纽,应用只跟它通信,它负责把数据分发给Prometheus和Jaeger。

核心配置

yaml 复制代码
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317        # 应用通过这个端口推数据过来

processors:
  batch:
    timeout: 5s                        # 攒批超时,减少网络开销
  memory_limiter:
    limit_mib: 512                     # 防止数据堆积导致OOM

exporters:
  prometheus:
    endpoint: "0.0.0.0:8889"          # 暴露给Prometheus抓取
  otlp/jaeger:
    endpoint: 127.0.0.1:14250         # traces推给Jaeger
    tls:
      insecure: true                   # 内网不需要TLS

service:
  pipelines:
    metrics:
      receivers: [otlp]
      exporters: [prometheus]
    traces:
      receivers: [otlp]
      exporters: [otlp/jaeger]

关键点

  • receivers 定义数据怎么进来(OTLP gRPC)
  • exporters 定义数据往哪去(Prometheus收指标,Jaeger收traces)
  • processors 做批量和内存保护
  • 注意不要加 namespace 前缀,否则Grafana查询metric名会不匹配

2.3 Prometheus:指标存储

Prometheus 定时去 OTel Collector 拉数据(pull 模式)。

核心配置

yaml 复制代码
global:
  scrape_interval: 15s                 # 每 15 秒抓一次

scrape_configs:
  - job_name: "otel-collector"
    static_configs:
      - targets: ["localhost:8889"]    # Collector的Prometheus exporter端口

启动参数

bash 复制代码
prometheus.exe --config.file=prometheus.yml --storage.tsdb.retention.time=30d
  • retention.time=30d:数据保留 30 天,自动清理

2.4 Jaeger:链路追踪

Jaeger负责存储和查询traces,可以看到每次请求的完整调用链。

核心配置(Jaeger v2格式):

yaml 复制代码
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:14250       # 接收 Collector 推送的 traces

exporters:
  jaeger_storage_exporter:             # 写入存储(必须!否则UI为空)
    trace_storage: jaeger_storage

extensions:
  jaeger_query:                        # Web UI
    storage:
      traces: jaeger_storage
  jaeger_storage:                      # 内存存储后端
    backends:
      jaeger_storage:
        memory:
          max_traces: 100000

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [jaeger_storage_exporter]  # 必须包含!

踩坑提醒 :traces pipeline必须包含 jaeger_storage_exporter,否则Jaeger UI里Service下拉为空,这是最常见的坑。

2.5 Grafana:可视化

Grafana负责把数据做成图表,配置两个数据源即可:

yaml 复制代码
datasources:
  - name: Prometheus
    type: prometheus
    url: http://localhost:9090
    isDefault: true
  - name: Jaeger
    type: jaeger
    url: http://localhost:16686

部署顺序(很重要):

plain 复制代码
1. Jaeger          ← 必须先启动(Collector 要连它的 14250)
2. OTel Collector   ← 接收应用数据
3. Prometheus       ← 抓取指标
4. Grafana          ← 可视化

上述服务部署后控制台输出效果分别如下

  • jaeger服务启动后控制台输出
  • 启动Otel Collector后传输数据效果如下
  • 启动Prometheus后控制台输出效果如下
  • grafana启动后控制台输出

三、项目代码

3.1 和OpenTelemetry通信

.NET项目里只需要做一件事:通过OTel SDK把数据推给OTel Collector。后面的Prometheus、Jaeger、Grafana都是接收和展示,不需要应用关心。

NuGet 包

xml 复制代码
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />

服务注册(约 30 行代码):

csharp 复制代码
private static void ConfigureOpenTelemetry(this WebApplicationBuilder builder)
{
    string otlpEndpoint = builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]
        ?? "http://localhost:4317";

    builder.Services.AddOpenTelemetry()
        .WithTracing(tracing =>
                     {
                         tracing
                             .AddAspNetCoreInstrumentation()          // 自动采集HTTP请求traces
                             .AddSource("Declaration.*")                    // 通配自定义Span
                             .ConfigureResource(r => r.AddService("Magic.Declaration.WebAPI"))
                             .AddOtlpExporter(options =>
                                 {
                                     options.Endpoint = new Uri(otlpEndpoint);
                                     options.ExportProcessorType = ExportProcessorType.Batch;
                                 });
                     })
        .WithMetrics(metrics =>
                     {
                         metrics
                             .AddAspNetCoreInstrumentation()          // 自动采集HTTP请求指标
                             .AddRuntimeInstrumentation()             // .NET运行时指标(GC、CPU、内存)
                             .ConfigureResource(r => r.AddService("Magic.Declaration.WebAPI"))
                             .AddOtlpExporter(options =>
                                 {
                                     options.Endpoint = new Uri(otlpEndpoint);
                                 });
                     });
}

采集信息类别

类别 指标 说明
HTTP 请求 QPS、延迟分布、状态码 Instrumentation.AspNetCore 自动采集
.NET 运行时 GC、CPU、内存、线程池 Instrumentation.Runtime 自动采集

3.2 开发环境

为了保持开发环境的高效,且能看到采集到的基本数据,我们可以在开发环境配置将采集的数据输出到控制台。

csharp 复制代码
if (builder.Environment.IsDevelopment())
{
    tracing.AddConsoleExporter();  // traces 输出到控制台
}

启动应用后,控制台会直接输出每次请求的 trace 信息:

plain 复制代码
Activity.TraceId:            8d35e...
Activity.SpanId:             a7f2e...
Activity.DisplayName:        GET /api/order/detail
Activity.Kind:               Server
Activity.Duration:           00:00:00.1234
Resource associated with Activity:
    service.name: MyApp.WebAPI

效果如下

开发阶段用控制台就够了,不用装任何额外组件。

3.3 可拔插设计

这套可观测性栈是锦上添花型接入,不是项目运行的必要条件:

  • 不装 OTel Collector:应用正常跑,只是不采集数据
  • 不装 Prometheus/Jaeger:Collector 可以只做控制台输出
  • 环境变量不配 :代码里有默认值 http://localhost:4317

对业务代码零侵入:所有配置都收敛在服务注册阶段,业务代码不需要任何改动。


四、怎么验证和分析采集到的数据

4.1 验证数据流

部署完成后,先验证数据是否流通:

bash 复制代码
# 1. 检查 Collector 是否在接收数据
curl http://localhost:8888/metrics | findstr "accepted_spans"

# 2. 检查 Prometheus 是否有指标
curl "http://localhost:9090/api/v1/query?query=http_server_request_duration_seconds_count"

# 3. 检查 Jaeger 是否有 traces
curl http://localhost:16686/api/services

4.2 Jaeger:链路追踪分析

Jaeger UI地址:http://localhost:16686

定位链条

打开Jaeger面板后,点击某条Trace,展开看完整的调用链:

plain 复制代码
[HTTP GET /api/order/detail]  120ms
  ├── [MyApp.CreateOrder]       95ms    ← 你的自定义 Span
  │     ├── [FreeSql Query]      80ms   ← 数据库查询
  │     └── [Redis Get]           5ms   ← 缓存读取
  └── [HTTP Response]            10ms

Tips

链路追踪数据除了在Jaeger面板查看,也可以集成到Grafana,统一管理,本篇受篇幅限制不在赘述,下面分别给出Jaeger和Grafana的面板效果。

  • jaeger面板
  • grafana面板

常见场景

场景 操作
接口慢但不知道慢在哪 搜该接口的Trace → 看哪个Span耗时最长
间歇性超时 搜Duration > 1s的Trace → 对比正常和异常的差异
500 错误 搜Status = ERROR的Trace → 看Logs中的异常信息

4.3 Grafana:配置Dashboard

Grafana UI地址:http://localhost:3000

第一步:添加数据源(如果用 provisioning 文件部署则自动配置)

Configuration → Data Sources → Add data source → 选 Prometheus 和 Jaeger

第二步:创建 Dashboard

推荐先建一个包含以下面板的Dashboard:

面板 数据源 PromQL
实时QPS Prometheus sum(rate(http_server_request_duration_seconds_count[1m]))
P95延迟 Prometheus histogram_quantile(0.95, sum(rate(http_server_request_duration_seconds_bucket[5m])) by (le))
错误率 Prometheus sum(rate(http_server_request_duration_seconds_count{http_response_status_code=~"5.."}[5m])) / sum(rate(http_server_request_duration_seconds_count[5m]))
内存占用 Prometheus dotnet_gc_last_collection_heap_size_bytes
CPU使用率 Prometheus rate(dotnet_process_cpu_time_seconds_total[1m])

第三步:按路由分组

想看每个接口的独立指标,用 by (http_route) 分组:

plain 复制代码
sum(rate(http_server_request_duration_seconds_count[5m])) by (http_route, http_request_method)

Dashboard 设计原则

  • 顶部放概览数字(QPS、延迟、错误率、CPU)
  • 中间放趋势图(按时间变化)
  • 底部放明细表(按接口分组)

Grafana效果

注意:以上统计的方法,均可在Grafana社区官网查询到,也可以在这里(https://grafana.com/grafana/dashboards/)查询一些模板,直接套用。还可以直接把你的需求告诉你的智能体助手。


五、总结

集成可观测性的本质是让系统运行状态可见,而不是靠猜。

四个组件各司其职

  • OpenTelemetry Collector 做数据中枢
  • Prometheus 存指标
  • Jaeger 存链路
  • Grafana 做展示

.NET 项目要做的很少

  • 接入 OTel SDK,配置 OTLP 导出地址
  • 自动采集 HTTP 请求和运行时指标
  • 需要时加自定义 Span 定位业务逻辑
  • 开发阶段控制台就能看,生产环境按需部署

可拔插、零侵入

  • 不装组件不影响业务运行
  • 配置都在服务注册阶段,业务代码无感
  • 环境变量控制接入程度

可观测性不是一次性任务,而是持续的过程------先跑起来,遇到问题再逐步深入。


相关推荐
熊猫钓鱼>_>2 小时前
2026 前端的三场底层革命:当 Vue、React Native 和 Three.js 同时去掉中间层
前端·javascript·vue.js·react native·架构·webgl·three.js
OceanBase数据库官方博客4 小时前
OceanBase Lakebase 架构解析:独立部署与智能叠加层模式的技术实践
架构·oceanbase
AI 小老六5 小时前
Agent 工程化新底座:用 CLI 契约层打通 HTTP 接口与业务能力
网络·人工智能·http·ai·架构
用户8358086187915 小时前
深入剖析Java并发基石:AQS实现原理与自定义同步器实战
架构
ruxshui6 小时前
微服务下单跨服务数据不一致问题分析笔记
sql·微服务·架构
威联通安全存储6 小时前
SMT表面贴装线AOI检测网中TS-h2287XU-RP混合架构的物理部署
python·架构
Aray12347 小时前
论服务网格在云原生微服务项目中的设计与落地实践
微服务·云原生·架构
Mr.HeBoYan7 小时前
DPDK为什么越来越少使用rte_ring?——从Lock-Free到Run-to-Completion,重新理解现代DPDK架构演进(上)
linux·网络·算法·性能优化·架构·dpdk
初学AI的小高8 小时前
LangGraph 工作流设计:状态图驱动的多 Agent 编排实战
架构