こんにちは。ひさびさにみんな大好きPHPでの話を書きます。
お付き合いお願いします。
お題
こんなAPIがあったとします。
- 他のチーム、他人が公開している
- 実行後のレスポンスはすぐ返ってくる
- このAPIが向こう側で何をしているか詳しくは知らない
あなたはバッチで気軽にそのAPIをガンガン叩いています。
ところがそんなある日、API管理者から
「そんな無造作に使われると困るので、呼ぶのは秒間○○回までにしてください。」
と無慈悲に言われたとします。
政治的なアレもあり、どうやら呼び出し元が実行回数をコントロールしなくてはならなそうです。
さぁ、どうやって対応しますか?
というお題があった場合、自分だったらどう対応するかなぁと、ふと思い立ったのでその辺を書いてみます。
まず思いついた方法
まず最初に思いついた方法は以下になります。
以下のクラスを用意します。
- APIの実行回数を監視するクラス
- このクラスは監視メソッドを持つ
- 時刻はmicrotimeで扱う
- 自身が最初に呼ばれた時刻及び、呼ばれた回数を保持している
- API呼び出し直前に必ず呼んでもらう
そして、この監視メソッドは以下の処理を行います。
- 最初の命令から閾値となる秒数が過ぎていない
- かつ、監視していた実行数が許可された実行数を超えていたら
閾値 - 最初の計測から今回の処理までの実行間隔
の値でusleep
を挟む
イメージとしては制限を超えそうなら止めるという具合です。
といっても何いってんだこいつ?状態だと思うので、いちおう補足資料を以下に書きます。
フローチャート
フローチャートはこんな感じです。
(すいません、ちょっと見づらいですね)
イメージ
動作イメージを図に表すとこんな感じです。
閾値が1秒、制限時間内許可実行数が5回 (秒間5回の制限) の場合を例にします。
※ 例を極端にしています
ソースとテスト
ソース(sample_1/ExecutionController.php
)と、テスト(sample_1/ExecutionControllerTest.php
)はこうです。
だが、待ってほしい
これで一見うまくいっているように見えなくもないですが、1つ気をつけないといけない点があります。
閾値が1秒、制限時間内許可実行数が5回 (秒間5回の制限) の場合を例にします。
※ 例を極端にしています
- 6回目の監視時点で0.9秒しか立っていない場合、0.1秒のsleepを挟む
- 6回目の監視時点で0.1秒しか立っていない場合、0.9秒のsleepを挟む
ということになります。
裏を返すと 1回の処理が0.02秒 で終わってしまう処理でも通過することになります。
つまり、「秒間5回までとは言ったが、0.1秒で5回呼ばれるとは思わなかった!!」と言われる可能性もあるという点です。
※ 単位(sなのか、msなのか) によって許容範囲が変わるかもしれないという話です
あかんかもしれないパターンの図
図に表すとこんなイメージです。
処理を間引くパターン
上記の問題があったので、違うパターンを考えてみました。
以下のクラスを用意します。
- APIの実行回数を監視するクラス
- このクラスは監視メソッドを持つ
- 時刻はmicrotimeで扱う
- 自身が最後に呼ばれた時刻を保持している
- API呼び出し直前に必ず呼んでもらう
前述とほぼ一緒で、違うところは最後に呼ばれた時刻を保持することと、呼ばれた回数は保持しないというところです。
そして、この監視メソッドは以下の処理を行います。
- 最初の命令から閾値(マイクロ秒)以下で処理が実行されている場合、
閾値 - 前回の処理から今回の処理までの実行間隔
の値でusleep
を挟む
イメージとしては制限を超えないように処理を間引くという具合です。
フローチャート
フローチャートはこんな感じです。
イメージ
動作イメージを図に表すとこんな感じです。
ソースとテスト
ソース(sample_2/ExecutionController.php
)と、テスト(sample_2/ExecutionControllerTest.php
)はこうです。
特徴
閾値は、制限時間 / 制限時間内許可実行数
で求める。
1秒間に100回という制約の場合、 1 / 100 なので 0.01 となる。
監視している処理の実行間隔が上記の 0.01秒 以下で終わっている場合、1秒間に100回以上の処理が走ってしまう計算になる為、前回の処理から今回の処理までの実行間隔が0.01秒 以下で終わっている場合は、閾値 - 前回の処理から今回の処理までの実行間隔
の値で usleepを挟む。
※ 閾値が 0.01, 前回の処理から今回の処理までの実行間隔が 0.009 だとしたら 0.001秒 sleep をかける
要は x秒間にy回までの実行回数
にする為、均等に処理が実行されるということになる。
※ 1秒間に100回という制約の場合、処理間隔が 0.01 秒で収まる(ベストエフォート)
※ 100回を1秒間で均等に行うというイメージ
注意点
ただし注意点もあって、1秒間に1回という制限を設けた際に処理が0.01秒で終わるものでも、10回監視した場合は9秒以上処理に時間がかかるという事になります。
※ 例によって極端ですけど
その他
シングルトンは?
色々な場所から呼ぶAPIをコントロールしたい場合、シングルトンになっていると便利かと思います。
が、そんなときがもしあったらTrait
なりを用意してgetInstance()
的なメソッドを用意してあげればよいのではないでしょうか。
総括
というわけで、最初に考えたお題に対して2つの回答を用意してみました。
どれも一長一短あると思うので、
- 処理時間がある程度かかるもの、または、瞬殺で終わってもある程度許容できるものは前者のパターン
- 処理時間が瞬殺かつ、大量に呼ぶ必要があるものは後者のパターン
というように使い分けるのがいいのかなと個人的には思いました。
まぁ、よっぽどのことがない限りは後者のパターンで事足りるのではないかと。
(もちろん、他にいい方法はあると思います)
長くなってしまいましたが、現場からは以上です。
あ、いちおう免責としてこれを使ってなにかあっても責任は負えませんのであしからず。。