Per­for­mance ist ein wich­ti­ger Bestand­teil bei der Arbeit mit Daten. Jede Aktion kos­tet Zeit und Res­sour­cen, wodurch es wich­tig ist, die Arbeits­schritte zu über­wa­chen, um mög­lichst Zeit und Res­sour­cen dabei ein­zu­spa­ren. Daher wer­den fort­lau­fend Updates für Daten­ban­ken und ETL-Tools ent­wi­ckelt, aber auch die Ver­ar­bei­tung inner­halb die­ser Daten­ban­ken und Tools wird von den Ent­wick­lern stets wei­ter­ent­wi­ckelt und opti­miert. Doch woher weiß ich, dass ein Daten­bank Update oder eine Anpas­sung an einem State­ment zu einer Ver­bes­e­r­ung geführt hat? Die ein­fachste Mög­lich­keit ist es bei einem ETL-Tool in den Log zu schauen und nach der Aus­füh­rungs­zeit zu suchen. Bei einer Daten­bank kann man ent­spre­chend in Sys­tem­ta­bel­len oder Audit-Tabel­len prü­fen, wie lange diese Pro­zesse gedau­ert haben. Gerade bei Log Dateien kann dies ein auf­wen­di­ger Pro­zess sein. Außer­dem ist ein ein­ma­li­ger Abgleich der Aus­füh­rungs­zei­ten noch keine Bestä­ti­gung für die Opti­mie­rung eines Pro­zes­ses. Das liegt daran, dass die Aus­füh­rungs­zeit eines Pro­zes­ses in den meis­ten Fäl­len nicht nur von dem eige­nen Pro­zess abhängt, son­dern auch von Pro­zes­sen, die par­al­lel dazu in dem ETL-Tool oder auf der Daten­bank aus­ge­führt wer­den. Daher ist es sinn­voll meh­rere Aus­füh­rungs­zei­ten zu betrach­ten, und dies am bes­ten vor und nach der Anpas­sung. Wenn man sel­ber schon ein­mal ein Log­file ana­ly­siert hat weiß man, wie lange es dau­ern kann, bis man die rich­ti­gen Infor­ma­tio­nen gefun­den hat. Auch wenn man nach dem ers­ten Log­files deut­lich schnel­ler mit der Struk­tur umge­hen kann wird der Pro­zess mit der Menge an Log­files nicht merk­lich schnel­ler. Falls es sich bei den Anpas­sun­gen um ein Daten­bank Update han­delt ist es bes­ser nicht nur einen Pro­zess, son­dern meh­rere Pro­zesse zu betrach­ten. So steigt die Zahl der aus­zu­wer­ten­den Log­files von ein­zel­nen Files schnell in Berei­che von meh­re­ren hun­dert Files. Spä­tes­tens jetzt sollte einem klar wer­den, dass diese Arbeit nicht ohne Rechen­leis­tung erle­digt wer­den sollte. Dafür eig­nen sich Pro­gramme wie ela­s­tic­se­arch und Python.

Ela­s­tic­se­arch ist eine Such­ma­schine, in der bei­spiels­weise unsere Log­files abge­legt wer­den kön­nen. Mit Hilfe der Rest API kann anschlie­ßend auf die ein­zel­nen Log­files gefil­tert wer­den. Doch wie kann man das für eine Aus­wer­tung nut­zen? Hier kommt Python ins Spiel. Python ist durch seine zahl­rei­chen Biblio­the­ken ein pas­sen­des Tool für die Aus­wer­tung der Datein. Zusätz­lich bie­tet Python eine ent­spre­chende Anbin­dung an elasticsearch.

Um mit Python auf die Daten der Rest API zugrei­fen zu kön­nen, muss zunächst das ela­s­tic­se­arch package auf Python instal­liert werden.

Pip install elasticsearch

Anschlie­ßend kön­nen wir über Python mit der Aus­wer­tung begin­nen. Zunächst impor­tie­ren wir die ela­s­tic­se­arch library um dar­auf zugrei­fen zu kön­nen und defi­nie­ren eine ela­s­tic­se­arch Datenbank

from elasticsearch
import Elasticsearch
es = Elasticsearch([{'host'_'localhost','port':9200}])

Im Inter­net gibt es eine Reihe von öffent­li­chen Rest APIs mit denen man die Mög­lich­keit hat, die Funk­ti­ons­weise von Python und ela­s­tic­se­arch zu ler­nen. In unse­rem Bei­spiel neh­men wir eine Daten­bank über die Renn­ergeb­nisse von allen For­mel 1 Ren­nen zwi­schen 1950 und 2019. In einem ers­ten Schritt impor­tie­ren wir diese Daten aus der Rest API in unsere ela­s­tic­se­arch Daten­bank. Dabei legen wir einen Index an um die­sen spä­ter zu durchsuchen

i = 1
while r.status_code == 200:
r = requests.get('http://ergast.com/api/f1/1950/' + str(i) + '/results')
es.index(index="f11950", doc_type="doc", id=i, body=json.loads(r.content))
i = i+1
print(i)

Hier wird für jeden Sta­tus der Anfrage ein Ein­trag in dem Index „f11950“ ange­legt. In die­sem Bei­spiel wer­den alle Renn­ergeb­nisse des Jah­res 1950 in den Index ein­ge­tra­gen. Damit alle Ergeb­nisse ein­ge­le­sen wer­den kön­nen wird eine dop­pelte Schleife benö­tigt, um über die Jahre und die Ren­nen zu gehen.

i=2000
j=1
length=[]
time=[]
laps=[]
year_R=[]
Race_R=[]

for i in year:
    for j in race:
        result = requests.get('https://ergast.com/api/f1/'+ str(i) +'/' 
                 + str(j) +'/restults.json')
        if result.status_code == 200 and result.json()['MRData']['RaceTable']
                                         ['Races'] != []:
            url = result.json()['MRData']['RaceTable']['Races'][0]['Circuit']
                  ['url']
            circuit = requests.get(url)
            circ = circuit.text
            circl = circ[circ.find('Length'):-1]
            circlength = circl[15:20]
            length.append(circlength)
            timelap = result.json()['MRData']['RaceTable']['Races'][0]['Results']
                  [0]['Time']['time']
            time.append(timelap)
            lap = result.json()['MRData']['RaceTable']['Races'][0]['Results']
                  [1]['laps']
            laps.append(lap)
            year_R.append(i)
            Race_R.append(j)
        j=j+1
    i=i+1

Wir wol­len in die­sem Bei­spiel die Durch­schnitts­ge­schwin­dig­keit der “For­mel 1” Wagen im Laufe der Jahre betrach­ten. Dafür kön­nen wir aus dem Ergeb­nis eines Requests über eine Fil­te­rung die Gesamt­zeit des ers­ten Fah­rers aus­ge­ben lassen:

results.json()['MRData']['RaceTable']['Races'][0]['Results'][0]['Time']['time']

Die Ecki­gen Klam­mern öff­nen dabei immer einen wei­te­ren Bereich der unter­lie­gen­den json Abfrage. Neben der Zeit inter­es­siert noch die Länge der Stre­cke. Da in der Daten­bank nicht die zurück­ge­legte Stre­cke der Pilo­ten ein­ge­tra­gen ist müs­sen wir einen Umweg gehen. Für jede Renn­stre­cke ist die url zur Wiki­pe­dia-Seite der Stre­cke ange­ge­ben. Pro url kön­nen wir nun über einen Request den Text der ent­spre­chen­den url nach dem Schlag­wort Length durch­su­chen. Anschlie­ßend neh­men wir die 5 Zei­chen nach dem ers­ten Length im Text­kör­per der Renn­stre­cke. Diese Länge wird als Run­den­länge iden­ti­fi­ziert. Da im Laufe der Zeit die Run­den­an­zahl immer wie­der ange­passt wurde müs­sen wir uns die Run­den­an­zahl eben­falls aus einer json Abfrage aus­ge­ben las­sen. Zuletzt geben wir noch das Jahr und die Renn-num­mer aus. Jeder Wert wird in eine Liste ein­ge­tra­gen, die wir anschlie­ßend unter­su­chen können.

Bei einem Daten­bank­up­date inter­es­siert uns die Leis­tung der Daten­bank vor und nach dem Update. Um diese Punkte von­ein­an­der unter­schei­den zu kön­nen tei­len wir die ermit­tel­ten Lis­ten in zwei Berei­che. In unse­rem Bei­spiel wol­len wir uns die Aus­wir­kung von Heck- und Front­flü­geln in der For­mel 1 anschauen. Diese Flü­gel wur­den im Jahr 1968 ein­ge­führt. Die­ses Jahr neh­men wir daher als unse­ren Referenzwert.

Da unsere bezüg­lich der Länge der Stre­cke nicht voll­stän­dig gefüllt ist und dort teil­weise keine rich­ti­gen Werte ein­ge­tra­gen sind müs­sen wir diese fal­schen Infor­ma­tio­nen aus den Lis­ten ent­fer­nen. Hier­für suchen wir die Liste length nach lee­ren Ein­trä­gen ab. Falls ein lee­rer Ein­trag in die­ser Liste vor­han­den ist wird der Ein­trag ignoriert.

Anschlie­ßend wird jeweils auf das Jahr geprüft und alle Werte vor 1968 wer­den in eine Liste mit dem Pre­fix “old_” ein­ge­tra­gen, wäh­rend alle neue­ren Ein­träge in eine Liste mit dem Pre­fix “new_” ein­ge­tra­gen wer­den. Außer­dem muss die Zeit in eine Sinn­volle Ein­heit gebracht wer­den, in unse­rem Fall in Sekunden.

lenlength = len(length)

while k < lenlength:
    if length[k] != '' and length [k] != 'h row':
        vlength = length[k][0:length[k].find(' ')]
        totallength = float(laps[k])*float(vlength)
        tottime = time[k]
        hour = float(tottime[0:tottime.find(':')])
        resttime1= float(tottime[tottime.find(':')+1:])
        minute = float(resttime1[0:resttime1.find(':')])
        resttime2= float(resttime1[resttime1.find(':')+1:])
        second = float(resttime2)
        time_in_s = hour*360+minute*60+second

        if year_R[k] < 1968.0:
            old_time_u.append(time_in_s)
            old_totlength.append(totallength)
            old_year_race.append(float(year_R[k])+float(Race_R[k])/21)
        else:
            new_time_u.append(time_in_s)
            new_totlength.append(totallength)
            new_year_race.append(float(year_R[k])+float(Race_R[k])/21)

Wir haben jetzt also zwei mal drei Lis­ten, mit den unter­schied­li­chen Anga­ben year_race, wel­che der Zeit­stem­pel für unsere Unter­su­chung ist und tot­length, bzw. time_u aus denen wir die Durch­schnitts­ge­schwin­dig­keit errech­nen können.

Mit die­sen Lis­ten kann man einen Plot erstel­len, auf dem man die Durch­schnitts­ge­schwin­dig­keit in Abhän­gig­keit vom Jahr ermit­teln kann.

Performance Analyse von ETL-Tools mit Hilfe von Elasticsearch und Python Bild3

In unse­rem Bei­spiel haben wir noch den Mit­tel­wert der jewei­li­gen Zeit­räume bestimmt und in das Dia­gramm ein­ge­tra­gen. Das Dia­gramm wurde anschlie­ßend mit Hilfe der Python-Biblio­thek matplotlib.pyplot erstellt.

Eine wei­tere Anspre­chende Dar­stel­lung kann ein His­to­gramm sein. Dafür kön­nen die glei­chen Werte der Aus­wer­tung ver­wen­det werden.

Performance Analyse von ETL-Tools mit Hilfe von Elasticsearch und Python Bild4

Aus bei­den Plots kann man erse­hen, dass die Geschwin­dig­keit nach der Ein­füh­rung von Flü­geln in der For­mel 1 ange­stie­gen ist. Ana­log dazu könnte eine Aus­wer­tung von Log Files aus­se­hen, um zu bestä­ti­gen, dass durch ein Daten­bank Update die Lauf­zei­ten der Jobs ver­kürzt wurden.