
Ruby on Rails
イベント
マガジン
技術ブログ
はじめに SRE の寺島です。 MNTSQ の契約管理プロダクト(以下、CLM)では、テナントごとに MySQL のスキーマを分ける構成で Aurora MySQL に接続しています。このアーキテクチャは DB 接続数が増えやすく、運用上の大きな課題となっていました。 この記事では、接続数を抑えるために取り組んだ試行錯誤や検証結果をまとめています。同じようにスキーマ分離の構成で接続数の課題に直面している方の判断材料になれば幸いです。 TL;DR Rails の idle_timeout 短縮(300 秒 → 60 秒)により、日中の DB 接続数は約 50% 減少しました。 RDS Proxy は接続数を減らすどころか、約 2 倍に増やしました。 弊社のシステムのスキーマ分離型マルチテナント構成では、RDS Proxy は接続数削減につながりませんでした。 背景: 接続数がインスタンスサイズの制約になっていた CLM の本番 DB では、接続数の増加に伴いアラートが鳴るたびに max_connections を引き上げて運用していました。その結果、設定値はインスタンスクラスのエンジン既定値を大きく超過する状態となっていました。 Aurora MySQL における max_connections の既定値はインスタンスのメモリ容量に基づいて算出されます( Managing performance and scaling for Aurora MySQL )。既定値を超える設定は「メモリの裏付けがない接続」を受け入れる状態を意味し、突発的な接続バースト時のメモリ枯渇や、Performance Schema の肥大化による OOM のリスクを抱えることになります。つまり、安全に受け入れ可能な接続数を増やすには、メモリ容量(=インスタンスサイズ)の引き上げが不可欠でした。 一方で、リソースの使用状況には大きな偏りがありました。 CPU : 日中平均で 10% 強、最大でも 25% 程度と十分な余裕がある メモリ・接続数 : 接続枠を安全に確保するために、インスタンスサイズを引き上げざるを得ない 日中の CPU Utilization しかし、インスタンスを 1 サイズ引き上げても、メモリ由来で増える max_connections の既定値は 1,000 本程度に過ぎません。CPU が余っているにもかかわらず、接続数枠とメモリの安全マージンを確保するためだけにインスタンスを上げ続ければ、短期間で費用が倍々に膨らんでいきます。 そのため、インスタンスサイズの引き上げで対処するのではなく、接続数の増加そのものを抑制する根本的な対策が必要でした。 前提の環境 2026年10月時点 アプリケーション: Rails 8.1(Active Record、mysql2 アダプタ)。 idle_timeout と reaping_frequency の既定値は 300 秒と 60 秒 DB: Aurora MySQL アプリケーション実行基盤: ECS Fargate テナント分離: テナントごとに MySQL のスキーマを分け、接続時にスキーマを指定する なぜスキーマ分離だと増えやすいか 接続数の構造 一般的な Rails アプリケーション(単一スキーマ構成)では、基本的に 1 プロセスに対して 1 つのコネクションプールが割り当てられます。しかし CLM ではテナントごとにスキーマを分けているため、Active Record のコネクションプールもテナントごとに独立して生成されます。 そのため、1 つのプロセス内には「そのプロセスが処理を担当したテナントの数」だけプールが保持されていくことになります。 この前提を踏まえると、1 プロセスが抱える DB 接続数は、おおよそ次のような関係式で捉えることができます。 1 プロセスの接続数 = プールを持つテナント数 × テナントあたりの実接続数 この構造において留意すべきなのは、 database.yml に設定する pool 値はあくまで「テナントあたりの実接続数の上限」であり、プールを持つテナント数を抑えるものではないという点です。 一般的な Rails アプリケーションであれば pool 値の引き下げが接続数削減の直接的な手段になりますが、本構成ではテナントあたりの接続数が pool の上限に達していない限り、いくら pool 値を小さくしても全体の接続数は限定的になります。 そのため、接続数の増加を抑えるには、プールを抱えるテナント数そのものの挙動や、接続のライフサイクル(アイドル時間の制御など)に着目する必要があると考えました。 実測 本番の writer に対して、 performance_schema.threads と information_schema.innodb_trx のテーブルを 60 秒ごとに取得しました。これらを ECS Fargate タスクの IP と突き合わせ、コンポーネント別に「プールを持つテナント数」と「テナントあたりの接続数」を実測・分析しました(※数千本の接続がある writer への負荷を考慮し、 SHOW PROCESSLIST は避けています)。 計測の結果、主に次の2点がわかりました。 1. 接続数増加の主因はプールを持つテナント数の増加 接続数を押し上げていた主な要因は、プロセス数そのものやテナントあたりの接続数ではなく、各プロセスがプールを保持しているテナント数の増加でした。 Web(Puma) : 1 プロセスあたりの接続数は、深夜と日中で 約 4.6 倍 の開きがありました。この主因は、プールを保持するテナント数が深夜の「約 2 割」から日中には「約 7 割」へと拡大していたためです。テナントあたりの接続数自体も深夜の 1.1 本から日中には 1.4 本へと微増しますが、接続数全体の変動の大部分はプールを持つテナント数の増減で説明がつきます。 ワーカー(syoryuken) : 日中のプロセス数は深夜の約 2 倍にとどまる一方、接続数は 約 20 倍 に跳ね上がっており、日中の全体接続数増加分の約 4 割を占めていました。こちらもプロセス数のスケールアウト以上に、各プロセスが処理の中で多様なテナントに触れ、プールを抱え込んだことが主因でした。 以下のグラフは、Web の「1 プロセスあたりの DB 接続数」と「1 プロセスあたりのプールを持つテナント数」の推移を、深夜の最低値を 1.0 として相対化して重ねたものです。 2 本の線はほぼ重なって推移しており、日中にわずかに開く差分だけが、テナントあたりの接続数の増加分(1.1 本 → 1.4 本)です。 2. 接続のほぼすべてがアイドル状態 接続がどのように使われ、破棄されているかを調査したところ、接続が使われ続けているのではなく放置されて残っているだけだと分かりました。 使われている接続はごくわずか : トランザクションを開いている接続は平均数本(瞬間最大でも数十本)で、全体の 1% 未満でした。この数字は単発の autocommit クエリを含みませんが、後に RDS Proxy 経由で測った「クエリ実行中を含めて借用されている接続」も日中平均で全体の 5% 以下でした。60 秒以上続く長時間トランザクションもほぼ存在しません。 pool 設定は上限としてほぼ機能していない : テナントあたりの接続数が pool の上限に達していたサンプルは全体の 0.07% に過ぎませんでした。 接続の生存時間の中央値はちょうど 300 秒 : 接続確立から破棄までの生存時間の中央値は、ちょうど 300 秒でした。これは Rails の idle_timeout の既定値と完全に一致しており、一度使われた接続が 5 分間放置されて Reaper に切られるまで残り続けている実態が浮かび上がりました。 以上の実測から、CLM において接続数を左右している要素は pool ではなく、「プロセス数」「プールを持つテナント数」「接続の生存時間(アイドル時間)」の 3つであることが明確になりました。 打ち手の洗い出し 実測データを数え直して、打ち手ごとの削減量を見積もりました。 打ち手 削減の見込み 根拠 pool を下げる 全体で 1〜3% 程度 テナントあたりの接続数が上限に達していないため 長時間トランザクションの解消 ほぼゼロ(最大 1 本程度) 同時に開いている長時間トランザクションは常に 1 本以下のため idle_timeout の短縮 日中ピークで 30〜45% 減 接続の生存時間の中央値(300 秒)が idle_timeout と一致しているため RDS Proxy の導入 現状の 1/4 〜 1/3 まで削減 トランザクション単位での多重化による抜本的な削減(詳細は後述) アプリケーション側の設定変更として実質的に効果が見込めるのは、 idle_timeout の短縮だけ でした。しかし、 idle_timeout を短縮しても「プールを持つテナント数が全体のテナント数に比例して増えていく」という根本的な構造は変わりません。 そこで、接続数の決定要因そのものを変える本命のアプローチとして、 RDS Proxy の導入 を検討しました。 RDS Proxy はトランザクションの終了時に DB 接続をプールへ返却するため、DB 側の接続数を「クライアントが保持している接続数」から「同時に実行中のトランザクションと文の数」ベースへと多重化できるのではないか、と考えたためです。実測上、トランザクションを開いている接続は全体の 1% 未満(数本〜数十本)にとどまっていたことから、この仕組みがうまく機能すれば接続数を大幅に抑えられるのではないかと期待していました。 idle_timeout の短縮: 接続数を半減 設定値の決め方 Rails の Active Record には、一定時間使われていない接続を自動で破棄する Reaper という仕組みがあります( ActiveRecord::ConnectionAdapters::ConnectionPool::Reaper )。 reaping_frequency : Reaper が巡回してアイドル接続をチェックする間隔(既定値: 60 秒) idle_timeout : 最後に使われてから接続を維持する制限時間(既定値: 300 秒) Reaper は reaping_frequency の周期ごとに実行され、前回の使用から idle_timeout を超えている接続を切断します。つまり、一度使われた接続は最短で idle_timeout 、最長でも idle_timeout + reaping_frequency の時間が経過すると破棄されます。 今回の見直しでは、実測した「最終使用からの経過時間」の分布をもとに、接続が維持される上限時間を 360 秒から 90 秒 へと短縮することにしました。 idle_timeout : 300 秒 → 60 秒 reaping_frequency : 60 秒 → 30 秒 結果 本番環境に反映した直後から接続数は急減し、狙い通りの水準で安定しました。変更前後それぞれ平日 3 日の同じ時間帯で比較しています。 項目 変更前後の変化 日中の平均接続数 約 50% 減 日中のピーク 約 43% 減 新規接続の頻度 約 1.8 倍 writer の CPU(日中平均) ほぼ変化なし FreeableMemory ほぼ変化なし 次のグラフは、反映日を挟んだ writer の接続数の推移です。反映した時点で段差ができ、以後は低い水準で推移しています。 懸念された副作用についても、以下の通り問題ないことを確認しました。 CPU への影響(なし) : 接続の生存時間を縮めたことで接続の張り直し頻度は約 1.8 倍に増えましたが、接続確立時のオーバーヘッドは小さく、CPU 使用率に有意な変化はありませんでした。 メモリの挙動(想定通り) : 空きメモリの増加は 1% 未満にとどまりました。接続数削減の狙いは定常時のメモリを空けることではなく、接続が急増したときの変動幅を抑えることと max_connections を既定値に収めることにあるため、この結果は想定どおりです。 RDS Proxy の導入 仮説と試算 RDS Proxy を導入した場合、DB 側の接続数は次の 4 つの内訳の合計になると試算しました。 DB 側に残る接続 見込み 算出の根拠 同時に実行中のトランザクションと文 数本から数十本 実測値(全体の 1% 未満) pin されて返せない接続 全体の 1 から 3 割 後述の事前検証結果より Proxy がプールに保持する待機接続 ほぼスキーマ数と同じ本数 1 スキーマにつき 1 本の接続を維持すると仮定 Proxy を通さない接続 数本 運用系・メンテ用の直接接続 これらを積み上げると、全体の接続数は 当時の 1/4 〜 1/3 程度まで大幅に削減できる 見込みでした。 導入前に描いていた多重化のモデルが次の図です。上が直接接続、下が RDS Proxy 経由で、Proxy のプールを 1 つとして考え、DB 側の待機接続はテナントごとに 1 本に収まる想定でした。 事前検証: pin の発生率を測る RDS Proxy には、クライアント接続が「他のセッションに引き継げない状態」を作ると、そのクライアントを特定の DB 接続に固定するpin(ピン留め)という挙動があります( Avoiding pinning an RDS Proxy )。pin された接続はクライアントが切断するまでプールに返却されず、多重化に寄与しません。そのため、本番ワークロードで pin がどの程度発生するかが削減効果を左右します。 CLM のアプリケーションを調査したところ、該当する条件は次の 2 点でした。 接続時の SET クエリ(パラメータグループで回避) Rails が接続初期化時に発行する SET lock_wait_timeout は、Proxy が状態を追跡しない変数であるため、放置すると全接続が pin されてしまいます。これはパラメータグループ側に設定を移し、 database.yml から記述を外すことで回避しました。 16KB を超える大きな SQL(実測により影響度を推定) CLM には、1MB 級の INSERT や、巨大な IN 句を持つ preload が存在します。16KB 超のクエリは Proxy の制約で接続が pin されます(契約書を扱う以上、ある程度は避けられないと考えています)。 2 つ目の大きな SQL については発生箇所をすべて改修しきれないため、本番環境で発生頻度を計測しました。本番の general log を 5 分間ずつ複数回サンプリングし、16KB 超の SQL を発行した接続の発生率から pin される割合を推定したところ、中心的な見積もりで約 1 割、上限の見積もりでも 3 割弱でした。 この結果から「pin される接続は最大でも全体の 3 割程度に収まり、大半の接続は多重化される」と判断し、前述の試算(pin による固定: 1〜3 割)を置いて導入へと進めました。 段階導入と切り戻しの準備 RDS Proxy はホップが 1 つ増えるため、レイテンシ悪化や ERROR 9501 などの新しい障害モードが加わります。そこで、まずは遅延に寛容な非クリティカルのワーカー 1 種から本番投入し、1 営業日観測しました。 認証失敗や借用待ちは問題なく、処理量も維持されていたため、Web および全ワーカーへと展開しました。なお、万が一に備えて接続先はコンポーネント単位で即座に直接接続へ切り戻せる構成を担保しています。 結果: 接続数は減らず、逆に倍増した 3つの構成における接続数の比較 writer の DatabaseConnections について、以下の 3 つの構成で実測値を比較しました(※「 idle_timeout 短縮後の直接接続」を 1.0 とした相対比)。 構成 日中平均 深夜平均 日中ピーク 直接接続( idle_timeout 300 秒) 2.0 2.6 2.3 直接接続( idle_timeout 60 秒) 1.0(基準) 1.0(基準) 1.0(基準) Proxy 経由( idle_timeout 60 秒) 2.0 4.6 1.7 接続数を劇的に減らす本命として導入した RDS Proxy でしたが、結果は期待と大きくかけ離れるものでした。 日中平均で 2 倍、深夜平均で 4.6 倍に増加 : idle_timeout 短縮後の直接接続と比べて接続数が跳ね上がりました。短縮前(300 秒)の直接接続と比較しても、日中は同水準、深夜に至っては約 1.7 倍に悪化しています。 日中ピークのみ約 25% 抑制 : 短縮前(300 秒)の直接接続ピーク(2.3)と比較すると、Proxy 経由(1.7)は約 25% 低く抑えられていました。 パフォーマンスとエラーの検証 一方で、接続数以外の稼働状況は良好でした。 接続の借用待ち : 平均数十マイクロ秒、1 分間の最大値でも約 50 ミリ秒程度 エラー発生 : 認証失敗や ERROR 9501 (DB 接続取得のタイムアウト)はゼロ 遅延や接続エラーといった性能面の悪化はなく、純粋に DB 側の接続数だけが大幅に増えたという状況でした。 見積もりと実測の乖離 事前見積もりと本番実測値を突き合わせた結果は、次の通りでした。 DB 側に残る接続の内訳 当初の見積もり 本番での実測値 判定 同時に実行中のトランザクション 数本 数本 見積もり通り pin されて返せない接続 全体の 1〜3 割 プール全体の 5% 未満 想定より良好 Proxy を経由しない接続 数本 数本 見積もり通り Proxy がプールに保持する待機接続 1 スキーマあたり 1 本 1 スキーマあたり約 8 本 最大の誤算 懸念していた pin の発生率は 5% 未満と低く抑えられており、多重化の条件そのものは満たせていました。 接続数が減らなかった原因はシンプルで、 「Proxy が保持する待機接続」をスキーマあたり 1 本と見積もっていたのに対し、実際にはその約 8 倍に膨らんでいたため でした。 なぜ 1 つのスキーマに対して 8 本もの待機接続が残ってしまったのか 原因は、RDS Proxy のアーキテクチャと接続のライフサイクルにあります。次の 3 点です。 1. Proxy の接続プールは「ノードごと」に独立している RDS Proxy の内部は複数のノードで構成されており、接続プールはノード間で共有されません。本番環境の Proxy はノードが 8 台で、同一スキーマの DB 接続が 8 つのノード IP から別々に張られていました。1 つのスキーマに対して最大 8 本の待機接続が残る構造です。 2. 一度使われた接続は 5 分間 idle とみなされない 公式ドキュメントでは、 5 分間使われなかった DB 接続を idle とみなし、 MaxIdleConnectionsPercent を超えた分だけを閉じる とされています。言い換えると、最後の借用から 5 分以内の接続は閉じる対象になりません( AWS 公式ドキュメント )。 3. 複数プロセスからのアクセスにより、全ノードへ接続が拡散する Web やワーカーの複数プロセスからリクエストが送られる際、Proxy の負荷分散によって接続は異なるノードへ振り分けられます。また、Rails が使われていない接続を切断・再接続する際にも、別のノードに着くことがあります。 この結果、あるスキーマへのアクセスが短時間のうちに全 8 ノードへ分散し、それぞれのノードで DB 接続が確立されて 5 分間居座り続けます。 flowchart LR C["スキーマ A への<br/>クライアント接続<br/>(複数プロセス、60 秒ごとに作り直し)"] subgraph P["RDS Proxy(プールはノードごと)"] direction TB N1["ノード 1"] N2["ノード 2"] Nd["…"] N8["ノード 8"] end DB[("DB<br/>スキーマ A の接続<br/>最大 8 本")] C --> N1 C --> N2 C --> Nd C --> N8 N1 --> DB N2 --> DB Nd --> DB N8 --> DB クライアントから見ればある時点で張っている接続はわずかであっても、DB 側から見るとスキーマ A だけで 8 本の待機接続が保持されている状態になります。これが稼働している全テナントのスキーマで同時に発生した結果、待機接続が「スキーマ数 × ノード数」まで跳ね上がることになりました。 メカニズムを裏付ける実測データ この仮説は、本番のメトリクスとログからも裏付けられました。 ノード数の確認 : Proxy の CloudWatch メトリクス MaxDatabaseConnectionsAllowed は、 max_connections × MaxConnectionsPercent をノード数で等分した値の合計として出てくるので、1 ノード分の値で割るとノード数が分かります。同じメトリクスの SampleCount (1 分あたりノード数と同じ)と、Proxy が VPC に持つ ENI の本数も 8 で一致しました。ノード数の決定基準は AWS が公開しておらず、顧客側で指定も制限もできません。 全ノードへの接続の拡散 : DB 側の接続スナップショットを「ユーザー × スキーマ × ノード IP」で集計したところ、日中は平均 6.6 ノードに接続が分散しており、過半数のスキーマが 8 ノードすべてに接続を保持していました。 5 分間の生存ルール : Proxy のデバッグログから接続切断ログを分析したところ、最後の借用から切断されるまでの時間は最短でも 303 秒、中央値は 380〜430 秒であり、300 秒未満で切断された例は皆無でした(切断理由はすべて「There were too many idle connections in the connection pool」)。 (補足)深夜の接続数が 4.6 倍に跳ね上がった理由 前述の比較表で深夜の接続数が直接接続の 4.6 倍に達していたのは、この 5 分ルールと社内ツールの挙動が重なったためです。 CLM では、運用ツールが 数分間隔で全テナントのAPI を叩く仕様になっていました。直接接続のときは Rails が 60 秒で切断していたため表面化していませんでしたが、Proxy 経由では全スキーマの接続が 5 分以内に必ず再利用され、プールからほとんど解放されない状態が作られていました。 設定で下げられないかの模索 効かなかったアプローチ Proxy 側の設定変更や運用回避によって接続数を抑えられないか模索しましたが、有効な手段はありませんでした。 MaxIdleConnectionsPercent の引き下げ(効果なし) : すでに 10% と低めに設定していましたが、さらに下げても変化はありませんでした。前述の通り、接続が 5 分以内に再利用されて「アイドル判定」の対象にならないためです。 完全修飾テーブル名への移行(非現実的) : AWS が案内している根本対策は「SQL を schema.table 形式で記述し、接続時のスキーマ指定をなくす(1 つの共通プールにする)」ことでした。しかし、CLM のアプリケーション全体のコードベースを改修する必要があり、現実的ではありませんでした。 AWS サポートにも問い合わせたものの、「現在のスキーマ分離の接続方式を維持したまま、Proxy の設定だけで待機接続を大幅に削減するのは困難」という回答でした。 上限設定( MaxConnectionsPercent )によるキャップ案の検証 そこで考えたのが、 MaxConnectionsPercent を使って Proxy が保持するプール接続数そのものに上限(キャップ)を設ける案でした。 実測上、同時にアクティブとなっている接続(借用中)はプール全体の 5% 未満です。そのため、上限をかなり低く絞っても、トランザクションの同時実行に必要な接続枠(需要)自体は十分に足ります。 ここで焦点となったのは、「上限に達した際、Proxy がどのように振る舞うか」でした。 仮説 A(追い出し) : 未使用の別スキーマの待機接続を破棄し、要求されたスキーマで新しく DB 接続を開き直す 仮説 B(借用待ち) : 空きが出るまでクライアントを待たせ、最悪 ConnectionBorrowTimeout でエラーになる 仮説 A であれば接続数を物理的に頭打ちにしつつ正常稼働できますが、仮説 B であればレイテンシ悪化や障害につながります。AWS の公開ドキュメントからはどちらの挙動になるか判断がつかなかったため、開発環境で検証を実施しました。 試験 1: 上限到達時の挙動確認 あえて上限をごく小さな値に絞り、その上限数を大きく超える数のスキーマへ同時にクエリを発行し続けました。 結果 : すべてのクエリがエラーなく成功し、レイテンシも上限なしの環境と有意な差はありませんでした。 ログの裏付け : Proxy のログには The pool reached the maximum connection setting, and a connection with different properties was required と記録されており、 「上限到達時は別スキーマの待機接続を破棄して開き直す(仮説 A)」 動作であることが確認できました。 試験 2: 借用待ちが発生する境界値の測定 上限に対してどの程度まで同時借用需要が近づくと待ち時間が発生するかを測定しました。 結果 : 借用待ちが発生し始めるのは需要が上限の 9 割を超えてからであり、一時的に上限の 1.5 倍近い負荷をかけてもタイムアウト( ConnectionBorrowTimeout )には至りませんでした。 本番環境の定常需要は想定していた上限値の 2 割程度に収まる見込みだったため、この挙動が AWS の仕様として将来にわたって保証されるのであれば、接続数を抑える有力な解決策になるという段階まで検証を進めることができました。 AWS の回答と判断 AWS サポートおよび担当 TAM を通じて、サービスチームに仕様と設計の整合性を確認しました。いただいた見解は次の通りです。 接続追い出しの挙動は未保証 : 上限到達時に他スキーマの待機接続を切断して新規接続を優先する内部処理の順序や条件は公開されておらず、仕様として保証された動作ではない。 設定値の推奨プラクティス : MaxConnectionsPercent は「直近の最大接続数( DatabaseConnections )の 30% 上」に設定することが推奨されており、既存の接続数を意図的に下回るキャップを設ける運用は想定されたベストプラクティスに沿わない。 内部実装への依存リスク : 検証環境で再現できた挙動であっても、ドキュメント化されていない内部実装である以上、マネージドサービスの将来的なアップデートによって予告なく挙動が変更される可能性がある。 検証環境では期待通りに動作したものの、公式に仕様として保証されていない挙動(2026 年 10 月時点の確認です。AWS のドキュメントと挙動は更新されることがあります)に本番環境の可用性を委ねることはできないと判断しました。もし仕様変更や予期せぬ負荷パターンによって借用待ちが発生した場合、即座にサービス影響(レイテンシ悪化や接続タイムアウト)につながるリスクを抱えることになります。 一方で、上限を設けない運用のままでは、接続数は直接接続の 2 倍の水準を維持したまま、テナント数の増加に伴って増え続けてしまいます。 上限を設ければ接続数を頭打ちにできる見積もりでしたが、保証されない挙動に本番を預けることは許容できないと判断し、RDS Proxy の採用を見送って直接接続へ切り戻す判断をしました。 RDS Proxy を再検討する条件 今回の検証結果を踏まえると、単に「マルチテナント環境では RDS Proxy は使えない」という話にとどまらず、どういった条件下であれば RDS Proxy が有効な選択肢になり得るか、という損益分岐点も見えてきます。 Proxy が接続数の「上限」として機能する仕組み RDS Proxy を経由した場合、DB 接続数はクライアントのプロセス数には依存せず、以下の構造でおおむね頭打ちになると考えられます。 Proxy の上限 ≈ 待機接続(おおむね スキーマ数 × ノード数) + 同時借用のバースト分 待機接続(第 1 項) : 「ユーザー × スキーマ × ノード」の組み合わせごとに最低 1 本の接続が保持され、これが待機接続のベースラインになります。 同時借用のバースト分(第 2 項) : 同じ組み合わせに対して同時に複数のクエリ借用が発生した場合にのみ、接続が追加で開かれます。この追加接続も 5 分間はプールに残るため一時的な上乗せとなります。弊社は 1 テナントあたりの同時アクセスが少なく、組あたりの接続は深夜で 1 本、日中でも平均 2 本弱だったため、全体はおおむね第 1 項に近い値になりました。テナントあたりのトラフィックが多い環境では、第 2 項の分だけこの値を上回ります。 実測でも、日中のピーク接続数は「(スキーマ × ノード)の組み合わせ数」に「瞬間的な最大借用数」を足した値とほぼ一致していました。 前述の検証結果において、 idle_timeout 短縮前と比べて「日中のピークだけは約 25% 低く抑えられていた」のは、クライアント側の瞬間的なプロセス増加やスパイクがこの Proxy の接続上限に吸収されていたためです。 弊社の環境における損益分岐点と再検討の判断基準 今回の環境では、Proxy の上限値は「 idle_timeout 短縮後の直接接続ピーク」の 2 倍弱に位置していたため、導入によって接続数が増加する結果となりました。 しかし、直接接続が「プロセス数」に比例するのに対し、Proxy 経由は「スキーマ数 × ノード数」で頭打ちになるという構造の差を踏まえると、再検討の判断基準は次の 2 点に整理できます。 接続数の支配的要因が「テナント数」ではなく「プロセス数」であること テナント数の増加が主因であるフェーズでは、テナント増に比例して Proxy の上限そのものが押し上がるため効果は見込めません。一方で、Web の台数増やワーカーの大規模なスケールアウトによって「プロセス数の伸び」が先行し、直接接続のピークが「スキーマ数 × ノード数」を恒常的に上回る規模になれば評価は逆転します。この領域に達して初めて、Proxy は「接続数を一定の枠内に抑え込むバッファ」として機能します。 最新のノード数に基づく上限の再試算 Proxy の内部ノード数はインスタンスサイズや AWS 側のリソース配置によって変動します。再検討にあたっては、その時点の「スキーマ数」とメトリクスから把握できる「ノード数」を掛け合わせ、最新の上限ラインを引き直して判断する必要があります。 まとめ 本記事では、スキーマ分離型マルチテナント環境における DB 接続数の削減と、RDS Proxy 導入検証の顛末について紹介しました。 今回の取り組みでは、手元の idle_timeout 短縮により、副作用なく接続数を半減させる成果が得られました。一方で本命だった RDS Proxy では、事前にログから pin 率を計測して入念に試算したにもかかわらず、本番環境の複数ノードや待機仕様が重なり、想定外に接続数が倍増するという結果になりました。 同じような構成でコネクション数に悩んでいる方や、RDS Proxy の導入可否を検討されている方の参考になれば幸いです。 謝辞 一連の問い合わせに丁寧に対応いただいた AWS の担当 TAM、SA、サポートの皆さまに感謝します。 関連記事と参考資料 MNTSQ のワーカーのオートスケール(以前の記事) RDS Proxy のマルチテナント環境での多重化の制約(AWS ドキュメント) MaxIdleConnectionsPercent と idle 判定(AWS ドキュメント) MaxConnectionsPercent と ConnectionBorrowTimeout(AWS ドキュメント) RDS Proxy の pinning の条件(AWS ドキュメント)
こんにちは。株式会社タイミーのバックエンドエンジニアの神山( @dak2 )です。 2026/9/19 に名古屋の大須演芸場で開催された名古屋 Ruby 会議05 に登壇してきました。 タイミーでは、世界中で開催される技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度があり、そちらを活用して現地でカンファレンスに参加してきました。 今回は『特殊変数大全』というテーマで登壇しました。印象に残ったセッションや、当日の様子を紹介します。 Road to 名古屋 Ruby 会議05 会場の大須演芸場は、大須観音や大須商店街のある賑やかな大須界隈に位置する、1965年開館の寄席です。 www.osuengei.nagoya そんな場所で喋れる機会があったら、もちろん気になるじゃないですか? しかも今回は出囃子を生演奏してくれるとのことで、これは絶対に壇上で喋りたいやつじゃん…!となるわけです。また、せっかくなので落語の着物を着たいと思ってました。 ということで、勢いよくプロポーザルを出しました。あと、このプロポーザルは自信がありました。 Accepted された時は嬉しかったですね。 2026/9/19(土) が開催日でしたが、前日入りで名古屋に訪れました。 ホテル代が高騰してた(アジア競技大会)のでカプセルホテルに泊まったんですが、かなり居心地良かったですよ。ウェルビー栄というサウナの聖地なんですがね。最高でした。 うなぎ食べたり、名古屋城行ったりしつつ、なぜか名古屋城のほとりに金シャチ横丁という食べ歩きができる施設があるんですが、そこでスライドを直してましたね。まさかお城の周辺でスライド直すとは思ってなかった。 自分のセッション 落語の寄席は流れがあって、前半 → 仲トリ → 仲入り → 食いつき → ヒザ → トリ とざっくりこういう流れで進んでいきます。 仲入りは、寄席の途中に設けられる休憩です。今回、自分は昼休憩直前の『仲入り前』の枠で登壇しました。 speakerdeck.com 本当にたまたま Ruby のリファレンスで特殊変数を読んでいて、Ruby::Box を有効にすると $_ はどう動くんだろう?と試したら、古い値が返るバグを見つけたところから探索が始まりました。 特殊変数、本当に奥深いんですよね、一枚岩じゃないんですよ。同じ $ でもプロセスやスレッド、ブロックごとに違ったりとか。それを注意深く追ってると、Box と Ractor で扱いが交差してくるしで一筋縄ではいかない。 スライドの内容を考えるにあたって、Ruby の処理系を真面目に追うことになりました。 皆さんも特殊変数追ってみてください(根気は必要そうです) スライドは少しでも落語調にしたくて UI を調整しました。スライド右の方にはめくりを表す白の余白、スライド下部には緋毛氈を表す装飾を施しました。フォントは寄せ文字の有料フォントです。このためにフォントを初めて買いました。(会場のコントラストの関係で、黒赤だと見づらかったようです) 演奏していただいた出囃子の楽曲は、たぬきという曲でした。化けるたぬきということで、$ の形をしながらもいろんな場所で姿を変える特殊変数を表したいというコンセプトでした。 これは撮影してもらった登壇時の写真です。初めて落語の着物を着ました。せっかくの機会なので自費でレンタルしました。めちゃくちゃ暑かったけど、こんな機会は滅多にないので着れてよかったです。 Special thanks to @tonyfactory210 他の方のセッション 片月亭 黒曜さんの型の話 「型 再考」、お聞きいただきありがとうございました! こちら発表資料です!(一応Claudeで可能な限り裏取りしましたが、なにか間違った記述あったらすみません) https://t.co/QGlKiZGnra #nagoyark05 — 黒曜@Leaner Technologies (@kokuyouwind) 2026年9月19日 やはり、漸進的型付けはそれ自体が難しいという感想を抱いた。型付け自体の有用性は認めつつ、型付けで表現するということは、その表現可能な集合に文法が規定されるんだなという感想を持ちました。Ruby のように言語自体の表現力が非常に高い言語だとそりゃ難しいよなあと。untyped とかそもそも型検査を skip するようなハッチが生まれがち。それを直す契機は大体バグが起きた時なのもまた難しいポイントですね。 盆暗亭 しおいさんの Socket.getifaddrs の話 名古屋Ruby会議05にてお話しした『時うどん〜Socket.getifaddrsで学ぶネットワーク編』の発表スライドです。出囃子は『ふるさと』でした。お付き合いいただきましてありがとうございました! https://t.co/A5sjIqClTD #nagoyark05 — Misaki Shioi (しおい) (@coe401_) 2026年9月20日 上方落語の時うどんをアレンジしていてこだわりを感じました。うどん1杯を食べるために1文足りない主人公がいま何刻かを店主に言わせて、そこから数えて1文をちょろまかす話ですが、それがネットワークインターフェースになっており、近代化を感じました(?) ネットワークインターフェースの数の数え方は勉強になりました。 Socket オブジェクトを触ってみましたが、 UDPSocket などあって探索しがいがあり面白そう。追ってみたいなと思いました。 kamipo さんの基調講演 名古屋Ruby会議05の登壇資料です #nagoyark05 https://t.co/8akp5Xk9y5 — Ryuta Kamizono (@kamipo) 2026年9月22日 Rails の JOIN とどう向き合ってきたかというお話しでした。 joins、元々種類ごとに並び替えられてたんですね。left_joins 最後だったと。string joins の位置、join alias で where を先に書かないといけない問題の対応とか。 where.missing と where.associated 周りの話は興味深かったです。関連の有無を判定するSQLについて、JOINを使う方法と、NOT EXISTS / EXISTSを使う方法の違いが印象に残りました。着実な最適化がされてきたんだなと ActiveRecord の進化の歴史の一部を感じられました。 LATERAL JOIN 知らなかったので勉強になりました。サブクエリを行単位で適用できるのか。 観光など 台湾料理 矢場味仙 で台湾ラーメン食べたんですが、美味しかった!ただ、非常に辛かったです…! 愛知県長久手市にある Totopia Brewery というクラフトブルワリーにお邪魔しました。Hazy IPA のラインナップを推していて非常に香り高い一杯を提供してくれます。ずっと行きたかったので行けて最高でした! Noxphobia - TDH Hazy Double IPA 終わりに 名古屋Ruby会議05に参加してよかったです。滅多にできないことをやれましたし、何より勉強になるセッションが多く、気づきを得られました。 改めてですが、タイミーでは、世界中で開催される技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度があります。こちらご興味をお持ちいただけましたら、ぜひカジュアル面談にお申し込みください! product-recruit.timee.co.jp カジュアル面談のお申し込みはこちら
スタメンでCTOしております、 @tnir ( X: @takuya_stmn , note: @takuya_stmn ) です。 2023年9月、スタメン技術ブログで次の記事を公開しました。 ドメイン駆動設計は何を解決する手法なのか 記事では、TUNAG の技術的負債解消に向けてドメイン駆動設計(DDD)の導入を検討している、と書きました。その後の対外的な印象として、「スタメンは DDD を積極導入している」と読まれる場面がありました。 本記事の目的は二つです。 当時の検討表明と、いまの実装実態のあいだにある差分を正確に共有すること その後に行った小規模な試験導入(PoC)から得た、Rails 文脈での技術的・組織的な学びを共有すること 結論を先に述べます。私たちの設計の軸は ActiveRecord で、DDD はバックエンドのごく小さな範囲で試しました。その結果、ActiveRecord を軸にする方針を選んでいます。ただしこれは DDD全般の評価ではなく、私たちの文脈での記録です。小さく試し、合うものを残し、合わないものを手放すプロセス自体は、今でも有効なエンジニアリング判断だと考えています。 2023年の記事が書いていたこと 当時の記事の冒頭は、次のとおりです。 株式会社スタメンでは、企業エンゲージメント構築サービス TUNAG(ツナグ)の技術的負債解消と今後の持続的成長のため、ドメイン駆動設計(DDD)の導入を検討しています。 記事は実装報告ではなく、コアドメインを軸に「DDD が何を解決する手法なのか」を整理する解説でした。実装詳細は扱わない、とも明記しています。締めは、将来に向けた表明です。 つまり本文の事実は「導入を検討している」までであり、「すでに積極導入している」ではありません。ただし、解説の熱量や「邁進していきます」という結びが重なると、読後印象としては積極導入企業のように残りやすい面もありました。このギャップを、いま実装と検証結果に基づいて補正します。 当時の課題提起がなければ、机上の議論で終わっていた可能性もあります。検討を公開し、その後に実コードで検証まで進めたからこそ、本記事で共有するトレードオフが見えました。 何を、どこまで試したか 私たちのプロダクトは、大きくバックエンド(API)とフロントエンドに分かれます。 領域 DDD との関係 フロントエンド 導入なし バックエンド全体 教科書的な全面適用はなし。LoC ベースで全体の 1% を大きく下回る範囲で、部分的・実践的な試験のみ 試験の主例 社内教育・テスト(ナレッジテスト)領域の新規 API 見送り後の比較例 タスク依頼・ToDo 領域の新規 API 本記事では前者を 業務DXサービスA API 、後者を 業務DXサービスB API と呼びます。どちらも TUNAG 本体から独立した形で実装されたグリーンフィールドサービスのバックエンドです。 業務DXサービスA API は、MVC でくくると Model がおおよそ 2,000 LoC 前後、View + Controller がおおよそ数百 LoC 規模でした。期間は 2023年12月からの約3ヶ月です。ここでの DDD は、Evans 流の戦術パターンを全面適用したものではなく、不変条件が重要な箇所に限定した実用主義的な導入(DDD-lite としても呼ばれることがある)でした。設問・選択肢・合格判定・受験セッションなど、「壊れると困るルール」が比較的はっきりしている領域だったため、試験対象として適していました。 実コードでは、たとえば次が確認できます。 ドメイン層を ActiveRecord から分離した pure Ruby のオブジェクトがある 選択肢数・重複・合格ラインなどの不変条件を、生成時や操作時に検証する実装がある 一部の更新経路では、リポジトリ経由で集約を再構成してから永続化する 一方で、次の限界もありました。 単純更新・複製・一部セッション更新は ActiveRecord 直叩きのまま残り、層を迂回した 一部のドメインオブジェクトは、振る舞いに乏しいデータホルダーに近い ドメインイベントや、依存逆転を徹底した古典的構成までは採っていない つまり「DDD を名乗るに足る部分実装はあった」が、「プロダクト全体の標準アーキテクチャになった」わけではありません。 時系列は次のとおりです。 時期 内容 2023年9月 技術ブログで導入検討を表明 2023年12月〜約3ヶ月 業務DXサービスA API で部分的・実践的な DDD を試験導入 2024年3〜4月 PoC 振り返り。本格採用を見送り、業務DXサービスB API では採用しない方針を合意 2024年5月〜約3ヶ月 業務DXサービスB API を、DDD 非採用前提で初期開発 現在 フロント未導入。バックエンドでも局所試験の知見にとどまり、標準は ActiveRecord を軸にした設計 業務DXサービスB API は「DDD 導入の第2弾」ではありません。振り返りの結果、採らないと決めたあとの開発です。境界の分離や外部モデルの漏洩防止など、マイクロサービスとして当然の設計はありますが、それを DDD の戦術導入とは数えません。 なぜ本格採用を見送ったか — 技術的・組織的な理由 ここが本記事の本題です。「やらなければよかった」ではなく、「何が効き、何が合わなかったか」を残します。 1. 本来の課題と手法がずれた 2023年の検討の出発点は、TUNAG の技術的負債解消でした。ところが検証が進むにつれ、 手法の導入そのものが目的化しやすい 局面がありました。 負債解消で本当に解きたかったのは、たとえば次のようなことだと考えています。 変更影響の見通しを良くすること ドメイン知識をコード上で追跡しやすくすること チームが安全に改修できる速度を保つこと 一方で、層分け・用語・パターンの整備が進むと、「DDD らしく書けているか」が評価軸になりやすい。すると、負債解消や変更容易性への寄与より、導入の完成度が議論の中心になります。業務DXサービスA API での試験は、この目的化を本体から切り離して観測するためにも有用でした。振り返りの結論は明確で、 その時点のスタメンの文脈では、DDD の本格導入は TUNAG の技術的負債解消に適していない 、でした。 2. Rails / ActiveRecord との摩擦コストが高かった 業務DXサービスA API は Rails API です。ActiveRecord は永続化だけでなく、クエリ・バリデーション・ライフサイクル・関連の中心でもあります。ここに戦術的 DDD(集約の再構成、リポジトリ、pure ドメインモデル)を部分導入すると、少なくとも次のコストが発生しました。 二重モデル化 : DB 行としての AR と、ドメイン上のオブジェクトを往復させる変換が必要になる 経路の分岐 : 不変条件が強い操作だけドメイン経由、単純 CRUD は AR 直叩き、という二系統が同居する 認知負荷 : 「この変更はどの層に書くべきか」の判断自体が、機能追加のたびに発生する 複雑な採点や、正式版の訂正ルールのように、壊れ方が高い箇所では、pure なドメインオブジェクトに寄せる利点はありました。逆に、大半の更新が CRUD に近い画面・API では、変換と層分けのコストが、得られる安全性を上回りやすかった、というのが率直な感触です。 Rails way(責務を散らさない Model、明確な UseCase、必要なら concern)を主軸にした方が、少人数チームでの変更速度と学習コストのバランスが良かった、というのが本格採用見送りの技術的な中核です。 3. 効く範囲が、プロダクト全体のボトルネックと一致しなかった DDD が相対的に効きやすいのは、ルールが複雑で、不変条件の破壊がそのまま障害になる領域です。業務DXサービスA API でも、設問制約や採点のようにその種の箇所には部分適用の合理性がありました。 しかし TUNAG 本体を含むプロダクト全体の負債の主因が、「集約境界が無いこと」だけだったわけではありません。認証委譲、サービス分割、スキーマ契約、運用、データ整合、画面を含む変更伝播など、別種の複雑性の方が大きい局面も多い。戦術 DDD を局所に厚くしても、プロダクト全体の変更容易性が線形に改善するとは限らない、というのが PoC 後の整理です。 4. 小さく試せたから、大きく間違わずに済んだ 見送り自体を失敗とは捉えていません。新規の小さい API で約3ヶ月試し、振り返り、次の新規 API では使わないと決めた。この切れ方の方が、本体へ先に全面適用するより健全でした。 組織として得られた整理は、次です。 DDD は銀の弾丸ではない 導入を目的化すると、負債解消から離れる Rails 中心のプロダクトでは、戦術 DDD の全面適用より、ActiveRecord を軸に複雑箇所だけ設計を厚くする方が現実的なことが多い 手法の採否は、流行や完成度ではなく、事業成長と開発生産性(保守性・変更容易性を含む)で見る いまの設計方針 PoC 後の標準は、ActiveRecord を軸に置いています。UseCase(アプリケーションサービス)でユースケースの流れとトランザクション境界を担保しつつ、永続化モデル側は ActiveRecord 本来の表現力を活かす。そのうえで、モデルを無秩序に太らせないために、関心事ごとに concern へ閉じる、という形です。 これは「Fat Model に戻した」という意味ではありません。ドメイン層と AR を二重に持つコストを下げつつ、ユースケースのオーケストレーションは UseCase に残し、モデルに散らばりがちな関心事だけを concern で分割する、という選択です。Concern の濫用が神オブジェクトの隠蔽になりうることは認識したうえで、DDD-lite の二重モデルより、この分割の方がチームの認知負荷と変更速度に合う、と判断しています。 対外的な事実関係は、次で足ります。 2023年の記事は導入検討の表明であり、全面採用の報告ではない フロントエンドでは DDD を導入していない バックエンドでも、LoC ベースで全体の 1% を大きく下回る範囲での部分的・実践的な試験にとどまる 主例は業務DXサービスA API(2023年12月〜約3ヶ月) 2024年3〜4月の振り返りで本格採用を見送り、業務DXサービスB API(2024年5月〜約3ヶ月)では非採用前提で開発した 現在のスタメンを「DDD積極導入企業」と呼ぶのは実態に合わない 小さく検証し、合わなければ採用しない、という判断は今後も取りうる 手法の是非を好みで決めず、小さく試し、事業成長と変更容易性を含む生産性、および本来の課題(技術的負債解消)への寄与で採否を決める。業務DXサービスA API での試験と、業務DXサービスB API での見送りは、その一例です。 おわりに この記事は、過去のチャレンジ計画ブログ記事を消すためのものではありません。当時の検討や、その後の試験を否定するものでもありません。むしろ、2023年の記事が課題を言語化し、仮説を外に出したからこそ、机上の空論で終わらせず実コードでトレードオフを検証できました。話題の手法をスローガンで終わらせず、小さなプロダクトに落として確かめ、本格採用を見送った記録です。 エンジニアリングにおいて重要なのは、流行を追うことでも、一度選んだ手法に固執することでもありません。自社のプロダクト特性、フレームワークの前提、チームの認知負荷に照らして、効く設計を残し、効かない設計を手放せることです。今回の PoC は、その判断をコードと振り返りで一度通した事例だと考えています。 設計の話をするときも、採用の話をするときも、「スタメンは DDD を積極導入している」ではなく、「小さく試し、Rails / ActiveRecord を軸に UseCase と concern で整理する設計を主軸として選び直した」と読んでいただけると、実態に近いです。 今現在、主に採用しているシンプルな Rails Way に準じたアーキテクチャに関しては別の記事でコメントできればと思います。
動画
該当するコンテンツが見つかりませんでした












