Aufbau eines Abhängigkeitsgraphen unserer Python-Codebasis
Einleitung
Im Einklang mit unseren Wurzeln im Hochfrequenzhandel bewegt sich Hudson River Trading (HRT) schnell. Wie bei jeder Kennzahl im Ingenieurwesen hat Geschwindigkeit ihre Nachteile. In den letzten fünf Jahren verzeichnete HRT ein exponentielles Wachstum der Größe und Vernetzung seiner forschungsorientierten Python-Codebasis, bedingt durch eine Kombination aus einer pragmatischen Ingenieurskultur, die generell „gut genug“ über „perfekt“ stellt, unserer kollaborativen Arbeitsumgebung, die den Codeaustausch zwischen Teams fördert, und einer Phase beschleunigten Wachstums. Als unsere Python-Codebasis auf Millionen von Zeilen anwuchs, erhöhten sich die Importzeiten um eine Größenordnung, Codeänderungen wurden teurer zu testen und die Lint-Zeiten stiegen weit außerhalb des nützlichen Bereichs an – wir erlebten die Auswirkungen von Code-"Verhedderung".
Verhedderung
Code-"Verhedderung" ist ein Konzept, das HRTler aus Beschreibungen desselben Problems in Dropbox-Publikationen über ihre eigene Python-Codebasis übernommen haben. Wir nennen Code "verheddert", wenn sein Abhängigkeitsgraph viele überlappende Zyklen aufweist und nicht verwandte Teile der Codebasis durch indirekte und unintuitive Importpfade gekoppelt sind. Verhedderung kann in jeder großen Codebasis (auch in anderen Sprachen) ein Problem darstellen!
Unserer Erfahrung nach beeinträchtigt die Verhedderung die Leistung sowohl von Laufzeitimporten als auch von statischer Analyse (z. B. mypy) und verursacht eine enge Kopplung, die die Zuverlässigkeit verringern kann. Von diesen Problemen ist die Laufzeit-Import-Überlastung für unsere Benutzer das größte Problem, da sie die Entwicklungs-Iterationsschleife verlangsamt und CPU-Zeit im Rechenzentrum verschwendet. Dies ist für HRT wahrscheinlich problematischer als für die meisten anderen Python-Shops, da kurzlebige Python-Prozesse einen beträchtlichen Teil unserer Rechenlast ausmachen.
Die negativen Auswirkungen der Verhedderung können sich schnell verstärken – ein paar falsch platzierte Importe und plötzlich sind Hunderte von Modulen miteinander gekoppelt. Die Auswirkungen der Import-Überlastung werden durch Verhedderung verstärkt, da der Import eines Moduls in einem Zyklus letztendlich alle Module in diesem Zyklus (und ihre Abhängigkeiten) transitiv importiert.
Während einige Importe sehr schnell sind, gibt es viele Fälle, die erhebliche Kosten verursachen. Eine häufige Ursache für Überlastung ist der Zugriff auf das Dateisystem – zum Beispiel durchsucht das nun veraltete `pkg_resources`-Modul das Dateisystem, um Ressourcen zu finden. Dieser Prozess wird besonders problematisch, wenn er über unser Network File System läuft. Eine weitere Quelle für Rechenaufwand ist das Laden sperriger, monolithischer C-Erweiterungen durch Pakete wie pandas und NumPy – und sogar proprietärer Erweiterungen. Zusätzlich verursachen einige unserer reinen Python-Module eine Reihe kostspieliger statischer Initialisierungsschritte, wie z. B. die Erkennung von Umgebungsmerkmalen oder die Handhabung der dynamischen Registrierung von Klassen oder Rückrufen.
Für sich genommen führt jede dieser Maßnahmen zu einer überschaubaren Importlast; in den am stärksten verhedderten Teilen unserer Codebasis kann der kumulative Effekt jedoch über 30 Sekunden Importzeit für die meisten Programme betragen. Diese Überlastung verlangsamt die Entwicklungs-Iterationsschleife und verschwendet CPU-Zeit in unserer verteilten Computerumgebung.
Abhängigkeitsmanagement
Auf hoher Ebene besteht unser Ansatz zur Entwirrung darin, eine geschichtete Architektur zu etablieren und aufrechtzuerhalten, bei der Module in niedrigeren Schichten nicht aus Modulen in höheren Schichten importieren. Die Einrichtung einer ordnungsgemäßen Schichtung hilft den Aufrufern, nur das zu importieren, was sie benötigen.
Idealerweise sollte unser Abhängigkeitsgraph einem gerichteten azyklischen Graphen ähneln, bei dem Module topologisch nach ihrer zugewiesenen Schicht geordnet sind. In der Praxis sind jedoch einige Zyklen akzeptabel, solange sie relativ klein und auf ein (Unter-)Paket beschränkt sind.
Der Übergang zu einem besseren Paradigma für das Abhängigkeitsmanagement erfordert die Identifizierung der aktuellen Ursachen für Verhedderung, die Refaktorierung der Codebasis zur Umstrukturierung von Abhängigkeiten und die Implementierung von Abhängigkeitsvalidierung zur Vermeidung zukünftiger Regressionen. Und all diese Arbeit muss getan werden, ohne die Entwicklung der Codebasis zu unterbrechen!
Verhedderungs-Tools: Verstehen der Verhedderung
Als wir erkannten, dass Verhedderung vielen unserer Probleme mit der Entwicklererfahrung zugrunde liegt, machten wir uns daran, ein Toolkit zur Analyse des Abhängigkeitsgraphen unserer Codebasis zu entwickeln – Tangle Tools. Tangle Tools analysiert Python-Quellcode, um einen Abhängigkeitsgraphen der gesamten Codebasis zu erstellen (Knoten entsprechen Modulen, Kanten Imports). Unsere Benutzer können dann die Befehlszeilen- und Browser-Schnittstellen nutzen, um Abhängigkeiten zu entdecken, zu navigieren und zu refaktorieren.
Ein typischer Workflow zur Entwirrung umfasst
-
Finden einer unerwünschten transitiven Abhängigkeit
-
Nachverfolgung von Importpfaden von einer Quelle zur unerwünschten Abhängigkeit
-
Berechnet ein Flussnetzwerk zwischen Quelle und Abhängigkeit
-
Identifiziert, welche Kanten den Fluss bei Entfernung reduzieren würden
-
Refaktorierung von Importen, um die Quelle von der Abhängigkeit zu trennen
-
Nutzt Code-Transformationen zur Automatisierung gängiger Refactorings (z. B. Verschieben eines Symbols in ein neues Modul und Aktualisieren bestehender Referenzen)
-
Implementierung von Abhängigkeitsvalidierung zur Vermeidung von Regressionen
-
Benutzer schreiben Abhängigkeitsregeln, um die Abhängigkeiten ihrer Module einzuschränken
-
Diese Regeln werden in unserer Continuous-Integration-Pipeline überprüft

Nichts davon wäre ohne umfangreiche Nutzung von Open-Source-Bibliotheken möglich gewesen! Wir haben die integrierte `ast`-Bibliothek von Python verwendet, um unseren Python-Quellcode auf Importe zu parsen. Diese Parsing-Arbeit wurde über das leistungsstarke `concurrent.futures`-Modul der Standardbibliothek parallelisiert, sodass wir Tausende von Modulen schnell verarbeiten konnten. Unter der Haube verwenden wir die gerichtete Graph-Datenstruktur von NetworkX und seine umfangreiche Bibliothek von Graphenalgorithmen – wir fanden die Flussalgorithmen besonders nützlich. Schließlich verwenden wir die `libcst`-Bibliothek, um automatisierte Quellcode-Refactorings durchzuführen, die als Transformationen des konkreten Syntaxbaums geschrieben sind.
Schlussfolgerung
Durch die Entwicklung dieser Workflows für Abhängigkeitsmanagement und Refactoring konnten wir erhebliche Fortschritte bei der Entwirrung erzielen. Zuvor war das Auffinden von Importabhängigkeiten ein langsamer manueller Prozess, und das Refaktorisieren von Abhängigkeiten war ein "Whack-a-Mole"-Spiel. Jetzt können wir unseren vollständigen Abhängigkeitsgraphen durchsuchen und effiziente Refactorings finden, die die Grundursachen der Verhedderung beheben.
Lernen Sie die Autoren kennen
George Farcasiu - Kernentwickler
- George Farcasiu hat an einer Reihe von Projekten im Python-Ökosystem von HRT gearbeitet, als Schöpfer von Python-Tools für statische Analyse und Abhängigkeitsmanagement, als Mitwirkender am verteilten Computing-Framework und der -Umgebung sowie als Betreuer von Build-/Test-/Continuous-Integration-Entwicklertools.
Noah Kim - Kernentwickler
- Noah Kim konzentriert sich hauptsächlich auf die Nutzung des CPython-Interpreters durch HRT. Er ist außerdem der aktuelle Betreuer von Tangle Tools, das Teil einer umfassenderen Anstrengung zur Verbesserung des Python-Paket-Ökosystems des Unternehmens ist.
Jacob Brugh - Kernentwickler
- Jacob Brughs jüngste Tätigkeitsbereiche umfassen die Verbesserung der Leistung von HRTs Python-Binding-Code für unsere C++-Trading-Bibliotheken und die Entwicklung interner Tools, die es uns ermöglichen, effiziente statische Analysen im großen Maßstab durchzuführen.
Jiahao Li - Kernentwickler
- Jiahao Li arbeitet an verschiedenen Projekten, die den verteilten Computing-Cluster und die Build-/Testplattform betreffen. Zu den jüngsten Projekten gehört die Überarbeitung der Testumgebung von HRT, um eine Abhängigkeitsverfolgung und eine intelligentere Testauswahl zu ermöglichen.
Dieser Artikel erschien ursprünglich im HRT Beat.
