Systems Performance - Conceitos
Descubra as principais métricas utilizadas na análise de performance e como interpretá-las corretamente
Com anos de experiência na área, presenciei a evolução da infraestrutura de TI: da transição de servidores físicos para virtuais à administração de storages em redes de fibra óptica, da implantação de servidores blade à migração para a nuvem em meados de 2017. Uma constante em todas essas mudanças? A importância do desempenho e performance dos sistemas.
Cada desafio de performance exigia um mergulho profundo nos "bits and bytes" da tecnologia em questão, o que me permitiu acumular uma vasta gama de conceitos e metodologias ao longo do tempo.
Este (longo) post visa compartilhar esse conhecimento, abordando métricas de desempenho, trade-offs ao otimizar e quando parar de analisar e realizar otimizações. Com base nisso tudo, iremos nos aprofundar posteriormente em aplicar metodologias em cima disso tudo.
Antes de começarmos
Antes de prosseguirmos, é importante esclarecer alguns termos-chave que serão utilizados ao longo deste post.
Cache: Uma área de armazenamento rápido que evita comunicação direta com uma camada de armazenamento mais lenta. Geralmente esse armazenamento é feito em memória RAM, o que melhora o desempenho das operações.
Gargalo: É um recurso que limita a performance do sistema. Identificar e eliminar gargalos é uma atividade super importante na melhoria de desempenho de um sistema.
IOPS: Operações de entrada e saída (I/O) por segundo é uma medida da taxa de operações de transferência de dados. Para operações de I/O em disco, IOPS se refere ao número de leituras e gravações por segundo.
Latência: Expliquei no post anterior, mas é a medida do tempo que uma operação passa aguardando para ser processada.
Saturação: O grau em que um recurso tem trabalho acumulado em fila que não consegue processar de imediato.
Tempo de resposta: O tempo necessário para uma operação ser concluída, incluindo o tempo de espera e o tempo gasto para processar a operação, assim como o tempo de transferência do resultado de toda operação.
Throughput: A quantidade de trabalho realizado em um determinado período. Em comunicações, o termo é frequentemente usado para se referir à taxa de dados (bytes por segundo ou bits por segundo). Em outros contextos, como em bancos de dados, throughput pode se referir à taxa de operações (operações por segundo ou transações por segundo).
Workload: Refere-se à entrada ou carga aplicada ao sistema. Em um banco de dados, a carga de trabalho consiste nas consultas e comandos enviados pelos clientes.
Conceitos
Latência
Em alguns ambientes, a latência é o principal ponto de desempenho, enquanto em outros ela está entre as métricas mais importantes, junto com throughtput. A latência se refere ao tempo de espera antes que uma operação seja realizada, como o estabelecimento de uma conexão de rede antes de transferir dados em uma requisição HTTP. Esse tempo de resposta inclui tanto a latência quanto o tempo necessário para completar toda a operação.
A latência pode ser medida a partir de diferentes pontos, sendo frequentemente especificada pelo alvo da medição. Por exemplo, o tempo de carregamento de um site pode incluir a latência do DNS, da conexão TCP e o tempo de transferência dos dados. Em níveis mais amplos, todos esses componentes podem ser considerados parte da latência geral percebida pelo usuário, como o tempo desde o clique em um link até a página estar totalmente carregada.
Como a latência é uma métrica baseada no tempo, ela permite diversos cálculos e comparações diretas, facilitando a identificação e priorização de problemas de desempenho. Por exemplo, é mais simples comparar 200 ms de I/O de rede com 100 ms de I/O de disco do que comparar métricas diferentes como IOPS. Converter outras métricas para tempo ou latência sempre que possível torna a análise muito mais objetiva, permitindo assim prever aumentos de velocidade ao reduzir ou eliminar a latência.
Para referência, as abreviações de unidade de tempo estão listadas abaixo:
Trade-offs
Ao otimizar o desempenho, é crucial considerar os trade-offs mais comuns. O ditado popular "bom, bonito e barato" ilustra bem esse conceito. No nosso caso, iremos usar “bom, rápido e barato” e geralmente você só pode escolher dois desses atributos ao considerar os trade-offs ao otimizar um sistema.
Muitos projetos e times priorizam prazos e custos mais baixos, deixando o desempenho para depois. Essa abordagem pode se tornar problemática quando decisões iniciais comprometem futuras melhorias de desempenho. Exemplos incluem a escolha de uma arquitetura de armazenamento, o uso de uma linguagem de programação ou sistema operacional ineficientes, ou a escolha de componentes sem ferramentas robustas de análise de desempenho.
Na otimização de desempenho, um trade-off comum ocorre entre CPU e memória. Tradicionalmente, a memória é usada para cache, reduzindo o uso da CPU. Porém, com a abundância de CPU disponível hoje em dia, essa relação pode se inverter, o tempo livre de CPU pode ser dedicado à compressão de dados por exemplo, diminuindo assim o uso de memória.
Otimizações
A otimização de desempenho é mais eficaz quando realizada o mais próximo possível do local onde o workload da aplicação é executado. A tabela abaixo ilustra uma aplicação com diversas possibilidades de ajustes.
Ao otimizar no nível da aplicação, é possível eliminar ou reduzir consultas ao banco de dados, resultando em melhorias de desempenho significativas. Se ao invés de olharmos para as queries e otimizarmos o acesso ao disco, obviamente iremos melhorar as operações de I/O, porém, um custo já foi incorrido na execução de todo código, o que pode resultar em ganhos marginais.
Embora otimizar no nível da aplicação seja o mais eficaz nesse caso, ele nem sempre é o mais adequado em termos de observabilidade. Consultas lentas podem ser melhor entendidas ao analisar o tempo gasto na CPU ou as operações de I/O no sistema de arquivos e disco. Essas métricas são facilmente observáveis por meio de ferramentas do sistema operacional.
Grandes melhorias de desempenho, incluindo correções de regressões, são frequentemente identificadas à medida que o código da aplicação evolui. Nesses contextos, a otimização e a observabilidade a partir do sistema operacional podem ser negligenciadas. No entanto, é importante lembrar que a análise de desempenho do sistema operacional pode também identificar problemas no nível da aplicação, frequentemente de maneira mais eficiente do que a observação apenas a partir da aplicação.
Diferenciando carga de trabalho e problemas arquiteturais
Uma aplicação pode apresentar um desempenho ruim devido a má configuração do software (Framework, Sistema Operacional) quanto no hardware, assim como na sua arquitetura e implementação. No entanto, uma aplicação também pode ter um desempenho ruim simplesmente por estar submetida a uma carga de trabalho excessiva, resultando em filas de espera e altas latências.
Se a análise da arquitetura mostrar muito enfileiramento, mas não identificar problemas na execução dessas tarefas enfileiradas, o problema pode ser a carga excessiva aplicada, este é o momento de adicionar mais instâncias/servidores/VMs para lidar com o aumento dessa carga.
Já um problema de arquitetura pode ser uma aplicação single-thread e está constantemente utilizando a CPU, com requisições enfileirando enquanto outras CPUs estão ociosas. Nesse caso, o desempenho é limitado pela arquitetura single-thread da aplicação. Outro exemplo de problema de arquitetura pode ser uma aplicação multi-thread que disputa um único bloqueio, de forma que apenas uma thread consegue avançar enquanto as outras aguardam.
Por outro lado, um problema de carga de trabalho pode ser uma aplicação multi-thread que está utilizando todas as CPUs disponíveis. Nesse caso, o desempenho é limitado pela capacidade de CPU disponível, ou seja, há mais carga de trabalho do que as CPUs conseguem processar.
Escalabilidade
A imagem acima apresenta um perfil típico de throughput conforme a carga de trabalho do sistema cresce. Inicialmente, se observa uma escalabilidade linear, onde o throughput aumenta proporcionalmente à carga. No entanto, chega um ponto (marcado pela uma linha pontilhada), onde a contenção por um recurso começa a degradar o throughput. Esse ponto é chamado de knee point, representando a transição entre duas funções. Além desse ponto, o throughput deixa de escalar linearmente devido ao aumento da contenção pelo recurso, e eventualmente, os custos adicionais de contenção e coerência fazem com que menos requisições sejam atendidas, resultando assim em uma diminuição do throughput. Esse ponto de saturação pode ocorrer quando um componente atinge 100% de utilização ou quando a utilização se aproxima de 100%, e as filas de espera começam a se tornar frequentes e significativas.
A degradação do desempenho em uma escalabilidade não linear, em termos de tempo de resposta médio ou latência, é representada na imagem abaixo.
Obviamente, tempos de resposta mais altos são indesejáveis. O perfil de degradação "rápida" pode ocorrer com ao utilizar muita memória, quando o sistema começa a mover páginas de memória para o disco para liberar a memória principal, ou seja, o sistema começa a “swapar”. Já o perfil de degradação "lenta" pode ocorrer ao utilizar mais CPU. Outro exemplo de degradação "rápida" é utilização de I/O do disco. À medida que a carga (e a consequente utilização do disco) aumenta, operações de I/O começarão a ser enfileiradas. A escalabilidade linear do tempo de resposta pode ocorrer se a aplicação começar a retornar erros quando os recursos não estiverem disponíveis ao invés de enfileirar mais operações.
Métricas
Métricas de desempenho (ou performance), são estatísticas geradas pelo sistema, aplicações ou ferramentas adicionais que medem suas atividades. Elas são observadas para análise e monitoramento de desempenho, através da linha de comando (CLI) ou de modo gráfico (GUI).
Alguns tipos de métricas de desempenho de sistemas:
Throughput (Taxa de Transferência): Pode ser medido em operações ou volume de dados por segundo;
IOPS (Operações de I/O por Segundo): Mede apenas operações de input/ouput (leituras e escritas);
Utilização: Indica o quão ocupado está um recurso, geralmente expresso em porcentagem;
Latência: Tempo de operação, apresentado como média ou percentil.
O throughput depende do contexto, por exemplo, o throughput de um banco de dados geralmente mede a quantidade de queries ou requisições por segundo, enquanto o throughput de rede mede o volume de bits ou bytes por segundo.
As métricas de desempenho não são “gratuitas”, em algum momento, ciclos de CPU devem ser gastos para coletá-las. Isso gera sobrecarga, que pode afetar o desempenho do que está sendo medido ou monitorado.
Utilização
O termo utilização é frequentemente utilizado em sistemas operacionais para descrever o uso de dispositivos, como CPU e discos. A utilização pode ser baseada no tempo ou na capacidade.
Utilização Baseada no Tempo
A utilização baseada no tempo é definida na teoria das filas, onde a média do tempo que o servidor ou recurso esteve ocupado, junto com a razão U = B/T.
Onde U é a utilização e B é o tempo total que o sistema esteve ocupado durante T, o período de observação.
Esta é também a definição de utilização mais comum disponível nas ferramentas de monitoração disponíveis em sistemas operacionais, como por exemplo, o iostat
que denomina essa métrica como %b (percent busy).
Esta métrica de utilização indica quão ocupado está um componente. Quando um componente se aproxima de 100% de utilização, o desempenho pode degradar seriamente devido à contenção pelo recurso. Nessa caso, outras métricas podem ser verificadas para confirmar se o componente se tornou um gargalo no sistema.
Alguns componentes podem atender múltiplas operações em paralelo. Para esses, o desempenho pode não degradar mesmo com 100% de utilização, pois eles podem aceitar mais carga de trabalho. Um exemplo para ilustrar melhor seria um ônibus que está em constante operação, seguindo sua rota sem interrupções. O ônibus está sendo utilizado a 100% em termos de movimento, pois está sempre em serviço. No entanto, mesmo estando continuamente em operação, ele pode aceitar mais passageiros a cada parada. Isso significa que, apesar de estar a 100% de utilização em termos de atividade, ainda há capacidade adicional para transportar mais pessoas.
Um disco que está 100% ocupado também pode aceitar e processar mais cargas de trabalho, por exemplo, armazenando gravações no cache do disco para serem concluídas posteriormente. Um outro exemplo, arrays de disco frequentemente operam a 100% de utilização porque provavelmente algum disco está ocupado o tempo todo, mas o array possui discos ociosos suficientes para aceitar mais trabalho.
Utilização Baseada na Capacidade
Essa definição de utilização é usada no contexto de planejamento de capacidade, onde um sistema ou componente é capaz de entregar uma certa quantidade de taxa de transferência. Em qualquer nível de desempenho, o sistema ou componente está operando em alguma proporção de sua capacidade. Essa proporção é chamada de utilização.
Esta definição de utilização é em termos de capacidade, ao invés de se basear no tempo, isso implica que um disco com 100% de utilização não pode aceitar mais trabalho. Com a definição baseada no tempo, 100% de utilização apenas significa que ele está ocupado 100% do tempo, mas não necessariamente que está operando em sua capacidade máxima.
Para o exemplo anterior do ônibus, 100% de capacidade pode significar que o ônibus está cheio e não pode aceitar mais passageiros.
Saturação
A saturação se refere ao grau em que mais carga de trabalho é solicitado a um recurso do que ele pode processar. A saturação começa a ocorrer quando a utilização atinge 100% (baseada na capacidade), pois a carga de trabalho extra não pode ser processada imediatamente e começa enfileirar.
A figura acima mostra que a saturação aumenta de forma linear além da marca de 100% de utilização baseada na capacidade à medida que a carga continua a crescer. Qualquer grau de saturação representa um problema de performance, já que o tempo é gasto aguardando, ou seja, aumentando a latência.
Para a utilização baseada no tempo (percentual de ocupação), a formação de filas e, consequentemente, a saturação podem não iniciar exatamente na marca de 100% de utilização. Isso vai depender do quanto o recurso pode processar tarefas em paralelo.
Imagine um servidor de banco de dados configurado para processar até 200 queries por segundo (100% de capacidade). Se a demanda aumentar para 250 queries por segundo, as 50 queries adicionais não poderão ser processadas imediatamente e começarão a se acumular em uma fila, resultando em saturação. Isso causa aumento na latência, pois as consultas adicionais precisam esperar para serem processadas.
Por outro lado, considere um servidor que pode processar múltiplas requests em paralelo devido a múltiplos núcleos de CPU. Mesmo quando a utilização baseada no tempo atinge 100%, ele pode continuar a aceitar novas requests sem um aumento significativo na latência, desde que consiga gerenciar eficientemente o processamento de forma paralela.
Profiling
O profiling é o processo de coleta e análise de dados sobre o comportamento de um sistema ou aplicação para construir um modelo detalhado de desempenho. permitindo identificar gargalos com intuito de otimizar a performance. Ele é normalmente realizado por meio de amostragem periódica do estado do sistema em intervalos de tempo definidos. Isso envolve a captura de informações como registros de utilização de CPU, memória ou atividades de I/O, que, quando agregadas, fornecem insights sobre o funcionamento do sistema ao longo do tempo.
Diferentemente de métricas como IOPS e throughput, que fornecem medidas quantitativas específicas, o uso de amostragem no profiling oferece uma visão mais abrangente das atividades e interações dentro do sistema. A granularidade e o detalhamento das informações obtidas dependem diretamente da taxa de amostragem: taxas mais altas permitem uma resolução temporal mais fina e detalhes mais precisos, enquanto taxas mais baixas reduzem a sobrecarga no sistema, mas podem perder eventos transitórios importantes.
Por exemplo, para compreender detalhadamente o uso da CPU, pode-se realizar uma amostra do ponteiro de instrução (instruction pointer) ou capturar stack traces em intervalos de tempo frequentes. Ao registrar onde a CPU está executando essas instruções, é possível entender quais funções ou trechos de código consomem mais recursos computacionais.
Caching
Cache é frequentemente utilizado para melhorar o desempenho. Um cache armazena resultados de uma camada de armazenamento mais lenta em uma camada mais rápida, para ser utilizado posteriormente. Um bom exemplo disso é o armazenamento em cache de blocos de disco na memória RAM.
Uma métrica para entender o desempenho do cache é a taxa de acertos (hit rate) de cada cache. O número de vezes que os dados necessários foram encontrados no cache (hits) versus o total de acessos (hits + misses):
Taxa de Hits = hits / (hits + misses)
Quanto maior, melhor, pois uma taxa mais alta reflete mais dados acessados com sucesso a partir de mídias mais rápidas. A imagem abaixo ilustra a melhoria de desempenho esperada para o aumento da taxa de hits do cache.
Alguns exemplos de cache:
Cache de CPU: As CPUs utilizam múltiplos níveis de cache (L1, L2, L3) para acelerar o acesso à memória principal. O cache L1 é extremamente rápido, mas pequeno, enquanto o L3 é maior e mais lento. Isso permite que a CPU acesse dados rapidamente sem precisar acessar a memória principal constantemente.
Cache de disco na RAM: Um sistema operacional pode armazenar blocos de disco frequentemente acessados na memória RAM. Isso reduz o tempo de acesso aos dados, já que a RAM é bem mais rápida que os discos.
Cache de Aplicação: Aplicações web normalmente utilizam caches em memória, como o Redis ou Memcached, para armazenar resultados de queries do banco de dados. Isso diminui a necessidade de acesso ao banco de dados, melhorando a velocidade de resposta da aplicação.
Quero entrar mais a fundo sobre caches em um próximo artigo, onde vou abordar alguns algoritmos como MRU, LRU, LFU e também sobre caches Hot, Warm e Cold.
A dificuldade de identificar os riscos
A noção de “conhecidos-conhecidos”, “conhecidos-desconhecidos” e “desconhecidos-desconhecidos”, é bem importante para o campo de desempenho. De início pode parecer confuso, mas vamos aos exemplos:
Conhecidos-Conhecidos
São coisas que você sabe. Você sabe que deve verificar uma métrica de desempenho e também conhece seu valor atual, por exemplo, você sabe que deve monitorar a utilização da CPU e também sabe que o valor está, em média, em 10%.
Conhecidos-Desconhecidos
São coisas que você sabe que não sabe. Você sabe que pode verificar uma métrica ou a existência de um subsistema, mas ainda não o observou, por exemplo, Você sabe que pode usar profiling para verificar o que está consumindo a CPU, mas ainda não realizou essa verificação.
Desconhecidos-Desconhecidos
São coisas que você não sabe que não sabe. Você não tem conhecimento de certos aspectos que podem impactar o desempenho, então não os está verificando. Exemplo, você pode não saber que interrupções de dispositivos (como uma placa de rede de alta velocidade) podem se tornar grandes consumidoras de CPU, portanto, não está monitorando esse ponto.
Existe uma afirmação que é "quanto mais você sabe, mais você descobre que não sabe.", e isso é verdade! À medida que você aprende mais sobre os sistemas, você se torna ciente de mais desconhecidos-desconhecidos, que então se tornam conhecidos-desconhecidos que você pode começar a verificar.
Quando parar de analisar
Analisar o desempenho de um sistema pode ser desafiador, especialmente quando não se sabe quando parar. Com tantas ferramentas e aspectos a serem examinados, é comum se perguntar qual é a abordagem correta.
Existem três cenários onde pode ser apropriado encerrar a análise de desempenho:
Primeiro, quando a maior parte do problema já foi explicado. Por exemplo, se uma aplicação estava utilizando três vezes mais CPU do que o esperado e, ao investigar, descobre-se que chamadas de API mal otimizadas estão consumindo 60% do tempo de execução, você já possui uma parte significativa do problema. Nesse caso, é possível interromper a análise, corrigir o que foi descoberto e reavaliar o sistema após as mudanças;
Segundo, quando o retorno sobre o investimento (ROI) potencial é menor que o custo da análise. Problemas que podem economizar milhões anuais justificam meses de análise, enquanto questões menores que geram retornos de centenas de dólares podem não valer o tempo investido.
Terceiro, quando há oportunidades de ROI maiores em outras áreas, priorizando assim onde focar os esforços;
Concluindo
Acredito que isso é tudo que tenho para compartilhar a respeito de conceitos. A ideia inicial era trazer mais alguns pontos como perspectivas de análises, mas o artigo ficaria bem maior do que já está. Então resolvi fazer um resumão para preparar vocês para que nos próximos artigos possamos nos aprofundar mais em metodologias.
Até mais!