"Se um trabalhador quiser fazer bem o seu trabalho, ele deve primeiro afiar suas ferramentas." - Confúcio, "Os Analectos de Confúcio. Lu Linggong"
Primeira página > Programação > Um servidor da web com desempenho e extensível com ZIG e Python

Um servidor da web com desempenho e extensível com ZIG e Python

Postado em 2025-03-22
Navegar:483

Prefácio

Sou apaixonado pelo meu interesse no desenvolvimento de software, especificamente pelo quebra -cabeça de criar sistemas de software ergonomicamente que resolvem o conjunto mais amplo de problemas, tornando o mínimo possível de comprometer. Também gosto de pensar em mim como desenvolvedor de sistemas, que, pela definição de Andrew Kelley, significa um desenvolvedor interessado em entender completamente os sistemas com os quais estão trabalhando. Neste blog, compartilho com você minhas idéias sobre como resolver o seguinte problema: Construindo um aplicativo corporativo de pilha completa confiável e performente . Um grande desafio, não é? No blog, concentro -me na parte "Performant Web Server" - é aí que sinto que posso oferecer uma nova perspectiva, pois o resto é bem tridado, ou não tenho nada a acrescentar.

Uma ressalva principal - haverá sem amostras de código , eu realmente não testei isso. Sim, essa é uma falha importante, mas na verdade implementando isso levaria muito tempo, o que eu não tenho, e entre publicar um blog falho e não publicá -lo, fiquei com o primeiro. Você foi avisado.

A performant and extensible Web Server with Zig and Python

e que peças montaríamos nosso aplicativo?

    ...
  • um servidor da Web em zig, intimamente integrado ao kernel Linux. Esta é a parte do desempenho, em que vou me concentrar neste blog.
  • Um back -end do Python, integrado ao ZIG. Esta é a parte complexa.
  • integração com sistemas de execução duráveis, como temporal e fluente. Isso ajuda a confiabilidade e não será discutido no blog.
  • com nossas ferramentas decididas, vamos começar!

Os coroutines são superestimados de qualquer maneira?

ZIG não tem suporte de nível de idioma para coroutinas :( e coroutines é com o que todo servidor da web com desempenho é construído. Então, não há sentido em tentar?

Hold, on, vamos primeiro colocar nossos sistemas programador de chapéu. Coroutines não são uma bala de prata, nada é. Quais são os benefícios e desvantagens reais envolvidos?

é o conhecimento comum de que os coroutines (threads de espaço de usuários) são mais light e mais rápido. Mas de que maneira exatamente? (As respostas aqui são em grande parte especulações, tomam com um grão de sal e testem você mesmo)

eles começam com menos espaço de pilha por padrão (2kb em vez de 4 MB). Mas isso pode ser ajustado manualmente.
  • é melhor cooperar com o Scheduler do Usuáriospace. Como o agendador do kernel está preventivo, as tarefas executadas por threads são fatias de tempo alocadas. Se as tarefas reais não se encaixarem nas fatias - algum tempo de CPU for desperdiçado. Ao contrário de, digamos, Goroutines, que se encaixam no maior número de micro-tarefas executadas por diferentes goroutinas no mesmo tempo possível com o THREAD OS.

A performant and extensible Web Server with Zig and Python o tempo de execução Go, por exemplo, multiplexes goroutines nos threads do sistema operacional. Os threads compartilham a tabela de páginas, bem como outros recursos pertencentes a um processo. Se introduzirmos o isolamento e a afinidade da CPU com a mistura - os threads funcionarão continuamente em seus respectivos núcleos de CPU, todas as estruturas de dados do sistema operacional permanecerão na memória sem necessidade de ser trocadas, o agendador do Usuário alocará tempo de CPU para goroutines com precisão, porque ele usa o modelo multitações cooperativas. A competição é possível?

As vitórias no desempenho são alcançadas marginando a abstração no nível do OS de um thread e substituindo-o pelo de uma goroutina. Mas nada está perdido na tradução?

Podemos cooperar com o kernel?

Vou argumentar que a abstração "verdadeira" no nível do OS para uma unidade de execução independente não é nem um thread - na verdade é o processo do sistema operacional. Na verdade, a distinção aqui não é tão óbvia - tudo o que distingue encadeamentos e processos é os diferentes valores PID e TID. Quanto aos descritores de arquivos, memória virtual, manipuladores de sinal, recursos rastreados - se eles são separados para a criança, são especificados nos argumentos para o syscall "clone". Assim, usarei o termo "processo" para significar um segmento de execução que possui seus próprios recursos do sistema - principalmente tempo da CPU, memória, descritores de arquivos abertos.

A performant and extensible Web Server with Zig and Python agora por que isso é importante? Cada unidade de execução tem suas próprias demandas por recursos do sistema. Cada tarefa complexa pode ser dividida em unidades, onde cada uma pode fazer sua própria solicitação de recursos - memória e tempo da CPU. E quanto mais adiante a árvore das subtarefas você vai, em direção a uma tarefa mais geral - o gráfico de recursos do sistema forma uma curva de sino com caudas longas. E é sua responsabilidade garantir que as caudas não invadam o limite de recursos do sistema. Mas como isso é feito, e o que acontece se esse limite for de fato invadido?

Se usarmos o modelo de um único processo e muitas coroutinas para tarefas independentes, quando uma coroutina ultrapassa o limite de memória - porque o uso da memória é rastreado no nível do processo, todo o processo é morto. Isso é no melhor caso - se você utilizar os cgroups (que é automaticamente o caso de pods em Kubernetes, que possuem um cgroup por vagem) - todo o cgroup é morto. Fazer um sistema confiável precisa que isso seja levado em consideração. E quanto à hora da CPU? Se nosso serviço for atingido com muitas solicitações intensivas em computação ao mesmo tempo, ele não responderá. Em seguida, prazos, cancelamentos, tentativas, reinicializações seguem.

A única maneira realista de lidar com esses cenários para a maioria das pilhas de software convencional está deixando "gordura" no sistema - alguns recursos não utilizados para a cauda da curva - e limitando o número de solicitações simultâneas - que, novamente, levam a recursos não utilizados. E mesmo com isso, vamos matar ou não responder de vez em quando - incluindo solicitações "inocentes" que estão no mesmo processo que o Outlier. Esse compromisso é aceitável para muitos e serve sistemas de software na prática o suficiente. Mas podemos fazer melhor?

Um modelo de simultaneidade

Como o uso de recursos é rastreado por processo, idealmente geraríamos um novo processo para cada unidade de execução pequena e previsível. Em seguida, definimos o Ulimit para o tempo e a memória da CPU - e estamos prontos para ir! Ulimit possui limites suaves e duros, o que permitirá que o processo termine graciosamente ao atingir o limite suave e, se isso não ocorrer, possivelmente devido a um bug - seja rescindido com força ao atingir o limite rígido. Infelizmente, a geração de novos processos no Linux é lenta, a geração de novos processos por solicitação não é suportada para muitas estruturas da Web, além de outros sistemas como temporal. Além disso, a comutação de processos é mais cara - que é mitigada pela fixação de vaca e CPU, mas ainda não é ideal. Os processos de longa duração são uma realidade inevitável, infelizmente.

A performant and extensible Web Server with Zig and Python Quanto mais passamos da abstração limpa de processos de curta duração, quanto mais trabalho no nível do sistema operacional precisaríamos cuidar de nós mesmos. Mas também há benefícios a serem obtidos - como fazer uso de io_uring para o Batching IO entre muitos threads de execução. De fato, se uma grande tarefa for composta de subjugas - nós realmente nos preocupamos com a utilização individual de recursos? Apenas para perfis. Mas se, para a grande tarefa, pudéssemos gerenciar (cortar) as caudas da curva de campainha de recursos, isso seria bom o suficiente. Assim, poderíamos gerar tantos processos quanto os pedidos que desejamos lidar simultaneamente, fazer com que eles tenham vida longa e simplesmente reajuste o Ulimit por cada nova solicitação. Portanto, quando uma solicitação ultrapassa suas restrições de recursos, ele recebe um sinal do sistema operacional e é capaz de rescindir graciosamente, não afetando outras solicitações. Ou, se o alto uso de recursos for intencional, poderíamos pedir ao cliente que pague por uma cota de recursos mais alta. Parece muito bom para mim.

Mas o desempenho ainda sofrerá, em comparação com uma abordagem coroutine por solicitação. Primeiro, copiar a tabela de memória de processo é caro. Como a tabela contém referências às páginas de memória, poderíamos fazer uso de grandes páginas, limitando assim o tamanho dos dados a serem copiados. Isso só é diretamente possível com idiomas de baixo nível, como o ZIG. Além disso, a multitarefa no nível do sistema operacional é preventiva e não é cooperativa, o que sempre será menos eficiente. Ou é?

Multitarefa cooperativa com Linux

existe o syscall sched_yield, que permite que o thread entregue a CPU quando concluir sua parte do trabalho. Parece bastante cooperativo. Poderia haver uma maneira de solicitar uma fatia de tempo de um determinado tamanho também? Na verdade, existe - com a política de agendamento Agend_deadline. Esta é uma política em tempo real, o que significa que, para a fatia de tempo da CPU solicitada, o thread funciona ininterrupto. Mas se a fatia for invadida - a preempção entra em ação e seu thread for trocado e depresente. E se a fatia estiver submet - o thread poderá ligar para o Scheding_yield para sinalizar um acabamento antecipado, permitindo que outros threads sejam executados. Parece o melhor dos dois mundos - um modelo cooperativo e preemtivo.

A performant and extensible Web Server with Zig and Python Uma limitação é o fato de que um thread sched_deadline não pode gastar. Isso nos deixa com dois modelos de simultaneidade - um processo por solicitação, que define o prazo para si e executa um loop de eventos para IO eficiente, ou um processo que, desde o início, gera um tópico para cada micro -tarefa, cada um dos quais define seu próprio prazo e faz uso de filas para comunicação uma. O primeiro é mais reto, mas requer um loop de eventos no espaço do usuário, o último faz mais uso do kernel.

Ambas as estratégias alcançam o mesmo fim que o modelo Coroutine -

cooperando com o kernel, é possível fazer com que as tarefas de aplicativos sejam executadas com interrupções mínimas

. Python como uma linguagem de script incorporada

isso é tudo para o lado das coisas de alto desempenho e baixa latência e baixo nível, onde o zig brilha. Mas quando se trata do negócio real do aplicativo, a flexibilidade é muito mais valiosa que a latência. Se um processo envolve pessoas reais que assinam documentos - a latência de um computador é insignificante. Além disso, apesar do sofrimento no desempenho, os idiomas orientados a objetos dão ao desenvolvedor melhores primitivas para modelar o domínio dos negócios. E no final mais distante disso, sistemas como Flowable e Camunda permitem que a equipe gerencial e de operações programe a lógica de negócios com mais flexibilidade e uma barreira mais baixa de entrada. Idiomas como o Zig não ajudarão com isso, e só estão no seu caminho.

A performant and extensible Web Server with Zig and Python python, por outro lado, é um dos idiomas mais dinâmicos que existem. Aulas, objetos - são todos dicionários sob o capô e podem ser manipulados em tempo de execução da maneira que você quiser. Isso tem uma penalidade de desempenho, mas torna a modelagem dos negócios com aulas e objetos e muitos truques inteligentes práticos. O ZIG é o oposto disso - existem intencionalmente poucos truques inteligentes no ZIG, oferecendo controle máximo. Podemos combinar seus poderes com eles interoperados?

de fato, podemos, devido a ter suporte o C ABI. Podemos fazer com que o intérprete Python seja executado dentro do processo em zig, e não como um processo separado, reduzindo a sobrecarga no custo de tempo de execução e no código de cola. Isso nos permite fazer uso dos alocadores personalizados do ZIG no Python - definindo uma arena para processar a solicitação individual, reduzindo assim, se não eliminando a sobrecarga de um coletor de lixo e definir uma tampa de memória. Uma grande limitação seria os fios de desova do tempo de execução do CPYTHON para a coleção de lixo e a IO, mas não encontrei evidências de que ela o faça. Poderíamos conectar o Python em um loop de eventos personalizados em Zig, com rastreamento de memória por coro-cor-rotina, usando o campo "Contexto" no AbstractMemoryloop. As possibilidades são ilimitadas.

Conclusão

discutimos os méritos de simultaneidade, paralelismo e várias formas de integração com o kernel do sistema operacional. A exploração carece de benchmarks e código, que espero compensar a qualidade das idéias oferecidas. Você já tentou algo semelhante? O que são seus pensamentos? Feedback bem -vindo :)

Leitura adicional

https://linux.die.net/man/2/clone
  • https://man7.org/linux/man-pages/man7/sched.7.html
  • https://man7.org/linux/man-pages/man2/sched_yield.2.html
  • https://rigtorp.se/low-latency-guide/
  • https://eli.thegreenplace.net/2018/measuring-context-switching-andemory-overheads-for-linux-threads/
  • https://hadar.gr/2017/lightweight-goroutines
Declaração de lançamento Este artigo é reproduzido em: https://dev.to/brogrmersyjohn/a-performant-and-enstensible-web-severver-with-zig-and-python-4adl?1 Se houver alguma infração, entre em contato com [email protected] para excluí-lo.
Tutorial mais recente Mais>

Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.

Copyright© 2022 湘ICP备2022001581号-3