CryostatとGraphQLによるインスタンス間のJFR管理

Red Hat で Java Platform Advocate として OpenJDK を担当している伊藤ちひろ(@chiroito)です。

この記事は、Red Hat Developerのブログ記事、Manage JFR across instances with Cryostat and GraphQL | Red Hat Developer の翻訳記事です。


https://developers.redhat.com/sites/default/files/styles/article_feature/public/2020_Java_ContainerJFR_Featured_Article__A-01.png?itok=K_g-UdJY

Cryostatは、Java Flight Recorder (JFR) を用いたJavaアプリケーションの監視をクラウド上で管理します。Cryostat 2.1では、複数のアプリケーション、コンテナ、Kubernetesポッドのフライトレコーディングを制御するためのGraphQLがサポートされており、強力な絞り込み機能を備えています。この記事では、GraphQL サポートを追加した動機について説明し、期待される結果とともにいくつかのクエリ例を共有し、GraphQL エンドポイントでの基礎となる Web リクエストを見ていきます。

なぜCryostatでGraphQLを使うのか?

Cryostatの以前のバージョンは、単純なHTTP REST APIでフライトレコーディングを公開しました。そのAPIは、各リクエストを単一の概念的なアクションに制限していました - 例えば、1つのレプリカインスタンスで1つの記録を開始します。しかし、Cryostatは、複数のスケールされたレプリカ内の多数のインスタンスを実行するマイクロサービスアーキテクチャを作成する開発者にサービスを提供し、各レプリカインスタンスは、様々な目的のために実行されている多くのフライトレコーディングを保持します。

したがって、単純なコンテナの12個のレプリカで同一の記録を開始しようとする開発者は、12回のAPIリクエストが必要でした。さらに悪いことに、10個のマイクロサービス上の10個のレプリカで同一の記録を開始しようとする開発者は、10x10=100のAPIリクエストを行う必要があります(よくある使用例です)。

明らかに、Cryostatはこれらすべての条件を管理するために、柔軟で強力なAPIを必要としています。GraphQLは、厳選されたインスタンスのグループに対してクエリを発行するための、シンプルで有能な言語です。複数のアクションを単一のAPIリクエストに減らすことで、通信量のオーバーヘッドが減少するため、全体の性能が大幅に改善されます。さらなる利点として、開発者は複雑なアクションを実行するために、APIのJSONレスポンスを解析し、レスポンスデータに対して繰り返しアクションを実行するカスタムクライアントを書くのではなく、より短くシンプルなクエリーを書けます。

ターゲットJVMとそれに属する稼働中または保管された記録、および一般的なCryostatアーカイブに存在する記録に対するクエリは、稼働中または保管された記録を開始、停止、保管、削除する命令と組み合わせて、CryostatとJDK Flight Recorder周辺の自動化を構築するための強力なクエリを作成できます。

クエリの例

Cryostatは/api/beta/graphqlエンドポイントでGraphQLで記述されたクエリを受け付けます。もしあなたがGraphQLを全く知らないのであれば、この記事の例でどの部分が単なるGraphQLの構文や概念なのか、それともCryostat特有の動作なのかをより理解するために、このドキュメントを簡単に見てみることをお勧めします。

最初の簡単なクエリを以下に示します。

query {
    targetNodes {
            name
            nodeType
            labels
            target {
                alias
                serviceUri
            }
    }
}

これは Cryostat に targetNodes の結果を求めます。これは Cryostat が認識している全ての JVM ターゲットを返すクエリです。レスポンスは、それらのオブジェクトの配列です。クエリは、namenodeType のような様々なフィールドを指定するので、配列内のオブジェクトは、それらのフィールドだけを含みます。

もう一つ、より高度なクエリを紹介します。

query {
environmentNodes(filter: { name: "my-app-pod-abcd1234" }) {
            descendantTargets {
                doStartRecording(recording: {
                    name: "myrecording",
                template: "Profiling",
                templateType: "TARGET",
                        duration: 30
                   }) {
                      name
                      state
                }
            }
    }
}

これは Cryostat に environmentNodes の結果を求めます。これは Cryostat が認識している、JVM ターゲットアプリケーションではないデプロイメントグラフ内の全てのノードを返すクエリです。Red Hat OpenShift のコンテキストでは、結果は、例えば DeploymentsDeploymentConfigs、そしてそれらに属する Pods を含むでしょう。

前のクエリでは名前ベースの絞り込みを行い、my-app-pod-abcd1234 という名前のノードのみを選択しました。この文字列が、プロジェクトの名前空間内の 1 つの Pod に判定されると仮定します。そのポッドに対して、このクエリは、入れ子になったクエリ descendantTargets を実行し、前の例のクエリと同様に、JVMターゲット・オブジェクトの配列を生成します。

それらのJVMターゲットの各々で、doStartRecordingの更新処理を実行します。その名前が示すように、この更新処理は、提供された構成を使用して、JVMターゲットのそれぞれで新しいJDKフライトレコーディングを開始するようにCryostatを仕向けます。レスポンスは、これらの記録それぞれの名前と状態(myrecordingRUNNINGであると予想される)を含んでいます。

先ほどの例は、GraphQL APIの威力と実用性を示しています。ここでは、1回のリクエストで、CryostatがOpenShiftと通信して特定のPodに属するCryostat対応JVMをすべて決定し、各JVMで記録を開始するという複雑な動作を実行できます。

最後にクエリの例を挙げます。

query {
    targetNodes(filter: { annotations: "PORT = 9093" }) {
            recordings {
                    active(filter: { labels: "mylabel = redhatdevelopers" }) {
                    doArchive {
                            name
                    }
                }
            }
    }
}

OpenShiftに慣れている人なら、ここのlabelsの仕分けは見慣れたものになるかもしれません。このラベルの選択子はOpenShiftのラベルの選択子と同じ構文で、Cryostatの記録ラベルだけでなく、デプロイメントのグラフのラベルや注釈にも適用されます。

mylabel = redhatdevelopers, env in (prod, stage), または !mylabel のような式を使用できます。また、複数のラベル選択子を配列として指定することで、論理的なAND接続を作成できます。

active(filter: { labels: ["mylabel = redhatdevelopers", "env in (prod, stage)"] })

Cryostat が特定の名前空間のデプロイメントのグラフで見ているものをよりよく理解するために、次のようなコマンドで Discover API エンドポイントを確認します。

$ curl https://my-cryostat.openshift.example.com/api/v2.1/discovery | jq

いくつかの異なる種類のフィルタがあり、それらを組み合わせて様々な入れ子になったクエリに適用できます。そのため、多数の可能な意味があるリクエストを繋ぎ合わせられます。CryostatのGraphQL queries.graphqlsとtypes.graphqlsリポジトリを参照し、実装されている全てのクエリと更新処理を確認し、どのフィルタがどの種類のフィルタを受け入れるかを学びます。この名前は自明であるべきです。FilterInputで終わるタイプはフィルタであり、doで始まるタイプフィールドは、実際には、記録を開始したり、記録を保管場所へコピーするなどのアクションを実行する入れ子となった更新処理です。

APIエンドポイントの詳細

この記事の例では、GraphQLエンドポイントの典型的なクエリのデータ本体、実行可能な動作、および期待される応答がどのようなものかを示しています。このセクションでは、これらのリクエストをどのように行うか、具体的な詳細を詳しく見ていきます。

Cryostat 2.1の主要なGraphQLエンドポイントは、POST /api/beta/graphqlです。このパスに送られるPOSTリクエストは、{ query: "the_query" }のような形式の本文が含まれます。これは、前のセクションの例で示されたように、THE_QUERYをクエリのテキストに置き換えています。以下は、リクエストとそのメタデータの具体例です。

POST /api/beta/graphql HTTP/1.1
Accept: application/json, */*;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 171
Content-Type: application/json
Host: localhost:8181
User-Agent: HTTPie/3.1.0

{
    "query": "query {\n    targetNodes {\n        name\n        nodeType\n        labels\n        target {\n            alias\n            serviceUri\n        }\n    }\n}\n"
}

GET /api/beta/graphqlのリクエストでも同じ結果が得られます。

"/api/beta/graphql?query={targetNodes{name nodeType labels target{alias serviceUri}}}"
HTTP/1.1 200 OK
content-encoding: gzip
content-length: 247
content-type: application/json

GraphQLをさらに活用

前のセクションの例では、いくつかの可能なクエリの絞り込みを探りました。あなたの環境の中でGraphQLインターフェースの力を発揮するために、次の演習を試してみてください。

  1. すべてのJVMアプリケーションを一度にスナップショットし、得られた記録をCryostatアーカイブにコピーするGraphQLクエリを作成します。
  2. cURL またはお好みの HTTP クライアントを使用して Cryostat にリクエストを送信し、クエリを実行します。
  3. このAPIリクエストを使ったスクリプトを作成し、毎日午後5時に実行するようにcrontabに追加します。
  4. このスクリプトをCryostat自動化ルールと組み合わせ、JVMアプリケーションが現れるたびに、新しい 継続的な監視記録を自動的に開始させます。

これらの部品はそれぞれ単独では単純ですが、一緒になると強力な自動化された流れが形成されます。

最後にもう一つ。古いバージョンのCryostatにおける標準APIは2.1でもまだ存在し、GraphQLに適さないアクションや機能のために、このAPIにさらに追加されたものがあります。それは、AuthorizationヘッダではなくJWTトークンを使用してJDKフライトレコーディングファイルをダウンロードするための新しいエンドポイントです。

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。