死んだ接続を使わずに、データベースを起こす
Mythexで公開したアプリとそのデータベースは、誰も使っていないときにスリープします。アプリが目覚めると、最初のリクエストを10〜12秒保留することがありました。スリープ中に死んでしまったデータベース接続を、再利用しようとしていたためです。現在、Spring Bootのアプリには、スリープを越えても安全なデータベース設定を与え、あらゆるスタックのアプリも同じように作るよう、エージェントに指示しています。ホスティング基盤において、目覚めた後の最初のリクエストは、12.7秒から2.3秒になりました。
アプリとデータベースがスリープする理由
Mythexで公開されたアプリは、数分のアイドルの後にサスペンドするマシンで動きます。そのデータベースはサーバーレスのPostgresで、5分のアイドル後にスリープし、次のクエリで目覚めます。これによって、静かなアプリの料金は安く抑えられます。データベースについては、実際に使った分だけ課金されます(ホスティングの制限を参照)。そして、スリープしているアプリはコンピュートに費用をかけません。課金側の話は、データベースを実際の使用量で課金するで書いています。
問題は、この2つが別々にスリープし、接続が何であるかについて意見が一致しないことです。
症状:最初の訪問が遅い
ほとんどのサーバーアプリは、接続プールを持っています。これは、データベース接続を数本、準備された状態で開いておき、リクエストごとに新しい接続を開く費用を払わずに済むようにする仕組みです。常時稼働するアプリには、これが正しい設計です。
スリープによって、次のことが起きます。
- アプリが実行中で、プールに数本のアイドル接続があります。
- 誰も訪問しません。アプリのマシンはサスペンドします。凍結され、プールはそのまま残ります。
- データベースがスリープし、その側のすべての接続を切ります。
- 訪問者が到着します。アプリは、そのままの状態で目覚めますが、もうデータベースが知らない接続を、まだ保持しています。
- プールがそのうちの1本の接続を渡します。その接続が死んでいることを知るには、試して待たなければならず、それから諦めることになります。
Spring Bootのアプリでは、そうした死んだ接続の検証が、最初のリクエストを約10秒ブロックしたうえで、「Connection is not available」という失敗に至っていました。訪問者にとってサイトは単に固まり、その後にエラーが出ることもありました。
Springのアプリが最も影響を受けた理由
Spring Bootは人気のあるJavaフレームワークで、既定ではHikariCPという接続プールを使います。Springはそのプールを設定から構成でき、アイドル接続をいくつ保持するかも含まれます。
しかしMythexは、各アプリにデータベースをpostgresql://user:pass@host/dbという形式の単一のDATABASE_URLとして渡します。Springはこの形式を直接読めません。そのため、Mythex上のSpringアプリは、そのURLを解析して、自分でプールをコードで組み立てていました。そして手作りのプールは、Springのspring.datasource.*の設定をすべて無視します。正しいプールの設定が分かっていても、それが効く場所がなかったのです。
修正その1:Springが使える設定
プロジェクトにデータベースがある場合、Springがネイティブに読める環境変数も与えるようになりました。
SPRING_DATASOURCE_URL、SPRING_DATASOURCE_USERNAME、SPRING_DATASOURCE_PASSWORD。DATABASE_URLから変換され、URLで別途指定されていない限りSSLが必須です。- スリープを越えて生き残るプールの設定です。
| 設定 | 値 | 理由 |
|---|---|---|
| 最小アイドル接続数 | 0 | リクエストの間に接続を開いたままにせず、古くなるものを残さない |
| アイドルタイムアウト | 30秒 | 使われていない接続を早めに閉じる |
| 最大寿命 | 2分 | 古くなる前に、すべての接続を退役させる |
| 接続タイムアウト | 5秒 | データベースの応答が遅くても、リクエストを長く保留しない |
アイドル接続を保持しないため、目覚めたアプリは死んだ接続の山をかき分ける代わりに、新しい接続を1本開くだけになります。
これらの変数が存在すると、Springがプール自体を構成し、設定が適用されます。プロジェクトが自分で設定したものは常に優先されます。私たちは不足している部分だけを埋めます。Spring以外のアプリはこれらの変数を無視します。
ほぼ何も効かなくなるところだった詳細
Springが環境変数を設定にマッピングするルールは、間違えやすいものです。minimum-idleという設定は、SPRING_DATASOURCE_HIKARI_MINIMUMIDLEと書かなければなりません。ダッシュを取り除き、単語をつなげるのです。SPRING_DATASOURCE_HIKARI_MINIMUM_IDLEと書くと、より自然に見えますが、それは別の、存在しない設定(minimum.idle)にマッピングされ、黙って無視されます。
こうした間違いは、すべてのレビューを通り抜けて、本番では何もしません。そこで、テストで2つのことを確認するようにしました。値が設定されていること、そしてアンダースコアで区切られた形式の変数がないことです。
修正その2:他のすべてのスタック
Springはプールを持つ唯一のフレームワークではありません。また、すべての言語のすべてのライブラリを事前に設定することはできません。そこで、Mythexのエージェント(あなたのアプリを書くAI)は、どのスタックでも従うべき標準の指示を受けるようになりました。公開されたアプリとそのデータベースは、アイドル時にスリープするため、開いたままの接続は次のリクエストでは死んでいるかもしれない、という内容です。エージェントは次のことをすべきです。
- アイドルのデータベース接続を開いたままにしないか、使う前に各接続を確認します。
- 接続エラーの場合は、1回だけ再試行します。
DATABASE_URLを解析して独自のプールを組み立てるのではなく、プラットフォームが環境に提供するデータベース設定を使います。
最後の点で、修正その1とつながります。提供された設定を使って作られたアプリは、スリープに安全な構成を自動的に取り込みます。
結果
私たちが公開アプリを動かしているホスティング基盤で測定したところ、目覚めた後の最初のリクエストは、12.7秒から2.3秒になりました。
得られた教訓
- スリープするすべての層は、何が生き残るかについて合意している必要があります。 凍結されたアプリとスリープしたデータベースは、それぞれ妥当な選択をしていました。しかし組み合わせると、アプリは、もはや真実ではない状態を持ったまま目覚めていました。
- 常時稼働のサーバー向けに設計された既定値は、ゼロにスケールするアプリには合いません。 接続を温かく保つのは良い考えですが、その周りのすべてが冷えてしまえば話は別です。
- フレームワークには、そのフレームワークの形式でデータを渡してください。 1つのURLは私たちにとっては単純でしたが、すべてのSpringアプリに、自分の設定を無視する手作りのプールを強いていました。
- 設定名のつづりが、フレームワークが読む形と一致するかテストしてください。 正しい値でも名前を間違えると、黙って何もしません。
Mythexでのデータベースの仕組みに初めて触れる場合は、データベースのドキュメントと、Postgresとは何かの解説をご覧ください。