저는 소프트웨어 개발, 특히 타협을 최소화하면서 가장 광범위한 문제를 해결하는 소프트웨어 시스템을 인체공학적으로 만드는 퍼즐에 관심이 많습니다. 나는 또한 나 자신을 시스템 개발자라고 생각하고 싶습니다. Andrew Kelley의 정의에 따르면 이는 작업 중인 시스템을 완전히 이해하는 데 관심이 있는 개발자를 의미합니다. 이 블로그에서는 안정적이고 성능이 뛰어난 풀스택 엔터프라이즈 애플리케이션 구축이라는 문제를 해결하기 위한 제 아이디어를 여러분과 공유합니다. 꽤 어려운 일이지 않습니까? 블로그에서는 "성능이 뛰어난 웹 서버" 부분에 초점을 맞췄습니다. 나머지 부분은 잘 다루어졌거나 추가할 내용이 없기 때문에 이 부분에서 새로운 관점을 제공할 수 있다고 생각합니다.
주요 주의 사항 - 코드 샘플이 없습니다. 실제로 테스트해 본 적은 없습니다. 예, 이것은 주요 결함이지만 실제로 이것을 구현하는 데는 시간이 많이 걸릴 것입니다. 그럴 필요가 없으며 결함이 있는 블로그를 게시하고 전혀 게시하지 않는 사이에 전자를 고수했습니다. 경고를 받았습니다.
그리고 어떤 부분으로 애플리케이션을 조립할까요?
도구가 결정되었으니 시작해 보세요!
Zig는 코루틴을 언어 수준으로 지원하지 않습니다. :( 그리고 코루틴은 모든 성능이 뛰어난 웹 서버가 구축하는 데 사용됩니다. 그렇다면 노력할 필요가 없나요?
잠깐만, 먼저 시스템 프로그래머 모자를 쓰자. 코루틴은 만병통치약이 아니며, 아무것도 아닙니다. 관련된 실제 이점과 단점은 무엇입니까?
코루틴(사용자 공간 스레드)이 더 가볍고 빠르다는 것은 상식입니다. 그런데 정확히 어떤 방식으로요? (여기에 나온 답변은 대부분 추측이므로, 직접 테스트해 보세요.)
예를 들어 Go 런타임은 고루틴을 OS 스레드에 다중화합니다. 스레드는 페이지 테이블과 프로세스가 소유한 다른 리소스를 공유합니다. CPU 격리 및 선호도를 혼합에 도입하면 스레드는 해당 CPU 코어에서 지속적으로 실행되고 모든 OS 데이터 구조는 교체할 필요 없이 메모리에 유지되며 사용자 공간 스케줄러는 CPU 시간을 고루틴에 할당합니다. 협동적 멀티태스킹 모델을 사용하기 때문입니다. 경쟁도 가능한가요?
성능 향상은 스레드의 OS 수준 추상화를 무시하고 이를 고루틴의 추상화로 대체함으로써 달성됩니다. 그런데 번역에서 빠진 부분은 없나요?
독립적인 실행 단위에 대한 "진정한" OS 수준 추상화는 스레드가 아니라 실제로 OS 프로세스라고 주장하겠습니다. 실제로 여기서 구별은 그다지 명확하지 않습니다. 스레드와 프로세스를 구별하는 것은 서로 다른 PID 및 TID 값뿐입니다. 파일 설명자, 가상 메모리, 신호 처리기, 추적된 리소스의 경우 이러한 항목이 자식에 대해 별도인지 여부는 "clone" syscall에 대한 인수에 지정됩니다. 따라서 "프로세스"라는 용어를 사용하여 자체 시스템 리소스(주로 CPU 시간, 메모리, 열린 파일 설명자)를 소유하는 실행 스레드를 의미합니다.
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?
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.
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?
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.
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.
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.
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.
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 :)
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!