Kapitel 1. Einführung
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
In den letzten Jahrzehnten sind die Computersysteme immer komplexer geworden. Das Wissen darüber, wie sich Software verhält, hat eine Vielzahl von Geschäftskategorien hervorgebracht, die alle versuchen, die Herausforderung zu lösen, Einblick in komplexe Systeme zu erhalten. Ein Ansatz, um diesen Einblick zu bekommen, ist die Analyse der Datenprotokolle, die von allen in einem Computersystem laufenden Anwendungen erzeugt werden. Logs sind eine großartige Informationsquelle. Sie können dir genaue Daten darüber liefern, wie sich eine Anwendung verhält. Allerdings schränken sie dich ein, weil du nur die Informationen erhältst, die die Ingenieure, die die Anwendung entwickelt haben, in diesen Protokollen veröffentlicht haben. Um zusätzliche Informationen im Log-Format von einem System zu erhalten, musst du das Programm dekompilieren und dir den Ausführungsfluss ansehen. Ein anderer beliebter Ansatz ist die Verwendung von Metriken, um herauszufinden, warum sich ein Programm so verhält, wie es sich verhält. Metriken unterscheiden sich von Protokollen durch das Datenformat. Während Protokolle explizite Daten liefern, fassen Metriken Daten zusammen, um zu messen, wie sich ein Programm zu einem bestimmten Zeitpunkt verhält.
Beobachtbarkeit ist eine neue Praxis, die sich diesem Problem aus einem anderen Blickwinkel nähert. Man definiert Beobachtbarkeit als die Fähigkeit, beliebige Fragen zu stellen und komplexe Antworten von einem bestimmten System zu erhalten. Ein wesentlicher Unterschied zwischen Beobachtbarkeit, Logs und Metrik-Aggregation sind die Daten, die du sammelst. Wenn du Beobachtbarkeit praktizierst, musst du jede beliebige Frage zu jedem Zeitpunkt beantworten können. Die einzige Möglichkeit, über Daten nachzudenken, besteht darin, alle Daten zu sammeln, die dein System erzeugen kann, und sie nur dann zu aggregieren, wenn es für die Beantwortung deiner Fragen notwendig ist.
Nassim Nicholas Taleb, der Autor von Bestsellern wie Antifragile: Things That Gain From Disorder (Penguin Random House), hat den Begriff "Schwarzer Schwan" für unerwartete Ereignisse mit schwerwiegenden Folgen populär gemacht, die zu erwarten gewesen wären, wenn man sie vorher beobachtet hätte. In seinem Buch The Black Swan (Penguin Random House) erklärt er, wie relevante Daten bei der Risikominderung für diese seltenen Ereignisse helfen können. Schwarze Schwäne kommen in der Softwareentwicklung häufiger vor, als wir denken, und sie sind unvermeidlich. Da wir davon ausgehen können, dass wir diese Art von Ereignissen nicht verhindern können, besteht unsere einzige Option darin, so viele Informationen wie möglich über sie zu haben, um sie anzugehen, ohne dass die Geschäftssysteme in kritischer Weise beeinträchtigt werden. Die Beobachtbarkeit hilft uns dabei, robuste Systeme aufzubauen und künftige Black Swan-Ereignisse abzumildern, denn sie basiert auf der Prämisse, dass du alle Daten sammelst, die eine Antwort auf eine künftige Frage geben können. Die Untersuchung von Black Swan-Ereignissen und die Praxis der Beobachtbarkeit laufen in einem zentralen Punkt zusammen, nämlich in den Daten, die du von deinen Systemen sammelst.
Linux-Container sind eine Abstraktion, die auf einer Reihe von Funktionen des Linux-Kernels aufbaut, um Computerprozesse zu isolieren und zu verwalten. Der Kernel, der traditionell für die Ressourcenverwaltung zuständig ist, sorgt auch für die Isolierung von Aufgaben und die Sicherheit. Die wichtigsten Funktionen, auf denen Container in Linux basieren, sind Namensräume und cgroups. Namensräume sind die Komponenten, die Aufgaben voneinander isolieren. Wenn du dich in einem Namensraum befindest, erlebst du das Betriebssystem in gewisser Weise so, als gäbe es keine anderen Aufgaben, die auf dem Computer laufen. Cgroups sind die Komponenten, die für die Ressourcenverwaltung zuständig sind. Aus betrieblicher Sicht geben sie dir eine fein abgestufte Kontrolle über die Ressourcennutzung, z. B. CPU, Festplatten-E/A, Netzwerk usw. In den letzten zehn Jahren hat sich mit der zunehmenden Beliebtheit von Linux-Containern die Art und Weise, wie Softwareentwickler große verteilte Systeme und Rechenplattformen entwerfen, verändert. Multitenant Computing ist inzwischen vollständig auf diese Funktionen im Kernel angewiesen.
Indem wir uns so sehr auf die Low-Level-Fähigkeiten des Linux-Kernels verlassen, haben wir eine neue Quelle von Komplexität und Informationen erschlossen, die wir bei der Entwicklung beobachtbarer Systeme berücksichtigen müssen. Der Kernel ist ein ereignisgesteuertes System, was bedeutet, dass alle Arbeiten auf der Grundlage von Ereignissen beschrieben und ausgeführt werden. Das Öffnen von Dateien ist eine Art Ereignis, die Ausführung eines beliebigen Befehls durch eine CPU ist ein Ereignis, der Empfang eines Netzwerkpakets ist ein Ereignis und so weiter. Berkeley Packet Filter (BPF) ist ein Subsystem im Kernel, das diese neuen Informationsquellen untersuchen kann. BPF ermöglicht es dir, Programme zu schreiben, die sicher ausgeführt werden, wenn der Kernel ein Ereignis auslöst. BPF gibt dir starke Sicherheitsgarantien, die verhindern, dass du Systemabstürze und bösartiges Verhalten in diese Programme einbaust. Die BPF ermöglicht eine neue Welle von Tools, die Systementwicklern helfen, diese neuen Plattformen zu beobachten und mit ihnen zu arbeiten.
In diesem Buch zeigen wir dir die Möglichkeiten, die dir die BPF bietet, um jedes Computersystem besser beobachten zu können. Wir zeigen dir auch, wie du BPF-Programme mit Hilfe verschiedener Programmiersprachen schreibst. Wir haben den Code für deine Programme auf GitHub gestellt, damit du ihn nicht kopieren und einfügen musst. Du findest ihn in einem Git-Repository, das zu diesem Buch gehört.
Doch bevor wir uns den technischen Aspekten der BPF widmen, wollen wir uns ansehen, wie alles begann.
Die Geschichte der BPF
Im Jahr 1992 schrieben Steven McCanne und Van Jacobson das Papier "The BSD Packet Filter: A New Architecture for User-Level Packet Capture". Darin beschreiben die Autoren, wie sie einen Netzwerk-Paketfilter für den Unix-Kernel implementiert haben, der 20 Mal schneller war als der damalige Stand der Technik bei der Paketfilterung. Paketfilter haben einen bestimmten Zweck: Sie sollen Anwendungen, die das Netzwerk des Systems überwachen, mit direkten Informationen aus dem Kernel versorgen. Mit diesen Informationen konnten die Anwendungen entscheiden, was sie mit den Paketen machen wollten. Die BPF führte zwei große Innovationen in der Paketfilterung ein:
-
Eine neue virtuelle Maschine (VM), die für die effiziente Arbeit mit registerbasierten CPUs entwickelt wurde.
-
Die Verwendung von anwendungsspezifischen Puffern, die Pakete filtern können, ohne alle Paketinformationen zu kopieren. Dadurch wurde die Datenmenge, die die BPF benötigt, um Entscheidungen zu treffen, minimiert.
Diese drastischen Verbesserungen führten dazu, dass alle Unix-Systeme die BPF als Technologie der Wahl für die Filterung von Netzwerkpaketen übernahmen und alte Implementierungen, die mehr Speicher verbrauchten und weniger leistungsfähig waren, ablösten. Diese Implementierung ist immer noch in vielen Derivaten des Unix-Kernels enthalten, auch im Linux-Kernel.
Anfang 2014 stellte Alexei Starovoitov die erweiterte BPF-Implementierung vor. Dieses neue Design wurde für moderne Hardware optimiert, sodass der daraus resultierende Befehlssatz schneller ist als der vom alten BPF-Interpreter erzeugte Maschinencode. Diese erweiterte Version erhöhte auch die Anzahl der Register in der BPF VM von zwei 32-Bit-Registern auf zehn 64-Bit-Register. Die Erhöhung der Anzahl der Register und ihrer Breite eröffnete die Möglichkeit, komplexere Programme zu schreiben, da die Entwickler mehr Informationen über Funktionsparameter austauschen konnten. Neben anderen Verbesserungen machten diese Änderungen die erweiterte BPF-Version bis zu viermal schneller als die ursprüngliche BPF-Implementierung.
Das ursprüngliche Ziel dieser neuen Implementierung war die Optimierung des internen BPF-Befehlssatzes, der Netzwerkfilter verarbeitet. Zu diesem Zeitpunkt war die BPF noch auf den Kernel-Space beschränkt und nur wenige Programme im User-Space konnten BPF-Filter schreiben, die der Kernel verarbeiten konnte, wie Tcpdump und Seccomp, über die wir in späteren Kapiteln sprechen. Heute erzeugen diese Programme immer noch Bytecode für den alten BPF-Interpreter, aber der Kernel übersetzt diese Anweisungen in die viel bessere interne Darstellung.
Im Juni 2014 wurde die erweiterte Version der BPF für den Nutzerraum freigegeben. Dies war ein Wendepunkt für die Zukunft der BPF. Alexej schrieb in dem Patch, mit dem diese Änderungen eingeführt wurden: "Dieses Patch-Set demonstriert das Potenzial der eBPF."
Die BPF wurde zu einem Kernel-Subsystem der obersten Ebene und beschränkte sich nicht mehr auf den Netzwerk-Stack. Die BPF-Programme ähnelten immer mehr den Kernel-Modulen und legten großen Wert auf Sicherheit und Stabilität. Im Gegensatz zu Kernelmodulen musst du bei BPF-Programmen deinen Kernel nicht neu kompilieren und sie werden garantiert ohne Absturz fertiggestellt.
Der BPF Verifier, über den wir im nächsten Kapitel sprechen, hat diese erforderlichen Sicherheitsgarantien hinzugefügt. Er stellt sicher, dass jedes BPF-Programm abgeschlossen wird, ohne abzustürzen, und er sorgt dafür, dass Programme nicht versuchen, auf Speicher außerhalb des Bereichs zuzugreifen. Diese Vorteile sind jedoch mit gewissen Einschränkungen verbunden: Programme dürfen eine bestimmte Größe nicht überschreiten und Schleifen müssen begrenzt werden, um sicherzustellen, dass der Speicher des Systems nicht durch ein schlechtes BPF-Programm aufgebraucht wird.
Mit den Änderungen, die den Zugriff auf die BPF aus dem User-Space ermöglichen, haben die Kernel-Entwickler auch einen neuen Systemaufruf (Syscall) hinzugefügt: bpf
. Dieser neue Syscall ist das Herzstück der Kommunikation zwischen User-Space und Kernel. Wie du diesen Syscall für die Arbeit mit BPF-Programmen und -Maps nutzen kannst, wird in den Kapiteln 2 und 3 dieses Buches beschrieben.
BPF-Maps werden der Hauptmechanismus für den Datenaustausch zwischen dem Kernel und dem User-Space sein. In Kapitel 2 wird gezeigt, wie diese speziellen Strukturen verwendet werden, um Informationen vom Kernel zu sammeln und Informationen an BPF-Programme zu senden, die bereits im Kernel laufen.
Die erweiterte BPF-Version ist der Ausgangspunkt für dieses Buch. In den letzten fünf Jahren hat sich die BPF seit der Einführung dieser erweiterten Version erheblich weiterentwickelt. Wir behandeln im Detail die Entwicklung der BPF-Programme, der BPF-Maps und der Kernel-Subsysteme, die von dieser Entwicklung betroffen sind.
Architektur
Die Architektur der BPF innerhalb des Kernels ist faszinierend. Wir werden uns im ganzen Buch mit den Details beschäftigen, aber in diesem Kapitel wollen wir dir einen kurzen Überblick über ihre Funktionsweise geben.
Wie wir bereits erwähnt haben, ist die BPF eine hochentwickelte VM, die Code-Anweisungen in einer isolierten Umgebung ausführt. In gewisser Weise kannst du dir die BPF wie die Java Virtual Machine (JVM) vorstellen, ein spezialisiertes Programm, das Maschinencode ausführt, der aus einer höheren Programmiersprache kompiliert wurde. Compiler wie LLVM und in naher Zukunft auch die GNU Compiler Collection (GCC) unterstützen die BPF und ermöglichen es dir, C-Code in BPF-Anweisungen zu kompilieren. Nachdem dein Code kompiliert wurde, stellt die BPF mit Hilfe eines Verifiers sicher, dass das Programm vom Kernel sicher ausgeführt werden kann. Sie verhindert, dass du Code ausführst, der dein System gefährden könnte, indem er den Kernel zum Absturz bringt. Wenn dein Code sicher ist, wird das BPF-Programm in den Kernel geladen. Der Linux-Kernel verfügt außerdem über einen Just-in-Time-Compiler (JIT) für BPF-Anweisungen. Der JIT wandelt den BPF-Bytecode direkt nach der Überprüfung des Programms in Maschinencode um und vermeidet so diesen Overhead bei der Ausführungszeit. Ein interessanter Aspekt dieser Architektur ist, dass du dein System nicht neu starten musst, um BPF-Programme zu laden; du kannst sie bei Bedarf laden und du kannst auch deine eigenen Init-Skripte schreiben, die BPF-Programme laden, wenn dein System startet.
Bevor der Kernel ein BPF-Programm ausführt, muss er wissen, an welchen Ausführungspunkt das Programm angehängt ist. Es gibt mehrere Anknüpfungspunkte im Kernel, und die Liste wird immer länger. Die Ausführungspunkte werden durch die BPF-Programmtypen definiert; wir besprechen sie im nächsten Kapitel. Wenn du einen Ausführungspunkt auswählst, stellt der Kernel auch bestimmte Funktionshilfen zur Verfügung, die du verwenden kannst, um mit den Daten zu arbeiten, die dein Programm empfängt, wodurch Ausführungspunkte und BPF-Programme eng miteinander verbunden sind.
Die letzte Komponente der BPF-Architektur ist für den Datenaustausch zwischen dem Kernel und dem User-Space zuständig. Diese Komponente wird BPF-Map genannt, und wir sprechen in Kapitel 3 über Maps. BPF-Maps sind bidirektionale Strukturen zur gemeinsamen Nutzung von Daten. Das bedeutet, dass du sie von beiden Seiten, dem Kernel und dem User-Space, schreiben und lesen kannst. Es gibt verschiedene Arten von Strukturen, von einfachen Arrays und Hash Maps bis hin zu speziellen Maps, in denen du ganze BPF-Programme speichern kannst.
Im weiteren Verlauf des Buches gehen wir auf jede Komponente der BPF-Architektur näher ein. Außerdem lernst du, wie du die Erweiterbarkeit der BPF und die gemeinsame Nutzung von Daten nutzen kannst. Die Beispiele reichen von der Stack-Trace-Analyse bis hin zur Netzwerkfilterung und Laufzeitisolierung.
Fazit
Wir haben dieses Buch geschrieben, um dich mit den grundlegenden BPF-Konzepten vertraut zu machen, die du bei deiner täglichen Arbeit mit diesem Linux-Subsystem brauchen wirst. Die BPF ist eine Technologie, die sich noch in der Entwicklung befindet, und neue Konzepte und Paradigmen wachsen, während wir dieses Buch schreiben. Idealerweise hilft dir dieses Buch dabei, dein Wissen zu erweitern, indem es dir eine solide Grundlage für die grundlegenden Komponenten der BPF bietet.
Das nächste Kapitel befasst sich direkt mit der Struktur von BPF-Programmen und wie der Kernel sie ausführt. Es behandelt auch die Punkte im Kernel, an denen du diese Programme anhängen kannst. So lernst du alle Daten kennen, die deine Programme nutzen können und wie du sie verwendest.
Get Linux Observabilität mit BPF now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.