---
title: '用 SQL 排查支付故障，OpenTelemetry 接入 Databend Cloud 实战'
date: 2026-11-04
slug: category-engineering/sql-payment-failure-opentelemetry
cover_url: 'https://static.databend.cn/img/202611/1104-01.png'
description: '支付请求为什么变慢，失败订单涉及多少金额？把 OpenTelemetry 的日志、链路和指标接入 Databend Cloud 后，数据工程师可以用熟悉的 SQL 逐步查清这些问题。本文通过一个电商 Demo，演示从数据接入、调用链还原到业务影响分析的过程，并讨论云原生湖仓为何适合承载这类数据。'
tags: [databend]
category: Engineering
authors:
  - Jeremy
---

排查一次支付故障，找到报错日志只是开始。还要沿着调用链看时间花在了哪里，再确认失败的请求涉及哪些订单。如果日志存在 Elasticsearch 或 Loki，链路存在 Jaeger 或 Tempo，指标由 Prometheus 管理，这些信息就需要在不同系统之间核对。进一步分析业务影响时，数据工程师还得把可观测数据与订单数据关联起来，增加了数据导出、字段整理和拼表的工作。

{/* truncate */}

OpenTelemetry（下文简称 OTel）为日志、链路和指标提供了统一的采集与传输规范。应用通过 SDK 或自动埋点产生数据，经由 OTLP 协议发送给 Collector，再由 Collector 处理和导出。数据最终存在哪里、怎样跨信号查询，以及如何关联业务表，仍需要后端来解决。

湖仓和分析数据库也在沿着这个方向接入可观测数据。对已经使用 Databend 做数据分析的团队来说，把 OTel 数据接入现有湖仓，就能沿用熟悉的 SQL 查询方式，并在业务表已入仓的前提下，直接分析故障与订单之间的关系。

Databend 提供的 [OpenTelemetry Collector exporter](https://github.com/databendlabs/opentelemetry-exporter-databend) 负责这一步接入，将 logs、traces、metrics 写成三张普通表。下面用 Databend Cloud 上的一次实际演示，看看这条路径如何跑通。本文截图、动图和视频均来自这次演示。应用与订单数据由模拟程序生成，文中的查询结果来自实际运行。

---

## 接入原理与数据结构

![](https://static.databend.cn/img/202611/1103-02-architecture-zh.png)

数据从应用到查询，经过以下三个步骤：

1. 应用使用 OTel SDK 或自动埋点，通过 OTLP 发给 Collector；

2. Collector 里的 `databend` exporter 把每批数据编码成 NDJSON，通过 Databend 原生的 HTTP Streaming Load 接口写入；

3. 数据落在 `otel_logs`、`otel_traces`、`otel_metrics` 三张表里，在 Worksheet、BendSQL、BI 工具或 Grafana 里用 SQL 查询。

exporter 在写入方式和数据保留上做了以下设计：

**通过 Databend 原生接口写入。**exporter 直接调用 Databend `/v1/streaming_load`，每批数据上传一次，无需拼接 SQL 字面量或引入额外驱动。

**支持三类信号和五种指标类型。**gauge、sum、histogram、exponential histogram、summary 均可写入，各类型中的 exemplar、bucket 边界、min/max 和分位数等信息也会保留。

**数据原样保留，不做有损压平。** 每行是一条日志、一个 span 或一个指标数据点。常用的过滤字段（时间、服务名、trace_id、span_id、名称、指标类型）单独成列，完整的 OTLP 对象放在 `VARIANT` 类型的 `record` 列里。属性类型、span 的 events 和 links、状态以及 dropped 计数都会保留。

**时间戳精确到纳秒。** Databend 的 TIMESTAMP 使用微秒精度，因此这里用 `UInt64` 保存原始纳秒值，64 位整数也不会因经过 float64 转换而丢失精度。

**用 Collector 标准能力做可靠性。** 发送队列（可配持久化存储）、重试退避、超时、TLS、认证扩展都直接复用 Collector 的 exporterhelper 和 confighttp，配置方式和其他 exporter 一样。

**支持自动建表，也可按最小权限配置。** 默认 `create_schema: true`，启动时自动建库建表；生产环境可以让管理员用仓库里的 `schema.sql` 预先建表，exporter 账号只给 INSERT 权限，设置 `create_schema: false`。

---

## 跑通 Demo，将三类数据写入 Databend Cloud

演示代码、完整配置和复现步骤见 Demo 说明。下面按连接、编译、启动和发送数据的顺序操作，也可以先看这段约 50 秒的终端演示：

![](https://static.databend.cn/img/202611/1103-03-demo-terminal.gif)

### 第 1 步：获取连接信息

登录 app.databend.com，点首页右上角的 **Connect**。exporter 需要的是 **Host**（形如 `<tenant>.gw.<region>.default.databend.com`）和 **Warehouse**。

![](https://static.databend.cn/img/202611/1103-04-console-00-connect.png)

> 截图中的租户信息已打码。

建议给 exporter 单独建一个用户，不要用管理员账号。在 Worksheet 中执行以下 SQL：

```SQL
CREATE ROLE otel_exporter_demo_role;
-- 只有 create_schema: true 时需要
GRANT CREATE DATABASE ON *.* TO ROLE otel_exporter_demo_role;
CREATE USER otel_exporter_demo IDENTIFIED BY '<强密码>'
  WITH DEFAULT_ROLE = 'otel_exporter_demo_role';
GRANT ROLE otel_exporter_demo_role TO otel_exporter_demo;
```

### 第 2 步：编译包含 Databend exporter 的 Collector

本次演示使用的 exporter 尚未纳入 OpenTelemetry Collector Contrib 官方发行版，需要通过 OpenTelemetry Collector Builder（ocb）构建自定义 Collector。仓库已提供 builder 配置：

```Bash
git clone https://github.com/databendlabs/opentelemetry-exporter-databend
cd opentelemetry-exporter-databend
go run go.opentelemetry.io/collector/cmd/builder@v0.164.0 \
  --config examples/builder-config.yaml
## 产物：./_build/otelcol-databend
```

需要 Go 1.26 或更高版本。如果你已经有自己的 Collector 发行版，把 exporter 加到 builder 配置的 `exporters` 里即可。

### 第 3 步：配置并启动 Collector

以下列出 exporter 和数据管道的主要配置，完整配置见文档 https://github.com/databendlabs/opentelemetry-exporter-databend/tree/main/examples：

```YAML
exporters:
  databend:
    endpoint: ${env:DATABEND_ENDPOINT}     # https://<Host>
    username: ${env:DATABEND_USERNAME}
    password: ${env:DATABEND_PASSWORD}
    headers:
      X-Databend-Warehouse: ${env:DATABEND_WAREHOUSE}
    database: otel_demo
    create_schema: true
    sending_queue:
      enabled: true
    retry_on_failure:
      enabled: true

service:
  pipelines:
    logs:    { receivers: [otlp], processors: [memory_limiter, batch], exporters: [databend] }
    traces:  { receivers: [otlp], processors: [memory_limiter, batch], exporters: [databend] }
    metrics: { receivers: [otlp], processors: [memory_limiter, batch], exporters: [databend] }
```

凭据通过环境变量注入，无需写入配置文件：

```Bash
export DATABEND_ENDPOINT=https://<tenant>.gw.aws-us-east-2.default.databend.com
export DATABEND_USERNAME=otel_exporter_demo
export DATABEND_PASSWORD='<强密码>'
export DATABEND_WAREHOUSE=default
./_build/otelcol-databend --config collector-cloud.yaml
```

本次演示中，出现 `Everything is ready. Begin running and processing data.` 后，Collector 已启动，exporter 已连接 Databend Cloud，并完成三张表的创建。

![](https://static.databend.cn/img/202611/1103-05-demo-terminal-collector.png)

### 第 4 步：发送数据并确认入库

可以使用支持 OTLP 的程序发送数据。本地接收地址为 `127.0.0.1:4317`（gRPC）或 `127.0.0.1:4318`（HTTP）。已接入 OTel 的服务可将 `OTEL_EXPORTER_OTLP_ENDPOINT` 配置为对应协议的 Collector 地址。

演示使用一个模拟电商应用，一次结账请求依次经过 frontend、checkout、inventory、payment 四个服务。大约 8% 的请求会在支付环节失败（`card declined`），大约 10% 的支付耗时会超过 400 ms。它用官方 OTel Go SDK 同时上报 traces、logs、metrics。

![](https://static.databend.cn/img/202611/1103-06-demo-terminal-shop.png)

回到 Worksheet，可以看到三张表中已经写入数据：

![](https://static.databend.cn/img/202611/1103-07-console-01-tables.png)

这次一共模拟了 2,100 次结账，写入 6,300 条日志、13,628 个 span、693 个指标数据点。本次运行中，Collector 未出现 error 或 warn 日志。

---

## 用 SQL 排查支付故障，分析订单影响

数据写入后，就可以在 Worksheet 中查询。先从报错日志入手，再通过 `trace_id` 还原调用链，下面的动图展示了这个过程：

![](https://static.databend.cn/img/202611/1103-08-demo-console.gif)

### 从日志中定位支付错误

```SQL
SELECT to_timestamp((time_unix_nano // 1000)::Int64) AS ts, service_name,
       record:body:stringValue::STRING AS message,
       record:attributes[0]:value:stringValue::STRING AS error,
       trace_id
FROM otel_demo.otel_logs
WHERE record:severityText::STRING = 'ERROR'
ORDER BY time_unix_nano DESC LIMIT 20;
```

![](https://static.databend.cn/img/202611/1103-09-console-02-error-logs.png)

报错集中在 payment 服务，原因是 `card declined: insufficient funds`。本例中，OTel SDK 将当前 span 的上下文写入日志，因此可以通过日志中的 `trace_id` 继续查找对应链路。

### 用 trace_id 还原请求调用链

从日志中选取一个 `trace_id`，查询该请求的所有 span，再用 `repeat()` 绘制文字版瀑布图：

```SQL
SELECT service_name, name AS span_name, offset_ms, duration_ms,
       repeat('·', floor(offset_ms / 40)::INT)
         || repeat('█', greatest(1, ceil(duration_ms / 40))::INT) AS timeline,
       status
FROM (SELECT service_name, name, time_unix_nano,
             record:status:message::STRING AS status,
             round((time_unix_nano - min(time_unix_nano) OVER ()) / 1e6, 1) AS offset_ms,
             round((record:endTimeUnixNano::UInt64
                    - record:startTimeUnixNano::UInt64) / 1e6, 1) AS duration_ms
      FROM otel_demo.otel_traces WHERE trace_id = '<trace_id>')
ORDER BY time_unix_nano;
```

![](https://static.databend.cn/img/202611/1103-10-console-04-trace-waterfall.png)

这次请求总耗时 482 ms，库存检查用了 20 ms，`PaymentService/Charge` 用了 462 ms，并最终返回失败。耗时主要集中在支付环节。

### 统计各服务的 P95 延迟与错误数

可以直接根据 span 计算延迟和错误数，无需为这次查询预先定义指标：

```SQL
SELECT service_name, name AS span_name, count(*) AS spans,
       round(avg(dur_ms), 1) AS avg_ms,
       round(quantile_cont(0.95)(dur_ms), 1) AS p95_ms,
       sum(if(record:status:code::INT = 2, 1, 0)) AS errors
FROM (SELECT *, (record:endTimeUnixNano::UInt64
                 - record:startTimeUnixNano::UInt64) / 1e6 AS dur_ms
      FROM otel_demo.otel_traces)
GROUP BY service_name, span_name
ORDER BY p95_ms DESC;
```

![](https://static.databend.cn/img/202611/1103-11-console-03-service-latency.png)

结账接口的 P95 延迟为 450.9 ms，支付环节的 P95 为 430.2 ms，inventory 的 P95 仅为 37.4 ms。

### 关联日志与链路，统计失败订单金额

接下来可以把技术故障与订单影响放在一起分析。本例的日志记录了订单金额，span 记录了处理耗时，通过 `trace_id` 和 `span_id` 即可关联：

```SQL
SELECT l.service_name,
       l.record:attributes[0]:value:stringValue::STRING AS error,
       count(*) AS failed_orders,
       round(avg((t.record:endTimeUnixNano::UInt64
                  - t.record:startTimeUnixNano::UInt64) / 1e6), 1) AS avg_charge_ms,
       round(sum(l.record:attributes[1]:value:doubleValue::DOUBLE), 2) AS lost_revenue_usd
FROM otel_demo.otel_logs l
JOIN otel_demo.otel_traces t ON l.trace_id = t.trace_id AND l.span_id = t.span_id
WHERE l.record:severityText::STRING = 'ERROR'
GROUP BY ALL;
```

![](https://static.databend.cn/img/202611/1103-12-console-05-logs-join-traces.png)

本次模拟中，177 笔订单因扣款失败未成交，涉及金额 36,058.96 美元。日志与链路存入 Databend Cloud 后，一次 JOIN 就能将失败原因、订单金额和支付耗时放在同一份结果中。如果订单表也在 Databend Cloud 中，还可以继续按用户、渠道或商品分析影响。

### 查看直方图指标与关联链路

指标表的每一行对应一个数据点，保留了直方图的 bucket 计数、边界和 min/max。exemplar 中的 `trace_id` 还可以用于查询对应的请求链路，进一步查看该样本的执行过程。

![](https://static.databend.cn/img/202611/1103-13-console-07-histogram.png)

### 将查询结果转为请求量趋势图

Worksheet 支持将查询结果切换为图表视图。下图展示了结账接口按 10 秒窗口统计的请求量：

![](https://static.databend.cn/img/202611/1103-14-console-08-chart.png)

---

## 把可观测数据纳入湖仓，能带来什么？

Databend 是云原生湖仓，基于对象存储保存数据，将计算与存储分离，并提供 SQL 分析能力。结构化的业务表和半结构化的日志、链路可以在同一平台中查询。Databend Cloud 提供托管服务，让团队按工作负载配置计算资源。这样的架构为可观测数据的长期保留和关联分析提供了基础。

结合上面的 Demo，用户往往更看重的是数据写入之后能做什么。排查支付故障时，我们已经能在同一条查询中看到错误原因、处理耗时和失败订单金额。如果业务数据也在 Databend Cloud 中，后续就可以继续分析故障影响了哪些用户、哪些渠道，而无需先把日志导出到另一个分析系统。

对数据工程师来说，可观测数据可以和业务数据一起纳入日常分析。要让这样的分析方便开展，既需要统一的查询方式，也需要考虑数据保留、字段变化和现有工具的接入。Databend Cloud 在这些方面提供了相应支持：

**三类信号使用同一套 SQL。** 日志、链路和指标写入同一引擎后，可以通过 `trace_id`、`span_id` 或服务名等字段查询和关联，减少跨系统核对信息的工作。上面的日志与链路 JOIN 就是一个直接例子。

**与已有业务表直接关联。** 当订单、用户和账单数据已经在 Databend Cloud 中，并具备可用的关联字段时，可以直接用 SQL 分析业务影响，无需为一次排查额外导出和搬运数据。分析也可以从单次故障延伸到一段时间内的失败分布。

**按保留周期和查询负载分别配置资源。** 对象存储承载历史数据，计算资源按写入和查询需求配置。团队可以根据复盘、容量规划或审计的需要决定日志与链路的保留时长，避免仅因历史数据增加就同步扩大计算规模。实际成本仍取决于数据量、保留周期和读写负载。

**保留动态属性，按分析需要提取字段。** OTel 属性会随服务和版本变化，`VARIANT` 列可以保存这些半结构化内容。查询时通过 `record:attributes`、`record:status:code` 等路径取值，新增属性通常无需修改表结构，也为后续分析保留了原始信息。

**沿用已有的 OTel 采集方式。** 对已经通过 OTLP 向 Collector 发送数据的应用，接入 Databend Cloud 主要是在 Collector 中增加 exporter 和管道配置。需要同时写入其他后端时，也可以配置多个 exporter，保留已有的监控流程。

**继续使用熟悉的分析工具。** 数据以普通表的形式存储，可以通过支持 Databend 的 BI 工具查询。Databend 的 Grafana 数据源插件也支持用 SQL 配置看板和告警。需要由 AI Agent 辅助查询时，同样可以复用这些表和 SQL 接口。

---

## 组件状态与使用边界

本文演示使用的 exporter 是**开发阶段的独立组件**，尚未进入 OpenTelemetry Collector Contrib 官方发行版。团队计划在收集社区反馈后推动捐赠，使用时需了解以下边界：

- 投递语义是 **at-least-once**：如果写入已经提交但连接在返回前断了，重试可能产生重复行，目前没有去重；

- 队列溢出、重试耗尽或进程异常退出（未开持久化队列时）可能丢数据，生产环境建议开启持久化队列，并配置 batch 和 memory_limiter；

- 表结构变更由运维负责，`CREATE TABLE IF NOT EXISTS` 不会迁移已有表；没有自动 TTL；

- 暂不支持 Profiles 信号；

本次演示对应版本的单元测试包含 race 检测，覆盖率为 90.4%；CI 会启动真实的 Databend 实例运行端到端测试，校验五种指标类型以及纳秒时间戳和整数精度。本文 Demo 已在 Databend Cloud（v1.2.952）上完整运行。

## 获取代码与演示资料
>
> - 代码和文档：github.com/databendlabs/opentelemetry-exporter-databend
>
> - 本文的演示代码、全部 SQL 和 Collector 配置位于仓库的 `demo/` 目录，欢迎试用，并通过 Issue 或 PR 反馈问题。如果已经在生产环境接入，也欢迎分享数据规模和使用场景，帮助我们确定后续的优化方向。
>
>
