Em busca de um SQLite mais rápido
(avi.im)- Em ambientes serverless e de edge, quando várias instâncias do SQLite rodam juntas, a espera por I/O síncrona aumenta a latência de cauda, e pesquisadores de Helsinki e Cambridge testaram uma forma de reduzi-la com I/O assíncrona e desagregação de armazenamento
- O io_uring do Linux permite que a aplicação continue executando outras tarefas durante requisições de I/O por meio de uma fila de submissão e uma fila de conclusão, servindo de base para reduzir bloqueios de thread
- No SQLite, se durante a execução de
sqlite3_step()a página B-Tree necessária não estiver no cache, o disco é lido com I/O síncrona comoread()do POSIX, e a thread para até o fim da operação - Em vez de trocar apenas chamadas POSIX, os pesquisadores alteraram a VM e a BTree no projeto de reescrita em Rust Limbo para se adequarem a um modelo de execução assíncrono
- Nos benchmarks, a latência de cauda p999 caiu em até 100 vezes, mas p90 e p99 ficaram quase iguais aos do SQLite, e a avaliação com múltiplos readers/writers segue como trabalho futuro
Pesquisa para tornar o SQLite mais rápido
- Pesquisadores da University of Helsinki e de Cambridge discutem em “Serverless Runtime / Database Co-Design With Asynchronous I/O” como aplicar I/O assíncrona e desagregação de armazenamento ao SQLite
- Este artigo serve de base para o projeto Limbo, uma reescrita do SQLite em Rust
- Como é um paper de workshop, ele é curto e tem foco em serverless e computação de edge
- A ideia central é que, mesmo que o SQLite já seja rápido, a latência de cauda em ambientes multitenant pode cair ainda mais se o modelo de execução mudar
Como o io_uring reduz a espera por I/O
- O io_uring do kernel Linux fornece uma interface de I/O assíncrona
- O nome vem do ring buffer compartilhado entre espaço do usuário e espaço do kernel, o que reduz o overhead de cópia de buffers entre os dois lados
- Depois de enviar uma requisição de I/O, a aplicação pode continuar executando outras tarefas até que o sistema operacional sinalize a conclusão
- O fluxo de funcionamento é o seguinte
- A chamada de sistema
io_uring_setup()configura duas áreas de memória: a fila de submissão e a fila de conclusão - A aplicação coloca a requisição de I/O na fila de submissão e usa
io_uring_enter()para avisar o sistema operacional a iniciar o processamento - Diferentemente de
read()ewrite(), o controle volta ao espaço do usuário sem bloquear a thread - A aplicação executa outras tarefas e faz polling periódico da fila de conclusão para verificar se a operação terminou
- A chamada de sistema
O gargalo de I/O síncrona na execução de consultas do SQLite
- Aplicações com SQLite abrem o arquivo do banco de dados com
sqlite3_open(), e nesse processo são feitas chamadas de I/O de baixo nível do sistema operacional, comoopendo POSIX sqlite3_prepare()transforma instruções SQL comoSELECTeINSERTem uma sequência de instruções de bytecodesqlite3_step()executa essas instruções de bytecode até produzir uma linha para leitura ou até a execução terminar- Se houver uma linha para ler, retorna
SQLITE_ROW - Quando a instrução termina, retorna
SQLITE_DONE
- Se houver uma linha para ler, retorna
- Durante a execução, o pager de backend é chamado e percorre a B-Tree que representa tabelas e linhas
- Se a página B-Tree necessária não estiver no cache de páginas do SQLite, ocorre acesso ao disco
- O SQLite lê o conteúdo da página do disco para a memória usando I/O síncrona, como
readdo POSIX - Nesse período,
sqlite3_step()bloqueia a thread do kernel - Para fazer trabalho concorrente enquanto espera pelo I/O, a aplicação precisa usar mais threads
- O SQLite lê o conteúdo da página do disco para a memória usando I/O síncrona, como
Por que embutir SQL em serverless e edge
- Se a computação serverless roda na edge e o banco de dados está na nuvem, surge o custo de ida e volta na rede entre a função serverless e a nuvem
- Uma opção é posicionar os dados junto da edge, mas a proposta é que uma abordagem melhor seja embutir o banco de dados dentro do runtime de edge
- O Cloudflare Workers já alcança algo nessa linha, mas expõe uma interface de KV
- KV não se encaixa bem em todos os domínios de problema
- Mapear dados tabulares para o modelo KV piora a experiência do desenvolvedor
- Também há custo de serialização e desserialização
- SQL pode ser mais adequado, e o SQLite, por ser um banco de dados embarcado, pode ser incluído diretamente no runtime serverless
Por que não é simples apenas trocar o SQLite para io_uring
- O SQLite usa I/O síncrona tradicional baseada em
read()ewrite()do POSIX - Em aplicações pequenas isso pode não ser um grande problema, mas ao executar centenas de bancos SQLite em um único servidor, isso pode virar gargalo
- Em ambientes onde é preciso maximizar a utilização dos recursos do servidor, a I/O síncrona vira uma limitação
- O SQLite também enfrenta questões de concorrência e multitenancy
- Como o I/O é síncrono e bloqueante, aplicações na mesma máquina disputam recursos
- Como resultado, a latência aumenta
- Não é fácil simplesmente substituir chamadas de I/O do POSIX por io_uring
- Aplicações que usam I/O bloqueante precisam ser reprojetadas para o modelo de I/O assíncrona do io_uring
- A biblioteca SQLite precisa conseguir devolver o controle à aplicação enquanto o I/O está em andamento
- Em vez de alterar só algumas chamadas do SQLite, os pesquisadores optaram por reescrevê-lo em Rust e usar io_uring
O modelo de execução assíncrona do Limbo
- O Limbo é um projeto que reescreve o SQLite em Rust e altera os componentes VM e BTree para suportar I/O assíncrona
- Instruções de bytecode síncronas são substituídas por equivalentes assíncronos
- Por exemplo, a instrução
Nextavança o cursor e, se necessário, busca a próxima página- Na versão síncrona tradicional, se houver I/O de disco, a execução bloqueia até ler a página e retornar ao chamador
- Na versão assíncrona,
NextAsyncé enviada e retorna imediatamente - O chamador então pode bloquear depois ou executar outro trabalho
- A I/O assíncrona elimina bloqueios e melhora a concorrência
- Para elevar ainda mais a utilização de recursos, também se propõe a desagregação de armazenamento, separando engine de consulta e engine de armazenamento
- Como explicação relacionada, o texto também aponta para Disaggregated Storage - a brief introduction
Resultados dos benchmarks e perguntas em aberto
- Os benchmarks simulam um runtime serverless multitenant
- Cada tenant tem seu próprio banco de dados embarcado
- O número de tenants varia de 1 a 100, em incrementos de 10
- No SQLite, cada tenant usa uma thread separada, e as consultas são executadas em cada thread para medição
- A consulta usada foi
SELECT * FROM users LIMIT 100, repetida 1000 vezes - O Limbo executou o mesmo experimento, mas usando corrotinas Rust
- Como resultado, a latência de cauda em p999 caiu em até 100 vezes
- A latência de consulta do SQLite não se degradou gradualmente com o aumento do número de threads
- O trabalho ainda está em andamento, e o paper deixa algumas questões em aberto
- Em Future Work, os autores tratam de benchmarks adicionais com vários readers e writers
- Os ganhos ficam evidentes principalmente depois de p999
- O desempenho em p90 e p99 é quase igual ao do SQLite
- O código do Limbo está disponível como open source
- Atualmente, o Limbo é um projeto oficial da Turso, que também publicou um texto de apresentação
Ainda não há comentários.