- A OpenRun é uma plataforma de implantação de apps web para ferramentas internas e armazena arquivos estáticos, código do app e arquivos de configuração em SQLite em vez do sistema de arquivos, gerenciando o estado da implantação com foco no banco de dados
- O principal objetivo é processar atualizações de apps, nas quais vários arquivos mudam juntos, como uma única transação, evitando que páginas web quebradas sejam servidas durante a troca de versão
- Ao usar o hash SHA256 antes da compressão como chave primária, reduz o armazenamento duplicado de arquivos entre versões do app e entre apps de staging, preview e production
- O método de armazenamento em SQLite simplifica rollback, backup, armazenamento de hash para ETag e armazenamento com compressão Brotli, e, se necessário, dados em GZip ou sem compressão também podem ser tratados junto com a adição de colunas
- Atualmente funciona em nó único e, ao oferecer suporte a múltiplos nós, pretende usar Postgres compartilhado junto com cache local em arquivo SQLite para reduzir latência
Como a OpenRun armazena arquivos
- OpenRun é uma plataforma open source de implantação para ferramentas internas code-first e implanta apps web em um único nó ou em um cluster Kubernetes no modelo GitOps
- Em vez de manter conteúdo estático no sistema de arquivos como um servidor web comum, a OpenRun armazena dados do app, como arquivos estáticos, código do app e arquivos de configuração, em SQLite
- Como os metadados do app são gerados dinamicamente, armazená-los no banco de dados é algo natural, e lidar com os arquivos na mesma camada de armazenamento facilita gerenciar junto o estado da implantação
- Ao criar e atualizar apps, os arquivos são enviados do GitHub ou do disco local para o banco de dados SQLite
- Só usa o sistema de arquivos local no modo de desenvolvimento
Por que escolheram SQLite
- O maior benefício são as atualizações transacionais
- É possível agrupar várias mudanças de arquivo em uma única transação
- Graças ao isolamento, um app web quebrado não é servido durante a atualização
- Se ocorrer um erro de implantação, é possível fazer rollback no nível da transação do banco de dados
- Mesmo quando vários apps são atualizados ao mesmo tempo, dá para reverter tudo de uma vez
- É mais simples do que encontrar e limpar arquivos alterados no sistema de arquivos
- A OpenRun faz controle de versão automático de todas as atualizações, e os dados dos arquivos são armazenados em uma tabela com o seguinte esquema
CREATE TABLE files (sha text, compression_type text, content blob, create_time datetime, PRIMARY KEY(sha));
- Como usa o hash SHA256 do conteúdo antes da compressão como chave primária, o mesmo conteúdo de arquivo é armazenado apenas uma vez, mesmo em várias versões
- Cada app de production tem um staging app e pode ter vários preview apps, o que pode gerar duplicação de arquivos
- O armazenamento baseado em SQLite evita armazenar em duplicidade, mesmo entre apps diferentes, arquivos com o mesmo conteúdo
Backup, cache e tratamento de compressão
- Todo o estado do sistema, metadados e arquivos podem ser armazenados em backup com ferramentas de backup para SQLite como o Litestream
- Se o SHA do conteúdo necessário para o cabeçalho ETag de cache do navegador for armazenado uma vez no upload do arquivo, não é preciso recalculá-lo depois
- O conteúdo dos arquivos é armazenado na tabela SQLite em formato comprimido com Brotli
- Nessa abordagem com banco de dados, também é possível armazenar dados comprimidos com GZip ou sem compressão adicionando colunas à tabela
files
Desempenho e plano para múltiplos nós
- Na OpenRun, a abordagem com banco de dados SQLite oferece bom desempenho
- Como não existe uma implementação equivalente baseada em sistema de arquivos, não foi feito um benchmark comparativo direto
- Segundo o benchmark da equipe do SQLite, em algumas cargas de trabalho o SQLite pode ter desempenho melhor do que usar diretamente o sistema de arquivos
- A OpenRun atualmente roda em nó único
- No futuro, quando houver suporte a múltiplos nós, a ideia é usar um banco de dados Postgres compartilhado para armazenar metadados e dados de arquivos em vez de SQLite local
- Essa abordagem pode causar problemas de latência
- Para evitar a latência no acesso ao Postgres, o plano é usar um banco de dados SQLite local como cache de arquivos
Por que a abordagem com sistema de arquivos é mais comum
- Um dos motivos de a maioria dos servidores web usar sistema de arquivos é a conveniência
- É possível copiar e atualizar arquivos com ferramentas tradicionais do sistema de arquivos, como rsync e tar
- Outro motivo é o contexto histórico
- O sistema de arquivos já era usado antes de existirem bons bancos de dados relacionais in-process
- Para usar um banco de dados como armazenamento de arquivos, é necessária uma interface de API para upload de arquivos, e isso nem sempre é viável
1 comentários
Opiniões no Hacker News
Alguns anos atrás, experimentei essa ideia, em parte inspirado pelo artigo “35% Faster Than The Filesystem”: https://www.sqlite.org/fasterthanfs.html
Minhas anotações da época estão aqui: https://simonwillison.net/2020/Jul/30/fun-binary-data-and-sq...
Criei https://datasette.io/plugins/datasette-media, um plugin para servir arquivos estáticos a partir de SQLite no Datasette, e ele funciona bem, mas, sinceramente, não o usei muito desde que o criei
Um conceito relacionado é servir tiles de mapas a partir de SQLite, e https://datasette.io/plugins/datasette-tiles faz isso. Descobri que o formato MBTiles era, na verdade, um banco de dados SQLite cheio de PNGs
Se você quiser experimentar SQLite para servir arquivos, a ferramenta CLI “sqlite-utils insert-files” pode ser útil para a configuração inicial do banco de dados: https://sqlite-utils.datasette.io/en/stable/cli.html#inserti...
O hash do conteúdo só precisa ser gerado uma vez no upload do arquivo, sem a necessidade de recriá-lo a cada reinicialização do servidor web nem de ter uma etapa de build que altere os nomes reais dos arquivos. Também é possível aplicar isso dinamicamente a arquivos no sistema de arquivos (veja a implementação embedFS em https://github.com/benbjohnson/hashfs), mas o banco de dados torna isso um pouco mais fácil
O requests-cache, se não me falha a memória, armazena requisições em cache no SQLite por
(date, URI): https://github.com/requests-cache/requests-cache/blob/main/r...Busca por pyfilesystem SQLite: https://www.google.com/search?q=pyfilesystem+sqlite
Busca por sendfile mmap SQLite: https://www.google.com/search?q=sendfile+mmap+sqlite
https://github.com/adamobeng/wddbfs é um “provedor webdavfs que permite ler o conteúdo de um banco de dados sqlite”
Também parece haver uma boa forma de implementar um sistema de arquivos sobre SQLite, colocando permissões de arquivo Unix e permissões de atributos estendidos xattrs por cima
Será que o SQLite seria mais rápido ou mais conveniente do que, por exemplo, ngx_http_memcached_module.c? Também fico curioso se o SQLite tem ACL por célula
Ao ler arquivos estáticos, é preciso abrir, ler e fechar o arquivo a cada requisição, então há mais trocas de contexto, mesmo que a camada do sistema de arquivos tenha cacheado o conteúdo do arquivo. Se quiser tornar isso rápido, o caminho adequado é adicionar um frontend de cache, não transformar tudo em banco de dados. Ele será mais rápido que SQLite e também mais fácil de manter e depurar
Isso inclui sistemas de arquivos que rodam totalmente em espaço de usuário. FUSE fica de fora porque as chamadas passam pelo kernel
Há uma limitação em dizer que “atualizações transacionais” são o principal benefício. Quer o servidor use SQLite ou o sistema de arquivos, isso por si só não impede que um webapp quebre durante uma atualização.
Cada página no navegador é uma árvore de recursos buscados por requisições HTTP separadas, então não é alvo de um sistema de transações/atualizações atômicas do lado do servidor. Mesmo que todos os recursos sejam trocados em uma transação no servidor, o navegador ainda pode ver uma combinação de recursos antigos e novos.
A solução comum é dar a todos os sub-recursos da página (bundles JavaScript, folhas de estilo, mídia etc.) nomes (URLs) que incluam um hash do conteúdo ou uma versão. Se o documento HTML raiz carrega a versão X, todos os sub-recursos também devem carregar a versão X correspondente.
Além disso, ao atualizar de X para Y, é preciso continuar servindo os sub-recursos de X por algum tempo depois de começar a servir a página Y. Se eles não forem mantidos até que se tenha certeza razoável de que nenhum navegador ainda esteja carregando uma página X, a página X pode quebrar.
Portanto, se você quiser colocar o HTML raiz e os sub-recursos em um único bundle substituído atomicamente, isso na verdade não funciona. Isso removeria sub-recursos antigos que ainda podem estar sendo referenciados.
Em alguns casos, talvez você também queira versionar alguns sub-recursos, como arquivos de mídia, separadamente do documento HTML. Para atualizá-los sem invalidar todo o cache de elementos estruturais do app, como blocos de JavaScript ou folhas de estilo, talvez o sistema de build da página também precise levar isso em conta.
Quando uma grande empresa experimentou isso (na época, ela via uma parte considerável da web), a maioria dos usuários (mais de 80%) permanecia no webapp por cerca de 2 a 3 dias. É bem provável que houvesse viés por causa de pessoas que deixavam abas abertas durante o fim de semana.
O percentil 95 era de cerca de 2 semanas, e 100% era de cerca de 600 dias. Ou seja, havia usuários que deixaram uma aba aberta por quase 2 anos.
Se o objetivo for 100%, será preciso esperar bastante. Esses números são todos de memória, e eu já não trabalho mais naquela empresa.
O cenário em que um usuário fica muito tempo em uma página e depois recebe um link quebrado é mais um problema do lado das SPAs.
Concordo em geral, mas atualizações transacionais só evitam uma categoria de problemas relacionados a atualização. Outros problemas no nível da aplicação também podem produzir uma experiência quebrada.
É possível continuar servindo versões antigas de conteúdo estático referenciado por hashes de conteúdo, mas isso não está implementado no Clace atualmente.
O truque principal é fazer upload das mudanças que não são HTML antes das mudanças no HTML, para não referenciar arquivos antes que eles existam. Se você quiser deixar o app o mais complexo possível, pode aplicar busca em profundidade ao upload. Mas, se valoriza sua saúde mental, é melhor suavizar o problema e fazer o app enviar os assets primeiro.
Quando trabalhei em um pequeno estúdio de jogos em 2011/2012, recomendei mover todos os assets menores que 100 KB para um banco sqlite3, criar “arquivos pak” e armazenar os offsets desses arquivos dentro do banco sqlite3.
Essa escolha foi influenciada por uma apresentação post-mortem do Richard Hipp, em que ele disse que, olhando em retrospecto, teria sido melhor tratar BLOBs como inodes, colocando-os em offsets mais ao fim dentro do banco de dados e anexando os BLOBs ao arquivo.
O carregamento de assets ficou extremamente rápido. Como era um jogo mobile, só uma parcela muito pequena dos assets não estava no banco. Também é interessante ver mais pessoas adotando essa abordagem depois.
Outra vantagem fácil de deixar passar é que você pode anexar uma quantidade quase ilimitada de metadados ao lado do conteúdo, permitindo encontrar arquivos “semelhantes” com consultas ao banco de dados.
Colocamos muitos metadados no DB, e acho que o arquivo pak final tinha 200 MB, enquanto o banco tinha cerca de 20 MB. Novamente: era um jogo mobile.
Do lado do cliente, a pior coisa foi um único join interno duplo que não conseguimos eliminar por causa da complexidade do lado do servidor. Era frustrante não podermos implementar o servidor nós mesmos; a equipe com quem trabalhávamos era muito ruim em desenvolvimento de software e mudava coisas sem comunicar toda a especificação do backend, o que fazia o build quebrar de repente.
Também usamos um banco sqlite3 separado para replays do jogo; depois do fim da partida, era possível reproduzir o jogo inteiro e ver o que cada oponente fez. Isso também era ótimo para testes automatizados.
No sistema de controle de mudanças lix, também acabamos colocando os arquivos no SQLite em vez de lidar com o sistema de arquivos e com git. Este texto aborda o problema que enfrentamos: https://opral.substack.com/i/150054233/breaking-git-compatib...
Problemas como bloqueio de arquivos e concorrência são resolvidos pelo SQLite.
Com SQLite, é possível consultar arquivos com SQL em vez de usar APIs de sistema de arquivos específicas de cada plataforma.
As consultas SQL podem ser escritas com segurança de tipos usando Kysely https://kysely.dev/, sem ORM.
Mas é preciso tomar cuidado: bancos SQLite não encolhem se você não fizer vacuum. Basicamente, isso copia os dados para um arquivo separado e apaga o original.
É algo que precisa ser feito manualmente em um momento que faça sentido dentro da aplicação, então, ao usar para gravar e apagar dados binários, é preciso ficar atento ao uso de disco.
Curiosamente, o CMS gerador de site estático que criei funciona exatamente do modo oposto ao descrito aqui.
Enquanto o site está sendo desenvolvido/atualizado, todas as páginas e posts são entradas em um banco SQLite e são manipulados por uma interface web que mostra a versão editável do site.
Depois, o site é despejado no sistema de arquivos como páginas estáticas para implantação direta, ou baixado como zip e enviado para outro lugar, inclusive serviços de hospedagem totalmente estática.
Segundo “Appropriate Uses For SQLite” https://www.sqlite.org/whentouse.html do SQLite, o tráfego web que o SQLite consegue lidar depende de quão intensamente o site usa o banco de dados.
Em geral, sites com menos de 100 mil hits por dia devem funcionar bem com SQLite. 100 mil/dia é uma estimativa conservadora, não um limite superior rígido. Há casos em que o SQLite lidou com 10 vezes esse tráfego.
O site do SQLite (https://www.sqlite.org/) obviamente também usa SQLite e, em 2015, processava cerca de 400 mil a 500 mil requisições HTTP por dia, das quais 15% a 20% eram páginas dinâmicas que tocavam no banco de dados. O conteúdo dinâmico usa cerca de 200 instruções SQL por página web.
Essa configuração roda em uma única VM que compartilha o servidor físico com outras 23 VMs e, ainda assim, mantém a média de carga abaixo de 0,1 na maior parte do tempo. Referência: https://news.ycombinator.com/item?id=33975635
Para uma carga de trabalho predominantemente de leitura, como servir arquivos estáticos, o SQLite consegue lidar com muito mais. Se os headers de cache de conteúdo estiverem configurados, o navegador faz cache do conteúdo, então requisições ao servidor passam a ser necessárias apenas para novos clientes.
Na maioria dos casos de uso, não parece que o SQLite vá ser o gargalo.
A ideia de servir conteúdo estático com SQLite tendo como base apenas a página “35% Faster Than The Filesystem” de 2017 parece, para dizer o mínimo, mal amadurecida.
Servidores web modernos como o Nginx usam estratégias otimizadas para lidar com arquivos estáticos. Começa com sendfile, vai até operações com io_uring e splice, e roda dentro de um pool de threads bem projetado sobre epoll, kqueue ou eventport, conforme a necessidade.
Já o melhor que o SQLite pode oferecer por padrão é algo como suporte a I/O com mapeamento em memória (https://www.sqlite.org/mmap.html).
Essa abordagem pode fazer sentido para um serviço de cliente único, como uma webapp hospedada localmente (veja também https://github.com/electron/asar). Mas, em um site grande, como outros comentários disseram, é uma tentativa de resolver um problema que não existe.
Faço bastante computação científica de alto desempenho e, especialmente ao acessar dados em paralelo, muitas vezes a abordagem mais flexível e rápida foi um banco de dados SQLite somente leitura em um ramdisk.
Parece extremamente hacky, mas é mais fácil de configurar e mais rápido do que qualquer outra abordagem que encontrei até agora.
Vi um amigo da área de astronomia dizer que muita gente nas ciências precisa se familiarizar com bancos de dados. Caso contrário, acaba, sem perceber, gastando um esforço enorme para criar seu próprio banco de dados péssimo.
O motivo de essa abordagem não ser mais comum é que sistemas de arquivos são excelentes para lidar com arquivos.
Se você precisa de atualizações atômicas, faça checkout em um novo diretório e troque o link simbólico.
Já vi várias versões de usar um banco de dados como se fosse um sistema de arquivos; há aspectos bons, mas quando algo dá errado também há aspectos que viram um pesadelo.
Aí dá para usar algo como btrfs e fazer deduplicação na camada do sistema de arquivos.
Há um problema no argumento de que, como muitos arquivos podem mudar durante uma atualização do app, usar um banco de dados permitiria tratar todas as mudanças de forma atômica em uma transação e evitar servir páginas web quebradas durante a troca de versão.
Isso se deve ao fato de que o arquivo SQLite é bloqueado para leituras durante escritas para alcançar isolamento serializável. Então a conclusão é que é melhor fazer as operações de banco de dados em um arquivo offline e trocar o arquivo antigo pelo novo em produção.
No fim, isso é quase o mesmo que usar um arquivo tar ou um diretório separado a ser substituído pelo novo conteúdo.
Arquivos estáticos são muito mais fáceis de servir estaticamente. Não há necessidade de servi-los a partir de um programa que gerencia conexões SQLite em tempo real e tenta alcançar alguma mágica estranha de “concorrência de atualização”. Esse problema pode ser resolvido sem nenhuma dificuldade.
Gerenciar um CMS como um banco de dados SQLite é aceitável, mas, se o conteúdo é estático e servido em tempo real, é melhor usar arquivos estáticos.