📱

Get Our Mobile App

Take your business learning on the go!

Download on the App StoreGet it on Google Play

Как FastAPI обрабатывает 1000+ запросов в секунду – Наглядный пример

Артём Шумейко21:21

Transcription

Wie schreibt man performanten Python-Code, der Hunderte oder sogar Tausende von Anfragen pro Sekunde verarbeiten kann? Genau auf diese Frage erhalten Sie in der heutigen Videoantwort. Ich zeige Ihnen anhand des beliebtesten und gefragtesten Frameworks Fast API, wie asynchroner Code und synchroner Code verarbeitet werden. Wie man diesen oder jenen Codeabschnitt skalieren kann. Und ich werde nicht nur oberflächlich darüber sprechen, sondern Ihnen direkt unter die Haube schauen lassen, wie asynchroner Code aufgebaut ist, wie Multithreading bei uns funktioniert und was wir dadurch gewinnen. Und wir werden unseren Server mit einer sehr, sehr großen Anzahl von Anfragen belasten. So können Sie diesen Code sogar nachvollziehen und sich davon überzeugen, wie leistungsfähig aktuelle asynchrone Frameworks sind. Freunde, bevor wir beginnen, informiere ich Sie, dass diese Lektion aus meinem praktischen Kurs zur Backend-Entwicklung in Python stammt, in dem Sie alle wichtigen Nuancen, Ansätze und Konzepte der Backend-Entwicklung lernen. Dies sind Architekturstile, dies sind Muster, dies ist das Testen von Code, CI/CD, Docker, Redis, Caching, Hintergrundaufgaben, asynchrone, synchrone. All dies verpacken wir in ein riesiges Projekt, an dem Sie alle Kenntnisse festigen. Und zusätzlich zu mehr als hundert Video-Lektionen, die Sie oft auf YouTube sehen, wenn sie Ihnen gefallen, dann wird Ihnen der Kurs zu 100 % gefallen. Er wird genauso spannend, motivierend und lehrreich sein wie meine Videos auf YouTube. Aber mehr noch, der Kurs enthält über dreißig praktische Aufgaben, um alle erworbenen Kenntnisse zu festigen. Sie können Ihr eigenes vollständiges Projekt unter der Anleitung eines Senior-Entwicklers schreiben. Diskutieren Sie Architektur, Konzepte, Technologien. Sie erhalten zwei vollständige Beratungen mit einem Senior-Entwickler zu Ihrem Projekt. Außerdem erhalten Sie Zugang zur Community von Python-Entwicklern, in der Ihnen geholfen wird, alle Probleme zu lösen, auf die Sie stoßen. Und natürlich können Sie sich Bewertungen und sogar Video-Bewertungen von denen ansehen, die den Kurs abgeschlossen haben, von denen, die Arbeit gefunden haben. Bei uns lernen Junioren, Mid-Level und Senioren. Alle lernen etwas Neues für sich, denn es gibt wirklich sehr, sehr viele Nuancen. Und das Wichtigste: Nur bis zum 31. August, also noch buchstäblich ein paar Tage, gilt die Aktion für den Kurs, und am 1. September werden die Preise erhöht. Wenn Sie also ein solider Backend-Entwickler werden, skalierbaren, zuverlässigen Code schreiben und das schnell und sicher tun wollen, dann sollten Sie dem Kurs unbedingt beitreten. Freunde, ich werde Sie über den Link in der Beschreibung erwarten. Gehen Sie hin, machen Sie sich mit allen Tarifen, Bewertungen und dem Kursplan vertraut. Sie können sogar kostenlos beginnen. Wir haben drei kleine Projekte, die Sie noch vor dem Kauf des Kurses schreiben können. Im Allgemeinen machen Sie sich damit vertraut, alles über den Link in der Beschreibung. Und ich wünsche Ihnen viel Spaß beim Ansehen. [Musik] In dieser Lektion wird es meiner Meinung nach absolut fantastisches Material geben. Wir werden anschaulich sehen, was die Vorteile von FastAPI sind, was sein Phänomen ist und was das Phänomen aller asynchronen Frameworks in Python ist. Wir werden unseren Server mit vielen Anfragen belasten und sehen, was mit synchronen Endpunkten in Python passiert, mit asynchronen. und den Unterschied verstehen. Es wird absolut großartig. Ich freue mich sehr, Ihnen dieses Material geben zu können. Ich hoffe, es gefällt Ihnen. Also, was werden wir tun? Wir werden unseren FastAPI-Service testen und belasten. Dafür schreiben wir zwei einfache Endpunkte, dann löschen wir sie, aber zum Testen schreibe ich sie. Wir werden einen synchronen Endpunkt haben. Bisher haben wir nur synchrone Endpunkte geschrieben. Ich habe nichts über Asynchronität gesagt, weil es bisher keine Asynchronität gab. Aber jetzt werden wir sie gemeinsam erstellen. Wir werden einen synchronen Endpunkt haben. Wir werden hier auch eine ID entgegennehmen, um zu unterscheiden, welcher Endpunkt gerade gestartet wurde, welcher bereits geantwortet hat, welcher nicht. Dies wird natürlich eine synchrone Funktion sein. Nennen wir sie S. Wir nehmen hier eine ID entgegen. Und im Wesentlichen wird die Arbeit dieser Funktion darin bestehen, zu schlafen. Wir importieren. Und wir importieren asyncio. Ich denke, Sie verstehen bereits, wozu time da ist. Zum Schlafen. im synchronen Format. Und denselben Endpunkt machen wir asynchron as. Und hier wird natürlich asing def sein. Ah, ja, die Funktion as sollte man nicht so nennen. Nennen wir sie asing fun. Und hier wird funk asininkio sleep sein. Das heißt, beide werden 3 Sekunden schlafen. Und wir fügen ein wenig, ein wenig Logging, ein wenig Print hinzu. Wir schreiben hier print. Ich werde jetzt bei mir nachschauen, um zu schreiben, wie es richtig ist. Wir werden die Zeit verfolgen. Das heißt, wir werden verfolgen, erstens, welcher Endpunkt gerade gestartet wurde, und wir werden die aktuelle Zeit schreiben time to und wir werden auf zwei Nachkommastellen runden. Das heißt, uns interessieren die Mikrosekunden nicht so sehr. Uns ist wichtig zu verstehen, ob die Endpunkte wirklich gleichzeitig gestartet sind, plus/minus, oder ob sie sich zu unterschiedlichen Zeiten nacheinander warten, ob sie sich gegenseitig blockieren. Wie ich gesagt habe, wird es heute in dieser Lektion sehr interessant sein. Hier begann, begann. Wir ändern es in beendet. Beendet. Und dieselbe Zeit und ID. Dort interessant. Hier machen wir natürlich asнсин. Und versuchen wir, unseren Server anzugreifen. Wie werden wir unseren Server angreifen? Wir haben jetzt UVCorn gestartet. Schauen wir mal. Ja, es läuft. Alles funktioniert gut, aktualisiert sich. Und wir schreiben ein kleines Skript, das uns angreifen wird. Erinnern Sie sich, wir haben bereits eine Funktion verwendet, die fast parallel und gleichzeitig viele, viele, viele asynchrone Funktionen startet. Und auf diese Weise können wir Code gleichzeitig ausführen. Jetzt werden wir diese Funktion zum Testen verwenden. Wir benötigen die Bibliothek Asinayom. Wir benötigen die Bibliothek A und OHTTP, um asynchrone Anfragen zu stellen. Wir könnten auch über Multithreading in Python eine große Menge paralleler Anfragen erstellen, aber wir behandeln Threads in diesem Kurs fast nicht, wir sprechen nur im Mentoring darüber. Denn in der realen Arbeit werden Threads äußerst selten verwendet. Ich frage Interviewer sehr gerne: "Leute, verwendet ihr Threads, Prozesse?" Weil manchmal danach auf dem Vorstellungsgespräch gefragt wird, sagen sie mir: "Ja, nein, wir verwenden sie überhaupt nicht." Nun, das ist für mich natürlich lustig, weil ich gerade gefragt wurde, was ein Thread ist, wie er sich von einem Prozess unterscheidet, und dann stellt sich heraus, dass es bei der Arbeit nicht benötigt wird. Nun, solche Fälle gibt es, also machen wir es einfacher. Wir machen es über Asynchronität. Und hier wähle ich eine virtuelle Umgebung aus, in der diese beiden Bibliotheken installiert sind. Und zuerst schreiben wir eine einfache Anfrage an unsere API. Als ob wir ein anderer Entwickler wären, ja? Versuchen wir es genau in diesem Sinne zu interagieren. Ah, get data, zum Beispiel, hier werden wir entgegennehmen, was wir entgegennehmen werden, eine ID. Erinnern Sie sich, wir haben geschrieben, erinnern Sie sich wahrscheinlich, vor 2 Minuten haben wir eine ID geschrieben. Und wir werden sie senden. Die Adresse wird folgende sein. Es wird HTTP 127001 sein. Man kann localhost schreiben. Im Grunde wird es keinen Unterschied geben. Anstelle von 17001 ist der Port obligatorisch. Wir werden hier auch unbedingt die Endpunkt selbst entgegennehmen. Man kann den Pfad nennen, man kann road nennen, man kann endpoint nennen. Aber Endpunkt gibt es im Englischen nicht, also so. Endpoint ist eine Zeichenkette. Und wir werden ihn hier hineinleiten. Vorsichtig so hineinleiten. Weiter, um eine Anfrage über iOSTP zu stellen, kann man einfach die Dokumentation aufrufen und die Anfrage von dort kopieren. Ich habe sie kopiert, sie lebt in meinem Kopf, deshalb erinnere ich mich, wie eine Anfrage erstellt wird. Wir deklarieren eine Sitzung, einen asynchronen Kontextmanager. Innerhalb noch ein asynchroner Manager. Dies ist kein production-ready Code. Das heißt, die client session wird nicht jedes Mal in Anwendungen erstellt, sondern normalerweise einmal pro Anwendung und dann wird diese Sitzung wiederverwendet. In diesem Fall ist dies ein vereinfachtes Beispiel. Wir lernen jetzt nicht iOSTP, wir wollen nur unseren Server angreifen. Das ist unser Ziel. session get, das heißt, wir senden eine GET-Anfrage, es gibt auch POST-Anfragen, natürlich, uns interessiert GET. Dieses URL asp, das heißt, ein response. Und hier können wir schreiben, dass wir die Ausführung beendet haben. Beendet. Beendet die Ausführung und übergeben die ID. Und wir können auch die Ausführung beginnen, hier einen Log machen. Ausführung begonnen. Und diese Funktion. Wir können sie jetzt über Aight starten, natürlich. Ja, ich habe vergessen zu sagen, dass wenn wir in Jupyter über solche Zellen arbeiten, Sie wahrscheinlich in einer Python-Datei arbeiten, wenn Sie meinen Code abtippen. Aber wenn Sie Jupyter installiert haben, dann super, damit ist es bequem zu arbeiten. Und in normalem Python kann man eine Funktion nicht einfach awaiten, man muss aso run aufrufen. Was ist run? Und diese Funktion so. Aber da dies Jupyter ist, kann man hier sehr gut direkt in der Zelle diese Funktion starten, zum Beispiel, und, nun, irgendeinen und endpunkt wird sy sein, ja, und wir haben vergessen, die ID hier zu übergeben. So awaiten wir und wir haben 3 Sekunden Wartezeit, ja, alles ist erledigt. Auf der FastAPI-Seite haben wir hier auch einen Log, dass es begonnen und beendet hat, ja, genau 3 Sekunden plus/minus, das ist nicht so wichtig. Dasselbe können wir für den asynchronen Endpunkt machen. Eine Anfrage an den asynchronen senden. Auch hier begonnen, beendet, hat auch 3 Sekunden gedauert. Nun, im Grunde nicht überraschend. Wir haben noch keinen Angriff auf den Server, keine Last. Versuchen wir jetzt anzugreifen. Machen wir die Funktion asinio, genau die, die es ermöglicht, viele, viele, viele Funktionen sofort zu starten, auf die Ausführung aller zu warten, Ergebnisse zu erhalten. Ergebnisse interessieren uns überhaupt nicht. Unsere Ergebnisse sind die Prints. Wir sehen, ob die Funktion ausgeführt wurde oder nicht. Und ich könnte hier 10 Mal so schreiben. So. Aber das ist nicht unser Ansatz, nicht der Programmierer-Ansatz. Wir lieben es, alles zu optimieren, auch ich. Deshalb werde ich hier eine Entpackung eines solchen Arrays machen. Und get data for e in range. Zum Beispiel, drei Anfragen. Wir werden eine ID übergeben und den Endpunkt angreifen. Oder, oder sy. Ich erinnere noch einmal daran, dass unser ASН-Endpunkt eine asynchrone Funktion in Python ist. Unter Verwendung davon, als ob wir eine Anfrage an eine Datenbank simulieren, vielleicht an einen Broker, an einen Cache, an eine externe API, egal. Und die synchrone Funktion heißt so. Alles hier. Syn syn syn syn. Und der Schlaf ist auch synchron, blockierend. Schauen wir uns diese Aktionen an. Starten wir zuerst den synchronen Endpunkt. Starten. Wir haben gleichzeitig drei Anfragen gesendet, und nach 3 Sekunden haben wir gleichzeitig die Antwort erhalten. In FastAPI erhalten wir auch eine Bestätigung, dass wir plus/minus gleichzeitig Anfragen an diesen Endpunkt erhalten haben und gleichzeitig geantwortet haben. Obwohl die Funktion synchron ist und die Ausführung blockieren sollte. Etwas stimmt nicht. Ich werde noch nicht alle Karten aufdecken. Versuchen wir, den asynchronen Endpunkt zu durchbrechen. Dasselbe. Starten, überprüfen. Und nach 3 Sekunden erhalten wir hier die Bestätigung, dass 3 Sekunden vergangen sind. Und hier kann man anhand der Zeitstempel, anhand der Zeitmarken sehen, dass 3 Sekunden vergangen sind. Hier wird die Reihenfolge natürlich immer unterschiedlich sein. Das heißt, wir haben 0,12 gesendet, dann kam 21. Es kann auch eine völlig andere Reihenfolge kommen, weil dort der Unterschied Mikromillisekunden beträgt. Das ist nicht wichtig. Wichtig ist, dass alles zurückgekommen ist. Und ich werde Ihnen immer noch nicht verraten, was überhaupt der Unterschied ist, warum sie bei uns gleich funktionieren. Angeblich hast du, Artem, gesagt, dass Asynchronität toll ist, Synchronität schlecht. Tatsächlich ist es so. Schauen wir uns ein größeres Volumen an. Bisher haben wir ein kleines Volumen. Und machen wir zum Beispiel 30 gleichzeitige Anfragen. Wie werden wir verstehen, dass sie gleichzeitig sind? Wir starten und sehen, dass hier sofort 30 Prints ausgegeben wurden. Das bedeutet, dass 30 Anfragen gesendet wurden. Und nach 3 Sekunden. Hm, genauso in der Asynchronität kam alles gut und schnell an. Auf der Seite gibt es auch eine Bestätigung dafür. Alles ist erledigt. Aber versuchen wir es jetzt mit dem synchronen Endpunkt, einem Angriff von 30 Anfragen. Auch hier haben wir alles sofort gesendet. 3 Sekunden sind vergangen und wir haben alles erhalten. Und in FastAPI haben wir auch alles gut erhalten. Aber das Problem. Wenn wir die Anzahl der Anfragen erhöhen, zum Beispiel noch um das Zehnfache und 300 Anfragen an den asynchronen senden, dann senden wir hier sofort 300 Anfragen und erhalten nach 3 Sekunden 300 Antworten. Und FastAPI wird hier entsprechend beenden, beenden, beenden, beenden. Und wenn wir 300 Anfragen an den synchronen Endpunkt senden, hier haben wir sie gesendet, zu unserer Überraschung wird die Antwort nach 3 Sekunden nicht kommen. Und wenn wir zu FastAPI wechseln und hier die Konsole öffnen, sehen wir, dass er erst 150 begonnen hat, hier erst 190, und wir haben 300 gleichzeitige Clients. Und das ist noch eine kleine Anwendung, eigentlich. Und warten wir, bis hier 299, 292 steht, ja? Nun, das ist es, er hat alle beendet. 24 Sekunden haben wir gewartet, anstatt drei. Hier stimmt etwas nicht. Und wir werden keine Magie verwenden, wir importieren noch eine Bibliothek, threading, für Threads, Multithreading in Python. Und es gibt eine solche wunderbare Funktion in diesem Modul, die heißt Active Count. Und wir werden sie ausdrucken. Diese Funktion zeigt an, wie viele Threads gerade ausgeführt werden. Und wir schreiben hier auch S. schreiben Threads, Doppelpunkt und die Anzahl der Threads. Und in der Asynchronität schreiben wir auch die Anzahl der Threads. Und was ist der Punkt? Wir werden verfolgen. Ursprünglich haben wir einen Thread, natürlich, keine Last, einen Thread. Wenn die Anzahl der Anfragen stark ansteigt, dann generiert die synchrone Funktion, die über FastAPI beginnt, neue, neue, neue Threads. Und soweit ich weiß, liegt die Begrenzung auf der Ebene des Starlet-Frameworks, auf dem FastAPI basiert, bei 40 Threads. Möglicherweise ist dies in FastAPI selbst implementiert. Ich bin nicht so tief eingedrungen, aber glauben Sie mir einfach, irgendwo gibt es dort eine Begrenzung. dass Threads gestartet werden, aber damit FastAPI überhaupt nicht langsam aussieht, ja, denn wenn es nur einen Thread gäbe, dann würden sich tatsächlich alle Clients gegenseitig blockieren, es wäre äußerst unangenehm. Schauen wir uns diese Metrik an. Wir greifen wieder die Asynchronität an, den asynchronen Endpunkt, wo wir über s, über await alles machen. Und schauen Sie, ja, alles ist abgeschlossen, hat alles 3 Sekunden gedauert, wie zuvor. Und Threads sind überall, überall, überall einer. Das heißt, wir leben in einem Thread, es werden keine neuen erstellt. Und wenn wir jetzt den synchronen Endpunkt angreifen, schauen Sie, Threads 27, 36, 38, 41 und mehr als 41 wird es nicht geben. Das heißt, wenn wir eine sehr, sehr große Last haben, steigt die Anzahl der Threads stark an, aber nicht mehr als eine bestimmte Anzahl von 41. Und FastAPI versucht, alle Aufgaben auf diese Weise auszuführen, denn wenn es keine Threads erstellen würde, würden wir wie lange warten? 300 Mal 3 Sekunden. Wir würden 1.000 Sekunden warten. Das sind wie viele? 15 Minuten. Und wir haben es in 24 Sekunden geschafft, ja, wie Sie sich erinnern. Und hier ist eine anschauliche Demonstration, wie Frameworks wie Flask und Django funktionieren. Wie sie skaliert werden können. Es werden viele Threads erstellt, die Anfragen verarbeiten. Das heißt, früher, als es noch keine Asynchronität in Python gab, ein kleiner historischer Exkurs, wurde Multithreading verwendet, um, nun ja, quasi parallele, konkurrierende Anfragen zu stellen. Tatsächlich werden Threads alle 5 Millisekunden umgeschaltet. Das heißt, zuerst hat einer gearbeitet, dann ein anderer nach 5 Millisekunden. Und für das menschliche Auge ist es nicht wahrnehmbar, dass ein Umschalten stattfindet. Aber tatsächlich findet es ständig statt und es entsteht das Gefühl, dass alles parallel funktioniert. Tatsächlich nicht. Und Threads können tatsächlich nicht unendlich viele erstellt werden. Sie haben, sie belegen Speicher, sie belegen Ressourcen. Und darin liegt die Schwierigkeit der Skalierung und die Kostspieligkeit. Das heißt, sehen Sie, obwohl wir mit vierzig Threads arbeiten, haben wir es in 24 Sekunden geschafft, ja, nicht 15 Minuten. Danke. Aber es ist immer noch achtmal mehr als über asynchrone über asynchrone Funktionen. Wie kann man das skalieren? Natürlich wird in der Produktion nicht nur ein Uvicorn gestartet. Hier ist der Punkt. Wir haben jetzt nur einen Uvicorn gestartet, das heißt, einen sogenannten Worker oder auf Deutsch einen Arbeiter. Das heißt, ein Programm, könnte man sagen. Und in der Produktion werden sie normalerweise viele gestartet. Das heißt, je nach Anwendung sind es manchmal 100 Worker, manchmal 1.000, manchmal 10, manchmal zwei. Hängt von der Last ab. Wir können innerhalb von Uvicorn, wenn wir rello ausschalten, das heißt, es ist unbedingt notwendig, ihn auszuschalten, die Anzahl der Arbeiter, Worker, zum Beispiel fünf oder zehn angeben. Versuchen wir es mit fünf. Ich habe eine leistungsstarke Maschine, ich kann das tun. Ich habe ein MacBook Pro, also wird es damit keine Probleme geben. Wir starten unseren UVCorn erneut. Wir sehen, dass bei uns, schauen Sie, application startup Complete fünfmal wiederholt wird. Eins, 2, 3, 4, 5. Das heißt, jeder Worker wird hier seine Logs ausgeben. Das heißt, sie leben alle in dieser einen Konsole, könnte man sagen, in einem Terminal. Versuchen wir jetzt erneut, 300 Anfragen an den synchronen Endpunkt zu senden. Threads werden hier immer noch 42 sein, wie Sie sich vorstellen können. Die Begrenzung verschwindet nicht, aber der Witz ist, dass jeder Worker jetzt eine Begrenzung von 40 Threads hat, und wir werden schneller fertig sein. 15 Sekunden. Ja, es kann noch schneller gehen. Ich kann die Anzahl der Worker zum Beispiel auf 30 erhöhen. Das heißt, ich habe eine ziemlich leistungsstarke Maschine. Für mich ist das keine Grenze. Ich habe viele, viele Worker gestartet. Alle haben geschrieben, dass sie bereit sind. Wunderbar. Wir starten unser Lasttest und in 3 Sekunden, das heißt, wenn ich noch mehr Worker mache, wird es nicht 32 sein, sondern 3,1 oder 3,0, äh, alle Antworten werden zurückkommen. Das heißt, FastAPI wird alle unsere Anfragen verarbeiten. Und das ist wunderbar in dem Sinne, dass wir viel Geld ausgegeben haben, viele Worker gestartet haben, wir viel Speicher benötigt haben, und wir die gleiche Effizienz erreicht haben, die bei einem Worker, in einem Thread war. Welchen Schluss ziehen wir also? Wir entfernen die Worker, wir setzen rello auf true, starten alles neu. Wir kommen zu dem Schluss, dass Asynchronität großartig ist, dass sie wirklich die finanziellen Kosten senkt, dass sie unseren Code beschleunigt und die Komplexität der Entwicklung nicht wesentlich verändert, eigentlich. Das heißt, wenn wir über das Thema Threads sprechen, ist das ein komplexes Thema zum Erlernen. Wenn wir über Asynchronität sprechen, ist das ein ziemlich verständliches Thema, besonders wenn es nicht zu kompliziert mit tiefgründiger Sprache erklärt wird. Ich hoffe sehr, dass ich es mehr oder weniger verständlich erkläre. Versuchen wir zum Experiment noch 3.000 gleichzeitige Anfragen zu machen und zu sehen, ob mein Computer abstürzt, was überhaupt passiert, ja, auf den ASNK-Endpunkt. Wir starten. Wir haben sofort 3.000 Prints. Das bedeutet, dass wir 3.000 Anfragen gesendet haben. Und zu unserer Überraschung haben wir nicht mehr 3 Sekunden gewartet, sondern vier. Dennoch hat FastAPI gut damit umgegangen. Alle 3.000 Anfragen. Als nächstes schicken wir 30.000. Ich hoffe, alles funktioniert. Wir haben gestartet. Ja, sie starten nicht mehr alle so schnell. Man muss warten. Und, nun, hier wird es nicht mehr 3 Sekunden dauern. Hier wird es viel länger dauern. Ja, eine sichtbare, unsichtbare Grenze wurde überschritten. Es ist ein Problem aufgetreten, das wir bereits auf einer Ebene haben. Anscheinend haben wir die Anzahl der Anfragen überschritten. Aber im Allgemeinen, denke ich, haben Sie die Essenz verstanden. Das heißt, wenn wir viele, viele Anfragen machen, gewinnen wir bei der Verwendung von Asynchronität. Und das ist wunderbar. y