Keycloak起動時におけるオフラインセッションロード時間のチューニングについて

#はじめに
株式会社 日立製作所の岡井倫人です。

Keycloakとはオープンソースのアイデンティティ・アクセス管理ソフトウェアです。詳しくは（[Keycloakで実現するAPIセキュリティ](https://thinkit.co.jp/series/9721)）を参照ください。

Keycloakでは、リフレッシュトークンに紐づいたセッション情報はメモリで管理されているため、クラスタ停止時等にセッションと一緒にリフレッシュトークンも無効になります。数週間・数カ月等の期限が長いリフレッシュトークンを扱う場合、このような振る舞いが望ましくないことがあり、オフライントークンと呼ばれるDBに永続化されるリフレッシュトークンが用いられることがあります。オフライントークンの詳細は（[Keycloakのオフライントークンについて](https://qiita.com/yuichi-nakamura/items/ddce7e125d3ee04c595c)）を参照ください。現在、オフライントークンを使用した大規模環境で、Keycloakを全台停止後に再起動した場合、Keycloakの起動時間が長いとコミュティで報告されています。例えば（[Keycloak開発者のML](https://groups.google.com/g/keycloak-dev/c/ujWCcOE-GyI)）では10万人ユーザの環境で4分、（[JIRAチケット](https://issues.redhat.com/browse/KEYCLOAK-11019)）では52万8千人ユーザの環境で24分かかると報告されています。

このように多くの方が報告していることから、オフライントークンを使用した大規模環境でKeycloakの起動時間を短縮することは重要であると言えます。そこで、今回Keycloakの起動時間を短縮するために、色々な方法を試してみようと思います。

#まずはチューニングしてみる
まずはKeycloakをチューニングしてみます。 オフライントークンを使用している場合、KeycloakはオフラインセッションをDBに格納します。オフラインセッションに関しては（[Keycloakのセッションについて](https://qiita.com/yuichi-nakamura/items/0707654d1b17761cb0eb)）を参照ください。Keycloakは起動時にDBから全てのオフラインセッションをロード（以降、プレロード）します。これにより、ログインするユーザ数が多ければ多いほどプレロードに時間がかかり、結果としてKeycloakの起動時間が長くなります。そこで、プレロードに関係するパラメータをチューニングして、プレロードの実行時間、つまりKeycloakの起動時間を短縮してみます。

##測定環境
今回はAWS環境でKeycloakの起動時間を測定します。図 1のような構成を用意します。

![図1測定環境の構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/756740/95b13c66-df82-292c-7201-801a33ecbd44.png "図 1 測定環境の構成")
　　　　　　　　　　　　　　　　　　　　<b>図 1 測定環境の構成

またDBに永続化されているセッション数は十分に大きい値として1000万人ユーザ環境とします。Keycloakの起動に10万人ユーザ環境で4分（[Keycloak開発者のML](https://groups.google.com/g/keycloak-dev/c/ujWCcOE-GyI)）や52万8千人ユーザ環境で24分（[JIRAチケット](https://issues.redhat.com/browse/KEYCLOAK-11019)）かかるという報告から1000万ユーザ環境の場合、およそ400分以上かかることが予想されます。

##パラメータチューニング
プレロードの実行時間は、一度の処理でDBから読み込めるパラメータであるsessionsPerSegmentを大きくすることで、短縮できます（[参考資料](https://access.redhat.com/solutions/4678231)）。しかし、sessionsPerSegmentを大きくしすぎると、RDSやEC2のメモリやCPUの使用率が飽和してしまい、結果としてプレロードの実行時間が長くなってしまいます。そのため、これらが飽和しないようにしつつ、sessionsPerSegmentを大きくしていきます。パラメータチューニングの結果を表 1に示します。

　　<b>表 1 パラメータチューニングの結果

| \# | 項目 | 値 |
| --- | --- | --- |
|1|RDS<br>インスタンスタイプ | db.m5.2xlarge |
|2 | EC2<br>インスタンスタイプ | m4.2xlarge |
|3 | PostgreSQL<br>work_memパラメータ | 512MB |
|4 | Keycloak<br>ヒープサイズ | 48GB |
|5 | Keycloak<br>sessionsPerSegment | 22,528 |

また、表 1の値を用いた際のKeycloakの起動時間を表 2に示します。

　<b>表 2 Keycloakの起動時間

| 平均(min) | 分散(min^2) |
| --- | --- |
|12.3985 | 0.00710861 |
　　　※測定回数は5回

パラメータチューニングしたところ、Keycloakの起動時間は約12分となりました。予想していた400分よりはるかに早い起動時間となりました。この時点でかなり早くなっていますが、もっと早くしたいところです。

#offlien_user_sessionテーブルにインデックスを追加してみる
プレロードの実行対象となっているoffline_user_sessionテーブルにインデックスを追加することで、Keycloakの起動時間を早くできるか確認してみます。
offline_user_sessionテーブルに対してプレロードで実行されるSQL文は以下となります。

```
select sess from offline_user_session sess where sess.offline_flag = :offline AND (sess.created_on > :lastCreatedOn OR (sess.created_on = :lastCreatedOn AND sess.user_session_id > :lastSessionId)) order by sess.created_on,sess.user_session_id;
```



検索条件とソートから、以下のインデックスを追加することで、Keycloakの起動時間を早くすることができると考えられます。

```
CREATE INDEX idx_offline_uss_test ON offline_user_session(offline_flag, created_on, user_session_id);
```

まずは、インデックスを追加しない場合と追加した場合で実行計画を取得してみます。それぞれの実行計画は以下の通りです。

<b>インデックスを追加しない場合の実行計画

```
Gather Merge  (cost=1903127.91..2778509.63 rows=7502748 width=487) (actual time=24852.813..44444.826 rows=9003900 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   ->  Sort  (cost=1902127.89..1911506.32 rows=3751374 width=487) (actual time=23897.296..26650.257 rows=3001300 loops=3)
         Sort Key: created_on, user_session_id
         Sort Method: external merge  Disk: 1428424kB
         Worker 0:  Sort Method: external merge  Disk: 1428424kB
         Worker 1:  Sort Method: external merge  Disk: 1574688kB
         ->  Parallel Seq Scan on offline_user_session sess  (cost=0.00..671883.33 rows=3751374 width=487) (actual time=0.019..1735.980 rows=3001300 loops=3)
               Filter: (((offline_flag)::text = '1'::text) AND ((created_on > 1605831648) OR ((created_on = 1605831648) AND ((user_session_id)::text > '19786d20-c95d-411b-bf81-2ebadd8e6591'::text))))
               Rows Removed by Filter: 332033
Planning Time: 0.148 ms
Execution Time: 45014.271 ms
```

<b>インデックスを追加した場合の実行計画

```
Index Scan using idx_offline_uss_test on offline_user_session sess  (cost=0.56..1942396.12 rows=9003297 width=487) (actual time=810.193..12058.299 rows=9003900 loops=1)
   Index Cond: ((offline_flag)::text = '1'::text)
   Filter: ((created_on > 1605831648) OR ((created_on = 1605831648) AND ((user_session_id)::text > '19786d20-c95d-411b-bf81-2ebadd8e6591'::text)))
   Rows Removed by Filter: 996100
Planning Time: 0.200 ms
Execution Time: 12546.873 ms
```

インデックスを追加することで最上位ノードがGather MergeからIndex Scanとなり、追加したインデックスがSQL文の実行に使用されていることがわかります。さらにインデックスを追加しない場合と比較して、SQL文のcostも下がり、実行時間も大幅に短くなりました。
次にKeycloakの起動時間を測定してみます。結果を表 3に示します。比較対象として表 2の結果も合わせて記載しています。


　　<b>表 3 offline_user_sessionテーブルにインデックスを
　　　　　　　追加したKeycloakの起動時間

| | 平均(min) | 分散(min^2) |
| --- | --- | --- |
| offline_user_sessionテーブルに<br>インデックスを追加した場合 |4.94947|0.00428759|
|offline_user_sessionテーブルに<br>インデックスを追加しない場合 |12.3985|0.00710861|
　　　　　　　　　　※測定回数は5回

offline_user_sessionテーブルにインデックスを追加することでKeycloakの起動時間を7分以上短縮し、約2.5倍早くすることができました。

#offlien_client_sessionテーブルにインデックスを追加してみる
プレロードの実行対象となっているoffline_client_sessionテーブルにもインデックスを追加して、Keycloakの起動時間を測定してみます。
offline_client_sessionテーブルに対してプレロードで実行されるSQL文は以下となります。

```
select sess from offline_client_session sess where sess.offline_flag = :offline and sess.user_session_id IN (:userSessionIds) order by sess.user_session_id;
```

検索条件とソートから、以下のインデックスを追加することで、Keycloakの起動時間を早くすることができると考えられます。

```
CREATE INDEX idx_offline_cl_test ON offline_client_session(offline_flag, user_session_id);
```

まずは、インデックスを追加しない場合と追加した場合で実行計画を取得します。それぞれの実行計画は以下の通りです。

<b>インデックスを追加しない場合の実行計画

```
Index Scan using idx_us_sess_id_on_cl_sess on offline_client_session sess  (cost=0.56..85.80 rows=10 width=326) (actual time=0.026..0.108 rows=10 loops=1)
   Index Cond: ((user_session_id)::text = ANY ('{4bda2e42-3872-4f32-bd4b-fe819bf8b96f,2b2d0b2e-b6db-4a5f-9a0b-932cc2cb4225,f53792b0-0cd7-4202-837e-d1ea9a690f09,31ac1ad2-9cb7-4369-82de-125c5f8f658f,026d3a68-9db6-4fff-9397-52ef53dbf378,3f9dee85-03df-4c87-9b23-ab906efaed7d,e254dd41-51dd-4de1-8137-d7aba5083edc,b52c6543-ff2b-4aff-bb40-5fe608675263,26277e9d-7e01-4879-894a-45a50229bec3,d63bd948-796f-47f9-a250-3e01a6af2417}'::text[]))
   Filter: ((offline_flag)::text = '1'::text)
Planning Time: 0.127 ms
Execution Time: 0.121 ms
```

<b>インデックスを追加した場合の実行計画

```
Sort  (cost=65.72..65.74 rows=10 width=326) (actual time=0.196..0.197 rows=10 loops=1)
   Sort Key: user_session_id
   Sort Method: quicksort  Memory: 30kB
   ->  Index Scan using idx_offline_cl_test on offline_client_session sess  (cost=0.56..65.55 rows=10 width=326) (actual time=0.041..0.188 rows=10 loops=1)
      Index Cond: (((offline_flag)::text = '1'::text) AND ((user_session_id)::text = ANY ('{4bda2e42-3872-4f32-bd4b-fe819bf8b96f,2b2d0b2e-b6db-4a5f-9a0b-932cc2cb4225,f53792b0-0cd7-4202-837e-d1ea9a690f09,31ac1ad2-9cb7-4369-82de-125c5f8f658f,026d3a68-9db6-4fff-9397-52ef53dbf378,3f9dee85-03df-4c87-9b23-ab906efaed7d,e254dd41-51dd-4de1-8137-d7aba5083edc,b52c6543-ff2b-4aff-bb40-5fe608675263,26277e9d-7e01-4879-894a-45a50229bec3,d63bd948-796f-47f9-a250-3e01a6af2417}'::text[])))
Planning Time: 0.183 ms
Execution Time: 0.213 ms
```

インデックスを追加することで、SQL文の実行に使用されるインデックスは既存のuser_session_idのインデックスから追加したインデックスに変わりました。しかし、追加しない場合と比較して、SQL文のcostは下がりましたが、実行時間は長くなってしまいました。
次にKeycloakの起動時間を測定してみます。結果を表 4に示します。こちらの結果はoffline_user_sessionテーブルに先ほど紹介したインデックスを追加している状態での結果となりますので、比較対象として表 3の結果も合わせて記載しています。

<b> 表 4 offline_user_sessionテーブルとoffline_client_sessionテーブルに
　　　　　インデックスを追加したKeycloakの起動時間

| |平均(min)|分散(min^2)|
| --- | --- | --- |
|offline_client_sessionテーブルに<br>インデックスを追加した場合|5.17027|0.0837679|
|offline_client_sessionテーブルに<br>インデックスを追加しない場合|4.94947|0.00428759|
　　　　　　　　　　※測定回数は5回

offline_client_sessionテーブルにインデックスを追加してもKeycloakの起動時間は早くなりませんでした。

#おわりに
今回、Keycloakをチューニングして、offline_user_sessionテーブルに一つのインデックスを追加することで、大規模環境におけるKeycloakの起動時間を大幅に短縮できました。こちらはKeycloakの性能に大きな影響を及ぼしますので、コミュニティにコントリビュートしていきたいと思っています。