2 puntos por GN⁺ 2023-11-08 | 1 comentarios | Compartir por WhatsApp
  • El PR de refactorización del PDS de Bluesky atproto, #1705, cambia el PDS para usar un datastore SQLite de inquilino único y modifica el almacenamiento para guardar el repo de cada usuario y el estado privado de su cuenta en su propio archivo SQLite
  • La DB de usuario se guarda con la estructura de ruta /${dbDirectory}/${sha256Hex(did).slice(0,2)}/${did}, y la clave de firma de cada repo se almacena junto al archivo SQLite correspondiente
  • La abstracción existente de acceso a datos de usuario se reemplaza por ActorStore, y como SQLite no soporta transacciones concurrentes, las operaciones de escritura deben establecer explícitamente una store y una transacción
  • Los file handles de DB abiertos y las claves de firma se gestionan con LRUCache; se mantienen en memoria hasta 30k file handles abiertos y 30k claves, y cuando una DB sale del caché se cierra su file handle
  • Para gestionar el estado del servicio se introducen 3 DB SQLite separadas, ejecutadas en modo WAL para permitir lecturas concurrentes y replicación por streaming, y se planea incluir Litestream o una herramienta similar en la distribución de PDS

Cambios clave del PR

  • El PR #1705 refactoriza el PDS sobre la base de un datastore SQLite de inquilino único
  • Cada usuario tiene su propio archivo SQLite dedicado, y en ese archivo se almacenan el repo del usuario y el estado privado de su cuenta
  • Las DB de usuario se almacenan en una ruta jerárquica basada en el hash del DID
    • Formato de ruta: /${dbDirectory}/${sha256Hex(did).slice(0,2)}/${did}
  • La repo signing key de cada repo se guarda en la misma ubicación que el archivo SQLite

ActorStore y modelo de transacciones

  • La abstracción de acceso a datos de usuario cambia de los anteriores “services” a ActorStore
  • La principal diferencia de ActorStore es que separa las clases para lectura y escritura
  • Como SQLite no soporta transacciones concurrentes, para realizar operaciones de escritura se debe establecer explícitamente una store y una transacción
  • El log de commits incluye retrabajo de reader y transactor, manejo de race conditions de transacciones en actor store, limpieza de la interfaz de store, entre otros cambios

Gestión de caché y file handles

  • Se mantiene un LRUCache para las claves de firma y las bases de datos
  • Los límites configurados son los siguientes
    • Máximo de 30k file handles abiertos
    • Máximo de 30k claves mantenidas en memoria
  • Cuando una base de datos es expulsada del caché, se cierra su file handle
  • Entre los commits relacionados están actor store in lru cache y fix open handles

Tres DB SQLite para el estado del servicio

  • Además de las DB por usuario, se introducen 3 bases de datos SQLite separadas para gestionar el estado del servicio
    • service DB: gestiona información de cuentas, códigos de invitación, refresh tokens y más
    • did cache DB: contiene solo una tabla para el caché de resolución de DID
    • sequencer DB: contiene solo una tabla para gestionar el orden de todas las actualizaciones de repo de un servicio
  • Cada archivo SQLite se ejecuta en WAL mode
  • El objetivo de WAL mode es permitir lecturas concurrentes y replicación por streaming
  • Se planea incluir Litestream o una herramienta similar en la distribución de PDS

Estado de revisión y merge

  • Este PR está compuesto por 143 commits en total y fue fusionado desde la rama pds-sqlite-refactor hacia la rama pds-v2
  • La fecha de merge fue el 1 de noviembre de 2023 y el commit de fusión es 8449ceb
  • El revisor devinivy dejó varias notas y comentarios antes de aprobar los cambios
  • Devinivy evaluó que la refactorización tiene “muchas simplificaciones excelentes” y que en general se siente más ordenada
  • Después del merge, la rama pds-sqlite-refactor fue eliminada

Pregunta posterior

  • El 28 de febrero de 2025, npetrangelo revisó la magnitud de los cambios de este PR y pidió un resumen de los trade-offs entre la arquitectura previa en Postgres y la arquitectura en SQLite introducida por este PR
  • El texto proporcionado no incluye una respuesta de Bluesky a esa pregunta

1 comentarios

 
GN⁺ 2023-11-08
Opiniones de Hacker News
  • Me gusta SQLite, pero el enfoque de tener un esquema o una base de datos separados por tenant suele traer muchas dificultades.
    Si se usa seguridad a nivel de fila (RLS) en una instancia compartida, aunque falle una migración se puede hacer rollback completo, pero con esquemas por tenant, si una migración de datos falla por datos inesperados, los usuarios quedan en distintas versiones del esquema hasta encontrar la causa.
    Al llegar a escala de sharding, de todos modos pueden pasar cosas parecidas, pero hasta entonces una base de datos única es lo más sencillo, y más adelante quizá haya que unir datos o transferir la propiedad de recursos de forma atómica.
    No digo que esté en contra de esta configuración; tiene sus casos de uso, pero en mi empresa estamos saliendo a toda velocidad de los esquemas por tenant. Si no se invierte bien, trae demasiados problemas, y rara vez se está preparado para eso cuando se propone la idea inicial.
    Lo curioso es que hace unos 10 años una app empezó con SQLite por tenant, pasó a esquemas por tenant en PostgreSQL, y ahora va hacia un esquema único con RLS, así que terminó avanzando exactamente en la dirección contraria.

    • Habiendo trabajado con bases de datos enormes en producción, no quiero volver a hacerlo.
      Cuando la carga crece lo suficiente, cualquier cambio se vuelve riesgoso, porque no se pueden probar por completo todos los casos extremos de rendimiento.
      También es un patrón común que un usuario del tier gratuito encuentre una ruta de código sin índice y rompa producción.

    • Que una migración de datos falle y deje a algunos usuarios en otra versión del esquema puede no ser un problema grave.
      Si el servicio es tan grande y complejo, normalmente las actualizaciones de esquema se hacen por etapas: 1. hacer que el código sea compatible con el esquema futuro, 2. migrar los datos, 3. eliminar el soporte del esquema anterior.
      Por eso, en general debería ser seguro operar durante bastante tiempo en el estado entre los pasos 1 y 2. Claro que los bugs nuevos son la excepción, pero desde el punto de vista operativo, mientras se siga este procedimiento, también me parece aceptable un sistema que vuelva a un estado intermedio de migración.

    • Si el producto tiene menos de 100 clientes, que cada usuario esté en una versión distinta del esquema incluso podría ser algo bueno.
      Cada cliente puede tener calendarios y requisitos de actualización distintos, y conozco negocios que hacen trabajo a medida para algunos clientes, al punto de que en la práctica ni siquiera ejecutan el mismo código.
      Al final depende de la estructura del negocio.

    • Para ser justos, hace 10 años RLS todavía no existía. Apareció en PostgreSQL 9.5 en 2016.

    • https://blog.turso.tech/introducing-embedded-replicas-deploy...

      https://electric-sql.com/

  • No entiendo qué significa decir que “SQLite no soporta transacciones concurrentes”.
    Según entiendo, sí las soporta siempre que no se acceda al archivo .db mediante un sistema de archivos compartido como UNC o NFS: https://www.sqlite.org/wal.html
    Lo he usado para leer y actualizar una base de datos desde varios threads/procesos en la misma máquina, y si se necesita una vista consistente o no se quiere mantener una transacción abierta por mucho tiempo, también se pueden hacer snapshots con la API de backup de sqlite.
    Quizá se me esté escapando algo, y no estoy totalmente seguro porque no he tocado SQLite en varios años.

    • No era así. Me equivoqué. En realidad es más bien múltiples lectores, un solo escritor.
      Parece que lo había estado asumiendo y no lo había verificado con suficiente cuidado. Aun así, la mayoría de las bases de datos que hice con SQLite eran más de lectura que de escritura.
      Corrijo lo dicho.

    • Si esperan un poco, hctree [1] se estabilizará, y podrán elegir entre el mecanismo backend tradicional y un backend recién implementado con soporte de concurrencia.

      [1] https://sqlite.org/hctree/doc/hctree/doc/hctree/index.html

    • Según la documentación, el escritor solo agrega contenido nuevo al final del archivo WAL, por lo que lectura y escritura pueden ocurrir al mismo tiempo, pero como solo hay un archivo WAL, solo puede haber un escritor escribiendo a la vez.
      Creo que lo que decía el artículo original es que las operaciones de actualización deben ejecutarse de forma secuencial.

    • Si el tráfico es bajo, funciona, pero cuando las transacciones crecen o aumenta la cantidad de escrituras concurrentes, aun con WAL activado llega un punto en que aparecen problemas de database locked.
      Se puede esquivar hasta cierto punto a nivel de aplicación, pero en general, si llegaste a ese punto, deberías considerar seriamente otro backend de base de datos.

    • Probablemente significa que, al menos la última vez que lo revisé, no hay bloqueo a nivel de fila, y el bloqueo a nivel de tabla también es muy limitado.
      Según la documentación, el escritor sigue tomando un bloqueo sobre toda la base de datos.

  • Es interesante, y me gusta la estrategia de mantener una relación 1:1 entre un usuario y una base de datos.
    Pero me pregunto cómo manejan los datos que requieren agregación entre usuarios. Si sigo a otro usuario y ese usuario publica algo, ¿cómo se actualiza mi base de datos con la nueva publicación? ¿O la estructura apunta solo a datos persistentes como datos de perfil o relaciones de seguimiento, y los datos interactivos como el feed se manejan por separado?
    También me gusta que el “connection pooling” no sea más que limitar la cantidad de handles abiertos con una caché LRU. Es interesante que, como cada conexión a la DB es de un solo thread, manejen la concurrencia a nivel de tenencia y no a nivel de conexión.
    Parece que encima de esto sería fácil agregar rate limiting por base de datos para prevenir abusos de un usuario específico.
    También me pregunto si existe una forma sencilla de configurar Litestream para una cantidad arbitraria de bases de datos.

  • Siempre da gusto ver que crece la adopción de SQLite/Litestream en servidores. Nosotros también lo estamos usando al crear apps nuevas.
    SQLite + Litestream es una mejor opción para bases de datos por tenant, y replicar y respaldar en S3/R2 cuesta muchísimo menos que una costosa base de datos administrada en la nube [1].
    Es hasta 3900% más barato que SQLServer en Azure

[1] https://docs.servicestack.net/ormlite/litestream

  • No entiendo qué significa que sea 3900% más barato.

  • En mi trabajo anterior en fintech, la empresa guardaba las cuentas de los clientes como archivos sqlite3 cifrados en almacenamiento de blobs, y encajaba bastante bien con el patrón de acceso.

    • Me da curiosidad cómo manejaban el bloqueo de archivos al volver a subirlos después de modificarlos.
  • A simple vista parece una combinación de lo peor con lo horrible.
    Ojalá alguien escriba un buen artículo que explique las ventajas con números reales y analice los posibles defectos. Si se aprende bien, podría ser un tema realmente interesante.

    • ¿Puedes explicar por qué esto parece una “combinación de lo peor con lo horrible”?
      A simple vista, parece una decisión bastante razonable, sobre todo si suponemos que están creando un sistema distribuido que muchos usuarios, que no son administradores de sistemas profesionales, van a ejecutar y desplegar.
      Creo que ese debería ser el objetivo aquí, y esperaría que evitar la necesidad de instalar, configurar y administrar una base de datos adicional u otro servidor sea una meta de diseño.
  • Me gustaría que alguien que conozca mejor Bluesky explique qué datos se almacenan en SQLite y cuáles no.
    Supongo que no serán cosas como mensajes entre usuarios.

    • Creo que los mensajes entre usuarios también se almacenan en esas bases de datos SQLite.
      Piensa en el correo electrónico. Si envías un email y pones a cinco personas en copia, siete personas guardan cada una una copia del mismo correo en su propio servidor de email.
      Es decir, no hay una base de datos central que contenga un email y a la que otras personas hagan referencia.
      El sharding de bases de datos relacionales funciona básicamente así.
      Este tipo de desnormalización de datos se vuelve casi indispensable a medida que una aplicación escala, especialmente en aplicaciones de muchos a muchos con una proporción alta de lecturas frente a escrituras.
      Si la proporción de lecturas frente a escrituras es baja, una arquitectura de base de datos relacional con un solo maestro y varios esclavos puede manejar una cantidad sorprendentemente grande de solicitudes y datos.
    • Incluye todos los posts y respuestas que publicaste como usuario.
      Actualmente, Bluesky prácticamente aloja por cuenta propia el único PDS, pero el objetivo final es que todos los usuarios finales tengan su propio PDS.
      Inrupt/SOLID llama a este concepto “pod”.
      En la práctica, ayer incorporaron el segundo PDS de producción, así que hay avances.
    • Si con mensajes te refieres a mensajes directos, es decir, mensajes privados entre dos partes, Bluesky actualmente no tiene esa función.
      Solo hay mensajes públicos que se transmiten a todo el mundo.
      No investigué por separado si hay planes para mensajes directos.
  • ¿Por qué aplicar un hash sha256 a los usuarios para dividirlos en directorios de destino de dos caracteres?
    ¿No sería md5 mucho más rápido y resolvería el mismo problema?

    • Supongo que ese hash se calcula relativamente pocas veces, así que la diferencia de rendimiento queda enterrada en el ruido.
      Además, tiene más valor no tener que responder a la pregunta “¿por qué usaron un hash inseguro?” y eliminar o minimizar una categoría potencial de problemas de seguridad.
    • A esa escala quizá también les preocupen las colisiones.
      O quizá, como yo, están sepultados bajo las herramientas de seguridad de la empresa y no quieren crear una excepción por cada uso de md5.
    • No es saludable dejar circulando un hash criptográfico roto.
      Si no necesitas un hash seguro, hay muchos hashes no criptográficos rápidos.
    • Es probable que esto se deba menos a las colisiones y más a los límites del sistema de archivos, es decir, la cantidad máxima de archivos dentro de un directorio.
  • ¿Bluesky todavía funciona por invitación?

    • Sí, pero no por algo como “growth hacking”.
      Es una forma de limitar el crecimiento mientras escalan el sistema en el backend y en la prevención de abusos.
      Hay una lista de espera exclusiva para desarrolladores, y puedes obtener acceso bastante rápido: https://atproto.com/blog/call-for-developers