Zig 및 Python을 사용하는 성능이 뛰어나고 확장 가능한 웹 서버

Linda Hamilton
풀어 주다: 2024-10-07 06:12:01
원래의
620명이 탐색했습니다.

머리말

저는 소프트웨어 개발, 특히 타협을 최소화하면서 가장 광범위한 문제를 해결하는 소프트웨어 시스템을 인체공학적으로 만드는 퍼즐에 관심이 많습니다. 나는 또한 나 자신을 시스템 개발자라고 생각하고 싶습니다. 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

제한 사항은 SCHED_DEADLINE 스레드가 포크될 수 없다는 것입니다. 이로 인해 동시성을 위한 두 가지 모델이 남게 됩니다. 하나는 자체적으로 기한을 설정하고 효율적인 IO를 위해 이벤트 루프를 실행하는 요청당 프로세스이거나 처음부터 각 마이크로 작업에 대해 스레드를 생성하는 프로세스입니다. 자체 기한을 설정하고 서로 통신하기 위해 대기열을 사용합니다. 전자는 더 간단하지만 사용자 공간에 이벤트 루프가 필요하고 후자는 커널을 더 많이 사용합니다.

두 전략 모두 코루틴 모델과 동일한 목적을 달성합니다. 커널과 협력하여 최소한의 중단으로 애플리케이션 작업을 실행할 수 있습니다.

내장된 스크립팅 언어로서의 Python

이것은 모두 Zig가 빛나는 고성능, 짧은 지연 시간, 낮은 수준의 측면을 위한 것입니다. 그러나 애플리케이션의 실제 비즈니스에 있어서는 유연성이 대기 시간보다 훨씬 더 중요합니다. 프로세스에 실제 사람이 문서에 서명하는 과정이 포함된 경우 컴퓨터의 대기 시간은 무시할 수 있습니다. 또한 성능 저하에도 불구하고 객체 지향 언어는 개발자에게 비즈니스 영역을 모델링할 수 있는 더 나은 기본 요소를 제공합니다. 그리고 가장 먼 곳에서는 Flowable 및 Camunda와 같은 시스템을 통해 관리 및 운영 직원이 더 유연하고 낮은 진입 장벽으로 비즈니스 로직을 프로그래밍할 수 있습니다. Zig와 같은 언어는 이에 도움이 되지 않으며 방해만 할 뿐입니다.

A performant and extensible Web Server with Zig and Python

반면에 Python은 가장 역동적인 언어 중 하나입니다. 클래스, 객체 - 모두 내부적으로는 사전이며 런타임에 원하는 대로 조작할 수 있습니다. 이는 성능 저하를 가져오지만 클래스와 개체 및 많은 기발한 트릭을 사용하여 비즈니스를 모델링하는 데 실용적입니다. Zig는 그 반대입니다. Zig에는 의도적으로 영리한 트릭이 거의 없으므로 최대한의 제어가 가능합니다. 상호 운용을 통해 이들의 힘을 합칠 수 있을까요?

실제로 둘 다 C ABI를 지원하므로 가능합니다. 별도의 프로세스가 아닌 Zig 프로세스 내에서 Python 인터프리터를 실행하여 런타임 비용과 글루 코드의 오버헤드를 줄일 수 있습니다. 이를 통해 Python 내에서 Zig의 사용자 지정 할당자를 사용할 수 있습니다. 개별 요청을 처리하기 위한 영역을 설정하여 가비지 수집기의 오버헤드를 제거하지 않더라도 줄이고 메모리 캡을 설정할 수 있습니다. 주요 제한 사항은 가비지 수집 및 IO를 위한 스레드를 생성하는 CPython 런타임이지만, 그런 증거는 찾지 못했습니다. AbstractMemoryLoop의 "컨텍스트" 필드를 사용하여 코루틴별 메모리 추적을 통해 Python을 Zig의 사용자 정의 이벤트 루프에 연결할 수 있습니다. 가능성은 무한합니다.

결론

동시성, 병렬성, OS 커널과의 다양한 통합 형태의 장점에 대해 논의했습니다. 탐색에는 벤치마크와 코드가 부족하므로 제공되는 아이디어의 품질이 이를 보완하기를 바랍니다. 비슷한 것을 시도해 보셨나요? 당신의 생각은 무엇입니까? 피드백 환영합니다 :)

추가 읽기

  • 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

위 내용은 Zig 및 Python을 사용하는 성능이 뛰어나고 확장 가능한 웹 서버의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:dev.to
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
저자별 최신 기사
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿