Was KDB + ist, die Q-Programmiersprache, welche Stärken und Schwächen sie haben, finden Sie in meinem vorherigen
Artikel und kurz in der Einleitung. In diesem Artikel implementieren wir einen Dienst für Q, der den eingehenden Datenstrom verarbeitet und verschiedene Aggregationsfunktionen pro Minute im Echtzeitmodus berechnet (dh es bleibt Zeit, alles bis zum nächsten Datenelement zu berechnen). Das Hauptmerkmal von Q ist, dass es sich um eine Vektorsprache handelt, mit der Sie nicht mit einzelnen Objekten arbeiten können, sondern mit ihren Arrays, Arrays von Arrays und anderen komplexen Objekten. Sprachen wie Q und die damit verbundenen Sprachen K, J, APL sind bekannt für ihre Kürze. Oft kann ein Programm, das mehrere Codebildschirme in einer vertrauten Sprache wie Java umfasst, in mehreren Zeilen darauf geschrieben werden. Genau das möchte ich in diesem Artikel demonstrieren.

Einführung
KDB + ist eine Spaltendatenbank, die sich auf sehr große Datenmengen konzentriert und auf bestimmte Weise (hauptsächlich nach Zeit) sortiert ist. Es wird vor allem in Finanzorganisationen eingesetzt - Banken, Investmentfonds, Versicherungsunternehmen. Q-Sprache ist eine interne Sprache von KDB +, mit der Sie effektiv mit diesen Daten arbeiten können. Die Ideologie von Q ist Kürze und Effizienz, während Klarheit geopfert wird. Dies wird durch die Tatsache gerechtfertigt, dass die Vektorsprache in jedem Fall schwer wahrzunehmen ist und die Kürze und der Reichtum der Aufnahme es Ihnen ermöglichen, einen viel größeren Teil des Programms auf einem Bildschirm zu sehen, was letztendlich das Verständnis erleichtert.
In diesem Artikel implementieren wir ein vollwertiges Q-Programm, das Sie vielleicht ausprobieren möchten. Dazu benötigen Sie das aktuelle Q. Sie können die kostenlose 32-Bit-Version von der Website des kx-Unternehmens herunterladen -
www.kx.com . Wenn Sie interessiert sind, finden Sie an derselben Stelle Referenzinformationen zu Q, dem Buch
Q For Mortals und verschiedenen Artikeln zu diesem Thema.
Erklärung des Problems
Es gibt eine Quelle, die alle 25 Millisekunden eine Datentabelle sendet. Da KDB + hauptsächlich im Finanzbereich verwendet wird, gehen wir davon aus, dass es sich um eine
Handelstabelle handelt , in der die folgenden Spalten enthalten sind: Zeit (Zeit in Millisekunden), sym (Firmenname an der Börse -
IBM ,
AAPL , ...), Preis (Preis) durch welche Aktien gekauft wurden), Größe (Transaktionsgröße). Ein Intervall von 25 Millisekunden wird willkürlich gewählt, es ist nicht zu klein und nicht zu groß. Seine Anwesenheit bedeutet, dass die beim Dienst eintreffenden Daten bereits gepuffert sind. Es wäre einfach, die Pufferung auf der Serviceseite zu implementieren, einschließlich der dynamischen Pufferung, abhängig von der aktuellen Last, aber der Einfachheit halber bleiben wir bei einem festen Intervall.
Der Dienst sollte jede Minute für jedes eingehende Zeichen aus der Sym-Spalte eine Reihe von Aggregationsfunktionen zählen - Maximalpreis, Durchschnittspreis, Summengröße usw. nützliche Informationen. Der Einfachheit halber nehmen wir an, dass alle Funktionen inkrementell berechnet werden können, d.h. Um einen neuen Wert zu erhalten, reicht es aus, zwei Zahlen zu kennen - die alte und die eingehende. Beispielsweise haben die Funktionen max, durchschnitt, sum diese Eigenschaft, die Medianfunktion jedoch nicht.
Wir gehen auch davon aus, dass der eingehende Datenstrom nach Zeit geordnet ist. Dies gibt uns die Möglichkeit, nur in letzter Minute zu arbeiten. In der Praxis reicht es aus, mit den aktuellen und vorherigen Minuten arbeiten zu können, falls Aktualisierungen verspätet sind. Der Einfachheit halber werden wir diesen Fall nicht betrachten.
Aggregierte Funktionen
Nachfolgend sind die erforderlichen Aggregatfunktionen aufgeführt. Ich habe sie so weit wie möglich genommen, um die Belastung des Dienstes zu erhöhen:
- hoch - maximaler Preis - maximaler Preis pro Minute.
- Niedrigpreis - Der Mindestpreis pro Minute.
- firstPrice - erster Preis - der erste Preis pro Minute.
- lastPrice - letzter Preis - der letzte Preis pro Minute.
- firstSize - erste Größe - die erste Deal-Größe in einer Minute.
- lastSize - letzte Größe - die letzte Deal-Größe in einer Minute.
- numTrades - count i - die Anzahl der Transaktionen pro Minute.
- Volumen - Summengröße - Die Summe der Transaktionsgrößen pro Minute.
- pvolume - sum price - die Summe der Preise pro Minute, die für avgPrice erforderlich sind.
- Umsatz - Summenpreis * Größe - Gesamtvolumen der Transaktionen pro Minute.
- avgPrice - pvolume% numTrades - Durchschnittspreis pro Minute.
- avgSize - Volumen% numTrades - durchschnittliche Geschäftsgröße pro Minute.
- vwap - Umsatz% Volumen - der durchschnittliche Preis pro Minute, gewichtet nach der Größe der Transaktion.
- cumVolume - Summenvolumen - akkumulierte Transaktionsgröße für die gesamte Zeit.
Besprechen Sie sofort einen nicht offensichtlichen Punkt - wie diese Spalten zum ersten Mal und für jede nächste Minute initialisiert werden. Einige Spalten des Typs firstPrice müssen jedes Mal mit null initialisiert werden, ihr Wert ist nicht definiert. Andere Datenträgertypen müssen immer auf 0 gesetzt werden. Es gibt immer noch Spalten, die einen kombinierten Ansatz erfordern. Beispielsweise muss cumVolume aus der vorherigen Minute und für den ersten auf 0 kopiert werden. Alle diese Parameter werden mithilfe des Datentypwörterbuchs (analog zum Datensatz) festgelegt:
Ich habe dem Wörterbuch zur Vereinfachung sym und time hinzugefügt. Jetzt ist initWith eine fertige Zeile aus der endgültigen aggregierten Tabelle, in der das richtige sym und die richtige Zeit festgelegt werden müssen. Sie können es verwenden, um der Tabelle neue Zeilen hinzuzufügen.
aggCols benötigen wir beim Erstellen einer Aggregatfunktion. Die Liste muss aufgrund der Besonderheiten der Reihenfolge, in der Ausdrücke in Q berechnet werden (von rechts nach links), invertiert werden. Das Ziel besteht darin, eine Berechnung in Richtung von hoch nach cumVolume bereitzustellen, da einige Spalten von den vorherigen Spalten abhängig sind.
Spalten, die von der vorherigen in eine neue Minute kopiert werden sollen, sym-Spalte zur Vereinfachung hinzugefügt:
rollColumns:`sym`cumVolume;
Jetzt teilen wir die Spalten in Gruppen ein, je nachdem, wie sie aktualisiert werden sollen. Drei Typen können unterschieden werden:
- Batterien (Volumen, Umsatz, ..) - Wir müssen den Eingabewert zum vorherigen hinzufügen.
- Mit einem speziellen Punkt (hoch, niedrig, ..) - der erste Wert in einer Minute wird aus den Eingabedaten entnommen, der Rest wird mit der Funktion gezählt.
- Der Rest. Immer mit einer Funktion gezählt.
Definieren Sie Variablen für diese Klassen:
accumulatorCols:`numTrades`volume`pvolume`turnover; specialCols:`high`low`firstPrice`firstSize;
Berechnungsreihenfolge
Wir werden die aggregierte Tabelle in zwei Schritten aktualisieren. Aus Effizienzgründen verkleinern wir zuerst die eingehende Tabelle, sodass für jedes Zeichen und jede Minute eine Zeile übrig bleibt. Die Tatsache, dass alle unsere Funktionen inkrementell und assoziativ sind, garantiert uns, dass sich das Ergebnis dieses zusätzlichen Schritts nicht ändert. Sie können die Tabelle mit der Auswahl drücken:
select high:max price, low:min price … by sym,time.minute from table
Diese Methode hat ein Minus - die Menge der berechneten Spalten ist vordefiniert. Glücklicherweise wird die Auswahl in Q auch als eine Funktion implementiert, mit der Sie dynamisch erstellte Argumente ersetzen können:
?[table;whereClause;byClause;selectClause]
Ich werde das Format der Argumente nicht im Detail beschreiben, in unserem Fall sind nur durch und ausgewählte Ausdrücke nicht trivial und sie sollten Wörterbücher der Formularspalten sein! Ausdrücke. Somit kann die Einschnürungsfunktion wie folgt definiert werden:
selExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover!parse each ("max price";"min price";"first price";"last price";"first size";"last size";"count i";"sum size";"sum price";"sum price*size");
Aus Gründen der Übersichtlichkeit habe ich die Analysefunktion verwendet, die einen String mit einem Q-Ausdruck in einen Wert umwandelt, der an die eval-Funktion übergeben werden kann und der in der Funktionsauswahl erforderlich ist. Beachten Sie auch, dass der Vorprozess als Projektion (d. H. Als Funktion mit teilweise definierten Argumenten) der Auswahlfunktion definiert ist und ein Argument (Tabelle) fehlt. Wenn wir einen Vorprozess auf eine Tabelle anwenden, erhalten wir eine geschrumpfte Tabelle.
In der zweiten Phase wird die aggregierte Tabelle aktualisiert. Zuerst schreiben wir den Algorithmus im Pseudocode:
for each sym in inputTable idx: row index in agg table for sym+currentTime; aggTable[idx;`high]: aggTable[idx;`high] | inputTable[sym;`high]; aggTable[idx;`volume]: aggTable[idx;`volume] + inputTable[sym;`volume]; …
In Q ist es üblich, anstelle von Schleifen Map / Reduce-Funktionen zu verwenden. Da Q jedoch eine Vektorsprache ist und alle Operationen sicher auf alle Symbole gleichzeitig angewendet werden können, können wir in erster Näherung überhaupt auf einen Zyklus verzichten und Operationen mit allen Symbolen gleichzeitig ausführen:
idx:calcIdx inputTable; row:aggTable idx; aggTable[idx;`high]: row[`high] | inputTable`high; aggTable[idx;`volume]: row[`volume] + inputTable`volume; …
Aber wir können noch weiter gehen, in Q gibt es einen einzigartigen und äußerst leistungsfähigen Operator - den generalisierten Zuweisungsoperator. Sie können den Wertesatz in einer komplexen Datenstruktur mithilfe einer Liste von Indizes, Funktionen und Argumenten ändern. In unserem Fall sieht es so aus:
idx:calcIdx inputTable; rows:aggTable idx;
Um einer Tabelle zuzuweisen, benötigen Sie leider eine Liste von Zeilen, keine Spalten, und Sie müssen die Matrix (Liste der Spalten in eine Liste von Zeilen) mithilfe der Flip-Funktion transponieren. Für eine große Tabelle ist dies nicht erforderlich. Stattdessen wenden wir die verallgemeinerte Zuordnung mithilfe der Kartenfunktion (die wie ein Apostroph aussieht) separat auf jede Spalte an:
.[aggTable;;:;]'[(idx;)each aggCols; (row[`high] | inputTable`high;row[`volume] + inputTable`volume;…)];
Wir verwenden wieder die Projektionsfunktion. Beachten Sie auch, dass das Erstellen einer Liste in Q auch eine Funktion ist und wir sie mit der Funktion each (map) aufrufen können, um eine Liste von Listen zu erhalten.
Damit die Menge der berechneten Spalten nicht festgelegt wird, erstellen Sie den obigen Ausdruck dynamisch. Zunächst definieren wir die Funktionen zur Berechnung jeder Spalte, wobei wir die Variablen row und inp verwenden, um auf aggregierte Daten und Eingabedaten zu verweisen:
aggExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`avgPrice`avgSize`vwap`cumVolume! ("row[`high]|inp`high";"row[`low]&inp`low";"row`firstPrice";"inp`lastPrice";"row`firstSize";"inp`lastSize";"pvolume%numTrades";"volume%numTrades";"turnover%volume";"row[`cumVolume]+inp`volume");
Einige Spalten sind speziell, ihr erster Wert sollte nicht von einer Funktion berechnet werden. Wir können feststellen, dass es das erste in der Spaltenzeile [`numTrades] ist - wenn es 0 hat, ist der Wert zuerst. Q hat eine Auswahlfunktion -? [Boolesche Liste; Liste1; Liste2] - die einen Wert aus Liste 1 oder 2 auswählt, abhängig von der Bedingung im ersten Argument:
Hier habe ich mit meiner Funktion eine generische Zuordnung aufgerufen (Ausdruck in geschweiften Klammern). Der aktuelle Wert (das erste Argument) und ein zusätzliches Argument, das ich im 4. Parameter übergebe, werden an ihn übergeben.
Separat fügen wir Batterielautsprecher hinzu, da für sie die gleiche Funktion gilt:
Dies ist eine übliche Zuordnung nach den Standards von Q, ich weise nur eine Liste von Werten gleichzeitig zu. Erstellen Sie schließlich die Hauptfunktion:
Mit diesem Ausdruck erstelle ich dynamisch eine Funktion aus einer Zeichenfolge, die den oben zitierten Ausdruck enthält. Das Ergebnis sieht folgendermaßen aus:
{[aggTable;idx;inp] rows:aggTable idx; isFirst:0=row`numTrades; .[aggTable;;:;]'[(idx;)each aggCols ;(cumVolume:row[`cumVolume]+inp`cumVolume;… ; high:?[isFirst;inp`high;row[`high]|inp`high])]}
Die Berechnungsreihenfolge der Spalten ist invertiert, da in Q die Berechnungsreihenfolge von rechts nach links ist.
Jetzt haben wir zwei Hauptfunktionen, die für Berechnungen erforderlich sind. Es bleibt noch ein wenig Infrastruktur hinzuzufügen, und der Dienst ist bereit.
Letzte Schritte
Wir haben Preprocess- und UpdateAgg-Funktionen, die die ganze Arbeit erledigen. Es ist jedoch weiterhin erforderlich, den korrekten Übergang in Minuten sicherzustellen und die Indizes für die Aggregation zu berechnen. Zuerst definieren wir die Init-Funktion:
init:{ tradeAgg:: 0#enlist[initWith];
Wir definieren auch die Rollfunktion, die die aktuelle Minute ändert:
roll:{[tm] if[currTime>tm; :init[]];
Wir brauchen eine Funktion, um neue Zeichen hinzuzufügen:
addSyms:{[syms] currSyms,::syms;
Und schließlich die Upd-Funktion (der traditionelle Name dieser Funktion für Q-Services), die vom Client aufgerufen wird, um Daten hinzuzufügen:
upd:{[tblName;data]
Das ist alles. Hier ist der vollständige Code unseres Service, wie versprochen, nur ein paar Zeilen:
initWith:`sym`time`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover`avgPrice`avgSize`vwap`cumVolume!(`;00:00;0n;0n;0n;0n;0N;0N;0;0;0.0;0.0;0n;0n;0n;0); aggCols:reverse key[initWith] except `sym`time; rollColumns:`sym`cumVolume; accumulatorCols:`numTrades`volume`pvolume`turnover; specialCols:`high`low`firstPrice`firstSize; selExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover!parse each ("max price";"min price";"first price";"last price";"first size";"last size";"count i";"sum size";"sum price";"sum price*size"); preprocess:?[;();`sym`time!`sym`time.minute;selExpression]; aggExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`avgPrice`avgSize`vwap`cumVolume!("row[`high]|inp`high";"row[`low]&inp`low";"row`firstPrice";"inp`lastPrice";"row`firstSize";"inp`lastSize";"pvolume%numTrades";"volume%numTrades";"turnover%volume";"row[`cumVolume]+inp`volume"); @[`aggExpression;specialCols;{"?[isFirst;inp`",y,";",x,"]"};string specialCols]; aggExpression[accumulatorCols]:{"row[`",x,"]+inp`",x } each string accumulatorCols; updateAgg:value "{[aggTable;idx;inp] row:aggTable idx; isFirst:0=row`numTrades; .[aggTable;;:;]'[(idx;)each aggCols;(",(";"sv string[aggCols],'":",/:aggExpression aggCols),")]}"; / ' init:{ tradeAgg::0#enlist[initWith]; currTime::00:00; currSyms::`u#`symbol$(); offset::0; rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg; }; roll:{[tm] if[currTime>tm; :init[]]; rollCache,::offset _ rollColumns#tradeAgg; offset::count tradeAgg; currSyms::`u#`$(); }; addSyms:{[syms] currSyms,::syms; `tradeAgg upsert @[count[syms]#enlist initWith;`sym`time,cols rc;:;(syms;currTime),(initWith cols rc)^value flip rc:rollCache ([] sym: syms)]; }; upd:{[tblName;data] updMinute[data] each exec distinct time from data:() xkey preprocess data}; updMinute:{[data;tm] if[tm<>currTime; roll tm; currTime::tm]; data:select from data where time=tm; if[count msyms:syms where not (syms:data`sym)in currSyms; addSyms msyms]; updateAgg[`tradeAgg;offset+currSyms?syms;data]; };
Testen
Überprüfen Sie die Leistung des Dienstes. Führen Sie dazu einen separaten Prozess aus (fügen Sie den Code in die Datei service.q ein) und rufen Sie die Init-Funktion auf:
q service.q –p 5566 q)init[]
Starten Sie in einer anderen Konsole den zweiten Q-Prozess und stellen Sie eine Verbindung zum ersten her:
h:hopen `:host:5566 h:hopen 5566
Erstellen Sie zunächst eine Liste mit 10.000 Zeichen und fügen Sie eine Funktion zum Erstellen einer zufälligen Tabelle hinzu. In der zweiten Konsole:
syms:`IBM`AAPL`GOOG,-9997?`8 rnd:{[n;t] ([] sym:n?syms; time:t+asc n#til 25; price:n?10f; size:n?10)}
Ich habe der Liste der Zeichen drei echte Zeichen hinzugefügt, um die Suche in der Tabelle zu vereinfachen. Die Funktion rnd erstellt eine zufällige Tabelle mit n Zeilen, wobei die Zeit von t bis t + 25 Millisekunden variiert.
Jetzt können Sie versuchen, Daten an den Dienst zu senden (fügen Sie die ersten zehn Stunden hinzu):
{h (`upd;`trade;rnd[10000;x])} each `time$00:00 + til 60*10
Sie können im Dienst überprüfen, ob die Tabelle aktualisiert wurde:
\c 25 200 select from tradeAgg where sym=`AAPL -20#select from tradeAgg where sym=`AAPL
Ergebnis:
sym|time|high|low|firstPrice|lastPrice|firstSize|lastSize|numTrades|volume|pvolume|turnover|avgPrice|avgSize|vwap|cumVolume --|--|--|--|--|-------------------------------- AAPL|09:27|9.258904|9.258904|9.258904|9.258904|8|8|1|8|9.258904|74.07123|9.258904|8|9.258904|2888 AAPL|09:28|9.068162|9.068162|9.068162|9.068162|7|7|1|7|9.068162|63.47713|9.068162|7|9.068162|2895 AAPL|09:31|4.680449|0.2011121|1.620827|0.2011121|1|5|4|14|9.569556|36.84342|2.392389|3.5|2.631673|2909 AAPL|09:33|2.812535|2.812535|2.812535|2.812535|6|6|1|6|2.812535|16.87521|2.812535|6|2.812535|2915 AAPL|09:34|5.099025|5.099025|5.099025|5.099025|4|4|1|4|5.099025|20.3961|5.099025|4|5.099025|2919
Wir werden nun Lasttests durchführen, um herauszufinden, wie viele Daten der Dienst pro Minute verarbeiten kann. Ich möchte Sie daran erinnern, dass wir das Intervall für Aktualisierungen auf 25 Millisekunden festgelegt haben. Dementsprechend sollte ein Dienst (im Durchschnitt) in mindestens 20 Millisekunden pro Aktualisierung passen, damit Benutzer Zeit haben, Daten anzufordern. Geben Sie im zweiten Vorgang Folgendes ein:
tm:10:00:00.000 stressTest:{[n] 1 string[tm]," "; times,::h ({st:.zT; upd[`trade;x]; .zT-st};rnd[n;tm]); tm+:25} start:{[n] times::(); do[4800;stressTest[n]]; -1 " "; `min`avg`med`max!(min times;avg times;med times;max times)}
4800 ist zwei Minuten. Sie können versuchen, zuerst alle 25 Millisekunden 1000 Zeilen zu starten:
start 1000
In meinem Fall liegt das Ergebnis bei einigen Millisekunden pro Update. Daher werde ich die Anzahl der Zeilen sofort auf 10.000 erhöhen:
start 10000
Ergebnis:
min| 00:00:00.004 avg| 9.191458 med| 9f max| 00:00:00.030
Wieder nichts Besonderes, aber das sind 24 Millionen Zeilen pro Minute, 400 Tausend pro Sekunde. Für mehr als 25 Millisekunden wurde das Update nur fünfmal verlangsamt, anscheinend beim Ändern der Minute. Erhöhung auf 100.000:
start 100000
Ergebnis:
min| 00:00:00.013 avg| 25.11083 med| 24f max| 00:00:00.108 q)sum times 00:02:00.532
Wie Sie sehen können, kommt der Dienst kaum zurecht, schafft es aber dennoch, über Wasser zu bleiben. Diese Datenmenge (240 Millionen Zeilen pro Minute) ist extrem groß. In solchen Fällen ist es üblich, mehrere Klone (oder sogar Dutzende von Klonen) des Dienstes auszuführen, von denen jeder nur einen Teil der Zeichen verarbeitet. Trotzdem ist das Ergebnis beeindruckend für die interpretierte Sprache, die sich hauptsächlich auf die Datenspeicherung konzentriert.
Es kann sich die Frage stellen, warum die Zeit nicht linear mit der Größe jedes Updates wächst. Der Grund ist, dass die Quetschfunktion tatsächlich eine C-Funktion ist, die viel effizienter arbeitet als updateAgg. Beginnend mit einer Update-Größe (ca. 10.000) erreicht updateAgg seine Obergrenze und die Ausführungszeit hängt nicht von der Größe des Updates ab. Aufgrund des vorläufigen Schritts Q ist der Dienst in der Lage, solche Datenmengen zu verarbeiten. Dies unterstreicht, wie wichtig es ist, bei der Arbeit mit Big Data den richtigen Algorithmus auszuwählen. Ein weiterer Punkt ist die korrekte Speicherung von Daten im Speicher. Wenn die Daten nicht in Spalten gespeichert oder nicht nach Zeit geordnet wären, würden wir so etwas wie einen TLB-Cache-Fehler kennenlernen - das Fehlen einer Speicherseitenadresse im Prozessoradresscache. Das Auffinden der Adresse dauert im Fehlerfall etwa 30-mal länger und kann bei verstreuten Daten den Dienst mehrmals verlangsamen.
Fazit
In diesem Artikel habe ich gezeigt, dass die KDB + - und Q-Datenbank nicht nur zum Speichern von Big Data und zum einfachen Zugriff über Select geeignet ist, sondern auch zum Erstellen von Datenverarbeitungsdiensten, die Hunderte Millionen Zeilen / Gigabyte Daten selbst in einem einzigen Q-Prozess verarbeiten können . Die Q-Sprache selbst ermöglicht aufgrund ihrer Vektornatur, des eingebauten Interpreters des SQL-Dialekts und einer sehr erfolgreichen Reihe von Bibliotheksfunktionen eine äußerst kurze und effiziente Implementierung von Algorithmen im Zusammenhang mit der Datenverarbeitung.
Ich werde feststellen, dass das oben Genannte nur ein Teil der Fähigkeiten von Q ist, es hat andere einzigartige Eigenschaften. Zum Beispiel ein extrem einfaches IPC-Protokoll, das die Grenze zwischen einzelnen Q-Prozessen aufhebt und es Ihnen ermöglicht, Hunderte dieser Prozesse in einem einzigen Netzwerk zu kombinieren, das sich auf Dutzenden von Servern in verschiedenen Teilen der Welt befinden kann.