Heim > Backend-Entwicklung > Python-Tutorial > Ein leistungsstarker und erweiterbarer Webserver mit Zig und Python

Ein leistungsstarker und erweiterbarer Webserver mit Zig und Python

Linda Hamilton
Freigeben: 2024-10-07 06:12:01
Original
628 Leute haben es durchsucht

머리말

저는 소프트웨어 개발, 특히 타협을 최소화하면서 가장 광범위한 문제를 해결하는 소프트웨어 시스템을 인체공학적으로 만드는 퍼즐에 관심이 많습니다. 나는 또한 나 자신을 시스템 개발자라고 생각하고 싶습니다. Andrew Kelley의 정의에 따르면 이는 작업 중인 시스템을 완전히 이해하는 데 관심이 있는 개발자를 의미합니다. 이 블로그에서는 안정적이고 성능이 뛰어난 풀스택 엔터프라이즈 애플리케이션 구축이라는 문제를 해결하기 위한 제 아이디어를 여러분과 공유합니다. 꽤 어려운 일이지 않습니까? 블로그에서는 "성능이 뛰어난 웹 서버" 부분에 초점을 맞췄습니다. 나머지 부분은 잘 다루어졌거나 추가할 내용이 없기 때문에 이 부분에서 새로운 관점을 제공할 수 있다고 생각합니다.

주요 주의 사항 - 코드 샘플이 없습니다. 실제로 테스트해 본 적은 없습니다. 예, 이것은 주요 결함이지만 실제로 이것을 구현하는 데는 시간이 많이 걸릴 것입니다. 그럴 필요가 없으며 결함이 있는 블로그를 게시하고 전혀 게시하지 않는 사이에 전자를 고수했습니다. 경고를 받았습니다.

A performant and extensible Web Server with Zig and Python

그리고 어떤 부분으로 애플리케이션을 조립할까요?

  • 편안한 프런트엔드이지만 최소한의 종속성을 원한다면 WASM 형식 ​​HTMX의 Zig가 있습니다.
  • Linux 커널과 긴밀하게 통합된 Zig 웹 서버입니다. 이것이 제가 이 블로그에서 집중적으로 다루게 될 성능 부분입니다.
  • Zig와 통합된 Python 백엔드입니다. 이것이 복잡한 부분입니다.
  • Temporal 및 Flowable과 같은 내구성 있는 실행 시스템과 통합됩니다. 이는 신뢰성에 도움이 되므로 블로그에서는 다루지 않습니다.

도구가 결정되었으니 시작해 보세요!

어쨌든 코루틴은 과대평가되어 있나요?

Zig는 코루틴을 언어 수준으로 지원하지 않습니다. :( 그리고 코루틴은 모든 성능이 뛰어난 웹 서버가 구축하는 데 사용됩니다. 그렇다면 노력할 필요가 없나요?

잠깐만, 먼저 시스템 프로그래머 모자를 쓰자. 코루틴은 만병통치약이 아니며, 아무것도 아닙니다. 관련된 실제 이점과 단점은 무엇입니까?

코루틴(사용자 공간 스레드)이 더 가볍고 빠르다는 것은 상식입니다. 그런데 정확히 어떤 방식으로요? (여기에 나온 답변은 대부분 추측이므로, 직접 테스트해 보세요.)

  • 기본적으로 더 적은 스택 공간(4MB 대신 2KB)으로 시작합니다. 하지만 수동으로 조정할 수도 있습니다.
  • 사용자 공간 스케줄러와 협력하는 것이 좋습니다. 커널 스케줄러는 선점형이므로 스레드가 수행하는 작업에는 시간 조각이 할당됩니다. 실제 작업이 조각에 맞지 않으면 일부 CPU 시간이 낭비됩니다. 예를 들어, 다른 고루틴에 의해 수행되는 많은 마이크로 작업을 OS 스레드의 동일한 시간 조각에 최대한 맞추는 고루틴과는 대조적입니다.

A performant and extensible Web Server with Zig and Python

예를 들어 Go 런타임은 고루틴을 OS 스레드에 다중화합니다. 스레드는 페이지 테이블과 프로세스가 소유한 다른 리소스를 공유합니다. CPU 격리 및 선호도를 혼합에 도입하면 스레드는 해당 CPU 코어에서 지속적으로 실행되고 모든 OS 데이터 구조는 교체할 필요 없이 메모리에 유지되며 사용자 공간 스케줄러는 CPU 시간을 고루틴에 할당합니다. 협동적 멀티태스킹 모델을 사용하기 때문입니다. 경쟁도 가능한가요?

성능 향상은 스레드의 OS 수준 추상화를 무시하고 이를 고루틴의 추상화로 대체함으로써 달성됩니다. 그런데 번역에서 빠진 부분은 없나요?

커널과 협력할 수 있나요?

독립적인 실행 단위에 대한 "진정한" OS 수준 추상화는 스레드가 아니라 실제로 OS 프로세스라고 주장하겠습니다. 실제로 여기서 구별은 그다지 명확하지 않습니다. 스레드와 프로세스를 구별하는 것은 서로 다른 PID 및 TID 값뿐입니다. 파일 설명자, 가상 메모리, 신호 처리기, 추적된 리소스의 경우 이러한 항목이 자식에 대해 별도인지 여부는 "clone" syscall에 대한 인수에 지정됩니다. 따라서 "프로세스"라는 용어를 사용하여 자체 시스템 리소스(주로 CPU 시간, 메모리, 열린 파일 설명자)를 소유하는 실행 스레드를 의미합니다.

A performant and extensible Web Server with Zig and Python

Warum ist das nun wichtig? Jede Ausführungseinheit hat ihre eigenen Anforderungen an Systemressourcen. Jede komplexe Aufgabe kann in Einheiten zerlegt werden, wobei jede einzelne ihre eigene, vorhersehbare Ressourcenanforderung stellen kann – Speicher und CPU-Zeit. Und je weiter Sie im Baum der Unteraufgaben nach oben gehen, hin zu einer allgemeineren Aufgabe – das Diagramm der Systemressourcen bildet eine Glockenkurve mit langen Enden. Und es liegt in Ihrer Verantwortung, sicherzustellen, dass die Tails nicht die Systemressourcengrenze überschreiten. Aber wie geht das und was passiert, wenn diese Grenze tatsächlich überschritten wird?

Wenn wir das Modell eines einzelnen Prozesses und vieler Coroutinen für unabhängige Aufgaben verwenden und eine Coroutine das Speicherlimit überschreitet, wird der gesamte Prozess abgebrochen, da die Speichernutzung auf Prozessebene verfolgt wird. Das ist im besten Fall der Fall – wenn Sie Kontrollgruppen verwenden (was bei Pods in Kubernetes automatisch der Fall ist, die eine Kontrollgruppe pro Pod haben) – wird die gesamte Kontrollgruppe getötet. Um ein zuverlässiges System zu schaffen, muss dies berücksichtigt werden. Und wie sieht es mit der CPU-Zeit aus? Wenn unser Dienst gleichzeitig mit vielen rechenintensiven Anfragen konfrontiert wird, reagiert er nicht mehr. Dann folgen Fristen, Absagen, Wiederholungsversuche, Neustarts.

Der einzig realistische Weg, mit diesen Szenarien für die meisten Mainstream-Software-Stacks umzugehen, besteht darin, „Fett“ im System zu belassen – einige ungenutzte Ressourcen für das Ende der Glockenkurve – und die Anzahl gleichzeitiger Anfragen zu begrenzen – was wiederum führt zu ungenutzten Ressourcen. Und selbst dann kommt es hin und wieder dazu, dass OOM getötet wird oder nicht mehr reagiert – auch bei „unschuldigen“ Anfragen, die sich zufällig im selben Prozess wie der Ausreißer befinden. Dieser Kompromiss ist für viele akzeptabel und leistet in der Praxis gute Dienste für Softwaresysteme. Aber können wir es besser machen?

Ein Parallelitätsmodell

Da die Ressourcennutzung pro Prozess verfolgt wird, würden wir idealerweise für jede kleine, vorhersehbare Ausführungseinheit einen neuen Prozess erzeugen. Dann legen wir das Ulimit für CPU-Zeit und Speicher fest – und schon kann es losgehen! ulimit verfügt über weiche und harte Grenzen, die es dem Prozess ermöglichen, bei Erreichen der weichen Grenze ordnungsgemäß beendet zu werden. Wenn dies nicht geschieht, möglicherweise aufgrund eines Fehlers, wird er bei Erreichen der harten Grenze zwangsweise beendet. Leider ist das Erzeugen neuer Prozesse unter Linux langsam. Das Erzeugen neuer Prozesse pro Anfrage wird von vielen Web-Frameworks und anderen Systemen wie Temporal nicht unterstützt. Darüber hinaus ist der Prozesswechsel teurer – was durch CoW und CPU-Pinning abgemildert wird, aber immer noch nicht ideal ist. Langwierige Prozesse sind leider eine unvermeidliche Realität.

A performant and extensible Web Server with Zig and Python

Je weiter wir uns von der sauberen Abstraktion kurzlebiger Prozesse entfernen, desto mehr Arbeit auf Betriebssystemebene müssten wir für uns selbst erledigen. Es lassen sich aber auch Vorteile erzielen – beispielsweise die Verwendung von io_uring zum Stapeln von E/A zwischen vielen Ausführungsthreads. Wenn eine große Aufgabe tatsächlich aus Unteraufgaben besteht – ist uns dann wirklich die individuelle Ressourcennutzung wichtig? Nur zur Profilerstellung. Aber wenn wir für die große Aufgabe die Enden der Glockenkurve der Ressource verwalten (abschneiden) könnten, wäre das ausreichend. Wir könnten also so viele Prozesse wie die Anfragen, die wir gleichzeitig bearbeiten möchten, erzeugen, sie langlebig gestalten und einfach das ulimit für jede neue Anfrage neu anpassen. Wenn also eine Anfrage ihre Ressourcenbeschränkungen überschreitet, erhält sie ein Betriebssystemsignal und kann ordnungsgemäß beendet werden, ohne dass andere Anfragen davon betroffen sind. Oder wenn der hohe Ressourcenverbrauch beabsichtigt ist, können wir den Kunden auffordern, für ein höheres Ressourcenkontingent zu zahlen. Klingt für mich ziemlich gut.

Aber die Leistung wird im Vergleich zu einem Coroutine-pro-Anfrage-Ansatz immer noch leiden. Erstens ist das Kopieren um die Prozessspeichertabelle herum teuer. Da die Tabelle Verweise auf Speicherseiten enthält, könnten wir Hugepages verwenden und so die Größe der zu kopierenden Daten begrenzen. Dies ist nur mit Low-Level-Sprachen wie Zig direkt möglich. Darüber hinaus ist das Multitasking auf Betriebssystemebene präventiv und nicht kooperativ, was immer weniger effizient ist. Oder doch?

Kooperatives Multitasking mit Linux

Es gibt den Systemaufruf sched_yield, der es dem Thread ermöglicht, die CPU freizugeben, wenn er seinen Teil der Arbeit abgeschlossen hat. Scheint recht kooperativ zu sein. Könnte es auch eine Möglichkeit geben, eine Zeitscheibe einer bestimmten Größe anzufordern? Tatsächlich gibt es das – mit der Planungsrichtlinie SCHED_DEADLINE. Dies ist eine Echtzeitrichtlinie, was bedeutet, dass der Thread für die angeforderte CPU-Zeitscheibe ununterbrochen ausgeführt wird. Wenn das Slice jedoch überschritten wird, greift die Vorkaufssperre und Ihr Thread wird ausgelagert und priorisiert. Und wenn das Slice unterschritten wird, kann der Thread sched_yield aufrufen, um ein frühes Ende zu signalisieren, sodass andere Threads ausgeführt werden können. Das scheint das Beste aus beiden Welten zu sein – ein kooperatives und präventives Modell.

A performant and extensible Web Server with Zig and Python

Eine Einschränkung besteht darin, dass ein SCHED_DEADLINE-Thread nicht gegabelt werden kann. Dies lässt uns zwei Modelle für Parallelität übrig – entweder einen Prozess pro Anfrage, der die Frist für sich selbst festlegt und eine Ereignisschleife für effizientes IO ausführt, oder einen Prozess, der von Anfang an einen Thread für jede Mikroaufgabe erzeugt legt seine eigene Frist fest und nutzt Warteschlangen für die Kommunikation untereinander. Ersteres ist einfacher, erfordert aber eine Ereignisschleife im Userspace, letzteres nutzt den Kernel stärker aus.

Beide Strategien erreichen das gleiche Ziel wie das Coroutine-Modell – durch die Zusammenarbeit mit dem Kernel ist es möglich, Anwendungsaufgaben mit minimalen Unterbrechungen auszuführen.

Python als eingebettete Skriptsprache

Das ist alles für die Seite mit hoher Leistung, geringer Latenz und niedrigem Level, bei der Zig glänzt. Aber wenn es um das eigentliche Geschäft der Anwendung geht, ist Flexibilität viel wertvoller als Latenz. Wenn bei einem Prozess echte Personen Dokumente abzeichnen, ist die Latenz eines Computers vernachlässigbar. Auch wenn die Leistung beeinträchtigt ist, bieten objektorientierte Sprachen dem Entwickler bessere Grundelemente für die Modellierung der Geschäftsdomäne. Und ganz am Ende ermöglichen Systeme wie Flowable und Camunda den Führungskräften und Betriebsmitarbeitern, die Geschäftslogik flexibler und mit geringeren Eintrittsbarrieren zu programmieren. Sprachen wie Zig werden dabei nicht helfen, sondern stehen Ihnen nur im Weg.

A performant and extensible Web Server with Zig and Python

Python hingegen ist eine der dynamischsten Sprachen, die es gibt. Klassen, Objekte – sie alle sind unter der Haube Wörterbücher und können zur Laufzeit nach Belieben manipuliert werden. Dies führt zu Leistungseinbußen, macht die Modellierung des Geschäfts mit Klassen und Objekten und vielen cleveren Tricks jedoch praktisch. Zig ist das Gegenteil davon – es gibt bei Zig absichtlich wenige clevere Tricks, um Ihnen maximale Kontrolle zu geben. Können wir ihre Kräfte bündeln, indem wir sie zusammenarbeiten lassen?

Das können wir tatsächlich, da beide das C ABI unterstützen. Wir können den Python-Interpreter innerhalb des Zig-Prozesses und nicht als separaten Prozess ausführen lassen, wodurch der Overhead bei den Laufzeitkosten und dem Glue-Code reduziert wird. Dies ermöglicht es uns außerdem, die benutzerdefinierten Allokatoren von Zig in Python zu nutzen – wodurch ein Bereich für die Verarbeitung der einzelnen Anforderungen geschaffen wird, wodurch der Overhead eines Garbage Collectors reduziert oder gar eliminiert wird und eine Speicherobergrenze festgelegt wird. Eine große Einschränkung wäre, dass die CPython-Laufzeit Threads für Garbage Collection und IO erzeugt, aber ich habe keine Beweise dafür gefunden, dass dies der Fall ist. Wir könnten Python in eine benutzerdefinierte Ereignisschleife in Zig mit Speicherverfolgung pro Coroutine einbinden, indem wir das Feld „Kontext“ in AbstractMemoryLoop verwenden. Die Möglichkeiten sind grenzenlos.

Abschluss

Wir haben die Vorzüge von Parallelität, Parallelität und verschiedenen Formen der Integration mit dem Betriebssystemkernel besprochen. Der Erkundung mangelt es an Benchmarks und Code, was hoffentlich durch die Qualität der angebotenen Ideen wettgemacht wird. Haben Sie etwas Ähnliches versucht? Was denken Sie? Feedback willkommen :)

Weiterführende Literatur

  • 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-and-memory-overheads-for-linux-threads/
  • https://hadar.gr/2017/lightweight-goroutines

Das obige ist der detaillierte Inhalt vonEin leistungsstarker und erweiterbarer Webserver mit Zig und Python. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Neueste Artikel des Autors
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage