Ir al contenido

Blog ·

Dos pedidos, una máquina

Un servidor dedicado se vende exactamente una vez. Bloquear todo el conjunto de candidatas costó en silencio una reserva, y la cura fue bloquear de una en una.

Por Provibr

Un plan de VPS vende de más a propósito y a nadie le quita el sueño. Una máquina dedicada no puede: hay una, y si dos pedidos creen ambos que se la han llevado, un lunes por la mañana un cliente oirá que aquello que pagó el viernes pertenece a otra persona.

El requisito ocupa una frase. Reservar antes de los efectos secundarios y devolver capacidad de forma transaccional tras una caída o un fallo. Construirlo produjo dos mediciones que cambiaron el diseño, y una de ellas costó una máquina que estaba ahí, libre.

El árbitro es un índice

La regla de que dos pedidos nunca pueden retener la misma máquina vive en un índice único parcial sobre el identificador del equipo, restringido a las reservas en estado retenida. No en la aplicación, no en un bloqueo que tome el código, no en un comentario.

Esa es a propósito la misma forma que el índice de ocupación de unidades de bastidor. La capa de aplicación produce un rechazo legible antes de la colisión, y la base de datos decide quién gana de verdad. Solo una de las dos puede ser la autoridad, y tiene que ser la que ningún camino de código añadido el año que viene pueda esquivar.

Retenida es el único estado que retiene algo, y eso se expresa como tabla junto al índice y no como columna booleana, porque un booleano al lado del estado es una segunda verdad que solo es correcta mientras todos los escritores se acuerden. Una prueba compara la tabla con el índice.

Hay cuatro estados y dos de ellos forman pareja a propósito: retenida, convertida, caducada, cancelada. La simplificación tentadora es un único liberada para las dos últimas, y dejaría sin respuesta la pregunta de si alguien la anuló o si simplemente quedó ahí, que es justo la pregunta que llega cuando un cliente llama por un pedido que no salió adelante. Un barrido que escribe en silencio el mismo estado que una persona borra la diferencia entre una decisión y un plazo vencido.

Bloquear todo el conjunto costó una reserva

La primera versión seleccionaba las máquinas candidatas y las bloqueaba en una sola consulta con FOR UPDATE SKIP LOCKED, saltándose lo que ya estuviera tomado. Se lee bien y está mal, y solo lo mostró una prueba de concurrencia.

Ocho pedidos simultáneos sobre tres máquinas aptas deberían producir tres reservas. Produjeron dos. La primera transacción agarró las tres filas, usó una y retuvo las otras dos hasta confirmar; todos los demás pasaron de largo por las tres y no encontraron nada. El conjunto no estaba agotado: estaba secuestrado.

La cura es bloquear una candidata cada vez: toma la primera, y si otra persona la tiene, pasa a la siguiente. La misma prueba de nuevo: tres reservas sobre tres máquinas distintas y cinco rechazos honestos. Dos pedidos sobre una sola máquina dan una reserva y un rechazo.

Esa cura trae su propio riesgo, que es la longitud del bucle. Con cien pedidos simultáneos, la última transacción recorrería cien candidatas antes de ganar, y una transacción larga entretiene a todas las demás. Por eso el recorrido se detiene en veinticinco candidatas, y por encima de ahí la respuesta honesta es «vuelva a intentarlo en un momento».

Conviene notar que saltar y fallar se cuentan por separado. Una candidata retenida por otra persona se salta, y eso no es un intento, porque no ha ido nada mal. Una candidata que perdió una carrera de inserción sí es un intento, y de esos hay como mucho cinco.

Una violación de unicidad envenena la transacción

Esta es la trampa alrededor de la cual está construido el bucle de reintento, y es fácil escribir código que parece correcto y se cae en la primera colisión de verdad.

Dentro de una transacción de PostgreSQL, una violación de clave única aborta la transacción. Toda instrucción posterior se rechaza entonces con otro error, «la transacción actual está abortada», hasta que alguien deshace. Así que un bucle que captura la violación e intenta la siguiente candidata no funciona en absoluto: la inserción de la segunda candidata falla por un motivo que nada tiene que ver con esa segunda candidata.

Cada intento corre por tanto dentro de un punto de guardado, que en nuestro constructor de consultas es sencillamente una transacción anidada. Deshacer hasta el punto de guardado, la transacción exterior vuelve a estar sana, siguiente candidata.

Hay un detalle relacionado sobre cómo se captura la violación. Nuestro constructor de consultas envuelve los errores del controlador en un tipo propio y cuelga el auténtico de la causa, así que comprobar un código en el nivel superior no ve nada. Hay que recorrer la cadena de causas, y hay que mirar además el nombre de la restricción, porque esta tabla tiene dos restricciones únicas y significan cosas opuestas: una dice que la máquina está tomada, la otra dice que este pedido ya tiene una reserva y aquí está.

El rechazo tiene que decir de qué clase es

El asignador construye la lista de candidatas dos veces, y la diferencia entre ambos resultados es un mensaje para un cliente.

La primera pasada es la lista completa, con un motivo para cada máquina que no cumplió: sede equivocada, fabricante equivocado, no lista, y así. La segunda pasada es lo que realmente se pudo bloquear en ese instante. Si la lista completa dio una ganadora y la bloqueable no, entonces sí había algo que vender y alguien llegó una fracción de segundo antes. Esa es una frase distinta de «no hay nada»: una significa vuelva a intentarlo, la otra significa compre más hierro. Merecen botones distintos.

El motivo congelado de la máquina elegida va a la fila de la reserva, porque un mes después cada equipo está en otro estado y ya no se puede reconstruir por qué el asignador escogió esta. Entonces la fila lo dice por sí misma.

La clave de idempotencia se consulta dos veces, y esa segunda vez es otra cosa que solo encontró una prueba de concurrencia. Cinco peticiones simultáneas con la misma clave, sobre cuatro máquinas, produjeron una reserva y cuatro rechazos, incluido uno de una transacción que no pudo bloquear nada y por tanto nunca llegó a la inserción ni a la rama de repetición, mientras existía sin más una reserva con su clave. Preguntar otra vez justo antes de rechazar lo arregla, y funciona porque en nuestro nivel de aislamiento cada instrucción toma una instantánea nueva, así que un hermano recién confirmado sí es visible.

La garantía que sobrevive a todo esto es estrecha y merece escribirse tal cual: cinco peticiones con la misma clave producen una reserva y cuatro veces «alguien fue más rápido». Quienes pierden todavía no ven a quien gana, porque en ese instante aún no había confirmado. No hay versión de eso que no implique esperarse unos a otros, y esperar convierte cada minuto de mucho tráfico en una cola.

La reserva se convierte en el vínculo

Cuando el pedido se completa, la máquina tiene que pasar de reservada a vendida. La implementación evidente libera la reserva y luego escribe el vínculo de servicio, y contiene una ventana de unos milisegundos en la que la máquina está libre. En un día de mucho tráfico, esa ventana se encuentra.

Por eso la conversión escribe el vínculo y pasa la reserva a convertida en una sola transacción, y la fila sigue retenida hasta que el vínculo existe. En cada instante de esa transacción, un segundo pedido ve o bien la reserva o bien el vínculo, y ambos lo excluyen. No hay instante libre porque ninguna instrucción crea uno. Es la misma forma que ascender una reserva de bastidor a ubicación, una capa más arriba.

El barrido es una limpieza, no un guardián

Una reserva caduca a los quince minutos por omisión, y un trabajador barre las caducadas cada minuto.

Ese número no son las cuatro horas que recibe una reserva de bastidor, y la diferencia está en qué espera cada una. Una reserva de bastidor espera a una persona con un carro: alguien tiene que ir a la sala, poner raíles, cambiar cables. Una reserva de máquina espera a una compra: pantalla de pago, control de fraude, confirmación. Mientras tanto, el inventario más caro que tiene un proveedor no se puede vender. Quince minutos es generoso para un pago interrumpido que alguien retoma, y lo bastante corto para que una tormenta de pedidos abandonados no vacíe el estante medio día.

Lo importante es dónde se aplica el plazo. Se comprueba dentro de la conversión, no en el barrido. Si el barrido fuera el único sitio, una reserva caducada entre dos pasadas todavía se convertiría, y un guardián que pasa una vez por minuto no guarda nada. El barrido deja la administración en orden y devuelve la máquina al inventario; eso es trabajo de limpieza.

Además no tiene interruptor de funcionalidad, y es una excepción deliberada. Hay un interruptor de despliegue sobre todo el módulo de centro de datos, y repetirlo aquí significaría que apagar el módulo deja máquinas reservadas para siempre por pedidos que ya nadie puede ver. Un interruptor cuyo efecto secundario es desactivar un plazo no es un interruptor: es una fuga.

Lo que anotaríamos

  1. Ponga el árbitro en la base de datos. La aplicación debe producir el rechazo legible; el índice debe decidir quién gana.
  2. Bloquee de una candidata en una. Bloquear un conjunto retiene filas que no va a usar, y el síntoma es una venta perdida y no un error.
  3. Limite el recorrido. Bloquear por candidata cambia un conjunto secuestrado por un bucle largo, y el bucle también necesita un límite.
  4. Una violación de unicidad aborta la transacción. Reintentar sin punto de guardado produce un error que nombra lo que no es.
  5. Mire el nombre de la restricción. Una tabla suele tener varias unicidades, y para el usuario significan cosas distintas.
  6. «No hay nada» y «alguien fue más rápido» son dos mensajes. Construir la lista de candidatas dos veces es la forma de distinguirlos.
  7. Ascienda en lugar de liberar y reescribir. Cualquier secuencia con un instante libre acaba encontrándose.
  8. Aplique el plazo en el punto de uso. Un barrido periódico es tarea doméstica, y una tarea doméstica no es una regla.

Para dejar claro qué es esto: el trabajo de logística de centro de datos está detrás de un interruptor de despliegue apagado por omisión, con un ajuste por organización encima, así que nada de esto se puede comprar hoy. Dos cosas están sin demostrar. El barrido de caducidad se midió como función y no como trabajador en marcha. Y no se intentó una petición cruda contra las acciones de servidor con el interruptor cerrado.

Del trabajo de plataforma de Provibr. Ese mismo ascender en vez de reescribir, aplicado a unidades de bastidor, está en la reserva se convierte en la ubicación, y por qué las unidades libres de un bastidor no son lo mismo que capacidad vendible está en el espacio en rack no es capacidad.

Empiece con un host y un agente

Cree una cuenta, instale el agente con un solo comando y conecte su primer host de Proxmox. El periodo de prueba dura 14 días y no pide ningún dato de pago.