otelsql 入门指南:Go SQL 的 OpenTelemetry 监控

博客文章在发布后不会更新。这篇文章已经发布一年多了,其内容可能已过时,部分链接可能无效。在依赖任何信息之前,请务必核实。

otelsql 是 Go 编程语言中 database/sql 库的一个监控库。它在应用程序与数据库交互时生成跟踪(traces)和指标(metrics)。通过这样做,该库可以帮助您识别 SQL 查询中可能影响应用程序性能的错误或延迟。

让我们深入了解如何使用这个库!

入门

otelsql 是 database/sql 接口的一个包装层。当用户使用经过包装的数据库接口时,otelsql 会生成遥测数据,并将操作传递给底层数据库。

在接下来的示例中,您将使用 Docker Compose 来运行 otelsql 存储库中的 otel-collector 示例。此示例使用带有 otelsql 监控的 MySQL 客户端。生成的遥测数据将推送到 OpenTelemetry Collector。然后,它会在 Jaeger 上显示跟踪数据,在 Prometheus 服务器上显示指标数据。

数据流如下

flowchart LR;
    A[MySQL client]-->B[OpenTelemetry Collector];
    B-->C["Jaeger (trace)"];
    B-->D["Prometheus (metrics)"];

让我们克隆 otelsql 存储库在这里,运行示例,并查看最重要的几行代码。

git clone https://github.com/XSAM/otelsql.git

在 otelsql 文件夹中,您还可以检查 git 标签到 v0.29.0(撰写此文时最新的标签),以确保示例是可运行的,因为运行示例的步骤将来可能会有所更改。

git checkout tags/v0.29.0

让我们进入 otel-collector 示例文件夹并启动所有服务。

cd example/otel-collector
docker compose up -d

在构建映像并运行服务后,让我们检查服务日志,以确保 SQL 客户端已完成。

docker compose logs client

然后,我们可以访问位于 localhost:16686 的 Jaeger UI 和位于 localhost:9090 的 Prometheus UI 来查看结果。

这里我们在 Jaeger 上查看跟踪图。我们可以看到与数据库的每次操作的持续时间和参数。

example of Jaeger UI

这里我们在 Prometheus 上查看指标 db_sql_latency_milliseconds_sum

example of Prometheus UI

更多 otelsql 生成的指标选项可以在 otelsql 文档中找到。

理解示例

让我们先看一下 docker-compose.yaml 文件。

version: '3.9'
services:
  mysql:
    image: mysql:8.3
    environment:
      - MYSQL_ROOT_PASSWORD=otel_password
      - MYSQL_DATABASE=db
    healthcheck:
      test:
        mysqladmin ping -h 127.0.0.1 -u root --password=$$MYSQL_ROOT_PASSWORD
      start_period: 5s
      interval: 5s
      timeout: 5s
      retries: 10

  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.91.0
    command: ['--config=/etc/otel-collector.yaml']
    volumes:
      - ./otel-collector.yaml:/etc/otel-collector.yaml
    depends_on:
      - jaeger

  prometheus:
    image: prom/prometheus:v2.45.2
    volumes:
      - ./prometheus.yaml:/etc/prometheus/prometheus.yml
    ports:
      - 9090:9090
    depends_on:
      - otel-collector

  jaeger:
    image: jaegertracing/all-in-one:1.52
    ports:
      - 16686:16686

  client:
    build:
      dockerfile: $PWD/Dockerfile
      context: ../..
    depends_on:
      mysql:
        condition: service_healthy

此 Docker Compose 文件包含五个服务。client 服务是由 Dockerfile 构建的 MySQL 客户端,其源代码是示例文件夹中的 main.go。client 服务在 mysql 服务启动后运行。然后,它初始化 OpenTelemetry 客户端和 otelsql 监控,向 mysql 服务发出 SQL 查询,并通过 OpenTelemetry Protocol (OTLP) 将指标和跟踪数据发送到 otel-collector 服务。

接收到数据后,otel-collector 服务会转换数据格式,并将指标数据发送到 prometheus 服务,将跟踪数据发送到 jaeger 服务。

让我们检查 main.go,看看 client 服务中发生了什么。这是 main 函数。

func main() {
	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
	defer cancel()

	conn, err := initConn(ctx)
	if err != nil {
		log.Fatal(err)
	}

	shutdownTracerProvider, err := initTracerProvider(ctx, conn)
	if err != nil {
		log.Fatal(err)
	}
	defer func() {
		if err := shutdownTracerProvider(ctx); err != nil {
			log.Fatalf("failed to shutdown TracerProvider: %s", err)
		}
	}()

	shutdownMeterProvider, err := initMeterProvider(ctx, conn)
	if err != nil {
		log.Fatal(err)
	}
	defer func() {
		if err := shutdownMeterProvider(ctx); err != nil {
			log.Fatalf("failed to shutdown MeterProvider: %s", err)
		}
	}()

	db := connectDB()
	defer db.Close()

	err = runSQLQuery(ctx, db)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Example finished")
}

这个 main 函数非常直接。它与 otel-collector 服务初始化连接,该连接由 tracer provider 和 meter provider 使用。然后,它使用 connection 和一个 shutdown 方法来配置 tracer provider 和 meter provider,这确保了遥测数据可以在应用程序退出前正确推送到 otel-collector 服务。完成 OpenTelemetry 客户端设置后,它调用 connectDB 方法来使用 otelsql 库与 MySQL 数据库进行交互。让我们看看这里的细节。

func connectDB() *sql.DB {
	// Connect to database
	db, err := otelsql.Open("mysql", mysqlDSN, otelsql.WithAttributes(
		semconv.DBSystemMySQL,
	))
	if err != nil {
		log.Fatal(err)
	}

	// Register DB stats to meter
	err = otelsql.RegisterDBStatsMetrics(db, otelsql.WithAttributes(
		semconv.DBSystemMySQL,
	))
	if err != nil {
		log.Fatal(err)
	}
	return db
}

我们没有使用 Go 提供的 sql.Open 方法,而是使用 otelsql.Open 来创建一个 sql.DB 实例。otelsql.Open 返回的 sql.DB 实例是一个包装器,它将所有 DB 操作传递并监控给底层 sql.DB 实例(由 sql.Open 创建)。当用户使用此包装器发送 SQL 查询时,otelsql 可以看到查询并使用 OpenTelemetry 客户端生成遥测数据。

除了使用 otelsql.Openotelsql 还提供了三种额外的初始化监控的方法:otelsql.OpenDBotelsql.Registerotelsql.WrapDriver。这些额外的方法涵盖了不同的用例,因为有些数据库驱动程序或框架不提供直接创建 sql.DB 的方式。有时,您可能需要这些额外的方法来手动创建 sql.DB 并将其推送到那些数据库驱动程序。您可以查看 otelsql 文档中的示例来了解如何使用这些方法。

接着,我们使用 otelsql.RegisterDBStatsMetrics 来注册 sql.DBStats 中的指标数据。指标记录过程在后台运行,并在注册后根据需要更新指标的值,因此我们无需担心为此创建单独的线程。

在拥有一个由 otelsql 包装的 sql.DB 后,我们就可以使用它来执行查询。

func runSQLQuery(ctx context.Context, db *sql.DB) error {
	// Create a parent span (Optional)
	tracer := otel.GetTracerProvider()
	ctx, span := tracer.Tracer(instrumentationName).Start(ctx, "example")
	defer span.End()

	err := query(ctx, db)
	if err != nil {
		span.RecordError(err)
		return err
	}
	return nil
}

func query(ctx context.Context, db *sql.DB) error {
	// Make a query
	rows, err := db.QueryContext(ctx, `SELECT CURRENT_TIMESTAMP`)
	if err != nil {
		return err
	}
	defer rows.Close()

	var currentTime time.Time
	for rows.Next() {
		err = rows.Scan(&currentTime)
		if err != nil {
			return err
		}
	}
	fmt.Println(currentTime)
	return nil
}

这个 runSQLQuery 方法首先创建一个父 Span(这是一个可选步骤,它使查询 Span 具有父级,并且在跟踪图中看起来更好),然后从 MySQL 数据库查询当前时间戳。

在此方法之后,client 应用程序完成并退出。这些是理解示例最重要的几行代码。

将示例用作游乐场

在理解了示例之后,我们可以将其用作游乐场,使其变得稍微复杂一些,以了解它在实际场景中的用法。

使用以下代码替换示例中的 runSQLQuery 方法。

func runSQLQuery(ctx context.Context, db *sql.DB) error {
    // Create a parent span (Optional)
    tracer := otel.GetTracerProvider()
    ctx, span := tracer.Tracer(instrumentationName).Start(ctx, "example")
    defer span.End()

    runSlowSQLQuery(ctx, db)

    err := query(ctx, db)
    if err != nil {
        span.RecordError(err)
        return err
    }
    return nil
}

func runSlowSQLQuery(ctx context.Context, db *sql.DB) {
    db.QueryContext(ctx, `SELECT SLEEP(1)`)
}

这次,我们在示例中添加了一个新的查询,这是一个慢查询,需要 1 秒才能返回。让我们看看会发生什么,以及如何识别这个慢查询。

为了使此更改生效,我们需要重新构建 client 服务。

docker compose build client
docker compose up client

在客户端完成后,我们可以查看 Jaeger 上我们刚刚生成的跟踪的跟踪图。

example of real-world-like Jaeger UI

从这张图中,我们知道整个示例需要 1 秒才能完成。导致此延迟的根本原因与与数据库的网络延迟和时间戳查询无关。是 SELECT SLEEP(1) 查询导致了延迟。

您还可以通过指标中的数据库聚合统计数据来了解延迟。这就是 otelsql 可以提供的可观测性,因此您可以了解您的应用程序正在对数据库做什么。

兼容性

您可能担心与其他数据库以及其他第三方数据库框架(如 ORM)的兼容性问题,并想知道此监控的可用性有多广泛。

从实现的角度来看,只要数据库驱动程序或数据库框架通过 database/sql 结合 context 与数据库(任何数据库,不只是 SQL 数据库)进行交互,otelsql 应该都能正常工作。

这是一个 示例,展示了 otelsql 如何与 Facebook 的 Go 实体框架一起使用。

其他炫酷功能

现在您已经体验了主要功能,让我们花点时间探索 otelsql 提供的其他炫酷功能。

Sqlcommenter 支持

otelsql 集成了 Sqlcommenter,这是一个开源的 ORM 自动监控库,通过将注释注入 SQL 语句来与 OpenTelemetry 合并,从而实现数据库的上下文传播。

使用 WithSQLCommenter 选项,otelsql 为它监控的每个 SQL 语句注入一个注释。

例如,发送到数据库的 SQL 查询

SELECT * from FOO

变成

SELECT * from FOO /*traceparent='00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',tracestate='congo%3Dt61rcWkgMzE%2Crojo%3D00f067aa0ba902b7'*/

然后,支持 Sqlcommenter 的数据库可以记录此查询的某个特定跟踪的操作,并将其跟踪 Span 发布到跟踪存储,这样您就可以在一个地方看到您的应用程序跟踪 Span 与来自数据库的查询跟踪 Span 相关联。

example of Sqlcommenter from google cloud document

图片来自 google cloud 文档

自定义 Span 名称

如果您不喜欢默认的 Span 名称,可以使用 otelsql.WithSpanNameFormatter 来自定义 Span 名称。

这是示例用法

otelsql.WithSpanNameFormatter(func(ctx context.Context, method otelsql.Method, query string) string {
    return string(method) + ": " + query
})

然后,Span 名称可以变成 {method}: {query}。下面是一个 Span 名称的示例

sql.conn.query: select current_timestamp

过滤 Span

您可以使用 otelsql.SpanOptions 中的 otelsql.SpanFilter 来过滤掉您不想生成的 Span。当您想丢弃某些 Span 时,这很有用。

下一步?

您现在应该能够将从这篇博文中学习到的内容应用到您自己的 otelsql 安装中了。

我很想听听您的经验!如果您觉得 otelsql 有帮助,请给它点星!如果您遇到任何问题,请随时 联系我们创建问题

最后修改于 2024 年 10 月 2 日:将 autoinstrument 更新为单个词 (#5284) (82bd738d)