Elixier als Entwicklungsziel für Python Async

In dem Buch „Python. Zu den Höhen der Exzellenz “Luciano Ramallo beschreibt eine Geschichte. Im Jahr 2000 nahm Luciano an Kursen teil, und einmal sah Guido van Rossum das Publikum an. Sobald ein solches Ereignis auftauchte, begannen alle, ihm Fragen zu stellen. Auf die Frage, welche Funktionen Python von anderen Sprachen ausgeliehen hat, antwortete Guido: "Alles, was in Python gut ist, wird von anderen Sprachen gestohlen."

Das ist tatsächlich so. Python hat lange im Kontext anderer Programmiersprachen gelebt und nimmt Konzepte aus seiner Umgebung auf: Asyncio wird entlehnt, dank Lisp erschienen Lambda-Ausdrücke, und Tornado wurde aus libevent kopiert. Aber wenn jemand Ideen ausleihen sollte, dann ist es Erlang. Es wurde vor 30 Jahren erstellt, und alle Konzepte in Python, die derzeit implementiert werden oder nur skizziert sind, funktionieren seit langem in Erlang: Multi-Core-Nachrichten als Grundlage für Kommunikation, Methodenaufrufe und Selbstbeobachtung in einem Live-Produktionssystem. Diese Ideen finden in der einen oder anderen Form ihren Ausdruck in Systemen wie Seastar.io .


Wenn Sie Data Science nicht berücksichtigen, bei dem Python nun außer Konkurrenz ist, ist alles andere bereits in Erlang implementiert: Arbeiten mit einem Netzwerk, Umgang mit HTTP- und Web-Sockets, Arbeiten mit Datenbanken. Daher ist es für Python-Entwickler wichtig zu verstehen, wo sich die Sprache bewegen wird: auf einer Straße, die bereits vor 30 Jahren vergangen ist.

Um die Geschichte der Entwicklung anderer Sprachen zu verstehen und den Fortschritt zu verstehen, haben wir Maxim Lapshin ( erlyvideo ), den Autor des Projekts Erlyvideo.ru, zu Moscow Python Conf ++ eingeladen.

Unter dem Kürzel steht die Textversion dieses Berichts, nämlich: In welche Richtung muss sich das System entwickeln, das weiterhin vom einfachen linearen Code zu libevent und darüber hinaus migriert, was häufig vorkommt und was die Unterschiede zwischen Elixir und Python sind. Besonderes Augenmerk legen wir auf die Verwaltung von Sockets, Threads und Daten in verschiedenen Programmiersprachen und Plattformen.


Erlyvideo.ru verfügt über ein Videoüberwachungssystem, bei dem die Zugriffskontrolle für Kameras in Python geschrieben ist. Dies ist eine klassische Aufgabe für diese Sprache. Es gibt Benutzer und Kameras, Videos, von denen sie sich ansehen können: Einer sieht einige Kameras, während andere eine reguläre Site sehen.

Python wurde gewählt, weil es praktisch ist, einen solchen Dienst darauf zu schreiben: Es gibt schließlich Frameworks, ORMs und Programmierer. Die entwickelte Software wird verpackt und an Benutzer verkauft. Erlyvideo.ru ist ein Unternehmen, das Software verkauft und nicht nur Dienstleistungen anbietet.

Welche Probleme mit Python möchte ich lösen.

Warum gibt es solche Probleme mit Multicore? Wir ließen Flussonic auf Stadiencomputern laufen, noch bevor Intel dies tat. Aber Python hat Schwierigkeiten damit: Warum werden immer noch nicht alle 80 Kerne unserer Server verwendet, um zu funktionieren?

Wie kann man nicht unter offenen Steckdosen leiden? Die Überwachung der Anzahl offener Steckdosen ist ein großes Problem. Wenn es den Grenzwert erreicht, schließen und verhindern Sie auch Leckagen.

Haben vergessene globale Variablen eine Lösung? Das Durchsickern globaler Variablen ist die Hölle für jede Garbage Collection-Sprache wie Java oder C #.

Wie kann man Eisen verwenden, ohne Ressourcen zu verschwenden? Wie kann man ohne 40 Jung-Arbeiter und 64 GB RAM auskommen, wenn man Server effizient nutzen und nicht hunderttausende Dollar im Monat auf unnötige Hardware werfen will?

Warum Multicore benötigt wird


Damit alle Kerne voll ausgelastet sind, werden viel mehr Arbeitskräfte benötigt als Kerne. Zum Beispiel werden für 40 Prozessorkerne 100 Arbeiter benötigt: Ein Arbeiter ging zur Datenbank, der andere ist mit etwas anderem beschäftigt.

Ein Arbeiter kann 300-400 MB verbrauchen . Wir schreiben dies immer noch in Python und nicht in Ruby on Rails, das ein Vielfaches an RAM verbrauchen kann, und 40 GB werden schnell und einfach verschwendet. Es ist nicht sehr teuer, aber warum Speicher kaufen, wo Sie nicht kaufen können.

Multi-Core hilft dabei, gemeinsam genutzte Daten zu fummeln und den Speicherverbrauch zu reduzieren . So können viele unabhängige Prozesse bequem und sicher ausgeführt werden. Es ist viel einfacher zu programmieren, aber teurer aus dem Speicher.

Socket-Verwaltung


Auf dem Web Socket werden die Laufzeitdaten der Kameras aus dem Backend abgefragt. Die Python-Software verbindet sich mit Flussonic und fragt die Statusdaten der Kameras ab: Ob sie funktionieren oder nicht, gibt es neue Ereignisse.

Auf der anderen Seite stellt der Client eine Verbindung her und wir senden diese Daten über den Web-Socket an den Browser. Wir möchten Kundendaten in Echtzeit übertragen: Die Kamera wurde ein- und ausgeschaltet, die Katze aß, schlief, riss ein Sofa auf, drückte den Knopf und vertrieb die Katze.

Aber zum Beispiel ist ein Problem aufgetreten: Die Datenbank hat nicht auf die Anfrage geantwortet, der gesamte Code ist ausgefallen, es waren zwei Sockets offen. Wir haben angefangen nachzuladen, haben etwas gemacht, wieder dieses Problem - es gab zwei Steckdosen. Der DB-Fehler wurde falsch verarbeitet und zwei offene Verbindungen hingen. Dies führt im Laufe der Zeit zu Steckdosenlecks.

Vergessene globale Variablen


Erstellte ein globales Diktat für die Liste der Browser, die über den Web-Socket verbunden sind. Eine Person meldet sich bei der Site an, wir öffnen für sie einen Web-Socket. Dann setzen wir den Web-Socket mit seiner Kennung in eine Art globales Diktat und es stellt sich heraus, dass eine Art Fehler auftritt.

Zum Beispiel haben sie eine Verbindungsverknüpfung in dikt aufgezeichnet, um Daten zu senden. Eine Ausnahme funktionierte, ich vergaß den Link zu löschen und die Daten hingen . Nach einiger Zeit werden also allmählich 64 GB vermisst, und ich möchte den Arbeitsspeicher auf dem Server verdoppeln. Dies ist keine Lösung, da die Daten ohnehin verloren gehen.
Wir machen immer Fehler - wir sind Menschen und können nicht alles im Auge behalten.
Die Frage ist, dass einige Fehler auftreten, auch solche, die wir nicht erwartet hatten.

Historischer Ausflug


Um zum Hauptthema zu gelangen, wollen wir uns mit der Geschichte befassen. Über alles, worüber wir jetzt über Python, Go und Erlang sprechen, sind andere Leute vor ungefähr 30 Jahren diesen ganzen Weg gegangen. Wir in Python gehen einen langen Weg und füllen die Unebenheiten auf, die bereits vor Jahrzehnten passiert sind. Der Weg wiederholt sich auf erstaunliche Weise.

Dos


Wenden wir uns zunächst DOS zu, es ist am nächsten. Vor ihm gab es ganz andere Dinge und nicht jeder ist am Leben, der sich an Computer vor DOS erinnert.

Das DOS-Programm besetzte (fast) ausschließlich den Computer . Während beispielsweise ein Spiel läuft, wird nichts anderes ausgeführt. Sie werden nicht ins Internet gehen - es ist noch nicht da und Sie werden nicht einmal weiterkommen. Es war traurig, aber die Erinnerungen daran sind warm, weil es mit der Jugend verbunden ist.

Kooperatives Multitasking


Da DOS sehr schmerzhaft war, tauchten neue Herausforderungen auf und Computer wurden leistungsfähiger. Vor Jahrzehnten entwickelten sie das Konzept des kooperativen Multitasking , noch vor Windows 3.11.

Daten werden durch Prozesse getrennt und jeder Prozess wird separat ausgeführt: Sie sind irgendwie voneinander geschützt. Fehlerhafter Code in einem Prozess kann den Code im Browser nicht verderben (dann sind die ersten Browser bereits erschienen).

Die nächste Frage lautet: Wie wird die Rechenzeit auf verschiedene Prozesse verteilt? Dann war es nicht so, dass es nicht mehr als einen Kern gab, ein Dual-Prozessor-System war eine Seltenheit. Das Schema lautete wie folgt: Während ein Prozess beispielsweise auf eine Festplatte für Daten ging, erhält der zweite Prozess die Steuerung vom Betriebssystem. Der erste wird in der Lage sein, die Kontrolle zu erlangen, wenn der zweite freiwillig selbst gibt. Ich vereinfache die Situation sehr, aber der Prozess erlaubte es irgendwie freiwillig, ihn vom Prozessor zu entfernen .

Präventives Multitasking


Kooperatives Multitasking führte zu folgendem Problem: Der Prozess konnte einfach hängen bleiben, weil er schlecht geschrieben war. Wenn die Verarbeitung des Prozessors lange dauert, blockiert er den Rest . In diesem Fall stürzte der Computer ab und es konnte nichts getan werden, zum Beispiel das Fenster zu wechseln.

In Reaktion auf dieses Problem wurde präemptives Multitasking erfunden. Das Betriebssystem selbst steuert jetzt streng: Entfernt Prozesse aus der Ausführung, trennt ihre Daten vollständig, schützt den Prozessspeicher voneinander und gibt jedem eine gewisse Rechenzeit. Das Betriebssystem weist jedem Prozess die gleichen Zeitintervalle zu .

Das Thema Zeitplanung ist noch offen. Noch heute überlegen sich die OS-Entwickler, was in welcher Reihenfolge, an wen und wie viel Zeit sie für das Management geben sollen. Heute sehen wir die Entwicklung dieser Ideen.

Streams


Das war aber nicht genug. Prozesse müssen Daten austauschen: Über das Netzwerk ist teuer, irgendwie immer noch kompliziert. Daher wurde das Konzept der Strömungen erfunden.
Threads sind einfache Prozesse, die einen gemeinsamen Speicher haben.
Streams wurden mit der Hoffnung erstellt, dass alles leicht, einfach und unterhaltsam sein wird. Jetzt wird Multithread-Programmierung als Antipattern betrachtet . Wenn die Geschäftslogik in Threads geschrieben ist, sollte dieser Code höchstwahrscheinlich weggeworfen werden, da er wahrscheinlich Fehler enthält. Wenn Sie den Eindruck haben, dass keine Fehler vorliegen, haben Sie sie einfach noch nicht gefunden.

Multithread-Programmierung ist eine äußerst komplexe Sache. Es gibt nur wenige Leute, die sich wirklich der Fähigkeit verschrieben haben, über Themen zu schreiben, und die etwas wirklich zum Laufen bringen.

In der Zwischenzeit erschienen Mehrkerncomputer . Sie brachten schreckliche Dinge mit. Die Herangehensweise an die Daten war völlig anders. Es stellten sich Fragen nach der Lokalität der Daten. Jetzt müssen Sie verstehen, von welchem ​​Kernel aus Sie zu welchen Daten wechseln.

Ein Kern muss die Daten hier und der andere dort ablegen und diese Dinge auf keinen Fall verwirren, da Cluster tatsächlich im Computer vorhanden sind. In einem modernen Computer befindet sich ein Cluster, wenn ein Teil des Speichers mit einem Kern und der andere mit einem anderen Kern verlötet ist. Die Laufzeit zwischen diesen Daten kann um Größenordnungen variieren.

Python-Beispiele


Betrachten Sie ein einfaches Beispiel für „Service, der dem Käufer hilft. Er wählt auf mehreren Plattformen den besten Preis für die Ware aus: Wir fahren im Namen der Ware und suchen Handelsplätze mit einem Mindestpreis.

Dies ist der Code im alten Django, Python 2. Heute ist er nicht sehr beliebt, nur wenige Leute starten Projekte darauf.

@api_view(['GET']) def best_price(request): name = request.GET['name'] price1 = http_fetch_price('market.yandex.ru', name) price2 = http_fetch_price('ebay.com', name) price3 = http_fetch_price('taobao.com', name) return Response(min([price1,price2,price3])) 

Wenn eine Anfrage eingeht, gehen wir zu einem Backend und dann zu einem anderen. An Stellen, an denen http_fetch_price , werden Threads blockiert. In diesem Moment begibt sich der ganze Arbeiter auf eine Reise zu Yandex.Market, dann zu eBay, dann bis zu einem Timeout auf Taobao und gibt am Ende eine Antwort. Die ganze Zeit steht der ganze Arbeiter .

Es ist sehr schwierig, mehrere Backends gleichzeitig abzufragen. Dies ist eine schlimme Situation: Es wird Speicher verbraucht, eine große Anzahl von Mitarbeitern muss gestartet und der gesamte Dienst überwacht werden. Es ist notwendig zu prüfen, wie häufig solche Anfragen sind, müssen Sie noch Mitarbeiter entlassen oder gibt es noch zusätzliche. Das sind genau die Probleme, von denen ich gesprochen habe. Es müssen nacheinander mehrere Backends abgefragt werden .

Was sehen wir in Python? Ein Prozess pro Task, in Python gibt es noch keinen Multicore. Die Situation ist klar: In Sprachen dieser Klasse ist es schwierig, ein sicheres einfaches Multicore zu erstellen, da es die Leistung beeinträchtigt .

Wenn Sie von verschiedenen Threads zum Diktat wechseln, kann der Zugriff auf die Daten folgendermaßen geschrieben werden: Kleben Sie zwei Python-Instanzen in den Speicher, damit sie die Daten durchsuchen - sie brechen sie einfach. Um zum Beispiel zu diktieren und nichts zu zerbrechen, müssen Sie Mutexe davor setzen. Wenn es vor jedem Diktat einen Mutex gibt, wird das System ungefähr 1000-mal langsamer - es ist einfach unpraktisch. Es ist schwierig, es in einen Multicore zu ziehen.

Wir haben nur einen Thread der Ausführung und nur Prozesse können skalieren . Tatsächlich haben wir DOS innerhalb des Prozesses neu erfunden - die Skriptsprache von 2010. Innerhalb des Prozesses gibt es etwas, das DOS ähnelt: Während wir etwas tun, funktionieren alle anderen Prozesse nicht. Niemand mochte die enormen Kostenüberschreitungen und die langsame Reaktion.

Sockelreaktoren sind vor einiger Zeit in Python erschienen, obwohl das Konzept selbst schon vor langer Zeit geboren wurde. Jetzt können Sie die Bereitschaft mehrerer Steckdosen gleichzeitig erwarten.

Zunächst wurde der Reaktor auf Servern wie Nginx gefragt. Auch durch den richtigen Einsatz dieser Technologie ist sie populär geworden. Dann kroch das Konzept in Skriptsprachen wie Python und Ruby.
Die Idee des Reaktors ist, dass wir zur ereignisorientierten Programmierung übergegangen sind.

Ereignisorientierte Programmierung


Ein Ausführungskontext erzeugt eine Anfrage. Während auf eine Antwort gewartet wird, wird ein anderer Kontext ausgeführt. Es ist bemerkenswert, dass wir fast dieselbe Entwicklungsstufe durchlaufen haben wie der Übergang von DOS zu Windows 3.11. Nur Menschen taten dies vor 20 Jahren und in Python und Ruby erschien es vor 10 Jahren.

Verdreht


Dies ist ein ereignisgesteuertes Netzwerk-Framework. Es erschien im Jahr 2002 und ist in Python geschrieben. Ich nahm das obige Beispiel und schrieb es auf Twisted um.

 def render_GET(self, request): price1 = deferred_fetch_price('market.yandex.ru', name) price2 = deferred_fetch_price('ebay.com', name) price3 = deferred_fetch_price('taobao.com', name) dl = defer.DeferredList([price1,price2,price3]) def reply(prices): request.write('%d'.format(min(prices))) request.finish() dl.addCallback(reply) return server.NOT_DONE_YET 

Es kann zu Fehlern und Ungenauigkeiten kommen, und die notorische Fehlerbehandlung reicht nicht aus. Das ungefähre Schema lautet jedoch wie folgt: Wir stellen keine Anfrage, bitten Sie jedoch, diese Anfrage zu einem späteren Zeitpunkt zu bearbeiten, wenn noch Zeit ist. In der Zeile mit defer.DeferredList wollen wir die Antworten aus mehreren Abfragen zusammenfassen.

Tatsächlich besteht der Code aus zwei Teilen. Im ersten Teil, was vor der Anfrage passiert ist, und im zweiten, was danach war.
Die gesamte Geschichte der ereignisorientierten Programmierung ist gesättigt von dem Schmerz, den linearen Code „vor der Anforderung“ und „nach der Anforderung“ zu brechen.
Dies tut weh, weil die Codeteile gemischt sind: Die letzten Zeilen werden noch in der ursprünglichen Anforderung ausgeführt, und die reply wird aufgerufen.

Es ist nicht leicht, genau zu bedenken, weil wir den linearen Code gebrochen haben, aber es musste getan werden. Ohne ins Detail zu gehen, wird der Code, der von Django zu Twisted umgeschrieben wurde , eine unglaubliche Pseudobeschleunigung erzeugen .

Idee verdreht

Ein Objekt kann aktiviert werden, wenn der Socket bereit ist.
Wir nehmen Objekte, in denen wir die notwendigen Daten sammeln, aus dem Kontext und binden deren Aktivierung an den Socket. Die Verfügbarkeit von Sockets ist jetzt eine der wichtigsten Kontrollen für das gesamte System. Objekte werden unsere Kontexte sein.

Gleichzeitig trennt die Sprache immer noch den Begriff des Ausführungskontexts, in dem Ausnahmen leben. Der Ausführungskontext lebt getrennt von Objekten und ist lose mit diesen verbunden . Hier ergibt sich das Problem, dass wir versuchen, Daten in Objekten zu sammeln: Es gibt keinen Weg ohne sie, aber die Sprache unterstützt sie nicht.

All dies führt zu einer klassischen Callback-Hölle. Wofür sie beispielsweise Node.js lieben - bis vor kurzem gab es überhaupt keine anderen Methoden, aber es erschien immer noch in Python. Das Problem besteht darin, dass an den Punkten der externen E / A Code-Unterbrechungen auftreten , die zu einem Rückruf führen.

Es gibt viele Fragen. Ist es möglich, die Kanten der Lücke im Code zu "kleben"? Ist es möglich, zum normalen menschlichen Code zurückzukehren? Was tun, wenn ein logisches Objekt mit zwei Sockets arbeitet und einer davon geschlossen ist? Wie man nicht vergisst, die Sekunde zu schließen? Kann man irgendwie alle Kerne nutzen?

Async io


Eine gute Antwort auf diese Fragen ist Async IO. Dies ist ein steiler, wenn auch kein einfacher Schritt nach vorne. Async IO ist eine komplizierte Sache, unter deren Haube es viele schmerzhafte Nuancen gibt.

 async def best_price(request): name = request.GET['name'] price1 = async_http_fetch_price('market.yandex.ru', name) price2 = async_http_fetch_price('ebay.com', name) price3 = async_http_fetch_price('taobao.com', name) prices = await asyncio.wait([price1,price2,price3]) return min(prices) 

Die Codelücke wird unter der Syntax async/await ausgeblendet. Wir haben alles genommen, was vorher war, sind aber in diesem Code nicht zum Netzwerk gegangen. Wir haben Callback(reply) aus dem vorherigen Beispiel entfernt und es hinter await versteckt - der Stelle, an der der Code mit einer Schere abgeschnitten wird. Es wird in zwei Teile unterteilt: den aufrufenden Teil und den Rückrufteil, der die Ergebnisse verarbeitet.

Dies ist ein großartiger syntaktischer Zucker . Es gibt Methoden, um mehrere Erwartungen in eine zu bringen. Das ist cool, aber es gibt eine Nuance: Alles kann durch eine "klassische" Steckdose zerbrochen werden . In Python gibt es immer noch eine große Anzahl von Bibliotheken, die synchron zum Socket gehen, eine timer library und alles für Sie ruinieren. Wie das zu debuggen ist, weiß ich nicht.

Aber Asyncio hilft nicht bei Lecks und Multicore . Daher gibt es keine grundlegenden Änderungen, obwohl es besser geworden ist.

Wir haben immer noch alle Probleme, über die wir am Anfang gesprochen haben:

  • leicht mit Steckdosen zu lecken;
  • leicht zu belassende Links in globalen Variablen;
  • sehr sorgfältige Fehlerbehandlung;
  • Es ist immer noch schwer, Multi-Core zu machen.

Was zu tun ist


Ob sich dies alles entwickeln wird, weiß ich nicht, aber ich werde die Implementierung in anderen Sprachen und Plattformen zeigen.

Isolierte Ausführungskontexte. In Ausführungskontexten werden Ergebnisse akkumuliert, Sockets gespeichert: logische Objekte, in denen normalerweise alle Daten zu Rückrufen und Sockets gespeichert werden. Ein Konzept: Nehmen Sie Ausführungskontexte, kleben Sie sie an Ausführungsfäden und isolieren Sie sie vollständig voneinander.

Paradigmenwechsel von Objekten. Verbinden wir den Kontext mit dem Thread der Ausführung. Es gibt Analoga, das ist nichts Neues. Wenn jemand versucht hat, den Apache-Quellcode zu bearbeiten und Module darauf zu schreiben, weiß er, dass es einen Apache-Pool gibt. Keine Links zwischen Apache-Pools erlaubt . Daten aus einem Apache-Pool - der mit Anforderungen verknüpfte Pool befindet sich darin, und Sie können nichts daraus abrufen.

Theoretisch ist es möglich, aber wenn Sie dies tun, wird entweder jemand schimpfen, oder er wird den Patch nicht akzeptieren, oder er wird ein langes und schmerzhaftes Debugging in der Produktion durchführen. Danach wird niemand mehr das tun und es anderen erlauben, solche Dinge zu tun. Es ist einfach nicht mehr möglich, auf Daten zwischen Kontexten zu verweisen, eine vollständige Isolierung ist erforderlich.

Wie tausche ich meine Aktivitäten aus? Was benötigt wird, sind keine kleinen Monaden, die in sich geschlossen sind und nicht miteinander kommunizieren. Wir brauchen sie, um zu kommunizieren. Ein Ansatz ist das Versenden von Nachrichten. Dies ist ungefähr der Weg, den Windows beim Austausch von Nachrichten zwischen Prozessen eingeschlagen hat. Unter normalen Betriebssystemen können Sie keine Verknüpfung zum Speicher eines anderen Prozesses herstellen, aber Sie können wie unter UNIX über das Netzwerk oder wie unter Windows über Nachrichten signalisieren.

Alle Ressourcen innerhalb des Prozesses und des Kontexts werden zu einem Thread der Ausführung . Wir haben zusammengeklebt:

  • Laufzeitdaten in einer virtuellen Maschine, in der Ausnahmen auftreten;
  • der Thread der Ausführung, wie das, was auf dem Prozessor ausgeführt wird;
  • Ein Objekt, in dem alle Daten logisch gesammelt werden.

Herzlichen Glückwunsch - wir haben UNIX in einer Programmiersprache erfunden! Diese Idee wurde um 1969 erfunden. Bisher ist es noch nicht in Python, aber Python wird wahrscheinlich dazu kommen. Und vielleicht kommt sie nicht - ich weiß es nicht.

Was gibt es


Zuallererst die automatische Kontrolle über Ressourcen . Bei Moscow Python Conf ++ 2019 wurde gesagt, dass Sie ein Programm auf Go schreiben und alle Fehler verarbeiten können. Das Programm wird wie angegossen aussehen und monatelang funktionieren. Dies ist wahr, aber wir behandeln nicht alle Fehler.

Wir sind lebendige Menschen, wir haben immer Fristen, den Wunsch, etwas Nützliches zu tun und den 535. Fehler für heute nicht zu bewältigen. Code, der mit Fehlerbehandlung übersät ist, ruft bei niemandem ein warmes Gefühl hervor.

Deshalb schreiben wir alle „happy path“, und dann werden wir es bei der Produktion herausfinden. Seien wir ehrlich: Nur wenn Sie etwas verarbeiten müssen, beginnen wir mit der Verarbeitung. Defensive Programmierung ist etwas anders und keine kommerzielle Entwicklung.

Wenn wir also eine automatische Fehlerüberwachung haben, ist dies in Ordnung . Aber die Betriebssysteme haben es vor 50 Jahren erfunden: Wenn ein Prozess abstirbt, wird alles, was er öffnet, automatisch geschlossen. Heutzutage muss niemand mehr Code schreiben, der die Dateien hinter dem getöteten Prozess bereinigt. Dies gibt es in keinem Betriebssystem seit 50 Jahren, aber in Python müssen Sie dies immer noch sorgfältig und sorgfältig mit Ihren Händen verfolgen. Es ist seltsam.

Sie können Heavy Computing in einen anderen Kontext bringen , aber es kann bereits zu einem anderen Kern gehen. Wir haben die Daten geteilt, wir brauchen keine Mutexe mehr. Sie können die Daten in einem anderen Kontext senden und sagen: "Sie werden es irgendwo tun und mich dann darüber informieren, dass Sie fertig sind und etwas getan haben."

Eine asynchrone Implementierung ohne die Worte "async / await" . Weiter eine kleine Hilfe von der virtuellen Maschine, von der Laufzeit. Dies ist, worüber wir mit async/await : Sie können auch in Nachrichten konvertieren, async/await entfernen und auf der Ebene der virtuellen Maschine abrufen.

Erlang-Prozesse


Erlang wurde vor 30 Jahren erfunden. Die bärtigen Jungs, die damals nicht sehr bärtig waren, sahen sich UNIX an und übertrugen alle Konzepte in die Programmiersprache. Sie beschlossen, dass sie jetzt ihr eigenes Ding haben würden, um nachts zu schlafen und leise ohne Computer fischen zu gehen. Damals gab es noch keine Laptops, aber die Bärtigen wussten bereits, dass dies im Voraus überlegt werden sollte.

Wir haben Erlang (Elixier) - aktive Kontexte, die sich selbst ausführen . Weiter mein Beispiel zu Erlang. Auf Elixir sieht es mit einigen Variationen ungefähr gleich aus.

 best_price(Name) -> Price1 = spawn_price_fetcher('market.yandex.ru', Name), Price2 = spawn_price_fetcher('ebay.com', Name), Price3 = spawn_price_fetcher('taobao.com', Name), lists:min(wait4([Price1,Price2,Price3])). 

Wir starten mehrere Abholer - dies sind verschiedene neue Kontexte, auf die wir warten. Sie warteten, sammelten die Daten und gaben das Ergebnis als Mindestpreis zurück. All dies ist ähnlich wie async/await , jedoch ohne die Worte "async / await".

Eigenschaften von Elixir


Elixir befindet sich an der Basis von Erlang und alle Sprachkonzepte sind leise auf Elixir portiert. Was sind seine Merkmale?

Verbot von prozessorübergreifenden Links. Mit Prozess meine ich einen einfachen Prozess innerhalb einer virtuellen Maschine - Kontext. Vereinfacht gesagt, sind Datenverknüpfungen innerhalb eines anderen Objekts in Erlang verboten, wenn sie nach Python portiert wurden. Sie können eine Verknüpfung zum gesamten Objekt als geschlossenes Feld haben, aber Sie können nicht auf die darin enthaltenen Daten verweisen. Sie können nicht einmal syntaktisch einen Zeiger auf Daten abrufen, die sich in einem anderen Objekt befinden. Sie können nur über das Objekt selbst wissen.

Innerhalb von Prozessen (Objekten) gibt es keine Mutexe. Das ist wichtig - ich persönlich möchte nie in meinem Leben mit der Geschichte des Debuggens von Multithread-Flügen zur Produktion in Berührung kommen. Das wünsche ich niemandem.

Prozesse können sich um die Kerne bewegen, es ist sicher. Wir müssen nicht mehr wie in Java eine Reihe anderer pointer umgehen und neu schreiben, wenn wir Daten von einem Ort an einen anderen verschieben: Wir haben keine gemeinsamen Daten und internen Links. Woher kommt beispielsweise das Problem der Hüftschwäche? Aufgrund der Tatsache, dass jemand auf diese Daten verweist.

Wenn wir Daten innerhalb des Heaps zur Komprimierung an einen anderen Speicherort übertragen, müssen wir das gesamte System durchlaufen. Es kann Dutzende von Gigabyte belegen und alle Zeiger aktualisieren - das ist verrückt.

Volle Thread-Sicherheit , da die gesamte Kommunikation über Nachrichten erfolgt. Nach alledem kam es zu einem Verdrängungsprozess . Er hat es einfach und billig.

Nachrichten als Basis der Kommunikation. Innerhalb von Objekten, normalen Funktionsaufrufen und zwischen Nachrichtenobjekten. Das Eintreffen von Daten aus dem Netzwerk ist eine Nachricht, die Antwort eines anderen Objekts ist eine Nachricht, etwas anderes außerhalb ist ebenfalls eine Nachricht in einer eingehenden Warteschlange. Dies ist unter UNIX nicht möglich, da es keine Wurzel hat.

Methodenaufrufe. Wir haben Objekte, die wir Prozesse nennen. Methoden zu Prozessen werden über Nachrichten aufgerufen.

Beim Aufrufen von Methoden wird auch eine Nachricht gesendet. Es ist toll, dass es jetzt mit einer Auszeit getan werden kann. Wenn uns etwas langsam antwortet, rufen wir die Methode für ein anderes Objekt auf. Gleichzeitig sagen wir aber, dass wir bereit sind, nicht länger als 60 Sekunden zu warten, weil ich einen Kunden mit einem Timeout von 70 Sekunden habe. Ich muss ihm "503" sagen - komm morgen, jetzt warten sie nicht auf dich.

Darüber hinaus kann die Beantwortung des Anrufs verschoben werden . Innerhalb des Objekts können Sie die Aufforderung zum Aufrufen der Methode annehmen und sagen: "Ja, ja, ich werde Sie jetzt entlassen, kommen Sie in einer halben Stunde zurück, ich werde Ihnen antworten." Sie können nicht sprechen, aber still beiseite legen. Wir benutzen es manchmal.

Wie arbeite ich mit einem Netzwerk?


Sie können linearen Code, Rückrufe oder im Stil von asyncio.gather . Ein Beispiel, wie das aussehen wird.

 wait4([ ]) -> [ ]; wait4(List) -> receive {reply, Pid, Price} -> [Price] ++ wait4(List -- [Pid]) after 60000 -> [] end. 

In der Funktion wait4 aus dem vorherigen Beispiel wait4 wir die Liste der Personen, von denen wir noch auf Antworten warten. Wenn wir mit der receive eine Nachricht von diesem Prozess erhalten, schreiben wir sie in die Liste. Wenn die Liste vorbei ist, geben wir alles zurück, was war, und akkumulieren die Liste. Wir haben gleichzeitig drei Objekte gebeten, uns die Daten zu fahren. Wenn sie es nicht in 60 Sekunden zusammen geschafft haben und mindestens einer von ihnen nicht mit OK geantwortet hat, haben wir eine leere Liste. Es ist jedoch wichtig, dass wir eine allgemeine Zeitüberschreitung für eine sofortige Anfrage an eine ganze Reihe von Objekten festlegen.

Jemand könnte sagen: "Denken Sie, libcurl hat das gleiche." Hierbei ist es jedoch wichtig, dass es nicht nur eine HTTP-Auslösung, sondern auch eine DB-Auslösung sowie einige Berechnungen geben kann, beispielsweise die Berechnung einer optimalen Anzahl für den Client.

Fehlerbehandlung


Vom Stream sind Fehler an das Objekt übergeben worden, die jetzt ein und dasselbe sind . Jetzt wird der Fehler selbst nicht an den Thread, sondern an das Objekt angehängt, an dem er ausgeführt wurde.

Das ist viel logischer. Wenn wir alle möglichen kleinen Quadrate und Kreise an die Tafel zeichnen, in der Hoffnung, dass sie zum Leben erweckt werden und uns Ergebnisse und Geld bringen, zeichnen wir normalerweise Objekte, nicht die Flüsse, in denen diese Objekte ausgeführt werden. Beispielsweise können wir bei Lieferung eine automatische Nachricht über den Tod eines anderen Objekts erhalten .

Introspection oder Debugging in der Produktion


Was gibt es Schöneres, als zur Kasse zu gehen und zu belasten, besonders wenn der Fehler nur unter Last während der Stoßzeiten auftritt. Zur Hauptverkehrszeit sagen wir:

- Komm schon, ich werde jetzt neu starten!
- Gehen Sie aus der Tür und es gibt einen Neustart bei jemand anderem!

Hier können wir ein lebendes System betreten, das gerade läuft und nicht speziell darauf vorbereitet ist. Dazu müssen Sie es nicht mit dem Profiler neu starten, mit dem Debugger neu erstellen.

Ohne Performance-Einbußen in einem Live-Produktionssystem können wir uns eine Liste von Prozessen ansehen: Was ist in ihnen, wie funktioniert das alles? All dies ist kostenlos aus der Box.

Boni


Der Code ist super zuverlässig. Zum Beispiel ist Python anfällig für old vs async und wird es nicht weniger als fünf Jahre lang bleiben. Angesichts der Geschwindigkeit, mit der Python 3 implementiert wurde, sollten Sie nicht hoffen, dass es schnell sein wird.

Das Lesen und Verfolgen von Nachrichten ist einfacher als das Debuggen von Rückrufen . Es ist wichtig. Es scheint, als hätten wir immer noch Rückrufe für die Verarbeitung von Nachrichten, die wir sehen können. Was ist dann besser? Durch die Tatsache, dass Nachrichten ein Stück Daten im Speicher sind. Sie können es mit Augen sehen und verstehen, was hierher gekommen ist. Es kann zum Tracer hinzugefügt werden, um eine Liste der Nachrichten in einer Textdatei abzurufen. Dies ist bequemer als Rückrufe.

Wunderschönes Multi-Core- Speichermanagement und Introspection in einem Live- Produktionssystem.

Die Probleme


Natürlich hat Erlang auch Probleme.

Verlust der maximalen Leistung durch die Tatsache, dass wir uns nicht mehr auf Daten in einem anderen Prozess oder Objekt beziehen können. Wir müssen sie bewegen, aber das ist nicht kostenlos.

Der Aufwand für das Kopieren von Daten zwischen Prozessen. Wir können ein Programm in C schreiben, das auf allen 80 Kernen läuft und ein Datenarray verarbeitet, und wir gehen davon aus, dass es es korrekt und korrekt ausführt. In Erlang ist dies nicht möglich: Sie müssen die Daten sorgfältig ausschneiden, auf eine Reihe von Prozessen verteilen und alles im Auge behalten. Diese Kommunikation kostet Ressourcen - Prozessorzyklen.

Wie schnell oder langsam ist es? Wir schreiben seit 10 Jahren Erlang-Code. Der einzige Konkurrent, der diese 10 Jahre überlebt hat, ist in Java geschrieben. Mit ihm haben wir fast vollständige Leistungsgleichheit: Jemand sagt, dass wir schlechter sind, jemand, der sie sind. Aber sie haben Java mit all seinen Problemen, angefangen mit JIT.

Wir schreiben ein Programm, das Zehntausende von Sockets bedient und Dutzende von GB Daten durch sich selbst pumpt. Plötzlich stellt sich heraus, dass in diesem Fall die Richtigkeit der Algorithmen und die Fähigkeit, all dies in der Produktion zu debuggen, wichtiger ist als potenzielle Java-Buns . Milliarden von Dollar wurden investiert, aber dies bringt dem Java JIT keine magischen Vorteile.

Aber wenn wir dumme und sinnlose Benchmarks messen wollen, wie "Fibonacci-Zahlen berechnen", dann wird Erlang hier wahrscheinlich noch schlechter sein als Python oder vergleichbar.

Der Overhead der Nachrichtenzuordnung. Manchmal tut es weh. Zum Beispiel haben wir einige Teile in C im Code, und an diesen Stellen funktionierte es überhaupt nicht mit Erlang. , , .

Erlang , , . , , receive send receive . — , . , , .

Python


. . , Python - .

, . - Python, , 20 , 40.

, . - , , Elixir, , .

Moscow Python Conf++ . , 6 4 . , , ) ) . Call for Papers 13 , 27 .

Source: https://habr.com/ru/post/de476112/


All Articles