SkillGridAprende informática practicando

Consistencia: lo que cedes cuando los datos viven en dos sitios

CAP como un compromiso real y no como un eslogan, consistencia fuerte frente a eventual, el retraso de réplica y leer tus propias escrituras, y por qué los reintentos obligan a que toda escritura sea idempotente.

17 min · 55 XP

La lección anterior terminó con una nota incómoda. Añadiste réplicas de lectura para aguantar más tráfico, y lo primero que compraste fue una segunda copia de la verdad que puede contradecir a la primera. Esta lección trata de esa contradicción: cuándo ocurre, cuánto cuesta y cómo evitar que llegue al usuario en forma de fallo.

En el momento en que los datos existen en dos máquinas aparece una pregunta que antes no existía: ¿qué copia es la correcta? En una máquina la respuesta es trivial, porque solo hay una. En dos máquinas tienes que decidir dos cosas separadas. Primera: qué puede devolver una lectura mientras una escritura aún se está propagando. Segunda: qué hacer cuando las máquinas no pueden hablarse en absoluto. Ambas decisiones tienen costes reales, y ninguna se evita eligiendo mejor hardware. De esto se discute bajo el nombre de CAP, así que merece la pena entender la discusión con exactitud.

Probablemente hayas oído CAP como elige dos de tres: Consistency, Availability, Partition tolerance. Esa versión se recuerda, se repite en todas partes y engaña de una forma que lleva a malas decisiones. Leída al pie de la letra, sugiere que puedes elegir consistencia y disponibilidad y simplemente saltarte la tolerancia a particiones. No puedes. Una partición no es un ajuste que rechazas; es un cable de red arrancado, un switch que se reinicia, un enlace entre centros de datos saturado, una pausa del recolector de basura lo bastante larga para que la otra máquina se rinda. Si tu sistema corre en más de una máquina conectada por una red, habrá particiones, y la tolerancia a particiones no está en el menú.

ClientNode ANode Bthe link between them is downwrite balance = 90acceptedA has 90, B still has 100read my balance100B could not have known about the writeno partition means no tradeoff at all

Fíjate en lo que cuesta de verdad cada rama, porque fallan de maneras completamente distintas. Negarse a responder es ruidoso: el usuario ve una página de error, el botón de pago devuelve 503, y te enteras por tu monitorización en segundos. Dejar que las copias divergan es silencioso: cada petición tiene éxito, el sitio es rápido, y dos usuarios miran dos saldos distintos sin ninguna señal de que algo va mal. Cuál quieres depende de la función, no de la empresa. Para una transferencia bancaria el fallo ruidoso es obviamente correcto, porque una respuesta segura y equivocada sobre el dinero de alguien es peor que ninguna respuesta. Para un contador de me gusta o de visitas el fallo silencioso es obviamente correcto, porque a nadie le hace daño que un contador se desvíe en uno durante dos segundos.

WHEN THE LINK BREAKS, PICK ONE BEHAVIOUR

  refuse the write        A returns 503 until the link returns
  accept and diverge      A returns 200, B stays stale
  accept a quorum only    A needs 2 of 3 nodes to answer
  last write wins         conflicts resolved by timestamp

  the partition is never optionalLas cuatro opciones reales durante una partición. Fíjate en que tres son variantes de negarse o divergir, no un tercer eje.

Otra corrección importa, porque cambia decisiones de diseño a diario. La consistencia no solo va de fallos; también cuesta latencia incluso cuando todo está sano. Consistencia fuerte significa que una escritura no termina hasta que suficientes copias confirman que ocurrió. Si esas copias están en tres centros de datos en tres continentes, y el viaje de ida y vuelta de red entre ellos es de 70 ms, entonces una escritura que debe ser confirmada por una mayoría paga esa distancia en cada petición. Por eso el compromiso de CAP a veces se reformula como una elección entre consistencia y latencia incluso cuando nada está roto: ponerse de acuerdo lleva un tiempo proporcional a la distancia entre las copias, y la distancia no es negociable.

Ahora las dos palabras cotidianas. Consistencia fuerte significa que, una vez confirmada una escritura, toda lectura posterior en cualquier lugar la ve. No hay ningún momento en que una lectura correcta pueda devolver el valor antiguo. El precio es que cada lectura o espera al acuerdo o se enruta por el único sitio que tiene autoridad, lo cual es exactamente tan lento y tan frágil como suena. Consistencia eventual promete lo contrario, y merece la pena enunciarlo con precisión porque normalmente se enuncia mal: después de que cesan las escrituras, las copias convergen, y si no llegan escrituras nuevas coincidirán en un tiempo acotado. No hay ninguna promesa sobre lo que ve una lectura mientras tanto. Esa laguna no es un fallo del modelo, es el modelo.

BANK BALANCE                     LIKE COUNTER

  money leaves an account          a stranger taps a heart
  a wrong read costs money         a wrong read costs nothing
  must be strong                   eventual is fine
  cost: one writer at a time       cost: two users see 41 vs 42
  reject a write rather than lie   show 41 now, 42 in 800 msEl mismo sistema distribuido, sosteniendo ambos tipos de datos a la vez, con dos promesas distintas sobre ellos.

La consecuencia importante es que la consistencia no es una propiedad del sistema, es una propiedad de los datos. La misma aplicación puede mantener un saldo bancario bajo consistencia fuerte y un contador de me gusta bajo consistencia eventual, y a nadie le parece raro en cuanto se dice en voz alta. Pregunta qué cuesta una respuesta equivocada. Si una lectura obsoleta de un saldo pudiera permitir a alguien retirar dinero dos veces, esa lectura tiene que ser fuerte, y merece la pena pagar la latencia y la disponibilidad por ella. Si una lectura obsoleta de un contador de me gusta muestra 41 en vez de 42 durante ochocientos milisegundos, el coste es que nadie lo nota, y pagar por consistencia fuerte ahí sería puro desperdicio. Elegir por función es toda la habilidad, y es un juicio de negocio expresado como uno técnico.

Hay una opción intermedia entre fuerte y eventual que resuelve la mayoría de las quejas reales, y la conociste en la lección anterior: leer tus propias escrituras. Es más débil que la consistencia fuerte, porque otros usuarios pueden ver el valor antiguo, y más fuerte que la eventual pura, porque quien hizo el cambio siempre lo ve. Cuesta una regla de enrutado: las lecturas de un usuario que escribió hace poco van al primary. La mayoría de los productos necesitan exactamente esto y nada más, lo que conviene recordar cuando alguien propone hacer todo el sistema fuertemente consistente para arreglar una página de perfil.

Ahora la parte que lo ata todo, y es el concepto que separa un sistema distribuido que funciona de una demostración. Toda llamada de red puede fallar de una forma en la que no sabes qué pasó. La petición puede haberse perdido a la ida, o puede haberse procesado y haberse perdido la respuesta a la vuelta. Desde el cliente esos dos casos son idénticos: un timeout. Así que el cliente reintenta, y si el primer intento en realidad tuvo éxito, el reintento es un segundo pago real, un segundo correo, un segundo envío. Tampoco reintentar es opcional, porque no reintentar convierte cualquier parpadeo transitorio en un fallo visible para el usuario. Es el mismo razonamiento que la entrega al menos una vez, y se aplica a toda RPC, no solo a las colas de mensajes.

CREATE TABLE payments (
  id           bigserial PRIMARY KEY,
  idem_key     text NOT NULL UNIQUE,
  account_id   bigint NOT NULL,
  amount_cents integer NOT NULL
);

-- the retry sends the same idem_key, so the second insert loses
INSERT INTO payments (idem_key, account_id, amount_cents)
VALUES ('pay-9f2c-1', 42, 1290)
ON CONFLICT (idem_key) DO NOTHING;La única estructura que hace seguro un reintento de escritura: el cliente aporta una clave y la base de datos obliga a que la clave se use una vez.

Hay un atajo que evita parte de esta maquinaria: hacer la operación idempotente por construcción. Fijar un valor a un estado dado, como estado igual a enviado, puede repetirse cualquier número de veces con el mismo resultado, así que no necesita clave. Sumar a un valor, como saldo igual a saldo más 100, no puede, porque repetirlo cambia el resultado. Fíjate en que es una propiedad de la operación, no del código que la llama. Donde la regla de negocio lo permita, prefiere la operación que puede repetirse sin peligro, y marca las que no con una clave de idempotencia explícita, para que el caso inseguro sea una decisión deliberada y visible y no un descuido.

El último problema difícil es hacer que dos sistemas separados cambien juntos. Dentro de una base de datos, una transacción te da atomicidad: varias sentencias tienen éxito o todas se revierten, y nunca ves un estado a medio aplicar. A través de dos servicios con dos bases de datos, esa garantía no está disponible en la misma forma. El protocolo clásico, el commit en dos fases, hace que un coordinador pida a cada participante que se prepare y luego les diga a todos que confirmen. Funciona, y tiene una debilidad famosa: el coordinador es un punto único de fallo, y si muere después de la fase de preparación, los participantes se quedan con bloqueos y esperando, a veces mucho tiempo. Por eso su uso se limita sobre todo a sistemas que pueden permitirse el retraso y el coste de coordinación.

La alternativa práctica es aceptar que habrá estados intermedios y diseñar para ellos. Una saga parte el trabajo en transacciones locales, una por servicio, cada una de las cuales confirma por su cuenta, más una secuencia de acciones compensatorias que deshacen los pasos anteriores cuando falla uno posterior. Hacer un pedido pasa a ser: reservar el stock, cobrar la tarjeta y, si falla crear el envío, devolver el dinero y liberar el stock. Fíjate en a qué renuncia honestamente. Hay una ventana en la que el dinero ya está cobrado y el pedido aún no está confirmado, y los usuarios pueden ver esa ventana si no la escondes tras un estado como procesando. Ese es un coste real, y el beneficio es que ningún servicio mantiene un bloqueo mientras espera a otro, así que ningún fallo aislado congela todo el flujo. La mayoría de los sistemas que necesitan esto recurren a la saga, porque falla en trozos pequeños y recuperables en lugar de atascarse en uno grande.

Práctica

  1. Un servicio de pagos y un servicio de comprobación de fraude corren en dos centros de datos. El enlace entre ellos se rompe. El equipo debe decidir qué hace el servicio de pagos con las peticiones entrantes. ¿Qué razonamiento es correcto?

    Pistas
    • La red no es una de las opciones que puedas rechazar.
    • Pregunta qué cuesta una respuesta segura y equivocada sobre dinero.
    • Negarse ruidosamente y responder mal son las alternativas reales.
  2. Empareja cada garantía con la promesa que hace de verdad y con lo que cuesta.

    Pistas
    • Una de ellas solo promete algo sobre quien escribió.
    • La convergencia es una promesa sobre el futuro, no sobre la próxima lectura.
    • Pregunta quién puede ver el valor antiguo y durante cuánto tiempo.
  3. A la API de pagos de abajo se la llama con reintento al expirar el tiempo. Hazla segura: la misma clave nunca debe crear un segundo pago, y una llamada repetida debe seguir informando del resultado de la primera.

    El código se ejecuta en tu equipo, no en el nuestro: no lo ejecutamos. Comprueba tu solución con cada requisito y marca los cumplidos.

    Pistas
    • El almacén recuerda lo que ya ha visto, indexado por la clave de idempotencia.
    • Una repetición debe devolver el resultado guardado, no crear una entrada nueva.
    • Devuelve un objeto con el id del pago y si esta llamada lo creó.

Preguntas sobre esta lección

¿Es gratis?

Sí. El primer nivel de cada materia es gratis y no necesita cuenta. Los niveles más profundos y los grados superiores son de pago: eso es lo que mantiene vivo el proyecto.

¿Para qué edad es?

Está escrito para adolescentes curiosos a partir de unos 12 años y para adultos que empiezan de cero. No se supone experiencia previa en programación; las lecciones de redes y física no necesitan matemáticas más allá del colegio.

¿Necesito saber programar ya?

No. Las rutas de lógica, física y redes empiezan desde cero. El código lo escribes en la sección de lenguajes, que también arranca desde el principio.

¿Hay que instalar algo?

No. Todo funciona en el navegador: teoría, práctica y las simulaciones interactivas. No hay nada que descargar ni que configurar.

¿Cuánto se tarda en una materia?

Una lección dura entre 10 y 15 minutos. Un grado junior son unas 12 a 20 lecciones, así que son unas semanas de práctica constante, no una tarde.

¿Hay que seguir el orden?

Dentro de un grado los niveles se abren uno tras otro, porque cada uno se apoya en el anterior. Entre materias eres libre: empieza por lógica, física o redes.