はじめに
New Relic Advent Calendar 21日目の記事になります。
こんにちは、いしです。
株式会社フォトクリエイトでエンジニアをやっています。
普段はフロントエンドをメインで担当していますが、今年はありがたいことにGoでAPI実装する機会を頂きました。その際にNew Relicの機能をいくつか導入したので、まだまだ勉強中の身ではありますがNew Relic APM導入について簡単に紹介します。
New Relic APMとは
APMとは、Application Performance Managementの略で、アプリケーションやシステムの性能を管理・監視することを指します。
APMを導入していると、アプリケーションやシステムの応答時間やそのアプリケーションを構成する様々な要素のパフォーマンスを可視化でき、何か問題があれば迅速に調査することができるようになります。
導入してみた
仕様言語、ライブラリ
New RelicのAPMを導入したアプリケーションは、Go×GraphQLで作成しています。
GraphQLサーバーを構築するライブラリはgqlgenを使用しており、webフレームワークはechoです。
- Go: version go 1.18
- echo: v4.9.0
- gqlgen: v0.17.10
フロントエンドはReactで作ったSPAです。
GraphQLなので、どのデータを取得/更新するにもhttps://hoge.com/query
という単一のURIにリクエストが送られ、リクエストボディに実際のリクエストのデータが格納されています。
データベースはポスグレです。
導入の流れ
公式ドキュメントを参考に、下記のように導入してみます
-
go getでパッケージを取得
$ go get github.com/newrelic/go-agent/v3/newrelic
-
アプリケーションに
github.com/newrelic/go-agent/v3/newrelic
パッケージをインポートimport github.com/newrelic/go-agent/v3/newrelic
-
main関数、またはinitブロックに以下を追加することで、Goエージェントを初期化
app, err := newrelic.NewApplication( newrelic.ConfigAppName("Your Application Name"), newrelic.ConfigLicense("YOUR_NEW_RELIC_LICENSE_KEY") )
-
echoパッケージを使用しているので、New Relicが提供しているecho統合パッケージをimport
import "github.com/newrelic/go-agent/v3/integrations/nrecho-v4"
-
インポートしたパッケージをwrapして使用する
e.Use(nrecho.Middleware(nrApp))
これにより全リクエストのcontextに自動でNew Reilcのトランザクションが格納され、計測されるようになります。
課題
ミニマムに導入し計測され始めたのですが、まだまだ実運用できるデータはあまり取れず課題がいくつかありました。
課題1: /queryに集約されてしまう
冒頭で記載した通り、GraphQLの特性上https://hoge.com/query
という単一のURIにリクエストが集約されてしまい、全てのリクエストが/queryで表示されているので、どのレスポンスにどれくらいの時間がかかっているかの判別がつきませんでした。
課題2: 複数のドメインオブジェクトを含めたリクエストが計測できない
GraphQLのメリットとして、複数のドメインオブジェクトのデータを一度のリクエストで柔軟に取得できます。
Gitを例にすると、下記のようにレポジトリ一覧とユーザー一覧を一度のリクエストで取得できるのですが、このようにリクエストが飛んできた際も/query
に集約されてしまうので、このリクエストのレスポンスが仮に10秒かかっていた際、repositoryとuserのどちらに時間がかっているのかの判別がつきませんでした。
query getRepositoryAndUser {
repository {
...省略
}
user {
...省略
}
}
課題3: DBの計測ができていない
これは課題というより設定していないので当然ですが、APIレスポンスの時間は測れたとしてもDBのレスポンス時間までは計測していないので、どのDBクエリに時間がかかりボトルネックとなっているかの判別がつきませんでした。
対応策
各課題に対して行ったことです
課題1の対応:フロントで定義した命名をトランザクション名とする
GraphQLのリクエスト時に、フロント側で任意に命名することができます。
query getRepositoryAndUser {
repository {
...省略
}
user {
...省略
}
}
↑の例で言えばgetRepositoryAndUser
がフロント側で定義したクエリ名です。
New Relicのトランザクションにも任意に名前をつけることができるので、このフロントで定義したクエリ名をトランザクション名としました。
gqlgenではAroundOperatins関数が用意されており、この関数はクライアントからリクエストを受け取ったときに最初に呼ばれるミドルウェアです。その関数内で、下記のようにsetNameでトランザクション名を設定しました。
server.AroundOperations(func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
txn := newrelic.FromContext(ctx)
oc := graphql.GetOperationContext(ctx)
// フロント側で設定したquery名をトランザクション名に設定
txn.SetName(oc.Operation.Name)
res := next(ctx)
return res
})
こうすることにより、フロントの命名に依存はされはしますが、どのリクエストにどれくらいかかっているかが一目でわかるようになりました。(数値は黒塗りにしています)
課題2の対応:ルートリゾルバ毎にセグメントする
New RelicのAPMでは、1つのトランザクションを複数のセグメントで区切ることができます。セグメントで区切り計測することで、外部呼び出し、DB呼び出し、キューへのメッセージ追加、バックグラウンドタスクなどの関数やコードブロックにかかる時間を計測することができます。
https://docs.newrelic.com/jp/docs/apm/agents/go-agent/instrumentation/instrument-go-segments/
query getRepositoryAndUser {
repository {
...省略
}
user {
...省略
}
}
↑の例でいえば、ルートリゾルバであるrepositoryとuserでそれぞれセグメントを定義し、どちらにどれくらいの時間がかかっているかを計測できれば良さそうです。
改修後のコードはこちらです
// ルートリゾルバ毎に実行されるミドルウェア
server.AroundRootFields(func(ctx context.Context, next graphql.RootResolver) graphql.Marshaler {
rfCtx := graphql.GetRootFieldContext(ctx)
rootResolverName := rfCtx.Object
txn := newrelic.FromContext(ctx)
// ルートリゾルバ毎にsegmentを作成し、実行時間を計測する。
sgm := txn.StartSegment(rootResolverName)
defer sgm.End()
m := next(ctx)
return m
})
AroundRootFields関数もgqlgenが用意している関数で、ルートリゾルバが呼び出される毎に実行され、ルートリゾルバの名前でセグメントを定義しています。
実際のNew Relicの管理画面の見え方は下記の通りです。
getEventsAndSchoolsForPromotion
というトランザクションをevents
とschools
というセグメントで区切っており、それぞれ並列で実行され計32.65msかかっていることがわかりました。
課題3:DB計測する
DB計測を行いました。DBはポスグレを使用していますがNew Relicにポスグレ用の統合パッケージが用意されていたので、公式ドキュメントを参考に導入しました。
下記のように全てのDBクエリ実行の際にcontextを渡す必要があり、単純な更新とはいえ数百の関数を改修するのは少し手間でした。
- func CreateEvent(tx *sql.Tx, input CreateEventInput) (int, string, error) {
+ func CreateEvent(ctx context.Context, tx *sql.Tx, input CreateEventInput) (int, string, error) {
b := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
q := b.Insert("events").
// ...省略
var eId int
var createTs string
- err := q.RunWith(tx).QueryRow().Scan(&eId, &createTs)
+ err := q.RunWith(tx).QueryRowContext(ctx).Scan(&eId, &createTs)
if err != nil {
return 0, "", errors.WithStack(err)
}
// ...省略
}
結果はこちらです
無事、どのselect文にどれだけ時間がかかっているか分かるようになりました。レスポンスの遅いAPIがあった際、ボトルネックの調査もしやすそうです。
DB実行時にcontextを渡すということは、DB実行がキャンセルされた際もcontextを伝いアプリケーションにキャンセルが伝播されていくので、本来あるべき姿になった気がしています。
所感
他の言語やRESTライクなアプリケーションとは異なり、Go×GraphQLのAPM導入は一定の手間がかかりました。Go×GraphQLの導入方法はNew Relicのドキュメントや検索をしてもなかなか見つからず、New Relicの担当者に導入事例を尋ねてもまだ具体的なご紹介が得られない状況でした。そのため、手探りで進めていく必要がありました。
GraphQLはいくつかの特有のクセがあるため、外部サービスの導入は容易ではない印象です。一方で、世の中にはGo×GraphQLのアプリケーションを利用していて、APMを導入している企業も多いと考えられます。より良い方法があれば是非知りたいものです。
APMに限らず、New Relicの導入においてハマることがあれば、New Relicの担当者に質問すると、丁寧かつ迅速にご回答いただけるので非常に助かりました!
APMでできることやりたいことはまだあるので、業務の合間に粛々と続けていけたらと思います。
終わりに
以上、New Relic APM導入例について書きました。
New RelicはAPM以外にもいろんな機能があり、最近はSession Replayというユーザーがエラーを起こした行動を動画で確認できる(らしい)機能のwaiting listが承認されたりしてワクワクしています。
New Relicは非常に強力なツールなので、ぜひ利用してみてはいかがでしょうか!
また、フォトクリエイト社ではエンジニアを募集しています。
手前味噌ですが、エンジニアチームは少数精鋭でモダンな技術をスピーディーに取り入れており、エンジニアの環境としてはとても良いものに感じています。Github Copilotがリリースされた際には有償にも関わらずすぐに全てのエンジニアが利用できるようになりました。また、GraphQLだけでなくgRPCを採用してアプリを作っていたりします。
「これ使ってみたいやってみたい」と思った技術やサービスがあれば、合理的な理由があれば挑戦させて頂ける環境です。
個人的に一番好きな点は、PdM含め怖い人がいません。みんな優しい。心理的安全性大事!
もし少しでもご興味をお持ちいただけたならば、是非カジュアル面談でお話しさせてください!