2 pontos por GN⁺ 2024-12-17 | Ainda não há comentários. | Compartilhar no WhatsApp
  • 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 como read() 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() e write(), 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

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, como open do POSIX
  • sqlite3_prepare() transforma instruções SQL como SELECT e INSERT em uma sequência de instruções de bytecode
  • sqlite3_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
  • 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 read do 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

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() e write() 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 Next avanç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.

Ainda não há comentários.