
No ecossistema PHP, atualmente existem dois conectores para operar com o servidor Tarantool – é a extensão oficial PECL , escrita em C, e , escrita em PHP. Eu sou o autor da última.
Neste artigo, gostaria de compartilhar os resultados dos testes de desempenho de ambas as bibliotecas e mostrar como, com algumas mudanças mínimas no código, é possível alcançar um aumento de 3 a 5% no desempenho (em testes sintéticos!).
O que vamos testar?
Vamos testar os conectores mencionados acima síncronos conectores, executados de forma assíncrona, paralela e assíncrona-paralela. 🙂 Também não queremos alterar o código dos próprios conectores. Atualmente, existem algumas extensões disponíveis que permitem alcançar o desejado:
- – um framework assíncrono de alto desempenho para PHP. É utilizado por gigantes da internet como Alibaba e Baidu. Desde a versão 4.1.0, apareceu o método mágico SwooleRuntime::enableCoroutine(), permitindo "com uma linha de código converter bibliotecas de rede síncronas PHP em assíncronas".
- Async – até recentemente, uma extensão bastante promissora para trabalho assíncrono em PHP. Por que até recentemente? Infelizmente, por uma razão que eu desconheço, o autor deletou o repositório e o futuro do projeto é incerto. Teremos que usar das forks. Assim como o Swoole, essa extensão permite, com um simples gesto, ativar a assíncronicidade substituindo as implementações padrão de TCP e TLS por suas versões assíncronas. Isso é feito através da opção "async.tcp = 1«.
- – uma extensão bastante nova do famoso Joe Watkins, autor de bibliotecas como phpdbg, apcu, pthreads, pcov, uopz. A extensão fornece uma API para trabalho multithread em PHP e é posicionada como uma substituição para pthreads. Uma limitação significativa da biblioteca é que ela funciona apenas com a versão ZTS (Zend Thread Safe) do PHP.
Como vamos testar?
Vamos iniciar uma instância do Tarantool com o log de gravação antecipada desativado (wal_mode = none) e um buffer de rede aumentado (readahead = 1 * 1024 * 1024). A primeira opção eliminará o trabalho com o disco, a segunda – permitirá ler mais solicitações do buffer do sistema operacional, minimizando assim o número de chamadas de sistema.
Para benchmarks que lidam com dados (inserção, exclusão, leitura etc.), antes de iniciar o benchmark será (re)criado um espaço memtx, onde os valores do índice primário são gerados por um gerador de valores inteiros ordenados (sequence).
O DDL do espaço é assim:
space = box.schema.space.create(config.space_name, {id = config.space_id, temporary = true})
space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}, sequence = true})
space:format({{name = 'id', type = 'unsigned'}, {name = 'name', type = 'string', is_nullable = false}})Se necessário, antes de iniciar o benchmark, o espaço é preenchido com 10.000 tuplas do tipo
{id, "tuplе_"}O acesso às tuplas é feito por um valor de chave aleatória.
O próprio benchmark consiste em uma única solicitação ao servidor, que é executada 10.000 vezes (revoluções), que, por sua vez, são realizadas em iterações. As iterações são repetidas até que todas as variações de tempo entre 5 iterações estejam dentro da margem de erro aceitável de 3%*. Após isso, o resultado médio é tomado. Entre as iterações, há uma pausa de 1 segundo, para não permitir que o processador entre em throttling. O coletor de lixo do Lua é desligado antes de cada iteração e é forçado a ser acionado após sua conclusão. O processo PHP é iniciado apenas com as extensões necessárias para o benchmark, com buffer de saída ativado e coletor de lixo desativado.
* O número de revoluções, iterações e o limite de erro podem ser alterados nas configurações do benchmark.
Ambiente de teste
Os resultados publicados abaixo foram obtidos em um MacBookPro (2015), sistema operacional — Fedora 30 (versão do núcleo 5.3.8-200.fc30.x86_64). O Tarantool foi executado no Docker com o parâmetro "--network host".
Versões dos pacotes:
Tarantool: 2.3.0-115-g5ba5ed37e
Docker: 19.03.3, build a872fc2f86
PHP: 7.3.11 (cli) (compilado: 22 de outubro de 2019 08:11:04)
tarantool/client: 0.6.0
rybakit/msgpack: 0.6.1
ext-tarantool: 0.3.2 (+ patch para 7.3)*
ext-msgpack: 2.0.3
ext-async: 0.3.0-8c1da46
ext-swoole: 4.4.12
ext-parallel: 1.1.3
* Infelizmente, o conector oficial não funciona com a versão do PHP > 7.2. Para compilar e executar a extensão no PHP 7.3, foi necessário usar .
Resultados
Modo síncrono
O protocolo do Tarantool usa formato binário para serializar mensagens. No conector PECL, a serialização está profundamente escondida nas profundezas da biblioteca e não é possível influenciar o processo de codificação a partir do código userland . O conector em PHP puro, por outro lado, oferece a possibilidade de personalizar o processo de codificação expandindo o codificador padrão ou utilizando a própria implementação. Por padrão, estão disponíveis dois codificadores, um baseado em (extensão oficial do MessagePack PECL), o outro é baseado em (em PHP puro).
Antes de comparar os conectores, vamos medir o desempenho dos codificadores MessagePack para o conector PHP e, nos testes futuros, utilizaremos aquele que mostrar o melhor resultado:

Embora a versão PHP (Pura) seja mais lenta que a extensão PECL, em projetos reais eu ainda recomendaria utilizar , pois na extensão oficial do MessagePack a especificação do formato está implementada apenas parcialmente (por exemplo, não há suporte para tipos de dados personalizados, sem os quais você não poderá usar Decimal — um novo tipo de dado apresentado no Tarantool 2.3) e possui uma série de outras (incluindo problemas de compatibilidade com PHP 7.4). Além disso, o projeto, de forma geral, parece estar abandonado.
Então, vamos medir o desempenho dos conectores em modo síncrono:

Como pode ser visto no gráfico, o conector PECL (Tarantool) apresenta melhor desempenho em comparação com o conector em PHP (Client). Mas isso não é surpresa, considerando que o último, além de ser implementado em uma linguagem mais lenta, realiza, na verdade, mais trabalho: a cada chamada, um novo objeto é criado Request e Response (no caso de Select ― ainda Critérios, e no caso de Update/Upsert ― Operações), entidades separadas Conexão, Packer e Handler também acrescentam sobrecarga. É evidente que a flexibilidade tem seu custo. No entanto, de forma geral, o interpretador PHP mostra um bom desempenho, embora haja diferença, mas ela é insignificante e talvez será ainda menor com o uso de preloading no PHP 7.4, sem mencionar o JIT no PHP 8.
Vamos em frente. No Tarantool 2.0 foi adicionado suporte ao SQL. Vamos tentar executar operações Select, Insert, Update e Delete usando o protocolo SQL e comparar os resultados com os equivalentes no noSQL (binários):

Os resultados do SQL não são muito impressionantes (lembrando que ainda estamos testando o modo síncrono). No entanto, eu não me preocuparia com isso antes do tempo, pois o suporte ao SQL ainda está em desenvolvimento ativo (relativamente recentemente, por exemplo, foi adicionada a suporte a ) e, judging pela lista , o mecanismo SQL aguarda várias otimizações no futuro.
Assíncrono
Bem, vamos ver agora como a extensão Async pode nos ajudar a melhorar os resultados acima. Para a programação assíncrona, a extensão fornece uma API baseada em corrotinas, que usaremos. Através de experiência prática, descobrimos que a quantidade ideal de corrotinas para o nosso ambiente é 25:

Distribuímos 10.000 operações entre 25 corrotinas e vemos o que conseguimos:

O número de operações por segundo aumentou mais de 3 vezes para !
Infelizmente, o conector PECL não iniciou com ext-async.
E quanto ao SQL?

Como vocês podem ver, no modo assíncrono, a diferença entre o protocolo binário e o SQL tornou-se dentro da margem de erro.
Swoole
Novamente, descobrimos a quantidade ideal de corrotinas, agora para o Swoole:

Vamos ficar com 25. Faremos o mesmo truque que com a extensão Async - distribuiremos 10.000 operações entre 25 corrotinas. Além disso, adicionaremos mais um teste, no qual dividiremos todo o trabalho em 2 processos (ou seja, cada processo executará 5.000 operações em 25 corrotinas). Os processos serão criados utilizando SwooleProcess.
Resultados:

O Swoole mostra um resultado ligeiramente inferior em comparação com o Async quando executado em um único processo, mas com 2 processos a situação muda radicalmente (o número 2 foi escolhido não por acaso, na minha máquina, exatamente 2 processos mostraram o melhor resultado).
Aliás, na extensão Async também há uma API para trabalhar com processos, no entanto, não percebi nenhuma diferença ao executar benchmarks em um ou vários processos (não posso descartar a possibilidade de algum erro da minha parte).
SQL vs protocolo binário:

Assim como com o Async, a diferença entre as operações binárias e SQL é neutralizada no modo assíncrono.
Parallel
Como a extensão Parallel não se trata de corrotinas, mas de threads, vamos medir a quantidade ideal de threads paralelas:

Ela é igual a 16 na minha máquina. Vamos executar os benchmarks dos conectores em 16 threads paralelas:

Como podem ver, o resultado é ainda melhor do que com as extensões assíncronas (exceto o Swoole executado em 2 processos). Observe que para o conector PECL, na parte de operações Update e Upsert, está vazio. Isso se deve ao fato de que essas operações falharam com erro - não sei se por causa do ext-parallel, ext-tarantool ou ambos.
Agora, vamos comparar o desempenho do SQL:

Notou alguma semelhança com o gráfico para os conectores executados de forma síncrona?
Tudo junto
Por fim, vamos reunir todos os resultados em um único gráfico para ver a imagem geral das extensões testadas. Adicionaremos ao gráfico apenas um novo teste que ainda não fizemos - iremos executar corrotinas Async em paralelo usando Parallel*. A ideia de integrar as extensões mencionadas acima já pelos autores, mas um consenso ainda não foi alcançado, teremos que fazer isso sozinhos.
* Não conseguimos executar corrotinas Swoole com Parallel; parece que essas extensões são incompatíveis.
Então, os resultados finais:

Em conclusão
Na minha opinião, os resultados são bastante impressionantes, e por algum motivo estou convencido de que isso ainda não é o limite! Cabe a vocês decidir se isso é necessário em um projeto real, apenas direi que para mim foi um experimento interessante, permitindo avaliar o quanto pode ser "extraído" de um conector TCP síncrono com o mínimo de esforço. Se você tiver ideias para melhorar os benchmarks - ficarei feliz em considerar seu pull request. Todo o código com instruções de execução e resultados foi publicado em um .
Fonte: habr.com
