.NET 分布式故障?OpenTelemetry + ClickStack 助你 2 次点击定位!
本文字数:11742;估计阅读时间:30分钟
作者:Alex Soffronow Pagonidis
当你看到一条日志,内容是 Order abc123 failed: error:timeout。遇到这种情况,你会困惑是哪个服务超时了:是支付服务、数据库还是网络?如果你打开 ClickStack,点击跟踪 ID (trace ID),便能立即看到完整的请求时间线:Order API 等待 Payment Service 响应了 3 秒,而 Payment Service 在连接中断时仍在执行欺诈检查。只需两次点击,根源问题便清晰可见。
这就是分布式追踪 (distributed tracing) 的强大之处。通过在 ASP.NET 中进行最少量的 OpenTelemetry 配置,你就可以将原本孤立的日志信息,在 ClickStack 中转化为完整的跨服务执行视图,精确地洞察时间消耗的环节以及跨服务边界的问题发生点。
在本文中,我们将构建两个集成 OpenTelemetry 的 ASP.NET 服务,将数据存储到 SQLite 数据库,并将追踪 (traces)、日志 (logs) 和指标 (metrics) 发送到 ClickStack。
ClickStack 是一个开源的、面向 OpenTelemetry 的一站式可观测性平台。它支持接收标准 OTel 数据,将其存储在 ClickHouse 中,并提供直观的 UI 界面供你探索数据。同时,它也保留了对底层遥测数据 (telemetry data) 的直接 SQL 访问能力。
在本文中,我们将构建两个互相调用并将数据持久化到 SQLite 数据库的 ASP.NET 服务:
• Order API: 负责接收订单、验证库存信息、调用 Payment Service,并将已完成的订单保存至 SQLite 数据库。
• Payment Service: 模拟支付处理过程,支持配置不同的失败模式,并将支付结果保存至 SQLite 数据库。
这两个服务都集成了 OpenTelemetry,并通过 OTLP/gRPC 协议将全部三种信号(追踪、日志、指标)导出到 ClickStack。我们之所以使用 SQLite,是为了使演示环境保持独立和简洁,同时演示了通过 EF Core 集成,数据库操作的 span (跨度) 如何自动生成。SQLite 层通过 EF Core 的集成包实现了自动追踪,这意味着数据库操作将自动呈现在 ClickStack 中,无需手动创建任何 span。
单个订单的流程如下:
1. 客户端向 Order API 发送 POST 请求。
2. Order API 验证库存信息(从 SQLite 数据库中的产品目录获取)。
3. Order API 通过 HTTP 调用 Payment Service。
4. Payment Service 执行欺诈检查,处理支付费用,并将结果保存至 SQLite 数据库。
5. Order API 接收支付结果,随后将订单保存至 SQLite 数据库。
完成后,我们将能够利用 ClickStack 追踪一个跨越多个服务和数据库调用的单一请求的完整生命周期:包括 Order API 验证请求,通过 HTTP 调用进入 Payment Service 进行欺诈检查和费用处理,并在两端进行数据库写入,所有这些操作都将归属于同一个 trace ID。
• 开箱即用,全面支持 OpenTelemetry。 ClickStack 原生提供 OTLP/gRPC 端点。只需将你的 OpenTelemetry SDK (OTel SDK) 指向该端点,traces、logs 和 metrics 数据便能自动开始流动。无需自定义数据导出器,无需手动设置 schema,也无需管理复杂的中间数据管道。
• 底层采用 ClickHouse 驱动。 ClickHouse 是一款专为大规模数据集上的实时分析而设计的开源列式数据库。所有遥测数据 (telemetry data) 都存储在 ClickHouse 表中,这意味着能够实现优异的列式压缩(通常为 10-20 倍),对数十亿 spans (trace spans) 进行亚秒级分析查询,并通过倒排索引 (inverted indexes) 实现 全文搜索 。你将获得一个真实数据库的强大能力,而非一种功能受限的查询语言。此外,与传统可观测性解决方案相比,其总成本仅为其一小部分。
• 关联信号。 由于 ClickStack 能够同时接收 traces、logs 和 metrics,因此可以自动实现它们之间的关联:点击日志行可跳转到其父级 trace,查看特定 trace 时间窗口内的日志,或者从指标 (metrics) 中的延迟峰值深入分析,定位导致该峰值的具体 spans。
• 全面支持 SQL 访问。 你的遥测数据存储在标准的 ClickHouse 表中。可以直接通过 SQL 查询这些数据,为实时聚合构建 物化视图 (materialized views) ,或者在内置 UI 的基础上集成 Grafana 等工具。
与 ElasticSearch 相比,ClickHouse 在实际基准测试中实现了约 5 倍的更优压缩率和 4 倍以上的查询速度提升。 Trip.com 也已从 Elasticsearch 迁移至 ClickHouse,并在相同的硬件条件下,构建了一个 50PB 的日志平台,数据容量提升了 4 倍。
整个技术栈在 Docker Compose 中运行。ClickStack 全面负责可观测性:其镜像集成了用于数据存储的 ClickHouse、用于数据摄取的 OTLP/gRPC 收集器以及用于数据探索的可观测性 UI。
services:
clickstack:
image: docker.io/clickhouse/clickstack-all-in-one:2.21.0
ports:
- "8080:8080" # ClickStack UI
- "18123:8123" # ClickHouse HTTP (Play UI)
volumes:
- ./clickstack/entry.sh:/etc/local/entry.sh:ro
- clickhouse_data:/var/lib/clickhouse
- clickhouse_logs:/var/log/clickhouse-server
healthcheck:
test: ["CMD-SHELL", "wget -qO /dev/null http://127.0.0.1:8123/ping || exit 1"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
接着,我们添加了两个 ASP 服务。这些服务在启动前会检查 ClickStack 的健康状况。此外,我们还包含了一个 seed-data 容器,它会在所有服务启动并运行后自动生成流量:
order-api:
build:
context: .
dockerfile: src/OrderApi/Dockerfile
ports:
- "5000:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- OTEL_EXPORTER_OTLP_ENDPOINT=http://clickstack:4317
- PaymentService__BaseUrl=http://payment-service:8080
depends_on:
clickstack:
condition: service_healthy
OTEL_EXPORTER_OTLP_ENDPOINT 环境变量是 OpenTelemetry (OTel) SDK 确定数据发送目标所需的全部信息。ClickStack 默认在端口 4317 上开放一个 OTLP/gRPC 接收器。
启动所有服务:
docker compose up -d
OpenTelemetry 配置
Program.cs 中的 OpenTelemetry (OTel) 配置负责设置 traces、metrics 和 logs:
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService(DiagnosticConfig.ServiceName))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddSource(DiagnosticConfig.ActivitySourceName)
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddMeter(DiagnosticConfig.MeterName)
.AddOtlpExporter());
builder.Logging.AddOpenTelemetry(options =>
{
options.IncludeFormattedMessage = true;
options.IncludeScopes = true;
options.AddOtlpExporter();
});
几点注意事项:
• 三个检测库 (instrumentation libraries) 涵盖了常见的应用场景: AddAspNetCoreInstrumentation() 用于捕获传入的 HTTP 请求, AddHttpClientInstrumentation() 用于捕获传出的 HTTP 调用, AddEntityFrameworkCoreInstrumentation() 则用于捕获数据库操作。
• ConfigureResource(resource => resource.AddService(DiagnosticConfig.ServiceName)) :通过此配置,我们的服务名称将显示在 ClickStack 中。
• AddSource(DiagnosticConfig.ActivitySourceName) :此方法指示 tracer 监听我们自定义的 spans(详细内容见下文)。
• AddOtlpExporter() :在每个信号 (signal) 上,该方法通过 OTLP/gRPC 将数据发送到 OTEL_EXPORTER_OTLP_ENDPOINT 环境变量指定的端点(在我们这里是 ClickStack)。
• Logs :通过 builder.Logging.AddOpenTelemetry() 进行独立配置。 IncludeFormattedMessage 和 IncludeScopes 选项确保日志消息易于阅读,并包含完整的上下文信息。
自定义 spans 和 metrics
DiagnosticConfig 类集中管理了所有遥测定义:
public static class DiagnosticConfig
{
public const string ServiceName = "payment-service";
public const string ActivitySourceName = "PaymentService.Payments";
public const string MeterName = "PaymentService.Metrics";
public static readonly ActivitySource ActivitySource = new(ActivitySourceName);
public static readonly Meter Meter = new(MeterName);
public static readonly Counter<long> PaymentsProcessed = Meter.CreateCounter<long>(
"payments.processed",
description: "Number of payments processed");
public static readonly Histogram<double> FraudCheckDuration = Meter.CreateHistogram<double>(
"fraud_check.duration",
unit: "ms",
description: "Duration of fraud check processing");
}
在 .NET 生态中,OpenTelemetry 是基于 System.Diagnostics 构建的,因此 ActivitySource 和 Meter 是用于创建 spans 和 metrics 的原生基本类型。
实践中,其实现方式如下:PaymentProcessor 类会为每个处理步骤创建子 spans:
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
var paymentId = Guid.NewGuid().ToString("N")[..12];
// Start Activity for trace and enrich it with tags
using var activity = DiagnosticConfig.ActivitySource.StartActivity("process-payment");
activity?.SetTag("payment.id", paymentId);
activity?.SetTag("payment.order_id", request.OrderId);
activity?.SetTag("payment.amount", request.Amount);
// Step 1: Fraud check (creates its own child span)
var fraudScore = await RunFraudCheckAsync(paymentId, request);
// Step 2: Determine outcome based on configured rates
var outcome = DetermineOutcome();
// Step 3: Process the charge (creates its own child span)
var result = await ProcessChargeAsync(paymentId, request, outcome, fraudScore);
// Persist to SQLite (auto-instrumented by EF Core)
await using var db = await _dbFactory.CreateDbContextAsync();
db.Payments.Add(result);
await db.SaveChangesAsync();
// Record metrics
DiagnosticConfig.PaymentsProcessed.Add(1,
new KeyValuePair<string, object?>("status", result.Status),
new KeyValuePair<string, object?>("payment_method", request.PaymentMethod));
return result;
}
当检测到可疑分数时,欺诈检查 span 会记录一个事件。所有这些都将显示在 ClickStack 的 trace waterfall 中:
private async Task<int> RunFraudCheckAsync(string paymentId, PaymentRequest request)
{
using var activity = DiagnosticConfig.ActivitySource.StartActivity("fraud-check");
var sw = Stopwatch.StartNew();
// Simulate fraud check latency (10-50ms)
var delay = Random.Shared.Next(10, 51);
await Task.Delay(delay);
var fraudScore = Random.Shared.Next(0, 101);
activity?.SetTag("fraud.score", fraudScore);
activity?.SetTag("fraud.delay_ms", delay);
if (fraudScore > 70)
{
activity?.AddEvent(new ActivityEvent("suspicious-activity", tags: new ActivityTagsCollection
{
{ "fraud.score", fraudScore },
{ "payment.amount", request.Amount },
}));
}
sw.Stop();
DiagnosticConfig.FraudCheckDuration.Record(sw.Elapsed.TotalMilliseconds);
return fraudScore;
}
可配置的故障模式
支付服务不仅能处理成功请求,它还能模拟真实的故障模式,从而在演示中生成种类丰富的日志和追踪数据(这些故障率可在 PaymentConfiguration.cs 中进行配置)。
超时场景对于分布式追踪(Distributed tracing)而言尤其值得关注:支付服务会模拟 3 到 8 秒的延迟,而订单 API(Order API)的 HTTP 客户端超时设置为 3 秒。这会造成一种情况:订单 API 收到 TaskCanceledException 错误时,支付服务可能仍在正常处理请求。这两种情况都会在 ClickStack 的追踪中清晰地展现出来。
跨服务分布式追踪
当订单 API 调用支付服务时,追踪上下文(trace context)会通过 HTTP 请求头自动传播。这是因为 AddHttpClientInstrumentation() 会将 traceparent 头注入到出站请求中,而支付服务端的 AddAspNetCoreInstrumentation() 则负责提取这些头。整个过程无需任何手动关联。
OrderService 为订单处理的每个步骤创建追踪跨度(span),这与我们之前为支付服务所做的方式相同。最终的追踪瀑布图(trace waterfall)将展示完整的调用链:place-order → validate-order → call-payment-service → HTTP POST /payments → (Payment Service spans) → SaveChanges (EF Core/SQLite)。
两个服务都使用 Entity Framework Core 将数据持久化到 SQLite 数据库中。
自动注入的数据库追踪跨度
OpenTelemetry.Instrumentation.EntityFrameworkCore 包会利用 EF Core 内部的 DiagnosticSource 事件。每次执行 SaveChangesAsync()、FirstOrDefaultAsync() 等 EF Core 操作时,都会自动生成符合 OpenTelemetry 数据库语义约定的追踪跨度。在我们的启动配置中,只需一行代码即可完成设置:
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation() // <-- instruments database calls
.AddSource(DiagnosticConfig.ActivitySourceName)
.AddOtlpExporter())
Order API 包含一个 /generate-traffic 端点,用于生成逼真的负载。在 Docker Compose 中,seed-data 容器会在启动时自动调用此端点。要获取更多数据,您只需运行:
curl -X POST http://localhost:5000/generate-traffic
当流量开始流动后,请在 http://localhost:8080 打开 ClickStack 界面。
由于 OTel 流水线将所有三种信号发送到 ClickStack,您将获得仅凭日志无法实现的功能:自动发现的服务地图、分布式追踪瀑布图、关联的日志到追踪视图以及数据库操作分解。ClickStack UI 提供了一种简单的方式来探索这些数据:您可以搜索所有类型的信号、进行过滤,并使用日志聚类来分组相似模式,从而加速根本原因分析。ClickStack 还通过 ClickHouse 闪电般的倒排索引支持全文搜索,并且最近的版本已直接在 ClickStack UI 中增加了文本索引支持。
分布式追踪与日志
一个成功的订单追踪显示了完整的瀑布图:
1. place-order (Order API)
2. validate-order (Order API)
3. call-payment-service (Order API)
4. HTTP POST /payments (由 HttpClientInstrumentation 自动埋点)
5. process-payment (Payment Service)
6. fraud-check (Payment Service)
7. process-charge (Payment Service)
8. EF Core SaveChanges spans 在两侧 (自动埋点)
您可以深入查看瀑布图中的任何 span 或日志,以查看它们的所有属性。
追踪错误
ClickStack 中的事件模式允许您通过自动聚类相似消息,快速识别错误中的模式。这样,您只需审查少量分组,而无需浏览数百万条消息。
点击进入一个分组以查看单独的消息:
然后点击其中任意一项,即可查看消息属性、追踪瀑布图、日志上下文以及相关服务地图。
日志追踪关联
在追踪请求期间发出的每行日志都会自动携带 trace ID 和 span ID。在 ClickStack 中,您可以点击任意日志行,直接跳转到父追踪,无需手动关联。OTel 日志导出器会自动处理此功能。
同样,反向操作也适用:当你查看某个追踪时,ClickStack 会自动显示该追踪执行期间发出的日志。由于我们的数据库调用已经进行了插桩,这意味着我们也能在瀑布图中看到每个数据库操作。这样一来,你无需手动搜索匹配追踪 ID 的日志;它们会直接在上下文中显示。这种自动关联是 OTel + ClickStack 流水线最大的优势之一——你无需任何手动配置即可获得整体视图。
指标
你可以在 ClickStack 中基于各项指标构建自定义仪表板。演示版预装了一个仪表板,方便我们监控订单处理服务,并能轻松访问警告和错误日志。
你还可以基于这些指标定义告警。ClickStack 支持与 Slack、PagerDuty 或通过通用 Webhook 进行告警集成。
内置仪表板
ClickStack 也自带多款开箱即用的仪表板。这些仪表板可用于监控 ClickHouse,展示服务(自动发现)和数据库调用中最相关的指标,并支持你探索 Kubernetes 事件。
服务仪表板重点展示了主要端点、延迟和错误。其中数据可以通过 SQL 或 Lucene 进行筛选。服务地图还能基于分布式追踪自动发现 order-api 和 payment-service 之间的关系,无需手动配置。
最后,数据库标签页显示了我们服务中数据库操作的统计数据。由于我们使用了 EF Core 自动插桩,每个查询和保存操作都通过标准 db.* 属性进行捕获。你可以一目了然地查看操作延迟、吞吐量和错误率。
本演示旨在突出简洁性和清晰度。关于如何为大规模生产工作负载优化 ClickStack,请查阅 ClickStack 性能调优(Performance Tuning) 文档,其中提供了详尽的指导。以下是您可能需要添加的一些内容:
• 资源属性(Resource attributes) :添加 deployment.environment 、 service.version 和 service.instance.id ,以帮助在生产环境中筛选数据。在 Kubernetes 中, OTel Operator 或 OTEL_RESOURCE_ATTRIBUTES 环境变量(env var)可以自动注入 k8s.namespace.name 、 k8s.pod.name 、 k8s.deployment.name 及其他集群元数据。ClickStack 的 默认表模式(default table schema) 已将这些 Kubernetes 属性物化到专用列中,以实现快速过滤;您只需确保它们存在于您的 OTel 资源中。
• 批处理导出器调优(Batch exporter tuning) :默认的批处理导出器设置(512 批次大小,5 秒导出间隔)较为合理,但您可能需要根据实际吞吐量进行调整。
• 安全性(Security) :为 OTLP 端点启用 TLS 并添加身份验证头(authentication headers)。ClickStack 支持使用 API 密钥(API keys)进行 OTLP 数据摄入。
• 物化视图(Materialized views) :随着数据量的增长,ClickStack 可以自动利用 增量物化视图(incremental materialized views) 来加速仪表盘和告警的响应。您可以定义一个在数据插入时预聚合(pre-aggregate)数据的视图(例如,每服务每分钟的平均请求持续时间),ClickStack 将透明地利用它来驱动任何匹配的可视化。无需更改仪表盘。
• 告警(Alerting) :基于已保存的搜索(例如,错误率激增)或仪表盘图表(例如,p99 延迟超过阈值)设置 告警(alerts) 。ClickStack 会定期评估这些告警,并通过 Slack、PagerDuty 或通用 webhook 进行通知。
通过在 ASP.NET 中进行少量 OpenTelemetry 配置,我们能够将单一的超时日志行转化为一个完整的跨服务视图,清晰地展现了实际发生的一切,涵盖了 HTTP 调用、应用程序代码和数据库操作。我们不再需要猜测是哪个服务出现了故障,也无需费力地拼接日志,而是能够端到端地跟踪请求:明确地看到时间消耗在哪里、错误出现在何处、产生了哪些日志,以及涉及了哪些数据库调用。
ClickStack 通过接受标准的 OpenTelemetry 数据、自动关联所有信号并将所有内容存储到 ClickHouse 中,从而简化了这一过程。用户将获得一个快速、灵活的后端,其配备了用于探索的可视化界面,并在需要深入挖掘时提供 SQL 访问。
克隆演示项目,运行 docker compose up -d,并亲自动手尝试。模拟一些故障,打开一条追踪,并追踪请求流。
• 演示源(https://github.com/ClickHouse/dotnet-otel-clickstack-demo)
• ClickStack 文档(https://clickhouse.com/docs/en/observability)
• ClickStack 性能调优(https://clickhouse.com/docs/use-cases/observability/clickstack/performance_tuning)
• ClickHouse 中的全文搜索 — 现已正式发布 (GA)(https://clickhouse.com/blog/full-text-search-ga-release)
• OpenTelemetry.NET 文档(https://opentelemetry.io/docs/languages/dotnet/)
• OTel 数据库语义规范(https://opentelemetry.io/docs/specs/semconv/db/database-spans/)
/END/
征稿启示
面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:[email protected]
关于我们
ClickHouse(clickhouse.com) 是面向 AI 时代打造的高性能实时分析数据库,能够以极致性能处理海量数据分析任务。凭借高并发、低延迟和云原生架构,ClickHouse 广泛应用于可观测性、数据仓库、实时分析及 AI 数据基础设施等场景。我们致力于帮助企业在公有云平台上构建安全、弹性且高性价比的实时分析与 AI 数据平台,加速释放数据价值,推动智能化创新与数字化转型。目前,Trip.com、DiDi、Meta、Sony、Netflix、Deutsche Bank、Sierra、Cloudflare 等全球领先企业均在使用 ClickHouse 支撑其关键业务和数据分析平台。
点击上方关注我们