Rubyエージェントは、非常に長時間実行されるトランザクションや、決して終了しないトランザクションをトレースするようには設計されていません。このガイドでは、それがメモリの増加を引き起こす理由を説明し、それを回避するための戦略を提供します。
問題
アプリケーションのメモリ使用量が継続的に増加し、それが長時間開いたままになっている1つ以上のトランザクションと相関しています。これは以下の場合に最も一般的です:
- 大規模なバッチの処理や、多数のデータベースクエリまたは外部呼び出しの発行など、長期間1つのトランザクションに留まるバックグラウンドジョブまたはワーカー
- プロセスの存続期間中存在するスレッド(エージェントはこれを1つの継続的に実行されているトランザクションとして扱います)
- 通常よりもはるかに長く実行されるか、または完了しないトランザクション
これが発生する理由
トランザクション内のトレースされた作業単位ごとに、エージェントはセグメントを作成し、トランザクショントレースを構築して各セグメントの親の排他時間を計算できるようにします。transaction_tracer.limit_segments設定(デフォルトは4000)は、トランザクショントレースに追加されるそれらのセグメントの数を制限します。
ただし、その制限に達した後でも、親セグメントの排他的な時間計算を正確に保つために、エージェントは追加の作業単位ごとにセグメントを作成し続け、その開始時間と終了時間を追跡します。実行され続けるトランザクションの場合、このタイミングデータは、トランザクションが開いている限り蓄積され続けます。これは、そのトランザクションに関連付けられたメモリが、トランザクションが終了するまで解放されないことを意味し、長時間実行されるトランザクションや終了しないトランザクションの場合、非常に長い時間になる可能性があります。
ソリューション
設定オプションでメモリの増加を制限する
以下のオプションはいずれもトランザクションを短くするものではありませんが、トランザクションが開いている間にエージェントが保持するデータ量を制限できます。
トレースの制限に達したら、セグメントのタイミングデータを制限します。長時間実行されるトランザクションが
transaction_tracer.limit_segmentsで許可されているよりも多くのセグメントを作成すると予想される場合は、transaction_tracer.cap_segment_artifactsを有効にします(エージェントバージョン10.7.0以降で利用可能、デフォルトでは無効)。セグメントの制限に達すると、エージェントはそのトランザクション内の以降のセグメントの排他時間データの記録を停止し、トランザクションのメモリ増加を制限します。セグメントのタイミングの収集を停止するため、トレードオフとしてトランザクション内の親セグメントのタイミングデータの精度が低下します。
セグメントの制限自体を下げてください。単一のトランザクショントレースに何千ものノードが必要ない場合、
transaction_tracer.limit_segments(デフォルトは4000)を下げると、エージェントはトレースへの新しいセグメントの追加をより早く停止します。トランザクションが実際にこの制限に達しているかどうかを確認するには、デバッグレベルのロギングをオンにしてSegment limit of [segment_limit] reached, ceasing collection.を探してください。スパンイベントのボリュームを削減します。終了した各セグメントは、トランザクショントレースとは独立して、スパンイベントも作成します。異常に多数のセグメントを作成するトランザクションの場合は、
span_events.max_samples_stored(デフォルト2000)を下げるか、スパンレベルのディストリビューティッド(分散)トレーシングの詳細が不要な場合は、span_events.enabled: falseを使用してアプリ全体のスパンイベントを完全に無効にします。これによりメモリ増加の要因の1つは減少しますが、上記で説明した根本的なセグメント/タイミングの蓄積は停止しないため、解決策ではなく部分的な緩和策として扱ってください。Sidekiqジョブがウェブトランザクションを肥大化させるのを防ぎます。長時間実行されるトランザクションが、実際にはリクエスト内でSidekiqジョブを実行するために長時間実行されるウェブトランザクションである場合、デフォルトではジョブの作業はそのウェブトランザクション内のネストされたセグメントになります。そのため、遅いジョブやセグメントが多いジョブは、ウェブトランザクションの期間とセグメント数を引き延ばします。
sidekiq.separate_transactions(エージェントバージョン10.4.0以降で利用可能、デフォルトでは無効)を有効にすると、エージェントはジョブの開始と同時にウェブトランザクションを終了し、代わりにジョブを独自のトランザクションとして記録します。長時間実行されるスレッドの自動トレースをオフにします。単一のジョブではなく、プロセスの存続期間全体にわたって存在するスレッドから増加が発生している場合は、
instrumentation.thread.tracingを無効にすることで、エージェントがスレッドを自動的にインストゥルメントされた状態にするのを停止できます。すべてのスレッドトレースを無効にしたくない場合は、単一のスレッドをNewRelic::Agent.disable_all_tracingでラップして、その単一のスレッドのトレースをオフにすることができます。ミドルウェアからのセグメント数を減らします。サードパーティのRackまたはRailsミドルウェアスタックが大規模なウェブトランザクションの場合、
disable_middleware_instrumentationを使用すると、エージェントが各ミドルウェアを独自のセグメントにラップするのを停止します。
カスタムインストゥルメンテーションを使用します。
トランザクションを分割します。長いトランザクションの場合は、カスタムインストゥルメンテーションを使用して、トランザクション内の各作業単位を独自の短いトランザクションとして計装することを検討できます。各トランザクションのセグメントデータは、1つのトランザクションの期間全体にわたって蓄積されるのではなく、そのトランザクションが終了するとすぐに記録および解放されます。
NewRelic::Agent::Tracer.in_transactionの使用:require 'new_relic/agent/tracer'def process_large_batch(items)items.each do |item|NewRelic::Agent::Tracer.in_transaction(partial_name: 'Custom/process_item', category: :task) doprocess_item(item)endendendこれにより、継続的に増大する1つのトランザクションを多数の短命なトランザクションに置き換えながら、メトリクス、トレース、エラーレポートを期待どおりに機能させることができます。
ジョブの実行期間中、セグメントの蓄積を完全に停止します。長時間実行されるジョブの本体を
NewRelic::Agent.disable_all_tracingでラップすると、ブロック内で実行された作業がトランザクションにアタッチされることはないため、上記で説明した蓄積が完全に停止します:def perform(*args)NewRelic::Agent.disable_all_tracing dodo_the_long_running_work(*args)endendトレードオフとして、ブロック内のすべての計装の詳細が失われます。そのトレードオフに価値があるかどうかは、ジョブに対してどの程度の可視性が必要かによって異なります。
OpenTelemetryを使用して長時間実行される作業を計装する
上記のオプションを適用してもエージェントのトランザクションモデルに適合しないコードについては、その部分のコードをOpenTelemetry Ruby SDKでインストゥルメントされた状態にし、OTLP経由でNew Relicにエクスポートすることを検討してください。ジョブの全期間にわたってデータを保持する同じ長時間実行トランザクションオブジェクトを持つ代わりに、OpenTelemetryは各スパンが終了するとすぐにエクスポートします。
重要
これは、RubyエージェントのOpenTelemetry APIサポートとは異なります。その機能は、OpenTelemetry APIコールをエージェント独自のトランザクションおよびセグメントモデルに変換するため、上記で説明したのと同じメモリ動作の対象となります。独自のOTLPエクスポーターを備えたスタンドアロンのOpenTelemetry SDKを使用すると、エージェントのトランザクション/セグメントトラッキングを完全に回避できます。
詳細については、OpenTelemetryとNew Relicの概要を参照してください。