Tuning – Quando e como?

radio

Apesar de discorrer sobre técnicas e comandos, esse post é mais uma prosa pois quando se trata de ajuste fino de sistemas computacionais entra a questão:
Há um padrão a seguir?

A resposta é fácil e tem sabor amargo; não há um padrão e, se houvesse, o tuning não seria necessário.

Esses ajustes finos podem ocorrer em diversos níveis, desde a camada 1 à camada 7 do modelo OSI – o hardware é sempre o primeiro ítem a ser “tunado”, uma vez que para qualquer projeto deve-se dimensionar primeiramente a estrutura física.

Um bom exemplo de tuning nesse nível é o CPD do facebook. O hardware foi projetado por eles, o link que chega ao hack é 10GB (não, não há CATx nesse CPD), a energia já chega estabilizada a -48, a refrigeração foi projetada por eles porque coolers também geram calor e fricção que gera ainda mais calor. Eu recomendo fortemente que você assista esse video falando sobre a estrutura do Facebook. Para chegar nesse nível de excelência obviamente choraram (e certamente ainda choram) muitos engenheiros, mas sempre há mais para se fazer.

Passando de um foco para adiante, para projetos de nível humano é bem mais simples fazer tuning, mas não tão simples a ponto de qualquer um o fazer. Instalar um servidor e rodar um serviço é uma tarefa relativamente fácil e muitas das vezes uma instalação padrão é suficiente porque a demanda de recursos na maioria das vezes é suprida pelo hardware super-dimensionado (ou preventivamente dimensionado para escalar). Para esse objetivo não basta saber onde colocar valores, nem há uma tabela para tais valores. Sempre é necessário considerar experiências anteriores e também alheias, de forma a ter bases para atingir o objetivo. No decorrer desse post vou exemplificar alguns tunings de sistema operacional (Linux) desde a camada de rede e alguns tunings de aplicação (nginx e MySQL) para melhor entendimento do assunto.

Usando um exemplo real, em um determinado cliente haviamos implementado um serviço web com base de dados MySQL. O padrão utilizado inicialmente foi uma versão um pouco desatualizada porém de fácil instalação, pois bastava usar o gerenciador de pacotes da distribuição para instalá-lo. A aplicação web normalmente é instalada em um servidor a parte e a base de dados também recebe um servidor dedicado, mas para esse projeto foi erroneamente dimensionado um servidor de 2 sockets six-core HT ( o que faz aparecer no sistema 24 núcleos), 24G de RAM e 4 discos SAS com uma controladora RAID.

O objetivo era somente atender requisições simples onde as conexões registrariam um dado tipo de informação, até bastante curta. O número de fontes a fazer a comunicação estava poucas centenas de milhares acima de 2 milhões de origens. Não vou discorrer sobre dimensionamento de hardware, afinal o assunto aqui é solucionar o caos. Digo isso porque se você está fazendo um tuning para embelezar algo que já está funcionando a contento, de forma a continuar imperceptível as mudanças, então você está desperdiçando tempo.

O tuning é o ponto em que você precisa espremer até a última gota de água da pedra; é a hora que você já fez tudo que é possível e está partindo para o milagre – sim, estou falando do tuning corretivo, o tuning que você não tem muito tempo para resolver ou o tuning que pode custar suas próximas noites de sono por periodo indeterminado.

Voltando à plataforma anteriormente citada, por sí só já seria um desafio se utilizasse o padrão mínimo 4 servidores com as devidas configurações distribuidas pois estava algumas vezes acima do padrão de acessos que costumamos ter nesse tipo de projeto (estou preservando alguns valores e nomes justamente para evitar engenharia social). Resultou que atendeu, mascarando uma gravíssima deficiência que gerou milhões de locks no banco de dados e consequentemente consumo demasiado de todos os recursos disponíveis de sistema, entre os quais a CPU, que chegou ao average de 34.0 – o que representava 3400%, considerando 100% por core, havia fila de concorrência para mais 10 núcleos. Já que estamos discorrendo sobre núcleos, vamos entrar em detalhes.

Vi algumas vezes pessoas que sabem onde ler o número de processadores do sistema, mas não sabem identificá-los. Quantos cores tem? Quantos sockets? Os HTs estão ativos? – e para exclarecer essas questões, abaixo segue um exemplo:

Para responder as perguntar anteriores:
(A) Quantos cores tem?
– A primeira linha mostra processor : 23. Isso quer dizer, a partir de 0, se vê 24 processadores.
(B) Quantos sockets?
– A linha physical id : 1 é quem diz: são 2 sockets (0 e 1).
(C) Os HTs estão ativos?
– A linha cpu core : 6 indica que sim, os HTs estão ativos. O “porquê” é claro. Se existem apenas 2 sockets e o CPU cores é 6, então só existem 12 núcleos físicos. A tese pode também ser confirmada na linha flags, onde entre as tantas está o HT.

Dando então continuidade ao assunto, algo precisava ser feito em relação a isso. Outra coisa que estava sendo trucidada pelo tráfego era o balanceador – Linux também. Mas esse deixaremos para falar mais adiante. Seguindo com a providência do caso anterior – os servidores no cliente foram trocados (não havia a menor necessidade, porém estava em contrato que deveria acontecer), mas mesmo sendo servidores um pouco mais parrudos, a situação seria idêntica à anterior, pois toda a limitação estava sendo imposta pela má configuração de software como um todo, não apenas a aplicação, mas também o SO.




Deveria começar por algum lugar, então fiz longos estudos sobre a base de dados, pois a engine default é MyISAM e ela já não comportaria a atual carga, tendo em vista que vários milhões de slow queries estavam sendo geradas. E aí vemos que não basta capricho no desenvolvimento da aplicação se os recursos não ajudam. Digo isso porque após muito estudo e esforço para modelar uma nova solução, decidi que apenas 1 tabela deveria ser modificada para solucionar o problema; apenas 1 tabela foi convertida para InnoDB. Acreditem ou não, foi o suficiente para eliminar slow queries, derrubar a carga do sistema para 6.0 lvg e poupar inclusive memória. Monitorando a base de dados vi que a eficiência de chaves da base estava próximo a 100% então o problema realmente não era queries mal formadas, mas sim as limitações impostas pela tecnologia utilizada para aquele momento – quero dizer com isso que MyISAM tem sim suas vantagens e continuará tendo por um longo tempo, mas para aquela solução específica não era ideal tê-lo em uma tabela. Para chegar a essa conclusão, muitos estudos foram necessários; transactions, buffers, flags e daí por diante. Mas não foi só isso que venceu todas as barreiras.

No primeiro setup, o sistema operacional rodava com Reiserfs como seu sistema de arquivos em uma única partição raiz sobre RAID5. Antes desse ‘ponto’ já temos diversas coisas a discorrer.
O sistema de arquivos embora ainda bom e podendo sim ser utilizado, não era ideal para base de dados. E mais; o ideal é correr a base de dados sobre uma partição isolada. Então no novo setup utilizei XFS com a opção noatime em uma partição exclusiva para a base. O sistema de arquivos XFS é recomendado para base de dados e é citado em diversos benchmarks. A opção noatime é um tipo de ‘pulo do gato’. Essa opção desabilita a verificação de criação e modificação de arquivos da partição em que for apontada. Como vantagem, o I/O passa a ser menor obviamente, ainda mais que o MySQL modifica os arquivos constantemente. Em contrapartida, uma analise forense seria dificultada por conta do empobrecimento de informações na partição. Eu fico com a primeira opção.

Ainda dá pra melhorar mais, mexendo do scheduler. Essa é outra característica que deve ser estudada. Não vou entrar nos detalhes de scheduler porque levaria um tempo enorme digitando e estou começando a sentir preguiça. Para detalhes a respeito, por favor pesquise sobre as opções que podem ser vistas em /sys/block/sda/queue/scheduler.
A opção escolhida e considerada como melhor para banco de dados foi o NOOP. Eu vi inclusive gráficos mostrando uma ligeira vantagem sobre DEADLINE e por essa pequena vantagem obviamente optei por ele, já que realmente faria também esse ajuste.

Em relação ao RAID, a melhor opção para o banco é RAID0 (obrigado por reparar o erro em relação ao tipo, Cleiton), já que o dado é distribuido pelos discos. Isso aumenta exponencialmente a capacidade de escrita, porém é certo que na falha de disco os dados se perderão. Uma opção então é usar RAID 10, onde soma-se espelhamento e distribuição de dados. O caso é que bem ou não isso gera I/O, mas é uma boa opção em ambientes que não tenham replicação remota. O RAID 5 não é uma boa opção para sistemas que dependam de I/O porque ele distribui o mesmo dado por todos os discos. Sugiro que leia a respeito de RAID e seus tipos para entender melhor a respeito.

Em relação aos discos, as controladoras RAID podem operar com 3 boas tecnologias, sendo SAS, SCSI e SSD. Aqui estou falando de ‘tecnologia ao alcance de todos’. Tratando-se de custo benefício, SAS atende suficientemente bem para muitos casos, com excessões como por exemplo o Facebook.

Uma coisa importante é ter conhecimento dos recursos existentes. Por mais que haja boa vontade e capacidade, a experiência em muitas vezes leva vantagem. Poderia ser suficiente o tuning até aqui, mas ainda há mais o que fazer, como por exemplo o tuning do READ e AHEAD do disco. O valor default empregado pelo Linux é de 128 e isso também deve ser optimizado para o hardware em questão. Essa informação pode ser tratada pelo programa blockdev. Como essa parte é um pouco grande, sugiro que leia esse post onde relato a configuração desse recurso para o mesmo caso no qual estou dando o exemplo.

Para finalizar com o sistema de arquivos, configurei o swappiness (sysctl vm.swappiness deve devolver o valor de 60 se você não fez nenhuma modificação) para utilizar swap somente após esgotar a memória física. Desse modo radical, digo de passagem, evitamos I/O causado pela memória virtual em um momento inoportuno. Claro que em um sistema instalado em uma hardware com poucos recursos, não adiantaria muito fazer esse ajuste. E esse tipo de coisa deve ser avaliado, aliás, cada um dos ítens de tuning devem ser avaliados para a melhor opção e por isso essa é uma tarefa tão exaustiva, pois depende de estudos de caso, condicionamento e experiências que não são absorvidas do dia para a noite.

Tudo pode parecer bastante elaborado até esse ponto, mas depois sempre é necessário fazer um profiling.
Quando já estava com tudo rodando, notei que o desempenho poderia e deveria ser melhorado e para tal, aumentei o número de processos do servidor web, o que causou uma queda geral do sistema. Então, nesse momento o essencial é manter a calma e ter organização o processo de tuning, porque se você fizer muitos passos de uma vez para ver o resultado depois, certamente você terá problema e não saberá a origem. Fora isso, um problema pode ser causado por outro, o chamado efeito cascata, que gerará falso-positivo e te fará mexer em lugares errados, portanto, tenha paciência e faça experimentos com tempo e calma.

Quando o servidor caiu, eu sabia exatamente como reverter; bastava reduzir o número de processo para ter a máquina no estado anterior, já que foi essa a última alteração que fiz. Porém algo estava errado. Não poderia perder toda a comunicação por apenas ter aumentado o número de processos do servidor web. Confesso que não fui direto na solução, mas foi relativamente rápido até eu desconfiar que no número de arquivos descritores não estavam sendo o suficiente. Logo, tive que aumentar esses valores e seguidamente pude reestabelecer a comunicação tal qual era antes. Esse processo foi feito através do /etc/security/limits.conf e também está bem descrito como fazê-lo nesse post. Então, inclui no limits.conf o usuario do servidor web e incrementei o nproc e o nofile. O problema estava resolvido, mas a carga saiu de 6.00 para 21.00. Havia mais memória sobrando do que no estado inicial onde a carga era de 6.00; as queries no banco sairam de 920 qps para 1250 qps, mas algo não estava bem. O ganho foi muito baixo para o tanto de recursos que o sistema passou a consumir. Então nesse momento entrou o feeling. O número de processos inicial era de 2, quando modifiquei passei para 5. Claro que algo estava errado!

Mais uma vez, com foco na última modificação, sabia como reverter a situação, mas voltar atrás não traria benefícios, então resolvi baixar de 5 para 4 o número de processos e voilá! O processamento caiu para 14.00 sobrou montes de memória e o número de queries subiu para 1520 qps! Reparou uma coisa? Baixei o número de processos para subir o número de queries. Esse tipo de coisa só dá pra sentir com experimentação, portanto fazê-lo com calma e passo a passo garantirá o resultado final.

Mantive o foco na aplicação até agora, mas entre esse tuning e a versão anterior da instalação, já me debatia com o tuning mais sensível que poderiamos ter para essa plataforma – o balanceador Linux.
O balanceamento é feito via software e realmente a eficiência desse software é incrível para suportar toda a carga provinda de mais de 2.2 milhões de origens, sendo que parte do tráfego é SSL desencapsulado pelo balanceador.

Dos recursos, praticamente nada se consumia de memória, nem de processador, porém o sistema degradou pouco a pouco com o passar do tempo, de forma a gerar frações de congelamento e no início congelamento completo, de forma que foi necessário reiniciar o hardware manualmente por diversas vezes até encontrar e tratar a fonte dos problemas. E os problemas foram tratado com mais tuning.

A primeira fase ao tratar qualquer problema é a análise, não adianta se desesperar com pressão – isso leva tempo. E nessa hora alguém tem que “represar” as cobranças e tratar da parte política $eja e$$a qual for. Não se preocupe com custo operacional, isso não é problema seu – solucionar o problema sim, o é, mas para isso você tem que ter calma para estudar a situação.

Nesse balanceador os recursos eram sobressalentes, o hardware não era velho, o CPD devidamente refrigerado, as fontes de alimentação devidamente providas. O rack devidamente organizado sem equipamentos que pudessem gerar interferencia ao redor. O sistema instalado era estável, atualizado e não haviam bugs de hardware. Logo, só poderia ser gargalo de rede.

A primeira análise e desconfiança foi obviamente na interface de rede. Não por acaso, já que em uma análise de rede que nem sempre evidencia um ponto de partida, é necessário usar o modelo OSI como mapa para rastrear o problema.

Como você deve ter percebido, a dois paragrafos acima citei as possibilidades envolvidas na camada física. Subindo gradativamente (nem tão perto nem tão distante) desconfiei então da interface de rede. Poderia ser um problema de bug de driver em último caso, mas inicialmente poderia ser na camada de enlace. Dados poderiam estar se perdendo por um problema no switch ou cabo, por exemplo. Problemas físicos geralmente geram colisão na interface de rede, gargalos geram drops. Com o comando ifconfig pude reparar que não haviam colisões na saída e o número de drops era irrelevante. Não havia problema nas camadas iniciais, então não poderia estar tão mais distante a solução, uma vez que era certo que o problema não era software porque não havia utilização de recursos que acusassem isso. Portanto, foi necessário iniciar o tuning no kernel.

Uma coisa importante para a análise é conhecer a aplicação que roda na camada superior. Uma de suas características é que ao menos 1 vez ao dia cada um desses 2.2 milhões de origens haverá de fazer uma requisição ao servidor. Se a conexão não acontece nesse momento seja por timeout ou por qualquer causa que gere indisponibilidade, então uma série de retries acontece em um tempo que escala até um valor máximo. Com apenas esses dados já fica claro que o gargalo gerou “ondas” de acesso. Infelizmente concentrei-me nos resultados visuais e não gerei gráficos da situação, o que bem viria a calhar para essa explicação. Mas imagine algo como uma senóide; uma vez que um fluxo constante encontra um gargalo,o comportamento muda; não acontece a transação que viria a seguir e uma nova tentativa contecerá após um tempo X. Isso significa que no balanceador o fluxo que estava em uma constante de 5MB/s iria aumentar gradativamente até atingir o pico, onde as ondas atingiriam o ponto a tornar-se estável novamente. Do lado do servidor, requisições não chegavam, então a carga caía, posteriormente se elevava. Pense nisso como uma senóide, era um comportamento bastante parecido.

No primeiro momento, fiz alguns ajustes mais conhecidos para controle das conexões; reaproveitamento de sockets, redução do timeout syn e reciclagem dos sockets:

E obviamente, ajustes na tabela conntrack em ambos, o servidor da aplicação e no balanceador. vendo primeiro o uso do conntrack:

Você pode aumentar conforme sua inspiração, mas se quiser algo bem preciso, há uma fórmula que pode ser útil:
CONNTRACK_MAX = RAMSIZE (in bytes) / 16384 / (x / 32)

E o hashsize pode ser o valor de conntrack_max / 8, mas pra ficar bem bonito no post:
HASHSIZE = CONNTRACK_MAX / 8 = RAMSIZE (in bytes) / 131072 / (x / 32)

A situação estava bastante complicada em relação às conexões. Analisando o estado dos sockets dava pra ver gargalos terríveis. Para analisar o comportamento dos sockets usei esses comandos (qualquer um serve):

Primeiro tratei de ver se não havia um syn flood gerado por um grupo de IPs qualquer:




Os valores eram baixos, alguns poucos retries, o que me levaram a crer que todo o tráfego era legítimo, ou seja, eu não tinha ninguém para por culpa até o momento.

Eram valores realmente preocupantes e mostravam claramente um gargalo. Mas quais eram as razões disso? O balanceamento é feito com nginx, muito eficiente e robusto. Mas nesse primeiro momento de análise no balanceador, o que me preocupava era o servidor da base de dados. Isso porque o balanceador continha um altíssimo número de SYN_SENT, ou seja – havia gargalo ainda do “lado de dentro”. Foi essa análise que me fez concluir a necessidade dos ajustes no limits.conf, porque a aplicação web necessitava de mais arquivos descritores. Após aumentar o limits, aconteceu uma ligeira melhora, que me levou a fazer ajustes também no servidor web da segunda máquina. Ainda dentro do sistema (já na GUI), um certa parte do sistema retornava uma mensagem de que não era capaz de iniciar algumas threads. Haveria já esgotado o limite de processos e arquivos descritores definidos para a aplicação web? Analisando um pouco mais o servidor da aplicação:

Minha surpresa foi que todos os valores estavam dentro dos limites definidos! Só restava uma possiblidade; aquela parte da aplicação web não era iniciada pelo usuário do servidor Apache. Por sorte, alguém especialista na aplicação estava comigo e me falou qual era o processo responsável pela parte problemática da aplicação, mas ainda todos acreditavamos que esse processo era responsabilidade da aplicação web. De qualquer forma, tive que descobrir quem executava o processo. Vou usar um nome fictício para não expor detalhes confidenciais da aplicação, chamando-o de ZeSilva.

Oras, vejam só! O Usuário não era mesmo o servidor web, era o TonhoSilva. Logo, tive que aumentar também seus limites nproc e nofile. Detalhes podem ser visto no post citado anteriormente.

Analisando os logs, encontrei também isso:

Esse processo ZeSilva era responsável por uma boa fase da comunicação com o serviço como um todo e era o gargalo que gerava SYN_SENT no balanceador. Após esses últimos ajustes, essa flag foi eliminada do balanceador.

Do lado da aplicação web, a carga e consumo de memória cairam um bocado, mas percebia-se na aplicação uma certa instabilidade; demora nas respostas e acessos imcompletos. Por conta disso, revi todas as flags utilizadas no tuning (e algumas podem nem estar aqui, porque estou escrevendo de cabeça o processo como foi), sockets abertos (zerados também os SYN_RECV no servidor da aplicação), limites em arquivos descritores e número de processos. Tudo parecia perfeito e ainda que não percebesse nenhuma lentidão na resposta do console, tive que analisar o I/O em disco:

 


Me devolveu uma utilização de 50% de uso de CPU.

Me devolveu uma utilização de aproximadamente 50% também. Quanto mais próximo de 100%, maior a saturação do disco, estando completamente saturado em 100% e ai já passou da hora de rever a solução. Mas em uma condição de completo caos onde havia uma altíssima parte do tráfego gerado por novas tentativas de comunicação e 50% dos recursos estavam livres ainda, já dava para respirar sossegado; estavamos seguramente no pico!

Como constatado, o servidor da aplicação parecia estar muito bem definido já nesse ponto, então passou a ser óbvio que o gargalo estava no balanceador.

Não poderia deixar de verificar os limites do usuário nginx, claro:

Sim, tive que aumentar.

Diversos dos mesmos testes foram feitos no balanceador, considerando também que não havia carga, nem consumo demasiado de recursos do sistema. O console estava bastante estável portanto após todos esses filtros, foi possível determinar que bastava “abrir a torneira” do nginx. Esse sim me causou até um certo espanto, pois aumentei os valores de worker_rlimit_nofile e worker_connections em mais de 500% (claro, pouco a pouco até que tudo se mantivesse estável).

O resultado final foi um fluxo constante de 5MB/s que durou aproximadamente 30 horas, até que toda a planta estivesse estabilizada com sua comunicação devidamente atualizada. Com isso, a carga de dados caiu para 500KB/s, ou seja, 1/10!

Resumidamente, há males que vem para bem. Foram tantos tunings que agora estabilizado é possível quase dobrar a planta de 2.2 milhões para lá seus 3.8 milhões sem engargalar. Claro, no balanceador sim novos ajustes podem ser necessários na camada de rede.

Quando tudo estava nessa condição abençoada, pude parar para caprichar um pouco mais na base, como NÃO está descrito no post anteriormente citado, mas foram ajustes mais pontuais, não relacionados profundamente ao desempenho. Mas voltarei a escrever sobre base de dados com algumas dicas interessantes, assim como também escreverei um pouco sobre o fantástico nginx.

Como pode ser visto aqui, o processo de tuning é bastante longo. Além dos comandos chaves citados aqui, diversos outros foram necessários, alguns outros sequer serviram para dar pistas, mas o melhor de tudo é que não foi um tipo de análise que envolvesse pesquisa de bug na aplicação. Nesse caso eu teria que escrever um livro dedicado a essa aventura, então, os comandos não chegaram ao nível de chamadas de sistema da libc (ufa).

Além disso, para escrever esse post eu levei diversas horas. Pode imaginar o tempo da análise? Foram alguns dias de sofrimento nivel hard. Por isso, meus 2 centavos a respeito de tuning: só faça se for essencialmente necessário. E se for essencialmente necessário, tenha controle emocional e foco, porque é fácil tomar caminhos errados que induzam a um reinicio ou desistência.

Em um post futuro também falarei sobre “Etical hacking”, também conhecido como “Pentest” ou “Penetration test”, espero que você que leu até esse ponto, acompanhe!

Se gostou, não deixe de compartilhar; dê seu like no video e inscreva-se no nosso canal Do bit Ao Byte Brasil no YouTube.

Prefere seguir-nos pelo facebook? Basta curtir aqui.

Prefere twitter? @DobitAoByte.

Próximo post a caminho!

Comments

comments

Djames Suhanko

Djames Suhanko é Perito Forense Digital. Já atuou com deployer em sistemas de missão critica em diversos países pelo mundão. Programador Shell, Python, C, C++ e Qt, tendo contato com embarcados ( ora profissionalmente, ora por lazer ) desde 2009.

Deixe uma resposta