Pesquisar este blog

terça-feira, 3 de março de 2015

Processos em Sistemas Distribuídos

Neste post serão apresentados os diferentes tipos de processos que são uma parte crucial dos sistemas distribuídos. O conceito de processo foi definido pelos estudiosos de Sistemas Operacionais, onde um processo é um programa em execução. Os Sistemas Operacionais atuais conseguem administrar vários programas executando ao mesmo tempo, como um editor de texto sendo executado ao mesmo tempo que uma impressão é realizada.

Conceitos de Threads e Processos

Para entender o que é um processo, primeiro deve-se entender o que é uma thread e como estes doios conceitos estão relacionados. Para executar um programa, o Sistema Operacional criará um número de processos virtuais, cada um para rodar um programa diferente. Para saber qual processo pertence a qual programa, o Sistema Operacional possui uma tabela, onde ficam armazenados os dados de permissão, áreas da memória, arquivos abertos, dados da conta do usuário, etc. O Sistema Operacional deve cuidar para que um processo não interfira na execução de outro, por exemplo dois processos não podem compartilhar a mesma área na memória, etc, pois quando isso acontece temos aquele famoso erro da tela azul.

Ao criar um processo, o Sistema Operacional deve criar um endereço totalmente independente, antes de alocar uma nova área na memória ele deve zerar o seu conteúdo, para que não haja interferência de um processo em outro. Por isso, trocar o processo que está em execução tem um grande custo, além de ter que salvar, o estado daquele processo, o que inclui a memória, as permissões de acesso e o que estava sendo executado naquele momento, o Sistema Operacional também deverá alterar a tabela de processos, que agora possui dados inválidos. Caso o Sistema Operacional esteja sobrecarregado e não consiga guardar na memória, todo o conteúdo necessário, ele deverá fazer uma troca (do inglês swap) de dados entre a memória e o sistema de armazenamento de arquivos, da máquina, o que tornará a sua execução ainda mais lenta.

Uma thread possui um pedaço de código, totalmente independente das outras threads, porém uma thread possui o mínimo de informação possível, para permitir que a CPU seja compartilhada pelo maior número de threads possível. Várias threads do mesmo programa não precisam guardar individualmente as permissões de acesso, que serão as mesmas pra todas elas. A memória é compartilhada entre as Threads, uma vez que elas estão executando no mesmo programa, com isso o Sistema Operacional não precisa controlar o acesso aos dados pelas Threads, isso é deixado para os aplicativos fazerem.

Com isso, desenvolver um sistema multi-thread requer uma esforço intelectual adicional, controlar quais dados cada thread irá acessar, para evitar que elas travem.

Por outro lado, um sistema multi-thread irá, geralmente, rodar mais rápido que um sistema single-thread, principalmente nas máquinas multicores disponíveis hoje no mercado. Já que, usando múltiplas threads será possível aproveitar o máximo do hardware disponível.

Neste mesmo blog, existem posts explicando como programar com threads, com synchronized e com java callable

Threads em Sistemas distribuídos

Uma propriedade muito importante das Threads, é que elas disponibilizam uma maneira simples de bloquear as chamadas ao sistema, sem ter que bloquear o processo onde ela está sendo executada. Isso faz com que as threads sejam muito atrativas em um sistema distribuído, já que com elas, é muito mais fácil manter múltiplas conexões lógicas ao mesmo tempo. Ao invés de ficar esperando uma resposta do servidor, o sistema pode executar outras tarefas, enquanto a resposta não chega.

Um exemplo bem factível deste uso é, quando um servidor de um serviço web recebe uma chamada, ao travar a execução de um processo para cada chamada, o que limitaria a resposta delas ao número de cores disponíveis na máquina onde ele está alocado, ele usa as Threads e com isso, todos os passos que requerem uma resposta mais demorada, como uma conexão http, tanto de entrada, quanto de saída, podem ser colocadas em espera, enquanto as respostas não são retornadas assim, o servidor conseguirá receber muitas requisições, pois grande parte delas será executada em segundo plano.

Clientes Multithread

Sistemas baseados na internet podem sofrer quedas de desempenho, dependendo da velocidade da rede, uma simples página pode demorar segundos até abrir. Este desenpenho pode ser afetado tanto pela velocidade da rede do cliente, quando por uma sobrecarga de um servidor.

A maneira usual de esconder estes problemas de instabilidade e latência, de rede é inicializar a comunicação imediatamente e ir realizando algum processamento, enquanto os dados são carregados. Um grande exemplo de quando isso acontece é com os WebBrowsers, geralmente uma página da Web é composta de um HTML contendo texto plano e uma série de outros elementos, como javascripts, imagens, ícones, etc. Os browsers implementados com multithread, baixam o conteúdo texto do html e já mostram ele ao usuário, enquanto isso múltiplas conexões são abertas com o servidor, afim de baixar os outros elementos daquela página. Enquanto o texto da página é construído e começa a ser mostrado ao usuário, as outras informações vão sendo baixadas.

Muitas das páginas disponíveis na web, estão armazenadas em múltiplos servidores, com isso pode-se baixar o conteúdo de várias máquinas ao mesmo tempo, paralelizando uma tarefa.

Servidores Multithread

Apesar de existirem muitas vantagens em construir clientes multithreads, a grande vantagem em implementar um sistema MultiThread está nos servidores. Os servidores possuem um maior poder de processamento e desenvolver tarefas multithreads neles, faz com que o aproveitamento das máquinas seja muito melhor. Neste post serão apresentados os diferentes tipos de processos que são uma parte crucial dos sistemas distribuídos.

Considere um sistema de arquivos distribuídos, onde cada operação de leitura deve esperar pela resposta do hardware de armazenamento de dados. Estes servidores ficam esperando por requisições de dados, e assim que elas chegam ele executa a tarefa e retorna uma resposta ao cliente.

Na implementação de um servidor de arquivo, o trabalho de ler os dados do sistema de armazenamento deve ser implementado em uma thread, assim o processo principal, de receber as chamadas, não ficará travado aguardando a resposta do hardware, ele fica livre para receber chamadas, enquanto o processamento da leitura é realizado. Assim que a leitura completa, o sistema procede e compõe a resposta.

Nesta implementação múltiplas chamadas de leitura podem ser recebidas em paralelo e não necessitam ficar aguardando a execução de uma chamada completar.

Virtualização

Threads e processos, de certa forma, são maneiras de fazer múltiplas coisas ao mesmo tempo. Com eles é possível construirmos programas que aparentam serem executados em paralelo. Em um computador com uma única CPU, algo não comum hoje em dia, esta execução em paralelo é uma ilusão. A rápida troca entre as threads e processos cria esta ilusão de paralelismo.
Este tipo de ilusão também pode ser extendido a outros recursos da máquina, o que também é conhecido por virtualização de recursos.
A virtualização já é conhecida há muito tempo, porém ela vem ganhando muita importância nos sistemas distribuídos

O papel da virtualização nos sistemas distribuídos

Um dos papéis mais importantes, realizados pela virtualização, na década de 70, foi permitir que softwares legados rodassem nos hardwares dos mainframes. Isto incluía não só diversos programas, como Sistemas Operacionais. Este suporte aos softwares antigos, foi muito bem aplicado pela IBM, nos seus Mainframes 370 (e seus sucessores) os quais ofereciam uma máquina virtual onde muitos sistemas operacionais foram portados.

O hardware, assim como os sistemas de baixo nível mudam constantemente, enquanto os softwares de alto nível, que são mais abstratos, são bem mais estáveis. Com isso os sistemas legados não acompanham a evolução dos sistemas de baixo nível, o que pode gerar uma incompatibilidade entre eles. A virtualização pode ajudar bastante neste tipo de caso ao portar estes sistemas legados para as novas plataformas de hardware.

Um outro fato interessante é que as conexões de redes são quase que obrigatórias nas máquinas. Estas conexões fazem com que os administradores de sistema tenham que manter um grande número de servidores diferentes, cada um com um tipo diferente de aplicação. Porém, alguns dos recursos destes servidores, precisam ser compartilhados entre as máquinas. Neste caso a virtualização pode ajudar bastante, rodando diversos sistemas operacionais diferentes, e emulando diversos tipos de hardware diferente, tudo sobre uma mesma plataforma.

Arquitetura de Máquinas Virtuais

Existem muitas maneiras diferentes de se virtualizar um sistema e para compreender os tipos diferentes de virtualização é importante conhecer os quatro tipos diferentes de interfaces providas por um computador:
  • A interface entre o hardware e o software, que consiste em instruções de máquinas que podem ser invocadas por qualquer programa.
  • A interface entre o hardware e o software que só pode ser invocada por programas com privilégios, como um sistema operacional.
  • A interface que possui as chamadas de sistema, oferecidas pelo sistema operacional.
  • A interface de uma biblioteca geralmente formando uma API. Que em muitos casos empacota as chamadas de sistema mencionadas anteriormente.

A essência da virtualização é emular o comportamento destas interfaces.

A virtualização pode ser feita de duas maneiras, uma delas pode-se prover uma camada de abstração de instruções, como é feito pela Máquina Virtual Java (JVM), ou mesmo pode ser emulada, como é feita pelo software Wine, que emular o Windows em um sistema operacional Linux. Isso é feito de maneira a emular geralmente apenas um processo.

Uma outra forma de realizarmos a Virtualização é implementar um sistema que emula uma camada de hardware, fornecendo uma interface de software que emula o hardware como uma interface. Com isso é possível disponibilizar esta interface para diferentes programas. Desta maneira pode-se ter múltiplos Sistemas Operacionais rodando concorrentemente e independente em uma mesma plataforma. Como exemplos podemos citar o Virtualbox e VMWare.

Containers

Um container é uma unidade de software, uma forma de empacotamento, que contêm, tudo o que é necessário, inclusive as dependências, para se executar um software.

O container garante que o software rodará, da mesma maneira, independente da plataforma, ou computador onde ele rode. Containerizar um software garante a ele, uma certa independência de plataforma. 

Com o uso deles, foi possível separar responsabilidades, como os desenvolvedores passaram a cuidar, apenas da parte lógica da aplicação e suas dependências, enquanto que a equipe de infra, cuida do ambiente de execução daquela aplicação. Com este isolamento, fica mais fácil identificar problemas de execução e de quem é a responsabilidade pelas falhas de um aplicativo. Os containers surgiram para acabar com a famosa frase dos desenvolvedores, na minha máquina funciona. Uma vez que é possível empacotar e testar o software, por completo, não há mais motivos para um desenvolvedor entregar algo que não execute da maneira correta. Isto ainda não impede a entrega de softwares com bugs, mas torna mais fácil descobrir onde eles estão.

É bem comum, comparar os containers, com os ambientes virtuais, e de certa forma isto é correto, uma vez que os containers isolam a execução entre várias aplicações. Porém, os containers oferecem uma solução, muito mais leve, que as máquinas virtuais, uma vez que eles compartilham, entre si, o sistema operacional. Isso acontece, pois as máquinas virtuais, emulam todo o aparato de hardware, entre os softwares, e o sistema operacional, já um container, emula um sistema operacional.

Docker, Jetty, Tomcat, Wildfy e SpringBoot são exemplos de containers que podemos usar para desenvolver aplicativos.

Em um data center, a utilização de um container, não substitui a utilização de uma máquina virtual, lembre-se que um hardware muito grande, pode emular diversas máquinas através da virtualização, e dentro de uma instância virtual, podemos ter diversos containers.

Clientes

Network User Interface

Uma das principais tarefas das máquinas clientes é prover, para o usuário, uma maneira eficiente e uma boa interface de acesso aos dados dos servidores remotos. A primeira decisão de design é o cliente ter uma parte de código responsável por cada serviço diferente que ele irá acessar.

A segunda opção é prover um acesso direto aos serviços remotos, somente provendo uma interface conveniente, o que significa que o cliente somente será usado como um terminal, sem qualquer necessidade de armazenamento de dados. Neste caso tudo será processado e armazenado no servidor.

Um dos exemplos de NUI é o XServer, o servidor de janelas do Unix, que é utilizado para controlar terminais que incluem, o monitor, o teclado e possívelmente um mouse. A principal parte do sistema é formado por um kernel, que contem alguns drivers específicos para aquele terminal e é altamente dependente do hardware.

Um aspecto interessante do sistema X é que o kernel e os aplicativos podem estar em diferentes máquinas.

Este sistema disponibiliza uma biblioteca chamada XLib, que serve para executar chamadas de baixo nível e controlar o teclado, o mouse e a tela.

Um dos principais aplicativos que são executados sobre o XServer são os Window Managers, que na verdade são os Look and Feels dos sistemas X. Estes aplicativos são responsáveis por controlar os eventos, a aparência, as janelas do sistema.

O mesmo computador Unix pode ter vários window managers para prover ao usuário diferentes interfaces, acessos e usos do hardware.

Pelo apresentado até aqui, o sistema X encaixa-se no tipo de arquitetura cliente-servidor, já que o kernel recebe as requisições para manipular as janelas, e estas requisições podem vir de máquinas remotas.

Servidores

Design Geral

Um servidor é um provedor de um serviço específico, para vários clientes. Essencialmente, cada servidor é organizado da mesma maneira, eles esperam uma requisição de um cliente e depois cuida para que esta requisição seja atendida da maneira correta, enquanto ele retorna para o estado de espera por uma nova requisição.

Existem diversas maneiras de se organizar os servidores. Nos servidores iterativos, ele cuidará das requisições e, se necessário, retorna a resposta para os clientes. Um servidor concorrente não cuida sozinho da requisição, ele encaminha ela para uma thread separada, ou um processo diferente e depois disto ele fica aguardando uma nova requisição. Um servidor web é um exemplo de um servidor concorrente.

Para se conectar com um servidor, devemos acessar uma requisição para um ponto de entrada (End point), também conhecido por porta onde o servidor está rodando. Cada servidor fica escutando uma porta específica.

E como fazer para que os clientes saibam qual é a porta de um serviço? Uma das soluções encontradas para isso foi configurar a mesma porta, sempre, para serviços comumente utilizados, como a porta FTP 21, SSH 22 e HTTP 80.

É comum associar um end point para cada serviço, porém, implementar cada serviço em um servidor diferente muitas vezes será um desperdício de recursos de hardware. Em um serviço típico de UNIX é comum termos uma série de servidores rodando simultaneamente, onde a maioria deles fica passivamente aguardando um cliente se conectar. Ao invés de ter muitos servidores passivos rodando concorrentemente, fica mais eficiente ter um super servidor, escutando cada porta de cada serviço específico configurado.

Uma outra preocupação ao desenvolver um sistema de servidor é como e quando este servidor ou o serviço poderá ser interrompido. Por exemplo se um usuário decide fazer um download de um arquivo muito grande, e após alguns minutos ele descobre que está fazendo o download do arquivo errado e gostaria de interromper aquele download. Uma solução bem comum para este tipo de problema é matar a parte do cliente, o que é muito comum nos aplicativos da internet, e com isso a conexão será interrompida abruptamente.

A solução mais elegante é prover um outro serviço no servidor, que ficará escutando por uma mensagem específica, também conhecida como Out of Band data, que é responsável por terminar a transmissão de dados. Estes dados também podem ser enviados pela mesma conexão por onde o download está sendo realizado, para isto basta fazer com que o serviço seja capaz de receber este tipo de mensagem.

Um último problema a ser levado em consideração é se o seu servidor será ou não stateless (sem estado). Os servidores stateless não guardam o estado do seu cliente e poderão mudar o seu próprio estado sem informar os seus clientes.

Por exemplo, um servidor que possui uma página de web estática é stateless, já que ele somente responde por requisições HTTP que podem ser de upload ou download de alguns arquivos, muitas vezes HTML. Assim que a requisição é processada, o servidor se esquece completamente daquele cliente. Com isso a página hospedada naquele servidor pode ser alterada, sem que qualquer cliente seja avisado.

Perceba que muitos servidores stateless mantem informações dos seus clientes, porém se estas informações foram perdidas, isso não afetará de forma alguma o serviço provido por aquele servidor.

Diferentemente de um servidor stateless, estão os servidores stateful, os quais armazenam as informações dos seus clientes. Um exemplo típico é um webmail, que precisa armazenar os dados de cada cliente, afim de mostrar para ele somente as informações referentes a sua conta. Note que se os dados das conexões dos clientes forem perdidos, todo mundo será deslogado do sistema, afetando o seu serviço.

Cluster de Servidores

Montar um cluster de servidores nada mais é do que juntar algumas máquinas, conectadas através de uma rede, onde cada uma destas máquinas roda um ou mais servidores.

Na maioria dos casos os clusters estão organizados em três camadas, onde a primeira delas consiste em um roteador, onde as requisições chegam e são direcionadas. 

Este roteador pode variar muito, por exemplo a camada de transporte da arquitetura OSI aceita conexões TCP e as repassa para um dos servidores do cluster. Já em um servidor web, que recebe conexões HTTP, mas repassa parte destas conexões para os servidores de aplicação para um processamento remoto e somente depois de coletar os dados deste processamento é que a resposta será enviada ao seu cliente.

Com isso chega-se a terceira camada, que consistem em servidores que processam dados, geralmente cache, banco de dados ou servidores de arquivos.

Um dos principais fatores de design de um cluster de servidores é esconder quantas máquinas estão sendo utilizadas para processar as requisições. Os clientes não precisam saber como é a organização interna do cluster, e isso é obtido através da configuração de um único end point.

Uma das maneiras mais comuns de se conectar a um servidor é estabelecer uma conexão TCP entre o cliente e o servidor. Assim que o roteador recebe uma conexão de um cliente ele irá determinar qual é o melhor servidor para cuidar daquela requisição e redirecionar a chamada para aquele servidor. O servidor irá responder a requisição diretamente para o cliente, porém ele irá adicionar o ip do roteador no HEAD da chamada. A maneira mais comum de realizar este roteamento é chamada de Round Robin, onde cada requisição vai para a próxima máquina da fila, e cada máquina que é utilizada vai para o final da fila.


Apresentação

Nenhum comentário:

Postar um comentário