😺

CastingONEでotelsqlを導入した話

2023/07/05に公開

はじめまして、CastingONEでバックエンドエンジニアを担当していますなかゆうです!
最近気温の変化で体調を崩しております、みなさんもお身体お気をつけてお過ごしください。

はじめに

CastingONEではさまざまなGoのライラブラリを使用しておりますが、最近Goのテスト実行時にSQLのパフォーマンスをトレースできるようにしたので記事にしました

モチベーション

サービス内で新規にrepositoryメソッドとそのテストを実装した時、実際にどれくらいのパフォーマンスがでているのかが気になっていました。そこで社内の@takashabeさんに質問したり、自分で色々調べてみるとotelやGoのライブラリotelsqlの存在を知りました。otelsqlとエクスポート先のオブザーバビリティツールを使用することで、テスト時に実行されたSQLのトレース情報の可視化が可能になることがわかったので導入に至りました

実際の動くコードだけ見たいよって方は、以下のリンクを踏んでください!

https://github.com/yutaronakayama/otelsql-trace-test

ライブラリ

以下に今回使用しているサービスなどについて簡単に説明します。

otelsqlについて

Goライブラリのotelsqlはdatabase/sqlの各種操作をテレメトリデータとして収集することができるライブラリです。インフラ層などで実装されているSQLの実行結果のトレースが可能です。収集したテレメトリデータはotelgoなどを通じてバックエンドサービスに送信することができます。

(※ここでotelといっているは、OpenTelemetryの略語で otelは、テレメトリデータの収集とエクスポートを可能にするオープンソースの可観測性フレームワークです。)詳細は以下のリンクを参考ください。

参考:https://opentelemetry.io/

オブザーバビリティツール「Jaeger」について

Jaegerは、分散トレーシングシステムで分散システムをGUI上で監視したりトラブルシューティングに利用可能です。今回otelsqlを使って計測されたデータをエクスポート先のJaegerに送ることでトレースデータの可視化が可能となります。

参考:https://www.jaegertracing.io/docs/1.45/

事前に用意したサンプルデータ

今回localでdbを立ち上げ簡単な usersテーブルとそのデータを用意しました。今回トレース対象のデータとなります。

CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  email VARCHAR(255) UNIQUE NOT NULL
);

INSERT INTO users (name, email) VALUES
  ('taro yamada', 'taro-yamada@example.com'),
  ('taro go', 'taro-go@example.com');

実装

前章で紹介させていただいたotelsqlライブラリをもとに簡単にlocalで動くコードを紹介させていただきます。

main.go
package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"

	_ "github.com/go-sql-driver/mysql"
	"github.com/uptrace/opentelemetry-go-extra/otelsql"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

type User struct {
	UserID int    `db:"id"`
	Name   string `db:"name"`
	Email  string `db:"email"`
}

func initTracer() func() {
	// Jaegerへトレース情報を送るためのエクスポータの作成
	exporter, err := jaeger.New(
		jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://localhost:14268/api/traces")),
	)
	if err != nil {
		log.Fatal(err)
	}

	// トレースプロバイダの設定
	bsp := sdktrace.NewBatchSpanProcessor(exporter)
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithSpanProcessor(bsp),
		sdktrace.WithSampler(sdktrace.AlwaysSample()),
	)
	otel.SetTracerProvider(tp)
	otel.SetTextMapPropagator(propagation.TraceContext{})

	return func() {
		if err := tp.Shutdown(context.Background()); err != nil {
			log.Fatal(err)
		}
	}
}

func selectUsers(db *sql.DB) ([]*User, error) {
	users, err := db.Query("SELECT id, name, email FROM users")
	if err != nil {
		return nil, err
	}
	defer users.Close()

	ret := []*User{}
	for users.Next() {
		user := &User{}
		if err := users.Scan(&user.UserID, &user.Name, &user.Email); err != nil {
			return nil, err
		}
		ret = append(ret, user)
	}
	return ret, nil
}

func main() {
	shutdown := initTracer()
	defer shutdown()

	// MySQLデータベースへの接続
	dsn := "root:@tcp(localhost:3306)/otelsql?parseTime=true"
	db, err := otelsql.Open("mysql", dsn)
	if err != nil {
		log.Fatalf("failed to connect to the MySQL database: %v", err)
	}
	defer db.Close()

	// ユーザ情報を取得
	users, err := selectUsers(db)
	if err != nil {
		log.Fatalf("failed to select users: %v", err)
	}
	fmt.Println(users)
}



main_test.go
package main

import (
	"log"
	"testing"

	_ "github.com/go-sql-driver/mysql"
	"github.com/stretchr/testify/assert"
	"github.com/uptrace/opentelemetry-go-extra/otelsql"
)

func Test_selectUsers(t *testing.T) {
	shutdown := initTracer()
	defer shutdown()
	dsn := "root:@tcp(localhost:3306)/otelsql?parseTime=true"
	db, err := otelsql.Open("mysql", dsn)
	if err != nil {
		log.Fatalf("failed to connect to the MySQL database: %v", err)
	}
	defer db.Close()

	tests := []struct {
		name string
		want []*User
	}{
		{
			name: "work",
			want: []*User{
				{
					UserID: 1,
					Name:   "taro yamada",
					Email:  "taro-yamada@example.com",
				},
				{
					UserID: 2,
					Name:   "taro go",
					Email:  "taro-go@example.com",
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := selectUsers(db)
			assert.NoError(t, err)
			assert.ElementsMatch(t, tt.want, got)
		})
	}
}

上記で書かれたコードについて簡単説明させていただくと、まずmain関数の最初に initTracer() を呼び出しています。この関数内でJaeger用のエクスポータの初期化を行い、プロバイダに設定します。あとは otelsql.Openでデータベースに接続し、ユーザ情報を取得するための selectUsersメソッドを用意しmain内で呼び出して完成です。

最後にmain_test.goで selectUsersメソッドのテストを簡単に作成します。

ポイントは、標準ライブラリのdatabase/sqlを使っている方は、おそらく sql.Openを使用されているかと思います。その場合database周りの変更は、SQLをotelsqlに変更するだけで終わりなのでかなり簡単に実装することができます。

実装をしてみた結果

go testコマンドを実行すると、SQLのトレース結果をJaeger上で可視化するところまで可能となりました。

1688115996520

SQLが問題なくtraceできているか、処理速度はどれくらいだったのかなど欲しかった情報が簡単にJaeger上で見ることができるようになりました。いざ実装してみたけど、サービス内で動作が遅かったり、処理が止まっているが具体的にどこが原因なのかがわからない場合には何かと便利になると思います。またotelsqlを使うための変更もかなり軽量であったため、導入も楽に行えたと感じました

最後に

みなさんもotelsqlをサービスに導入してみてはいかがでしょうか!
最後までみていただきありがとございました!

また、弊社ではいっしょに働いてくれるエンジニアを募集中です!!社員でもフリーランスでも、フルタイムでも短時間の副業でも大歓迎なので、気軽にご連絡ください!!

https://www.wantedly.com/projects/1063903

https://www.wantedly.com/projects/768662

Discussion