uiu です。ハローでは普段バックエンド開発をメインに担当していますが、創業以来片手間でインフラも担当しています。
ハローでは、少数精鋭のメンバーの意識をプロダクト開発に集中するため、インフラ面では Cloud Run などマネージドなサービスを最大限に活用しています。
今回は、久しぶりにインフラに意識の一部を捧げ、いくつかの眠れない夜を過ごす機会があったので、インフラ面の話について紹介しようと思います。
スタートアップと PostgreSQL
AutoReserve はサービス立ち上げ以来、DB は PostgreSQL、APPサーバーは Ruby on Rails のバックエンド構成で運用してきています。
特に PostgreSQL は立ち上げ以来安心して使い続けられている技術要素です。サービス運用から(ある規模までの)分析まで PostgreSQL だけで回せる点は、少人数でプロダクト開発するスタートアップにとって利点が大きかったと思っています。
また、Rails + PostgreSQL 構成の運用に関しての知見は、GitLab Handbook や Heroku など質の高いドキュメントにまとまっていて、困ったときに頼りになることも多いです。
Google Cloud の Cloud SQL for PostgreSQL 上で運用しており、トラフィックに応じてスケールアップすることでこれまでサービスを成長させてきました。
大きいテーブルでは、1億行を超えるレコードを持つテーブルもありますが、Declarative Partitioning などの機能を利用することで問題なく運用できています。
課題
PostgreSQL でサービスをスケールさせていく上で、1点ぶち当たる問題は DB コネクション数の上限に到達することです。
PostgreSQL はベストプラクティスとされるコネクション上限が小さいことで知られており、たとえば Cloud SQL のメモリが100GB以上ある環境でもデフォルト設定では高々 1000 コネクション程度です。
たとえば、オートスケール等でAPPサーバーのインスタンス数が増えたタイミングで、PostgreSQL 側のDBコネクション数の枠が足りなくなり、エラーが発生します。
もちろん Rails 側で connection pooling の仕組みはありますが、1 worker 辺り 1 connection を消費することを考えると一定以上のトラフィックが発生するとコネクションが足りなくなります。
キャッシュすることでスループットを高めるなどのアプリケーション側で対処ができるケースもありますが、write が多いケースでは持続的な解決策ではありません。
AutoReserve でもピークタイムで以下のような問題が発生していました:
- コネクション上限に到達し、新しいDBコネクションが作れないことにより、一部リクエストがサーバーエラーになる
- 同時コネクション数が増えることで、メモリ使用率が上がり、それと伴うようにDBプロセスがクラッシュする
当初は max_connections 設定を上げることでエラーを回避していましたが、DBプロセスがクラッシュする等の現象が発生するなど不安定性が増したため、解決になりませんでした。
max_connections 設定を上げるだけで、スループットが下がることを報告している記事 もあります。
PgBouncer 導入
Standalone Connection Pooler として PgBouncer を導入することにしました。
PgBouncer は、DBサーバーやAPPサーバーとは独立したサーバーとして動作します。 APPサーバーからDBに直接コネクションを貼る構成では、APPサーバーの数に比例してコネクション数が増えますが、PgBouncer が中間で connection を保持することでDBサーバーに貼られるコネクション数を一定数に抑えられます。
DBとAPPサーバーの間に新しいレイヤーを導入することになるため、PgBouncer 自体の不具合によってシステム全体が不安定性になるリスクがあります。
そのため、当初は導入を先延ばしにしていましたが、他の回避策では思ったような効果がなかったため、デメリットはあったものの PgBouncer の導入をトライすることにしました。
PgBouncer の導入後の効果
当然ですが、導入日(1/20辺り)以降、想定通りコネクション数が一定に抑えられています:
メモリ使用量についても大幅に改善しており、ピークタイムでもメモリ使用率が上がることはなくなりました:
これにより、DBクラッシュ等の大きな問題の頻度も減りました。
特にメモリ使用率については驚くほど改善しました。DB全体の安定性が体感でも増したため、僕個人としても睡眠の質が向上しました。
PgBouncer サーバー自体も現状安定して動作しています。その他、APIレスポンスタイム等のメトリクスが劣化することもなかったため、導入は成功だと言ってよさそうです。
PgBouncer デプロイ
デプロイ構成
以下のような構成でデプロイしています:
- Managed instance group 上に PgBouncer を docker で複数台立ち上げる
- internal tcp load balancer で複数台の PgBouncer にAPPサーバーからの接続を分散させる
- docker image は edoburu/pgbouncer を使用
app サーバーからDBまでの接続は以下のような経路になります:
app → internal tcp load balancer → pgbouncer (複数台) → db app: Rails アプリケーションサーバー db: PostgreSQL DB
PgBouncer 設定
transaction pooling mode を利用しています。
今回のケースでは、Rails 側ですでに session pooling を行っているため、transaction pooling mode でなければ効果がありません。
全体の connection が200程度になるように調整しています。
注意点
transaction pooling mode では session に依存する機能は使えないことに注意する必要があります。
advisory locks や prepared statements などの機能を使うことができなくなります。
https://www.pgbouncer.org/features.html#sql-feature-map-for-pooling-modes
Rails では、デフォルトで prepared statements が利用されたり、migration で advisory lock が取得されるため、これらの機能を無効にする必要があります。
そのため、database.yml で以下のように無効にしています:
production: &production <<: *default # ... prepared_statements: false advisory_locks: false
prepared statements を無効にすることでクエリに若干のオーバーヘッドが発生することが想定されますが、導入後の影響は軽微でした。
参考リンク
最後に、参考になったリンクをまとめておきます。
connection pooling については以下を参照しました:
Rails 開発では GitLab Handbook をお手本としてよく参考にしています。GitLab はフルリモート組織のため、開発ドキュメントが綺麗に整理されており、ドキュメントの書き方自体も学ぶ点が多いです。
Rails の本番運用でのベストプラクティスについては以下も参考にしています。author が Instacart での運用経験から書いたもので、著者のOSSライブラリを利用したり、アイデアをよく参考にしています。
まとめ
PostgreSQL のコネクション数上限と不安定性の問題を PgBouncer を導入して解決した事例を紹介しました。
PostgreSQL を長年運用している方にとっては常識的な内容ではあるかもしれませんが、誰かの参考になれば嬉しいです。
これからは、段階に応じて AlloyDB などよりスケーラビリティの高いDBへの乗り換えも計画していく予定です。
これからも開発生産性をMAXに保ちつつサービスを成長させていくために、インフラは極力シンプルに保ち、RDBMSを最大限に活用してプロダクト開発を進めていこうと思ってます。
株式会社ハローでは、シンプルに保つこと、高速にプロダクト開発をすること、を愛するエンジニアを募集しています。
業務委託や副業の形でも募集しているので、話を聞いてみたい方は uiu にぜひ気軽にお声がけください。