Cómo reintentar sin una tormenta
Un límite de uso es una regla por la que un servicio solo acepta cierto número de llamadas en poco tiempo. Un reintento es un segundo intento tras una llamada rechazada. Un reintento descuidado puede empeorar una sobrecarga.
El módulo sobre coste, trazas y fiabilidad reintenta una vez y luego pasa a una alternativa. Esta lección va más allá. Añade esperas que crecen y una dispersión, para que los clientes no se muevan juntos. Añade también un tope de intentos y un reintento que se puede repetir sin riesgo.
Al final de esta lección
Nota
Puedo reintentar una llamada sin causar una tormenta.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Límite de uso | el máximo de llamadas que un servicio acepta en una ventana de tiempo | |
| Espera creciente | una espera que crece tras cada intento fallido | 400, 800, 1600 |
| Dispersión | una pequeña espera distinta para cada cliente | |
| Tormenta | muchos clientes que reintentan en el mismo instante | |
| Idempotente | seguro de repetir: hacerlo dos veces tiene el efecto de una | |
| Reloj simulado | un número que movemos a mano en vez de esperar |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Cómo cuenta un límite de uso
El servicio corta el tiempo en ventanas de 1000 milisegundos. En cada ventana acepta un número fijo de llamadas, aquí 2. Rechaza el resto hasta que empieza la ventana siguiente.
El tiempo aquí es un número que movemos a mano. Nada espera de verdad, así que cada ejecución da la misma respuesta.
Ejemplo resuelto: 2 llamadas por ventana
| Hora (ms) | Ventana | Llamadas ya aceptadas | Resultado |
|---|---|---|---|
| 0 | 0 | 0 | Aceptada |
| 0 | 0 | 1 | Aceptada |
| 0 | 0 | 2 | Rechazada, la ventana está llena |
| 400 | 0 | 2 | Rechazada otra vez |
| 1200 | 1 | 0 | Aceptada, ventana nueva |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Un error frecuente
Advertencia
Mucha gente cree que reintentar al instante es lo más rápido para conseguir una respuesta. Si el servicio está sobrecargado, cada reintento inmediato añade carga y el servicio empeora para todos. Espera primero, y espera más cada vez.
Espera creciente y dispersión
Una espera creciente se duplica. Con una base de 400, las esperas son 400, 800, 1600. Ponle un tope, aquí 2000, o las esperas crecen sin fin.
Si seis clientes fallan juntos y todos esperan 400, todos vuelven juntos. Una dispersión suma un pequeño número distinto a cada cliente, y así vuelven uno a uno.
Ejemplo resuelto: base 400, tope 2000
| Intento fallido | Espera | Dispersión de la petición 3 | Siguiente intento en (desde 0) |
|---|---|---|---|
| 1 | 400 | 34 | 434 |
| 2 | 800 | 47 | 1281 (desde 434) |
| 3 | 1600 | 10 | 2891 (desde 1281) |
| 4 | 2000, el tope | No se usa | Se rinde si los intentos son 4 |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Un reintento debe poder repetirse sin riesgo
A veces una llamada funcionó pero la respuesta se perdió por el camino. El cliente cree que falló y lo intenta otra vez. Si el reintento cobra o envía algo de nuevo, lo has hecho dos veces.
Dale una clave a cada acción. El servidor recuerda las claves que ha visto. Una repetición con una clave ya vista no hace nada nuevo.
Escribe el ayudante del modelo, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Los tokens de una respuesta, o null si no trae ninguno.' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el ayudante del modelo, parte 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(\"estado \" + res.status);' ' const d = await res.json();' ' return { text: d.choices[0].message.content, inTokens: count(d, \"prompt_tokens\"), outTokens: count(d, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"Muestra grabada: ningún modelo respondió (\" + error.message + \").\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de reintentos, parte 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"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de reintentos, parte 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"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el script de reintentos, parte 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 ? \"con dispersión\" : \"sin dispersión\") + \": máximo de reintentos en un milisegundo \" + worst);' ' console.log(\" la petición 3 probó en \" + h[3].join(\" \") + \", la 5 en \" + h[5].join(\" \"));' ' }' ' const reply = await ask([{ role: \"user\", content: \"En una frase corta, ¿por qué añadir una espera aleatoria a un reintento?\" }], \"Para que muchos clientes no reintenten a la vez y no vuelvan a sobrecargar el servicio.\", 60);' ' console.log(\"el modelo dijo: \" + reply.text);' '}' 'main();' >> retry.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Ejecuta los reintentos
docker exec -w /home/node/project agent node retry.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Lo que imprime el Run
| Línea | Qué muestra |
|---|---|
| sin dispersión: máximo de reintentos en un milisegundo 4 | Cuatro clientes volvieron en el mismo instante |
| la petición 3 probó en 0 400 1200, la 5 en 0 400 1200 2800 | Sin dispersión las horas son ordenadas e iguales |
| con dispersión: máximo de reintentos en un milisegundo 1 | Ningún par de reintentos comparte instante |
| la petición 3 probó en 0 434 1281 2891, la 5 en 0 448 1259 2883 | Cada cliente espera algo distinto |
| el modelo dijo: ... | Una frase del laboratorio. Una muestra grabada si ningún modelo responde |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.