Datenbanksystem für mind. 100k Inserts / Updates pro Sekunde

Kannst du so ein Datensatz hier mal reinstellen? Würde das gerne mal mit Node.js und dotnet core testen.
 
Den blog Post hatte ich auch schon entdeckt. Da ich aber keine besonderen Features benötige sah es für mich so aus, wie wenn der DictReader immer noch am schnellsten ist. Und den nehme ich ja her.
Den Datensatz kann ich leider nicht teilen, sind aber nur Netflows mit nfdump als CSV ausgegeben.
 
schau dir pandas an und die apache arrow bewegung allgemein (pandas,parquet,pyspark). Sowohl für den csv import als auch allgemein

Für dein Endziel wäre ein Konstrukt kafka -> spark consumer eine idee
 
  • Gefällt mir
Reaktionen: Falc410
Falc410 schrieb:
Ich habe noch gar nicht angefangen irgendwelche Inserts zu machen. Im Moment bin ich noch an dem Schritt, dass ich alles in Dictionaries sammel. Aber selbst das dauert zu lange.

Ich weiß, dass Python langsamer ist als C aber ich nehm am Anfang natürlich erstmal das, was ich am besten kenne. Wie gesagt, ursprünglich war geplant das in Echtzeit auszuführen. Verbindungsdaten kommen rein und sollen in Echtzeit ausgewertet und in eine Datenbank geschrieben werden. Mindestens 60.000 Zeilen pro Sekunde kommen rein.

Aber ich kann auch die 5 Minuten Chunks nehmen. Ein Tool zeichnet kontinuierlich auf und rotiert die Datei alle 5 Minuten, d.h. ich habe 5 Minuten Zeit die Datei einzulesen, auszuwerten und weg zu schreiben bevor die nächste kommt.

Ich habe jetzt mal 2 verschiedene Funktionen geschrieben um herauszufinden wie gross der Geschwindigkeitsunterschied ist. In der oberen Funktion baue ich nur ein Dict und in der zweiten stattdessen eine Liste die ich dann im Moment nur per Single Thread rausschreibe. Bei einem Testdatensatz auf meinem Laptop ist die obere Funktion einen Tick schneller, am richtigen Server ist die mit der Liste etwas schneller, aber beides noch deutlich zu langsam - insgesamt dauert es 700 Sekunden!

Und eigentlich mache ich nicht anderes als die CSV Datei in den Speicher zu laden und in jeder Zeile die IP anzuschauen ob die in einem bestimmten Netz liegt und wenn ja, wegschreiben.

Ich habe das jetzt auf einem System mit 32 Cores und 96 GB RAM getestet. RAM Verbrauch geht auf über 30 GB bei einer knapp 500MB CSV Datei. Das einlesen und starten der Threads dauert schon ziemlich lange. Wenn ich die Threads aber mal reduziere, z.B. Single oder 4, dann dauert das ewigkeiten.

Aber ich verstehe es trotzdem nicht - Ja, C ist sicher schneller als Python aber jeder nimmt doch Python für Big Data her und liest Millionen von Datensätzen ein. Muss doch möglich sein. Den IP Lookup mache ich mit netaddr und IPSet - das baut wahrscheinlich auch nur ein grosses Dict aber auf meinem Laptop ist das zumindest kein Problem.

Anscheinend macht die SSD bei mir im Laptop einiges aus. Ein 7MB Testdatensatz verarbeite ich in 6.5 Sekunden mit 4 Threads, am Server dauert das schon 8.3 Sekunden - a) weil er langsamer die Datei einliest und b) weil das erstellen von 32 Threads einen Overhead generiert. Aber das sollte bei 500MB Daten kein Thema mehr sein.

Code:
def outgoingPorts(row):
    if row['sa'] in local_net:
        if row['sa'] in src_list:
            src_list[row['sa']].append(int(row['sp']))
        else:
            src_list[row['sa']] = [row['sp']]

def iplist(row):
    if row['sa'] in local_net:
        ip_list.append(row['sa'])

if __name__ == '__main__':
    manager = Manager()
    src_list = manager.dict()
    ip_list = manager.list()
    start = time.time()
    pool = Pool(multiprocessing.cpu_count())
    with open('/root/uni-directional.csv') as f:
        csv_reader = csv.DictReader(f)
        #pool.map(outgoingPorts, csv_reader)  # generate ip / port list in memory
        pool.map(iplist, csv_reader)  # only write ip's to file

    with open('ip_list.txt', 'w') as file_handler:
        for item in ip_list:
            file_handler.write("{}\n".format(item))
    print('It took {0:0.1f} seconds'.format(time.time() - start))

Auf jeden Fall hätte ich jetzt die theoretisch 18 Millionen inserts / updates auf 33512 reduziert. Nur dauert die Vorauswahl halt schon viel zu lange. Theoretisch könnte ich das alles mit Bash Scripten machen wenn ich ne Funktion hätte die prüft ob eine IP in einem bestimmten Netzbereich liegt.


Uff, also wenn du mich fragst: 20 Zeilen mehr Code und weniger .map wirds wohl richten. So lahm wie das läuft und bei dem Ramverbrauch scheint da irgendwo Rekursion im Spiel zu sein.

Also ich würd die Csv zeilenweise in einer while abholen, ein dict mit den entsprechenden Elementen als Key befüllen und die dann einfach wegschreiben. Und das erstmal mit einem thread, schön simpel halten und dann erst dranflanschen wenn es performt...
 
  • Gefällt mir
Reaktionen: BeBur
Muss es eigentlich Python sein? Also gibt es dafür einen validen Grund oder fühlst Du dich da am wohlsten? Das klingt für mich eher nach klassischer ETL wobei das T wohl ansichtssache ist. Mit einem Multifileloader kriegt man zumindest in SSIS relativ bequem und schnell flatfiles verarbeitet und die Deltaerkennung lässt sich recht gut umsetzen.
Du könntest ja ein file mit veränderten Daten bereitstellen und die öffentlichen IP-Netze durch private ersetzen. ;)
 
mambokurt schrieb:
Uff, also wenn du mich fragst: 20 Zeilen mehr Code und weniger .map wirds wohl richten. So lahm wie das läuft und bei dem Ramverbrauch scheint da irgendwo Rekursion im Spiel zu sein.

Davon abgesehen, dass map ziemlich effizient ist, braucht man das fuers multi-threading. Warum 20 Zeilen mehr Code helfen sollen verstehe ich nicht?
Also ich würd die Csv zeilenweise in einer while abholen, ein dict mit den entsprechenden Elementen als Key befüllen und die dann einfach wegschreiben. Und das erstmal mit einem thread, schön simpel halten und dann erst dranflanschen wenn es performt...

Genau das passiert doch. with open, macht genau das. In einem Thread hatte ich das vorher, ist aber viel viel zu langsam, deswegen habe ich überhaupt erst angefangen mit multi-threads.

@parats Das Problem ist, wenn ich die IPs ersetze, funktioniert der check ob sie local IPs nicht mehr. Und genau das ist hier der Knackpunkt. Der Lookup dauert zu lange. Testdatensatz mit 6 MB CSV Datei dauert (mit rausschreiben der IPs in eine Text-Datei) 1.1 Sekunden, wenn ich aber den Lookup mache, ob die IP auch in dem Local Set liegt dauert es 16.3 Sekunden. Also das netaddr (https://netaddr.readthedocs.io/en/latest/introduction.html) ist verantwortlich.

Vielleicht muss ich zuerst das Dict bauen, dann gehe ich da nochmal drüber kopiere nur die IPs die auch local sind und dann mache ich den Datenbank import. Das sollte schneller gehen. Aber grundsätzlich ist eben dieser lookup das Problem.

Ich denke ich muss es selber implementieren so wie @bog es verlinkt hat und bitweise Operationen machen. Ich bin zwar nicht so fit in C, aber das sollte ich hinbekommen. Ich wollte halt bei Python bleiben da ich mich damit am besten auskenne und auch schon größere Projekte mit verschiedenen Datenbanken (MySQL, Postgresql oder MongoDB) in Python realisiert habe.

Und ja, es ist im Prinzip ETL wobei ich das T im Moment hinten anstelle, da ich erst den Rest optimieren wollte.

Edit: Also nur zum Vergleich, die Werte variieren natürlich immer etwas, je nachdem wie mein Rechner gerade ausgelastet ist.
CSV einlesen und jede Zeile prüfen ob local IP und dann in eine Liste (Set) schreiben: ca. 13 Sekunden
CSV einlesen, alle IPs in eine Set schreiben, über das Set iterieren und jede local IP in ein weiteres Set schreiben: ca. 3 Sekunden

Ich muss den Datensatz einfach reduzieren - check is local_ip reduziert zwar am meisten, ist aber am teuersten. Daher muss ich das später machen - aber am besten benötige ich eine bessere Lösung hier. Das Problem ist, dass ich IPv4 und IPv6 habe und das nicht vorher sehen kann, also es gibt keinen Flag oder ähnliches. Ich bekomme in der CSV Datei einen String und der kann IPv4 oder v6 sein.
 
Zuletzt bearbeitet:
60.000 Datensätze pro Sekunde heißt du hast >16µs pro Datensatz und somit >16000 Operationen bei 1GHz (1ns) ganz ohne Vektorisierung, MMX, AVX etcetc.
Dh ein normaler PC ist wahrscheinlich selbst single-threaded mehr als ausreichend im Falle einer schlanken C++ Anwendung.

Wenn du mal nen kleinen Datensatz mit zB 10 Beispielzeilen hier rein kopierst (am besten mit deinen Problem-Fällen im Sinne von Duplikaten oder was dir da passieren kann) wäre das sicher schnell gemacht.

Das ganze ist im Grunde "https://de.wikipedia.org/wiki/Erzeuger-Verbraucher-Problem" und daher bieten sich zumindest 2 Threads an. Einer nimmt die Daten in Empfang und ein anderer verkonsumiert sie.
Die Kommunikation dazwischen könnte für hohe Performance zB ein Ringbuffer sein. So erspart man sich Speicher-Operationen und arbeitet immer auf den selben Kilobyte die daher im Cache liegen werden. Dort kann man dann auch leicht prüfen, ob das produzieren den Buffer zum überlaufen bringt und kontrolliert darauf reagieren.
 
Ich hab mal die Erfahrung gemacht, dass bei einem Producer und einem Consumer der Verwaltungsoverhead so hoch ist, dass man keine Performance gewinnt. Aber ausprobieren könnte mans.

Mich würd auch Mal eine Demo Datei interessieren und nochmal eine Zusammenfassung, was genau zu implementieren ist. Ich würd mich Mal in C# und MSSQL probieren....
 
Drexel schrieb:
Ich hab mal die Erfahrung gemacht, dass bei einem Producer und einem Consumer der Verwaltungsoverhead so hoch ist, dass man keine Performance gewinnt.
Das kommt ganz auf die Kommunikation zwischen den Threads an. Wie hattest du es denn gelöst?
 
Ich mache bei sowas meist etwas in Richtung von Double-Buffering. Der Producer erstellt eine Liste mit x Einträgen und übergibt diese dann komplett an einen freien Consumer. Während dieser diese abarbeitet, macht der Producer mit einer ganz neuen Liste weiter. So muss man dann für bspw. 10.000 Einträgen nur ein einziges mal die Adresse der Liste zwischen den Threads austauschen, so ist der Overhead nahe 0.
 
  • Gefällt mir
Reaktionen: kuddlmuddl, bog und new Account()
Falc410 schrieb:
Davon abgesehen, dass map ziemlich effizient ist, braucht man das fuers multi-threading. Warum 20 Zeilen mehr Code helfen sollen verstehe ich nicht?


Genau das passiert doch. with open, macht genau das. In einem Thread hatte ich das vorher, ist aber viel viel zu langsam, deswegen habe ich überhaupt erst angefangen mit multi-threads.

- wie gesagt, kein Plan von Python, wenn du der Meinung bist das wäre effizient muss ich das hinnehmen, ich warte immer noch auf Beispieldaten dass wir da vielleicht mal nachvollziehen können was schief läuft ;)

Multithreading machts natürlich besser, aber wenn es single threaded schon beschissen läuft ist der Fehler nicht das threading sondern was anderes. Als ob dein Gabelstapler Modell X nur 1kg heben kann und du stellst 10.000 davon hin weil du 10 Tonnen bewegen musst statt dir erstmal einen anständigen Stapler zu holen. Meine Meinung. Ich mein was macht das? Csv Daten auslesen, auseinandernehmen, gucken ob x in Liste y. So lahm kann und darf das nicht laufen, da ist irgendwo der Wurm drin...
 
  • Gefällt mir
Reaktionen: BeBur
Ich habe mal eine reduzierte Version von der CSV Datei gebaut. Normalerweise sind noch mehr Columns enthalten. Ich habe es jetzt mal auf das kleinste Set reduziert, aber auch das hilft wenig. Es sind random generierte IP Addressen.

Jede IP kann entweder zu unserem Netz gehören oder eben nicht. Wenn ich ein CSV habe mit Bi-Directional Netflows, dann habe ich pro Zeile eine Source und Destination IP aber die können beide in unserem Netz sein, oder auch nur eine davon. Theoretisch kann es sogar sein, dass gar keine dazu gehört (wenn es um ein Netz geht, das mich nicht interessiert).

Nun habe ich 34 Subnetze, in der Größe zwischen /8, /16 bis hin zu /24 (dazwischen auch noch ein paar) und ein IPv6 Netz mit /36, wenn ich alle IPs zusammen zähle sind das laut Python: 4951760157141521099613824768 IPs in diesem Set. Mit netaddr kann ich eben einfach machen: if IP in meine_Subnetze - und das dauert halt sehr lange.
Leider sagt mir das CSV nicht ob es sich bei dem String um eine IPv4 oder IPv6 handelt, aber ich denke das könnte ich manuell herausfinden und dann vielleicht das IPSet nach v4 und v6 aufsplitten. Vielleicht bringt das noch Performance. Und den IPv4 check könnte ich theoretisch bitweise machen.

Aber ich habe ja schon herausgefunden, dass dieser Lookup für die miese Performanz verantwortlich ist. Der restliche Code ist schnell

Edit: Es scheint wohl nicht an der Größe des IPSet zu liegen. Wenn ich das IPv6 Netz herausnehmne bleiben 17327872 IPv4 Addressen übrig, aber der Vergleich dauert genau so lange. So wie ich das verstehe, arbeitet das IPset ja auch mit einer Hashmap, d.h. die Anzahl der Einträge dort sollte sich nicht auf die Performanz auswirken. Ich habe auch schon herausgefunden das netaddr ziemlich lahm ist. In der neuesten Version gab es deutliche Verbesserungen - aber die nutze ich schon.
Siehe auch: https://github.com/drkjam/netaddr/issues/152

Was noch helfen könnte, ist pypy zu benutzen anstatt CPython

Edit 2: Habe mal pypy3 installiert und der Wahnsinn! Wenn ich das selbe Python Script ausführe dauert es nur 1.1 Sekunden und mit normalen Python 3 satte 13 Sekunden! Unglaublich. Das ist jetzt noch Single Thread mit den Mini-Testdatensatz. Ich werde mal einen größeren probieren und dann auch mit multi-threads.
 

Anhänge

  • sample_data.txt
    89,2 KB · Aufrufe: 204
Zuletzt bearbeitet:
Achso der Performance Bottleneck ist nachzuschauen ob die Ip Adresse immer in einem Deiner Netze liegt? Das hatte ich bisher gar nicht so verstanden. Bin in Python nicht firm, und sehen in dem Code den Du mal gepostet nicht wirklich wie Du den Check machst, ich denke das funktioniert irgendwie über den Ausdruck 'in local_net'...

Ich würd das mal nachbauen in C#, die V4 netze sind immer genau /8, /16 und /24? Würd mich mal interessieren, wie schnell da eine C# Implementierung auf Deinen Beispieldaten ist.
 
Genau. Ich nutze die netaddr Library, weil diese nämlich IPv4 und IPv6 unterstützt. Die ganzen anderen Lösungen unterstützen normalerweise nur IPv4.

https://netaddr.readthedocs.io/en/latest/tutorial_03.html

Man kann sehr einfach prüfen ob eine IP in einem bestimmten IPNetwork liegt, da ich aber 35 verschiedene Subnetze habe, habe ich ein IPSet erstelllt. Die Größe des IPSet spielt keine Rolle für den Vergleich. Das IPSet habe ich local_net genannt und mit einem einfachen "if $IP in local_net:" kann man das überprüfen. Und das ist für die langsame Geschwindigkeit verantwortlich.

Wenn du das testen möchtest, müsstest du jetzt eine Liste von 35 Subnetzen errstellen, die meisten davon /24 und ein paar andere auch noch. Je nach Implementierung kann die Größe schon Auswirkungen auf die Implementierung haben.

Du kannst ja mal hergehen und diese z.B. nehmen (random Netze die ich mir ausgedacht habe, die anderen IPs aus dem Testdatensatz sind ja auch random generiert):
10.0.0.0/8
192.168.0.0/16
129.127.0.0/16
2001:1000:0000::/36
195.143.10.0/24
195.143.17.0/24
195.143.22.0/24
195.143.43.0/24
140.35.128.0/18
151.60.48.0/23

Wie gesagt, in der Realität sind es noch einige mehr, vor Allem noch mehr /24 und ein paar /16 aber sollte ausreichend sein für einen Test. Das Hauptproblem ist halt, dass es IPv4 und IPV6 gibt und man das dann auch manuell prüfen müsste in jeder Zeile. Die netaddr Library bietet da halt viel Komfort (zu lasten der Performance wie man sieht).

Im übrigen habe ich gestern Abend noch einen 600 MB Datensatz eingelesen - der auf einem Server mit 32 Threads ja fast 15 Minuten gebraucht hat mit Python 3. Auf meinem Laptop mit Single Thread und pypy3.5-7.0.0 gerade einmal 100 Sekunden. Und das obwohl die selbe netaddr Library benutzt wird. Also genau der selbe Code. Bin echt fasziniert was das JIT bringt.
 
Evtl musst Dir was selbst schreiben. Die IpAddress Class in C# unterstützt IPv4 und V6.

Sie unterstützt aber nicht das Checken ob eine Adresse in einem Subnetz liegt, dazu habe ich aber hier Code gefunden der relativ schlank aussieht: https://blogs.msdn.microsoft.com/kn...ess-calculations-with-c-subnetmasks-networks/

Zumindest die IpAddress Extensions sehen mir auch kompatibel mit V6 aus, die Subnetmask Class nicht, die ist auf 4 Bytes begrenzt...
 
Ich sag ja, nur mit IPv4 existieren viele Lösungen, mit v6 wird das schon komplizierter. Und dann eben nicht nur checken ob es in einem Subnetz liegt, sondern die ganze Liste! ggf. also 35x checken.

Ich bin mit pypy jetzt ganz zufrieden denke ich. Es müsste von der Performance ausreichend sein. Habe noch ein paar Tests gemacht.

  • pypy mag kein multi-threading (multiprocessing) - damit dauert mein Testdatensatz 568 Sekunden
  • Ich bin nun anscheinend I/O bound. Habe es es auf drei verschiedenen Rechner ausprobiert:
a) Macbook mit SSD: 100 Sekunden b) Dual-CPU Intel Xeon 16 Core Server mit SAS HDD RAID: 158 Sekunden c) VMWare virtuelle Umgebung in der Cloud - HDD ist übers Netz angebunden: 177 Sekunden

Erkenntnis: pypy benutzen mit Single Thread und schneller SSD Festplatte, dann kann ich ein 5 Minuten File in 100 Sekunden verarbeiten und hab noch 200 Sekunden für den Datenbankimport übrig. Ggf. könnte ich sogar auf Echtzeitverarbeitung übergehen.

Noch ein paar Sachen optimieren (also z.B. zuerst verdichten und die teure $IP in local_net Abfrage erst später machen) und es dürfte noch besser werden.
 
Supi, dann hast du es ja eigentlich :D was mir jetzt noch quick & dirty einfallen würde wär mal zu testen was da Zeit frisst, ipv4 oder v6, wenn es v4 ist kannst du zu einer ip einfach die Subnetze generieren und dann in deiner Liste gegenchecken? Mit v6 und Subnetzen kenne ich mich leider Null aus. Mir käme aber in den Sinn zusammenhängende Bereiche einfach zusammen auf einen Baum zu packen, dann könntest du an Hand der Ip den Baum langwandern und wärst relativ fix beim Ergebnis...

Ansonsten würd ich sagen erstmal die Liste durchgehen und IPs zusammenfassen und dann erst den lookup(les gerade willst du eh machen - 2 Dumme ein Gedanke ;))....
 
  • Gefällt mir
Reaktionen: Falc410
Zur Info: Ich hab mal in C# was Quick&Dirty gebastelt um das Szenario nachzustellen:
Ich habe Original Deine Subnetze genommen und den Inhalt Deiner Sample Datei so oft kopiert, dass ca. 110.00 Datensätzen drin sind, die Datei ist ca. 5MB groß und liegt auf ner Samsung Pro Evo SSD, CPU i5-6600K. Ich schaue nach ob die Ip Adressen aus der Datei in einer Liste von Subnetzen liegen und schreibe die Treffer als gesplittete Zeilen in eine Liste. Zugegebenermaßen nur 111 Ergebniss, knapp über 1 Mio. Checks, Durchlaufzeit 0,3-0,4 Sekunden, Speicherverbrauch lt. Taskmanager 5 MB...

Code:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;

namespace IpAddressChecker
{
    class Program
    {
        static void Main(string[] args)
        {
            DateTime start = DateTime.Now;

            IPAddress[] nets = new IPAddress[]
            {
                IPAddress.Parse("10.0.0.0"),
                IPAddress.Parse("192.168.0.0"),
                IPAddress.Parse("129.127.0.0"),
                IPAddress.Parse("2001:1000:0000::"),
                IPAddress.Parse("195.143.10.0"),
                IPAddress.Parse("195.143.17.0"),
                IPAddress.Parse("195.143.22.0"),
                IPAddress.Parse("195.143.43.0"),
                IPAddress.Parse("140.35.128.0"),
                IPAddress.Parse("151.60.48.0"),
            };

            IPAddress[] masks = new IPAddress[]
            {
                IPAddress.Parse("255.0.0.0"),
                IPAddress.Parse("255.255.0.0"),
                IPAddress.Parse("255.255.0.0"),
                IPAddress.Parse("ffff:ffff:f000::"),
                IPAddress.Parse("255.255.255.0"),
                IPAddress.Parse("255.255.255.0"),
                IPAddress.Parse("255.255.255.0"),
                IPAddress.Parse("255.255.255.0"),
                IPAddress.Parse("255.255.192.0"),
                IPAddress.Parse("255.255.254.0"),
            };

            List<string[]> result = new List<string[]>();
            int checkCounter = 0;

            string line;
            string[] parts;
            StreamReader file = new StreamReader(@"d:\sample_data.txt");
            line = file.ReadLine();
            while ((line = file.ReadLine()) != null)
            {
                parts = line.Split(';');

                for (int index = 0; index < nets.Length; index++)
                {
                    checkCounter++;
                    IPAddress compareAddress = IPAddress.Parse(parts[0]);
                    if (nets[index].AddressFamily == compareAddress.AddressFamily)
                    {
                        if (nets[index].IsInSameSubnet(compareAddress, masks[index]))
                        {
                            result.Add(parts);
                            break;
                        }
                    }
                }
            }

            Console.WriteLine("Checks: " + checkCounter);
            Console.WriteLine("Duration: " + (DateTime.Now - start));
            Console.ReadLine();
        }
    }
}

Code:
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;

namespace IpAddressChecker
{
    public static class IPAddressExtensions
    {
        public static IPAddress GetNetworkAddress(this IPAddress address, IPAddress subnetMask)
        {
            byte[] ipAdressBytes = address.GetAddressBytes();
            byte[] subnetMaskBytes = subnetMask.GetAddressBytes();

            if (ipAdressBytes.Length != subnetMaskBytes.Length)
                throw new ArgumentException("Lengths of IP address and subnet mask do not match.");

            byte[] networkAddress = new byte[ipAdressBytes.Length];
            for (int i = 0; i < networkAddress.Length; i++)
            {
                networkAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i]));
            }
            return new IPAddress(networkAddress);
        }

        public static bool IsInSameSubnet(this IPAddress address2, IPAddress address, IPAddress subnetMask)
        {
            IPAddress network1 = address.GetNetworkAddress(subnetMask);
            IPAddress network2 = address2.GetNetworkAddress(subnetMask);

            return network1.Equals(network2);
        }
    }
}

Ich hoffe, hier findet nicht jemand nen groben Fehler. :D
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Falc410
Zurück
Oben