OpenShift上のQuakurs APIアプリケーションのパフォーマンス検証手順

# はじめに
これから開発するアプリケーションをKubernetes/OpenShiftにデプロイした際、アプリケーションのPodでどの程度のCPU・メモリが使用されるかを見積もるにはどうすればよいでしょうか。最も確実な方法は、実環境で動作させて検証することだと思います。ただし、アプリケーションはこれから開発するため、実際には何らかのサンプルアプリケーションを作成してパフォーマンス検証を実施することになると思います。

当記事では、IBM CloudのマネージドOpenShift（Red Hat OpenShift on IBM Cloud）に、JavaフレームワークのQuarkusで作成したサンプルAPIアプリケーションをデプロイし、そのアプリケーションのパフォーマンス検証（スループット、PodのCPU・メモリ使用量の確認）を実施したときの手順を示します。

パフォーマンス検証のための負荷ツールはLocustを使用します。

# 前提環境
- IBM Cloudが使用可能であること
- Red Hat OpenShift on IBM Cloud 4.12
- Quarkus 3.2.4 Final
- Locust 2.16.1


# QuarkusでのサンプルAPIアプリケーションの作成
まず、サンプルのAPIアプリケーションをQuarkusで作成します。

QuarkusのCLIをインストールします。
[Quarkus CLIインストール手順](https://ja.quarkus.io/get-started/)

Quarkus CLIでアプリケーションの雛形を作成します。
```bash
quarkus create app openshift-performance-sample
cd openshift-performance-sample
```

`src/main/java/org/acme/GreetingResource.java`を以下のように編集します。コードの内容は以下の通りです。

- `/greeting/heavy`というエンドポイントを用意（`executeHeavyProcessing()`メソッド）
- `executeHeavyProcessing()`メソッドでは、インスタンス変数`processingTime`ナノ秒の間、ループを回す（各ループでは10ミリ秒未満のスリープを入れる）
- かかった処理時間を含むメッセージを戻り値として返す

ループを入れているのは、何も処理が無ければAPIが一瞬で終了してしまうためです。スリープだけさせるとCPU負荷も全くかからないため、指定時間だけループさせています（この部分は改善の余地あり）。

```java:GreetingResource.java
package org.acme;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.config.inject.ConfigProperty;

import java.util.Random;

@Path("/greeting")
public class GreetingResource {

    @ConfigProperty(name = "processing.time", defaultValue = "100000000")
    long processingTime;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/heavy")
    public String executeHeavyProcessing() throws Exception {
        long startTime = System.nanoTime();
        Random random = new Random();

        while (System.nanoTime() - startTime < processingTime) { // 100 ms
            Thread.sleep(random.nextInt(10));
        }

        long endTime = System.nanoTime();
        double duration = (endTime - startTime) / 1_000_000.0; // ms

        String message = "処理時間 : " + duration + " ms";
        System.out.println(message);

    	return message;
    }
}
```

上記のコードのインスタンス変数`processingTime`では、`@ConfigProperty`を用いて、値を設定ファイルや環境変数から変更可能にしています。

Quarkusの設定ファイルである`src/main/resources/application.properties`で、値を設定します（デフォルト値と同じなのであまり意味はないですが）。実際には、OpenShift上で環境変数を設定し、値を変更できるようにすることを目的としています。

```properties:application.properties
processing.time=100000000
```

では、アプリケーションの稼働を確認してみます。以下のコマンドで開発モードで起動します。

```bash
quarkus dev
```

標準でポート8080で起動するため、APIのエンドポイントである`/greeting/heavy`にcurlコマンドでアクセスしてみます。

```bash
❯ curl http://localhost:8080/greeting/heavy                                    
処理時間 : 104.354125 ms
```

正常にアクセスできることが確認できました。


# OpenShiftへのデプロイ
次に、作成したAPIアプリケーションをコンテナ化し、OpenShiftにデプロイします。

コンテナイメージを作成するため、QuarkusのExtension `container-image-docker` を追加します。

```bash
quarkus extension add 'container-image-docker'
```

コンテナイメージをビルドします。

```bash
# 通常はこちらのコマンドでOK
./mvnw clean package -Dquarkus.container-image.build=true -DskipTests=true

# M1/M2 Macなど、arm64 CPUを使用している場合は以下のコマンドを実行
# アプリケーションはビルド済みであることが前提
docker image build --platform linux/amd64 -f src/main/docker/Dockerfile.jvm .
```

作成されたイメージを確認し、イメージの repository, tag を確認します。

```bash
❯ docker images                                                                   
REPOSITORY                                                   TAG              IMAGE ID       CREATED        SIZE
imanishi/openshift-performance-sample                        1.0.0-SNAPSHOT   57b1af7ae5a6   24 hours ago   434MB
```

IBM CloudのコンテナレジストリであるIBM Cloud Container Registryに、作成したイメージをpushします。

```bash
# IBM Cloud Container Registryにpushするために、IBM Cloudにログイン
# リージョンは東京、リソースグループはDefaultを指定
ibmcloud login -a https://cloud.ibm.com -r jp-tok -g Default

# IBM Cloud Container Registryのリージョンをap-north(東京)に設定
ibmcloud cr region-set ap-north

# Container Registryにログイン
ibmcloud cr login

# Container Registryに名前空間「openshift-performance-sample」を作成
# この名前空間にイメージをpushする
ibmcloud cr namespace-add openshift-performance-sample

# docker tagコマンドで、作成したQuarkusのコンテナイメージを、Container Registryにpushできるようにタグ付け
# 「jp.icr.io/<名前空間>(先ほど作成したopenshift-performance-sample)/<イメージ名>:<タグ>」
# 「jp.icr.io」は東京リージョンのContainer Registryのホスト名
docker tag <OSユーザ名>/openshift-performance-sample:1.0.0-SNAPSHOT jp.icr.io/openshift-performance-sample/openshift-performance-sample:0.1

# Container Registryにpush
docker push jp.icr.io/openshift-performance-sample/openshift-performance-sample:0.1

# openshift-performance-sample名前空間にpushされていることを確認する
ibmcloud cr image-list --restrict openshift-performance-sample
```

次はOpenShiftへのデプロイの準備を実施します。OpenShift上に、Quarkusアプリケーションをデプロイするプロジェクト(名前空間)を作成します（ocコマンドが使えるように `oc login` が実行済みであることが前提）。

```bash
# プロジェクトを作成
oc new-project openshift-performance-sample

# 作成したプロジェクトを処理対象プロジェクトとして設定
oc project openshift-performance-sample
```

作成したプロジェクトでIBM Cloud Container Registryからイメージをpullできるよう、defaultプロジェクトからimagePullSecret(all-icr-io)をコピーします。

```bash
oc get secret all-icr-io -n default -o yaml | sed 's/default/openshift-performance-sample/g' | oc create -n openshift-performance-sample -f -
```

次に、コンテナをデプロイするためのマニフェストファイルを作成します。

Deploymentでは、PodにCPU・メモリの上限を設定するため、`resources.limits.cpu`は`500m`、`resources.limits.memory`は`256Mi`を設定します。また、APIの処理時間を環境変数`PROCESSING_TIME`として与えます。設定値はConfigMapから取得しています。なお、`@ConfigProperty`を使用した値の設定については、[Quarkus設定リファレンスガイド](https://ja.quarkus.io/guides/config-reference)も適宜参照ください。

```yaml:deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: openshift-performance-sample
spec:
  selector:
    matchLabels:
      app: openshift-performance-sample
  replicas: 1
  template:
    metadata:
      labels:
        app: openshift-performance-sample
    spec:
      containers:
        - name: openshift-performance-sample
          image: jp.icr.io/openshift-performance-sample/openshift-performance-sample:0.1
          resources:
            limits:
              memory: "256Mi"
              cpu: "500m"
          ports:
            - containerPort: 8080
          env:
            - name: PROCESSING_TIME
              valueFrom:
                configMapKeyRef:
                  name: openshift-performance-sample
                  key: processing-time
      imagePullSecrets:
        - name: all-icr-io
```

ConfigMapのマニフェストファイルです。環境変数`PROCESSING_TIME`に与える値を設定します。

```yaml:configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: openshift-performance-sample
data:
  processing-time: "500000000"
```

Serviceのマニフェストファイルです。ポート8080で公開されているQuarkusアプリを、ポート80で公開するようにします。

```yaml:service.yaml
apiVersion: v1
kind: Service
metadata:
  name: openshift-performance-sample
spec:
  selector:
    app: openshift-performance-sample
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: NodePort
```

各マニフェストファイルを`oc apply`し、さらにServiceを`oc expose`してRouteを作成し、外部からアクセスできるようにします。

```bash
oc apply -f manifest/configmap.yaml
oc apply -f manifest/deployment.yaml
oc apply -f manifest/service.yaml

oc expose svc/openshift-performance-sample
```

作成したRouteの情報を確認します。`openshift-performance-sample-xxxxxxxxx.jp-tok.containers.appdomain.cloud` のようなホスト名が取得できます。

```bash
oc get route
```

取得したホスト名に、APIエンドポイント`/greeting/heavy`を付けてcurlでアクセスします。なお、`oc expose`ではHTTPのRouteが作成されます。

```bash
❯ curl -w "\n" http://openshift-performance-sample-xxxxxxxxx.jp-tok.containers.appdomain.cloud/greeting/heavy
処理時間 : 502.130118 ms
```

ここまでで、QuarkusのAPIアプリケーションを、処理時間を500ms（0.5秒)に設定してOpenShiftにデプロイすることができました。

# Locustによる負荷
ここからはLocustを使って負荷をかけていきます。`locustfile.py`というファイルを作成し、以下のシナリオを記述します。単純に`/greeting/heavy`にアクセスして終了するというものになります。ユーザ数やホスト名はLocustの実行時に設定するため、シナリオ中には含めていません。

```python:locustfile.py
from locust import HttpUser, task

class OpenShiftPerformanceSample(HttpUser):
    @task
    def greeting_heavy(self):
        self.client.get("/greeting/heavy")
```

Locustをポート3000で起動します。`-f`オプションで上記の`locustfile.py`を指定しています。

```bash
locust --web-port 3000 -f locustfile.py 
```

`http://localhost:3000`にブラウザでアクセスすると以下のような画面が表示されます。各項目に以下の値を入力し、「Start swarming」ボタンをクリックすることで実行が開始されます。

- Number of users：負荷をかけるユーザ数
- Spawn rate：1秒あたりのユーザ増加数。「Number of users」で設定したユーザ数まで、1秒ごとにここで設定したユーザ数が増えていく
- Host：負荷をかける対象のホスト名（`http://`、または、`https://`を含めて記述）
![2023-08-15 at 23.20.24@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386149/a5f26409-41b2-d3c8-01b5-556319e8646e.png)

負荷をかけ始めると、全体のサマリーとして以下のような表形式で状況が表示されます。リクエスト総数、エラー数、レスポンスタイム、スループット(RPS)などが表示されます。
![2023-08-15 at 23.21.18@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386149/50d91c27-7c6b-0d91-001c-f693d27ed607.png)

「Charts」タブを選択すると、スループットやレスポンスタイムの状況がグラフとして表示されます。
![2023-08-15 at 23.22.45@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386149/740c1aa5-f97d-9b4b-c4ec-eb67e49b8169.png)


# OpenShiftでのパフォーマンスモニタリング
それでは最後に、Locustで負荷をかけた状態で、Podがどの程度のCPU・メモリを使用しているかを確認します。

OpenShiftのWebコンソールで、Developer Pespective（以下のスクリーンショットでは「開発者向け表示」）を選択し、「モニタリング」をクリックすると、CPU・メモリなどのリソース状況を確認することができます。

「ダッシュボード」で「Kubernetes / Compute Resources / Workload」を選択し、デプロイしたDeploymentを選択します。
![2023-08-18 at 00.16.53@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386149/da132d91-a40b-7279-d5dd-4b028deb96ae.png)

以下ではDeploymentと紐づくPodのCPUの使用状況が表示されています。`500m`(0.5)の上限を設定しましたが、実際に使用されているのは0.034程度で、上限に対して10%未満であることが分かります。
![2023-08-18 at 00.17.09@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386149/50ccc516-b4d5-d3f7-b0a2-11adaa29bb30.png)

メモリの使用状況も同様に表示されています。上限`256Mi`に対して、100MB程度（40%）が使用されていることが分かります。
![2023-08-18 at 00.17.21@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386149/d0d1de15-6280-8bae-f721-ce0c664d0eb6.png)

今回は単純なサンプルAPIアプリケーションに対して単純な負荷をかけましたが、サンプルAPIの処理を変えたり、Podのレプリカ数を変えたり、Locustのユーザ数などを変えることで、様々なケースの負荷を試すことができます。