バイセル Tech Blog

バイセル Tech Blogは株式会社BuySell Technologiesのエンジニア達が知見・発見を共有する技術ブログです。

バイセル Tech Blog

Railsの複数DB機能を利用した負荷の分散

はじめに

こんにちは。テクノロジー戦略本部 開発1部の酒井です。 今回は私たちが運用しているタイムレスオークションシステムの負荷対策で複数DBを導入したことについて紹介します。 タイムレスオークションシステムのバックエンドはRuby on Rails(以下、Rails)を基に開発されており、DBはAmazon Relational Database Service (RDS)を使ってます。DBの負荷に悩んでいる方や複数DBを検討している方の参考になれば幸いです。

背景

オークションシステムでは、入札が特にオークション終了間際に集中する傾向があります。そのため、以前はアクセスの集中する前にDBのスペックアップを目的として、手動でフェイルオーバーを行っていました。この手動でのフェイルオーバーは、人的なミスのリスクや、フェイルオーバーに伴うDBへの接続の瞬断がユーザーに影響を与える可能性がありました。

また、RDSのインスタンスはprimaryとreplicaの2台を運用していましたが、運用開始から今までreplicaはあまり活用されていませんでした。この問題を解決するため、今回Railsの複数DB機能を取り入れ、replicaを活用することでDBの負荷を分散させる試みを行いました。

Railsの複数DB機能とは

複数DBのサポートはRailsの6.0から標準機能として提供されているものです。 DBはreaderとwriterに分けられ、書き込みと読み込みで別のDBインスタンスを使うことになります。 アプリケーションがPOST、PUT、DELETE、PATCHのいずれかのリクエストを受け取ると、自動的にwriter DBに書き込みます。GETリクエストやHEADリクエストの場合はreplicaに送信します。

参考 Active Record で複数のデータベース利用

やったこと

まず初めにDBの追加です。 Railsがprimaryとreplicaを区別するため、replica側の設定にreplica: trueを追加しておく必要があります。

production:
  primary:
    <<: *default
    url: <%= ENV['DATABASE_URL'] %>
  primary_replica:
    <<: *default
    url: <%= ENV['REPLICA_DATABASE_URL'] %>
    replica: true ← 必須

次に、primaryとreplicaを自動で切り替えるための設定です。

こちらは、アプリケーション設定に以下の行を追加するだけで済みます。これにHTTPのメソッドを見て勝手にreplicaとwriterを切り替えてくれます。とても便利ですね。

config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session

しかし、上記設定だけだと以下のことに気をつけなければなりません。

  • 書き込みがあった直後に読み込みがあった場合どうなるのか
  • GETで書き込みをしているようなControllerがあった場合どうなるか

delay設定

まず、書き込みがあった直後に読み込みがあった場合どうなるのかについてです。

例えば、オークションで言うと入札があった直後にその金額を表示したい場合などがあります。

そういった場合にdelayの設定があります。設定自体は以下の一行を上記と同様にアプリケーション設定に追加するだけです。

# Railsではデフォルトで2秒に設定されます。
config.active_record.database_selector = { delay: 2.seconds }

この設定を入れることで、delayウィンドウの期間内であればGETリクエストやHEADリクエストをwriterに向けて送ることができます。

手動切り替え

そして、次にGETで書き込みをしているControllerがあった場合です。 基本的にそのような実装は避けるべきですが、Audit系のログをDBに書き込んでおきたい等、やむを得ない事情で書き込みをしていることはあると思います。この場合ActiveRecord::ReadOnlyErrorが発生してしまうため、手動でDBの切り替えを行う必要があります。具体的には、ApplicationRecordにてconnects_toメソッドでコネクション先を直接指定します。

ActiveRecord::Base.connected_to(role: :writing) do
  # このブロック内のコードはすべてprimaryに接続される
end

これは該当するところ1箇所ずつ対応していかなければなりません。しかし、私たちのチームではコードカバレッジ(C0)を100% に保ちながら開発しているため、手動切り替えが必要な箇所をある程度検知できました。

一方、それでも検知できない箇所がありました。手動切り替えの対象はライブラリも例外ではありません。実際にオークションでは認証機能としてdeviseを使用しています。

deviseの中でもGETで書き込みをしている箇所が存在し、手動切り替えの対象でした。

https://github.com/lynndylanhurley/devise_token_auth/blob/master/app/controllers/devise_token_auth/passwords_controller.rb#L47

この場合、対象となる箇所をオーバーライドをして、手動でコネクション先を直接指定することで解決できます。

# app/controllers/api/v1/auth/passwords_controller.rb

def edit
  ActiveRecord::Base.connected_to(role: :writing) do
    super
  end
end

成果

以前、primaryインスタンスの負荷はピーク時に80%近くまで上昇していましたが、現在は安定して30%程度に落ち着いています。その結果、これまで必要だった手動によるフェイルオーバーの実行が不要となり、データベースへの接続が途切れる心配もなくなりました。

しかし、replicaの方では負荷が高くなるという新たな問題が浮上しました。これまでDBのスペックを上げることでそれほど気にする必要がなかったのですが、接続先を分けたことにより、どこに負荷がかかっているかがより明確になりました。この問題に対処するため、replicaインスタンスの数を増やして負荷を軽減しました。

Before
Before

After
After

おわりに

いかがだったでしょうか。今回Railsの複数DB機能を入れたことでprimaryの負荷がかなり軽減されました。 これから複数DB機能を入れるか迷っている人の参考になれば幸いです!!

最後に、BuySell Technologiesではエンジニアを募集しています。興味がある方はぜひご応募ください!

herp.careers