staging.inyokaproject.org

Zeilenverarbeitendes Script auf Pi beschleunigen

Status: Gelöst | Ubuntu-Version: Kein Ubuntu
Antworten |

Humbi

Anmeldungsdatum:
14. Juni 2014

Beiträge: 76

Hallo Forum,

ich suche eine Möglichkeit mein Script zu beschleunigen. Untersatz ist ein Raspberry Pi 3 mit aktuellem Raspbian 8 und Pihole V3.2.1. Ich habe das Pihole um eine Seite ergänzt, die mir alle je angefragten Domains listet. Dazu läuft jede Nacht ein Cronjob mit folgendem Script.

Das Script kopiert die Log vom Vortag und geht sie Zeilenweise durch. In jeder Zeile wird der Domainteil extrahiert und die domain.log nach dieser Domain durchsucht. Ist sie noch nicht vorhanden wird sie hinzugefügt.

Ich hab das Script vorhin von Hand gestartet und es schafft ca. 1 Zeile pro Sekunde. Bei 30-50.000 Einträgen pro Log ist das zu viel. Vlt. liegt es auch an der domain.log, die mittlerweile immerhin ~ 1000 Einträge hat und die ja jedesmal durchforstet werden müssen.

domains.sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/bash
#Pfad zur Log
#/var/log/pihole.log *.1 *.2.gz usw
datei="pihole.log.1"
pfad="/home/pi/$datei"
i=0

#Prüfe ob Arbeitsdatei da
if [ ! -e "$pfad" ]; then
  cp "/var/log/pihole.log.1" "/home/pi"
fi

#Prüfe ob Datei leer ist, wenn nicht bearbeite sie
while [ -s "$pfad" ]
do
  #Lese die erste Zeile aus und davon den 7. Block = Domain
  block=$(/usr/bin/head -n 1 "$pfad" | /usr/bin/cut -d " " -f7 )

  #Nur Eintragen wenn noch nicht enthalten.
  /bin/grep -qi "$block" /home/pi/domains.log && /bin/echo $i >> /dev/null || /bin/echo "$block" >> /home/pi/domains.log

  i=$(( $i + 1 ))
  #Lösche erste Zeile aus Arbeitsdatei
  /bin/sed -i 1D "$datei"

done
#Lösche Arbeitsdatei
/bin/rm -f "$datei"

exit 0

Auszug aus der pihole.log:

Jan  3 00:08:48 dnsmasq[21725]: reply googleapis.l.google.com is 8.8.8.8
Jan  3 00:11:58 dnsmasq[21725]: query[A] e8.whatsapp.net from 8.8.8.8
Jan  3 00:11:58 dnsmasq[21725]: forwarded e8.whatsapp.net to 8.8.8.8

Danke schonmal für eure Vorschläge. Gruß Humbi

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4735

@Humbi: Ich bin hier ziemlich verwirrt was $datei und $pfad bedeuten. Ich würde sagen das ganze funktioniert nur wenn die beide für die gleiche Datei stehen und dann ist es a) unnötig die unter zwei Namen erreichen zu können, und b) sehr verwirrend das die Namen nicht an den gleichen Wert gebunden werden.

$i scheint mir auch sinnlos zu sein, denn das wird zwar hochgezählt, aber das einzige was wirklich damit gemacht wird ist /bin/echo $i >> /dev/null — also letztendlich wird da überhaupt nichts sinnvolles gemacht.

Was die Laufzeit angeht hat das Skript zwei Probleme, wobei eins davon echt gravierend ist. Und zwar das Du nicht einfach über die Zeilen iterierst, sondern Dir immer die erste Zeile mit head holst und dann nach dem die verarbeitet wurde die erste erste Zeile aus der Datei mit sed löschst. Dafür wird die Datei jedes mal ohne die erste Zeile in eine neue Datei kopiert weil man technisch nichts mitten aus einer Datei entfernen kann ohne mindestens alles was danach kommt umkopieren zu müssen. Da Du etwas am Anfang löschst, muss immer alles danach kopiert werden. Und das bei einem System das relativ langsame Hintergrundspeicherzugriffe hat. SD-Karte eben.

Das zweite was vielleicht nicht so optimal ist, ist die lineare Suche mit grep für jede Zeile aus dem Log. Da ist dann die Frage ob das einlesen der Datei in ein Array und die Suche in der Bash zu Programmieren nicht eventuell schneller ist als grep auf der SD-Karte für den Fall das die Datei nicht komplett gecached wird. Das müsste man ausprobieren und messen.

Ich bin mir auch nicht so sicher ob das mit dem grep semantisch so gewollt ist. Wenn es zum Beispiel eine Domain webmail.delphine-sind-lecker.at in der Datei gibt, und im Log die Domain mail.de steht, dann fügt Dein Skript die Domain nicht zur Datei hinzu, weil mail.de in der anderen Domain vorkommt.

Also auf jeden Fall solltest Du über die Logdatei zeilenweise iterieren ohne sie zu verändern. Dann wirst Du sie sehr wahrscheinlich auch gar nicht kopieren müssen, sondern kannst gleich die originale Logdatei verarbeiten. Das Konstrukt das ich dafür verwenden würde ist eine while-Schleife in die die Datei hinein gepiped wird:

1
2
3
while read -r line; do
    echo "$line"
done < 'test.txt'

Humbi

(Themenstarter)

Anmeldungsdatum:
14. Juni 2014

Beiträge: 76

Marc_BlackJack_Rintsch

Ja du hast Recht. Eigentlich kann ich alle $datei durch $pfad ersetzen.

Zu $i muss ich sagen, dass dieser Teil vor einem halben Jahr entstanden ist und ich mich vorhin selbst gefragt habe was es überhaupt bringt.

Ok, ich ersetz erstmal den Zugriff von Datei auf Variable und schau was das bringt.

Gruß Humbi

Humbi

(Themenstarter)

Anmeldungsdatum:
14. Juni 2014

Beiträge: 76

ok, die Umstellung auf Variable hat schonmal 6250% gebracht. Die Datei die noch 30.000 Zeilen beinhaltete wurde in 8 min abgearbeitet.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash
#Pfad zur Log
#/var/log/pihole.log *.1 *.2.gz usw
pfad="/home/pi/pihole.log.1"

#Prüfe ob Arbeitsdatei da
if [ ! -e "$pfad" ]; then
        cp /var/log/pihole.log.1 /home/pi
fi

#Lese $pfad zeilenweise aus
while read -r line
do
        #Extrahiere den 7. Block = Domain
        block=$(/bin/echo "$line" | /usr/bin/cut -d " " -f7 )

        #Nur Eintragen wenn noch nicht enthalten.
        /bin/grep -xqi "$block" /home/pi/domains.log || /bin/echo "$block" >> /home/pi/domains.log

done < "$pfad"

#Lösche Arbeitsdatei
/bin/rm -f "$pfad"

exit 0

Den grep Befehl hab ich um den Parameter -x erweitert, der in der man aber nicht im wiki steht.

-x, --line-regexp Select only those matches that exactly match the whole line.

Jetzt werden beide Vorkommnisse erkannt. "example.example.com" und "example.example" wenn es später vorkommt.

Danke für die schnelle Hilfe. 👍

Ich bleibe aber lieber beim kopieren der Datei als direkt auszulesen. Das macht ja nicht so viel aus. Habe mich aber gefragt ob eine zweite while Schleife mit if-Vergleich für die bereits geloggten Domains einen Vorteil bringt anstatt der Suche mit grep ?

Gruß Humbi

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11278

Irgendwie habe ich das Gefühl, dass man das Skript stark eindampfen kann:

1
2
3
#!/bin/bash
/usr/bin/cut -d " " -f7 /var/log/pihole.log* | cat /home/pi/domains.log - | sort -u > /home/pi/domains.log.tmp
/usr/bin/mv /home/pi/domains.log.tmp /home/pi/domains.log

Humbi

(Themenstarter)

Anmeldungsdatum:
14. Juni 2014

Beiträge: 76

seahawk1986 Ähm, ja, was soll ich dazu sagen? 30k Zeilen in 1 Sekunde! durchgeackert.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/bin/bash
#Pfad zur Log
#/var/log/pihole.log *.1 *.2.gz usw
pfad="/var/log/pihole.log.1"
log="/home/pi/domains.log"
tmp="/home/pi/log.tmp"

#cut besorgt die Domain only aus der Logdatei
#cat hängt die bisherigen Domains an die Eingabe an
#sort -u unique sortiert die Domains ohne doppelte Einträge, Ausgabe in tmp
/usr/bin/cut -d " " -f7 "$pfad" | cat "$log" - | sort -u > "$tmp"
/bin/mv "$tmp" "$log"

exit 0

Eine Frage hätte ich da aber. Dieser "-" bei dem cat Befehl heiß was genau?

Ich vermute mal so viel wie "anhängen" da ich ja alt und neu zusammenführen muss und es ohne nicht funktioniert.

Danke und Gruß Humbi

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11278

Humbi schrieb:

Eine Frage hätte ich da aber. Dieser "-" bei dem cat Befehl heiß was genau?

Damit gibt cat das aus, was vom stdin (also in dem Fall der Pipe) kommt, vgl. Manpage: http://manpages.ubuntu.com/manpages/trusty/man1/cat.1.html

when FILE is -, read standard input

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4735

@Humbi: Beim Raspi würde ich mal schauen ob und was da über eine RAM-Disk eingehängt ist und dort dann die temporäre Datei hinschreiben. Dann landet die nicht auf der SD-Karte.

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13242

seahawk1986 schrieb:

Humbi schrieb:

Eine Frage hätte ich da aber. Dieser "-" bei dem cat Befehl heiß was genau?

Damit gibt cat das aus, was vom stdin (also in dem Fall der Pipe) kommt, vgl. Manpage: http://manpages.ubuntu.com/manpages/trusty/man1/cat.1.html

Das wird hier aber nicht gebraucht, weil sort das genau so gut kann. Man braucht auch nicht die bash sondern die sh reicht. Außerdem sollte man den mv nur machen, wenn der vorherige Befehl erfolgreich war. Ich habe hier mal die Variante mit set -e gewählt; ansonsten verknüpft man halt mit &&. Damit dann:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/bin/sh

# exit on error
set -e

#Pfad zur Log
#/var/log/pihole.log *.1 *.2.gz usw
pfad="/var/log/pihole.log.1"
log="/home/pi/domains.log"
tmp="/home/pi/log.tmp"

#cut besorgt die Domain only aus der Logdatei
#cat hängt die bisherigen Domains an die Eingabe an
#sort -u unique sortiert die Domains ohne doppelte Einträge, Ausgabe in tmp
/usr/bin/cut -d " " -f7 "$pfad" | sort -u - "$log" > "$tmp"
/bin/mv "$tmp" "$log"

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13242

Nur mal so aus Spaß: Falls der awk auf dem System vernünftig funktioniert, kann man das auch damit machen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/sh

set -e

input=/var/log/pihole.log.1
log=/home/pi/domains.log

tmp="$log.tmp"

/bin/awk '
  FNR == NR { domains[$1] = 1 }
  FNR != NR { domains[$7] = 1 }
  END { for (d in domains) print d }' "$log" "$input" > "$tmp"

/bin/mv "$tmp" "$log"

Der Trick hier ist am Unterschied zwischen FNR und NR zu erkennen, ob es die erste Datei (das Log aller gefundenen Domänen) ist oder weitere Inputs, aus denen Domänen extrahiert werden. Man kann das auch auf mehrere /var/log/pihole.log.* in einem Schritt loslassen (das geht natürlich auch mit dem Ansatz mit sort):

1
2
3
4
5
6
7
...
input=/var/log/pihole.log
...
/bin/awk '
  FNR == NR { domains[$1] = 1 }
  FNR != NR { domains[$7] = 1 }
  END { for (d in domains) print d }' "$log" "$input"* > "$tmp"

Kannst ja mal messen, wie sich diese Variante verhält. Wird vermutlich keinen großen Unterschied machen.

Humbi

(Themenstarter)

Anmeldungsdatum:
14. Juni 2014

Beiträge: 76

Danke an Alle für die lehrreiche Hilfe.

Ich hab mich jetzt für die folgende Variante mit ohne cat entschieden und eine RAMDisk erzeugt, die auch gleich den Weg in ein anderes Script gefunden hat.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/bin/bash
#Pfad zur Log
#pfad="/home/pi/pihole.log.5"
pfad="/var/log/pihole.log.1"
log="/home/pi/domains.log"
tmp="/mnt/RAMDisk/log.tmp"

# exit on error
set -e

#cut besorgt die Domain only aus der Logdatei
#sort -u unique sortiert die Domains ohne doppelte Einträge, Ausgabe in tmp
/usr/bin/cut -d " " -f7 "$pfad" | sort -u "$log" > "$tmp"
/bin/mv "$tmp" "$log"

exit 0

rklm schrieb:

Kannst ja mal messen, wie sich diese Variante verhält. Wird vermutlich keinen großen Unterschied machen.

Ich denke 1 Sekunde ist nicht zu toppen, bzw. die Unterschiede so marginal dass es auch nicht mehr interessiert. Zudem läuft der Cronjob nachts. Ob er eine Sekunde oder 30 braucht wäre mir egal. So groß werden die Dateien nie, die jetzige ist mit 50k Zeilen schon im Urlaub entstanden und sort meistert das im Handumdrehen.

Danke an Alle für die zahlreichen Vorschläge. 👍

Gruß Humbi

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13242

Humbi schrieb:

Danke an Alle für die lehrreiche Hilfe.

Bitte. Du musst aber ein wenig genauer arbeiten:

Ich hab mich jetzt für die folgende Variante mit ohne cat entschieden und eine RAMDisk erzeugt, die auch gleich den Weg in ein anderes Script gefunden hat.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/bin/bash
#Pfad zur Log
#pfad="/home/pi/pihole.log.5"
pfad="/var/log/pihole.log.1"
log="/home/pi/domains.log"
tmp="/mnt/RAMDisk/log.tmp"

# exit on error
set -e

#cut besorgt die Domain only aus der Logdatei
#sort -u unique sortiert die Domains ohne doppelte Einträge, Ausgabe in tmp
/usr/bin/cut -d " " -f7 "$pfad" | sort -u "$log" > "$tmp"
/bin/mv "$tmp" "$log"

exit 0

Da ist ein entscheidender Fehler in Deinem Code. Der sort braucht unbedingt noch "-" als Argument vor oder hinter dem "$log". Sonst wird nämlich die Ausgabe von cut einfach verworfen und Du sortierst immer wieder dieselben Daten (siehe meine Anmerkung in 8923343). Das ist eine ziemlich teure NOP.

rklm schrieb:

Kannst ja mal messen, wie sich diese Variante verhält. Wird vermutlich keinen großen Unterschied machen.

Ich denke 1 Sekunde ist nicht zu toppen, bzw. die Unterschiede so marginal dass es auch nicht mehr interessiert.

Da bin ich deutlich neugieriger als Du.

Zudem läuft der Cronjob nachts. Ob er eine Sekunde oder 30 braucht wäre mir egal. So groß werden die Dateien nie, die jetzige ist mit 50k Zeilen schon im Urlaub entstanden und sort meistert das im Handumdrehen.

Ja, aber bei solchen Gelegenheiten kann man halt auch noch etwas lernen - selbst, wenn das für diesen Fall vielleicht (!) nix bringt, hat man dann einen weiteren Datenpunkt in der Erinnerung, den man vielleicht andernorts mal braucht.

Humbi

(Themenstarter)

Anmeldungsdatum:
14. Juni 2014

Beiträge: 76

rklm schrieb:

Der sort braucht unbedingt noch "-" als Argument vor oder hinter dem "$log".

Danke, hab ich übersehen.

rklm schrieb:

Kannst ja mal messen, wie sich diese Variante verhält. Wird vermutlich keinen großen Unterschied machen.

Da ich grad die Zeit dazu habe, habe ich es mal probiert:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/bin/bash
pfad="/var/log/pihole.log.1"
log="/home/pi/domains.log"
tmp="/mnt/RAMDisk/log.tmp"

# exit on error
set -e

#cut besorgt die Domain only aus der Logdatei
#sort -u unique sortiert die Domains ohne doppelte Einträge, Ausgabe in tmp
/usr/bin/cut -d " " -f7 "$pfad" | sort -u - "$log" > "$tmp"
/bin/mv "$tmp" "$log"

exit 0

.

1
time -p ./domains.sh

real 0.86 user 0.85 sys 0.01

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash
input="/var/log/pihole.log.1"
log="/home/pi/domains.log"
tmp="/mnt/RAMDisk/log.tmp"

# exit on error
set -e

/usr/bin/awk '
  FNR == NR { domains[$1] = 1 }
  FNR != NR { domains[$6] = 1 }
  END { for (d in domains) print d }' "$log" "$input" > "$tmp"

/bin/mv "$tmp" "$log"
exit 0

real 0.31 user 0.22 sys 0.02

awk ist tatsächlich nochmal mehr als doppelt so schnell.

Nur um Sicher zu gehen, dass ich den awk Befehl richtig verstanden habe. Weil ich hab bisher nicht viel mit awk gemacht und ich verbau ungern Befehlsätze die ich selber nicht versteh.

1
2
3
4
/usr/bin/awk '
  FNR == NR { domains[$1] = 1 }
  FNR != NR { domains[$6] = 1 }
  END { for (d in domains) print d }' "$log" "$input" > "$tmp"

Elementar ist hier die Reihenfolge der Bedingungen und der Eingabedateien. Awk beginnt mit den vorhandenen Domains aus "$log". Die absolut eingelesenen Zeilen NR entsprechen noch den Dateizeilen FNR (1:1) Wenn awk zum neuen "$input" kommt stimmen FNR und NR nicht mehr überein und es wird das 6. Feld der zeile genommen. Dann lässt du ein Array erzeugen deren Indizes die Domain beinhaltet und den Wert 1. Also z.B. [ubuntuusers.de]=1. Auf diese Weise wird eine Domain (Index) die bereits existiert einfach mit einem neuen (gleichen) Wert überschrieben. Anschließend werden einfach alle Indizes ausgegeben.

Hat ein bisschen gedauert aber der Gedanke ist schlicht genial. ☺

Danke und Gruß Humbi

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13242

Humbi schrieb:

rklm schrieb:

Kannst ja mal messen, wie sich diese Variante verhält. Wird vermutlich keinen großen Unterschied machen.

Da ich grad die Zeit dazu habe, habe ich es mal probiert:

awk ist tatsächlich nochmal mehr als doppelt so schnell.

Oha! Naja, das könnte man schon durch die Theorie vermuten: die assoziativen Arrays in awk sind Hashtabellen und damit ist der Aufwand O(n), während das Sortieren O(n*log(n)) sein dürfte. Ich denke aber nicht, dass Deine Datenmengen bereits so groß sind, dass sich das bemerkbar machen würde. Vermutlich liegt es eher daran, dass es nur einen Prozess gibt und keine Pipeline.

Nur um Sicher zu gehen, dass ich den awk Befehl richtig verstanden habe. Weil ich hab bisher nicht viel mit awk gemacht und ich verbau ungern Befehlsätze die ich selber nicht versteh.

1
2
3
4
/usr/bin/awk '
  FNR == NR { domains[$1] = 1 }
  FNR != NR { domains[$6] = 1 }
  END { for (d in domains) print d }' "$log" "$input" > "$tmp"

Elementar ist hier die Reihenfolge der Bedingungen und der Eingabedateien.

Die Reihenfolge der Eingabedateien, ja. Bedingungen in diesem Fall, nein, denn diese erfassen disjunkte Zeilen und sind nicht voneinander abhängig. Ein Beispiel, wo das nicht so ist, d.h., die Vertauschung der Bedingungen macht einen Unterschied:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
$ cat x1
111
111
111
111
$ cat x2
2222
2222
2222
2222
2222
$ awk 'FNR == 1 {inputs++} inputs == 1 {print "input 1 " $0}' x1 x2
input 1 111
input 1 111
input 1 111
input 1 111
$ awk 'inputs == 1 {print "input 1 " $0} FNR == 1 {inputs++}' x1 x2
input 1 111
input 1 111
input 1 111
input 1 2222

Awk beginnt mit den vorhandenen Domains aus "$log". Die absolut eingelesenen Zeilen NR entsprechen noch den Dateizeilen FNR (1:1)

Genau.

Wenn awk zum neuen "$input" kommt stimmen FNR und NR nicht mehr überein und es wird das 6. Feld der zeile genommen.

Genau. (Ich dachte, es wäre das siebte?)

Dann lässt du ein Array erzeugen deren Indizes die Domain beinhaltet und den Wert 1. Also z.B. [ubuntuusers.de]=1.

Ungefähr. Indices kenne ich eigentlich nur von echten Arrays, die man über eine Ganzzahl indiziert. Die Schlüssel sind die Domains, als Wert wird immer 1 verwendet.

Auf diese Weise wird eine Domain (Index) die bereits existiert einfach mit einem neuen (gleichen) Wert überschrieben.

Korrekt.

Anschließend werden einfach alle Indizes ausgegeben.

Alle Schlüssel, aber ja.

Hat ein bisschen gedauert aber der Gedanke ist schlicht genial. ☺

Danke für die Blumen, aber das ist bisschen übertrieben. Im Prinzip wird das assoziative Array (der Hash) hier nur verwendet, um eine Menge zu bilden - und zwar mit O(1) für jede Einfügeoperation. Das ist ein ziemlich übliches Vorgehen: Java's HashSet verwendet diesen Ansatz genau so wie Ruby's Set.

Das genialste daran ist noch, dass mir ein einfacher Weg eingefallen ist, die erste Datei von den weiteren zu unterscheiden. ☺

Antworten |