CONAN CPUs

Buildsysteme wie CMake, XMake oder CONAN sorgen unter anderem auch dafür, dass die Builds so schnell wie möglich durchlaufen.
Und auf unseren heutigen Multikern-CPUs bedeutet das, dass sie so viele CPU Kerne wie möglich parallel einsetzen.

Problematisch wird es nur, wenn das eben “zu viel” ist.


Neulich in der Firma …

Plötzlich schlägt eine Vielzahl an Builds fehl. Der MSVC meldet

Internal compiler errors

und der GCC schreibt:

Out of memory: Kill process (cc1plus)

auf die Konsole.

Die Meldung kannte ich, sie deutete darauf hin, dass ein Compilerprozess vom System terminiert wurde und dahinter steckt oft fehlender RAM (bzw. fehlender virtueller Speicher im Allgemeinen).

Bei der Meldung des Problems an die Buildserver Admins kam zurück, dass wir zu viele Kerne parallel nutzen und das herunter limitieren sollen.

Und das hat mir dann die Woche versaut …

Rechenformeln

Für mich zählte die alte Formeln: RAM = CPU-cores * 2GB
(Das absolute Minimum wäre RAM = CPU-cores * 1GB) Wird sie eingehalten, kann jeder Kern einen Compiler-Prozess erhalten und parallel daran werkeln.

Beim Raspberry PI 2 mit 4 Kernen und 1 GB wird die Regel verletzt, weshalb mir dort öfter der Build abbricht. Vor allem wenn Monster wie BOOST oder SQLite gebaut werden sollen.

Von einem offiziellen Buildserver in der Cloud erwarte ich allerdings, dass diese einfachen Regeln eingehalten werden, denn unser benutztes CONAN ermittelt die Anzahl der CPUs und startet dann eben genau so viele Prozesse wie möglich.

Nun wurde uns aber mitgeteilt, dass im Hintergrund Kubernetes für die Ressourcenverwaltung eingesetzt wird und dieses unsere Dockersitzungen verwaltet. Hier wird dann im virtualisierten Container die reale Anzahl an Kernen des Hosts ausgegeben, nur der nutzbare RAM wurde auf 16 GB reduziert. Das bedeutet laut Formel, dass man nur 8 Kerne und damit 8 Compilerjobs parallel nutzen sollte, CONAN ermittelte jedoch 32 Kerne und überforderte somit das System.

Workarounds als Lösung

Nachdem mir mitgeteilt wurde, dass es nicht möglich sei die aufgezählten CPU-Kerne im virtualisierten OS herunterzusetzen, blieb mir nur der Ausweg eine Methode zu finden, wie man CONAN dazu bringt weniger parallele Jobs zu starten.

Zum Glück wurde mit CONAN_CPU_COUNT bereits eine Umgebungsvariable eingeführt, mit der man exakt definieren kann, wie viele CPUs und damit parallele Jobs ausgeführt werden sollen.

Alternativ kann man auch in conanfile.py beeinflussen, damit nicht automatisch ein -j N an CMake geschickt wird, und anschließend lässt sich ein zusätzlicher Kommandozeilenparameter an CMake senden, der die gewünschte Anzahl an Jobs erzeugt.
Das sieht dann etwa so aus:

1def build(self):
2  job_count = 4
3  cmake = CMake(self)
4  cmake.configure()
5  cmake.parallel = False
6  cmake.build(args=["-j", str(job_count)])

Hartkodierte Limits sind falsch!

Jetzt kann man zwar mit CONAN_CPU_COUNT=8 das Limit hart setzen, womit der Build auf genau diesem Server läuft. Soll der Build aber auch auf anderen Servern laufen, kommt es dort sofort zu Problemen, wenn dieser mit weniger CPUs und RAM ausgestattet ist. Und genau das passierte auch bei uns.

Am Ende wurde eine neue Variable auf den Buildserven etabliert, die die maximal nutzbare Anzahl an CPUs definiert. Die Idee war, dass wir diese Variable dann im CI-Script an CONAN_CPU_COUNT zuweisen.
Auch dieser Schritt führte wieder ein Problem ein, nämlich dass CONAN den Dienst verweigerte, wenn diese externe Variable nicht definiert war. Dann wurde nämlich CONAN_CPU_COUNT auf einen Leerstring gesetzt, was zum Abbruch wegen einer Fehlkonfiguration führte.

Und der wahre Hintergrund war:

Tatsächlich war das Problem auf die Umstellung von CGROUPS v1 auf CGROUPS v2 zurückzuführen, die auf den Kubernetes Servern durchgeführt wurde.
Und wir nutzen die CONAN Version 1.50, welche nur CGROUPS v1 unterstützt.

Das Geheimnis liegt in cpu.py, welches bis v1.50 die beiden Pfade
/sys/fs/cgroup/cpu/cpu.cfs_quota_us und
/sys/fs/cgroup/cpu/cpu.cfs_period_us auslas und das CPU-Limit daraus errechnete, was nur unter CGROUPS v1 funktioniert.

Neuere Versionen von CONAN testen zuerst die CGROUPS v2 Pfade
/sys/fs/cgroup/cgroup.controllers und
/sys/fs/cgroup/cpu.max bevor sie auf die beiden V1 Pfade zurückgreifen.

Tja … nur leider lässt sich das Build-Image erst wieder mit dem nächsten größeren Software-Release auf CONAN 1.51+ upgraden.

Somit bleibt leider vorerst nur der hässliche Workaround mit CONAN_CPU_COUNT.

Fazit

Ich kotz’ gleich im Strahl …

Buildsysteme sollen ihre Umgebung korrekt erkennen können und sich darauf best-möglich einrichten. Und Hostsysteme sollen ihren Containern von außen nur die Ressourcen anzeigen, die sie nutzen dürfen.

Ich verstehe nicht, warum mit CGROUPS v2 die Kompatibilität der sysfs Pfade gebrochen wird, und ich verstehe auch nicht, warum Docker überhaupt erst die reale CPU-Anzahl an Prozesse im Container zurückliefert.

In jedem Fall hat diese unnötige Panne unzählige Stunden gekostet und wird in Zukunft noch weitere Zeit einnehmen.

Ich für meinen Teil habe daraus allerdings gelernt, dass Docker ganz unschöne Seiteneffekte produzieren kann, denn auch meine Codes würden nicht im sysfs nach Dateien suchen, wenn sie die CPU-Anzahl wissen wollen.

Und ganz nebenbei:

Auf meinem Azure Buildserver läuft eine VM mit einer CPU und 512 MB RAM. Eigentlich würden darauf alle Builds fehlschlagen, doch ich habe dort den Swapspace aktiviert. Und schon führt die Verletzung der obigen Formel nicht mehr zum Crash, es dauert nur länger, wenn die Platte rattert.

Soll heißen: Man kann die CPU/RAM/Job Überlastung auch noch anders abfangen.