Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

NewRelicをYappliのサービスに適用してみた

サーバーサイドエンジニアの田実です!

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() 内で変数部分を置換処理をしています。

newrelic_name_transaction関数によるトランザクション名変更

置換ロジックが困難なエンドポイントで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のカスタムパラメータを使ったチャートです。

カスタムパラメータを使ったAPMのチャート

おまけ: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を使ったトランザクションごとのログ検索もできます。

トレースIDを使ってアプリケーション全体のログを確認している図

それ以外にもログを監視して閾値を越えたらアラート通知を行うなど、NewRelicはYappliの運用に欠かせないものとなっています。

このようにヤプリではNewRelicなどを活用してオブザーバビリティを上げ、お客様に高い価値を提供し続けるために日々改善を続けています!