feature image

2024年4月10日 | ブログ記事

DBコネクションを張る場所に気を付けろというだけの話

この記事はtraP新歓ブログリレー2024、34日目の記事です。

はじめに

こんにちは!23Bの@cp20です!

今日はちょっと環境をプチ破壊しかけたしくじり事例について紹介していこうと思います。

何が起こったか

定期的にデータベース (MongoDB) にデータを保存して、フロント (Nuxt.js) からのリクエストが来た時にそこから値を読み出すようなアプリを作っていました。Nuxt.jsはフルスタックフレームワークなので、バックエンドもNuxt.jsで書いていました。

この図はFigmaで作っています

Nuxt.jsではバックエンドでの処理を1つの関数 (リクエストハンドラー) として書くことができて、そこにフロントからアクセスすることができます。今回はデータベースから値を読み取って返すだけのリクエストハンドラーを登録していました。

バックエンドがデータベースから値を取得する時、クエリというものを投げるのですが、クエリを投げるためにまずコネクションというのを確立する必要があります。お互いにやり取りするためにまず連絡先を交換するみたいなイメージです。

しかしボクのしてしまったまずい実装では、リクエストハンドラー内で毎回新たなコネクションを張るようになっていました。

するとどうなるか。リクエストごとにコネクションを張りまくる最悪のバックエンドが誕生します。しかもそれを閉じていないのでずっとコネクションが溜まっていきます。

実際は1000+コネクションぐらいでした

そしてサーバーは爆発しました。

その後じんわりとサーバーの負荷が高くなってるよアラートが出ていたのですが、いつものことかと思って放置していました。ボク一応SysAd班インフラチームのメンバーなんですが、いったい何をしているんでしょうね...?

解決策

Nuxt.jsのServer Pluginsを使って起動時に1つコネクションを張った上で全リクエストで使いまわすように書き換えました。

具体的にはserver/plugins/mongodb.tsに次ののような内容を書いて、リクエストハンドラー内からはevent.context.dbevent.context.collectionsとしてアクセスするように変更しました。

export default defineNitroPlugin(async (nitro) => {
  // 起動時に1回だけ実行される
  const { client, db, collections } = await connect();

  // 終了時
  nitro.hooks.hook('close', () => {
    client.close();
  });

  // リクエスト時
  nitro.hooks.hook('request', (event) => {
    event.context.db = db;
    event.context.collections = collections;
  });
});

ちなみにリクエストごとに新たにコネクションを張るオーバーヘッドがなくなった分、体感で100~200msぐらいレスポンスが速くなりました。嬉しいね。

教訓

DBコネクションを張る場所には気を付けよう!

昨今のバックエンドは抽象化が進んでエンドポイント単位 (リクエストハンドラー単位) でしっかりと分割されるようになりましたが、そうすると逆にこういう落とし穴にはまりがちなので気を付けましょう。

おわりに

明日は @ramdos っちの担当です!お楽しみに!

ちなみにボクの次のブログは5日後です。また会いましょう~


おまけ

タイトルはsetIntervalの第2引数に気を付けろというだけの話をオマージュしました (内容も割とオマージュしています)

おまけ2: コネクションプールについて補足

traP内でレビューを募ったらコネクションプールの話をされたので個人的に調べたこともあわせて補足しておきます。

ボクがやったことをもう少し具体的に書くと、もともとMongoDBのconnectメソッドをリクエストごとに呼ぶ設計だったのを一回だけconnectを呼んで使いまわす設計に変えたという話です。MongoDBはおそらくconnectメソッドが呼ばれたタイミングでコネクションプールを作り、minPoolSizeの数だけ先にコネクションを張っておいて、クエリを投げたタイミングで (もしコネクションが足りなかったら?) 追加でmaxPoolSizeまではコネクションを張ります。デフォルトでは最小が0、最大が100になるみたいです。

ボクの最初の設計で問題だったのは、リクエストごとにコネクションプールを作ってコネクションを張っていたことというわけですね。コネクションをいくら張っても限界がmaxPoolSizeで切られることなく、それぞれのコネクションプールごとにコネクションが溜まっていく (そして切断されずにいる) のでだんだんとデータベースに負荷がかかるというわけでした。

なので記事のタイトルを正確に言えば「DBコネクションプールを作る場所には気を付けよう」という感じかもしれません、が分かりにくいのでこのままにしておきます。

参考記事: https://qiita.com/tomoyanp/items/1041fc3ab60ef9c8b10c

おまけ3: データベースサーバー側での対策

今回コネクションが大量に張られてしまったが故にサーバーが爆発してしまったわけですが、ユーザーの怪しい実装は制御できないのでいつ何時サーバーを爆発させてしまうか分かりません。特にtraPのサーバーはかなり貧弱[1]なので、怪しいことをするとすぐに落ちてしまいます。

そこでデータベースのサーバー側からコネクション数を制限することでアプリは正常は動作しなくなる (かもしれない) 代わりにサーバーの無事を担保することができます。MongoDBの net.maxIncomingConnections オプションを使うことでコネクション数を制限できるみたいです。デフォルトだと100万近くあるので、これを適当に制限してあげると良いのではないでしょうか。


  1. 数GBのVPSn台に適当にサービスやらを分散させています ↩︎

cp20 icon
この記事を書いた人
cp20

23B ただのぷろぐらまーです icon: https://twitter.com/sora_douhu

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2024年3月22日
traPグラフィック班の活動紹介2024
haru10 icon haru10
2024年4月14日
Spotifyのクライアントを自作しよう
d_etteiu8383 icon d_etteiu8383
2024年4月14日
unityroomでAddressablesを使った話
inutamago_dogegg icon inutamago_dogegg
2024年3月17日
⑨でもわかる8bitアレンジ講習会
vPhos icon vPhos
2024年3月11日
思想の強いゲーム制作をしよう!
Kirby0717 icon Kirby0717
2024年4月22日
Reactで将棋作った話
mehm8128 icon mehm8128
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記