JavaアプリケーションをElastic APMで計測してみる

# はじめに

JavaアプリケーションでElastic APMを使って可視化してみたメモ。
APMに関する説明は他の方の投稿を参考にしてください。


# 環境

使用した環境は以下のとおり。

- CentOS 7.5
- Elasticsearch 7.8.0
- Kibana 7.8.0
- APM Server 7.8.0
- APM Java Agent 1.17.0

アプリケーションは測定対象のアプリケーションはWindows上で動かします。


## OpenJDKのインストール

まず、OpenJDK 8をインストールします。

```
# yum install java-1.8.0-openjdk-devel

# java -version
openjdk version "1.8.0_252"
OpenJDK Runtime Environment (build 1.8.0_252-b09)
OpenJDK 64-Bit Server VM (build 25.252-b09, mixed mode)
```

JAVA_HOMEを設定し、PATHにJavaのパスを追加しています。

```terminal
# echo "export JAVA_HOME=$(readlink -e $(which java)|sed 's:/bin/java::')" > /etc/profile.d/java.sh
# echo "PATH=\$PATH:\$JAVA_HOME/bin" >> /etc/profile.d/java.sh
# source /etc/profile
```

## Elasticsearchのインストール

Elasticsearch, Kibana, APM Serverを順番にインストールしていきます。
まずは、Elasticsearchをインストールします。

```
curl -L -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.8.0-x86_64.rpm
rpm -i elasticsearch-7.8.0-x86_64.rpm
```

Elasticsearchを外部からアクセスできるように設定します。
※今回の構成ではやらなくてもOKだったはずです。

```
# vi /etc/elasticsearch/elasticsearch.yml
#network.host: 192.168.0.1
network.host: 0.0.0.0
transport.host: localhost
```

自動起動するように設定し、起動まで実行します。

```
# systemctl enable elasticsearch
# systemctl start elasticsearch
```

最後に動作確認。

```
[root@elasticserver1 ~]# curl http://127.0.0.1:9200
{
  "name" : "elasticserver1",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "ZUiSSmOETuOAnZp2Adz9dQ",
  "version" : {
    "number" : "7.8.0",
    "build_flavor" : "default",
    "build_type" : "rpm",
    "build_hash" : "757314695644ea9a1dc2fecd26d1a43856725e65",
    "build_date" : "2020-06-14T19:35:50.234439Z",
    "build_snapshot" : false,
    "lucene_version" : "8.5.1",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}
```


## Kibanaのインストール

Elasticsearchのインストールが終わったら、次はKibanaをインストールします。

```
# wget https://artifacts.elastic.co/downloads/kibana/kibana-7.8.0-x86_64.rpm
# rpm -i kibana-7.8.0-x86_64.rpm
```

Kibanaを外部から接続できるように設定します。

```
# vi /etc/kibana/kibana.yml

#server.host: "localhost"
server.host: "0.0.0.0"
```

自動起動するように設定し、起動まで実行します。

```
# systemctl enable kibana
# systemctl start kibana
```

最後に動作確認。以下のURLをブラウザから開いて、Kibanaの画面が表示されることを確認します。

- http://[KibanaサーバーのIPアドレス]:5601



# APM Serverのインストール

サーバ側では最後の設定となるAPM Serverのインストールを実施します。

```
# curl -L -O https://artifacts.elastic.co/downloads/apm-server/apm-server-7.8.0-x86_64.rpm
# rpm -vi apm-server-7.8.0-x86_64.rpm

$ su - apm-server
$ apm-server setup --index-management
Index setup finished.
$ apm-server setup --pipelines
Loaded Ingest pipelines
```

APM Serverを外部からアクセスできるように設定します。

```
# vi /etc/apm-server/apm-server.yml
apm-server:
  # Defines the host and port the server is listening on. Use "unix:/path/to.sock" to listen on a unix domain socket.
  #host: "localhost:8200"
  host: "0.0.0.0:8200"
```

自動起動するように設定し、起動まで実行します。

```
# systemctl enable apm-server
# systemctl start apm-server
```

最後に動作確認。

```
# curl -L http://localhost:8200
{
  "build_date": "2020-06-14T17:10:16Z",
  "build_sha": "06c58bf4e5b675d04314bf44961ffd6b0e13f544",
  "version": "7.8.0"
}
```


# Java agentの設定

Java agentの設定方法は以下のサイトを参考にして実施します。

- [Java agent](https://www.elastic.co/guide/en/apm/agent/java/1.x/setup.html)

Java agentは以下の3つの方法で動かすことができます。

1. javaagentフラグを使用した手動セットアップ
2. apm-agent-attach-standalone.jarを使用した自動セットアップ
3. apm-agent-attachをアプリケーションに組み込み

Java agentのモジュールは以下からダウンロードします。

- https://search.maven.org/search?q=a:elastic-apm-agent


# javaagentフラグを使用した手動セットアップ

以下のようにjavaコマンド実行時に「-javaagent」フラグを指定する方法です。


```
java -javaagent:/path/to/elastic-apm-agent-<version>.jar -Delastic.apm.service_name=my-cool-service -Delastic.apm.application_packages=org.example,org.another.example -Delastic.apm.server_urls=http://localhost:8200 -jar my-application.jar
```

Tomcatならsetenv.shに設定したりと、少し手が入るので今回はapm-agent-attach-standalone.jarを使用した自動セットアップを試してみました。


# apm-agent-attach-standalone.jarを使用した自動セットアップ

apm-agent-attach-standalone.jarをダウンロードし、以下のように実行します。
事前にアプリケーションは起動しておきます。

apm-agent-attach-standalone.jarは以下からダウンロードします。今回ダウンロードしたのはv1.17.0です。

- https://search.maven.org/search?q=g:co.elastic.apm%20AND%20a:apm-agent-attach

```
>java -jar apm-agent-attach-1.17.0-standalone.jar --list
このコマンドでプロセスIDを調べます。

調べたプロセスIDを指定し以下を実行します。
>java -jar apm-agent-attach-1.17.0-standalone.jar --pid 4628 --config service_name=my-cool-service server_urls=http://localhost:8200
2020-07-18 08:18:46.651  INFO Attaching the Elastic APM agent to 4628
2020-07-18 08:18:50.017  INFO Done
```

[server_urls=http://localhost:8200]はAPM Serverのアドレスを指定します。


# Kibana APM UI

Java agentがアプリケーションから収集した情報はAPM Serverへ送信されます。
APM ServerはそれをElasticsearchに格納し、Kibana APM UIで可視化することができます。

まず、Kibanaのトップ画面から[APM]を選択します（画面左下）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179607/0f40c36f-a759-5143-8e15-73cba7d3e05c.png)

サービスの一覧が以下のように表示されます。
[my-cool-service]がapm-agent-attach-standalone.jarを実行した際に指定したサービス名です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179607/3502dff8-61f5-d13f-00af-928f72fe3d33.png)

[my-cool-service]を選択すると、以下のように様々な情報が表示されます。

- Time spent by span type: spanタイプごとの実行時間の割合
- Transaction duration: トランザクションの平均実行時間の推移
- Requests per minute: 1分ごとのリクエスト数の推移
- Transactions: トランザクションの一覧

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179607/891f50a9-d460-adaa-0c6f-3a4a08a1f195.png)

ここで、トランザクションの一覧から1つのトランザクションを選択します。
遷移後の画面でサンプルのトランザクションを選択すると、以下のようにSpanのタイムラインを表示することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179607/db508017-f7a9-0334-2579-f0afd9b46848.png)

作成したアプリケーションは、以下の流れになっています。

1. GETリクエストを受信。
2. 内部の別のGETを受け付けるRESTサービスをリクエスト。レスポンスを受信。
3. PostgreSQLへJDBCでSELECTを実行。

※本当は2.のRESTサービスを別サービスにしておけばよかったのですが、手抜きで同じサービス名にしています。そのため、モノリシックアーキテクチャでもトレーシングは有効だという例になっています。


既存のアプリケーションに対して、Java agentを外部からattachしただけでこれだけの情報を表示してくれます。
[SELECT FROM test]のSPANを選択すると、以下のように実行したSQLも表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179607/bcbdad4f-0a75-89d5-53c4-a04b1d063d9a.png)

このような情報が表示できるのはJava agentがフレームワークやJDBC等を自動で計測することができるからです。対応しているフレームワークやDBは以下に記載があります。

- [Supported technologies](https://www.elastic.co/guide/en/apm/agent/java/1.x/supported-technologies-details.html)


# apm-agent-attachをアプリケーションに組み込み

最初からAPMを使用することが決まってアプリケーションを開発するのであれば、apm-agent-attachをアプリケーションに組み込みのが良いと思います。

Mavenであれば、pom.xmlに以下の依存性を追記します。

```xml:pom.xml
		<dependency>
		    <groupId>co.elastic.apm</groupId>
		    <artifactId>apm-agent-attach</artifactId>
		    <version>${elastic-apm.version}</version>
		</dependency>
```

アプリケーションの起動メソッドでElasticApmAttacher.attach()メソッドを呼び出します。

```java
	public static void main(String[] args) throws Exception {
		logger.info("start ");

		ElasticApmAttacher.attach(); // ★ここ！

		org.apache.camel.spring.Main main = new org.apache.camel.spring.Main();
		main.setApplicationContextUri("META-INF/spring/camel-context.xml");
		main.run(args);
	}
```

最後に[elasticapm.properties]を以下のように作成し、クラスパスが通ったディレクトリに置きます。

```properties:elasticapm.properties
# サービス名
service_name=my-cool-service
# 計測対象のパッケージ
application_packages=example
# 接続先のAPM Serverのアドレス
server_urls=http://192.168.10.126:8200
```

# APIのPublic APIを使用して、独自のSpanを作成する。

他にも@CaptureSpanアノテーションをメソッドにつけて独自のSpanを作成することができます。

- https://www.elastic.co/guide/en/apm/agent/java/master/public-api.html#api-capture-span

まず、pom.xmlにapm-agent-apiを追加。

```xml:pom.xml
		<dependency>
		    <groupId>co.elastic.apm</groupId>
		    <artifactId>apm-agent-api</artifactId>
		    <version>${elastic-apm.version}</version>
		</dependency>
```

対象のメソッドに@CaptureSpanアノテーションを付与します。現在のSpanの子Spanとなります。

```java
    	@CaptureSpan(value = "TestSleep", type = "ext", subtype = "class-method")
    	public void sleep(int sleepMs) {
    		try {
    			 Thread.sleep(sleepMs);
    		} catch (InterruptedException e) {
    		}
    	}
```

特定のロジックに対してSpanを作成したい場合は、以下のようにstartSpanメソッドを使用します。ここではspanの名前を[TEST SPAN]に設定しています。

```java
		Span parentSpan = ElasticApm.currentSpan();
		Span span = parentSpan.startSpan("app", "java logic", "test");
		try (final Scope scope = span.activate()) {
		    span.setName("TEST SPAN");
			// ここにロジックが入る。
		} catch (Exception e) {
		    span.captureException(e);
		    throw e;
		} finally {
		    span.end();
		}
```

Kibanaで該当のトランザクションを表示すると以下のように、[TEST SPAN]が表示されるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179607/d381f61b-1eee-63ae-eb15-4f7c99b0acfa.png)



# Profiling configuration options

サンプリングプロファイラー（async-profiler）の周期に基づいて自動でメソッド単位のスパンを作成することができます。デフォルトだと全てのクラス・メソッドが対象となるため、対象を絞るように設定するのが好ましいです。
機能を有効にする場合、[profiling_inferred_spans_enabled]を"true"に設定します。なお、Windowsでは利用できません。

Linux上で本オプションを有効にした場合、以下のように可視化されました。
ここではMyCodeProcessor#processorメソッドが自動的に追加されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179607/9ab12e3e-a683-6fbb-1481-63834851ff7c.png)

[Profiling configuration options]については、以下にオプションの詳細の説明があります。

- [Profiling configuration options](https://www.elastic.co/guide/en/apm/agent/java/1.x/config-profiling.html)

以下は設定・実行例です。

```
# プロファイリング（スパンの推定）を有効にする。
JAVA_OPTS="$JAVA_OPTS -Delastic.apm.profiling_inferred_spans_enabled=true"
# スパンの推定の頻度を指定する。短いほど正確に取得できる。ただし、オーバーヘッドが大きくなる。
JAVA_OPTS="$JAVA_OPTS -Delastic.apm.profiling_inferred_spans_sampling_interval=50ms"
# スパンの推定の最小時間
JAVA_OPTS="$JAVA_OPTS -Delastic.apm.profiling_inferred_spans_min_duration=0ms"
# スパンを推定するクラスを指定する。値を設定すると推定するスパンを限定できるのでオーバーヘッドを減少できる。","区切りで指定する。
JAVA_OPTS="$JAVA_OPTS -Delastic.apm.profiling_inferred_spans_included_classes=org.example.myapp.*"
# スパンの推定から除外するクラスを指定する。","区切りで指定する。
JAVA_OPTS="$JAVA_OPTS -Delastic.apm.profiling_inferred_spans_excluded_classes=org.example.myapp2.*"

java -javaagent:/path/to/elastic-apm-agent-<version>.jar $JAVA_OPTS その他のオプション -jar my-application.jar
```


# APM Java agentによる負荷

agentはかなりのデータをAPM Serverへ送信しているだったので、負荷についてドキュメントを確認してみた。

- レイテンシー

シングルスレッドでのベンチマークでは、99%まで10マイクロ秒以下のオーバーヘッド。

- CPU

APM Serverへ送信する際にイベントをシリアル化、圧縮するなど、多少のCPU負荷はあるが、特に問題はない。

- メモリー

必要とするメモリサイズは小さく、通常はアプリケーションのヒープサイズを増やす必要はない。

- ネットワーク

agentが記録されたイベントをAPM Serverに送信するので、ネットワーク帯域はある程度必要となる。

agentはデフォルトで全てのトランザクションをサンプリングします。
そのため、Elasticsearchで必要とするストレージや計測対象のサーバの負荷を下げる目的で、サンプルレートを変更することができます。
transaction_sample_rateプロパティを0.0～1.0で調整するようです。


# アプリケーションログとの連携

アプリケーションログをElasticsearchに送信し、APMのトレースデータとログを関連付けて表示することもできます。
連携方法については以下に記事を書きました。

- [Elastic APMとアプリケーションログの連携](https://qiita.com/mkyz08/items/314ef09adcfc8a9bd073)


# 参考

APM APIを使用したアプリケーション実行時に、「No compatible attachment provider is available」というエラーが発生することがあります。
この場合、使用するJDKを変更すると動くことがあります。（OracleJDKだとエラーになるのかも）

```
[2020-07-19 06:50:03.555], [INFO ], e.Main, main, example.Main, start
Exception in thread "main" java.lang.IllegalStateException: No compatible attachment provider is available
        at co.elastic.apm.attach.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:602)
        at co.elastic.apm.attach.bytebuddy.agent.ByteBuddyAgent.attach(ByteBuddyAgent.java:273)
        at co.elastic.apm.attach.ElasticApmAttacher.attach(ElasticApmAttacher.java:159)
        at co.elastic.apm.attach.ElasticApmAttacher.attach(ElasticApmAttacher.java:113)
        at co.elastic.apm.attach.ElasticApmAttacher.attach(ElasticApmAttacher.java:70)
        at example.camelbegginer.restdsl.Main.main(Main.java:15)
```