サーバーサイドエンジニアの田実です!
Yappliではインフラ・アプリケーションの監視、メトリクスの計測にNewRelicを使っています。
今回、YappliのサーバーサイドアプリケーションにNewRelicを適用したので紹介したいと思います。
Go x gRPCなアプリケーションの場合
gRPCのサーバー側の実装は以下のようにInterceptorをかませることで、NewRelic APMによる計測やcontextを使ったトレースができるようになります。
import ( "github.com/newrelic/go-agent/v3/integrations/nrgrpc" "github.com/newrelic/go-agent/v3/newrelic" ) app, _ := newrelic.NewApplication( newrelic.ConfigAppName("{アプリケーション名}"), newrelic.ConfigLicense("{NewRelicのライセンスキー}"), newrelic.ConfigDistributedTracerEnabled(true), func(config *newrelic.Config) { config.Labels = map[string]string{ "Environment": "{環境名}", // 環境ごとに管理するために設定 } // 何も設定しないとコンテナのホスト名が設定されるので、識別できるように明示的に設定 config.HostDisplayName = "{環境名}" }, ) gRPCServer := grpc.NewServer( grpc.UnaryInterceptor( grpc_middleware.ChainUnaryServer( nrgrpc.UnaryServerInterceptor(app), ) ) )
gRPCのクライアント側の実装もInterceptorを使います。
import ( "github.com/newrelic/go-agent/v3/integrations/nrgrpc" ) conn, err := grpc.Dial(endpoint, grpc.WithStreamInterceptor(nrgrpc.StreamClientInterceptor), grpc.WithUnaryInterceptor(nrgrpc.UnaryClientInterceptor), )
Yappliではsqlxを使ってDBにアクセスしています。DBへのアクセスをトレースする場合、 sqlx.Open()
の呼び出しを変更する必要があります。MySQLの場合はnrmysql, SQLite3の場合はnrsqlite3をimportして指定します。
import ( "context" "github.com/jmoiron/sqlx" _ "github.com/newrelic/go-agent/v3/integrations/nrmysql" ) db, err := sqlx.Open("nrmysql", "{dsn}")
HTTPリクエストは newrelic.RoundTripper()
をTransportに設定します。
import "github.com/newrelic/go-agent/v3/newrelic" client := http.Client{ Transport: newrelic.NewRoundTripper(&http.Transport{ // ... } } client := http.Client{ Transport: newrelic.NewRoundTripper(http.DefaultTransport), }
contextからNewRelicのトランザクション(一連のリクエストに関するトレースのデータ)を抽出したり、トランザクションをcontextに埋め込む場合は newrelic.FromContext
newrelic.NewContext
を使います。
txn := newrelic.FromContext(ctx) ctx = newrelic.NewContext(ctx, txn)
Yappliではuber-go/zapを使ってロギングを行っています。zapのフィールドに入れる場合は FromContext()
でトランザクションを取得して紐づくメタデータを設定します。
import "github.com/newrelic/go-agent/v3/integrations/logcontext" txn := newrelic.FromContext(ctx) md := txn.GetLinkingMetadata() logger, _ := zap.NewProduction() logger = logger.WithZapOption(zap.Fields( zap.String(logcontext.KeyTraceID, md.TraceID), zap.String(logcontext.KeySpanID, md.SpanID), zap.String(logcontext.KeyEntityName, md.EntityName), zap.String(logcontext.KeyEntityType, md.EntityType), zap.String(logcontext.KeyEntityGUID, md.EntityGUID), zap.String(logcontext.KeyHostname, md.Hostname), ))
PHP x 独自フレームワークの場合
PHPのagentのインストールはこちらから。 NewRelicのextensionを使って、PHPアプリケーションが同一ホスト上で動くNewRelicのエージェントにデータを送信し、エージェントがNewRelicにHTTPでデータを送信するというアーキテクチャになっています。詳細はこちらを参照してください。
Laravelなどメジャーなフレームワークの場合はNewRelicのextensionが Controller名@Method名
という感じでよしなにトランザクション名をつけてくれるのですが、独自フレームワークの場合はトランザクション名を明示的に設定する必要があります。
<?php if (extension_loaded('newrelic')) { $path = convertToPath($_SERVER['PATH_INFO']); newrelic_name_transaction($path . '@' . $_SERVER['REQUEST_METHOD']); }
newrelic_name_transaction
関数を使うとトランザクション名を設定できます。Yappliの場合、 path@http_method
の形式で計測できるようにしています。
$_SERVER['PATH_INFO']
はそのまま使うと /api/hoge/111
/api/hoge/222
という感じでそれぞれのトランザクション名が設定されてしまうので、 /api/hoge/{id}
という感じでグルーピングできるように convertToPath()
内で変数部分を置換処理をしています。
置換ロジックが困難なエンドポイントでAPMによる詳細な計測が不要な場合は、グルーピングの範囲を広げて対応しています。
<?php switch ($_SERVER['SCRIPT_NAME']){ case '/asset': newrelic_name_transaction('asset@' . $_SERVER['REQUEST_METHOD']); break; }
また、カスタムパラメータを使ってクライアントや機能ごとにパフォーマンスを測定/分類できるようにしています。
<?php if (extension_loaded('newrelic')) { newrelic_add_custom_parameter('{アプリ識別子}', $applicationID); newrelic_add_custom_parameter('{APIバージョン}', $apiVersion); }
NewRelic APM側ではクエリビルダなどでカスタムパラメータを利用できます。以下の図はAPP_IDのカスタムパラメータを使ったチャートです。
おまけ:Go x NewRelicはどうやってトレースしているのかざっと見てみた
gRPCのUnaryServerInterceptorではトランザクションを開始してcontextにトランザクションを埋め込みます。
func UnaryServerInterceptor(app *newrelic.Application, options ...HandlerOption) grpc.UnaryServerInterceptor { // 省略 return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { txn := startTransaction(ctx, app, info.FullMethod) defer txn.End() ctx = newrelic.NewContext(ctx, txn) resp, err = handler(ctx, req) reportInterceptorStatus(ctx, txn, localHandlerMap, err) return } }
mysql driverをラップしたnrmysqlではラップしたConnやStmtを返すようになります。QueryContext()
などのcontext入りのメソッドを叩くとcontextに入ったトランザクションを使って計測を行っています。
func prepare(original driver.Stmt, err error, bld SQLDriverSegmentBuilder, query string) (driver.Stmt, error) { if nil != err { return nil, err } return optionalMethodsStmt(&wrapStmt{ bld: bld.useQuery(query), original: original, }), nil } func (w *wrapConn) Prepare(query string) (driver.Stmt, error) { original, err := w.original.Prepare(query) return prepare(original, err, w.bld, query) } // QueryContext implements StmtQueryContext. func (w *wrapStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { segment := w.bld.startSegment(ctx) rows, err := w.original.(driver.StmtQueryContext).QueryContext(ctx, args) segment.End() return rows, err }
まとめ
GoやPHPのアプリケーションにNewRelicを適用する方法を紹介しました! NewRelic APMを使うことでN+1クエリやインデックスが貼られていないテーブルが明確になりました。
また、分散トレーシング機能によってアプリケーションを横断した処理の把握がしやすくなったり、トレースIDを使ったトランザクションごとのログ検索もできます。
それ以外にもログを監視して閾値を越えたらアラート通知を行うなど、NewRelicはYappliの運用に欠かせないものとなっています。
このようにヤプリではNewRelicなどを活用してオブザーバビリティを上げ、お客様に高い価値を提供し続けるために日々改善を続けています!