Não é de hoje que a busca por alta performance perturba qualquer um que já tenha tentado criar um sistema, seja ele envolvendo software ou hardware. Afinal, um sistema de alta performance nos dá a capacidade de realizar mais trabalho, o que geralmente se traduz em maiores ganhos. Obter esse resultado não é uma tarefa simples e geralmente envolve buscarmos o estado da arte naquilo que fazemos.
Em 1965, o co-fundador das empresas Fairchild Semiconductor e Intel, Gordon Moore, observou que a cada ano os circuitos integrados estavam se tornando cada vez mais complexos, dobrando a quantidade de transistores, e sugeriu que este crescimento se manteria pelos próximos 10 anos. Mais tarde, em 1975, ele reviu essa estimativa e estabeleceu uma tendência de que a quantidade dobraria a cada 2 anos. Embora não tenha utilizado nenhuma evidência empírica para esta afirmação, o que percebemos é que ele estava certo, pelo menos até agora, a ponto de considerarmos essa afirmação como “Lei de Moore”. A diferença é que, talvez, não possamos mais considerar os transistores em um único circuito integrado.
Ao utilizar mais transistores nos processadores modernos, conseguimos aumentar a capacidade de processamento que passou de 8 bits, para 16, 32 e atualmente 64 bits, simultaneamente, tornando as operações de cálculo mais eficientes. Paralelamente a isso, também fizemos esforços para aumentar a frequência na qual os processadores trabalham, de forma a realizar mais operações por unidade de tempo. Mas isso tem um limite físico e, em algum momento, estes aumentos começaram a não fazer mais sentido. Aumentar o clock demasiadamente torna qualquer filamento uma antena e a energia se dissipa na forma de ondas eletromagnéticas ou mesmo na forma de calor e aumentar o número de transistores pode significar maiores distâncias, tornando a comunicação entre eles mais demorada.
A solução para isso foi a escalabilidade horizontal e duas iniciativas foram criadas nesse sentido. Uma delas foi o compartilhamento de pipelines cada vez maiores e mais complexos para o processamento simultâneo de duas ou mais tarefas independentes, o que veio a ser chamado de Hyperthreading. A outra foi o encapsulamento de mais de uma CPU na mesma pastilha, permitindo que múltiplos processos pudessem ser executados de forma independente e simultanea, dando origem aos processadores multi-core.
Seguindo essa mesma linha de raciocínio, expandiu-se o conceito de escalabilidade horizontal para o que chamamos de processamento distribuído, onde não precisamos ficar restritos a um único circuito ou equipamento. O processamento é dividido em partes que podem ser processadas de forma independente e simultânea, para a obtenção do resultado composto, com menor tempo.
Podemos inicialmente ter a impressão de que este modelo funciona bem para qualquer situação, porém, uma palavra é determinante para o sucesso dessa arquitetura: “INDEPENDÊNCIA”. Quando múltiplos processos são independentes, eles podem ser executados sequencialmente ou em paralelo, sem que seus resultados sejam afetados. Porém, nem tudo o que fazemos se comporta dessa maneira. Para citar um exemplo, imaginemos um cliente com um cartão que lhe oferece $100 de crédito. Ele pode efetuar a compra de um produto de $20 ou outra de $90, mas não poderá efetuar as duas simultaneamente. Embora as compras sejam independentes entre si, o saldo do cartão as torna relacionadas, exigindo um mecanismo de lock para serializar as operações. Agora, imagine a complexidade para implementar esse lock em um ambiente distribuído. Uma compra efetuada em qualquer parte do mundo precisa ser autorizada por um ponto único de controle do saldo, que é o banco emissor do cartão, fazendo com que o desempenho desse sistema não seja exatamente paralelo.
A adoção de medidas para a obtenção da alta disponibilidade (High Availability) geralmente se baseiam na redundância, que nada mais é do que utilizar mais de um componente para a execução de uma “mesma” tarefa, de forma que na ausência de um componente, o resultado possa ser obtido a partir do outro. Controlar o sincronismo entre estes componentes redundantes requer esforço e, como consequência, podemos ter uma redução na capacidade de processamento.
Utilizar um pool de máquinas como forma de multiplicar a capacidade de processamento pode funcionar desde que o que é feito em uma das máquinas, seja independente do que é feito nas demais, evitando assim o uso de mecanismos de lock que acarretam a contenção do sistema. Quando utilizamos o Oracle RAC (Real Application Cluster), por exemplo, temos dois ou mais servidores (nós) com acesso aos mesmos dados, podendo executar tarefas de forma independente. Mas, quando um destes nós efetua alterações nos dados compartilhados, o cache destas informações utilizado pelos outros nós precisa ser invalidado para evitar inconsistências nos dados. Essa necessidade faz com que os nós precisem constantemente se comunicar, para “passar o bastão” ao nó que tem o direito de alterar dados. Se esse “bastão” não está conosco, precisamos negociar essa passagem antes de prosseguir com a alteração.
Mas então, como utilizar esse ambiente de Alta Disponibilidade com Alto Desempenho? Como vimos acima, o segredo é a independência.
Podemos ter, por exemplo, um RAC servindo a duas aplicações diferentes, de forma que uma utilize primariamente o nó 1 e a outra utilize primariamente o nó 2, de forma que uma aplicação nunca dependa de dados alterados pelo outro nó. Isto reduz o acesso ao “Global Cache” já que, na maioria das vezes, o bastão está com o nó onde roda a aplicação e o banco não precisa “consultar” os demais nós do cluster.
Outra maneira de reduzir a contenção, embora não reduza o tempo de processamento, é fazer com que os sistemas deixem de adotar um comportamento síncrono, onde aguardamos a conclusão de uma tarefa para prosseguir com a seguinte. Ao adotar um comportamento assíncrono, nós podemos iniciar múltiplas tarefas sem que haja a necessidade de esperar pela conclusão de cada uma. Obviamente isso acarreta maior complexidade ao sistema e torna o tempo médio de resposta de uma tarefa maior, mas nos permite executar mais tarefas simultaneamente o que, no final das contas, corresponde a um maior desempenho geral. Ainda assim, essa mudança de paradigma requer a independência das tarefas, já que uma tarefa dependente não pode ser iniciada sem a conclusão da tarefa da qual depende.
Como vimos, a escalabilidade horizontal pode ajudar a aumentar o desempenho de um sistema. Mas isto não quer dizer, necessariamente, que a redundância utilizada na obtenção de alta disponibilidade possa ser considerada como “escalabilidade horizontal”. Criar sistemas com menos dependências elimina pontos de falha e reduz a contenção. Além disso, modelos mais simples são mais fáceis de manter, consomem menos recursos computacionais e, geralmente, dão melhores resultados.
Nós podemos imaginar a Alta Disponibilidade e o Alto Desempenho como dois objetivos independentes e podemos escolher, facilmente, o quão perto queremos estar de um ou outro. Porém, com inteligência, podemos criar sistemas que encurtem esta distância, nos permitindo a aproximação dos dois objetivos, simultaneamente.