2 pontos por GN⁺ 2023-11-08 | 1 comentários | Compartilhar no WhatsApp
  • O PR #1705 de refatoração do PDS do atproto da Bluesky altera o PDS para usar um datastore SQLite de tenant único e passa a armazenar o repo de cada usuário e o estado privado da conta em seus próprios arquivos SQLite
  • Os bancos de dados de usuário são armazenados na estrutura de caminho /${dbDirectory}/${sha256Hex(did).slice(0,2)}/${did}, e a chave de assinatura de cada repo é mantida junto ao respectivo arquivo SQLite
  • A abstração existente de acesso a dados de usuário é substituída pelo ActorStore e, como o SQLite não oferece suporte a transações concorrentes, operações de escrita precisam estabelecer explicitamente uma transação com a store
  • Handles de arquivos de banco de dados abertos e chaves de assinatura são gerenciados por LRUCache, mantendo em memória até 30 mil handles de arquivos abertos e 30 mil chaves; quando um DB é expulso do cache, o handle do arquivo é fechado
  • Para gerenciamento do estado do serviço, são introduzidos 3 DBs SQLite separados, executados em modo WAL para possibilitar leituras concorrentes e replicação por streaming; a distribuição do PDS deve incluir Litestream ou uma ferramenta semelhante

Principais mudanças do PR

  • O PR #1705 refatora o PDS para ser baseado em um datastore SQLite de tenant único
  • Cada usuário tem seu próprio arquivo SQLite dedicado, que armazena o repo e o estado privado da conta desse usuário
  • O DB de usuário é armazenado em um caminho hierárquico usando o hash do DID
    • Formato do caminho: /${dbDirectory}/${sha256Hex(did).slice(0,2)}/${did}
  • A repo signing key de cada repo é armazenada no mesmo local do arquivo SQLite

ActorStore e modelo de transações

  • A abstração de acesso a dados de usuário muda dos “services” existentes para ActorStore
  • A principal diferença do ActorStore é que as classes para leitura e escrita são separadas
  • Como o SQLite não oferece suporte a transações concorrentes, para realizar uma operação de escrita é necessário estabelecer claramente uma transação com a store
  • O log de commits inclui retrabalho de reader e transactor, tratamento de race em transações do actor store e limpeza da interface da store

Gerenciamento de cache e handles de arquivos

  • Um LRUCache é mantido para chaves de assinatura e bancos de dados
  • Os limites configurados são os seguintes
    • Máximo de 30 mil handles de arquivos abertos
    • Máximo de 30 mil chaves mantidas em memória
  • Quando um banco de dados é expulso do cache, o handle do arquivo é fechado
  • Commits relacionados incluem actor store in lru cache e fix open handles

3 DBs SQLite para estado do serviço

  • Além dos DBs por usuário, são introduzidos 3 bancos de dados SQLite separados para gerenciar o estado do serviço
    • service DB: gerencia informações de conta, códigos de convite, refresh tokens etc.
    • did cache DB: contém apenas uma única tabela para cache de DID resolution
    • sequencer DB: contém apenas uma única tabela que gerencia a ordem de atualização de todos os repos de um serviço
  • Cada arquivo SQLite é executado em WAL mode
  • O objetivo do WAL mode é permitir leituras concorrentes e replicação por streaming
  • Há um plano de incluir Litestream ou uma ferramenta semelhante na distribuição do PDS

Revisão e status do merge

  • Este PR é composto por um total de 143 commits e foi mesclado do branch pds-sqlite-refactor para o branch pds-v2
  • A data do merge foi 1º de novembro de 2023, e o commit de merge é 8449ceb
  • O revisor devinivy aprovou a mudança após deixar várias notas e comentários
  • devinivy avaliou que a refatoração traz “muitas simplificações excelentes” e, no geral, passa uma sensação de organização
  • Após o merge, o branch pds-sqlite-refactor foi excluído

Pergunta posterior

  • Em 28 de fevereiro de 2025, npetrangelo analisou a escala das mudanças deste PR e pediu um resumo dos trade-offs entre a arquitetura Postgres anterior e a arquitetura SQLite introduzida por este PR
  • O texto fornecido não inclui uma resposta da Bluesky a essa pergunta

1 comentários

 
GN⁺ 2023-11-08
Comentários no Hacker News
  • Gosto de SQLite, mas a abordagem de ter um schema ou banco de dados separado por tenant geralmente traz muitas dificuldades
    Em uma instância compartilhada, usando segurança em nível de linha (RLS), mesmo que uma migração falhe é possível fazer rollback de tudo, mas em schemas por tenant, se uma migração de dados falha por causa de dados inesperados, os usuários acabam presos em versões diferentes de schema até que a causa seja encontrada
    Quando se chega à escala de sharding, algo parecido pode acontecer de qualquer forma, mas até lá, um único banco de dados é o mais simples, e depois pode até ser necessário juntar os dados ou transferir a propriedade de recursos de forma atômica
    Não sou contra essa configuração e ela tem seus casos de uso, mas na empresa estamos fugindo com tudo de schema por tenant. Se você não investir direito, os problemas são numerosos demais, e raramente se está preparado para isso quando a ideia surge pela primeira vez
    O curioso é que, uns 10 anos atrás, o app começou com SQLite por tenant, depois migrou para schemas por tenant no PostgreSQL, e agora está indo para um schema único com RLS, ou seja, foi exatamente na direção oposta

    • Tendo lidado com bancos de dados gigantes em produção, não quero passar por isso de novo
      Quando a carga fica grande o suficiente, toda mudança vira um risco, porque não dá para testar completamente todos os casos extremos de performance
      Também é um padrão comum um usuário do plano gratuito encontrar um caminho de código sem índice e derrubar a produção

    • Alguns usuários ficarem em versões diferentes de schema por causa de falha em migração de dados talvez não seja um grande problema
      Em um serviço grande e complexo nesse nível, normalmente as atualizações de schema são feitas em etapas: 1. tornar o código compatível com o schema futuro, 2. migrar os dados, 3. remover o suporte ao schema antigo
      Então, em geral, deveria ser seguro operar por bastante tempo no estado entre as etapas 1 e 2. Claro, bugs novos são exceção, mas do ponto de vista operacional, um sistema que volta para um estado intermediário de migração também pode ser aceitável quando se usa esse procedimento

    • Se o produto tem menos de 100 clientes, pode até ser melhor que cada usuário esteja em uma versão diferente de schema
      Cada cliente pode ter cronogramas e exigências de upgrade diferentes, e conheço negócios que fazem trabalho sob medida para alguns clientes e, na prática, nem rodam o mesmo código
      No fim, depende da estrutura do negócio

    • Sendo justo, 10 anos atrás RLS ainda não existia. Surgiu no PostgreSQL 9.5 em 2016

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

      https://electric-sql.com/

  • Não entendo o que querem dizer com “SQLite não suporta transações simultâneas”
    Pelo que eu sei, suporta, desde que o arquivo .db não seja acessado por compartilhamento de arquivos como UNC ou NFS: https://www.sqlite.org/wal.html
    Já usei para ler e atualizar o banco de dados a partir de várias threads/processos na mesma máquina, e se for preciso uma visão consistente ou se não quiser manter a transação aberta por muito tempo, também dá para fazer snapshot com a sqlite backup API
    Posso estar deixando passar algo, e faz alguns anos que não mexo com SQLite, então não tenho certeza

    • Não, eu estava enganado. Na prática, é mais algo como múltiplas leituras, uma única escrita
      Acho que eu vinha assumindo isso sem conferir com cuidado suficiente. Ainda assim, a maioria dos bancos de dados que fiz com SQLite era muito mais voltada para leitura do que para escrita
      Correção feita

    • Se esperar um pouco, o hctree [1] deve estabilizar, e então será possível escolher entre os mecanismos de backend tradicionais e o backend com suporte a concorrência recém-implementado

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

    • Segundo a documentação, o escritor apenas acrescenta novo conteúdo ao fim do arquivo WAL, então leitura e escrita podem acontecer ao mesmo tempo, mas como existe apenas um arquivo WAL, só pode haver um escritor por vez
      Acho que o texto original queria dizer que as operações de atualização precisam ser executadas de forma sequencial

    • Com tráfego baixo funciona, mas quando as transações crescem ou aumenta o número de escritas simultâneas, em algum momento aparecem problemas de database locked, mesmo com WAL ativado
      Dá para contornar isso em certa medida no nível da aplicação, mas em geral, se você chegou nesse ponto, deveria considerar seriamente outro backend de banco de dados

    • Pelo menos da última vez que verifiquei, isso provavelmente quer dizer que não há bloqueio em nível de linha, e que até o bloqueio em nível de tabela é bem limitado
      Pela documentação, o escritor ainda bloqueia o banco de dados inteiro

  • Interessante, e gostei da estratégia de manter uma relação 1:1 entre usuário e banco de dados
    Mas fiquei curioso sobre como eles tratam dados que exigem agregação entre usuários. Se eu sigo outro usuário e ele publica algo, como meu banco de dados é atualizado com a nova postagem? Ou essa arquitetura vale apenas para dados persistentes como dados de perfil e relações de follow, enquanto dados interativos como feed são tratados separadamente?
    Também é legal que o “pooling de conexões” seja basicamente só limitar o número de handles abertos com um cache LRU. Como cada conexão de DB é single-threaded, também é interessante que a concorrência seja tratada no nível de tenancy, e não no nível da conexão
    Parece que também seria fácil colocar limitação de taxa por banco de dados em cima disso para bloquear abuso de usuários específicos
    Também fiquei curioso se existe uma forma simples de configurar o Litestream para um número arbitrário de bancos de dados

  • Sempre fico feliz de ver a adoção de SQLite/Litestream crescendo no servidor. Nós também usamos ao criar apps novos
    SQLite + Litestream é uma escolha melhor para bancos de dados por tenant, e replicar/fazer backup em S3/R2 sai muito mais barato do que um banco de dados gerenciado caro na nuvem [1]
    Até 3900% mais barato que o SQLServer da Azure

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

  • Não entendi o que significa 3900% mais barato

  • Num antigo emprego em fintech, a empresa armazenava as contas dos clientes como arquivos sqlite3 criptografados em armazenamento de blobs, e isso combinava bem com o padrão de acesso

    • Fico curioso sobre como eles lidavam com o bloqueio do arquivo ao reenviá-lo depois de uma modificação
  • À primeira vista, isso parece uma combinação do pior com o terrível
    Eu adoraria que alguém escrevesse um bom texto explicando as vantagens com números reais e analisando os defeitos esperados. Pode ser um tema realmente interessante se for aprendido direito

    • Você pode explicar por que isso parece uma “combinação do pior com o terrível”?
      À primeira vista, parece uma escolha bem razoável, especialmente se a ideia for construir um sistema distribuído que será executado e implantado por muitos usuários que não são administradores de sistemas profissionais
      E parece que esse deve ser o objetivo aqui, então eu esperaria que uma meta de projeto fosse evitar a necessidade de configurar, provisionar e administrar um banco de dados adicional ou outro servidor
  • Queria que alguém que conhece melhor o Bluesky explicasse quais dados vão para o SQLite e quais não vão
    Estou assumindo que coisas como mensagens entre usuários não vão

    • Acho que as mensagens entre usuários também ficam armazenadas nesses bancos de dados SQLite
      Pense em email. Se você envia um email e coloca cinco pessoas em cópia, sete pessoas acabam armazenando a mesma cópia do email em seus próprios servidores de email
      Ou seja, não é uma estrutura com um banco de dados central contendo um único email ao qual os outros fazem referência
      O sharding de banco de dados relacional basicamente funciona assim também
      Esse tipo de desnormalização de dados é quase indispensável à medida que a aplicação escala, especialmente em aplicações muitos-para-muitos com alta proporção de leituras em relação a escritas
      Se a proporção de leituras em relação a escritas for baixa, uma estrutura de banco de dados relacional com um único master e vários slaves ainda consegue lidar com uma quantidade surpreendente de requisições e dados
    • Como usuário, entram todos os posts e respostas que você publica
      Hoje, o Bluesky praticamente hospeda sozinho o único PDS, mas o objetivo final é que todo usuário final tenha seu próprio PDS
      A Inrupt/SOLID chama esse conceito de “pod”
      Na prática, eles integraram ontem o segundo PDS de produção, então há progresso
    • Se por mensagens você quer dizer mensagens diretas, isto é, mensagens privadas entre duas partes, o Bluesky atualmente não tem esse recurso
      Só existem mensagens públicas transmitidas para o mundo inteiro
      Não fui pesquisar se existem planos para mensagens diretas
  • Qual seria o motivo de fazer hash do usuário com sha256 e separá-lo em um diretório de destino de dois caracteres?
    md5 não seria muito mais rápido e resolveria o mesmo problema?

    • Meu palpite é que esse hash é calculado poucas vezes, então a diferença de desempenho se perde no ruído
      Também evita ter que responder à pergunta “por que você usou um hash inseguro?” e tem mais valor eliminar ou minimizar uma possível classe de problemas de segurança
    • Talvez eles se preocupem com colisões nessa escala
      Ou talvez, como eu, estejam soterrados pelas ferramentas de segurança da empresa e não queiram criar exceções separadas para cada uso de md5
    • Não é saudável deixar um hash criptográfico quebrado circulando por aí
      Se você não precisa de um hash seguro, há muitos hashes rápidos não criptográficos
    • Isso provavelmente tem mais a ver com limites do sistema de arquivos, isto é, o número máximo de arquivos dentro de um diretório, do que com colisões
  • O Bluesky ainda é só por convite?

    • Sim, mas não por algo como “growth hacking”
      É uma forma de limitar o crescimento enquanto eles escalam o sistema do lado do backend e da prevenção de abusos
      Há uma fila dedicada para desenvolvedores, e dá para conseguir acesso bem rápido: https://atproto.com/blog/call-for-developers