Jak szybki jest grep? -> grep vs awk, python, rugby, java, perl, C - przeszukiwanie logów - część 1.

Wpis musiałem podzielić na części, gdyż okazał sie za długi w porównaniu z możliwościami silnika tego bloga.



Przy pracach nad jednym z systemów bankowych powstała potrzeba optymalizacji czasu analizy logów z aplikacji. Przeprowadzone przy tej okazji testy pozwoliły bliżej przyjrzeć się problemowi doboru narzędzi do konkretnych zastosowań. W poniższym opracowaniu porównałem kilka programów mogących wyszukać w pliku sześć kolejnych znaków. Takie założenie odpowiada przeszukiwaniu plików logów wg. godziny i minuty, np. w poszukiwaniu zdarzeń, które zaszły w dwunastej minucie godziny dwunastej: "12:12:".
Testy rozpocząłem od przygotowania plików symulujących prawdziwe logi. Wygenerowałem plik mający 1 000 000 000 bajtów, składający się z 20 000 000 linii (w każdej linii znajduje sie 50 znaków):
0000001-> 2druga 3trzecia 4czwarta piata 11:59:50
...
2000000-> 2druga 3trzecia 4czwarta piata 12:24:57
Do stworzenia pliku testowego użyłem dwóch skryptów. Choć podobne, jeden z nich miał działać szybciej, przez tworzenie danych w pamięci RAM i zapisanie jednej porcji danych na dysku. Poniższy skrypt  "variable" teoretycznie powinien być szybszy:
Skrypt: variable
#!/bin/bash
# Copyright (c) 2016 Rafal Jackiewicz
#
mintime=10
hours=(`echo {10..23}`)
declare -i j=$mintime
tf100M="testfile_100M.txt"
if [ -f "$tf100M" ]; then
    rm "$tf100M"
fi
for (( i=1; $i<=2000000; i++ )) do {
    minutes=`shuf -i10-59 -n1`
    seconds=$(( $RANDOM % $mintime + 49 ))
    if  ((++j>23)) ; then
    j=$mintime
    echo $i
    echo -n "$bigstring" >> "$tf100M"
    bigstring=""
    fi
    # length: 50 chars per line
    bigstring+=`echo $( printf "%07u->" $i ) 2druga 3trzecia 4czwarta piata ${hours[(( $j-$mintime ))]}:$minutes:$seconds`$'\n'
done
echo -n "$bigstring" >> "$tf100M"
#2 000 000 linii
echo -n "lines in the file: "wc -l testfile_100M.txt
#20 000 000 linii
if [ -f testfile_1G.txt ]; then
    rm "testfile_1G.txt"
fi
for in {0..9}; do cat testfile_100M.txt >> testfile_1G.txt; done
echo -n "Lines in the file: "wc -l testfile_1G.txt
echo "End: fileCreate via variable."

Ten skrypt "direct" wykonujący wiele operacji zapisu do pliku powinien być wolniejszy:
Skrypt: direct
#!/bin/bash
# Copyright (c) 2016 Rafal Jackiewicz
# preferred /tmp in RAM
#
mintime=10
hours=(`echo {10..23}`)
declare -i j=$mintime
tf100M="/tmp/testfile_100M.txt"
if [ -f "$tf100M" ]; then
    rm "$tf100M"
fi
for (( i=1; $i<=2000000; i++ )) do {
    minutes=`shuf -i10-59 -n1`
    seconds=$(( $RANDOM % $mintime + 49 ))
    if  ((++j>23)) ; then
    j=$mintime
    echo $i
    fi
    # length: 50 chars per line
    echo $( printf "%07u->" $i ) 2druga 3trzecia 4czwarta piata ${hours[(( $j-$mintime ))]}:$minutes:$seconds >> "$tf100M"
done
#2 000 000 linii
mv "$tf100M" testfile_100M.txt
echo -n "lines in the file: "wc -l testfile_100M.txt
#20 000 000 linii
if [ -f testfile_1G.txt ]; then
    rm "testfile_1G.txt"
fi
for in {0..9}; do cat testfile_100M.txt >> testfile_1G.txt; done
echo -n "Lines in the file: "wc -l testfile_1G.txt
echo "End: fileCreateDirect."

Taki był rezultat uruchomienia powyższych skryptów:
Zaskakujące? Warto we własnym zakresie przetestować różnicę, gdy w poniższej linii zamienimy "bigstring+=" na "bigstring = "$bigstring +":
bigstring+=`echo $( printf "%07u->" $i ) 2druga 3trzecia 4czwarta piata ${hours[(( $j-$mintime ))]}:$minutes:$seconds`$'\n'
Skąd wynika ta nieoczekiwana różnica w czasach wykonania skryptów? Zakładam, że połączenie wolno działającego języka skryptowego z szybkim podsystemem dyskowym pozwoliło szybciej działać temu skryptowi, który wykonywał mniej operacji (pomimo dużej liczby dyskowych operacji IO).


W tym miejscu zamieszczę kilka słów wyjaśnienia.

1) Skrypty i programy zamieszczone na tej stronie są wersjami testowymi, a nie produkcyjnymi. Nie obsługują wszystkich wyjątków i możliwych błędów, nawet w miejscach, o których wiem. Nie było to potrzebne do zaprezentowania wyników testów i porównania ich.
2) Kod nie jest optymalny w wielu miejscach - i taki ma być. W programach użyłem wielu funkcji jako demonstracje technologii. Np. w skryptach tworzących plik testowy użyłem dwóch różnych funkcji tworzących losowe dane, a w programach, które będą zamieszczone poniżej, zaprezentowane są różne funkcje wspierające programowanie wieloprocesorowe:
  • Pamieć współdzielona zgodna z System V API i POSIX API, odwzorowanie plików w pamięci.
  • Semafory {wait4()}; pomiar czasu w trybie użytkownika i systemu (rusage), oraz high_resolution_clock.
  • Tworzenie procesów: fork, exit, execl.

3) Mimo tego, że testy wykażą różnice w czasach działania programów, co automatycznie skłania do ich wartościowania, to ja nie dyskredytuję żadnego z tych programów, czy języków. Każdy z nich ma pewne właściwości czyniące go użytecznym. Jedne mają działać szybko, przy ograniczonej funkcjonalności. Inne mają wiele możliwości, dużą ilość opcji i przełączników. Jeszcze inne mają służyć do szybkiej implementacji. Jak wyglądała by dzisiejsza informatyka bez awk, czy Javy?


Wykorzystany serwer:
Do testów w wirtualnym środowisku dostępne jest 16 rdzeni, co przy technologii Hyper-Threading udostępnia systemowi operacyjnemu możliwość uruchomienia 32 współbieżnych procesów. Macierz dysków i 120 GB RAM i zapewnia, że system pamięci masowej nie będzie wpływał na testy.


Testy:

Grep
Pierwszy test z tytułowym programem. Do niego będziemy odnosić wszystkie inne wyniki. Każdy z programów będę uruchamiać dwukrotnie, by mieć pewność powtarzalności uzyskanych wyników.

AWK

#!/bin/bash
awk  '/'"$1"'/ { print $0}' $2

Python

#!/usr/bin/python
import re
import sys
file = open(sys.argv[2], "r")
for line in file:
        if re.search(sys.argv[1], line):
              print line,

Ruby

Trzy wersje i porównanie czasów ich wykonania.
grepruby_slow_1
#!/usr/bin/ruby
File.open(ARGV[1]).each_line do |line|
  puts line if line.match(/#{ARGV[0]}/)
end
grepruby_slow_2
#!/usr/bin/ruby
File.open(ARGV[1]) { |f|
    f.each { |line|
        puts line if line.match(/#{ARGV[0]}/)
    }
}
grepruby_fast
#!/usr/bin/ruby
File.foreach(ARGV[1]).grep /#{ARGV[0]}/ do |f|
 puts f
end

Perl

#!/usr/bin/perl
use strict;
use warnings;
my ($bigfile, $needle) = @ARGV;
open my $fd, '<', $bigfile or die "Cannot open file: $!\n";
while(<$fd>) {
   print $_ if ($_ =~ /$needle/);
}

Java

W bonusie obsługa wyrażeń regularnych.
import java.util.regex.*;
import java.io.*;
class grepjava {
    public static void main(String args[]) {
        if (args.length != 2) {
            System.err.println("Podaj dwa argumenty: needle file");
            System.exit(1);
        }
        Pattern needleCompile = null;
        try {
            needleCompile = Pattern.compile(args[0]);
        catch (PatternSyntaxException e) {
            System.err.println("Skladnia: " + e.getDescription());
            System.exit(1);
        }
        BufferedReader in = null;
        try {
            in = new BufferedReader(new InputStreamReader(new FileInputStream(args[1])));
        catch (FileNotFoundException e) {
            System.err.println("Open: "+ e.getMessage());
            System.exit(1);
        }
        try {
            String searchString;
        Matcher match;
            while ( ( searchString = in.readLine() ) != null) {
                match = needleCompile.matcher( searchString);
                if (match.find())
                    System.out.println(searchString);
            }
        catch (Exception e) {
            System.err.println("Read:  " + e.getMessage());
            System.exit(1);
        }
    }
}

********

Więcej informacji:
Informatyka, FreeBSD, Debian


***

Inne wpisy:



Update: 2018.07.17
Create: 2018.07.17

ZFS compresja i deduplikacja na przykładzie danych z systemu NEXUS (Zettabyte File System)

Go to start of metadata
Wersja zfs:
cat /sys/module/zfs/version
0.7.5-1

Niestety, w tym konkretnym systemie NEXUS'a trzymane są pliki ".jar", a nie tylko kody i dane konfiguracyjne. Implikuje to dwa zaobserwowane efekty:
1) Duże, i przecież już skompresowane, pliki są przechowywane na dysku, zamiast samych kodów i mechanizmów budujących je. Nie można efektywnie skompresować pliku poddanego już kompresji, więc mechanizm kompresujący na poziomie systemu plików (sektorów) okazał się nieefektywny.
2) Nawet minimalna zmiana kodów powoduje, że zbudowane archiwum ma inne położenie bajtów (sektory są różne, chociaż zmiany w kodzie mogą dotyczyć jednej linii),czyli nie da się efektywnie przeprowadzić deduplikacji. 
Poniżej screeny ze statystykami. Kompresja na poziomie 1,16 - co nie jest najgorszym wynikiem. Deduplikacja na poziomie 1,23 - co jest już nie do przyjęcia, ze względu na relację: koszt obliczeniowy i zajętość pamięci RAM do zysku w postaci zwiększenia miejsca na dysku. W tym konkretnym przypadku, za względu na wadliwe użycie menadżera repozytoriów, zastosowanie mechanizmów systemu ZFS nie przynosi oczekiwanego zysku.


 ********

Więcej informacji:
Informatyka, FreeBSD, Debian


***

Inne wpisy:



Update: 2018.07.17
Create: 2018.07.17