🥦

Railsサーバーのリクエスト先を重み付きで振り分けた話

2024/11/29に公開

はじめに

WED株式会社でバックエンドエンジニアをしているbroccoli1002です。
レシート買取アプリ「ONE」では、 ユーザーが撮影したレシート画像からOCR(文字認識)を行い、 購入店舗や商品名、価格といった情報を構造化してデータベースに保存しています。
アプリケーションサーバーから社内のOCRサーバーにリクエストを送る仕組みが存在します。

今回の記事では、新規OCRサーバー導入時に、アプリケーション側でリクエストを重み付きで振り分ける仕組みを作った際の 工夫についてご紹介します。

課題

新しいOCRサーバーを導入するにあたり、既存OCRサーバーから新OCRサーバーへリクエストを切り替える必要がありました。
しかし、いきなり全リクエストを新OCRサーバーに送ると、万一問題が発生した場合の影響が大きいため、
旧サーバーと新サーバーの双方に対して比率を調整しながらリクエストを振り分ける仕組みを導入することにしました。

対応方法

リクエスト振り分けを実現するために、以下の対応を行いました。

振り分け説明図

新OCRサーバーでは画像処理に用いるモジュールを選べるようになっており、
実際には「旧OCR」「新OCR/モジュールA」「新OCR/モジュールB」のように3つ以上から抽選できるような仕組みが必要でした。
ただ、今回は説明のため簡略にし、新旧サーバーの2つのみを対象にしています。

  1. 新旧サーバー用クライアントモジュールを用意
  2. Railsの設定に重みを追加
  3. 重みに基づく抽選処理を実装

1. 新旧サーバー用クライアントモジュールを用意

新OCRサーバー用と旧OCRサーバー用の2つのクライアントモジュールを作成しました。
それぞれのモジュールは特定のOCRサーバーにリクエストを送信する責務を持っています。
この分離により、異なるサーバー仕様や要件に柔軟に対応できる設計となっています。

2. Railsの設定に重みを追加

Railsのconfigに各サーバーへのリクエスト比率を設定できる仕組みを追加しました。
この設定により、新旧サーバーへのリクエスト振り分け比率を動的に調整可能です。
初期段階では新サーバーへのリクエスト比率を低く設定し、 徐々に比率を高めて移行を進めることができます。

3. 重みに基づく抽選処理を実装

設定された重みに基づいて、 リクエスト時にどちらのサーバーを選択するかをランダムに決定する抽選処理を実装しました。
抽選処理により、リクエストが設定された比率に基づいて分散されるようになっています。


これらの仕組みにより、OCRの実行時にクライアントモジュールが呼び出されると、 内部で重みに基づいた抽選が行われ、適切なサーバーへリクエストが送られます。
比率を調整する際にはconfigを変更するだけで対応可能です。

振り分け処理の内容詳細

仕組み

例えば、新OCRサーバーへのリクエストを30%、旧OCRサーバーへのリクエストを70%に設定する場合、次のように設定します:

[新OCRサーバー, 3]
[旧OCRサーバー, 7]

この設定をもとに、内部では以下のような処理が行われます。
全てのコードは載せられませんが、一部を抜粋してご紹介します。

class OcrClient
  cattr_reader :clients, default: []
  cattr_accessor :dice, default: 0

  # クライアントの登録
  def self.register_client(client, weight)
    self.dice += weight
    clients << [client, dice]
  end

  # 重み付き選択
  def self.pick_client
    cast = rand(dice)
    clients.bsearch { |_, threshold| cast < threshold }.first
  end

  # …
end

上記のコードの結果、以下のようになります。

clients = [
[新OCRサーバー, 3],
[旧OCRサーバー, 10] # (3+7として加算値を設定)
]
dice = 10 # 0~9の乱数を生成

抽選時には、diceを振り、diceの値を超える最初の要素を当選とします。
diceの値による振り分けの結果は、以下のようになります:

0 | 1 | 2 => 新OCRサーバー: 確率(3/10)
3 | 4 | 5 | 6 | 7 | 8 | 9  => 旧OCRサーバー 確率(7/10)

乱数を生成し、生成された乱数値をもとに要素を選択することで、設定された比率に従った振り分けを実現しています。

動作確認のためのRSpec

specでは新OCRサーバーの比率を8、旧OCRサーバーの比率を2に設定しました。
設定後、特定回数抽選を行い、新OCRサーバーが選ばれた回数をカウントします。

実際には、リクエストが適切に振り分けられているかを確認するために、抽選を1000回実行。
理論上は約800回選ばれることが予想されますが、乱数によるばらつきも考慮し、700回から900回の範囲を許容範囲として設定しました。

テストはCIで通過し、設定した比率が正しく動作していることを確認できました。
所要時間は約3秒程度でした。

他により良い方法があるかもしれませんが、
記載した方法でRSpecを記載し、動作確認を行いました。

余談

Ruby本体でも「重み付き選択」の要望が過去に議論されていました。
興味のある方は以下のIssueをご覧ください:
https://bugs.ruby-lang.org/issues/4247

最後に

今回は、 アプリケーションからOCRサーバーへのリクエストを重み付けで振り分ける 仕組みについて解説しました。
重み付けで振り分ける仕組みにより、新旧サーバーの移行時も影響を抑えながら段階的に切り替えができるようになりました。

今後も設計や実装で工夫した内容を共有していきたいと思います。
最後までお読みいただきありがとうございました!

WED Engineering Blog

Discussion