Devo manter ou abandonar o Python para lidar com a simultaneidade?

31

Eu tenho um projeto LOC de 10K escrito em Django com bastante aipo ( RabbitMQ ) para assincronismo e trabalhos em segundo plano, quando necessário, e cheguei à conclusão de que partes do sistema se beneficiariam de serem reescritas em algo diferente do Django para melhor concorrência . Os motivos incluem:

  • Manuseio de sinais e objetos mutáveis. Especialmente quando um sinal dispara outro, lidar com eles no Django usando o ORM pode ser surpreendente quando as instâncias mudam ou desaparecem. Quero usar alguma abordagem de mensagens em que os dados transmitidos não sejam alterados em um manipulador ( a abordagem de copiar na gravação de Clojure parece boa, se eu entendi direito).
  • Partes do sistema não são baseadas na Web e precisam de melhor suporte para executar tarefas simultaneamente. Por exemplo, o sistema lê tags NFC e, quando alguém é lido, um LED acende por alguns segundos (tarefa de aipo), um som é reproduzido (outra tarefa de aipo) e o banco de dados é consultado (outra tarefa). Isso é implementado como um comando de gerenciamento do Django, mas o Django e seu ORM são síncronos por natureza e compartilham memória é limitante (estamos pensando em adicionar mais leitores NFC, e não acho que a abordagem do Django + Aipo funcione mais, Eu gostaria de ver melhores recursos de transmissão de mensagens).

Quais são os prós e os contras de usar algo como Twisted ou Tornado em comparação com o uso de idiomas como Erlang ou Clojure ? Estou interessado em benefícios e prejuízos práticos.

Como você chegou à conclusão de que algumas partes do sistema se sairiam melhor em outro idioma? Você está tendo problemas de desempenho? Quão graves são esses problemas? Se puder ser mais rápido, é essencial que seja mais rápido?

Exemplo 1: Django trabalhando fora de uma solicitação HTTP:

  1. Uma etiqueta NFC é lida.
  2. O banco de dados (e possivelmente o LDAP) é consultado e queremos fazer alguma coisa quando os dados estiverem disponíveis (luz vermelha ou verde, tocar um som). Isso bloqueia o uso do Django ORM, mas contanto que haja trabalhadores disponíveis, isso não importa. Pode haver um problema com mais estações.

Exemplo 2: “passagem de mensagens” usando sinais do Django:

  1. Um post_deleteevento é tratado, outros objetos podem ser alterados ou excluídos por causa disso.
  2. No final, as notificações devem ser enviadas aos usuários. Aqui, seria bom se os argumentos passados ​​para o manipulador de notificações fossem cópias de objetos excluídos ou a serem excluídos e garantissem que não fossem alterados no manipulador. (Isso poderia ser feito manualmente simplesmente não passando objetos gerenciados pelo ORM para manipuladores, é claro.)
Simon Pantzare
fonte
Eu acho que as melhores respostas vai acontecer se você explicar mais sobre por que você veio à conclusão
Winston Ewert
5
Antes que alguém diga que as questões de escolha de idioma estão fora do tópico, gritei dizendo que acho que essa é boa, pois é um problema prático com requisitos específicos. Espero que faça algumas comparações detalhadas.
Adam Lear
Torcido é o oposto de simultâneo! É um servidor único encadeado que é orientado a eventos; ele não o levará a lugar algum se você precisar de simultaneidade verdadeira.

Respostas:

35

Pensamentos iniciais

Como você chegou à conclusão de que algumas partes do sistema se sairiam melhor em outro idioma? Você está tendo problemas de desempenho? Quão graves são esses problemas? Se puder ser mais rápido, é essencial que seja mais rápido?

Assincronia de thread único

Existem várias perguntas e outros recursos da Web que já lidam com as diferenças, prós e contras da assincronia de thread único versus simultaneidade de vários threads. É interessante ler sobre o desempenho do modelo assíncrono de thread único do Node.js. quando E / S é o principal gargalo, e há muitas solicitações sendo atendidas ao mesmo tempo.

Os modelos Twisted, Tornado e outros assíncronos fazem excelente uso de um thread único. Como grande parte da programação da Web possui muitas E / S (rede, banco de dados etc.), o tempo gasto na espera de chamadas remotas aumenta significativamente. É o tempo que poderia ser gasto fazendo outras coisas - como iniciar outras chamadas ao banco de dados, renderizar páginas e gerar dados. A utilização desse segmento único é extremamente alta.

Um dos maiores benefícios da assincronia de thread único é que ela usa muito menos memória. Na execução de vários threads, cada thread requer uma certa quantidade de memória reservada. À medida que o número de threads aumenta, aumenta a quantidade de memória necessária apenas para a existência dos threads. Como a memória é finita, significa que há limites no número de threads que podem ser criados a qualquer momento.


Exemplo

No caso de um servidor Web, finja que cada solicitação recebe seu próprio encadeamento. Digamos que seja necessário 1 MB de memória para cada encadeamento e o servidor da Web tenha 2 GB de RAM. Esse servidor da Web seria capaz de processar (aproximadamente) 2000 solicitações a qualquer momento, antes que não houvesse memória suficiente para processar mais.

Se sua carga for significativamente maior que isso, as solicitações demorarão muito (ao aguardar a conclusão de solicitações mais antigas) ou você precisará lançar mais servidores no cluster para expandir o número possível de solicitações simultâneas. .


Simultaneidade multithread

A simultaneidade de vários segmentos depende da execução de várias tarefas ao mesmo tempo. Isso significa que, se um encadeamento estiver bloqueado aguardando o retorno de uma chamada do banco de dados, outras solicitações poderão ser processadas ao mesmo tempo. A utilização do encadeamento é menor, mas o número de encadeamentos em execução é muito maior.

O código multiencadeado também é muito mais difícil de raciocinar. Há problemas com bloqueio, sincronização e outros problemas divertidos de simultaneidade. A assincronia de thread único não sofre os mesmos problemas.

Porém, o código multiencadeado tem muito mais desempenho para tarefas intensivas da CPU . Se não houver oportunidades para um encadeamento "render", como uma chamada de rede que normalmente bloqueia, um modelo de encadeamento único simplesmente não terá nenhuma simultaneidade.

Ambos podem coexistir

É claro que há sobreposição entre os dois; eles não são mutuamente exclusivos. Por exemplo, o código multithread pode ser escrito de maneira não-bloqueante, para melhor utilizar cada thread.


A linha inferior

Há muitas outras questões a serem consideradas, mas eu gosto de pensar sobre as duas dessa maneira:

  • Se o seu programa estiver vinculado à E / S , a assincronia de thread único provavelmente funcionará muito bem.
  • Se o seu programa estiver vinculado à CPU , um sistema multi-thread provavelmente será o melhor.

No seu caso particular, você precisa determinar que tipo de trabalho assíncrono está sendo concluído e com que frequência essas tarefas surgem.

  • Eles ocorrem em cada solicitação? Nesse caso, a memória provavelmente se tornará um problema à medida que o número de solicitações aumentar.
  • Essas tarefas estão ordenadas? Nesse caso, você precisará considerar a sincronização se usar vários threads.
  • Essas tarefas são intensivas em CPU? Em caso afirmativo, um thread único é capaz de acompanhar a carga?

Não existe uma resposta simples. Você deve considerar quais são seus casos de uso e projetar de acordo. Às vezes, um modelo de thread único assíncrono é melhor. Outras vezes, é necessário usar vários threads para obter um processamento paralelo maciço.

outras considerações

Há outros problemas que você precisa considerar também, em vez de apenas o modelo de concorrência escolhido. Você conhece Erlang ou Clojure? Você acha que seria capaz de escrever um código multithread seguro em um desses idiomas para melhorar o desempenho do seu aplicativo? Vai demorar muito tempo para se atualizar em um desses idiomas, e o idioma que você aprenderá o beneficiará no futuro?

E as dificuldades associadas à comunicação entre esses dois sistemas? Será muito complexo manter dois sistemas separados em paralelo? Como o sistema Erlang receberá tarefas do Django? Como Erlang comunicará esses resultados de volta ao Django? O desempenho é significativo o suficiente para que a complexidade adicionada valha a pena?


Pensamentos finais

Eu sempre achei o Django rápido o suficiente e é usado por sites com tráfego intenso. Existem várias otimizações de desempenho que você pode fazer para aumentar o número de solicitações simultâneas e o tempo de resposta. É certo que eu não fiz nada com o Celery até agora; portanto, as otimizações de desempenho usuais provavelmente não resolverão nenhum problema que você possa ter com essas tarefas assíncronas.

Claro, sempre há a sugestão de jogar mais hardware no problema. O custo de provisionar um novo servidor é mais barato que o custo de desenvolvimento e manutenção de um subsistema totalmente novo?

Fiz muitas perguntas neste momento, mas essa era minha intenção. A resposta não será fácil sem análise e mais detalhes. Ser capaz de analisar os problemas se resume a conhecer as perguntas a serem feitas, no entanto ... então espero ter ajudado nessa frente.

Meu pressentimento diz que uma reescrita em outro idioma é desnecessária. A complexidade e o custo provavelmente serão grandes demais.


Editar

Resposta ao acompanhamento

Seu acompanhamento apresenta alguns casos de uso muito interessantes.


1. Django trabalhando fora de solicitações HTTP

Seu primeiro exemplo envolveu a leitura de tags NFC e a consulta ao banco de dados. Não acho que escrever essa parte em outro idioma seja útil para você, simplesmente porque a consulta ao banco de dados ou a um servidor LDAP ficará vinculada à E / S da rede (e potencialmente ao desempenho do banco de dados). Por outro lado, o número de solicitações simultâneas será vinculado pelo próprio servidor, pois cada comando de gerenciamento será executado como seu próprio processo. Haverá tempo de configuração e desmontagem que afeta o desempenho, pois você não está enviando mensagens para um processo já em execução. No entanto, você poderá enviar várias solicitações ao mesmo tempo, pois cada uma será um processo isolado.

Nesse caso, vejo duas avenidas que você pode investigar:

  1. Verifique se o seu banco de dados é capaz de lidar com várias consultas ao mesmo tempo com o pool de conexões. (O Oracle, por exemplo, exige que você configure o Django de acordo 'OPTIONS': {'threaded':True}.) Pode haver opções de configuração semelhantes no nível do banco de dados ou no Django que você pode ajustar para o seu próprio banco de dados. Independentemente do idioma em que você escreve suas consultas ao banco de dados, será necessário aguardar o retorno desses dados antes de acender os LEDs. O desempenho do código de consulta pode fazer a diferença, e o Django ORM não é muito rápido ( mas , geralmente rápido o suficiente).
  2. Minimize o tempo de configuração / desmontagem. Tenha um processo em execução constante e envie mensagens para ele. (Corrija-me se eu estiver errado, mas é nisso que a sua pergunta original está realmente se concentrando.) Se este processo está escrito em Python / Django ou se outro idioma / estrutura está coberto acima. Não gosto da ideia de usar comandos de gerenciamento com tanta frequência. É possível ter um pequeno pedaço de código em execução constantemente, que envia as mensagens dos leitores NFC para a fila de mensagens, que o Celery lê e encaminha para o Django? A configuração e desmontagem de um pequeno programa, mesmo que seja escrita em Python (mas não no Django!), Deve ser melhor do que iniciar e parar um programa Django (com todos os seus subsistemas).

Não tenho certeza de qual servidor da Web você está usando para o Django. mod_wsgipara Apache permite configurar o número de processos e encadeamentos dentro dos processos que os serviços solicitam. Certifique-se de ajustar a configuração relevante do seu servidor da web para otimizar o número de solicitações que podem ser reparadas.


2. “Passagem de mensagem” com sinais do Django

Seu segundo caso de uso também é bastante interessante; Não tenho certeza se tenho as respostas para isso. Se você estiver excluindo instâncias de modelo e desejar operá-las posteriormente, pode ser possível serializá-las JSON.dumpse desserializar JSON.loads. Será impossível recriar completamente o gráfico de objetos posteriormente (consultando modelos relacionados), pois os campos relacionados são carregados com preguiça no banco de dados e esse link não existe mais.

A outra opção seria marcar de alguma forma um objeto para exclusão e excluí-lo apenas no final do ciclo de solicitação / resposta (após todos os sinais terem sido atendidos). Pode exigir um sinal personalizado para implementar isso, em vez de confiar post_delete.

Josh Smeaton
fonte
1
muito FUD e dúvidas sobre bloqueio e outras coisas que não são problemas com o Erlang, nenhum dos problemas tradicionais de estado compartilhado que você lista são considerações com um idioma e tempo de execução projetados especificamente para não compartilhar o estado. Erlang pode lidar com dezenas de milhares de processos discretos em pouquíssimos RAM, pressão da memória também não é um problema.
@ Jarrod, eu pessoalmente não conheço Erlang, então aceitarei o que você diz a esse respeito. Caso contrário, quase tudo o que mencionei é relevante. Custo, complexidade e se as ferramentas atuais estão sendo adequadamente utilizadas ou não.
21711 Josh Smeaton
Esse é o tipo de resposta épica que eu realmente amo ler ^^. +1, bom trabalho!
precisa
Além disso, se você tem Django templates que podem ser usados em erlang com Erlydtl
Zachary K
8

Fiz um desenvolvimento altamente sofisticado e altamente escalável para um grande ISP dos EUA . Fizemos alguns números sérios de transações usando um servidor Twisted , e foi um pesadelo de complexidade fazer o Python / Twisted escalar em qualquer coisa que estivesse ligada à CPU . O limite de E / S não é um problema, mas o limite da CPU era impossível. Poderíamos montar sistemas rapidamente, mas fazê-los escalar para milhões de usuários simultâneos era um pesadelo de configuração e complexidade, se eles estivessem vinculados à CPU.

Eu escrevi um post sobre isso, Python / Twisted VS Erlang / OTP .

TLDR; Erlang venceu.


fonte
4

Problemas práticos com o Twisted (que eu amo e uso há cerca de cinco anos):

  1. A documentação deixa algo a desejar, e o modelo é bastante complexo para aprender de qualquer maneira. Acho difícil conseguir que outros programadores Python trabalhem no código Twisted.
  2. Acabei usando E / S de arquivo de bloqueio e acesso ao banco de dados por falta de boas APIs de bloqueio. Isso pode realmente prejudicar o desempenho.
  3. Não parece haver uma comunidade enorme e saudável usando o Twisted; por exemplo, o Node.js tem um desenvolvimento muito mais ativo, especialmente para programação de back-end da web.
  4. Ainda é Python, e pelo menos o CPython não é a coisa mais rápida.

Fiz um pouco de trabalho usando o Node.js. com o CoffeeScript e se o desempenho simultâneo é sua preocupação, isso pode valer a pena.

Você já pensou em executar várias instâncias do Django com algum arranjo para espalhar clientes entre instâncias?

Dickon Reed
fonte
1
A documentação do Python em geral deixa algo a desejar: / (Não estou dizendo que é tão ruim assim, mas para uma linguagem que a popular esperaria que fosse muito melhor).
Rook
3
Acho que a documentação do Python e, em particular, a documentação do Django são alguns dos melhores documentos para qualquer idioma. Muitas bibliotecas de terceiros deixam algo a desejar.
Josh Smeaton
1

Sugerirei o seguinte antes de considerar mudar para outro idioma.

  1. Use LTTng para registrar eventos do sistema, como falhas de página, alternâncias de contexto e esperas de chamada do sistema.
  2. Converta onde quer que esteja demorando muito tempo para usar a biblioteca C e use qualquer padrão de design que você desejar (multiencadeamento, baseado em eventos de sinal, retorno de chamada assíncrono ou tradicional do Unix select), o que é bom para E / S.

Eu não usaria threading no Python uma vez que o aplicativo tenha prioridade no desempenho. Eu usaria a opção acima, que pode resolver muitos problemas, como reutilização de software, conectividade com Django , desempenho, facilidade de desenvolvimento, etc.

Holmes
fonte