嵐を起こさずに再試行する
レート制限とは、サービスが短い時間に受け付ける呼び出しの数の上限です。再試行とは、断られた呼び出しをもう一度試すことです。考えなしの再試行は、過負荷をもっと悪くします。
コスト、トレース、信頼性のモジュールでは、一度再試行して、代わりの手に移りました。このレッスンは、さらに進みます。待ち時間を増やし、ばらつきを足し、試行の数を限り、何度やっても安全な再試行にします。
このレッスンのゴール
メモ
嵐を起こさずに、呼び出しを再試行することができる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| レート制限 | 時間の窓に、サービスが受け付ける最大の呼び出し数 | |
| バックオフ | 失敗するたびに、長くなっていく待ち時間 | 400、800、1600 |
| ばらつき | 呼び出しごとに少しずつ違う待ち時間 | |
| 嵐 | 多くの呼び出しが、同じ瞬間に再試行すること | |
| 冪等 | 二回やっても、一回の効果と同じで安全なこと | |
| 模擬の時計 | 待つ代わりに、手で進める数 |
学生が入力するセルは、このプレビューでは空白です。
レート制限の数え方
サービスは、時間を 1000 ミリ秒の窓に区切ります。それぞれの窓で、決まった数、ここでは 2 回を受け付けます。残りは、次の窓が始まるまで断ります。
ここでの時間は、手で進める数です。何も待たないので、毎回同じ答えになります。
例: 窓ごとに 2 回
| 時刻(ms) | 窓 | 受け付けずみ | 結果 |
|---|---|---|---|
| 0 | 0 | 0 | 受け付ける |
| 0 | 0 | 1 | 受け付ける |
| 0 | 0 | 2 | 断る。窓がいっぱい |
| 400 | 0 | 2 | また断る |
| 1200 | 1 | 0 | 受け付ける。新しい窓 |
学生が入力するセルは、このプレビューでは空白です。
よくある間違い
注意
すぐに再試行するのが、答えを得る最短の道だと考える人が多いです。サービスが過負荷なら、すぐの再試行は負荷を足すだけで、みんなにとって悪くなります。まず待ち、そのたびに長く待ちます。
バックオフとばらつき
バックオフは、倍になる待ち時間です。base が 400 なら、待ち時間は 400、800、1600 です。上限をつけ、ここでは 2000 にします。つけないと、待ち時間は際限なく伸びます。
6 つの呼び出しが同時に失敗して、どれも 400 待つと、同時に戻ってきます。ばらつきは、呼び出しごとに少し違う数を足すので、一つずつ戻ります。
例: base 400、上限 2000
| 失敗した試行 | バックオフ | リクエスト 3 のばらつき | 次の試行の時刻(0 から) |
|---|---|---|---|
| 1 | 400 | 34 | 434 |
| 2 | 800 | 47 | 1281(434 から) |
| 3 | 1600 | 10 | 2891(1281 から) |
| 4 | 2000、上限 | 使わない | tries が 4 ならあきらめる |
学生が入力するセルは、このプレビューでは空白です。
再試行は、繰り返しても安全にする
呼び出しは成功したのに、答えが途中で消えることがあります。呼び出し側は失敗と思って、もう一度試します。再試行がまた課金したり送ったりすると、二回してしまいます。
すべての操作に、鍵をつけます。サーバーは、見た鍵を覚えます。見た鍵での繰り返しは、新しいことを何もしません。
助っ人 model.js を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// 応答のトークン数。なければ null。' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
助っ人 model.js を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' const data = await res.json();' ' const text = data.choices[0].message.content;' ' return { text, inTokens: count(data, \"prompt_tokens\"), outTokens: count(data, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"記録済みサンプル: モデルが答えませんでした (\" + error.message + \")。\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
再試行のスクリプトを書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'function delay(attempt, base, cap) { return Math.min(base * Math.pow(2, attempt - 1), cap); }' 'function spread(id, attempt) { return (id * 7 + attempt * 13) % 50; }' 'function run(count, limit, window, base, cap, tries, spreadOn) {' ' const pending = [];' ' const history = {};' ' const counts = {};' ' for (let id = 1; id <= count; id++) { pending.push([0, id, 1]); history[id] = []; }' > retry.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
再試行のスクリプトを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' while (pending.length > 0) {' ' pending.sort(function (a, b) { return a[0] - b[0] || a[1] - b[1] || a[2] - b[2]; });' ' const item = pending.shift();' ' const slot = Math.floor(item[0] / window);' ' history[item[1]].push(item[0]);' ' counts[slot] = counts[slot] || 0;' ' if (counts[slot] < limit) { counts[slot] = counts[slot] + 1; continue; }' ' if (item[2] < tries) {' ' const extra = spreadOn ? spread(item[1], item[2]) : 0;' ' pending.push([item[0] + delay(item[2], base, cap) + extra, item[1], item[2] + 1]);' ' }' ' }' ' return history;' '}' >> retry.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
再試行のスクリプトを書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' for (const on of [false, true]) {' ' const h = run(6, 2, 1000, 400, 2000, 4, on);' ' const seen = {};' ' let worst = 0;' ' for (const id in h) h[id].slice(1).forEach(function (t) { seen[t] = (seen[t] || 0) + 1; worst = Math.max(worst, seen[t]); });' ' console.log((on ? \"ばらつきあり\" : \"ばらつきなし\") + \": 同じ 1 ミリ秒での再試行の最多 \" + worst);' ' console.log(\" リクエスト 3 の試行 \" + h[3].join(\" \") + \"、リクエスト 5 は \" + h[5].join(\" \"));' ' }' ' const reply = await ask([{ role: \"user\", content: \"短く一文で答えてください。再試行の待ち時間に、ばらつきを足すのは、なぜですか。\" }], \"多くの呼び出しが同じ瞬間に再試行して、サービスをまた過負荷にしないためです。\", 80);' ' console.log(\"モデル: \" + reply.text);' '}' 'main();' >> retry.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
再試行を動かす
docker exec -w /home/node/project agent node retry.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
Run の出力
| 行 | 意味 |
|---|---|
| ばらつきなし: 同じ 1 ミリ秒での再試行の最多 4 | 4 つの呼び出しが、同じ瞬間に戻った |
| リクエスト 3 の試行 0 400 1200、リクエスト 5 は 0 400 1200 2800 | ばらつきがないと、時刻が整って同じになる |
| ばらつきあり: 同じ 1 ミリ秒での再試行の最多 1 | 同じ瞬間に重なる再試行はない |
| リクエスト 3 の試行 0 434 1281 2891、リクエスト 5 は 0 448 1259 2883 | 呼び出しごとに待ち方が少し違う |
| モデル: ... | ラボからの一文。モデルが答えなければ記録済みサンプル |
学生が入力するセルは、このプレビューでは空白です。