OpenShiftに構築するアプリケーションの負荷分散設定の備忘録

# はじめに

OpenShift上に構築した独自アプリケーションである HULFT の通信経路の負荷分散について、必要な情報がどこにあるのかいろいろなページを参照したのでここにまとめておきます。

# 結論

- Route に対してTLSパススルー設定を行っている場合はデフォルト動作が接続元IPによる振り分けなので、他の振り分け方式に変更する必要がある
- Service を経由する通信は HTTP Keep-Alive の対象となるので、HTTPリクエストを振り分けたい場合、Keep-Alive を無効化して毎回TCPセッションを切断する必要がある

# 環境

動作検証環境は Red Hat OpenShift Container Platform のバージョン 4.16。
導入する独自アプリケーションは自社製品の HULFT。構成イメージは以下。

![構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2997186/212261fa-65d3-4935-ab2c-8ae3e00e1ddf.png)

:::note
図では Route からの通信が Service を経由するようにも見えますが、実際の通信は Service を経由せず、Route からコンテナに対し直接接続します。
:::

各通信はコンテナ終端でTLSによる暗号化が行われており、間の Route にはTLSパススルーの設定を行っている。

# 解決したかった課題

コンテナ版の HULFT を OpenShift 上に構築した当初、以下の二つの課題がありました。

- 外部から Route 経由で複数起動している転送コンテナに対して接続を行った際に、すべて同じコンテナに接続されてしまう
- 管理コンテナから Service 経由で複数起動している転送コンテナに対してAPI発行を行った際に、リクエストが全て同じコンテナで処理されてしまう

HULFT はファイル転送ミドルウェアのため、転送コンテナによる 対向HULFT との通信処理は負荷が高い部分です。コンテナ化によりファイル転送部分をスケーリングして起動できるようにしたのに、一部のコンテナに集中してしまうのはスペックを生かすことができなくなる問題でした。

# Route設定の見直し（外部HULFT-転送コンテナ間の接続）

Route の負荷分散設定については、アノテーションに HAProxy の設定を行うことで対応が可能と説明しているナレッジがありました。

https://access.redhat.com/solutions/6987368

Route のアノテーション設定については[こちらのページ](https://docs.redhat.com/en/documentation/openshift_container_platform/4.16/html/networking/configuring-routes#nw-route-specific-annotations_route-configuration)に記載されています。

|アノテーション|説明|
|-|-|
|haproxy.router.openshift.io/balance|Sets the load-balancing algorithm. Available options are random, source, roundrobin[1], and leastconn. The default value is source for TLS passthrough routes. For all other routes, the default is random.|
|haproxy.router.openshift.io/disable_cookies | Disables the use of cookies to track related connections. If set to 'true' or 'TRUE', the balance algorithm is used to choose which back-end serves connections for each incoming HTTP request.|

この説明より、`haproxy.router.openshift.io/balance` のデフォルト値はTLSパススルーの設定に依存し、以下であることがわかります。

|TLSパススルー|設定値|
|-|-|
|有効 |source
|無効 |random

前述のように、今回作成した Route では `termination.termination` を `passthrough` にしていたため、`source` が使用されていました。
これにより、転送コンテナへの接続元が一つの場合、一つの転送コンテナへ接続が集中してしまっていました。

:::note
`haproxy.router.openshift.io/balance` の設定値に `random` が記載されているのが Ver.4.8 以降のドキュメントのため、日本語で出てくる Ver.3 のドキュメントなどには記載されていないためご注意ください。
:::

## 対処方針

以下二つの対応を行いました。

- `haproxy.router.openshift.io/balance` に `leastconn` を設定
- `haproxy.router.openshift.io/disable_cookies` に `TRUE` を設定

転送コンテナは、転送を行うファイルのサイズに応じて処理にかかる時間が変わります。
そのため、分散はランダムではなくセッション数が少ないコンテナを利用する `leastconn` を選択しました。
これにより、10分かかるファイル転送と10秒で終わるファイル転送があった場合に、効率的に振り分けられることを期待しています。

HULFT10 for Container Platform では全部で3つの Route がありますが、転送コンテナが利用する二つについては上述の理由で `leastconn` とし、管理コンテナが利用するものについては `random` としました。
これは管理コンテナに対する通信は WebAPIリクエストであり処理に時間がかかるものではないため、 `leastconn` がふさわしくないためです。 `roundrobin` の採用も候補に挙がりましたが、`random` が後から追加された賢いアルゴリズムだと判断し、こちらを採用しました。

`random` のアルゴリズムについては以下の解説を参考にしました。
https://rheb.hatenablog.com/entry/haproxy-default-random

### leastconnの動作について

`leastconn`の動作について、設定反映後の検証で想定と違った部分がありました。

検証では3つのコンテナに対し、時間がかかる転送2つと、時間がかからない転送2つを実行しました。

動作検証前の想定では、セッション数を同じにするようにふるまうため、以下のようにした場合、時間がかかる転送それぞれにコンテナが一台ずつ割り当てられ、残りの転送を一つのコンテナでさばく想定でした。
（太い矢印が時間のかかる転送、短い矢印が時間のかからない転送を表しています）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2997186/a33497cf-9b66-4b82-8842-2ca19c67273e.png)

ですが、実際には時間がかかる転送二つとも同じコンテナに接続する動作となってしまいました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2997186/0f349975-79bb-4436-930c-c1bc2914c659.png)

転送の数を増やして確認したところ、以下のように２セッション以上は増えなかったため、`leastconn` で振り分け時に考慮されるのは2接続以上ある場合なのだろうと結論づけました。（細い矢印は同時接続ではなく、前の接続が終わってから接続しています）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2997186/1e95c625-2c48-49ed-bc77-d0c32a482aaf.png)

[HAProxyのドキュメント](https://www.haproxy.com/documentation/haproxy-configuration-manual/latest/)における `leastconn`の説明を見ると以下のように記載されています。

> The server with the lowest number of connections receives the connection. Round-robin is performed within groups of servers of the same load to ensure that all servers will be used. 
...中略...
It will also consider the number of queued connections in addition to the established ones in order to minimize queuing.

同じ負荷の場合はラウンドロビンにより接続先を決定すること、および負荷については接続数だけではなく次の接続を管理するキュー処理の部分も考慮するとなっています。

# Service 経由設定の見直し（管理コンテナ-転送コンテナ間の接続）

Service は L4 のロードバランサーと同じような動作をします。

振り分け動作について、公式ナレッジにて言及されています。
https://access.redhat.com/articles/7019068

振り分け動作は基本的にはランダムであり、Session-affinity を利用することにより、スティッキーセッションのような、接続元に応じて同じ Pod に振り分ける必要があるものに対応することができます。
デフォルトではこの動作は無効になっているので、今回の問題の原因ではありませんでした。

もう一つ影響を与えている要素として、ナレッジにて記載されている HTTP Keep-Alive があります。
今回は、これが影響を与えていると目星をつけました。

> Keep alive will impact the requests as long as the requests come from the same connection. The connection would stay open if the idle time between requests does not exceed the keep-alive timeout.

## 対処方針

HTTP Keep-Alive はサーバー側とクライアント側の両方で対応することで、有効になります。基本的にはデフォルトで有効になっているかと思います。
今回は、クライアント側の管理コンテナ内部のHTTPクライアントの設定で Keep-Alive を無効化しました。

管理コンテナでは golang を利用しているため、Transportの設定から無効化することができました。

```golang
type Transport struct {
    /// ... 中略 ...

	// DisableKeepAlives, if true, disables HTTP keep-alives and
	// will only use the connection to the server for a single
	// HTTP request.
	//
	// This is unrelated to the similarly named TCP keep-alives.
	DisableKeepAlives bool

   /// ... 中略 ...
}
```
https://pkg.go.dev/net/http#Transport