DEV Community

Ejecutamos todo un sistema de autenticación sobre almacenes que solo saben hacer get y put

La decisión

Un sistema de autenticación parece pedir a gritos una base de datos relacional. Usuarios, roles, sesiones, concesiones OAuth, refresh tokens, todo referenciado entre sí. El nuestro no usa ninguna. Corre sobre Azure Table Storage, un almacén que te da una partition key, una row key y casi nada más. Sin joins, sin secondary indexes que sirvan de algo, sin operador de incremento, sin transacciones multifila con la forma a la que estás acostumbrado. Fue una decisión. Esto es lo que compra esa decisión, los patrones que la hacen funcionar y la parte en la que SQL habría sido realmente más fácil.

La razón para hacerlo es el coste y la simplicidad operativa. Sin connection pool que agotar, sin instancia de base de datos que dimensionar, parchear o conmutar por error, con escalado por partición que obtienes gratis. Mantener una tabla no cuesta casi nada y no hay nada que se pueda agotar. El precio de eso es un almacén sin query planner, que solo rinde cuando tus patrones de acceso son tan predecibles como tonto es el almacén. Los de la autenticación lo son, así que todo el diseño se apoya en ello.

La clave hace el trabajo

Sin joins, diseñas la clave para que la consulta sea la clave. Desnormalizas a propósito, eliges las particiones para que tus lecturas calientes sean point-gets, y codificas identidades compuestas directamente en la row key, por ejemplo "{pk}|{rk}" para entradas que necesitan una identidad compuesta en un almacén que ofrece un solo hueco de row key. La pregunta "cómo busco esto" hay que responderla al diseñar la clave, no en tiempo de consulta.

Para la autenticación eso está bien, porque los patrones de acceso son conocidos y no cambian con el tiempo:

  • Obtener un usuario por id
  • Obtener las concesiones de un usuario
  • Consumir un código

No estás explorando los datos. Conoces cada pregunta de antemano.

Llevas tu propio change log

Cada fila lleva un Timestamp gestionado por el servidor, y es tentador construir el backup incremental sobre él: traer todo donde Timestamp > watermark. Eso funciona en una demo y muere en producción, porque el almacén indexa la partition key y la row key y nada más. Timestamp no está en ningún índice, así que filtrar por él inspecciona cada fila de la tabla. Es un full scan disfrazado de consulta, y se vuelve más lento cada día que la tabla crece.

Así que, en su lugar, el almacén escribe su propio change log. Cada mutación, cada upsert y cada borrado, agrega una fila a una tabla de change log indexada por el nombre lógico de la tabla, con el compuesto "{pk}|{rk}" como row key y un flag que indica si la fila se escribió o se borró. Ahora "qué cambió desde el watermark" es la lectura de una única partición pequeña e indexada en lugar de un escaneo de toda la tabla, y el backup hace point-read solo de las filas que de verdad se movieron. Reconstruyes change-data-capture a partir de escrituras ordinarias, y escala con el ritmo de cambios en lugar de con el tamaño de la tabla.

Concurrencia sin transacciones

Aquí no hay SELECT ... FOR UPDATE. Lo que obtienes en su lugar es una única primitiva atómica: la escritura condicional, que se te entrega como un ETag. Escribes una fila solo si la copia que leíste no ha cambiado por debajo de ti. Todo lo que necesita seguridad bajo acceso concurrente se expresa como optimistic concurrency más reintento en caso de conflicto.

El ejemplo más claro es el contador de bloqueo de cuenta. AccessFailedCount necesita incrementarse de forma atómica, pero el almacén no tiene incremento. Así que lees la fila, subes el contador, la vuelves a escribir condicionada al ETag que leíste, y reintentas si alguien se te adelantó. Ese bucle es cómo haces atómico un contador en un almacén que no tiene contador. Lanza cincuenta contraseñas erróneas a la vez y cada una de ellas surte efecto, porque el conflicto se detecta y se reintenta en lugar de perderse.

Un borrado puede ser un bloqueo

El consumo de un solo uso, un código de autorización o un token de un solo uso que debe canjearse exactamente una vez, es un borrado condicional. Es un borrado condicional por ETag: si el borrado tiene éxito, ganaste la carrera y puedes continuar; si falla porque la fila ya no estaba, alguien lo consumió antes y te detienes. El borrado es el bloqueo.

La misma idea sirve para la leader election, construida sobre un blob lease, un bloqueo hecho de una escritura atómica en lugar de un servicio de coordinación aparte. Casi nunca necesitas un servicio de bloqueos cuando la propia escritura es atómica.

Los borrados tienen que ser de primera clase

Que es la mitad de la razón por la que existe el change log. Un almacén clave-valor no tiene marcador de borrado: una fila borrada simplemente desaparece, así que cualquier cosa que escaneara en busca de cambios ni siquiera podría ver que se fue. Un backup que se basara en los timestamps de las filas arrastraría en silencio al usuario borrado para siempre. El change log registra el borrado como un borrado, de modo que una restauración lo reproduce en lugar de resucitarlo. Ese modo de fallo, un backup que trae de vuelta fielmente a todos los que eliminaste, es toda una historia en sí misma y merece su propia entrada, pero el patrón pertenece a esta lista: en un almacén sin registro de borrados, el registro de borrados lo llevas tú.

El otro lado del balance

Ahora el otro lado del balance, aquello a lo que renuncias:

  • Renuncias a las consultas ad-hoc: un patrón de acceso genuinamente nuevo puede significar un nuevo diseño de clave o un full scan, porque no hay query planner que te salve.
  • Renuncias a los joins, así que mantienes copias desnormalizadas a mano y te haces cargo de su consistencia.
  • Renuncias a las transacciones multifila, así que diseñas cada invariante para que viva dentro de un único ítem, porque la atomicidad de un solo ítem es todo lo que se te da.

Si tus datos son profundamente relacionales, o tus patrones de acceso son desconocidos y todavía cambiantes, esta es la herramienta equivocada y te va a doler. Que es exactamente por lo que le viene bien a la autenticación.

Una base de datos relacional se gana el sueldo respondiendo preguntas que aún no se te han ocurrido. Un sistema de autenticación no tiene esas preguntas. El conjunto de cosas que preguntas - obtener este usuario, consumir este código, elegir este líder, respaldar lo que cambió - es pequeño, conocido y estable. Renuncia al query planner que nunca ibas a usar y, a cambio, obtienes una capa de almacenamiento que no cuesta casi nada operar y que no tiene nada que conmutar por error. Para la mayoría del software es un mal trato. Para esto, es el correcto.

Estos patrones son el motor de almacenamiento que hay debajo de Authagonal: un diseño clave-valor que no cuesta casi nada operar, que es una gran parte de cómo ponemos cada funcionalidad en cada plan.

Comments

No comments yet. Start the discussion.