🔖

ゼロから学ぶ AWS Distro for OpenTelemetry 〜自動計装

2023/11/07に公開

こんにちは。
ご機嫌いかがでしょうか。
"No human labor is no human error" が大好きな吉井 亮です。

最近のアプリケーションは分散が当たり前になりました。Ops に関わる人間として思うことは、従来のログとメトリクスだけ取得している監視ではエラー時の対応が難しいということです。
何らかの APM を導入して追跡可能な状態にしておくことが必須です。

ということで、今回は AWS Distro for OpenTelemetry (以下、ADOT) を試してみました。

ADOT とは

AWS がサポートする OpenTelemetry のディストリビューションです。
アプリケーションのメトリクスやトレースなどを収集し、バックエンド (AWS サービスやサードパーティ) へ送信する仕組みを提供します。
また、AWS リソースやマネージドサービスとの親和性が高く、これらのパフォーマンスデータの収集に優れています。

ADOT に含まれている主なコンポーネントは以下です。

  • SDK
  • 自動計装エージェント
  • コレクター

img
https://aws-otel.github.io/docs/introduction

自動計装をやってみた

エージェントによる自動計装をやってみます。マニュアル計装はまた後日。
EC2 上の JAVA アプリケーションにエージェントを入れて起動させ、エージェントで収集したデータを CloudWatch へ送信します。

img

CloudWatch Agent

今回は CloudWatch Agent を使います。
アプリケーションや AWS リソースで収集したトレースを CloudWatch に送信するための CloudWatch Agent です。

ADOT コレクターを別途構築したほうが OpenTelemetry の全てを引き出せるはずですが、今回は CloudWatch にトレースデータを送信するだけなので CloudWatch Agent を選択しました。(本当は個人的に使ってみたかっただけ)

CloudWatch Agent の利点は、ログ・メトリクス・トレースの3点セットを収集できるところです。

インスタンスプロファイル作成

まずは、インスタンスプロファイルを作成します。手順は以下を参照ください。
Create IAM roles and users for use with CloudWatch agent

インスタンスプロファイル (IAM ロール) には以下3つのポリシーをアタッチします。

  • AWSXRayDaemonWriteAccess
  • CloudWatchAgentServerPolicy
  • AmazonSSMManagedInstanceCore

CloudWatch Agent インストール

CloudWatch Agent をインストールします。過去にインストール済みでも最新バージョンにしておきましょう。OpenTelemetry をサポートするには Version 1.300025.0 以降が必要です。

Amazon Linux 2 の場合は yum でインストールします。

sudo yum install amazon-cloudwatch-agent

その他詳しいことは Installing the CloudWatch agent を参照ください。

構成ファイル作成と読み込み

CloudWatch Agent 構成ファイルを作成します。
traces 項目がテレメトリデータを受け取るエンドポイントです。

cw-agent.json というファイルを作成して以下を貼り付けます。
ログやメトリクスも取得したい場合は Manually create or edit the CloudWatch agent configuration file を参照して JSON ファイルを作成してください。

cw-agent.json
{
  "agent": {
    "logfile": "/opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log",
    "region": "us-west-2"
  },
  "traces": {
    "traces_collected": {
      "otlp": {
        "grpc_endpoint": "127.0.0.1:4317",
        "http_endpoint": "127.0.0.1:4318"
      }
    }
  }
}

構成ファイルを CloudWatch Agent に読み込ませます。

sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c file:cw-agent.json 

今回は1台なのでファイルから読み込みましたが、台数が多い場合はパラメーターストアを使うのが便利です。
Installing the CloudWatch agent on EC2 instances using your agent configuration

念のため、netstat を打ってエンドポイントが準備 OK なことを確認します。

$ netstat -ant

Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 127.0.0.1:4318          0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:4317          0.0.0.0:*               LISTEN   

サンプルアプリケーション

OpenTelemetry のサイトにあるサイコロを振るサンプルアプリケーションを利用します。
Getting Started

上のサイトにある3つのファイルを作成し、同じディレクトリに保存します。

  • build.gradle.kts
  • DiceApplication.java
  • RollController.java

自動計装エージェント

aws-otel-java-instrumentation から最新のエージェントをダウンロードします。
build.gradle.kts に次の1行を追加します。 implementation("software.amazon.opentelemetry:aws-opentelemetry-agent:1.31.0")

build.gradle.kts
plugins {
  id("java")
  id("org.springframework.boot") version "3.0.6"
  id("io.spring.dependency-management") version "1.1.0"
}

sourceSets {
  main {
    java.setSrcDirs(setOf("."))
  }
}

repositories {
  mavenCentral()
}

dependencies {
  implementation("org.springframework.boot:spring-boot-starter-web")
  implementation("software.amazon.opentelemetry:aws-opentelemetry-agent:1.31.0")
}

サンプルアプリケーションを実行します。-javaagent オプションにエージェントの jar ファイルを指定します。

gradle assemble

OTEL_METRICS_EXPORTER=none \
OTEL_RESOURCE_ATTRIBUTES="service.name=dice-server" \
java -javaagent:./aws-opentelemetry-agent.jar -jar ./build/libs/java-simple.jar

起動しました。「opentelemetry-javaagent - version: 1.31.0-aws」とあるので意図通りに動いているようです。

BUILD SUCCESSFUL in 3s
4 actionable tasks: 4 executed
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
[otel.javaagent 2023-11-07 05:21:16:255 +0000] [main] INFO io.opentelemetry.javaagent.tooling.VersionLogger - opentelemetry-javaagent - version: 1.31.0-aws
2023-11-07T05:21:22.314Z  INFO 7881 --- [           main] otel.DiceApplication                     : Starting DiceApplication using Java 21.0.1 with PID 7881 

動作確認します。以下のコマンドを打つと 1~6 の数字がランダムで返ってきます。数回打ってみます。

curl http://localhost:8080/rolldice

CloudWatch

トレース情報を CloudWatch で確認します。
マネジメントコンソールで CloudWatch 画面を開きます。

ダッシュボード

左ペインの X-Ray トレースサービスマップ を開きます。
マップに EC2 インスタンスが表示されています。インスタンスを選択し ダッシュボードを表示 をクリックします。

レイテンシーやリクエスト数、HTTP レスポンスコードごと、スロットリングが表示されました。それっぽい感じです。

trace

メトリクスも表示されています。こちらは EC2 標準で取得しているメトリクスです。OpenTelemetry から取得したものではありません。

trace

トレース

左ペインの X-Ray トレーストレース を開きます。
curl コマンドを打った数だけトレースが取得されています。

trace

任意の ID をクリックします。

trace

アノテーション

思ってたより簡単に OpenTelemetry によるトレースを計装できました。
自動計装でもアプリケーションによっては運用で使用できると思います。ただ、もう少し詳しくデバッグしたいと感じます。
メソッドごとに span を取ってみます。

自動計装で span を追加するには opentelemetry-instrumentation-annotations ライブラリを使います。

build.gradle.kts にまたまた1行を追加します。

build.gradle.kts
plugins {
  id("java")
  id("org.springframework.boot") version "3.0.6"
  id("io.spring.dependency-management") version "1.1.0"
}

sourceSets {
  main {
    java.setSrcDirs(setOf("."))
  }
}

repositories {
  mavenCentral()
}

dependencies {
  implementation("org.springframework.boot:spring-boot-starter-web")
  implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:1.31.0")
  implementation("software.amazon.opentelemetry:aws-opentelemetry-agent:1.31.0")
}

さらに、コードにも import@WithSpan を追加します。
RollController.java を以下のように変更しました。

RollController.java
package otel;

import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.opentelemetry.instrumentation.annotations.WithSpan;


@RestController
public class RollController {
  private static final Logger logger = LoggerFactory.getLogger(RollController.class);

  @WithSpan
  @GetMapping("/rolldice")
  public String index(@RequestParam("player") Optional<String> player) {
    int result = this.getRandomNumber(1, 6);
    if (player.isPresent()) {
      logger.info("{} is rolling the dice: {}", player.get(), result);
    } else {
      logger.info("Anonymous player is rolling the dice: {}", result);
    }
    return Integer.toString(result);
  }

  @WithSpan
  public int getRandomNumber(int min, int max) {
    return ThreadLocalRandom.current().nextInt(min, max + 1);
  }
}

サンプルアプリケーションを再実行します。動作確認用の curl コマンドを打ちます。
CloudWatch 画面でトレースを見てみます。さきほどと違って span (棒線) が増えています。@WithSpan を記述した箇所ですね。

trace

サンプリング

トレースを取得することは予期せぬ問題のトラブルシューティングに欠かせません。
ただ、当然デメリットもあります。CloudWatch にトレースデータを送信すれば従量制で料金が発生します。アプリケーションのパフォーマンスにも影響します。

本番環境ではサンプリングを行い、トレースデータを削減します。データを間引いて CloudWatch へ送信するわけです。
サンプリングは確率で決定するため、エラーのあるトレースを逃してしまう欠点はあります。
OTEL_TRACES_SAMPLER=parentbased_traceidratioOTEL_TRACES_SAMPLER_ARG=0.3 を環境変数にセットしてアプリケーションを起動します。
0.3 は 30% という意味です。アプリケーションの様子を見ながら増減させてください。エラー率の低いアプリケーション、パフォーマンスがシビアなアプリケーションなど様々なケースがあると思います。

詳細な説明は Sampling をご覧ください。テールサンプリングというハイレベルなサンプリング方法もあります。

参考

About AWS Distro for OpenTelemetry
AWS Distro for OpenTelemetry Documentation
GitHub aws-observability
AWS Observability Best Practices

Discussion