BT

Prós e contras dos modelos de concorrência de microservices

| Postado por Glenn Engstrand , traduzido por Talles Lasmar em 29 jun 2016. Tempo estimado de leitura: 9 minutos |

Arquitetos que projetam microservices normalmente focam nos padrões, topologia, e granularidade, mas uma escolha fundamental a ser feita é a escolha do modelo de concorrência. Com a proliferação de tantas ferramentas open source, linguagens de programação e plataformas, arquitetos de software têm mais decisões a serem tomadas do que nunca.

É muito fácil se perder em detalhes de linguagem ou diferenças entre bibliotecas e perder de vista o que é importante.

Escolher o modelo de concorrência correto para seus microservices e como eles se conectam ao banco de dados pode ser a diferença entre uma solução que é somente suficiente e uma que é um produto espetacular.

Dar atenção ao modelo de concorrência é uma maneira efetiva de dar foco arquitetural nos trade-offs entre eficiência e complexidade. Conforme um serviço é decomposto em operações paralelas com recursos compartilhados, a aplicação ficará mais eficiente e suas respostas mostrarão menor latência (com certas limitações, veja Lei de Amdahl). Paralelizar operações e compartilhar recursos de maneira segura introduz maior complexidade ao código.

Contudo, quanto mais complexo um código é, mais difícil é para seus mantenedores o entenderem por completo; o que significa que os desenvolvedores estarão propensos a introduzir novos bugs a cada mudança.

Uma das responsabilidades mais importantes de um arquiteto é encontrar um bom equilíbrio entre eficiência e complexidade.

Modelo monothread e monoprocesso

O modelo de concorrência mais básico é monothread e monoprocesso. Essa é a maneira mais simples de escrever código.

Um serviço monothread e monoprocesso não consegue ser executado em mais de um núcleo ao mesmo tempo. Um servidor dedicado moderno costuma ter até 24 núcleos. Um serviço construído em torno desse modelo não conseguirá utilizar mais que um núcleo do servidor. O throughput desses serviços não aumentará quando houver uma carga maior e a utilização de CPU não ultrapassará mais que um dígito de porcentagem. Com tanta subutilização, uma maneira de compensar é ter pools de servidores maiores para suportar a carga.

Essa abordagem funciona, mas gera grande desperdício e despesa. Os provedores de computação em nuvem mais conhecidos oferecem instâncias com processador virtual de core único com custo razoavelmente barato permitindo essa abordagem escalar de forma granular.

Modelo monothread e multiprocesso sem reuso

A próxima etapa, tanto em complexidade quanto eficiência, seria o modelo monothread e multiprocesso no qual um novo processo é criado para cada requisição. O código para esse tipo de microservice é relativamente simples, mas possui mais complexidade que o modelo anterior.

O overhead da criação de processos e a constante criação e término das conexões com o banco de dados podem roubar tempo de processamento aumentando a latência nos serviços vizinhos. Esse modelo de concorrência cria mais conexões com o banco de dados, pois as conexões são por processo e não podem ser compartilhadas entre diferentes processos. Devido cada processo existir somente durante a requisição, cada requisição precisa reconectar a cada banco de dados.

Microservices que executam esse modelo de concorrência devem esperar até ser necessário para conectar ao banco de dados. Não há motivo para ter o custo de uma conexão com o banco de dados se o código não utiliza tal conexão. Enquanto não é possível fazer cache das conexões com o banco de dados entre processos, alguns ambientes suportam um cache opcode que permite armazenar as configurações como IP e credenciais do banco de dados; dois exemplos de cache opcode populares são o Zend OpCache e APC.

Modelo monothread e multiprocesso com reuso

O próximo aumento em complexidade e eficiência é o modelo monothread e multiprocesso com cada requisição reutilizando processos de trabalho existentes. Esse modelo é diferente do anterior, que sempre cria um novo processo a cada requisição. Nesse modelo de concorrência, um processo existente é reutilizado para suprir uma requisição sem que um novo processo seja criado para cada requisição.

A complexidade do serviço é relativamente simples, mas envolve um gerenciamento extra do ciclo de vida dos processos de trabalho. Por exemplo, programadores podem utilizar variáveis estáticas ao invés de passar vários parametros extras. Isso simplifica o código e não apresenta problema quando essas variáveis estáticas são reiniciadas a cada nova requisição. Se o código não as reinicia, então o comportamento vai se basear na requisição anterior e não na atual. A parte final da adição de complexidade é que há de se incluir uma lógica para recuperação de conexões ao banco de dados expiradas. Uma conexão pode expirar quando o banco de dados desconecta por provável inatividade.

Devido cada processo poder servir múltiplas requisições, não há necessidade de se reconectar a cada banco de dados a cada requisição; as conexões serão reutilizadas evitando custos de conexão e reduzindo a latência. Mas cada processo continua tendo que criar e gerenciar suas próprias conexões ao banco de dados. Já que os processos não podem compartilhar conexões, os bancos de dados compartilhados mantem mais conexões abertas. Conexões abertas em excesso podem piorar a performance do banco de dados. Isso ocorre porque conexões do banco possuem estado, fazendo com que o banco de dados da aplicação tenha que alocar recursos de seu próprio processo para cada conexão.

Modelo multithread e monoprocesso

Há uma maneira melhor de proteger bancos de dados com um número de conexões configuráveis, utilizando um pool de conexões no modelo multithread e monoprocesso com um processo de longa duração. Apesar de uma conexão ao banco de dados não poder ser compartilhada entre vários processos, ela pode ser compartilhada entre múltiplas threads em um mesmo processo.

Se um serviço possui 100 processos monothread cada um deles em 10 servidores, então o banco de dados verá 100 x 10 = 1000 conexões. Se possui 1 processo com 100 threads em 10 servidores e cada processo possuir 10 conexões no seu pool de conexões, então o banco de dados verá apenas 10 x 10 = 100 conexões e o serviço ainda poderá alcançar um throughput alto. Utilizar um pool de conexões entre threads é bem eficiente tanto para o serviço quanto para o banco de dados.

Essa técnica de pool de conexões alcança um alto throughput enquanto protege os bancos de dados, mas traz o custo de uma complexidade de código extra. Devido as threads terem que compartilhar conexões de banco de dados com estado, desenvolvedores devem estar aptos a identificar e corrigir bugs de concorrência como deadlock, livelock, inanição e condições de corrida. Uma maneira de lidar com esses tipos de bugs é serializar o acesso, mas serializar o acesso demais reduz o paralelismo. Esses tipos de bugs podem ser difíceis para desenvolvedores iniciantes identificar e corrigir.

Modelos multithread e monoprocesso de longa duração vêm em duas variedades; dedicando um thread por requisição ou compartilhando uma única thread entre todas as requisições. No primeiro modelo de concorrência, uma thread extra é associada a cada requisição o que limita o número de requisições sendo processadas em paralelo. Muitas threads podem levar a ineficiências pelo excesso de troca de contexto da CPU pelo sistema operacional.

No segundo modelo de concorrência, não há necessidade de fornecer uma thread extra para cada requisição, mas tarefas I/O bound devem executar em um thread pool separado para prevenir que todo o serviço espere uma operação demorada que encontrar. Se os resultados devem ser retornados para quem chamou, então o gerenciador de requisições deve esperar os resultados dados pelo thread pool estarem prontos.

Com a abordagem de não dedicar uma thread por requisição, espere um alto throughput e baixa latência em operações assíncronas, mas nenhum ganho real de performance em operações síncronas se comparado com utilizar uma thread dedicada para cada requisição.

Resumo

modelo de concorrência

considerações de eficiência

considerações de complexidade

monothread e monoprocesso

O serviço não conseguirá utilizar os cores do servidor por completo. Não espere um aumento de throughput com o aumento de carga e a utilização de CPU não ultrapassará 10%.

A abordagem mais simples e mais fácil de entender.

monothread e multiprocesso sem reuso

Os bancos de dados terão mais conexões abertas pois elas não podem ser reutilizadas entre processos. O excesso de conexões abertas degrada a performance do banco de dados.

Um código adicional deve estar presente para gerenciar o ciclo de vida dos processos. O código deve estar apto a se recuperar de conexões expiradas. Variáveis estáticas devem ser reiniciadas a cada requisição.

multithread e monoprocesso, com threads dedicadas a cada requisição

Ter um pool de conexões é bem eficiente tanto para o serviço quanto para o banco de dados, mas uma thread extra é atribuída a cada requisição o que limita o número de requisições sendo processadas em paralelo.

Devido as threads terem que dividir as conexões com o banco de dados, os desenvolvedores devem estar aptos a corrigir erros de concorrência como deadlock, livelock, inanição e condições de corrida.

multithread e monoprocesso, sem threads dedicadas a cada requisição

Ter um pool de conexões é bem eficiente tanto para o serviço quanto para o banco de dados. Espere um alto throughput para operações assíncronas.

Tarefas I/O bound devem rodar em um pool de threads a parte. Se for necessário retornar os resultados a quem chamou, então o gerenciador de requisição deve esperar o thread pool entregar tal resultado.

Conclusão

Antes de pensar sobre bibliotecas e linguagens, arquitetos de software devem refletir sobre a escolha do modelo de concorrência mais apropriado para sua cultura de engenharia e competência. Acertar no equilíbrio entre complexidade e eficiência ajudará a desvendar confusões e direcionar a escolha entre as várias plataformas disponíveis. Devido cada microservice ter menor escopo que uma aplicação monolítica, considere aprender um pouco mais sobre complexidade para poder alcançar maior eficiência.

Sobre o Autor

 Gleen Engstrand é líder técnico da equipe de arquitetura no Zoosk. Seu foco é na arquitetura de aplicações server side que precisam executar em escala B2C com custos de operação e publicação gerenciáveis. Gleen foi palestrante na conferência Lucene Revolution 2012 em Boston. Ele é especialista em quebrar aplicações monolíticas em microservices e em integrá-las com infraestrutura de comunicação em tempo real.

Avalie esse artigo

Relevância
Estilo/Redação

Olá visitante

Você precisa cadastrar-se no InfoQ Brasil ou para enviar comentários. Há muitas vantagens em se cadastrar.

Obtenha o máximo da experiência do InfoQ Brasil.

Dê sua opinião

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Receber mensagens dessa discussão
Comentários da comunidade

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Receber mensagens dessa discussão

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Receber mensagens dessa discussão

Dê sua opinião
BT