Im neuen Firebird-Newsletter sind einige interessante Vergleiche zwischen Firebird 3 und 4 aufgelistet. Ein Blick lohnt sich.
https://ib-aid.com/en/articles/firebird ... er-issue-1
Gruß
Martin
Firebird Performance Newsletter: Ausgabe 1
Moderator: martin.koeditz
- martin.koeditz
- Beiträge: 448
- Registriert: Sa 31. Mär 2018, 14:35
Martin Köditz
it & synergy GmbH
it & synergy GmbH
Ich habe mir nun den Bericht mal durchgelesen und frage mich, mit welchen Methoden auf die DB zugegriffen wird. Ich vermute mal, mit den nativen isc-Methoden.
Als App-Entwickler stehen mir diese aber nicht zur Verfügung:
a) sie sind viel zu aufwändig
b) sie sind mit aktuellen ADO-Methoden nicht verwendbar.
Somit ist man auf entsprechende Treiber angewiesen:
- ODBC
- OLEDB
- .Net
- usw.
Diese kapseln die isc-Routinen und stellen komfortable Objekte zur Verfügung (siehe PHP
) .
Aus reiner Anwendungssicht und dem zusätzlichen Aufwand der Anwendung kann ich diese Zahlen gar nicht erreichen, was sich durchaus an der CPU-Last des FB-Servers widerspiegelt.
Genau diese Woche hatte ich in einem Nicht-FB-Scenario vergleichbare Anforderungen (zu FB komme ich dann noch):
Eine AS/400 (System i, IBM i) dient als ERP-System, dessen Daten in ein BI-System geladen werden. Da die AS/400 angeblich die Daten nicht schnell genug bereitstellen konnte wurde ein SQL-Server als DWH dazwischengeschaltet. Mittels SSIS-Paketen erfolgt nun die tägliche vollständige Synchronisation.
Diese dauert nun unendlich lange, da sich keine der beiden Seiten die technischen Möglichkeiten auch nut angesehen hat.
Jeweils separat wurde ein IBM-Spezi und ein Microsoft-Spezi herangezogen, die nun jeder für sich dem Anderen mitteilten, dass sein System fast nichts zu tun hat. Es wurde gar nicht erst die Methoden selber untersucht.
Nun wurde ich denn gefragt, ob ich da mal was analysieren kann.
In der Folge habe ich ein kleines .Net-Programm, dass ich bei Interesse gerne zur Verfügung stelle, dass nun native beide Seiten betrachtet.
Alles mit den offiziellen .Net-Methoden:
Fall 1)
Um den Datendurchsatz zu testen habe ich rein die DataReader nur per Read() ohne die Daten selber auszulesen, abgefragt.
Der Durchsatz entsprach meinen Erwartungen: In Abhängigkeit des jeweiligen Rowvolumens konnten über das Netz zwischen 25.000 bis 125.000 Reads / Sekunde durchgeführt werden.
Der Kunde hat dies dann mal mit 5 parallelen Abfragen durchgeführt und konnte eine Übertragungsrate von 600MBit/Sekunde erreichen.
Fall 2)
Neben dem Read() habe ich dann zusätzlich eine Miniverarbeitung durchgeführt, also per GetValues() die Daten ausgelesen.
Der Durchsatz sank damit auf ca. 10.000 - 15.000 Zeilen/Sekunde.
Dies deutet also eher auf eine schwache CPU oder Hauptspeicherleistung hin oder ein Problem der .Net-Runtime.
DIes ließ sich aber durch die 5 parallelen Jobs liniar verbessern.
Dies war die Quelle.
Nun zum SQL-Server.
Fall 1)
Daten aus der Quelle komplett lesen, und anschließend per einzelnen Insert in den SQL-Server hochladen.
Durchsatz beim Lesen siehe oben Fall2, Insert ca. 1200 Zeilen Sekunde.
Fall 2)
Daten beim Lesen linear direkt in den SQL-Server schieben, wen wunderts: Lesen und Schreiben mit ca. 1200 Zeilen/Sekunde.
Nun bietet aber der SQL-Server in .Net einen sog, SqlBulCopy an. Diesem übergibt man entweder eine DataTable oder einen Reader.
Fall 1)
Laden einer DataTable per DataTable.Load(Reader), import ca. 3500 Zeilen/Sekunde. Aufruf Bulkcopy mit Datatable als Quelle: Insert ca. 23.000 - 25.000 Zeilen/Sekunde.
Fall 2)
Laden einer DataTable per DbDataAdapter mit der Fill(DataTable) Methode, Import, oh staune, 10.000 - 15.000 Zeilen/Sekunde, also Quellgeschwindigkeit. Der Insert in den SQL-Server hat sich nicht geändert.
Fall 3)
Übergabe des Readers an den BulkCopy, Lesen/Schreiben mit 10.000 - 15.000 Zeilen/Sekunde, also wieder Quellgeschwindigkeit.
Nach Analyse des SSIS-Paketes konnte ich feststellen, dass die Methode "Lesen von Quelle, Insert je Zeile" mit dem entsprechenden Durchsatz gewählt wurde.
Liest man nun die SSIS-Doku, wird native allerdings ein BulkCopy nur beim CSV-Import unterstützt. Hä?
Gut, noch ein wenig gesucht und gefunden:
SSIS unterstützt auch den Aufruf eines Scripts, also die Verwendung von .Net-Code. Mit diesem könnte man nun die Lesegeschwindigkeit zum Upload per BulkCopy verwenden. Mit parallelen Tasks, so 8-10, kommt der SQL-Server wahrscheinlich ins Schwitzen.
Nun komme ich zu inserer allseits beliebten Firebird (2.5 in Classic-Mode, also Prozess je Connection, oder 3.0 macht da kaum Unterschied).
In meinem ETL-Prozess verwende ich noch den guten 64-Bit-ODBC-Treiber:
Fall 1)
Nur Lesen, ca. 35.000 Zeilen/Sekunde.
Fall 2)
Lesen und in Liste speichern, 9500 Zeilen/Sekunde.
Fand ich in Ordnung, aber ich dachte, .Net kann es schneller.
Noch hatte ich meine modifzierte Version 6.8.0.0.
Fall 1)
Nur Lesen, ca. 12.000 Zeilen/Sekunde.
Fall 2)
Lesen und speichern, 8500 Zeilen/Sekunde.
Nun, das fand ich doch unbefriedigend, also habe ich die Version 7.10.0.1 runtergeladen (weil ich nicht überall 4.8 habe):
Fall 1)
Nur Lesen, ca. 35.000 Zeilen Sekunde.
Fall 2)
Lesen und speichern, ca. 9.500 Zeilen/Sekunde.
Da die Quellen ja nun mal vorliegen, habe ich mir mal angesehen, was da beim GetValues() so passiert, und sie da, es werden ein paar Funktionen je Spalte aufgerufen, die je Zeile abe schon mal aufgerufen waren.
Dies habe ich dann modifiziert und die Prüfungen umgangen.
Der Fall 2) lud mir die Daten nun mit 12.000 Zeilen/Sekunde, also 30% schneller.
Dies habe ich im Firebird-Tracker bereits eingestellt.
Da gibts bestimmt noch mehr Potential, ich werde berichten, denn der Insert-Teil mit dm neueren Treiber fehlt ja noch.
Aktuell schaft mein ETL mit ODBC ca. 1.500 - 2.500 Inserts/Sekunde. Das kann noch nicht das Ende sein!
Als App-Entwickler stehen mir diese aber nicht zur Verfügung:
a) sie sind viel zu aufwändig
b) sie sind mit aktuellen ADO-Methoden nicht verwendbar.
Somit ist man auf entsprechende Treiber angewiesen:
- ODBC
- OLEDB
- .Net
- usw.
Diese kapseln die isc-Routinen und stellen komfortable Objekte zur Verfügung (siehe PHP
![Fetzig 8-)](./images/smilies/icon_cool.gif)
Aus reiner Anwendungssicht und dem zusätzlichen Aufwand der Anwendung kann ich diese Zahlen gar nicht erreichen, was sich durchaus an der CPU-Last des FB-Servers widerspiegelt.
Genau diese Woche hatte ich in einem Nicht-FB-Scenario vergleichbare Anforderungen (zu FB komme ich dann noch):
Eine AS/400 (System i, IBM i) dient als ERP-System, dessen Daten in ein BI-System geladen werden. Da die AS/400 angeblich die Daten nicht schnell genug bereitstellen konnte wurde ein SQL-Server als DWH dazwischengeschaltet. Mittels SSIS-Paketen erfolgt nun die tägliche vollständige Synchronisation.
Diese dauert nun unendlich lange, da sich keine der beiden Seiten die technischen Möglichkeiten auch nut angesehen hat.
Jeweils separat wurde ein IBM-Spezi und ein Microsoft-Spezi herangezogen, die nun jeder für sich dem Anderen mitteilten, dass sein System fast nichts zu tun hat. Es wurde gar nicht erst die Methoden selber untersucht.
Nun wurde ich denn gefragt, ob ich da mal was analysieren kann.
In der Folge habe ich ein kleines .Net-Programm, dass ich bei Interesse gerne zur Verfügung stelle, dass nun native beide Seiten betrachtet.
Alles mit den offiziellen .Net-Methoden:
Fall 1)
Um den Datendurchsatz zu testen habe ich rein die DataReader nur per Read() ohne die Daten selber auszulesen, abgefragt.
Der Durchsatz entsprach meinen Erwartungen: In Abhängigkeit des jeweiligen Rowvolumens konnten über das Netz zwischen 25.000 bis 125.000 Reads / Sekunde durchgeführt werden.
Der Kunde hat dies dann mal mit 5 parallelen Abfragen durchgeführt und konnte eine Übertragungsrate von 600MBit/Sekunde erreichen.
Fall 2)
Neben dem Read() habe ich dann zusätzlich eine Miniverarbeitung durchgeführt, also per GetValues() die Daten ausgelesen.
Der Durchsatz sank damit auf ca. 10.000 - 15.000 Zeilen/Sekunde.
Dies deutet also eher auf eine schwache CPU oder Hauptspeicherleistung hin oder ein Problem der .Net-Runtime.
DIes ließ sich aber durch die 5 parallelen Jobs liniar verbessern.
Dies war die Quelle.
Nun zum SQL-Server.
Fall 1)
Daten aus der Quelle komplett lesen, und anschließend per einzelnen Insert in den SQL-Server hochladen.
Durchsatz beim Lesen siehe oben Fall2, Insert ca. 1200 Zeilen Sekunde.
Fall 2)
Daten beim Lesen linear direkt in den SQL-Server schieben, wen wunderts: Lesen und Schreiben mit ca. 1200 Zeilen/Sekunde.
Nun bietet aber der SQL-Server in .Net einen sog, SqlBulCopy an. Diesem übergibt man entweder eine DataTable oder einen Reader.
Fall 1)
Laden einer DataTable per DataTable.Load(Reader), import ca. 3500 Zeilen/Sekunde. Aufruf Bulkcopy mit Datatable als Quelle: Insert ca. 23.000 - 25.000 Zeilen/Sekunde.
Fall 2)
Laden einer DataTable per DbDataAdapter mit der Fill(DataTable) Methode, Import, oh staune, 10.000 - 15.000 Zeilen/Sekunde, also Quellgeschwindigkeit. Der Insert in den SQL-Server hat sich nicht geändert.
Fall 3)
Übergabe des Readers an den BulkCopy, Lesen/Schreiben mit 10.000 - 15.000 Zeilen/Sekunde, also wieder Quellgeschwindigkeit.
Nach Analyse des SSIS-Paketes konnte ich feststellen, dass die Methode "Lesen von Quelle, Insert je Zeile" mit dem entsprechenden Durchsatz gewählt wurde.
Liest man nun die SSIS-Doku, wird native allerdings ein BulkCopy nur beim CSV-Import unterstützt. Hä?
Gut, noch ein wenig gesucht und gefunden:
SSIS unterstützt auch den Aufruf eines Scripts, also die Verwendung von .Net-Code. Mit diesem könnte man nun die Lesegeschwindigkeit zum Upload per BulkCopy verwenden. Mit parallelen Tasks, so 8-10, kommt der SQL-Server wahrscheinlich ins Schwitzen.
Nun komme ich zu inserer allseits beliebten Firebird (2.5 in Classic-Mode, also Prozess je Connection, oder 3.0 macht da kaum Unterschied).
In meinem ETL-Prozess verwende ich noch den guten 64-Bit-ODBC-Treiber:
Fall 1)
Nur Lesen, ca. 35.000 Zeilen/Sekunde.
Fall 2)
Lesen und in Liste speichern, 9500 Zeilen/Sekunde.
Fand ich in Ordnung, aber ich dachte, .Net kann es schneller.
Noch hatte ich meine modifzierte Version 6.8.0.0.
Fall 1)
Nur Lesen, ca. 12.000 Zeilen/Sekunde.
Fall 2)
Lesen und speichern, 8500 Zeilen/Sekunde.
Nun, das fand ich doch unbefriedigend, also habe ich die Version 7.10.0.1 runtergeladen (weil ich nicht überall 4.8 habe):
Fall 1)
Nur Lesen, ca. 35.000 Zeilen Sekunde.
Fall 2)
Lesen und speichern, ca. 9.500 Zeilen/Sekunde.
Da die Quellen ja nun mal vorliegen, habe ich mir mal angesehen, was da beim GetValues() so passiert, und sie da, es werden ein paar Funktionen je Spalte aufgerufen, die je Zeile abe schon mal aufgerufen waren.
Dies habe ich dann modifiziert und die Prüfungen umgangen.
Der Fall 2) lud mir die Daten nun mit 12.000 Zeilen/Sekunde, also 30% schneller.
Dies habe ich im Firebird-Tracker bereits eingestellt.
Da gibts bestimmt noch mehr Potential, ich werde berichten, denn der Insert-Teil mit dm neueren Treiber fehlt ja noch.
Aktuell schaft mein ETL mit ODBC ca. 1.500 - 2.500 Inserts/Sekunde. Das kann noch nicht das Ende sein!
Die Testmethode ist gleich im zweiten Absatz verlinkt simple INSERT/UPDATE/DELETE test und unter dem Diagramm nochmal.
Ist reines SQL, was in einem Skript ausgeführt wird. Die einzelnen execute blocks für insert, update und delete könnte man auch in einem DB-Adminprogramm ausführen. Da die Million Operationen jeweils innerhalb eines execute blocks ausgeführt werden, ist es wurst, welche Client-SW verwendet wird, um das Skript oder die EBs auszuführen.
Es ging dabei nicht um die Optimierung von bulk load von außen, sondern um einen Serverperformance-benchmark für die drei Basisoperationen insert/update/delete in einem typischen Szenario. Eine Tabelle mit 8 Spalten, unterschiedliche Spaltentypen und einige Indizes drauf.
Grüße, vr
Danke für die Antwort. Aber das ist ja absolut ein praxisferner Test.
Wenn mit gesagt wird, dass eine DB um 10% schneller ist als vorher, ich aber meine Daten nicht um 10% schneller in die DB bekomme, ist das Augenwischerei.
Klar ist auch, dass ein "for I = 1 to 1000000:next" unheimlich schnell ist, aber was bringt mir das?
Zum .Net-Treiber kann ich auch nur feststellen, dass ich alleine die Readperformance von 9000 Zeilen/Sekunde auf 15500 Zeilen/Sekunde mit auslesen der Daten optimieren konnte. Der ODBC-Treiber schafft da auch nur 10000/Sekunde. Dies gilt ebenso für z.B. DataAdapter.Fill.
Wenn man im Treiber sieht, dass mit jedem Spaltenzugriff die Arraygrenzen geprüft werden um dieselbe Exception auszulösen, die die CLR-Runtime sowieso auslöst. Bei 100.000 Zeilen mal 100 Spalten macht das gut 20Mio unnötige Prüfungen (IsDBNull-Abfrage + GetValue). Und auch die kosten Zeit.
Wenn mit gesagt wird, dass eine DB um 10% schneller ist als vorher, ich aber meine Daten nicht um 10% schneller in die DB bekomme, ist das Augenwischerei.
Klar ist auch, dass ein "for I = 1 to 1000000:next" unheimlich schnell ist, aber was bringt mir das?
Zum .Net-Treiber kann ich auch nur feststellen, dass ich alleine die Readperformance von 9000 Zeilen/Sekunde auf 15500 Zeilen/Sekunde mit auslesen der Daten optimieren konnte. Der ODBC-Treiber schafft da auch nur 10000/Sekunde. Dies gilt ebenso für z.B. DataAdapter.Fill.
Wenn man im Treiber sieht, dass mit jedem Spaltenzugriff die Arraygrenzen geprüft werden um dieselbe Exception auszulösen, die die CLR-Runtime sowieso auslöst. Bei 100.000 Zeilen mal 100 Spalten macht das gut 20Mio unnötige Prüfungen (IsDBNull-Abfrage + GetValue). Und auch die kosten Zeit.
Du kannst damit die Hardware oder Virtualisierung beurteilen, auf der Dein Server läuft, und teilweise seine Konfiguration (anzahl pages, forced writes usw). Und Du hast eine Hausnummer, was ein Upgrade auf Firebird 4 in einem typischen Szenario bringt. Inserts in einer Schleife sind auch nicht unheimlich schnell, gerade das ist ein Bereich, der nicht beliebig skaliert, das zeigt Dir der Test auch. Alles mitten in der ETL wird dadurch gut repräsentiert, es ging nicht um bulk load von außen. Das wäre natürlich auch spannend, ist aber eine andere Geschichte.bfuerchau hat geschrieben: ↑Sa 10. Apr 2021, 06:57 Danke für die Antwort. Aber das ist ja absolut ein praxisferner Test.
Wenn mit gesagt wird, dass eine DB um 10% schneller ist als vorher, ich aber meine Daten nicht um 10% schneller in die DB bekomme, ist das Augenwischerei.
Klar ist auch, dass ein "for I = 1 to 1000000:next" unheimlich schnell ist, aber was bringt mir das?
Grüße, vr
Ja, einen Bulkload vesuche ich derzeit schon. Da ist der SQL-Server leider viel weiter, da in .Net eine eigene Klasse SqlBulkCopy angeboten wird.
Meine Tests laufen im Moment ganz gut.
Je nach Spaltenanzahl und Feldlängen varieirt dies derzeit zwischen 2.500 bis 8000 Inserts/Sekunde. Der SQL-Selver liegt da im Mittel bei bereits 20-25.000 Zeilen/Sekunde.
Für ETL müssen wir da noch irgendwas machen, ich berichte weiter und kann dann auch die verwendeten Verfahren vorstellen.
Ein im Internet gefundenes Beispiel mit einem
insert into table (f1, ..., fn)
select ?, ..., ? from rdb$datatbase
union all select ?, ..., ? from rdb$datatbase
union all select ?, ..., ? from rdb$datatbase
:
Scheitert an folgenden Problemen:
Beim einzelnen Select spielt die Typisierung keine Rolle, da die Types vom Ziel übernommen werden.
Bei mehreren Selects muss eine Typisierung "cast(? as ...)" erfolgen. Wahrscheinlich erfolgt eben erst die Prüfung des Union-Selects bevor die Zieltypen abgefragt werden.
Das nächste Problem scheitert an einer seltsamen Meldung, dass die Buffersize nicht ausreicht. Ich vermute hier die 32-K-Rowsize-Grenze obwohl diese ja nur zeilenweise und nicht blockweise erfolgt.
Es liegt dann hier unter Umständen an der Anzahl Parameter.
Zusätzlich sinkt der Durchsatz geradezu dramatisch.
Aber es gibt ja noch mehr Möglichkeiten.
Meine Tests laufen im Moment ganz gut.
Je nach Spaltenanzahl und Feldlängen varieirt dies derzeit zwischen 2.500 bis 8000 Inserts/Sekunde. Der SQL-Selver liegt da im Mittel bei bereits 20-25.000 Zeilen/Sekunde.
Für ETL müssen wir da noch irgendwas machen, ich berichte weiter und kann dann auch die verwendeten Verfahren vorstellen.
Ein im Internet gefundenes Beispiel mit einem
insert into table (f1, ..., fn)
select ?, ..., ? from rdb$datatbase
union all select ?, ..., ? from rdb$datatbase
union all select ?, ..., ? from rdb$datatbase
:
Scheitert an folgenden Problemen:
Beim einzelnen Select spielt die Typisierung keine Rolle, da die Types vom Ziel übernommen werden.
Bei mehreren Selects muss eine Typisierung "cast(? as ...)" erfolgen. Wahrscheinlich erfolgt eben erst die Prüfung des Union-Selects bevor die Zieltypen abgefragt werden.
Das nächste Problem scheitert an einer seltsamen Meldung, dass die Buffersize nicht ausreicht. Ich vermute hier die 32-K-Rowsize-Grenze obwohl diese ja nur zeilenweise und nicht blockweise erfolgt.
Es liegt dann hier unter Umständen an der Anzahl Parameter.
Zusätzlich sinkt der Durchsatz geradezu dramatisch.
Aber es gibt ja noch mehr Möglichkeiten.
Nun, mit den von mir entwickelten und mit .Net unterstützten Verfahren konnte ich eine DB-Kopie, die ich mal für Notfälle erstellt hatte, um den Faktor 2,5 beschleunigen. Die Kopie ist dann eforderlich, wenn gback auch nach gfix einfach scheitert. Die reinen Datentabellen lassen sich dann immer noch einfach auslesen, was der gback jedoch nicht tut.
Ein BulkCopy ist es letztlich nicht geworden, jeodch mit den .Net-Methoden
Parallel.ForEach(reder, writer)
wird der Vorgang schnell und zügig durchgeführt.
Per Reader, der eine Enumeration des DbDataReaders zur Verfügung stellt und einem parallelen Writer, der über getrennte Connections und Inserts die Daten lädt, konnte ich je nach Anzahl Spalten einen Durchsatz von 1000 - 6000 Inserts/Sekunde über 4 Threads Hauptthreads erreichen.
Ein BulkCopy ist es letztlich nicht geworden, jeodch mit den .Net-Methoden
Parallel.ForEach(reder, writer)
wird der Vorgang schnell und zügig durchgeführt.
Per Reader, der eine Enumeration des DbDataReaders zur Verfügung stellt und einem parallelen Writer, der über getrennte Connections und Inserts die Daten lädt, konnte ich je nach Anzahl Spalten einen Durchsatz von 1000 - 6000 Inserts/Sekunde über 4 Threads Hauptthreads erreichen.
Lass uns bitte einen neuen Thread dafür aufmachen und unsere Erfahrungen da austauschen. Ich hab vor 3 Jahren mal zu dem Thema gepostet, da ging es aber nur um das Firebird 4 bulk api. Über das OOAPI von Firebird 3 ist ebenfalls ein bulk load möglich, was erheblich schneller ist als insert in Schleife, aber sehr zäh, da es Doku dazu fast nur für C++ gibt, dazu hatte ich Firebird 3 UDR in Pascal/Delphi gepostet. Mit scripting im execute block und Parallelisierung kam ich auch weiter, das bringt jeweils auch eine ganze Menge. Aber wie gesagt besser neuer Thread, wir sind ziemlich OT im Moment
![Zwinkern ;-)](./images/smilies/icon_e_wink.gif)
Grüße vr
Da du ja Moderator bist, könntest du die Beiträge außer dem Ersten einfach in einen neuen Thread verschieben?
Ich möchte dann auf deine letzte Antwort noch etwas dazu sagen.
Ich möchte dann auf deine letzte Antwort noch etwas dazu sagen.