eBPF: Moderne Linux-Introspection-Funktionen oder der Kernel ist keine Black Box mehr





Jeder hat ein Lieblingsbuch über Magie. Jemand ist Tolkien, jemand ist Pratchett, jemand wie ich ist Max Fry. Heute erzähle ich Ihnen von meiner Lieblings-IT-Magie - von BPF und der modernen Infrastruktur.



BPF ist gerade auf dem Höhepunkt. Die Technologie entwickelt sich sprunghaft weiter, dringt in die unerwartetsten Orte ein und wird für den Durchschnittsbenutzer immer zugänglicher. Auf fast jeder populären Konferenz heute können Sie einen Bericht zu diesem Thema hören, und GopherCon Russia ist keine Ausnahme: Ich präsentiere Ihnen eine Textversion meines Berichts .



In diesem Artikel werden keine einzigartigen Entdeckungen gemacht. Ich werde nur versuchen, Ihnen zu zeigen, was BPF ist, was es kann und wie es Ihnen persönlich helfen kann. Wir werden uns auch die Funktionen im Zusammenhang mit Go ansehen.



Nachdem ich meinen Artikel gelesen habe, möchte ich, dass Ihre Augen genauso leuchten wie die Augen eines Kindes, das das Harry-Potter-Buch zum ersten Mal gelesen hat, damit Sie nach Hause kommen oder arbeiten und ein neues „Spielzeug“ in Aktion ausprobieren können.



Was ist eBPF?



Also, von welcher Art von Magie wird Ihnen ein 34-jähriger bärtiger Mann mit brennenden Augen erzählen?



Wir leben mit Ihnen im Jahr 2020. Wenn Sie Twitter öffnen, lesen Sie die Tweets mürrischer Herren, die behaupten, dass die Software jetzt von einer so schrecklichen Qualität geschrieben wird, dass es einfacher ist, alles wegzuwerfen und von vorne zu beginnen. Einige drohen sogar, den Beruf zu verlassen, weil sie es nicht länger aushalten können: Alles bricht ständig zusammen, unbequem, langsam.







Vielleicht haben sie recht: Ohne tausend Kommentare werden wir es nicht herausfinden. Aber ich werde definitiv zustimmen, dass der moderne Software-Stack komplexer als je zuvor ist.



BIOS, EFI, Betriebssystem, Treiber, Module, Bibliotheken, Netzwerke, Datenbanken, Caches, Orchestratoren wie K8s, Container wie Docker, schließlich unsere Software mit Laufzeiten und Garbage Collectors. Ein echter Profi kann die Frage beantworten, was passiert, nachdem Sie ya.ru mehrere Tage lang in Ihren Browser eingegeben haben.



Es ist sehr schwer zu verstehen, was in Ihrem System passiert, insbesondere wenn gerade etwas schief geht und Sie Geld verlieren. Dieses Problem hat zur Entstehung von Geschäftsbereichen geführt, die Ihnen helfen sollen, die Vorgänge in Ihrem System zu verstehen. Große Unternehmen haben ganze Sherlock-Abteilungen, die wissen, wo sie hämmern und welche Mutter festziehen müssen, um Millionen von Dollar zu sparen.



In Interviews frage ich oft Leute, wie sie Probleme beheben, wenn sie um vier Uhr morgens geweckt werden.



Ein Ansatz besteht darin, die Protokolle zu analysieren . Das Problem ist jedoch, dass nur diejenigen verfügbar sind, die der Entwickler in sein System eingegeben hat. Sie sind nicht flexibel.



Der zweite beliebte Ansatz ist das Studium von Metriken . Die drei beliebtesten Systeme für die Arbeit mit Metriken sind in Go geschrieben. Metriken sind sehr nützlich, aber sie helfen Ihnen nicht immer, die Ursachen zu verstehen, indem sie Ihnen ermöglichen, Symptome zu erkennen.



Der dritte Ansatz, der immer beliebter wird, ist die sogenannte Beobachtbarkeit: die Fähigkeit, beliebig komplexe Fragen zum Verhalten des Systems zu stellen und Antworten darauf zu erhalten. Da die Frage sehr komplex sein kann, erfordert die Antwort möglicherweise eine Vielzahl von Informationen, und bis die Frage gestellt wird, wissen wir nicht, was. Dies bedeutet, dass Flexibilität für die Beobachtbarkeit von entscheidender Bedeutung ist.



Geben Sie die Möglichkeit, die Protokollierungsstufe im laufenden Betrieb zu ändern? Mit einem Debugger eine Verbindung zu einem laufenden Programm herstellen und dort etwas tun, ohne dessen Arbeit zu unterbrechen? Verstehen Sie, welche Anforderungen in das System eingehen, visualisieren Sie die Quellen langsamer Anforderungen, sehen Sie, für welchen Speicher pprof verwendet wird, und erhalten Sie eine grafische Darstellung der zeitlichen Änderungen. Latenz einer Funktion und Abhängigkeit der Latenz von Argumenten messen? Alle diese Ansätze werde ich auf Beobachtbarkeit beziehen. Dies ist eine Reihe von Dienstprogrammen, Ansätzen, Kenntnissen und Erfahrungen, die Ihnen zusammen die Möglichkeit geben, wenn nicht alles, dann viel "Gewinn" direkt im Arbeitssystem zu erzielen. Modernes Schweizer IT-Messer.







Aber wie geht das? Es gab und gibt viele Instrumente auf dem Markt: einfach, komplex, gefährlich, langsam. Das Thema des heutigen Artikels ist jedoch BPF.



Der Linux-Kernel ist ereignisgesteuert. Fast alles, was im Kernel und im gesamten System geschieht, kann als eine Reihe von Ereignissen dargestellt werden. Eine Unterbrechung ist ein Ereignis, das Empfangen eines Pakets über das Netzwerk ist ein Ereignis, die Übertragung eines Prozessors zu einem anderen Prozess ist ein Ereignis, der Start einer Funktion ist ein Ereignis.

BPF ist also ein Subsystem des Linux-Kernels, das es ermöglicht, kleine Programme zu schreiben, die vom Kernel als Reaktion auf Ereignisse gestartet werden. Diese Programme können sowohl Aufschluss darüber geben, was in Ihrem System geschieht, als auch es steuern.



Es war eine sehr lange Einführung. Kommen wir der Realität näher.



1994 erschien die erste Version von BPF, auf die einige von Ihnen möglicherweise gestoßen sind, als sie einfache Regeln für das Dienstprogramm tcpdump zum Anzeigen oder Abhören von Netzwerkpaketen geschrieben haben. tcpdump könnte "Filter" setzen, um nicht alle, sondern nur die Pakete anzuzeigen, an denen Sie interessiert sind. Zum Beispiel "nur TCP-Protokoll und nur Port 80". Für jedes übergebene Paket wurde eine Funktion ausgeführt, um zu entscheiden, ob dieses bestimmte Paket gespeichert werden soll oder nicht. Es kann viele Pakete geben, was bedeutet, dass unsere Funktion sehr schnell sein muss. Unsere tcpdump-Filter wurden gerade in BPF-Funktionen konvertiert. Ein Beispiel dafür ist in der folgenden Abbildung dargestellt.





Ein einfacher Filter für tcpdump wird als BPF-Programm dargestellt



Die ursprüngliche BPF war eine sehr einfache virtuelle Maschine mit mehreren Registern. Dennoch hat BPF die Filterung von Netzwerkpaketen erheblich beschleunigt. Zu einer Zeit war dies ein großer Schritt nach vorne. 







Im Jahr 2014 erweiterte Alexey Starovoitov die Funktionalität von BPF. Er erhöhte die Anzahl der Register und die zulässige Größe des Programms, fügte die JIT-Kompilierung hinzu und erstellte einen Prüfer, der Programme auf Sicherheit überprüfte. Das Beeindruckendste war jedoch, dass neue BPF-Programme nicht nur während der Paketverarbeitung, sondern auch als Reaktion auf zahlreiche Kernelereignisse gestartet und Informationen zwischen Kernel und Benutzerbereich hin und her übertragen werden konnten.



Diese Änderungen haben den Weg für neue Anwendungsfälle für BPF geebnet. Einige Dinge, die zuvor durch das Schreiben komplexer und gefährlicher Kernelmodule erledigt wurden, sind jetzt über BPF relativ einfach zu erledigen. Warum ist das cool? Ja, denn jeder Fehler beim Schreiben eines Moduls führte häufig zu Panik. Nicht zur flauschigen Go-shnoy-Panik, sondern zur Kernel-Panik, danach - nur Neustart.



Der durchschnittliche Linux-Benutzer hat jetzt die Supermacht, unter die Haube zu schauen, die bisher nur Hardcore-Kernel-Entwicklern oder anderen zur Verfügung stand. Diese Option ist vergleichbar mit der Möglichkeit, mühelos ein Programm für iOS oder Android zu schreiben: Auf älteren Telefonen war dies entweder unmöglich oder viel schwieriger.



Die neue Version von BPF von Alexey heißt eBPF (vom Wort erweitert - erweitert). Aber jetzt hat es alle alten Versionen von BPF ersetzt und ist so populär geworden, dass jeder es der Einfachheit halber einfach BPF nennt.



Wo wird BPF eingesetzt?



Was sind also diese Ereignisse oder Auslöser, an die BPF-Programme angehängt werden können, und wie haben Menschen begonnen, diese neu entdeckte Kraft zu nutzen?



Derzeit gibt es zwei große Gruppen von Triggern.



Die erste Gruppe wird zur Verarbeitung von Netzwerkpaketen und zur Verwaltung des Netzwerkverkehrs verwendet. Dies sind XDP, Verkehrssteuerungsereignisse und einige mehr.



Diese Ereignisse werden benötigt, um:



  • , . Cloudflare Facebook BPF- DDoS-. ( BPF- ), . .

  • , , — , , . . Facebook, , , .

  • Erstellen Sie intelligente Balancer. Das bekannteste Beispiel ist das Cilium- Projekt , das im K8- Cluster am häufigsten als Mesh-Netzwerk verwendet wird. Cilium verwaltet den Verkehr: gleicht ihn aus, leitet ihn um und analysiert ihn. Und all dies geschieht mit Hilfe kleiner BPF-Programme, die vom Kernel als Reaktion auf das eine oder andere Ereignis im Zusammenhang mit Netzwerkpaketen oder -sockets gestartet werden.



Dies war die erste Gruppe von Auslösern, die mit Netzwerkproblemen verbunden waren und das Verhalten beeinflussen konnten. Die zweite Gruppe bezieht sich auf die allgemeinere Beobachtbarkeit; Programme aus dieser Gruppe haben meist nicht die Fähigkeit, etwas zu beeinflussen, sondern können nur "beobachten". Sie interessiert mich viel mehr.



Diese Gruppe enthält Auslöser wie:



  • perf events — , Linux- perf: , , minor/major- . . , , , - . , , , , .

  • tracepoints — ( ) , (, ). , — , , , , . - , tracepoints :
    • ;

    • , ;

    • API, , , , , API.



      , , , , , pprof .


  • USDT — , tracepoints, user space-. . : MySQL, , PHP, Python. enable-dtrace . , Go . -, , DTrace . , , Solaris: , , GC -, .



Nun, dann beginnt eine andere Ebene der Magie:



  • Mit ftrace-Triggern können wir ein BPF-Programm zu Beginn fast jeder Kernelfunktion ausführen. Voll dynamisch. Dies bedeutet, dass der Kernel Ihre BPF-Funktion aufruft, bevor Sie eine von Ihnen ausgewählte Kernelfunktion ausführen. Oder alle Kernelfunktionen - was auch immer. Sie können an alle Kernelfunktionen anhängen und eine schöne Visualisierung aller Aufrufe in der Ausgabe erhalten.

  • kprobes / uprobes bieten fast das Gleiche wie ftrace, nur dass wir bei der Ausführung einer Funktion an einer beliebigen Stelle einrasten können, sowohl im Kernel als auch im Benutzerbereich. In der Mitte der Funktion befindet sich eine Art If für eine Variable, und Sie müssen ein Histogramm der Werte dieser Variablen zeichnen. Kein Problem.

  • kretprobes/uretprobes — , user space. , , . , , PID fork.



Das Bemerkenswerteste an all dem, ich wiederhole, ist, dass unser BPF-Programm, wenn es auf einen dieser Trigger aufgerufen wird, sich gut umsehen kann: Funktionsargumente lesen, zeitgesteuert, Variablen lesen, globale Variablen, einen Stack-Trace erstellen, das speichern Übertragen Sie dann für später Daten zur Verarbeitung in den Benutzerbereich, rufen Sie Daten aus dem Benutzerbereich zum Filtern oder für einige Steuerbefehle ab. Schönheit!



Ich weiß nichts über dich, aber für mich ist die neue Infrastruktur wie ein Spielzeug, auf das ich lange ängstlich gewartet habe.



API oder Verwendung



Okay, Marco, du hast uns überredet, nach BPF zu schauen. Aber wie geht man damit um?



Lassen Sie uns einen Blick darauf werfen, woraus ein BPF-Programm besteht und wie man damit interagiert.







Erstens haben wir ein BPF-Programm, das, wenn es überprüft wird, in den Kernel geladen wird. Dort wird JIT in Maschinencode kompiliert und im Kernelmodus ausgeführt, wenn der Trigger, an den es angehängt ist, ausgelöst wird.



Das BPF-Programm kann mit dem zweiten Teil - dem User Space-Programm - interagieren. Es gibt zwei Möglichkeiten, dies zu tun. Wir können in einen Ringpuffer schreiben und der User-Space-Teil kann daraus lesen. Wir können auch in den Schlüsselwertspeicher schreiben und lesen, der als BPF-Map bezeichnet wird, und der Benutzerbereichsteil kann dasselbe tun und dementsprechend einige Informationen untereinander übertragen.



Gerader Weg



Der einfachste Weg, mit BPF zu arbeiten, mit dem Sie auf keinen Fall beginnen sollten, besteht darin, BPF-Programme ähnlich der C-Sprache zu schreiben und diesen Code mit dem Clang-Compiler in Code für virtuelle Maschinen zu kompilieren. Wir laden diesen Code dann direkt über den BPF-Systemaufruf und interagieren mit unserem BPF-Programm auch über den BPF-Systemaufruf.







Die erste verfügbare Vereinfachung ist die Verwendung der libbpf-Bibliothek, die mit den Kernelquellen geliefert wird und es Ihnen ermöglicht, nicht direkt mit dem BPF-Systemaufruf zu arbeiten. Tatsächlich bietet es praktische Wrapper zum Laden von Code, die mit sogenannten Maps zum Übertragen von Daten vom Kernel in den Benutzerbereich und zurück arbeiten.



bcc



Es ist klar, dass eine solche Verwendung alles andere als menschenfreundlich ist. Glücklicherweise erschien unter der Marke iovizor das BCC-Projekt, das unser Leben erheblich vereinfacht.







Tatsächlich bereitet es die gesamte Assembly-Umgebung vor und gibt uns die Möglichkeit, einzelne BPF-Programme zu schreiben, in denen der C-Teil automatisch zusammengestellt und in den Kernel geladen wird und der User-Space-Teil in einfachem und verständlichem Python ausgeführt werden kann.



bpftrace



Aber BCC sieht für viele Dinge kompliziert aus. Aus irgendeinem Grund schreiben die Leute besonders gerne keine Teile in C.



Dieselben Leute von iovizor haben das bpftrace-Tool eingeführt, mit dem Sie BPF-Skripte in einer einfachen Skriptsprache a la AWK (oder im Allgemeinen Einzeiler) schreiben können.







Der renommierte Experte für Leistung und Beobachtbarkeit, Brendan Gregg, hat die folgende Visualisierung der verfügbaren Möglichkeiten für die Arbeit mit BPF erstellt:







Vertikal haben wir die Einfachheit des Werkzeugs und horizontal seine Leistung. Es ist ersichtlich, dass BCC ein sehr leistungsfähiges Werkzeug ist, aber nicht sehr einfach. bpftrace ist viel einfacher, aber weniger leistungsfähig.



Beispiele für die Verwendung von BPF



Aber schauen wir uns die magischen Fähigkeiten an, die uns zur Verfügung stehen, mit konkreten Beispielen.



Sowohl BCC als auch bpftrace enthalten einen Tools-Ordner, der eine Vielzahl vorgefertigter interessanter und nützlicher Skripte enthält. Sie sind auch der lokale Stapelüberlauf, aus dem Sie Codestücke für Ihre Skripte kopieren können.



Hier ist beispielsweise ein Skript, das die Latenz für DNS-Abfragen anzeigt:



 ╭─marko@marko-home ~ 
╰─$ sudo gethostlatency-bpfcc
TIME      PID    COMM                  LATms HOST
16:27:32  21417  DNS Res~ver #93        3.97 live.github.com
16:27:33  22055  cupsd                  7.28 NPI86DDEE.local
16:27:33  15580  DNS Res~ver #87        0.40 github.githubassets.com
16:27:33  15777  DNS Res~ver #89        0.54 github.githubassets.com
16:27:33  21417  DNS Res~ver #93        0.35 live.github.com
16:27:42  15580  DNS Res~ver #87        5.61 ac.duckduckgo.com
16:27:42  15777  DNS Res~ver #89        3.81 www.facebook.com
16:27:42  15777  DNS Res~ver #89        3.76 tech.badoo.com :-)
16:27:43  21417  DNS Res~ver #93        3.89 static.xx.fbcdn.net
16:27:43  15580  DNS Res~ver #87        3.76 scontent-frt3-2.xx.fbcdn.net
16:27:43  15777  DNS Res~ver #89        3.50 scontent-frx5-1.xx.fbcdn.net
16:27:43  21417  DNS Res~ver #93        4.98 scontent-frt3-1.xx.fbcdn.net
16:27:44  15580  DNS Res~ver #87        5.53 edge-chat.facebook.com
16:27:44  15777  DNS Res~ver #89        0.24 edge-chat.facebook.com
16:27:44  22099  cupsd                  7.28 NPI86DDEE.local
16:27:45  15580  DNS Res~ver #87        3.85 safebrowsing.googleapis.com
^C%


Das Dienstprogramm zeigt die Ausführungszeit von DNS-Abfragen in Echtzeit an, sodass Sie beispielsweise einige unerwartete Ausreißer abfangen können.



Und dies ist ein Skript, das "ausspioniert", was andere auf ihren Terminals eingeben:



 ╭─marko@marko-home ~ 
╰─$ sudo bashreadline-bpfcc         
TIME      PID    COMMAND
16:51:42  24309  uname -a
16:52:03  24309  rm -rf src/badoo


Diese Art von Skript kann verwendet werden, um einen schlechten Nachbarn zu fangen oder die Sicherheit der Server eines Unternehmens zu überprüfen.



Skript zum Anzeigen von Flow-Aufrufen von Hochsprachen:



 ╭─marko@marko-home ~/tmp 
╰─$ sudo /usr/sbin/lib/uflow -l python 20590
Tracing method calls in python process 20590... Ctrl-C to quit.
CPU PID    TID    TIME(us) METHOD
5   20590  20590  0.173    -> helloworld.py.hello                  
5   20590  20590  0.173      -> helloworld.py.world                
5   20590  20590  0.173      <- helloworld.py.world                
5   20590  20590  0.173    <- helloworld.py.hello                  
5   20590  20590  1.174    -> helloworld.py.hello                  
5   20590  20590  1.174      -> helloworld.py.world                
5   20590  20590  1.174      <- helloworld.py.world                
5   20590  20590  1.174    <- helloworld.py.hello                  
5   20590  20590  2.175    -> helloworld.py.hello                  
5   20590  20590  2.176      -> helloworld.py.world                
5   20590  20590  2.176      <- helloworld.py.world                
5   20590  20590  2.176    <- helloworld.py.hello                  
6   20590  20590  3.176    -> helloworld.py.hello                  
6   20590  20590  3.176      -> helloworld.py.world                
6   20590  20590  3.176      <- helloworld.py.world                
6   20590  20590  3.176    <- helloworld.py.hello                  
6   20590  20590  4.177    -> helloworld.py.hello                  
6   20590  20590  4.177      -> helloworld.py.world                
6   20590  20590  4.177      <- helloworld.py.world                
6   20590  20590  4.177    <- helloworld.py.hello                  
^C%


Dieses Beispiel zeigt den Aufrufstapel eines Python-Programms.



Derselbe Brendan Gregg machte ein Bild, in dem er alle vorhandenen Skripte mit Pfeilen sammelte, die die Subsysteme angaben, die jedes Dienstprogramm "beobachten" kann. Wie Sie sehen, verfügen wir bereits über eine große Anzahl vorgefertigter Dienstprogramme - für fast jeden Anlass.





Versuchen Sie nicht, hier etwas zu sehen. Das Bild dient als Referenz



Was ist mit uns mit Go? 



Lassen Sie uns jetzt über Go sprechen. Wir haben zwei Hauptfragen:



  • Können Sie BPF-Programme in Go schreiben?

  • Ist es möglich, in Go geschriebene Programme zu analysieren?



Lass uns in Ordnung gehen.



Bisher ist Clang der einzige Compiler, der in ein Format kompiliert werden kann, das von der BPF-Maschine verstanden wird. Ein anderer beliebter Compiler, GCC, hat noch kein BPF-Backend. Und die einzige Programmiersprache, die zu BPF kompiliert werden kann, ist eine sehr eingeschränkte Version von C.



Das BPF-Programm hat jedoch einen zweiten Teil, der sich im Benutzerbereich befindet. Und es kann in Go geschrieben werden.



Wie oben erwähnt, können Sie mit BCC diesen Teil in Python schreiben, der Hauptsprache des Tools. Gleichzeitig unterstützt BCC im Haupt-Repository auch Lua und C ++ und im Repository von Drittanbietern auch Go .







Ein solches Programm sieht genauso aus wie ein Python-Programm. Am Anfang steht eine Zeile, in der ein BPF-Programm in C steht, und dann sagen wir, wo dieses Programm angehängt werden soll, und interagieren irgendwie damit. Beispielsweise erhalten wir Daten von der EPF-Karte.



Eigentlich ist das alles. Sie können das Beispiel auf Github genauer sehen .

Wahrscheinlich ist der Hauptnachteil, dass die C-Bibliothek libbcc oder libbpf für die Arbeit verwendet wird, und das Erstellen eines Go-Programms mit einer solchen Bibliothek sieht überhaupt nicht nach einem schönen Spaziergang im Park aus.



Neben iovisor / gobpf habe ich drei weitere aktuelle Projekte gefunden, mit denen Sie ein Userland-Teil in Go schreiben können.





Die Dropbox-Version benötigt keine C-Bibliotheken, aber Sie müssen den Kernel-Teil des BPF-Programms selbst mit Clang erstellen und ihn dann mit dem Go-Programm in den Kernel laden.



Die Cilium-Version bietet dieselben Funktionen wie die Dropbox-Version. Aber es ist erwähnenswert, schon allein deshalb, weil es von den Jungs vom Cilium-Projekt gemacht wird, was bedeutet, dass es zum Erfolg verurteilt ist.



Ich habe das dritte Projekt der Vollständigkeit halber mitgebracht. Wie die beiden vorherigen hat es keine externen C-Abhängigkeiten, erfordert die manuelle Montage eines BPF C-Programms, scheint aber nicht vielversprechend zu sein.



In der Tat gibt es eine andere Frage: Warum überhaupt BPF-Programme in Go schreiben? Wenn Sie sich BCC oder bpftrace ansehen, benötigen BPF-Programme im Allgemeinen weniger als 500 Codezeilen. Ist es nicht einfacher, ein Skript in bpftrace-Sprache zu schreiben oder ein wenig Python aufzudecken? Ich sehe hier zwei Gründe. 



Erstens liebst du Go wirklich und ziehst es vor, alles daran zu machen. Darüber hinaus lassen sich potenziell Go-Programme leichter von Computer zu Computer portieren: statische Verknüpfung, einfache Binärdateien usw. Aber alles ist alles andere als so offensichtlich, da wir an einen bestimmten Kern gebunden sind. Ich werde hier aufhören, sonst wird mein Artikel weitere 50 Seiten umfassen.



Die zweite Option: Sie schreiben kein einfaches Skript, sondern ein umfangreiches System, das BPF auch intern verwendet. Ich habe sogar ein Beispiel für ein solches System in Go :







Das Scope-Projekt sieht aus wie eine Binärdatei, die beim Start in der Infrastruktur von K8s oder einer anderen Cloud alles analysiert, was um sie herum geschieht, und zeigt, was Container, Services sind, wie sie interagieren usw. Und vieles davon wird mit BPF erledigt. Ein interessantes Projekt.



Go-Programme analysieren



Wenn Sie sich erinnern, hatten wir eine andere Frage: Können wir in Go geschriebene Programme mit BPF analysieren? Erster Gedanke - natürlich! Welchen Unterschied macht es in welcher Sprache das Programm geschrieben ist? Schließlich ist dies nur ein kompilierter Code, der wie alle anderen Programme etwas auf dem Prozessor berechnet, Speicher wie in sich selbst aufnimmt, über den Kernel mit der Hardware und über Systemaufrufe mit dem Kernel interagiert. Im Prinzip ist dies richtig, aber es gibt Merkmale mit unterschiedlichen Schwierigkeitsgraden.



Argumente übergeben



Eine Funktion ist, dass Go nicht das ABI verwendet, das die meisten anderen Sprachen verwenden. Zufällig beschlossen die Gründerväter, den ABI des Plan 9- Systems zu übernehmen , den sie sehr gut kannten.



ABI ist wie eine API, eine Interoperabilitätsvereinbarung, nur auf der Ebene von Bits, Bytes und Maschinencode.



Das wichtigste ABI-Element, das uns interessiert, ist, wie seine Argumente an die Funktion übergeben werden und wie die Antwort von der Funktion zurückgegeben wird. Während der Standard-ABI x86-64 Prozessorregister verwendet, um Argumente und Antworten zu übergeben, verwendet der ABI Plan 9 hierfür einen Stapel.



Rob Pike und sein Team hatten nicht vor, einen weiteren Standard zu erstellen: Sie hatten bereits einen fast vorgefertigten C-Compiler für das Plan 9-System, so einfach wie zwei zu zwei, den sie schnell in einen Compiler für Go umwandelten. Engineering-Ansatz in Aktion.



Dies ist jedoch kein sehr kritisches Problem. Erstens werden wir vielleicht bald in Go sehen , wie Argumente durch Register geleitet werden , und zweitens ist es nicht schwierig, Argumente vom Stapel aus BPF zu erhalten: Der Alias ​​sargX wurde bereits zu bpftrace hinzugefügt , und derselbe wird höchstwahrscheinlich in naher Zukunft in BCC erscheinen ...



Update : Von dem Moment an, als ich den Bericht erstellt habe, erschien sogar ein detaillierter offizieller Vorschlag für den Übergang zur Verwendung von Registern im ABI.



Eindeutige Thread-ID



Das zweite Feature hat mit Go's Lieblingsfeature, den Goroutinen, zu tun. Eine Möglichkeit, die Latenz einer Funktion zu messen, besteht darin, die Zeit zu sparen, die zum Aufrufen der Funktion benötigt wird, Zeit zum Beenden der Funktion und zum Berechnen der Differenz. und speichern Sie die Startzeit mit einer Taste, die den Funktionsnamen und die TID (Thread-Nummer) enthält. Die Thread-Nummer wird benötigt, da dieselbe Funktion gleichzeitig von verschiedenen Programmen oder verschiedenen Threads desselben Programms aufgerufen werden kann.



In Go wechseln Goroutinen zwischen System-Threads: Jetzt wird eine Goroutine für einen Thread und etwas später für einen anderen ausgeführt. Und im Fall von Go würden wir nicht die TID in den Schlüssel eingeben, sondern die GID, dh die ID der Goroutine, aber wir können sie nicht erhalten. Technisch gesehen existiert diese ID. Sie können es sogar mit schmutzigen Hacks herausziehen, da es sich irgendwo auf dem Stapel befindet. Dies ist jedoch nach den Empfehlungen der wichtigsten Go-Entwicklungsgruppe strengstens untersagt. Sie hatten das Gefühl, dass wir solche Informationen niemals brauchen würden. Sowie Goroutine lokalen Speicher, aber ich schweife ab.



Den Stapel erweitern



Das dritte Problem ist das schwerwiegendste. So ernst, dass es uns in keiner Weise hilft, die Latenz von Go-Funktionen zu messen, selbst wenn wir das zweite Problem irgendwie lösen.



Wahrscheinlich verstehen die meisten Leser gut, was ein Stapel ist. Derselbe Stapel, in dem Sie im Gegensatz zum Heap oder Heap Speicher für Variablen zuweisen können, ohne daran zu denken, sie freizugeben.



Wenn wir über C sprechen, hat der Stapel dort eine feste Größe. Wenn wir über diese feste Größe hinausgehen, tritt der berühmte Stapelüberlauf auf .



In Go ist der Stapel dynamisch. In älteren Versionen wurden Speicherblöcke verkettet. Es ist jetzt ein durchgehender dynamischer Block. Das heißt, wenn das ausgewählte Stück für uns nicht ausreicht, werden wir das aktuelle Stück erweitern. Und wenn wir nicht erweitern können, wählen wir einen anderen größeren aus und verschieben alle Daten vom alten zum neuen Ort. Es ist eine verdammt faszinierende Geschichte, die Sicherheitsgarantien, cgo, Müllsammler berührt, aber das ist ein Thema für einen anderen Artikel.



Es ist wichtig zu wissen, dass Go, um den Stapel zu verschieben, den Aufrufstapel des Programms und alle Zeiger auf dem Stapel durchlaufen muss.



Hier liegt das Hauptproblem: Uretsonden, mit denen eine BPF-Funktion angehängt wird, ändern den Stapel am Ende der Funktionsausführung dynamisch, um einen Aufruf an ihren Handler, das sogenannte Trampolin, zu leiten. Und eine solche Änderung in seinem Stapel, die für Go unerwartet ist, endet in den meisten Fällen mit einem Programmabsturz. Hoppla!



Diese Geschichte ist jedoch nicht einzigartig. Der C ++ - Stack-Unraveller stürzt auch zum Zeitpunkt der Ausnahmebehandlung ab.



Es gibt keine Lösung für dieses Problem. Wie in solchen Fällen üblich, tauschen die Parteien absolut vernünftige Argumente für die Schuld des anderen aus.



Aber wenn Sie wirklich Harnsonde setzen müssen, kann das Problem umgangen werden. Wie? Setzen Sie keine Harnröhre ein. Wir können an allen Stellen, an denen wir die Funktion verlassen, einen Kleiderschrank aufstellen. Es kann einen solchen Ort geben, oder vielleicht 50.



Und hier spielt uns die Einzigartigkeit von Go in die Hände.



Normalerweise würde dieser Trick nicht funktionieren. Ein ausreichend intelligenter Compiler kann die sogenannte Tail-Call-Optimierung durchführen , wenn wir nicht von einer Funktion zurückkehren und entlang des Aufrufstapels zurückkehren, sondern einfach zum Anfang der nächsten Funktion springen. Diese Art der Optimierung ist für funktionale Sprachen wie Haskell von entscheidender Bedeutung . Ohne sie hätten sie ohne Stapelüberlauf keinen Schritt machen können. Aber mit einer solchen Optimierung können wir einfach nicht alle Stellen finden, an denen wir von der Funktion zurückkehren.



Die Besonderheit ist, dass der Go-Compiler Version 1.14 noch keine Tail-Call-Optimierung durchführen kann. Dies bedeutet, dass der Trick, an alle expliziten Exits einer Funktion anzuhängen, funktioniert, wenn auch sehr mühsam.



Beispiele von



Denken Sie nicht, dass BPF für Go nutzlos ist. Dies ist bei weitem nicht der Fall: Wir können alles andere tun, was die oben genannten Nuancen nicht beeinflusst. Und wir werden. 

Schauen wir uns einige Beispiele an.



Nehmen wir ein einfaches Programm zur Vorbereitung. Grundsätzlich handelt es sich um einen Webserver, der Port 8080 überwacht und über einen HTTP-Anforderungshandler verfügt. Der Handler erhält den Parameter name und den Parameter Go von der URL und überprüft die "Site". Anschließend sendet er alle drei Variablen (Name, Jahr und Prüfstatus) an die Funktion prepareAnswer (), die eine Antwort als Zeichenfolge vorbereitet.







Die Standortüberprüfung ist eine HTTP-Anforderung, die mithilfe einer Pipe und einer Goroutine überprüft, ob der Konferenzstandort aktiv ist. Und die Funktion zum Vorbereiten der Antwort verwandelt alles in eine lesbare Zeichenfolge.



Wir werden unser Programm mit einer einfachen Curl-Anfrage auslösen:







Als erstes Beispiel werden wir bpftrace verwenden, um alle Funktionsaufrufe unseres Programms zu drucken. Wir fügen hier alle Funktionen hinzu, die unter main fallen. In Go haben alle Ihre Funktionen ein Symbol, das dem Paketnamen-Punkt-Funktionsnamen ähnelt. Unser Paket ist main und die Funktionslaufzeit wäre Laufzeit.







Wenn ich mich locke, werden der Handler, die Site-Validierungsfunktion und die Goroutine-Unterfunktion gestartet und dann die Antwortvorbereitungsfunktion. Klasse!



Als nächstes möchte ich nicht nur anzeigen, welche Funktionen ausgeführt werden, sondern auch deren Argumente. Nehmen wir die Funktion prepareAnswer (). Sie hat drei Argumente. Versuchen wir, zwei int zu drucken.

Wir nehmen bpftrace, nur jetzt kein Einzeiler, sondern ein Skript. Wir hängen an unserer Funktion an und verwenden die Aliase für die von mir erwähnten Stapelargumente.



In der Ausgabe sehen wir, was wir im Jahr 2020 bestanden, den Status 200 erhalten und 2021 einmal bestanden haben.







Die Funktion hat jedoch drei Argumente. Der erste ist eine Zeichenfolge. Was ist mit ihm?



Drucken wir einfach alle Stapelargumente von 0 bis 4. Und was sehen wir? Eine große Figur, eine kleinere Figur und unsere alten 2021 und 200. Was sind diese seltsamen Zahlen am Anfang?







Hier ist es nützlich, das Go-Gerät zu kennen. Wenn in C eine Zeichenfolge nur ein nullterminiertes Array von Zeichen ist, dann ist in Go eine Zeichenfolge tatsächlich eine Struktur, die aus einem Zeiger auf ein Array von Zeichen (übrigens nicht nullterminiert) und Länge besteht.







Wenn der Go-Compiler eine Zeichenfolge als Argument übergibt, erweitert er diese Struktur und übergibt sie als zwei Argumente. Und es stellt sich heraus, dass die erste seltsame Ziffer nur ein Zeiger auf unser Array ist und die zweite die Länge.



Und die Wahrheit: Die erwartete Länge der Zeichenfolge beträgt 22.



Dementsprechend korrigieren wir unser Skript ein wenig, um diese beiden Werte durch den Zeigerregisterstapel und den korrekten Offset zu erhalten, und geben es mit der integrierten Funktion str () als Zeichenfolge aus. Alles funktioniert:







Schauen wir uns die Laufzeit an. Zum Beispiel wollte ich wissen, welche Goroutinen unser Programm startet. Ich weiß, dass Goroutinen durch die Funktionen newproc () und newproc1 () ausgelöst werden. Lassen Sie uns mit ihnen verbinden. Das erste Argument für die Funktion newproc1 () ist ein Zeiger auf die Funktionsstruktur, die nur ein Feld enthält - einen Funktionszeiger:







In diesem Fall nutzen wir die Möglichkeit, Strukturen direkt im Skript zu definieren. Es ist etwas einfacher als mit Offset-Sets zu spielen. Hier haben wir alle Goroutinen herausgebracht, die gestartet werden, wenn unser Handler aufgerufen wird. Und wenn wir danach die Namen der Symbole für unsere Offsets erhalten, sehen wir nur unter ihnen unsere checkSite-Funktion. Hurra!







Diese Beispiele sind ein Tropfen auf den heißen Stein der BPF-, BCC- und bpftrace-Funktionen. Mit den richtigen Kenntnissen der Interna und der Erfahrung können Sie fast alle Informationen aus einem laufenden Programm abrufen, ohne es zu stoppen oder zu ändern.



Fazit



Das ist alles, worüber ich dir erzählen wollte. Ich hoffe ich konnte dich inspirieren.



BPF ist einer der trendigsten und vielversprechendsten Trends unter Linux. Und ich bin sicher, dass wir in den kommenden Jahren nicht nur in der Technologie selbst, sondern auch in den Werkzeugen und deren Vertrieb viel interessantere Dinge sehen werden.



Bevor es zu spät ist und nicht jeder etwas über BPF weiß, spielen Sie damit, werden Sie Zauberer, lösen Sie Probleme und helfen Sie Ihren Kollegen. Sie sagen, dass Zaubertricks nur einmal funktionieren.



Was Go betrifft, waren wir wie immer ziemlich einzigartig. Wir haben immer einige Nuancen: Entweder ist der Compiler anders als der ABI, wir brauchen eine Art GOPATH, einen Namen, der nicht Google sein kann. Aber wir sind zu einer Kraft geworden, mit der man rechnen muss, und ich glaube, dass das Leben nur besser werden wird.



All Articles