httr2::req_perform_parallel() の throttling がトークンバケット方式になった。

2025/03/12に公開

httr2 パッケージは R で HTTP リクエストを扱うためのパッケージです。ちなみに、「httr」は何と読むか知っていますか? 実はこれは「ヒッター」と発音します。hex sticker が野球のバッターのイラストになっているのはそのためです。

https://httr2.r-lib.org/index.html

この httr2 パッケージは、先週リリースされた v1.1.1 では、HTTP リクエストを並列に実行する関数 req_perform_paralell() にいくつかの機能追加が行われました。今回はそれについて少し紹介します。

参考:req_perform_*() の使い分け

req_perform_paralell() について紹介する前に、httr2 にはどのような関数が用意されているかを見てみましょう。大きく分けると、単一のリクエスト用のものと複数のリクエスト用のものがあります。

単一のリクエストを実行する関数

こういう感じで使います。

req <- request(...) |>
  ...

resp <- req |>
  ...
  req_perform()
  • req_perform(): 単一のリクエストを実行する基本の関数。迷ったらこれ。
  • req_perform_connection(): レスポンスをストリームとして扱える関数。LLM の API を使うならこれ(Server-sent event もハンドルできる)。
  • req_perform_promise(): レスポンスを非同期に扱う関数。

複数のリクエストを実行する関数

こういう感じで使います。

base_req <- request(...) |>
  ...

reqs <- lapply(1:10, \(x) base_req |> ...)

resps <- reqs |>
  ...
  req_perform_parallel()
  • req_perform_sequential(): 複数のリクエストを1つづつシーケンシャルに実行する関数。
  • req_perform_parallel(): 複数のリクエストを並列で実行する関数。
  • req_perform_iterative(): 前のレスポンスを元に次々にリクエストを実行していく関数。pagination がある API などに使うと便利。

req_perform_parallel() の使い方

req_perform_parallel() は、必ず req_throttle() と一緒に使うこと、とドキュメントに注意書きされています。

Never use it without req_throttle(); otherwise it's too easy to pummel a server with a very large number of simultaneous requests.

この関数は、許されるなら許されるだけ並列度を上げてリクエストを実行するので、その並列度に何の制限も書けていないと、とんでもない負荷がサーバーにかかってしまう危険があります。(そもそも並列度を上げると負荷がまずそうなリクエストは、req_perform_sequential() で実行した方が安全でしょう)

ちょっと分かりづらい点は、req_throttle() は個別のリクエストに適用します。つまり、こんな感じではなく、

base_req <- request(...)

resps <- lapply(1:10, \(x) base_req |> ...) |>
  req_throttle(...) |>
  req_perform_parallel()

こういう感じのコードを書くことになります。

base_req <- request(...) |>
  req_throttle(...)  

resps <- lapply(1:10, \(x) base_req |> ...) |>
  req_perform_parallel()

req_throttle() の使い方

さて、いよいよ req_throttle() の使い方です。v1.1.0 までは、この関数は rate を指定するものでした。

base_req <- request(...) |>
  req_throttle(rate = 30 / 60)  

これは、実際には 1 回リクエストを実行した後、次のリクエストまでの間を 1 / rate 秒間あけるようにスリープする、という実装になっていました。つまり、上のコードでは、最大2秒間づつスリープします。

それが、v1.1.1 からは以下のような指定をするように変わりました。

base_req <- request(...) |>
  req_throttle(capacity = 30, fill_time_s = 60)

これは、トークンバケットという方式を採用したからです。トークンバケットについてはこの記事が分かりやすかったです。

https://zenn.dev/sota_yamaguchi/articles/a0b7d22daa921d

上の指定だと、60秒(fill_time_s)ごとに 30 回(capacity)のリクエストができます。というのは、60秒間に30回リクエストするのも、1秒間に30回リクエストするのも許されます。もし合計のリクエストが 31回だったなら 31回目は1分後を待たなくてはいけないので、rate での指定とあまり変わりませんが、リクエストのバーストによって rate より速くなるケースが多いです。

逆に言うと、バーストによって並列度が一気に上がるので、rate と同じ感覚で設定するとサーバーに過剰な負荷がかかる可能性があります。気を付けながらチューニングしましょう。

参考: curl::multi_*()

ちなみに、並列のリクエストは curl パッケージが並列実行の機能を提供することによって成り立っています。もし httr2 を使わずもっとローレベルな処理を実装したい場合は、このあたりの関数を自分で使えばよさそうです。

https://jeroen.r-universe.dev/curl/doc/manual.html#multi

感想

こういう処理は httr パッケージのインターフェースではおそらく実現できなかったと思います。多少のとっつきづらさはありますが、httr2 パッケージという別パッケージをわざわざ作り直したのも納得だなあ、とだんだん感じることが増えてきました。httr2 パッケージについての日本語の記事をほとんど見かけないので、応援の意味も込めて記事を書いてみました。と言いつつ、自分ではまだ活用できていないのですが...

Discussion