Desempenho de aplicativos de rede Linux. Introdução
Os aplicativos da Web são agora usados em todos os lugares e, entre todos os protocolos de transporte, o HTTP ocupa a maior parte. Ao estudar as nuances do desenvolvimento de aplicações web, a maioria das pessoas presta muito pouca atenção ao sistema operacional onde essas aplicações realmente são executadas. A separação entre desenvolvimento (Dev) e operações (Ops) só piorou a situação. Mas com a ascensão da cultura DevOps, os desenvolvedores estão se tornando responsáveis pela execução de seus aplicativos na nuvem, por isso é muito útil que eles se familiarizem completamente com o back-end do sistema operacional. Isto é especialmente útil se você estiver tentando implantar um sistema para milhares ou dezenas de milhares de conexões simultâneas.
As limitações nos serviços web são muito semelhantes às de outras aplicações. Sejam balanceadores de carga ou servidores de banco de dados, todos esses aplicativos apresentam problemas semelhantes em um ambiente de alto desempenho. Compreender essas limitações fundamentais e como superá-las em geral ajudará você a avaliar o desempenho e a escalabilidade de seus aplicativos web.
Estou escrevendo esta série de artigos em resposta a perguntas de jovens desenvolvedores que desejam se tornar arquitetos de sistemas bem informados. É impossível compreender claramente as técnicas de otimização de aplicativos Linux sem mergulhar nos fundamentos de como elas funcionam no nível do sistema operacional. Embora existam muitos tipos de aplicativos, nesta série quero explorar aplicativos baseados na Web em vez de aplicativos de desktop, como navegadores ou editores de texto. Este material é destinado a desenvolvedores e arquitetos que desejam entender como funcionam os programas Linux ou Unix e como estruturá-los para alto desempenho.
Linux é sala de servidores sistema operacional e, na maioria das vezes, seus aplicativos são executados neste sistema operacional. Embora eu diga "Linux", na maioria das vezes você pode assumir com segurança que me refiro a todos os sistemas operacionais do tipo Unix em geral. No entanto, não testei o código que acompanha em outros sistemas. Portanto, se você estiver interessado no FreeBSD ou no OpenBSD, seus resultados podem variar. Quando tento algo específico do Linux, eu indico.
Embora você possa usar esse conhecimento para criar um aplicativo do zero e ele será perfeitamente otimizado, é melhor não fazer isso. Se você escrever um novo servidor web em C ou C++ para o aplicativo de negócios da sua organização, este pode ser seu último dia de trabalho. Porém, conhecer a estrutura desses aplicativos ajudará na escolha dos programas existentes. Você poderá comparar sistemas baseados em processos com sistemas baseados em threads, bem como com sistemas baseados em eventos. Você entenderá e apreciará por que o Nginx tem um desempenho melhor do que o Apache httpd, por que um aplicativo Python baseado em Tornado pode atender mais usuários em comparação com um aplicativo Python baseado em Django.
ZeroHTTPd: ferramenta de aprendizagem
ZeroHTTPd é um servidor web que escrevi do zero em C como ferramenta de ensino. Não possui dependências externas, incluindo acesso ao Redis. Executamos nossos próprios procedimentos Redis. Veja abaixo para mais detalhes.
Embora pudéssemos discutir longamente a teoria, não há nada melhor do que escrever código, executá-lo e comparar todas as arquiteturas de servidores entre si. Este é o método mais óbvio. Portanto, escreveremos um servidor web ZeroHTTPd simples usando cada modelo: baseado em processo, baseado em thread e baseado em evento. Vamos verificar cada um desses servidores e ver como eles funcionam em comparação entre si. ZeroHTTPd é implementado em um único arquivo C. O servidor baseado em eventos inclui utash, uma excelente implementação de tabela hash que vem em um único arquivo de cabeçalho. Em outros casos, não há dependências, para não complicar o projeto.
Existem muitos comentários no código para ajudá-lo a entender. Sendo um servidor web simples em poucas linhas de código, ZeroHTTPd também é uma estrutura mínima para desenvolvimento web. Possui funcionalidade limitada, mas é capaz de servir arquivos estáticos e páginas "dinâmicas" muito simples. Devo dizer que ZeroHTTPd é bom para aprender como criar aplicativos Linux de alto desempenho. Em geral, a maioria dos serviços web espera por solicitações, verifica-as e processa-as. Isso é exatamente o que o ZeroHTTPd fará. Esta é uma ferramenta de aprendizagem, não de produção. Não é ótimo no tratamento de erros e é improvável que possua as melhores práticas de segurança (ah, sim, eu usei strcpy) ou os truques inteligentes da linguagem C. Mas espero que faça bem o seu trabalho.
Página inicial do ZeroHTTPd. Ele pode gerar diferentes tipos de arquivos, incluindo imagens
Aplicativo para livro de visitas
Os aplicativos da web modernos geralmente não estão limitados a arquivos estáticos. Eles têm interações complexas com vários bancos de dados, caches, etc. Portanto, criaremos uma aplicação web simples chamada "Livro de Visitas", onde os visitantes deixam entradas sob seus nomes. O livro de visitas armazena entradas deixadas anteriormente. Há também um contador de visitantes na parte inferior da página.
Aplicação web "Livro de Visitas" ZeroHTTPd
O contador de visitantes e as entradas do livro de visitas são armazenados no Redis. Para comunicações com Redis são implementados procedimentos próprios, que não dependem da biblioteca externa. Não sou um grande fã de lançar código homebrew quando existem soluções publicamente disponíveis e bem testadas. Mas o objetivo do ZeroHTTPd é estudar o desempenho do Linux e o acesso a serviços externos, ao mesmo tempo que atender solicitações HTTP tem um sério impacto no desempenho. Devemos controlar totalmente as comunicações com Redis em cada uma de nossas arquiteturas de servidor. Em algumas arquiteturas utilizamos chamadas de bloqueio, em outras utilizamos procedimentos baseados em eventos. Usar uma biblioteca cliente Redis externa não fornecerá esse controle. Além disso, nosso pequeno cliente Redis executa apenas algumas funções (obter, definir e incrementar uma chave; obter e anexar a um array). Além disso, o protocolo Redis é extremamente elegante e simples. Você nem precisa ensiná-lo especialmente. O próprio fato de o protocolo fazer todo o trabalho em cerca de cem linhas de código mostra como ele é bem pensado.
A figura a seguir mostra o que o aplicativo faz quando o cliente (navegador) solicita /guestbookURL.
Como funciona o aplicativo de livro de visitas
Quando uma página do livro de visitas precisa ser emitida, há uma chamada para o sistema de arquivos para ler o modelo na memória e três chamadas de rede para o Redis. O arquivo de modelo contém a maior parte do conteúdo HTML da página da captura de tela acima. Existem também espaços reservados especiais para a parte dinâmica do conteúdo: postagens e contador de visitantes. Nós os recebemos do Redis, inserimos na página e entregamos ao cliente o conteúdo totalmente formado. A terceira chamada ao Redis pode ser evitada porque o Redis retorna o novo valor da chave quando incrementado. Porém, para nosso servidor, que possui uma arquitetura assíncrona baseada em eventos, muitas chamadas de rede são um bom teste para fins de aprendizado. Portanto, descartamos o valor de retorno do Redis do número de visitantes e o consultamos com uma chamada separada.
Arquiteturas de servidor ZeroHTTPd
Estamos construindo sete versões do ZeroHTTPd com a mesma funcionalidade, mas arquiteturas diferentes:
Iterativo
Servidor Fork (um processo filho por solicitação)
Servidor pré-fork (pré-fork de processos)
Servidor com threads de execução (uma thread por solicitação)
Servidor com criação pré-thread
Baseado em arquitetura poll()
Baseado em arquitetura epoll
Medimos o desempenho de cada arquitetura carregando o servidor com solicitações HTTP. Mas ao comparar arquiteturas altamente paralelas, o número de consultas aumenta. Testamos três vezes e calculamos a média.
Metodologia de teste
Configuração de teste de carga ZeroHTTPd
É importante que ao executar testes, todos os componentes não sejam executados na mesma máquina. Nesse caso, o sistema operacional incorre em sobrecarga adicional de agendamento à medida que os componentes competem pela CPU. Medir a sobrecarga do sistema operacional de cada uma das arquiteturas de servidor selecionadas é um dos objetivos mais importantes deste exercício. Adicionar mais variáveis será prejudicial ao processo. Portanto, a configuração da imagem acima funciona melhor.
O que cada um desses servidores faz?
load.unixism.net: É aqui que executamos ab, utilitário Apache Benchmark. Ele gera a carga necessária para testar nossas arquiteturas de servidores.
nginx.unixism.net: Às vezes queremos executar mais de uma instância de um programa de servidor. Para fazer isso, o servidor Nginx com as configurações apropriadas funciona como um balanceador de carga vindo de ab aos nossos processos de servidor.
zerohttpd.unixism.net: Aqui executamos nossos programas de servidor em sete arquiteturas diferentes, uma de cada vez.
redis.unixism.net: Este servidor executa o daemon Redis, onde as entradas do livro de visitas e os contadores de visitantes são armazenados.
Todos os servidores são executados no mesmo núcleo do processador. A ideia é avaliar o desempenho máximo de cada arquitetura. Como todos os programas de servidor são testados no mesmo hardware, esta é uma base para comparação. Minha configuração de teste consiste em servidores virtuais alugados da Digital Ocean.
O que estamos medindo?
Você pode medir diferentes indicadores. Avaliamos o desempenho de cada arquitetura em uma determinada configuração carregando os servidores com solicitações em diferentes níveis de paralelismo: a carga cresce de 20 para 15 usuários simultâneos.
Resultados do teste
O gráfico a seguir mostra o desempenho de servidores em diferentes arquiteturas em diferentes níveis de paralelismo. O eixo y é o número de solicitações por segundo, o eixo x são conexões paralelas.
Abaixo está uma tabela com os resultados.
solicitações por segundo
simultaneidade iterativo garfo pré-garfo transmissão pré-transmissão pol epoll
20
7
112
2100
1800
2250
1900
2050
50
7
190
2200
1700
2200
2000
2000
100
7
245
2200
1700
2200
2150
2100
200
7
330
2300
1750
2300
2200
2100
300
-
380
2200
1800
2400
2250
2150
400
-
410
2200
1750
2600
2000
2000
500
-
440
2300
1850
2700
1900
2212
600
-
460
2400
1800
2500
1700
2519
700
-
460
2400
1600
2490
1550
2607
800
-
460
2400
1600
2540
1400
2553
900
-
460
2300
1600
2472
1200
2567
1000
-
475
2300
1700
2485
1150
2439
1500
-
490
2400
1550
2620
900
2479
2000
-
350
2400
1400
2396
550
2200
2500
-
280
2100
1300
2453
490
2262
3000
-
280
1900
1250
2502
grande propagação
2138
5000
-
grande propagação
1600
1100
2519
-
2235
8000
-
-
1200
grande propagação
2451
-
2100
10 000
-
-
grande propagação
-
2200
-
2200
11 000
-
-
-
-
2200
-
2122
12 000
-
-
-
-
970
-
1958
13 000
-
-
-
-
730
-
1897
14 000
-
-
-
-
590
-
1466
15 000
-
-
-
-
532
-
1281
Pelo gráfico e tabela pode-se observar que acima de 8000 solicitações simultâneas restam apenas dois players: pré-fork e epoll. À medida que a carga aumenta, um servidor baseado em poll tem um desempenho pior do que um servidor de streaming. A arquitetura de pré-criação de threads é um concorrente digno do epoll, uma prova de quão bem o kernel do Linux agenda um grande número de threads.
Código-fonte ZeroHTTPd
Código-fonte ZeroHTTPd aqui. Existe um diretório separado para cada arquitetura.
Além de sete diretórios para todas as arquiteturas, há mais dois no diretório de nível superior: público e modelos. O primeiro contém o arquivo index.html e a imagem da primeira captura de tela. Você pode colocar outros arquivos e pastas lá, e o ZeroHTTPd deve servir esses arquivos estáticos sem problemas. Se o caminho no navegador corresponder ao caminho na pasta pública, o ZeroHTTPd procurará o arquivo index.html neste diretório. O conteúdo do livro de visitas é gerado dinamicamente. Possui apenas uma página inicial e seu conteúdo é baseado no arquivo 'templates/guestbook/index.html'. ZeroHTTPd adiciona facilmente páginas dinâmicas para extensão. A ideia é que os usuários possam adicionar modelos a este diretório e estender o ZeroHTTPd conforme necessário.
Para construir todos os sete servidores, execute make all do diretório de nível superior - e todas as compilações aparecerão neste diretório. Os arquivos executáveis procuram os diretórios public e templates no diretório a partir do qual são iniciados.
API do Linux
Você não precisa ser bem versado na API do Linux para entender as informações desta série de artigos. No entanto, recomendo ler mais sobre este tópico; existem muitos recursos de referência na Internet. Embora iremos abordar diversas categorias de APIs do Linux, nosso foco será principalmente em processos, threads, eventos e pilha de rede. Além de livros e artigos sobre a API do Linux, também recomendo a leitura de mana para chamadas de sistema e funções de biblioteca utilizadas.
Desempenho e escalabilidade
Uma observação sobre desempenho e escalabilidade. Teoricamente, não há conexão entre eles. Você pode ter um serviço web que funcione muito bem, com um tempo de resposta de alguns milissegundos, mas que não seja escalável. Da mesma forma, pode haver um aplicativo Web com desempenho insatisfatório que leva alguns segundos para responder, mas é dimensionado em dezenas para lidar com dezenas de milhares de usuários simultâneos. No entanto, a combinação de alto desempenho e escalabilidade é uma combinação muito poderosa. Aplicações de alto desempenho geralmente utilizam recursos com moderação e, assim, atendem com eficiência mais usuários simultâneos no servidor, reduzindo custos.
Tarefas de CPU e E/S
Finalmente, na computação sempre existem dois tipos possíveis de tarefas: para E/S e CPU. Receber solicitações pela Internet (E/S de rede), servir arquivos (E/S de rede e disco), comunicar-se com o banco de dados (E/S de rede e disco) são todas atividades de E/S. Algumas consultas ao banco de dados podem consumir um pouco da CPU (classificação, média de um milhão de resultados, etc.). A maioria dos aplicativos da Web é limitada pelo máximo de E/S possível e o processador raramente é usado em capacidade total. Quando você percebe que alguma tarefa de E/S está usando muita CPU, é provavelmente um sinal de arquitetura de aplicativo ruim. Isso pode significar que os recursos da CPU são desperdiçados no gerenciamento de processos e na alternância de contexto - e isso não é totalmente útil. Se você estiver fazendo algo como processamento de imagem, conversão de arquivo de áudio ou aprendizado de máquina, o aplicativo exigirá recursos poderosos de CPU. Mas para a maioria das aplicações este não é o caso.