オブジェクトリレーショナルマッピング(ORM)ツールは、データベースとプログラミング言語の間に、便利な抽象化レイヤーを提供します。ORMツールを使って、データベースのトランザクションを明示的なクエリとしてではなく、コードとして表現し、コードの可読性とフットプリントを大幅に向上させることができます。

Goコミュニティでは、開発者に優しい設計と幅広い機能により、最も人気のあるORMツールの1つであるGORMを使用しています。

GORMを使えば、アプリケーションのデータベース層をより簡単に開発できるかもしれませんが、それが期待されるパフォーマンスを満たしているかどうかは、どのようにして知ることができるのでしょうか?

そこで登場するのがNew Relic Goエージェントです。Go エージェントは、New Relic で Go アプリケーションを計測・観察することができる SDK です。

この記事では、a quick start example from GORM Guides で SDK を使用して Go アプリケーションを手動でインストゥルメントする方法と、New Relic One を使用してデータベース トランザクションのコストとコードのランタイムを監視する方法について紹介します。まだ New Relic のユーザーでない方は、無料でサインアップすることができます。無料アカウントには、100GB/月の無料データ取り込み、1人の無料フルアクセスユーザー、そして無制限の無料ベーシックユーザーが含まれています。アカウントを作成した後、この例に従って操作することができます。

ここでは、GORMクイックスタートの完全インストール版を紹介します。この例ではSQLiteを使用していますが、この記事ではPostgreSQLとMySQLの使用についても説明します。

package main
 
import (
   "context"
   "os"
   "time"
 
   _ "github.com/newrelic/go-agent/v3/integrations/nrsqlite3"
   "github.com/newrelic/go-agent/v3/newrelic"
 
   "gorm.io/driver/sqlite"
   "gorm.io/gorm"
)
 
type Product struct {
   gorm.Model
   Code  string
   Price uint
}
 
func main() {
   app, err := newrelic.NewApplication(
       newrelic.ConfigAppName("GORM App"),
       newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")),
       newrelic.ConfigDebugLogger(os.Stdout),
       newrelic.ConfigDistributedTracerEnabled(true),
   )
   if err != nil {
       panic(err)
   }
 
   // Wait for go-agent to connect to avoid data loss
   app.WaitForConnection(5 * time.Second)
 
   dialector := sqlite.Dialector{
       DriverName: "nrsqlite3",
       DSN:        "test.db",
   }
   gormDB, err := gorm.Open(dialector, &gorm.Config{})
   if err != nil {
       panic("failed to connect database")
   }
 
   // Migrate the schema
   gormDB.AutoMigrate(&Product{})
 
   // Create New Relic Transaction to monitor GORM Database
   gormTransactionTrace := app.StartTransaction("GORM Operation")
   gormTransactionContext := newrelic.NewContext(context.Background(), gormTransactionTrace)
   tracedDB := gormDB.WithContext(gormTransactionContext)
 
   // Create
   tracedDB.Create(&Product{Code: "D42", Price: 100})
 
   // Read
   var product Product
   tracedDB.First(&product, 1)                 // find product with integer primary key
   tracedDB.First(&product, "code = ?", "D42") // find product with code D42
 
   // Update - update product's price to 200
   tracedDB.Model(&product).Update("Price", 200)
   // Update - update multiple fields
   tracedDB.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // non-zero fields
   tracedDB.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
 
   // Delete - delete product
   tracedDB.Delete(&product, 1)
 
   // End New Relic transaction trace
   gormTransactionTrace.End()
 
   // Wait for shut down to ensure data gets flushed
   app.Shutdown(5 * time.Second)
}

New Relic の Go エージェントを設定する

コード例を見ながら、各ステップで行う決定について説明しましょう。Go エージェントには、初期化時に設定できるいくつかの設定オプションがありますが、ここでは取り上げないことに注意してください。詳細は、Go エージェントのドキュメントを参照してください。

 app, err := newrelic.NewApplication(
       newrelic.ConfigAppName("GORM SQLite App"),
       newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")),
       newrelic.ConfigDebugLogger(os.Stdout),
       newrelic.ConfigDistributedTracerEnabled(true),
   )
   if err != nil {
       panic(err)
   }

エージェントは、NewApplication() 関数を呼び出すことで初期化されます。その関数の中で、Go エージェントの実行方法と操作方法に関する様々なオプションを設定するために、多くの config 関数を呼び出すことができます

1. まず、アプリケーションに名前を付けます。この名前は、New Relic でアプリケーションのデータやダッシュボードを識別するために使用されるため、明確で分かりやすいものにすることが重要です。

newrelic.ConfigAppName("GORM SQLite App"),

2. Goエージェントの動作には、ライセンスキーが必要です。ライセンス キーがない場合は、エラーが返されます。この例では、シンプルにするために、NEW_RELIC_LICENSE_KEY という環境変数からキーを取得します。

newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")),

3. Go エージェントは独自のログを生成し、データのハーベスト サイクルや New Relic バックエンドとの通信などのイベントを記録します。また、利便性のために、アプリケーション データが New Relic でホストされている正確な Web アドレスも通知します。これはコード例として意図されたシンプルなワンショットアプリケーションであるため、この例では stdout にログを記録するだけで、それ以上ログを処理することはありません。

newrelic.ConfigDebugLogger(os.Stdout),

その他の設定オプションについては、Goエージェントのロギングに関するドキュメントで詳しく説明されています。

4. 最後に、分散トレース機能をオンにします。分散トレースは、エージェントが分散システム全体のサービスリクエストをトレースし、New Relic ダッシュボードでそれらのリクエストの視覚化を構築できる機能です。分散トレースを有効にすることは、このようなアプリケーションのトレースには必須ではありませんが、分散システム全体のプロセスの実行フローについて、より詳細な情報を取得するのに特に役立ちます。これについては、このブログポストで後ほど詳しく説明します。

newrelic.ConfigDistributedTracerEnabled(true),

このワンショット アプリケーションは非常に高速に実行されるため、エージェントが収集したデータを New Relic に送信する前に、アプリケーションの実行が終了してしまう可能性があります。以下のコード行は、アプリケーションが終了する前に、収集したすべてのデータが New Relic に送信されるようにするものです。このコードは、多くのデータを持つ本番アプリケーションを監視する場合には必要ないでしょう。

// Wait for go-agent to connect to avoid data loss
app.WaitForConnection(5 * time.Second)
 
// YOUR APPLICATION HERE
 
// Wait for shut down to ensure data gets flushed to New Relic Servers
app.Shutdown(5 * time.Second)

Using New Relic instrumented database drivers with GORM

New Relic Go エージェントには、最も人気のあるデータベースプロバイダのための、いくつかのインスツルメンテッドデータベースドライバが含まれています。いくつかの簡単なステップで、Go エージェントの統合ライブラリからこれらのドライバをインポートし、GORM のデフォルトデータベースドライバの代わりに使用することができます。

1. まず、Go エージェントの integrations ライブラリから New Relic データベースドライバをインポートします。先行するアンダースコアとともにインポートすることで、nrsqlite3 ドライバが SQLite との通信に使用されるデータベースドライバとして暗黙のうちに登録されます。

import (
   _ "github.com/newrelic/go-agent/v3/integrations/nrsqlite3"
  )

2. ほとんどのデータベースでは、GORMがNew Relicデータベースドライバを使用するように設定するために、カスタムGORM dialectorオブジェクトを作成する必要があります。dialectorオブジェクトはOpen()関数に渡され、GORMがどのようにデータベース接続を開くかを設定します。dialector オブジェクトの DriverName には、使用する New Relic データベースドライバの名前を指定し、DSN には接続先のデータベースの名前またはアドレスを指定しなければなりません。

import (
   _ "github.com/newrelic/go-agent/v3/integrations/nrmysql"
 )
func main() {
   gormDB, err := gorm.Open(mysql.New(mysql.Config{
       DriverName: "nrmysql",
       DSN:        "my connection string",
   }), &gorm.Config{})
 
   if err != nil {
       panic("failed to connect database")
   }
}

GORMをMySQLまたはPostgreSQLで使用する場合、dialectorオブジェクトを作成する必要はありません。その代わりに、Open()関数が暗黙のうちにインポートしたデータベースドライバを使用し、そのデータベースに固有のConfigオブジェクトでその使用方法を指定します。次の例は MySQL のものですが、PostgreSQL でもコードはまったく同じになります。

import (
   _ "github.com/newrelic/go-agent/v3/integrations/nrsqlite3"
 )
func main() {
   dialector := sqlite.Dialector{
       DriverName: "nrsqlite3",
       DSN:        "test.db",
   }
   gormDB, err := gorm.Open(dialector, &gorm.Config{})
   if err != nil {
       panic("failed to connect database")
   }
}

Goアプリケーションのインストゥルメント化

アプリケーションがGoエージェントを使用するように設定された後、最後のステップはコードのインスツルメンテーションを開始することです。

1. GoエージェントはSDKなので、アプリケーションで明示的にトレースを開始、停止する必要があります。これを行う最も簡単な方法は、トランザクショントレースを開始することです。

gormTransactionTrace := app.StartTransaction("GORM SQLite Operation")
// code you want to trace
gormTransactionTrace.End()

トランザクションが開始されると、直ちにトレースデータの収集が開始されます。トランザクションの名前は、New Relic でそれを識別します。

gormTransactionContext := newrelic.NewContext(context.Background(), txn)
tracedDB := db.WithContext(ctx)

2. トランザクションが開始されたら、そのトランザクションから明示的にコンテキストを作成する必要があります。Goでは、コンテキストは期限やキャンセルシグナル、その他のリクエストに対応した値をAPIやプロセス間で伝達します。バックグラウンドコンテキストは、タイムアウトやキャンセルポリシーを伝えない、空だけどnilではないコンテキストで、デフォルトで使用されることが多いです。コンテキストオブジェクトは、ctx := newrelic.NewContext(context.Background(), txn) という行で生成されます。これはGORM Operationトランザクションのトランザクショントレースと関連付ける追加のメタデータを持つバックグラウンドコンテキストです。このコンテキストはどのサービスコールがこのトランザクショントレースに属しているかを追跡するために使用されます。そのため、新しいGORMデータベースオブジェクトはtracedDB := db.WithContext(ctx) という行でコンテキストから生成されます。これはいくつかのハードコードされたデータベース操作の例で、どのように見えるかを示しています。

// Create New Relic Transaction to monitor GORM Database
gormTransactionTrace := app.StartTransaction("GORM Operation")
gormTransactionContext := newrelic.NewContext(context.Background(), gormTransactionTrace)
tracedDB := gormDB.WithContext(gormTransactionContext)
 
// Create
tracedDB.Create(&Product{Code: "D42", Price: 100})
 
// Read
var product Product
tracedDB.First(&product, 1)                 // find product with integer primary key
tracedDB.First(&product, "code = ?", "D42") // find product with code D42
 
// Update - update product's price to 200
tracedDB.Model(&product).Update("Price", 200)
// Update - update multiple fields
tracedDB.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // non-zero fields
tracedDB.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
 
// Delete - delete product
tracedDB.Delete(&product, 1)
 
// End New Relic transaction trace
gormTransactionTrace.End()

コードでトレースしたい個別のトランザクションごとに、これらの手順を繰り返す必要があります。コードをトレースしてインスツルメントする、より微妙で高度な方法については、Instrument Go transactionsのドキュメントで説明しています。これらの概念についてより詳細なチュートリアルをお探しの場合は、Go Agent Masterclassを近日中にリリースする予定です。

データの収集と分析

アプリケーションを実行するには、必要なgoモジュールがインストールされていることを確認し、次のスニペットに示すようにmain.goを実行する必要があります。

go mod tidy
go run main.go

New Relic Go エージェントは自動的に New Relic に接続し、トランザクションデータをキャプチャし、New Relic に送信します。アプリケーションはコマンドラインのアプリケーションログに、New Relic にあるアプリケーションデータに直接アクセスするための URL を出力します。この Web UI でアプリケーションのデータを確認することができます。

このサンプルアプリケーションでは、非常に短いトランザクションを1つ記録しただけなので、サマリーページはかなりまばらですが、スクロールダウンすると、トレースに関するいくつかの要約統計情報を見ることができます。レスポンスタイムやスループットなどの統計情報は、アプリケーションのトランザクションの平均的なパフォーマンスを高いレベルで把握するのに役立ち、CPU使用率やメモリ使用量などの統計情報は、ハードウェアのパフォーマンスを監視するのに役立ちます。前の例では、平均応答時間が7.84ミリ秒であったことがわかります。その平均応答時間の要因を把握するために、左側のペインから[Databases]を選択します。

Databases ビューは、データベーストランザクションを視覚化します。この例では、データベーストランザクションの大部分は SQLite の更新操作で、平均して更新に 0.341 ミリ秒かかりました。これに対して、selects には 0.108 ミリ秒、inserts には 0.155 ミリ秒がかかっています。このデータから、更新クエリは挿入や選択操作に比べて3倍近くもコストがかかることがわかります。これは貴重な情報ですが、アプリケーションのフローにおいて、これらの平均的な実行時コストが何を意味するかはわかりません。そこで、分散トレースの出番となるわけです。

分散トレースは、トランザクションの各サブセクション(スパン)の実行コストを分解し、実行された順に表示することで、データの文脈を把握することができます。これにより、あるセグメントをいつ実行したか、そのスパンで何が発生したか、そのセグメントで発生したイベントのコストを示す、トランザクションの全体的なビューが構築されます。我々のアプリケーションのトランザクショントレースには、順次実行される1つの挿入、2つの選択、および4つの更新のスパンが含まれています。これらのスパンの実行コストは合計で7.84ミリ秒になり、これがトランザクションの総実行時間でした。更新操作を追跡するスパンは、他のスパンと比較して、実行に時間がかかることと、その数が多いことの両方から、その実行時間の大部分を消費していることがわかります。