
O artigo
não fala sobre funcionários irresponsáveis, como se pode sugerir no título do artigo. Discutiremos um perigo técnico real que pode esperar por você, se você criar sistemas distribuídos.
Em um sistema corporativo, havia um componente. Esse componente coletou dados de usuários sobre um determinado produto e os registrou em um banco de dados. E consistia em três partes padrão: a interface do usuário, a lógica de negócios no servidor e as tabelas no banco de dados.
O componente funcionou bem e, por vários anos, ninguém tocou em seu código.
Mas uma vez, sem motivo, coisas estranhas começaram a acontecer no componente.
Ao trabalhar com alguns usuários, um componente no meio de uma sessão repentinamente começou a gerar erros. Isso acontecia com pouca frequência, mas como sempre, no momento mais inoportuno. E o que é mais incompreensível, os primeiros erros apareceram em uma versão estável do sistema em produção. Na versão em que por vários meses nenhum componente foi alterado.
Começamos a analisar a situação e verificamos o componente sob carga pesada. Isso funciona bem. Testes de integração bastante extensos e repetidos. Nos testes de integração, nosso componente funcionou bem.
Em uma palavra, o erro ficou claro quando e onde não estava claro.
Eles começaram a cavar mais fundo. Uma análise detalhada e uma comparação dos arquivos de log mostraram que a causa das mensagens de erro mostradas ao usuário é a violação de restrição na chave primária na tabela já mencionada no banco de dados.
O componente gravou dados na tabela usando o Hibernate e, algumas vezes, o Hibernate, ao tentar gravar a próxima linha, relatou uma violação de restrição.
Não vou aborrecer os leitores com mais detalhes técnicos e falar imediatamente sobre a essência do erro. Acontece que não apenas nosso componente grava na tabela acima, mas algumas vezes (extremamente raramente) algum outro componente. E ela faz isso de maneira muito simples, com uma instrução SQL INSERT simples. Um Hibernate funciona por padrão ao escrever da seguinte maneira. Para otimizar o processo de gravação, ele consulta o índice para a próxima chave primária uma vez e depois grava várias vezes apenas aumentando o valor da chave (10 vezes por padrão). E se ocorreu que, após a solicitação, o segundo componente ficou preso no processo e gravou dados na tabela usando o seguinte valor de chave primária, a tentativa subsequente de gravar no Hibernate levou à violação de restrição.
Se você estiver interessado em detalhes técnicos, veja-os abaixo.
Detalhes técnicos.
O código da classe começou assim:
@Entity @Table(name="PRODUCT_XXX") public class ProductXXX { @Id @Basic(optional=false) @Column( name="PROD_ID", columnDefinition="integer not null", insertable=true, updatable=false) @SequenceGenerator( name="GEN_PROD_ID", sequenceName="SEQ_PROD_ID", allocationSize=10) @GeneratedValue( strategy=GenerationType.SEQUENCE, generator="GEN_PROD_ID") private long prodId;
Uma discussão sobre um problema semelhante no Stackoverflow:
https://stackoverflow.com/questions/12745751/hibernate-sequencegenerator-and-allocationsize E aconteceu que, durante muitos meses após alterar o segundo componente e implementar as entradas na tabela, os processos de gravação do primeiro e do segundo componentes nunca se sobrepõem no tempo. E eles começaram a se cruzar quando, em uma das unidades que usavam o sistema, o horário de trabalho mudou ligeiramente.
Bem, os testes de integração foram tranqüilos, pois os intervalos de tempo para testar os dois componentes nos testes de integração também não se cruzaram.
De certa forma, podemos dizer que ninguém foi realmente culpado pelo erro.
Ou não é verdade?
Observações e Pensamentos
Depois de descobrir a verdadeira causa do erro, ele foi corrigido.
Mas não com esse final feliz, gostaria de terminar este artigo, mas reflito sobre esse erro como representante da vasta categoria de erros que ganharam popularidade após a transição de sistemas monolíticos para sistemas distribuídos.
Do ponto de vista de componentes ou serviços individuais no sistema Enterprise descrito, tudo foi feito, tudo parece estar certo. Todos os componentes ou serviços tiveram ciclos de vida independentes. E quando surgiu a necessidade de gravar na tabela no segundo componente, devido à insignificância da operação, foi tomada uma decisão pragmática para implementá-lo diretamente neste componente da maneira mais simples, e não para tocar no primeiro componente de trabalho estável.
Mas, infelizmente, o que aconteceu com frequência em sistemas distribuídos (e relativamente com menos frequência em sistemas monolíticos) aconteceu: a responsabilidade pela execução de operações em um objeto específico foi
espalhada entre subsistemas. Certamente, se as duas operações de gravação fossem implementadas no mesmo microsserviço, uma única tecnologia seria escolhida para sua implementação. E o erro descrito não teria ocorrido.
Os sistemas distribuídos, especialmente o conceito de microsserviços, ajudaram efetivamente a resolver vários problemas inerentes aos sistemas monolíticos. No entanto, paradoxalmente, a separação de responsabilidades por serviços individuais provoca o efeito oposto. Os componentes agora "vivem" o mais independente possível. E inevitavelmente há uma tentação, fazendo grandes mudanças em um componente, de "estragar aqui" uma pequena funcionalidade que seria melhor implementada em outro componente. Isso atinge rapidamente o efeito final, reduz o volume de aprovações e testes. Portanto, de uma mudança para outra, os componentes estão repletos de características incomuns para eles, os mesmos algoritmos e funções internos são duplicados, surgindo uma multivariância na solução de problemas (e às vezes seu não determinismo). Em outras palavras, um sistema distribuído se degrada com o tempo, mas de maneira diferente de um sistema monolítico.
A responsabilidade de "manchar" os componentes em grandes sistemas que consistem em muitos serviços é um dos problemas típicos e dolorosos dos sistemas distribuídos modernos. A situação é ainda mais complicada e confusa pelos subsistemas de otimização compartilhados, como cache, previsão das seguintes operações (previsão), bem como orquestração de serviços, etc.
Centralizando o acesso ao banco de dados, pelo menos no nível de uma única biblioteca, o requisito é bastante óbvio. No entanto, muitos sistemas distribuídos modernos cresceram historicamente em torno de bancos de dados e usam os dados armazenados neles diretamente (via SQL) e não através de serviços de acesso.
"Ajudando" a disseminação de responsabilidade e estruturas e bibliotecas ORM como o Hibernate. Utilizando-os, muitos desenvolvedores de serviços de acesso a bancos de dados, sem querer, desejam fornecer objetos o mais alto possível, como resultado da solicitação. Um exemplo típico é a solicitação de dados do usuário para exibi-los em uma saudação ou no campo com o resultado da autenticação. Em vez de retornar o nome de usuário na forma de três variáveis de texto (nome, nome, meio e sobrenome), essa solicitação geralmente retorna um objeto de usuário completo com dezenas de atributos e objetos conectados, como a lista de funções do usuário solicitado. Isso, por sua vez, complica a lógica de processar o resultado da solicitação, gera dependências desnecessárias do manipulador sobre o tipo de objeto retornado e ... provoca a disseminação de responsabilidades devido à possibilidade de implementar a lógica associada ao objeto de fora responsável por esse objeto de serviço.
O que fazer? (Recomendações)
Infelizmente, em alguns casos, a difusão de responsabilidades é forçada e, às vezes, até inevitável e justificada.
No entanto, se possível, você deve tentar cumprir o princípio de distribuição de responsabilidade entre os componentes. Um componente é uma responsabilidade.
Bem, se for impossível concentrar operações em determinados objetos estritamente em um sistema, essa mancha deve ser registrada com muito cuidado na documentação de todo o sistema ("supercomponente"), como a dependência específica dos componentes no elemento de dados, no objeto de domínio ou entre si.
Seria interessante conhecer sua opinião sobre esse assunto, bem como casos da prática confirmando ou refutando as teses deste artigo.
Obrigado por ler o artigo até o fim.
Ilustração "Multimedia Mikher", do autor do artigo.