🩺

ヘルスチェックは正常なのに ECS のサービス起動で失敗する?

に公開

はじめに

Amazon ECSで外部からHTTP通信を受け付ける場合、サービスとセットでALB(もしくはNLB)を構成する必要がある。

ALBの主な役割は負荷分散だが、ヘルスチェックによるコンテナの死活監視も重要である。
重要であるというか、このヘルスチェックでけっこうハマった

具体的には、ヘルスチェックは正常応答しているように見えるのに、なぜかECSのサービス起動で失敗するのだ。

この記事では、ヘルスチェックの概要と、なぜサービス起動に失敗したのか、その原因と解決方法について解説する。

ヘルスチェックとは

ヘルスチェックとは、その名の通り「サービスが正常に動作しているのか監視・確認すること」である。

例えば、既にサービスが稼働しており、アプリを変更してデプロイする場合を考えてみる。

ECSは新規タスクを作成し、ALBが新規タスクへのヘルスチェックを行う。

ヘルスチェック成功

ヘルスチェックが成功したら、新規タスクは「正常」とみなされて外部からのリクエストが送られてくるようになる。その後、古いタスクは破棄される。

ヘルスチェック失敗

ヘルスチェックが失敗したら、新規タスクは「非正常」とみなされて破棄される。
古いタスクは存続し、外部からのリクエストが送られ続ける。


つまり、ECSはヘルスチェックの結果を元にタスクを管理しているわけだ。

ヘルスチェックは正常なのにサービスが起動しない?

ここから、私が実際に遭遇した事象を紹介しよう。

コンテナにJava(SpringBoot)のアプリケーションを載せて、Webサービスを構築していたときのことである。

開発当初は問題なくタスクが起動していたのに、たまにタスクが起動できず何度かタスク作成→破棄が繰り返されるようになった。
ALBを見ているとヘルスチェックで失敗している。ふむふむ。ヘルスチェックでエラーということは、アプリ側でエラーを吐いているはずだな。

しかし、アプリが出力するアクセスログを見ると、ヘルスチェックに対して正常応答しているではないか。

ヘルスチェックの正常応答ログは出ているのに、なぜALBでは失敗とみなされるのだろうか?
まあ、最近、開発が進むにつれて、アプリの起動は少しずつ遅くなっているけど・・・


結論として、アプリの起動が遅いことが原因だった。
Javaは他の言語と比べても起動に時間がかかる。機能追加により、起動完了までの時間が徐々に伸びていたのだ。

先ほど述べたように、ECSでタスクが新規に作成されると、ALBがヘルスチェックを送信する。
しかしヘルスチェックは、アプリの起動完了を待つわけでは無い。ALB側の準備ができたらすぐにヘルスチェックを始めてしまう。

アプリの起動が遅い場合は、ヘルスチェックを少し待ってほしいわけだ。こういうときは、「ヘルスチェックの猶予期間」を設定すればよい。

ヘルスチェックの猶予期間

ヘルスチェックの猶予期間は、サービスに対して設定するものである。この猶予期間中はヘルスチェックの結果が無視される。
ちなみに、最大で2,147,483,647秒(約69年)まで指定可能だ。(そんなに?)

アプリの起動には60秒くらいかかっていたので、余裕をみて猶予期間には90秒を設定する。

よしよし、これで解決するはずだ・・・


解決しなかった。やはりヘルスチェックで失敗しているとみなされて、タスクが落ちる。

いや、待てよ。そもそも、「なぜかアクセスログには正常応答が記録されている」件が解決していないじゃないか。うーん、どういうことだ?

続・ヘルスチェックは正常なのにサービスが起動しない?

結論として、「猶予期間」と「正常/非正常のしきい値」と「ターゲットヘルスステータス」の関係が理解できていなかった。

ヘルスチェックの猶予期間は、先ほど書いたように猶予期間中のヘルスチェック結果を無視する。

ここでポイントなのは、結果を無視するだけでヘルスチェック自体は行われているということだ。

つまり、ヘルスチェックが失敗した場合、猶予期間中であってもターゲットは「非正常」と見なされる。

「非正常であるという結果」を無視するので、これでタスクが停止するわけでは無い。
ただし、猶予期間が終わった後に、依然として「非正常」であればECSはタスクを停止してしまう。

逆にいうと、猶予期間中に「非正常」→「正常」に復活する必要がある、ということだ。

「非正常」から「正常」になるまで

では、「非正常」から「正常」になるまでにどのくらいかかるのだろう?

まず、ターゲットヘルスステータスについて見てみる。

ターゲットヘルスステータス

ヘルスチェックが行われると、ALBはターゲットのヘルスステータスを更新する。
今回大事なステータスは以下3つだ。

  • initial:初期状態。ターゲットを登録中または最初のヘルスチェックを実行中
  • healthy:ターゲットが正常である
  • unhealthy:ターゲットが非正常である(ヘルスチェックで不合格)

最初はinitialから始まる。その状態で、1つヘルスチェックが正常であれば、healthyになる。
逆に、ヘルスチェックに何度か失敗すると、unhealthyになる。

正常のしきい値と非正常のしきい値

どのような条件で、正常(=healthy)と非正常(=unhealthy)は切り替わるのだろうか?

その設定が、ターゲットグループに設定する「正常のしきい値」と「非正常のしきい値」だ。
ついでにヘルスチェックの送信間隔も載せておく。

  • 正常のしきい値
    • 異常なターゲットが正常であると見なされるまでに必要なヘルスチェックの連続的な成功回数
    • デフォルト:5
  • 非正常のしきい値
    • ターゲットが異常であると見なされるまでに必要なヘルスチェックの連続的な失敗回数
    • デフォルト:2
  • 間隔
    • ここのターゲットのヘルスチェックの概算間隔
    • デフォルト:30秒

ステータスの遷移

ターゲットヘルスステータスの遷移を、しきい値をデフォルトとして図示すると以下のようになる。

もし、ヘルスチェックの猶予期間を設定しても、その間にステータスが「非正常」になってしまうと、「正常」に遷移するまでには5回×30秒=150秒かかる計算になる。

結論

これで答えが出た。まとめると、今回の例ではヘルスチェックの猶予期間として、

アプリが起動するまでの時間=60秒
ステータスが非正常→正常に遷移するまでの時間=150秒
合計:210秒

が必要となる。ただ、これだとギリギリなので、もう少し余裕を持たせてもよいかもしれない。

アプリ内のアクセスログで正常応答が記録されていたのは、まだ非正常→正常に遷移する前の過渡期だったわけだ。
単発では正常応答を返しても、まだ「正常のしきい値(=5回)」に満たなかったのでステータスが「非正常」のままであった。

これでようやく、安定してサービスが起動するようになった。

Discussion