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.
Usabilidade define performance
Performance é determinante para melhorar a usabilidade. Entretanto, distante do óbvio, algumas vezes o segredo não é “aumentar a velocidade”, mas diminuir.
William Santos, integrante da primeira turma de mentoria em arquitetura de software compartilha um cenário curioso, relacionado a sistemas de Home Broker. Segundo ele destaca, frequentemente as cotações de renda variável são atualizadas em intervalos menores aos que o olho humano é capaz de captar. Nestes casos, performance não necessariamente é positiva. Para tornar as atualizações perceptíveis, em um dos sistemas em que esteve envolvido, 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.
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.
Quando o 'negócio' não sugere performance
O ideal é sempre alinhar expectativas de performance antes de fazer qualquer decisão de design. Entretanto, com frequência, as expectativas do negócio são inexistentes ou vagas.
Tem pouca utilidade, por exemplo, determinar que o response time de um determinado endpoint deve ser abaixo de 300 ms se não se conseguir estimar um workload associado.
Quando sistemas são elaborados sem essa “expectativa” clarificada, é útil realizar testes de estresse afim de determinar a realidade atual e validar se esta é suficiente. Eventualmente, essa abordagem, entretanto, pode revelar “tempo perdido” com uma soluções que “ficam devendo”, ou desperdício com soluções que “sobram demais”.
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).
.NET Socket Starvation
.NET tem uma falha conhecida de design na classe HttpClient. Por algumas razões, HttpClient não libera os sockets que utiliza quando o método Dispose é invocado, o que pode fazer com que a aplicação sofra com Socket Starvation ao criarmos muitas instâncias. Eventualmente, isso causa “enfileiramento” de processamentos que precisam de novos sockets causando redução de throughput e, eventualmente, afetando outros recursos, como memória e CPU.
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
Empiricamente, é fácil observar que 20% das funcionalidades de um sistema são utilizadas 80% do tempo – trata-se da aplicação da proporção/princípio de Pareto. Geralmente, as exigências de performance serão maiores para essas funcionalidades.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.
Filas causam quedas em “efeito dominó”
A imagem abaixo relata comportamento de um sistema durante um incidente de performance e demonstra uma característica comum.
A formação de filas, frente a determinados recursos que estão indisponíveis, seja por contenção ou prontidão, acaba estressando outras partes de um sistema, notoriamente CPU, gerando quedas em dominó.
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.
Adoçã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.
Sumarizando
A performance é característica fundamental em qualquer arquitetura. Ela impacta muitos outros atributos de qualidade, sobretudo o custo de um sistema. Para ser bem projetada, depende do alinhamento de expectativas quanto a tempos de processamento e volumes de workload.
Melhorias de performance geralmente estão associadas com uso mais adequado de recursos computacionais. Quanto melhor o uso, mais performance. Um bom indicativo do uso efetivo dos recursos computacionais está na formação de filas “saudáveis” que garantam throughput equivalente ao arrival rate.
// 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?
A performance de um sistema pode ser degrada aos poucos, quando o sistema fica sob stress durante um tempo. Ou seja, ao realizar um teste de carga ou stress, além do planejamento da carga utilizada o tempo a ser executado o teste é fundamental.
typo: availability