Como você reproduz condições de erro e vê o que está acontecendo enquanto o aplicativo é executado?
Como você visualiza as interações entre as diferentes partes simultâneas do aplicativo?
Com base na minha experiência, a resposta para esses dois aspectos é a seguinte:
Rastreio Distribuído
O rastreamento distribuído é uma tecnologia que captura dados de tempo para cada componente simultâneo individual do seu sistema e os apresenta em formato gráfico. Representações de execuções simultâneas são sempre intercaladas, permitindo que você veja o que está sendo executado em paralelo e o que não é.
O rastreamento distribuído deve sua origem aos sistemas distribuídos (é claro), que são por definição assíncronos e altamente simultâneos. Um sistema distribuído com rastreamento distribuído permite que as pessoas:
a) identifique gargalos importantes, b) obtenha uma representação visual das 'execuções' ideais de sua aplicação, ec) forneça visibilidade sobre o comportamento simultâneo que está sendo executado; d) obtenha dados de tempo que podem ser usados para avaliar as diferenças entre as alterações no sistema (extremamente importante se você tiver SLAs fortes).
As conseqüências do rastreamento distribuído, no entanto, são:
Ele adiciona sobrecarga a todos os seus processos simultâneos, pois se traduz em mais código para executar e enviar potencialmente em uma rede. Em alguns casos, essa sobrecarga é altamente significativa - até o Google usa apenas o sistema de rastreamento Dapper em um pequeno subconjunto de todas as solicitações para não prejudicar a experiência do usuário.
Existem muitas ferramentas diferentes, nem todas interoperáveis entre si. Isso é um pouco melhorado por padrões como o OpenTracing, mas não totalmente resolvido.
Não diz nada sobre recursos compartilhados e seu status atual. Você pode adivinhar, com base no código do aplicativo e no gráfico que está mostrando, mas não é uma ferramenta útil nesse sentido.
As ferramentas atuais assumem que você tem memória e armazenamento de sobra. Hospedar um servidor de séries temporais pode não ser barato, dependendo de suas restrições.
Software de rastreamento de erros
Eu vinculo ao Sentry acima principalmente porque é a ferramenta mais usada por aí, e por boas razões - software de rastreamento de erros como a execução em tempo de execução do Sentry hijack para encaminhar simultaneamente um rastreamento de pilha dos erros encontrados para um servidor central.
O benefício líquido desse software dedicado em código simultâneo:
- Erros duplicados não são duplicados . Em outras palavras, se um ou mais sistemas simultâneos encontrarem a mesma exceção, o Sentry aumentará um relatório de incidente, mas não enviará duas cópias do incidente.
Isso significa que você pode descobrir qual sistema simultâneo está enfrentando qual tipo de erro sem precisar passar por inúmeros relatórios de erros simultâneos. Se você já sofreu spam de e-mail de um sistema distribuído, sabe como é o inferno.
Você pode até 'marcar' aspectos diferentes do seu sistema simultâneo (embora isso pressuponha que você não tenha trabalho intercalado em exatamente um encadeamento, o que tecnicamente não é simultâneo de qualquer maneira, já que o encadeamento está simplesmente pulando entre as tarefas com eficiência, mas ainda deve processar os manipuladores de eventos até a conclusão) e veja um detalhamento dos erros por tag.
- Você pode modificar esse software de tratamento de erros para fornecer detalhes adicionais com suas exceções de tempo de execução. Que recursos abertos o processo tinha? Existe um recurso compartilhado que este processo estava mantendo? Qual usuário teve esse problema?
Isso, além de rastreamentos meticulosos de pilha (e mapas de origem, se você precisar fornecer uma versão reduzida de seus arquivos), facilita a determinação do que está acontecendo de errado em grande parte do tempo.
- (Específico ao Sentry) Você pode ter um painel de relatórios Sentry separado para execuções de teste do sistema, permitindo detectar erros nos testes.
As desvantagens de tal software incluem:
Como tudo, eles adicionam em massa. Você pode não querer esse sistema em hardware incorporado, por exemplo. Eu recomendo fazer uma execução de teste desse software, comparando uma execução simples com e sem a amostragem de algumas centenas de execuções em uma máquina ociosa.
Nem todos os idiomas são igualmente suportados, pois muitos desses sistemas dependem da captura implícita de uma exceção e nem todos os idiomas apresentam exceções robustas. Dito isto, existem clientes para uma grande quantidade de sistemas.
Eles podem ser gerados como um risco de segurança, pois muitos desses sistemas são essencialmente de código fechado. Nesses casos, faça sua devida diligência em pesquisá-los ou, se preferir, faça o seu próprio.
Nem sempre eles podem fornecer as informações necessárias. Este é um risco com todas as tentativas de adicionar visibilidade.
A maioria desses serviços foi projetada para aplicativos da Web altamente simultâneos; portanto, nem todas as ferramentas podem ser perfeitas para o seu caso de uso.
Em resumo : ter visibilidade é a parte mais crucial de qualquer sistema concorrente. Os dois métodos que descrevi acima, em conjunto com painéis dedicados sobre hardware e dados para obter uma imagem sólida do sistema em um determinado momento, são amplamente utilizados em toda a indústria precisamente para abordar esse aspecto.
Algumas sugestões adicionais
Passei mais tempo do que me importo em corrigir o código por pessoas que tentaram resolver problemas simultâneos de maneiras terríveis. Sempre que encontrei casos em que as seguintes coisas poderiam melhorar muito a experiência do desenvolvedor (que é tão importante quanto a experiência do usuário):
Confie nos tipos . A digitação existe para validar seu código e pode ser usada em tempo de execução como uma proteção extra. Onde a digitação não existir, conte com asserções e um manipulador de erros adequado para detectar erros. O código simultâneo requer código defensivo e os tipos servem como o melhor tipo de validação disponível.
- Teste os links entre os componentes do código , não apenas o componente em si. Não confunda isso com um teste de integração completo - que testa todos os links entre todos os componentes e, mesmo assim, ele procura apenas uma validação global do estado final. Esta é uma maneira terrível de detectar erros.
Um bom teste de link verifica se, quando um componente se comunica com outro componente isoladamente , a mensagem recebida e a mensagem enviada são as mesmas que você espera. Se você tiver dois ou mais componentes que dependem de um serviço compartilhado para se comunicar, ative todos eles, faça com que eles troquem mensagens pelo serviço central e verifique se todos estão obtendo o que você espera no final.
A divisão de testes que envolvem muitos componentes em um teste dos próprios componentes e um teste de como cada um dos componentes se comunica também oferecem maior confiança na validade do seu código. Ter um corpo tão rigoroso de testes permite impor contratos entre serviços, bem como detectar erros inesperados que ocorrem quando eles estão em execução ao mesmo tempo.
- Use os algoritmos certos para validar o estado do seu aplicativo. Estou falando de coisas simples, como quando você tem um processo mestre aguardando que todos os trabalhadores concluam uma tarefa e só quer passar para a próxima etapa se todos os trabalhadores estiverem completos - este é um exemplo de detecção global terminação, para a qual existem metodologias conhecidas como o algoritmo do Safra.
Algumas dessas ferramentas vêm com os idiomas - o Rust, por exemplo, garante que seu código não terá condições de corrida no tempo de compilação, enquanto o Go possui um detector de conflito embutido que também é executado no tempo de compilação. Se você pode detectar problemas antes que eles atinjam a produção, é sempre uma vitória.
Uma regra geral: projetar falhas em sistemas concorrentes . Antecipe que serviços comuns travarão ou quebrarão. Isso vale mesmo para o código que não é distribuído entre as máquinas - o código simultâneo em uma única máquina pode depender de dependências externas (como um arquivo de log compartilhado, um servidor Redis, um maldito servidor MySQL) que podem desaparecer ou ser removidos a qualquer momento .
A melhor maneira de fazer isso é validar o estado do aplicativo de tempos em tempos - faça verificações de integridade para cada serviço e verifique se os consumidores desse serviço são notificados de problemas de saúde. Ferramentas modernas de contêineres como o Docker fazem isso muito bem e devem ser usadas para guardar coisas na área de areia.
Como você descobre o que pode ser tornado simultâneo e o que pode ser sequencial?
Uma das maiores lições que aprendi trabalhando em um sistema altamente simultâneo é esta: você nunca pode ter métricas suficientes . As métricas devem conduzir absolutamente tudo em seu aplicativo - você não é um engenheiro se não estiver medindo tudo.
Sem métricas, você não pode fazer algumas coisas muito importantes:
Avalie a diferença feita pelas mudanças no sistema. Se você não souber se o botão de ajuste A fez a métrica B subir e a métrica C cair, você não sabe como consertar seu sistema quando as pessoas enviam códigos inesperadamente malignos no sistema (e eles enviam o código para o sistema) .
Entenda o que você precisa fazer a seguir para melhorar as coisas. Até você saber que os aplicativos estão com pouca memória, não é possível discernir se deve obter mais memória ou comprar mais disco para seus servidores.
As métricas são tão cruciais e essenciais que fiz um esforço consciente para planejar o que quero medir antes mesmo de pensar no que um sistema exigirá. De fato, as métricas são tão cruciais que acredito que sejam a resposta certa para essa pergunta: você só sabe o que pode ser sequencial ou simultâneo quando mede o que os bits do seu programa estão fazendo. O design adequado usa números, não suposições.
Dito isto, certamente existem algumas regras práticas:
Sequencial implica dependência. Dois processos devem ser seqüenciais se um for dependente do outro de alguma maneira. Processos sem dependências devem ser simultâneos. No entanto, planeje uma maneira de lidar com falhas no fluxo que não impeça os processos a jusante de esperar indefinidamente.
Nunca misture uma tarefa vinculada de E / S com uma tarefa vinculada à CPU no mesmo núcleo. Não escreva (por exemplo) um rastreador da Web que ative dez solicitações simultâneas no mesmo encadeamento, raspe-as assim que elas chegarem e espere escalar para quinhentas - as solicitações de E / S vão para uma fila em paralelo, mas a CPU ainda passará por eles em série. (Esse modelo orientado a eventos de thread único é popular, mas é limitado por causa desse aspecto - em vez de entender isso, as pessoas simplesmente torcem as mãos e dizem que o Node não escala, para dar um exemplo).
Um único encadeamento pode fazer muito trabalho de E / S. Mas, para usar totalmente a simultaneidade do seu hardware, use conjuntos de threads que juntos ocupam todos os núcleos. No exemplo acima, o lançamento de cinco processos Python (cada um dos quais pode usar um núcleo em uma máquina de seis núcleos) apenas para o trabalho da CPU e um sexto encadeamento Python apenas para o trabalho de E / S serão dimensionados muito mais rapidamente do que você pensa.
A única maneira de tirar proveito da simultaneidade da CPU é através de um conjunto de threads dedicado. Um único encadeamento geralmente é bom o suficiente para muito trabalho vinculado de E / S. É por isso que os servidores Web orientados a eventos, como o Nginx, são dimensionados melhor (eles funcionam puramente com E / S) que o Apache (que confunde o trabalho com E / S com algo que requer CPU e inicia um processo por solicitação), mas por que usar o Node para executar dezenas de milhares de cálculos de GPU recebidos em paralelo é uma péssima ideia.