Document ID: /fineweb-2-swissfilter-quality_10-filterrobots/filtered/03594.jsonl.gz/949

This article was first published december 24, 2020 on Medium.com in english.
Vor einigen Tagen musste ich ein Shell Script schreiben, welches 5637 Queries generiert und diese in ein einziges File quetscht. Das Ziel war, diese Queries gegen eine Datenbank laufen zu lassen und Unterschiede zu entdecken was sein sollte und was wirklich war. Aber in diesem Artikel geht es nicht um Datenbankqueries, sondern um die schiere Mächtigkeit der Bash und weshalb es manchmal Sinn macht innezuhalten und Neues auszuprobieren.
Die Situation
Der Input stammt aus einem File mit Wertepaaren. 5637 Wertepaare um genau zu sein. Diese sehen so aus:
Value1.XYZ Value2.XYZ
Das erste Feld enthält den aktuellen Wert der in der DB gespeichert ist, das zweite Feld den neuen Wert. Also kreeirte ich ein Query Template, welches in einer Schlaufe welche sämtliche Wertepaare einliest, verwendet wird und die Muster “==VALUE1==” und “==VALUE2==” mit Value1.XYZ und Value2.XYZ ersetzt:
[==TITLE==] ... ROOT = DB/DO_SOMETHING(NEW_VALUE_NAME="==VALUE2==") ... IMPORTANT_FIELD = DB_NAME::==VALUE1== 0 ...
Das Template ist einiges grössser, aber aus Diskretion und der Übersicht zuliebe, habe ich die meisten Linien hier weggelassen. Und dann gibt es zuoberst noch das Feld “==TITLE==”, welches unique sein muss und aus einer Kombination von “Value1Value2” besteht. Das finale Query File besteht aus 90205 Zeilen.
Erster Versuch
Weil es sich nur um eine Fehlerbehebung handelte, habe ich ehrlicherweise nicht wirklich über die Performance des Loops nachgedacht. Weil ich ein Template und einen Loop benutze, war das sed für mich von Beginn weg das Werkzeug der Wahl und das Resultat sah so aus:
template=template.tpl workcopy=workcopy.tmp output=query.out while read -r line do array=(${line}) value1=${array[0]} value2=${array[1]} cp ${template} ${workcopy} sed -i s/==TITLE==/${value1}${value2}/g ${workcopy} sed -i s/==VALUE1==/${value1}/g ${workcopy} sed -i s/==VALUE2==/${value2}/g ${workcopy} cat ${workcopy} >> ${output} done < inputfile.txt
Nach einem Testlauf mit time erhielt ich folgende Messung
real 22m52.692s user 0m33.331s sys 1m47.344s
Wow, das dauerte lange. Fast 23 Minuten. Dieses Script wird für alle 20 DBs laufen, das heisst ich werde viel Zeit mit Warten verbringen. Ich entschloss mich das Script zu verbessern.
Zweiter Versuch
Ich verstand, dass ich in meinem ersten Versuch 5637 mal das Template ins Memory kopierte, dann wieder ins Filesystem schrieb, sed musste es von dort wieder lesen, zurück ins Memory kopieren, Änderungen berechnen, zurückschreiben, das ganze drei mal und dann musst die Workcopy nochmals vom Filesystem gelesen werden, ins Memory kopiert werden, an das Output File angehängt werden (welches ebenfalls vom Filesystem gelesen und ins Memory geladen werden muss) und dann wieder zurück ins Filesystem geschrieben werden muss. Nicht zu erwähnen, dass sed ebenfalls ins Memory geladen werden muss. Mit anderen Worten: sehr viel IO.
Also musste ich unnötigen IO loswerden und ich fokussierte mich zuerst auf die Template Kopiererei. Ich hängte einfach das Template an das bestehende Output File und ersetzte die Platzhalter. Es stellte sich als sehr schlechte Idee heraus und ich erkläre gleich weshalb. Hier ist der Code:
template=template.tpl workcopy=workcopy.tmp output=query.out while read -r line do array=(${line}) value1=${array[0]} value2=${array[1]} cat ${template} >> ${output} sed -i s/==TITLE==/${value1}${value2}/g ${output} sed -i s/==VALUE1==/${value1}/g ${output} sed -i s/==VALUE2==/${value2}/g ${output} done < inputfile.txt
Das Resultat war:
real 74m30.974s user 13m8.227s sys 46m4.676s
74 Minuten waren definitiv keine Option. Aber was war hier passiert? Ja, das unnötige Kopieren des Templates war damit vom Tisch. Aber zu was für einem Preis! Nun kopierte ich das gesamte Output File wieder und wieder vom Filesystem ins Memory und zurück. Und mit jeder Iteration werden mehr Read/Write Operationene dazuaddiert. Mit anderen Worten: diese Lösung war das Gegenteil von Skalierbar. Mit einem genügend grossen Inputfile kann das ganze System lahmgelegt werden. Also ruderte ich zurück und entschloss mich lediglich die sed Aufrufe zu optimieren.
Dritter Versuch
Die Workcopy vom Prozess zu eliminieren war eine schlechte Idee. Aber es gibt noch Raum für Verbesserungen: wenn ich die 3 sed Aufrufe optimiere, kann ich wesentlich mehr IOs sparen. Also änderte ich diese Stelle:
sed -i s/==TITLE==/${value1}${value2}/g ${workcopy} sed -i s/==VALUE1==/${value1}/g ${workcopy} sed -i s/==VALUE2==/${value2}/g ${workcopy}
zu:
template=template.tpl workcopy=workcopy.tmp output=query.out while read -r line do array=(${line}) value1=${array[0]} value2=${array[1]} cp ${template} ${workcopy} sed -i "s/==TITLE==/${value1}${value2}/g; s/==VALUE1==/${value1}/g; s/==VALUE2==/${value2}/g" ${workcopy} cat ${workcopy} >> ${output} done < inputfile.txt
Das Template wird nun wieder ins und aus dem Memory kopiert, aber bei der Optimierung von sed konnte ich n un zumindest 3 Lese/Schreibvorgänge auf einen reduzieren. Und dies endete in einem wesentlich besseren Resultat als mein erster Versuch:
real 14m26.738s user 0m20.367s sys 1m0.661s
Von 23 Minuten auf 15 herunter. Das war nicht schlecht, aber immer noch nicht gut. Also weshalb nicht alle Änderungen komplett im Memory machen? Kann Bash das überhaupt? Kann ich externe Tools wie sed oder awk umgehen?
Vierter Versuch
Die Antwort ist: ja, Bash kann das und sogar sehr gut.
Variablen können gefüllt werden mit Inhalt eines Files. Und Textmanipulationen können mit einer Linie Code gemacht werden. Aber es müssen ein paar Dinge beachtet werden!
Die Struktur bewahren
Wenn ich den Inhalt einer Variable ausprinte ohne doppöte Anführungszeichen, wird Bash Newlines unterdrücken. Statt eines strukturierten Text habe ich dann einen Wortsalat.
Das zweite ist, Bash schneidet mir Newlines am Anfang und Ende eines Strings ab. Liest man ein File in eine Variable ein und hängt den gleichen Inhalt immer wieder an sich selber an, muss sichergestellt werden, das das Template nicht nur eine Newline am Anfang und am ende besitzt, sondern einen Blankspace. Ich sah dies als einfachste Lösung um das Problem zu umgehen.
[==TITLE==] ... ROOT = DB/DO_SOMETHING(NEW_VALUE_NAME="==VALUE2==") ... IMPORTANT_FIELD = DB_NAME::==VALUE1== 0 ...
Nun zum Code. Ich entledigte mich der Workcopy und des sed Aufrufs. Das Template wurde in die Variable $template eingelesen, welche wiederum bei jeder Iteration in die Variable $tempvar kopiert wurde, in welcher ich dann die Änderungen machte:
template=$(cat template.tpl) output=query.out while read -r line do array=(${line}) value1=${array[0]} value2=${array[1]} tempvar="$template" tempvar=${tempvar/==TITLE==/${value1}${value2}} tempvar=${tempvar/==VALUE1==/${value1}} tempvar=${tempvar/==VALUE2==/${value2}} workcopy+="${tempvar}" done < ${pairfile} echo "$workcopy" >> ${output}
Das Resultat war überwältigend:
real 0m6.655s user 0m2.883s sys 0m3.272s
6 Sekunden statt 23 Minuten. Das ist schnell.
Fazit
Man wird sich fragen: weshalb der ganze Aufwand und nicht gleich von Beginn weg eine Sprache wie Perl oder Python verwenden? Und ich muss zugeben, ich bin selbst Fan dieser Sprachen. Aber manchmal muss es eben einfach ein Shell Script sein. Und dann bietet Shell durchaus sehr gute Möglichkeiten um eine beeindruckende Geschwindigkeit zu erreichen.
Wenn Du es bisher geschafft hast, so möchte ich mich bei Dir bedanken und ich hoffe, dieser Artikel wird dir eines Tages helfen und dir einige Zeit ersparen!