Mit dem Parameter --format
können Sie das Dateiformat ändern, um Speedscope-Profile oder Rohdaten zu generieren. Informationen zu anderen Optionen finden Sie unter py-spy record --help
, einschließlich Ändern der Abtastrate, Filtern, um nur Threads einzubeziehen, die die GIL enthalten, Profilieren nativer C-Erweiterungen, Anzeigen von Thread-IDs, Profilieren von Unterprozessen und mehr.
Top zeigt eine Live-Ansicht, welche Funktionen in Ihrem Python-Programm die meiste Zeit beanspruchen, ähnlich dem Unix-Top-Befehl. Py-Spy ausführen mit:
py-spy top --pid 12345
# OR
py-spy top -- python myprogram.py
ruft eine Live-Update-Übersicht Ihres Python-Programms auf:
py-spy kann mit dem dump
-Befehl auch den aktuellen Aufrufstapel für jeden Python-Thread anzeigen:
py-spy dump --pid 12345
Dadurch werden die Aufrufstapel für jeden Thread und einige andere grundlegende Prozessinformationen an die Konsole ausgegeben:
Dies ist nützlich, wenn Sie nur einen einzigen Aufrufstapel benötigen, um herauszufinden, wo Ihr Python-Programm hängt. Dieser Befehl bietet auch die Möglichkeit, die jedem Stapelrahmen zugeordneten lokalen Variablen auszudrucken, indem das Flag --locals
gesetzt wird.
Ziel dieses Projekts ist es, Ihnen die Möglichkeit zu geben, jedes laufende Python-Programm zu profilieren und zu debuggen, selbst wenn das Programm Produktionsdatenverkehr bedient.
Zwar gibt es viele andere Python-Profiling-Projekte, aber bei fast allen ist es erforderlich, das Profilierungsprogramm auf irgendeine Weise zu modifizieren. Normalerweise wird der Profiling-Code innerhalb des Ziel-Python-Prozesses ausgeführt, was die Funktionsweise des Programms verlangsamt und verändert. Dies bedeutet, dass es im Allgemeinen nicht sicher ist, diese Profiler zum Debuggen von Problemen in Produktionsdiensten zu verwenden, da sie normalerweise spürbare Auswirkungen auf die Leistung haben.
py-spy liest den Speicher des Python-Programms direkt mithilfe des Systemaufrufs „process_vm_readv“ unter Linux, des Aufrufs „vm_read“ unter OSX oder des Aufrufs „ReadProcessMemory“ unter Windows.
Um den Aufrufstapel des Python-Programms herauszufinden, schauen Sie sich die globale Variable PyInterpreterState an, um alle Python-Threads im Interpreter auszuführen, und durchlaufen dann jedes PyFrameObject in jedem Thread, um den Aufrufstapel zu erhalten. Da sich die Python-ABI zwischen den Versionen ändert, verwenden wir das Bindgen von Rust, um unterschiedliche Rust-Strukturen für jede Python-Interpreterklasse zu generieren, die uns wichtig ist, und verwenden diese generierten Strukturen, um das Speicherlayout im Python-Programm herauszufinden.
Aufgrund der Randomisierung des Adressraumlayouts kann es etwas schwierig sein, die Speicheradresse des Python-Interpreters zu ermitteln. Wenn der Ziel-Python-Interpreter mit Symbolen ausgeliefert wird, ist es ziemlich einfach, die Speicheradresse des Interpreters herauszufinden, indem man je nach Python-Version die Variablen interp_head
oder _PyRuntime
dereferenziert. Allerdings werden viele Python-Versionen entweder mit entfernten Binärdateien oder ohne die entsprechenden PDB-Symboldateien unter Windows ausgeliefert. In diesen Fällen durchsuchen wir den BSS-Abschnitt nach Adressen, die auf einen gültigen PyInterpreterState verweisen könnten, und prüfen, ob das Layout dieser Adresse unseren Erwartungen entspricht.
Ja! py-spy unterstützt die Profilerstellung nativer Python-Erweiterungen, die in Sprachen wie C/C++ oder Cython geschrieben sind, unter x86_64 Linux und Windows. Sie können diesen Modus aktivieren, indem Sie --native
in der Befehlszeile übergeben. Um optimale Ergebnisse zu erzielen, sollten Sie Ihre Python-Erweiterung mit Symbolen kompilieren. Für Cython-Programme ist außerdem zu beachten, dass py-spy die generierte C- oder C++-Datei benötigt, um Zeilennummern der ursprünglichen .pyx-Datei zurückzugeben. Weitere Informationen finden Sie im Blogbeitrag.
Durch die Übergabe des Flags --subprocesses
an den Datensatz oder die Draufsicht schließt py-spy auch die Ausgabe jedes Python-Prozesses ein, der ein untergeordneter Prozess des Zielprogramms ist. Dies ist nützlich für die Profilerstellung von Anwendungen, die Multiprocessing- oder Gunicorn-Worker-Pools verwenden. py-spy überwacht, ob neue Prozesse erstellt werden, hängt diese automatisch an und fügt Beispiele davon in die Ausgabe ein. Die Datensatzansicht enthält die PID und die Befehlszeile jedes Programms im Aufrufstapel, wobei Unterprozesse als untergeordnete Prozesse ihrer übergeordneten Prozesse angezeigt werden.
py-spy liest Speicher aus einem anderen Python-Prozess. Dies ist je nach Betriebssystem und Systemeinstellungen aus Sicherheitsgründen möglicherweise nicht zulässig. In vielen Fällen werden diese Sicherheitsbeschränkungen durch die Ausführung als Root-Benutzer (mit sudo o.ä.) umgangen. OSX erfordert immer die Ausführung als Root, aber unter Linux hängt es davon ab, wie Sie py-spy starten und welche Systemsicherheitseinstellungen Sie haben.
Unter Linux besteht die Standardkonfiguration darin, Root-Berechtigungen zu erfordern, wenn eine Verbindung zu einem Prozess hergestellt wird, der kein untergeordneter Prozess ist. Für py-spy bedeutet das, dass Sie ein Profil ohne Root-Zugriff erstellen können, indem Sie py-spy dazu bringen, den Prozess zu erstellen ( py-spy record -- python myprogram.py
). Für das Anhängen an einen vorhandenen Prozess durch Angabe einer PID ist jedoch normalerweise Root erforderlich ( sudo py-spy record --pid 123456
). Sie können diese Einschränkung unter Linux entfernen, indem Sie die Sysctl-Variable ptrace_scope festlegen.
py-spy versucht, nur Stack-Traces von Threads einzubeziehen, die aktiv Code ausführen, und Threads auszuschließen, die schlafen oder anderweitig inaktiv sind. Wenn möglich, versucht py-spy, diese Thread-Aktivitätsinformationen vom Betriebssystem abzurufen: indem es /proc/PID/stat
unter Linux einliest, indem es den Aufruf „mach thread_basic_info“ unter OSX verwendet und indem es prüft, ob der aktuelle SysCall bekanntermaßen inaktiv ist unter Windows.
Bei diesem Ansatz gibt es jedoch einige Einschränkungen, die dazu führen können, dass inaktive Threads weiterhin als aktiv markiert werden. Zunächst müssen wir diese Thread-Aktivitätsinformationen abrufen, bevor wir das Programm anhalten, da das Erhalten dieser Informationen von einem angehaltenen Programm dazu führt, dass es immer zurückgibt, dass dieses inaktiv ist. Dies bedeutet, dass eine potenzielle Race-Bedingung vorliegt, bei der wir die Thread-Aktivität erhalten und sich der Thread dann in einem anderen Zustand befindet, wenn wir den Stack-Trace erhalten. Auch die Abfrage des Betriebssystems nach Thread-Aktivität ist für FreeBSD- und i686/ARM-Prozessoren unter Linux noch nicht implementiert. Unter Windows werden auch Aufrufe, die auf E/A blockiert sind, noch nicht als inaktiv markiert, beispielsweise beim Lesen von Eingaben von stdin. Schließlich kann der von uns verwendete Ptrace-Anhang bei einigen Linux-Aufrufen dazu führen, dass inaktive Threads kurzzeitig aktiviert werden, was beim Lesen aus procfs zu Fehlalarmen führt. Aus diesen Gründen haben wir auch einen heuristischen Fallback, der bestimmte bekannte Aufrufe in Python als inaktiv markiert.
Sie können diese Funktionalität deaktivieren, indem Sie das Flag --idle
setzen, das Frames einschließt, die py-spy als inaktiv betrachtet.
Wir erhalten GIL-Aktivität, indem wir uns den Thread-ID-Wert ansehen, auf den das _PyThreadState_Current
-Symbol für Python 3.6 und früher zeigt, und indem wir das Äquivalent aus der _PyRuntime
Struktur in Python 3.7 und höher ermitteln. Diese Symbole sind möglicherweise nicht in Ihrer Python-Distribution enthalten, was dazu führt, dass die Entscheidung, welcher Thread an der GIL festhält, fehlschlägt. Die aktuelle GIL-Nutzung wird auch in der top
als %GIL angezeigt.
Durch die Übergabe des Flags --gil
werden nur Traces für Threads einbezogen, die an der globalen Interpretersperre festhalten. In manchen Fällen ist dies möglicherweise ein genauerer Überblick darüber, wie Ihr Python-Programm seine Zeit verbringt. Sie sollten sich jedoch darüber im Klaren sein, dass dadurch Aktivitäten in Erweiterungen verpasst werden, die die GIL freigeben, während sie noch aktiv ist.
OSX verfügt über eine Funktion namens Systemintegritätsschutz, die selbst den Root-Benutzer daran hindert, Speicher aus einer Binärdatei zu lesen, die sich in /usr/bin befindet. Leider gehört dazu auch der Python-Interpreter, der mit OSX ausgeliefert wird.
Es gibt verschiedene Möglichkeiten, damit umzugehen:
Wenn Sie py-spy in einem Docker-Container ausführen, wird in der Regel auch der Fehler „Berechtigungen verweigert“ angezeigt, selbst wenn Sie als Root ausgeführt werden.
Dieser Fehler wird dadurch verursacht, dass Docker den von uns verwendeten Systemaufruf „process_vm_readv“ einschränkt. Dies kann überschrieben werden, indem beim Starten des Docker-Containers --cap-add SYS_PTRACE
festgelegt wird.
Alternativ können Sie die Docker-Compose-YAML-Datei bearbeiten
your_service:
cap_add:
- SYS_PTRACE
Beachten Sie, dass Sie den Docker-Container neu starten müssen, damit diese Einstellung wirksam wird.
Sie können py-spy auch vom Host-Betriebssystem aus verwenden, um ein Profil eines laufenden Prozesses zu erstellen, der im Docker-Container ausgeführt wird.
py-spy benötigt SYS_PTRACE
um den Prozessspeicher lesen zu können. Kubernetes löscht diese Funktion standardmäßig, was zu dem Fehler führt
Permission Denied: Try running again with elevated permissions by going 'sudo env "PATH=$PATH" !!'
Der empfohlene Weg, damit umzugehen, besteht darin, die Spezifikation zu bearbeiten und diese Funktion hinzuzufügen. Bei einer Bereitstellung erfolgt dies durch Hinzufügen zu Deployment.spec.template.spec.containers
securityContext:
capabilities:
add:
- SYS_PTRACE
Weitere Details dazu hier: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container Beachten Sie, dass dadurch die vorhandenen Pods entfernt und neu erstellt werden .
Alpine Python verzichtet auf die manylinux
-Räder: pypa/pip#3969 (Kommentar). Sie können dieses Verhalten überschreiben, um pip zur Installation von py-spy auf Alpine zu verwenden, indem Sie Folgendes tun:
echo 'manylinux1_compatible = True' > /usr/local/lib/python3.7/site-packages/_manylinux.py
Alternativ können Sie eine Musl-Binärdatei von der GitHub-Release-Seite herunterladen.
Durch Festlegen der Option --nonblocking
unterbricht py-spy den Ziel-Python, von dem aus Sie ein Profil erstellen, nicht. Während die Auswirkungen der Stichprobenentnahme aus einem Prozess mit Py-Spy normalerweise äußerst gering sind, wird durch die Einstellung dieser Option eine Unterbrechung Ihres laufenden Python-Programms vollständig vermieden.
Wenn diese Option festgelegt ist, liest py-spy stattdessen den Interpreterstatus aus dem laufenden Python-Prozess. Da die Aufrufe, die wir zum Auslesen des Speichers verwenden, nicht atomar sind und wir mehrere Aufrufe ausführen müssen, um einen Stack-Trace zu erhalten, bedeutet dies, dass beim Sampling gelegentlich Fehler auftreten. Dies kann sich in einer erhöhten Fehlerrate beim Sampling bemerkbar machen oder darin, dass Teilstapelrahmen in die Ausgabe einbezogen werden.
Noch nicht =).
Wenn es Funktionen gibt, die Sie in py-spy sehen möchten, markieren Sie entweder das entsprechende Problem oder erstellen Sie ein neues, in dem beschrieben wird, welche Funktionalität fehlt.
py-spy folgt der CLICOLOR-Spezifikation. Wenn Sie also CLICOLOR_FORCE=1
in Ihrer Umgebung festlegen, druckt py-spy farbige Ausgaben, selbst wenn es an einen Pager weitergeleitet wird.
py-spy ist stark von der hervorragenden Arbeit von Julia Evans an rbspy inspiriert. Insbesondere stammt der Code zum Generieren von Flamegraph- und Speedscope-Dateien direkt von rbspy, und dieses Projekt verwendet die Read-Process-Memory- und Proc-Maps-Kisten, die von rbspy abgespalten wurden.
py-spy wird unter der MIT-Lizenz veröffentlicht. Den vollständigen Text finden Sie in der LIZENZ-Datei.