Libertyの性能情報をPythonのSidecarで取得する（mpMetrics）

- [Libertyの性能情報をPythonのSidecarで取得する（restConnector）](https://qiita.com/sotoiwa/items/15cac258ed90d7861bdf)

のJMX restConnectorではなくmpMetricsを使用した版。こちらの方が、認証を無効にでき、HTTPSでアクセスする必要もないので簡単。

## 参考リンク

- [MicroProfile メトリックを使用したモニタリング](https://www.ibm.com/support/knowledgecenter/ja/SSEQTP_liberty/com.ibm.websphere.wlp.doc/ae/rwlp_monitor_metrics_rest_api.html)
- [MicroProfile Metrics REST API](https://www.ibm.com/support/knowledgecenter/ja/SSEQTP_liberty/com.ibm.websphere.wlp.doc/ae/rwlp_mp_metrics_rest_api.html)
- [MicroProfile Metrics 1.1 ベンダー・メトリック](https://www.ibm.com/support/knowledgecenter/ja/SSEQTP_liberty/com.ibm.websphere.wlp.doc/ae/rwlp_monitor_metrics_rest_api.html)
- [New connection pool metrics (Monitor 1.0 with MicroProfile Metrics 1.1 updates)](https://www.openliberty.io/blog/2018/12/14/microprofile21-18004.html#mpMetrics)

## ローカルLibertyの準備

はじめにローカルでLibertyを動かして確認する。以下のような`server.xml`でLibertyを起動する。

`mpMetrics-1.1`フィーチャーを追加することで`/metrics`からメトリクスが取得できるようになり、`monitor-1.0`を追加することで、venderスコープの追加のメトリクスが取得できるようになる。`<mpMetrics authentication="false"/>`で認証を無効にすることができる。

```xml:server.xml
<server description="new server">

    <!-- Enable features -->
    <featureManager>
        <feature>webProfile-8.0</feature>
        <feature>monitor-1.0</feature>
        <feature>mpMetrics-1.1</feature>
    </featureManager>

    <!-- To access this server from a remote client add a host attribute to the following element, e.g. host="*" -->
    <httpEndpoint host="*" httpPort="9080" httpsPort="9443" id="defaultHttpEndpoint"/>

    <mpMetrics authentication="false"/>

    <!-- Automatically expand WAR files and EAR files -->
    <applicationManager autoExpand="true"/>

    <applicationMonitor updateTrigger="mbean"/>

    <webApplication id="sample" location="sample.war" name="sample"/>

</server>
```

## curl

`curl`でどのようなメトリクスが取得できるのかを確認する。

デフォルトではPrometheus Textで返ってくる。

```shell-session
$ curl -s http://localhost:9080/metrics
# TYPE base:classloader_total_loaded_class_count counter
# HELP base:classloader_total_loaded_class_count Java 仮想マシンが実行を開始した以降にロードされたクラスの総数を表示します。
base:classloader_total_loaded_class_count 9596
# TYPE base:cpu_system_load_average gauge
# HELP base:cpu_system_load_average 直前の 1 分間におけるシステム負荷の平均を表示します。システム負荷の平均は、使用可能なプロセッサーのキューに入れられている実行可能なエンティティーの数と、使用可能なプロセッサーで実行されている実行可能なエンティティーの数の合計の一定期間の平均値です。負荷平均を計算する方法はオペレーティング・システムに固有ですが、通常は、ダンプ時間依存の平均です。負荷平均が使用できない場合には、負の値が表示されます。この属性は、システム負荷に関するヒントを提供する目的で用意されたもので、頻繁に照会できます。このメソッドを実装するにはコストがかかる場合、一部のプラットフォームでは負荷平均を使用できない場合があります。
base:cpu_system_load_average 3.51025390625
# TYPE base:gc_ps_scavenge_time_seconds gauge
# HELP base:gc_ps_scavenge_time_seconds 概算累積コレクション経過時間 (ミリ秒) を表示します。このコレクターのコレクション経過時間が定義されていない場合、この属性は -1 を表示します。Java 仮想マシンの実装では、経過時間の測定に高解像度タイマーを使用する場合があります。コレクション経過時間が非常に短い場合、コレクション・カウントが増分されても、この属性は同じ値を表示することがあります。
base:gc_ps_scavenge_time_seconds 0.125
# TYPE base:thread_count counter
# HELP base:thread_count デーモン・スレッドと非デーモン・スレッドの両方を含めて、現在のライブ・スレッドの数を表示します。
base:thread_count 42
# TYPE base:classloader_current_loaded_class_count counter
# HELP base:classloader_current_loaded_class_count Java 仮想マシンに現在ロードされているクラスの数を表示します。
base:classloader_current_loaded_class_count 9564
# TYPE base:jvm_uptime_seconds gauge
# HELP base:jvm_uptime_seconds Java 仮想マシンの開始時刻 (ミリ秒) を表示します。この属性は、Java 仮想マシンが開始された概算時刻を表示します。
base:jvm_uptime_seconds 15.264000000000001
# TYPE base:memory_committed_heap_bytes gauge
# HELP base:memory_committed_heap_bytes Java 仮想マシンで使用するためにコミットされているメモリー量 (バイト) を表示します。このメモリー量は、Java 仮想マシンでの使用が保証されています。
base:memory_committed_heap_bytes 8.24180736E8
# TYPE base:thread_max_count counter
# HELP base:thread_max_count Java 仮想マシンが開始された以降またはピークがリセットされた以降のピーク・ライブ・スレッド・カウントを表示します。これには、デーモン・スレッドと非デーモン・スレッドが含まれます。
base:thread_max_count 50
# TYPE base:cpu_available_processors gauge
# HELP base:cpu_available_processors Java 仮想マシンが使用可能なプロセッサーの数を表示します。この値は、仮想マシンの特定の呼び出し中に変更されることがあります。
base:cpu_available_processors 8
# TYPE base:gc_ps_mark_sweep_count counter
# HELP base:gc_ps_mark_sweep_count 発生したコレクションの総数を表示します。このコレクターのコレクション・カウントが定義されていない場合、この属性は -1 をリストします。
base:gc_ps_mark_sweep_count 3
# TYPE base:thread_daemon_count counter
# HELP base:thread_daemon_count 現在のライブ・デーモン・スレッドの数を表示します。
base:thread_daemon_count 38
# TYPE base:classloader_total_unloaded_class_count counter
# HELP base:classloader_total_unloaded_class_count Java 仮想マシンが実行を開始した以降にアンロードされたクラスの総数を表示します。
base:classloader_total_unloaded_class_count 33
# TYPE base:gc_ps_scavenge_count counter
# HELP base:gc_ps_scavenge_count 発生したコレクションの総数を表示します。このコレクターのコレクション・カウントが定義されていない場合、この属性は -1 をリストします。
base:gc_ps_scavenge_count 7
# TYPE base:memory_max_heap_bytes gauge
# HELP base:memory_max_heap_bytes メモリー管理のために使用できるヒープ・メモリーの最大量 (バイト) を表示します。ヒープ・メモリーの最大サイズが定義されていない場合、この属性は -1 を表示します。このメモリー量がコミット済みメモリー量より多い場合、このメモリー量をメモリー管理のために使用できるとは保証されません。使用済みメモリー量がこの最大サイズを超えていない場合でも、Java 仮想マシンはメモリー割り振りに失敗することがあります。
base:memory_max_heap_bytes 3.817865216E9
# TYPE base:cpu_process_cpu_load_percent gauge
# HELP base:cpu_process_cpu_load_percent Java 仮想マシン・プロセスの「最近の CPU 使用量」を表示します。
base:cpu_process_cpu_load_percent 0.0
# TYPE base:memory_used_heap_bytes gauge
# HELP base:memory_used_heap_bytes 使用済みヒープ・メモリー量 (バイト) を表示します。
base:memory_used_heap_bytes 1.04625672E8
# TYPE base:gc_ps_mark_sweep_time_seconds gauge
# HELP base:gc_ps_mark_sweep_time_seconds 概算累積コレクション経過時間 (ミリ秒) を表示します。このコレクターのコレクション経過時間が定義されていない場合、この属性は -1 を表示します。Java 仮想マシンの実装では、経過時間の測定に高解像度タイマーを使用する場合があります。コレクション経過時間が非常に短い場合、コレクション・カウントが増分されても、この属性は同じ値を表示することがあります。
base:gc_ps_mark_sweep_time_seconds 0.27
# TYPE vendor:session_default_host_metrics_invalidated_total counter
# HELP vendor:session_default_host_metrics_invalidated_total このメトリックが有効になって以降にログアウトしたセッションの数。
vendor:session_default_host_metrics_invalidated_total 0
# TYPE vendor:session_default_host_metrics_invalidatedby_timeout_total counter
# HELP vendor:session_default_host_metrics_invalidatedby_timeout_total このメトリックが有効になって以降にタイムアウトによってログアウトしたセッションの数。
vendor:session_default_host_metrics_invalidatedby_timeout_total 0
# TYPE vendor:session_default_host_metrics_live_sessions gauge
# HELP vendor:session_default_host_metrics_live_sessions 現在ログインしているユーザーの数。
vendor:session_default_host_metrics_live_sessions 1
# TYPE vendor:threadpool_default_executor_active_threads gauge
# HELP vendor:threadpool_default_executor_active_threads タスクを実行しているスレッドの数。
vendor:threadpool_default_executor_active_threads 1
# TYPE vendor:session_default_host_metrics_create_total gauge
# HELP vendor:session_default_host_metrics_create_total このメトリックが有効になって以降にログインしたセッションの数。
vendor:session_default_host_metrics_create_total 1
# TYPE vendor:threadpool_default_executor_size gauge
# HELP vendor:threadpool_default_executor_size スレッド・プールのサイズ。
vendor:threadpool_default_executor_size 16
# TYPE vendor:session_default_host_metrics_active_sessions gauge
# HELP vendor:session_default_host_metrics_active_sessions 同時にアクティブなセッションの数。(あるセッションがアクティブであるとは、製品がそのユーザー・セッションを使用する要求を現在処理している場合をいいます。)
vendor:session_default_host_metrics_active_sessions 1
$
```

`Accept: application/json`ヘッダを渡すと結果がjsonで返ってくる。

```shell-session
$ curl -s -H "Accept: application/json" http://localhost:9080/metrics | jq .
{
  "vendor": {
    "servlet.com.ibm.ws.microprofile.metrics.public.PublicMetricsRESTProxyServlet.request.total": 1,
    "session.default_host_metrics.invalidated.total": 0,
    "session.default_host_metrics.invalidatedbyTimeout.total": 0,
    "session.default_host_metrics.liveSessions": 1,
    "threadpool.Default_Executor.activeThreads": 1,
    "session.default_host_metrics.create.total": 1,
    "threadpool.Default_Executor.size": 16,
    "session.default_host_metrics.activeSessions": 0,
    "servlet.com.ibm.ws.microprofile.metrics.public.PublicMetricsRESTProxyServlet.responseTime.total": 43263305
  },
  "base": {
    "classloader.totalLoadedClass.count": 9624,
    "cpu.systemLoadAverage": 3.64404296875,
    "gc.PSScavenge.time": 125,
    "thread.count": 40,
    "classloader.currentLoadedClass.count": 9592,
    "jvm.uptime": 66339,
    "memory.committedHeap": 824180736,
    "thread.max.count": 50,
    "cpu.availableProcessors": 8,
    "gc.PSMarkSweep.count": 3,
    "thread.daemon.count": 36,
    "classloader.totalUnloadedClass.count": 33,
    "gc.PSScavenge.count": 7,
    "memory.maxHeap": 3817865216,
    "cpu.processCpuLoad": 0.005175639772178392,
    "memory.usedHeap": 116114704,
    "gc.PSMarkSweep.time": 270
  }
}
$
```

`http://localhost:9080/metrics/base`のようにしてスコープを限定したり、`http://localhost:9080/metrics/base/memory.usedHeap`のようにして指定のメトリックのみを取得することもできる。

一般的に取得しそうなものは以下。名前を指定するものは、アプリケーションにアクセスが発生してそのMBeanが作成されないと取得できない。

|項目|スコープ|フィールド名|Prometheus Text|
|---|---|---|---|
|現在のヒープサイズ|base|`memory.committedHeap`|base:memory_committed_heap_bytes|
|現在のヒープ使用量|base|`memory.usedHeap`|base:memory_used_heap_bytes|
|スレッドプールサイズ|vendor|`threadpool.<スレッドプール名>.size`<br>（例）`threadpool.Default_Executor.size`|`vendor:threadpool_<スレッドプール名>_size`<br>（例）`vendor:threadpool_default_executor_size`|
|アクティブなスレッド数|vendor|`threadpool.<スレッドプール名>.activeThreads`<br>（例）`threadpool.Default_Executor.activeThreads`|`vendor:threadpool_<スレッドプール名>active_threads`<br>（例）`vendor:threadpool_default_executor_active_threads`|
|セッション数|vendor|`session.<仮想ホスト名>_<アプリケーション名>.liveSessions`<br>（例）`session.default_host_metrics.liveSessions`|`vendor:session_<仮想ホスト名>_<アプリケーション>_live_sessions`<br>（例）`vendor:session_default_host_metrics_live_sessions`|
|アクティブなセッション数|vendor|`session.<仮想ホスト名>_<アプリケーション名>.activeSessions`<br>（例）`session.default_host_metrics.activeSessions`|`vendor:session_<仮想ホスト名>_<アプリケーション>_active_sessions`<br>（例）`vendor:session_default_host_metrics_active_sessions`|
|接続プール数|vendor|`connectionpool.<JNDI名>.managedConnections`<br>（例）`connectionpool.jdbc_mydb.managedConnections`|`connectionpool_<JNDI名>_managed_connections`<br>（例）`connectionpool_jdbc_mydb_managed_connections`|
|空きプール数|vendor|`connectionpool.<JNDI名>.freeConnections`<br>（例）`connectionpool.jdbc_mydb.freeConnections`|`vendor:connectionpool_<JNDI名>_free_connections`<br>（例）`vendor:connectionpool_jdbc_mydb_free_connections`|

## Python

### コード作成

Pythonスクリプトは以下の通り作成。→[GitHub](https://github.com/sotoiwa/liberty_monitor)
セッションは取得しているが、metricsアプリケーションのものを取得しており、実際は取得したいアプリケーションを指定する必要がある。

```python:monitor_mpMetrics.py
#!/usr/bin/env python3

import argparse
import csv
import datetime
import logging
import os
import sys
import time

import requests
import urllib3


formatter = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s'
logging.basicConfig(level=logging.WARNING, format=formatter)
logger = logging.getLogger(__name__)


def request_with_retry(url, timeout, retry):
    """HTTPリクエストを行い、失敗した場合はリトライを行う。
    :param url:
    :param timeout:
    :param retry:
    :return: jsonデータ
    """

    session = requests.Session()

    # backoff_factorが1だとリトライの間のスリープ時間は[0s, 2s, 4s, 8s, ...]となる
    retries = urllib3.util.retry.Retry(total=retry, backoff_factor=1)

    session.mount('https://', requests.adapters.HTTPAdapter(max_retries=retries))
    session.mount('http://', requests.adapters.HTTPAdapter(max_retries=retries))

    headers = {'Accept': 'application/json'}

    logger.info('GET {}'.format(url))
    response = session.get(url, headers=headers, timeout=timeout)

    return response.json()


def write_header(filepath):
    """ヘッダー書き込む。
    filepathが指定され、ファイルが存在しなかった場合はファイルを作成してヘッダーを書く。
    filepathが指定され、ファイルが空の場合もヘッダーを書く。
    filepathが指定されなかった場合は標準出力にヘッダーを書く。
    :param filepath:
    :return:
    """

    fieldnames = [
        'Time',
        'Heap', 'UsedMemory',
        'PoolSize', 'ActiveThreads',
        'LiveCount', 'ActiveCount']

    # filepathが指定された場合
    if filepath:
        # ファイルが存在しない場合はファイルを作成してヘッダーを書く
        if not os.path.isfile(filepath):
            with open(filepath, 'w') as csv_file:
                writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
                writer.writeheader()
        # ファイルが存在する場合
        else:
            with open(filepath, 'r+') as csv_file:
                # ファイルが空の場合はヘッダーを書く
                if not csv_file.read():
                    writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
                    writer.writeheader()
    # filepathが指定されなかった場合は標準出力にヘッダーを書く
    else:
        writer = csv.DictWriter(sys.stdout, fieldnames=fieldnames)
        writer.writeheader()


def append_data(filepath, metrics):
    """性能情報の値を追記する。
    filepathが渡された場合はcsvに書き込み、渡されなかった場合は標準出力に書き込む。
    :param filepath:
    :param metrics:
    :return:
    """

    fieldnames = [
        'Time',
        'Heap', 'UsedMemory',
        'PoolSize', 'ActiveThreads',
        'LiveCount', 'ActiveCount']

    item = {
        'Time': datetime.datetime.now(),
        'Heap': metrics['base']['memory.committedHeap'],
        'UsedMemory': metrics['base']['memory.usedHeap'],
        'PoolSize': metrics['vendor']['threadpool.Default_Executor.size'],
        'ActiveThreads': metrics['vendor']['threadpool.Default_Executor.activeThreads'],
        'LiveCount': metrics['vendor']['session.default_host_metrics.liveSessions'],
        'ActiveCount': metrics['vendor']['session.default_host_metrics.activeSessions']
    }

    if filepath:
        # データをファイルに追記する
        with open(filepath, 'a') as csv_file:
            writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
            writer.writerow(item)
    else:
        # 標準出力に出力する
        writer = csv.DictWriter(sys.stdout, fieldnames=fieldnames)
        writer.writerow(item)


def main():

    # コマンド引数の処理
    parser = argparse.ArgumentParser()
    parser.add_argument('--host',
                        action='store',
                        type=str,
                        default='localhost',
                        help='接続先のホスト名を指定します default:localhost')
    parser.add_argument('--port',
                        action='store',
                        type=int,
                        default=9080,
                        help='接続先のポートを指定します default:9080')
    parser.add_argument('--interval',
                        action='store',
                        type=int,
                        default=60,
                        help='データの取得間隔（秒）を指定します default:60')
    parser.add_argument('--delay',
                        action='store',
                        type=int,
                        default=30,
                        help='モニターを開始するまでの待機時間（秒）を指定します default:30')
    parser.add_argument('--timeout',
                        action='store',
                        type=int,
                        default=2,
                        help='モニターリクエストのタイムアウト時間（秒）を指定します default:2')
    parser.add_argument('--retry',
                        action='store',
                        type=int,
                        default=10,
                        help='モニターのリトライ回数を指定します default:10')
    parser.add_argument('-f', '--filename',
                        action='store',
                        type=str,
                        help='出力先のファイル名を指定します default:未指定（標準出力）')
    args = parser.parse_args()
    host = args.host
    port = args.port
    timeout = args.timeout
    retry = args.retry
    filepath = args.filename

    # 待機時間
    time.sleep(args.delay)

    # ヘッダーを書く
    write_header(filepath)

    # MicroProfile MetricsのURL
    url = 'http://{}:{}/metrics'.format(host, port)

    # モニター
    while True:
        metrics = request_with_retry(url, timeout, retry)
        append_data(filepath, metrics)
        time.sleep(args.interval)


if __name__ == '__main__':
main()
```

### 実行例

```shell-session
usage: monitor_mpMetrics.py [-h] [--host HOST] [--port PORT]
                            [--interval INTERVAL] [--delay DELAY]
                            [--timeout TIMEOUT] [--retry RETRY] [-f FILENAME]

optional arguments:
  -h, --help            show this help message and exit
  --host HOST           接続先のホスト名を指定します default:localhost
  --port PORT           接続先のポートを指定します default:9080
  --interval INTERVAL   データの取得間隔（秒）を指定します default:60
  --delay DELAY         モニターを開始するまでの待機時間（秒）を指定します default:30
  --timeout TIMEOUT     モニターリクエストのタイムアウト時間（秒）を指定します default:2
  --retry RETRY         モニターのリトライ回数を指定します default:10
  -f FILENAME, --filename FILENAME
                        出力先のファイル名を指定します default:未指定（標準出力）
sotoiwa@SotonoMacBook-Pro:~/workspace/liberty_monitor/monitor_mpMetrics (master)
$ ./monitor_mpMetrics.py --interval 10 --delay 0
Time,Heap,UsedMemory,PoolSize,ActiveThreads,LiveCount,ActiveCount
2019-01-13 15:03:11.891491,592445440,233771544,16,1,0,0
2019-01-13 15:03:21.906834,592445440,235929560,16,1,0,0
2019-01-13 15:03:31.920073,592445440,236361128,16,1,0,0
2019-01-13 15:03:41.934451,592445440,236361128,16,1,0,0
```

## サイドカーとして動かす

[restConnectorの場合](https://qiita.com/sotoiwa/items/15cac258ed90d7861bdf#%E3%82%B5%E3%82%A4%E3%83%89%E3%82%AB%E3%83%BC%E3%81%A8%E3%81%97%E3%81%A6%E5%8B%95%E3%81%8B%E3%81%99)と基本的に同じはず。

### Libertyコンテナのビルド

上に記載した`server.xml`とアプリを含めてLibertyコンテナをビルドする。`mpMetrics-1.1`は`webProfile8`には含まれていないので、フィーチャーのインストールが必要。

```dockerfile:Dockerfile
FROM websphere-liberty:18.0.0.4-webProfile8
COPY --chown=1001:0 server.xml /config/
COPY --chown=1001:0 sample.war /config/apps/
RUN installUtility install --acceptLicense defaultServer
```

ファイル配置は以下。

```
liberty-mpmetrics
├── Dockerfile
├── sample.war
└── server.xml
```

ビルドする。

```shell
docker build -t sotoiwa540/liberty-mpmetrics:1.0 .
docker push sotoiwa540/liberty-mpmetrics:1.0
```

### Pythonサイドカーコンテナのビルド

Pythonのコンテナをビルドする。

```dockerfile:Dockerfile
FROM python:3-alpine

WORKDIR /usr/src/app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY monitor_mpMetrics.py ./

ENV PYTHONUNBUFFERED TRUE
ENTRYPOINT [ "python", "./monitor_mpMetrics.py" ]
```

ファイル配置は以下。

```
monitor-mpmetrics
├── Dockerfile
├── monitor_mpMetrics.py
└── requirements.txt
```

```shell
docker build -t sotoiwa540/monitor-mpmetrics:1.0 .
docker push sotoiwa540/monitor-mpmetrics:1.0
```

### Kubernetesへのデプロイ

以下のようにLiberty（with サイドカー）のマニフェストを作成。

```yaml:liberty-mpmetrics-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: liberty-mpmetrics
spec:
  selector:
    matchLabels:
      app: liberty
  replicas: 1
  template:
    metadata:
      labels:
        app: liberty
    spec:
      containers:
      - name: liberty
        image: sotoiwa540/liberty-mpmetrics:1.0
        imagePullPolicy: Always
        ports:
        - containerPort: 9080
      - name: monitor
        image: sotoiwa540/monitor-mpmetrics:1.0
        imagePullPolicy: Always
        command:
        - python
        - ./monitor_mpMetrics.py
        args:
        - --host
        - localhost
        - --port
        - "9080"
        - --interval
        - "60"
        - --timeout
        - "2"
        - --delay
        - "30"
```

デプロイする。

```shell
kubectl apply -f liberty-mpmetrics-deployment.yaml
```

### 稼働確認

LibertyのPodが稼働していることを確認。コンテナの数が2となっている。

```shell-session
$ kubectl get po
NAME                                 READY   STATUS    RESTARTS   AGE
liberty-mpmetrics-575d67dff8-9sx86   2/2     Running   0          46s
$
```

サイドカーの方のログを確認。csvが出力できた。

```shell-session
$ kubectl logs -f liberty-mpmetrics-575d67dff8-9sx86 -c monitor
Time,Heap,UsedMemory,PoolSize,ActiveThreads,LiveCount,ActiveCount
2019-01-13 07:30:35.752988,70123520,51206664,4,1,1,1
2019-01-13 07:31:35.820176,70123520,59221272,4,1,1,0
```

## TODO

取得するメトリクスを引数か環境変数で渡せるようにしたほうがよい。