Pela relevância, as expectativas mínimas de boa performance devem ser determinadas o mais cedo possível. Depois disso, devem ser consideradas na avaliação das proposições de design arquiteturais candidatas.
Definindo performance
De maneira superficial, performance, como atributo de qualidade, trata do tempo que um sistema necessita para completar uma determinada categoria de atividades. De forma mais precisa, entretanto, a performance também tem relação direta com a capacidade para suportar workloads – com tamanhos diversos em intervalos com mínimos e máximos bem-definidos – e restrições de recursos computacionais.
Relação com o custo
Melhorar a performance sempre impacta o custo, por isso, é importante determinar que abordagem utilizar. As vezes, melhorar a infraestrutura custa menos do que melhorar o código. Outras vezes, melhorar o código compensa a economia em infraestrutura. É importante ter sensibilidade.
Aspectos determinantes para a performance
Há quatro aspectos determinantes para a performance de um sistema de software e que, por isso, são preocupações arquiteturais: tamanho do workload, restrições para recursos computacionais, estratégias de enfileiramento e qualidade interna de implementação dos componentes.
Tamanho do workload
É fundamental, para o design de sistemas com boa performance, conhecer qual será o tamanho do workload que deverá ser suportado. Geralmente, é útil (e até mais fácil) realizar três tipos de previsões: pessimista, otimista e realista.
É a partir da previsão do workload que é possível determinar se o dimensionamento da infraestrutura é suficiente.
Restrições para recursos computacionais
O conhecimento das capacidades de processamento e características de performance das tecnologias autorizadas é determinante, no design arquitetural, para decisões relacionadas a organização e estrutura dos componentes.
As capacidades de resposta das tecnologias permitem antecipar qual será a realidade da performance, inclusive, estratégias para mitigar riscos de saturação.
Hardware faz diferença!
Certa vez, o time de desenvolvimento de um cliente comprometeu semanas de trabalho “otimizando” uma feature complexa que fazia uso intensivo de leituras e escritas em dispositivo persistente. Alguns testes apontavam ganhos de performance de 2000%! Entretanto, os ganhos não foram percebidos no ambiente produtivo.
No ambiente de desenvolvimento e testes o dispositivo persistente era um HDD. Em ambiente produtivo, era um SSD…
Estratégias de enfileiramento
Invariavelmente, em qualquer sistema, recursos computacionais poderão não estar disponíveis sempre que uma demada ocorrer – seja por não estarem em estado de prontidão ou por contenção em outra atividade. Sempre que isso acontece, há espera (wait time).
Qualidade interna de implementação dos componentes
De pouco adianta haverem bons recursos computacionais se o código não fazer o uso correto destes recursos. Processadores com múltiplos core e sistemas single-thread. Memória de sobra e processadores potentes, mas interrupções frequentes de execução para coletas de lixo evitáveis. Etc.
Pareto e a performance
Também é comum observar que 80% dos recursos computacionais de um sistema são consumidos por 20% do código. Há uma visão romântica de que a melhoria da performance ocorre “escovando bits“, entretanto, na prática, geralmente os ganhos mais percebidos acontecem por adaptações do design arquitetural, por exemplo, pela adição de caching, buscando minimizar o uso de recursos com alto custo computacional, como a rede.
Em sistemas com arquitetura otimizada para a performance, que adotam algoritmos certos e estruturas de dados apropriadas, eventualmente, observa-se uma espécie de Pareto3 (0,8% do código, responsável por 51% dos recursos). Nesses casos, e apenas nesses casos, há espaço para implantação de micro otimizações.
Relação com a escalabilidade
Performance e escalabilidade são atributos relacionados, porém distintos. A escalabilidade diz respeito a capacidade de um sistema de manter sua performance, em cenários de aumento de workload, mediante a adição de recursos computacionais em proporção favorável.
Para tornar mais clara a diferença, considere um sistema de e-commerce. Nele, a performance terá relação, por exemplo, com o tempo necessário para que um número esperado de clientes efetivem o checkout. Enquanto isso, a escalabilidade tem relação com a capacidade de ajustar o sistema para que quantidades maiores de clientes possam ser atendidos, mediante adição razoável de recursos (em proporção igual ou menor ao crescimento do workload).
A degradação da performance, frente ao aumento do workload, mesmo com a adição favorável de recursos, é indicador de atenção para problemas escalabilidade.
Métricas comuns relacionadas a performance
Tendo em vista o cuidado com a utilização de recursos, qualquer métrica de consumo poderá ser associada a performance. Já as métricas de tempo geralmente são response time, turnaround time ou throughput.
Eventualmente, o uso combinado de métricas de tempo e de consumo dão origem a bons indicadores.
Response time ou Turnaround time, nunca ambos
Considere uma API cujo uma das atribuições é receber arquivos, potencialmente grandes, com grande quantidade de dados para processamento. Como avaliar sua performance? Pelo response time, considerando tempo de resposta para o usuário após a requisição, ou pelo turnaround time, considerando o tempo total para processamento do arquivo recebido?
Processar arquivos grandes em uma interface interativa compromete o response time, não só daquela requisição, mas de todas as demais para aquele serviço. Afinal, “segura” recursos importantes e gera enfileiramento.
A abordagem mais recomendada é enfileirar o arquivo grande em um serviço de backend, que será avaliado pelo turnaround time e responder o mais rápido possível, ou seja, com o melhor response time.
Se há dúvidas sobre qual indicador usar, geralmente, a saída é decompor solução em dois componentes, cada um com sua métrica apropriada.
Response time
Em sistemas com interação com usuários, o response time indica o tempo consumido entre uma tarefa (ou ação) ser disparada e uma resposta satisfatória ser gerada. Valores comuns para response time são, geralmente, expressos de frações de segundo.
Turnaround time
Quando há necessidade de avaliar a performance do processamento de lotes de dados, como em dinâmicas de consolidação, a métrica de a ser observada é o turnaround time, ou seja, o tempo transcorrido entre o início do processamento do lote e a conclusão. Valores para turnaround time podem ser expressos em segundos, minutos, horas e até dias.
Não é incomum que o negócio relacione o turnaround time com “janelas de processamento”. Ou seja, períodos regulares, bem-definidos, que precisam ser respeitados.
Throughput
O throughput tem relação com a quantidade de operações de um determinado tipo que um sistema consegue executar em um intervalo de tempo definido.
Para fins de comparação, quando os lotes de processamento têm tamanho significativamente variável, ou em operações assíncronas respondendo uma fila, é interessante medir o throughput.
O throughput também é ser utilizado como indicativo de quantas requisições um sistema consegue atender simultaneamente.
Relação com recursos computacionais
Há uma relação forte entre a performance de um sistema e a capacidade dele de utilizar recursos adequadamente.
A melhor utilização dos recursos computacionais pode acontecer tanto pela utilização de técnicas de otimização de código quanto pela “substituição” do uso de recursos computacionais mais caros (por exemplo, acesso a disco ou a um servidor remoto) por outros mais baratos (memória RAM).
Relação com filas
A maioria dos recursos computacionais – tais como processadores, discos e mecanismos para transferência de dados – implementam algum suporte para enfileiramento. Em sistemas Web, por exemplo, requisições são enfileiradas sempre que o ritmo de chegada de novas requisições (arrival rate – ex: 5 req/s) for maior do que o ritmo com que os servidores conseguem atender essas requisições (service rate – ex: 3 req/s). O traffic intensity é determinado pela proporção percentual entre arrival rate e service rate.
Contenção e Disponibilidade de Recursos
Eventualmente, o enfileiramento não é “suportado” por um recurso em si, mas acontece em decorrência do próprio fluxo de execução do código e concorrência.
Quando um recurso computacional está sob contenção, ou seja, “bloqueado” atendendo uma demanda, nega novas requisições e cabe aos demandantes estabelecer políticas de retentativas, estabelecendo uma espécie de enfileiramento.
Eventualmente, um recurso está indisponível, mesmo que inoperante. Nesses casos, também caberá aos “demandantes” implementar políticas de retentativa.
Quando o traffic intensity permanece acima de 100% por algum tempo, há um aumento na queue length (quantidade de demandas esperando por serem atendidas e sendo atendidas em um determinado momento) e no wait time (tempo total na fila). A consequência direta observável é a degradação da performance. Além disso, caso demandas expirem (por time-out) ou o limite de capacidade da fila seja atingido, constata-se saturação do recurso.
Idealmente, recursos muito demandados devem, ao longo do tempo, otimizar o service time (tempo necessário para um recurso atender uma demanda e estar pronto para atender uma próxima) como medida para impactar menos o response time e, também, evitar a saturação.
A forma natural de “aliviar” o traffic intensity é paralelizando o atendimento das demandas em diversas instâncias do tipo de recurso computacional sendo demandado. Dessa forma, fazendo com que o service rate supere o arrival rate.
Em um sistema saudável, onde demandas não são perdidas e o traffic intensity seja menor que 100%, o throughput será igual ao arrival rate.
Utilization Law
A utilization law aponta que a taxa de utilização de um recurso computacional (eventualmente paralelizado) pode ser determinada pelo produto de seu throughput e a média do service time. Por exemplo, um recurso que trata 3 requisições por segundo, com um service time de 100ms tem utilização de 0.3 (ou, 30%).
Idealmente, a taxa de utilização de um recurso computacional não deve ultrapassar 80%.
Táticas modernas para garantir a performance
Sistemas com boa performance são aqueles que conseguem, utilizando os recursos computacionais especificados nas restrições, garantir que o tempo médio de atendimento de demandas, combinando processamentos (service times) e esperas (wait times), esteja conforme aos objetivos do negócio. Para isso, devem manter a taxa de utilização desses recursos computacionais abaixo de 80% considerando a carga máxima prevista.
A garantia da performance se dá, então, pelo controle rigoroso do arrival rate limitando-o ao especificado no projeto da arquitetura e ao equilíbrio entre o service time e o wait time para garantir que o response time de cada recurso seja o estipulado. Seja pela otimização do recurso em si, ou pelo tuning do ambiente (determinando número de instâncias)
Embora as medidas para melhoria de performance dependam de especificidades de cada sistema, há um conjunto comum de táticas frequentemente adotadas, sobretudo sob o ponto de vista arquitetural.
Priorização de requests
Todas as requests são importantes. Mas, algumas são mais importantes que as outras. Eventualmente, um sistema precisará garantir recursos suficientes para atender uma categoria requisições em detrimento de outras.
Uma medida comum é apartar a infraestrutura, dedicando recursos ao atendimento exclusivo de determinadas funcionalidades, separando enfileiramentos. Não raro, é útil segmentar também o sistema de software em componentes, facilitando processos de deploy e manutenção.
Outra boa medida para suportar a performance é desacoplar requisições mais complexas em etapas mais simples, usando, então, abordagens assíncronas, melhorando a performance percebida com respostas rápidas, enquanto parte do processamento fica postergado.
Desacoplando o aceite e a confirmação de pedidos
Sistemas de e-commerce têm adotado, frequentemente, a separação do aceite de um pedido e a confirmação mediante pagamento.
O aceite do pedido acontece rapidamente, com ótimo response time. Entretanto, este limita-se a “enfileirar” solicitação para confirmação, que é um processo com maior custo computacional.
Aceitar pedidos rapidamente é prioridade frente a confirmar pedidos!
Redução do overhead com chamadas remotas
Rede e GC são duas fontes comuns de problemas de performance. O agrupamento de serviços pode reduzir boa parte da pressão sobre a rede e um bocado de intervenções dos mecanismos de GC, demandando menos filas e melhorando o service time.
Adoção de Rate limiters
A performance de um sistema tem clara relação com as restrições relacionadas aos recursos ao ambiente operacional. O esperado é que a performance se mantenha previsível enquanto houverem recursos disponíveis, geralmente, entretanto, ela é comprometida quando os limites de recursos são ultrapassados. Por isso, uma das formas de proteger da performance é a utilização de rate limiters.
Rate limiters podem ser implementados em client-side, server-side ou em um middleware. Implementações em client-side tem como mérito principal a redução da pressão sobre a rede. Já implementações em server-side ou middleware são mais “confiáveis”, por estarem em ambientes gerenciados, e robustas, visto que permitem regras mais complexas.
De forma prática, a estratégia consistem em limitar o throughput como forma de proteger o response time.
Qualificação no consumo de recursos
Eventualmente, pode ser importante revisar a implementação dos componentes para garantir que recursos estejam sendo utilizados de maneira eficiente. Não raro, por exemplo, memória e CPU são comprometidos desnecessariamente em função de implementações ingênuas, causando aumento do service time. Outros bons exemplos são conexões com banco de dados e uso impróprio do sistema de arquivos que, com frequência, implicam em enfileiramentos.
Adotação de programação paralela e concorrente
Componentes devem aumentar o throughput usando scale out , seja externamente, pela criação de mais instâncias, devidamente balanceadas, seja internamente, pela adoção de threads. Essas medidas reduzem filas de requisições.
Em sistemas modelados para alto nível de paralelismo, é essencial evitar contenção de recursos. Isso é possível tentando criar o mínimo possível de recursos que possam ser utilizados apenas por um único “cliente” por vez.
Implementação de Caching
Caching pode gerar aumento perceptível de performance! Seja para evitar consultas complexas para o banco de dados ou para armazenar o resultado de computação de alto custo, caching é uma forma simples de substituir recursos de custo elevado por outros mais baratos, evitando enfileiramentos.
Aumentar a quantidade de recursos
Em muitas condições, pode-se amenizar e até resolver problemas de performance utilizando melhor infraestrutura. Entretanto, é importante estar atento que “colocar mais lata” (scale up) é uma solução paliativa e raramente sustentável no longo prazo.
// TODO
Antes de avançar para o próximo capítulo, recomendo a você as seguintes reflexões:
- Quais são os recursos computacionais que tem causado maiores problemas de performance no sistema em que você está trabalhando agora?
- Quais são as taxas de utilização desses recursos?
Há casos nos quais a performance pode ser restringida para fins de usabilidade. Um caso prático são sistemas de Home Broker. Frequentemente as cotações de renda variável são atualizadas em intervalos menores aos que o olho humano é capaz de captar. Neste caso, performance não necessariamente é positiva. Para tornar as atualizações perceptíveis, foi estabelecido um intervalo mínimo para propagá-las, de modo a descartar o envio da cotações que seriam imperceptíveis.
Desta forma, apenas atualizações perceptíveis passaram a ser transmitidas, o que aumentou o conforto do cliente, levando-o a ter mais clareza sobre as mudanças e podendo tomar decisões mais bem pensadas – ao mesmo tempo em que reduziu o consumo de recursos para a transmissão de cotações.
Existem cenários onde só temos o workload. Negócio não tem definido o tempo de execução e nem o recurso computacional. Seria um bom caminho executar um teste de carga em cima do workload informado e alinhar com negócio se o tempo é satisfatório? E se o custo do recurso computacional utilizado está dentro do esperado?
Uma prática interessante para identificar potenciais problemas de performance é efetuar testes de carga escalonando a quantidade de acessos simultâneos e comparando o response time. Se o response time subir de maneira maior que linear, existem problemas na aplicação.
A não utilização ou a utilização de forma inadequada de async await pode causar ThreadPool starvation, gerando problemas de performance em aplicações .Net.
Uma prática que não está 100% relacionada com performance mas que vale citar é a adoção de Circuit Breaker para a proteção caso algum recurso remoto sofra problemas. Assim se evita o acumulo de requisições a um recurso que sabidamente já está com problemas.
Um problema bastante comum quando falamos sobre a implementação de componentes em .NET (especialmente no full framework), é a utilização do HttpClient de forma inadequada, criando instâncias e utilizando o Dispose a cada request, acreditando que os recursos utilizados serão liberados. O fato é que o HttpClient não libera os sockets no Dispose, o que pode fazer com que a aplicação sofra com Socket Starvation ao criarmos muitas instâncias (Documentação oficial).
Alias, a forma como o Dispose é implementado no HttpClient pode ser considerado como uma falha de design, pois ela induz o desenvolvedor ao erro, já que não é aderente ao conceito de descarte fornecido pelo IDisposable.