ECS Service Connect はヘルスチェックの結果を元にトラフィックをルーティングしないので、起動に時間がかかるアプリケーションでは注意が必要です
ECS Service Connect はシンプルかつ高機能なサービス間通信を実現するための選択肢です。
Internal ALB を挟むことによるネットワークレイテンシが気になるものの、サービスメッシュを管理したくも無いといった場合などに特に有用です。
Amazon ECS Service Connect によるサービス間通信の管理
サービスメッシュのメリットを享受しつつ、Istio や App Mesh などの追加コンポーネントの管理不要で利用することができます。
Amazon ECS Service Connect によるサービス間通信の管理
一方で Internal ALB や Istio などと全く同じ機能が使えるわけでは無いので、その辺りはきちんと確認した上で扱う必要があります。
今回は containers-roadmap リポジトリでも issue として取り上げられている、アプリケーションのヘルスチェックとトラフィックルーティングについて確認してみます。
例えば ALB ではヘルスチェックが通っているターゲットと通っていないターゲットがあった場合に、ヘルスチェックが通っているターゲットのみにリクエストを送信します。
一方で ECS Service Connect はヘルスチェックの結果を元にリクエストをルーティングする機能がありません。
ECS の機能として Docker ヘルスチェックは存在するので、コンテナ起動後に動作していない場合はタスクを再作成することが可能です。
ただし、アプリケーションの起動に時間がかかるアプリケーションを利用した際、スケールアップ時に準備ができていないコンテナにリクエストを送ってしまう可能性があります。
また、現状リトライ回数が 2 で固定されているため、リトライ回数を増やして影響を緩和することもし辛いです。
試してみる
この問題を下図のような構成で試してみます。
アプリケーションとしては Spring Boot のアプリケーションを用います(大体 30 秒程度起動にかかります)。
別のコンテナに ECS Exec で乗り込み、 1 秒ごとにひたすら HTTP リクエストを送るスクリプトを実行します。
#!/bin/sh
while true; do
curl "http://app.local"
echo ""
sleep 1
done
スクリプトを実行した直後に ECS サービスのタスク数を 1 から 8 に増やします。
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
Hello, World!
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
Hello, World!
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
Hello, World!
Hello, World!
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
Hello, World!
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
Hello, World!
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
Hello, World!
Hello, World!
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
Hello, World!
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
Hello, World!
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
Hello, World!
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
途中から upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: 111
というエラーが発生して、その後解消しています。
こちらは、コンテナ自体は立ち上がっているのにサーバのプロセスが Listen できていない場合に発生するエラーになります。
コンテナは立ち上がったもののアプリケーション側の準備が整っていないコンテナが増えた結果、一時的にエラーが増え、アプリケーション側の準備が整ったことで解消したと考えられます。
ちなみに、タスク数を 1 から 2 に上げた場合はリクエストが失敗することはありませんでした。
Service Connect のリトライが効く上に、リトライ時は失敗したホストに接続しにいくことは無いためです。
Service Connect は、プロキシを通過して失敗した接続を再試行するようにプロキシを設定し、2 回目の試行では、前の接続のホストを使用しません。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/service-connect-concepts-deploy.html#service-connect-concepts-proxy
自動で 2 回までリトライしてくれることを考慮すると、同時に 3 個以上タスクが増えなければリトライで捌くことができるということになります。
タスク数を 1 から 8 に増やした際、初めてエラーが出てから最後のエラーが解消するまでに、エラーとなったリクエストは 19 回ありました。
その間に正常なリクエストは 11 個存在しました。
そのため、実際のエラー率は下記になります。
19 ÷ 30 ≒ 0.633
リトライを含めて計 3 回のリクエストのどれかが正常であれば、全体としては正常扱いになります。
8 個の全ホストから 3 個のホストを選ぶ組み合わせは 56 通りです。
また 7 個のリクエストを返せないホストから 3 個のホストを選ぶ組み合わせは 35 通りになります。
このことから考えると、3 回のリクエストしても異常なホストしか選択されない確率は下記になります。
35 ÷ 56 = 0.625
雑な計算にはなりますが、想定している通りの挙動になっていそうですね。
このことが問題になる場合、激しくスケールアウトさせないように設計したり、アプリケーション起動時間を短くするというのがまず対策として考えられます。
issue 内で紹介されている workaround
一方で issue を確認すると、余分なコンテナを追加することで対策することができると記載されているのでやってみます。
As a way to prevent this from occurring, an additional container can be added to the task definition with a dependency on the application container to be marked as HEALTHY (this means that there must be a health check defined for the application container). The container should be marked non-essential and designed to exit.
https://github.com/aws/containers-roadmap/issues/2334#issuecomment-2285926075
まず、起動が遅いアプリケーションコンテナにヘルスチェックを設定します。
こちらのコメントで記載されている通り、コンテナを追加します。
{
"name":"serviceconnecthold",
"image":"public.ecr.aws/docker/library/alpine:edge",
"cpu":0,
"portMappings":[],
"essential":false,
"environment":[],
"environmentFiles":[],
"mountPoints":[],
"volumesFrom":[],
"dependsOn":[
{
"containerName":"<---NAME OF THE MAIN CONTAINER--->",
"condition":"HEALTHY"
}
],
"systemControls":[]
}
アプリケーションコンテナのコンテナヘルスチェックが HEALTHY にならないと起動しないようにするのが肝ですね。
ヘルスチェックが通ると、余分なコンテナも起動して exit します。
余分なコンテナが起動すると全てのコンテナの準備が整ったと判断され、service connect エージェントが Running になり、タスクとしても「アクティブ化中」になります。
この状態であれば、先程のエラーを発生させずにタスク数を 1 から 8 にスケールアウトできました!
最後に
スケーリング設定の見直しやアプリケーション側の対応で解消できると良いですが、余分なコンテナを追加する方法が役に立つ時があるかもしれません。
また、ECS Service Connect のアップデートも定期的に実施されているので、こちらにも注目していきたいです。