ゼロから学ぶ AWS Distro for OpenTelemetry 〜自動計装
こんにちは。
ご機嫌いかがでしょうか。
"No human labor is no human error" が大好きな吉井 亮です。
最近のアプリケーションは分散が当たり前になりました。Ops に関わる人間として思うことは、従来のログとメトリクスだけ取得している監視ではエラー時の対応が難しいということです。
何らかの APM を導入して追跡可能な状態にしておくことが必須です。
ということで、今回は AWS Distro for OpenTelemetry (以下、ADOT) を試してみました。
ADOT とは
AWS がサポートする OpenTelemetry のディストリビューションです。
アプリケーションのメトリクスやトレースなどを収集し、バックエンド (AWS サービスやサードパーティ) へ送信する仕組みを提供します。
また、AWS リソースやマネージドサービスとの親和性が高く、これらのパフォーマンスデータの収集に優れています。
ADOT に含まれている主なコンポーネントは以下です。
- SDK
- 自動計装エージェント
- コレクター
https://aws-otel.github.io/docs/introduction
自動計装をやってみた
エージェントによる自動計装をやってみます。マニュアル計装はまた後日。
EC2 上の JAVA アプリケーションにエージェントを入れて起動させ、エージェントで収集したデータを CloudWatch へ送信します。
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 ファイルを作成してください。
{
"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")
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 レスポンスコードごと、スロットリングが表示されました。それっぽい感じです。
メトリクスも表示されています。こちらは EC2 標準で取得しているメトリクスです。OpenTelemetry から取得したものではありません。
トレース
左ペインの X-Ray トレース
→ トレース
を開きます。
curl コマンドを打った数だけトレースが取得されています。
任意の ID をクリックします。
アノテーション
思ってたより簡単に OpenTelemetry によるトレースを計装できました。
自動計装でもアプリケーションによっては運用で使用できると思います。ただ、もう少し詳しくデバッグしたいと感じます。
メソッドごとに span を取ってみます。
自動計装で span を追加するには opentelemetry-instrumentation-annotations
ライブラリを使います。
build.gradle.kts にまたまた1行を追加します。
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 を以下のように変更しました。
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
を記述した箇所ですね。
サンプリング
トレースを取得することは予期せぬ問題のトラブルシューティングに欠かせません。
ただ、当然デメリットもあります。CloudWatch にトレースデータを送信すれば従量制で料金が発生します。アプリケーションのパフォーマンスにも影響します。
本番環境ではサンプリングを行い、トレースデータを削減します。データを間引いて CloudWatch へ送信するわけです。
サンプリングは確率で決定するため、エラーのあるトレースを逃してしまう欠点はあります。
OTEL_TRACES_SAMPLER=parentbased_traceidratio
と OTEL_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