Laden...

Grobarchitektur Server / Datenzugriff

Erstellt von Luth vor 15 Jahren Letzter Beitrag vor 15 Jahren 2.290 Views
L
Luth Themenstarter:in
36 Beiträge seit 2006
vor 15 Jahren
Grobarchitektur Server / Datenzugriff

verwendetes Datenbanksystem: <PostgreSQL 8.3 mit NPGSQL 2.0 Data Provider>

Hallo zusammen.

Ich überlege und experimentiere jetzt schon eine Weile mit verschiedenen Varianten eines Servers rum, stecke aber irgendwie fest und könnte ein paar Denkanstöße gebrauchen.

Rahmenbedingungen sind wie folgt:

  1. TCP/IP-Server mit eigenem Protokoll und dahinter liegender Datenbank
  2. Anzahl handhabbarer Clients bzw. Anfragen maximieren
  3. Thin-Client - sämtliche Anfragevalidierung, Zwischenverarbeitung muss auf dem Server erfolgen
  4. Geschätzes Verhältnis Lesen zu Schreiben = 100 zu 1
  5. 25-50% der DB-Daten dürften sich im RAM halten lassen
  6. 95% der Clientanfragen beziehen sich auf 25% des Datenbestandes
  7. In festen Zeitabständen, zwischen 5 Minuten und 1 Stunde, erfolgen serverseitige Operationen auf einem größeren Teil des Datenbestandes
  8. Der Server braucht (bis auf ein wenig Administrationszeug) keinen View

Bisherige Probleme:

  1. SingleThreading ist zu langsam. Teils weil Anfragen länger dauern können, teils weil Prozessor und Datenbank so nicht auszulasten sind. Selbst wenn ich 10 Threads gleichzeitig Anfragen an die (lokale) DB schicken lasse, langweilt sich die DB bei 5% Prozessorleistung, während der Prozessor komplett auslgeastet ist.
    Lösung: Multithreading mit (eigenem?) Pool der beim Serverstart erzeugt wird.

  2. Erzeugung von Instanzen der DB-Zugriffsklassen bei Eintreffen einer Anfrage ist zu langsam, wenn man dort erst die SQL-Command Objekte erzeugt. Da kann ich keine keine PreparedStatements mehr nutzen, obwohl die Queries vorher feststehen. CommandObjekte statisch machen, Prepare nutzen und für Threadzugriffe locken war auch nicht so toll, da die Threads sich gegenseitig ausperren.
    Lösung: Die Threads erzeugen beim Serverstart jeweils eine Instanz der DB-Zugriffsklassen, bereiten CommandObjekte mit Prepare vor und bei Anfragen wird nur noch parametrisiert. Wenn die Threads ihre "Arbeitsobjekte" selbst halten, dürfte das auch die Synchronisation vereinfachen.

  3. Bei einigen Klassen, die jeweils nur eine Datenzeile kapseln, sind die Datasets zu langsam.
    Lösung: DataReader und händisch Objekte zusammenbasteln für die betroffenen Klassen.

  4. Bei vielen Anfragen wird auf Daten zugegriffen die sich nicht geändert haben (können). Die immer wieder abzufragen kostet einen Haufen Zeit. Beim Login eines Clients stehen 50% der benötigten Daten von vorneherein fest.
    Lösung: Einführung eines Cache der diesen Teil der Daten im RAM vorhält und Abgleich/Nachladen bei Bearbeitung einer Anfrage.

Soweit zu meinen Vorgedanken.

Mich würde nun vor allem interessieren wo, abgesehen vom erhöhten Mehraufwand für die Implementierung und Nacharbeit bei Änderungen der Datenbankmodelle, ihr dort Schwachstellen seht. Oder ob sogar das ganze Gedankengebäude in sich zusammenbricht, weil ich was unheimlich wichtiges übersehen habe ...

Im Anhang habe ich mal versucht meine momentanen Vorstellungen in ein Gemälde zu fassen. Die rot gestrichelten Linien kennzeichnen die Stellen, an denen ich Synchronisierungsbedarf für die Thread sehe.

3.728 Beiträge seit 2005
vor 15 Jahren
Verteilte Datenanwendung

Hallo Luth,

ich würde mir wegen der Datenbank-Verbindungen und Commands weniger Sorgen machen. ADO.NET macht Standardmäßig Connection Pooling (Das ist auch beim NPGSQL Provider so: http://npgsql.projects.postgresql.org/docs/manual/UserManual.html). Die Verbindungen werden also nie wirklich geschlossen, wenn Du connection.Close() aufrufst, sondern nur an den Pool zurückgegeben. Wenn gerade drei Threads auf Deinem Server zugreifen, sind das drei offene Connections. Über Min- und Max-PoolSize wird fpr gewöhnlich konfiguriert, wie viele Verbindungen der Pool vorhalten soll.

Viel bedenklicher an Deinem Konstrukt ist, dass Du DataTables abrufst und diese in Objektlisten (IList<Klasse1>) kopierst. Das ist zum einen mal eine Schleife und temporär doppelter Arbeitsspeicherverbrauch am Server. Da geht die Leistung hin. Also solltest Du entweder durchgehend mit (typisierten) DataSets/DataTables (DataRows gehen nicht, da sie nicht serialisierbar sind) arbeiten, oder direkt Objekte erzeugen (DataReader => Objektliste). Allerdings wird ein fertiger OR-Mapper wie z.B. das Entity Framework die Arbeit vermutlich effizienter und komfortabler erledigen, als ein eigenes Gebilde.

Warum arbeitest DU mit einem eigenen Protokoll und einem eigenen ThreadPool? Du könntest Remoting oder WCF verwenden. Das ist fertig und mit Sicherheit skalierbarer als ein eigenes Konstrukt. Es ist nicht sinnvoll Infrastruktur selber zu schreiben, die im .NET Framework bereits vorhanden ist. Du schreibst Dir ja auch nicht Deinen eigenen String-Datentypn, oder?

Caching würde ich nicht so implementieren, da die Synchronisierung des Caches vermutlich mehr kostet als nützt. Das RDBMS hat bereits ausgefeilte Caching-Implementierungen, so dass Du das Caching getrost dem RDBMS überlassen kannst. Es sei denn Du hast eine Daten aus verschiedenen Datenquellen bzw. Daten die aufwändig aufbereitet werden (Zellinhalte von Resultsets 1:1 in Objekte zu schreiben, ist keine Aufbereitung), dann würde ein AppServer-seitiges Cahing wieder Sinn machen.

Wenn Datenbankzugriffe zu lahm sind, solltest Du die Indizes optimieren. Das ist in den allermeisten Fällen die Lösung.

L
Luth Themenstarter:in
36 Beiträge seit 2006
vor 15 Jahren

Hallo Rainbird,

und erstmal danke für die Hinweise. Bei den meisten davon hege ich trotzdem noch leise Zweifel ob sie mir weiterhelfen.

ADO.NET macht Standardmäßig Connection Pooling (Das ist auch beim NPGSQL Provider so:
>
). Die Verbindungen werden also nie wirklich geschlossen, wenn Du connection.Close() aufrufst, sondern nur an den Pool zurückgegeben.

Das benutze ich bisher auch. Der Ansatz die Connections auf Threadbasis offen zu halten entspringt einem anderen Gedanken. Der Npgsql-Provider unterstützt unter anderem Prepared Commands, bei welchen der Query Plan von der Datenbank aufgehoben wird. Insbesondere bei Selects auf einzelne Datensätze habe ich mit Prepared Statements 2-10 mal mehr Anfragen / Sekunde ausführen können als ohne. Problem ist, dass dies scheinbar nur funktioniert, solange die Connection zwischendurch nicht geschlossen wird. Sollte ich da nichts übersehen haben erlauben mir die offen gehaltenen Connections also eine wesentlich höhere Abfragerate. Da ich nun nicht für jeden Client eine Connection vorhalten kann, kam der Gedanke auf, das über die Threads mit permanent offenen Connections und die Zuteilung der Threads an die Clients zu lösen.

Falls ich das irgendwie anders bewerkstelligen kann, wäre ich sehr dankbar für weitere Tips.

Viel bedenklicher an Deinem Konstrukt ist, dass Du DataTables abrufst und diese in Objektlisten (IList<Klasse1>) kopierst. Das ist zum einen mal eine Schleife und temporär doppelter Arbeitsspeicherverbrauch am Server. Da geht die Leistung hin. Also solltest Du entweder durchgehend mit (typisierten) DataSets/DataTables (DataRows gehen nicht, da sie nicht serialisierbar sind) arbeiten, oder direkt Objekte erzeugen (DataReader => Objektliste). Allerdings wird ein fertiger OR-Mapper wie z.B. das Entity Framework die Arbeit vermutlich effizienter und komfortabler erledigen, als ein eigenes Gebilde.

Ja. Wenn ich nicht irre, darf ich für den Entity-Framework Ansatz aber erst ein Visual Studio kaufen. Da sträube ich mich momentan noch dagegen, da ich hier an einem Hobbyprojekt rumbastel das in erster Linie dazu dient ein wenig über .NET zu lernen. Von daher wird es eher in Richtung Datareader und eigene Business-Objekte basteln gehen.

Warum arbeitest DU mit einem eigenen Protokoll und einem eigenen ThreadPool? Du könntest Remoting oder WCF verwenden. Das ist fertig und mit Sicherheit skalierbarer als ein eigenes Konstrukt. Es ist nicht sinnvoll Infrastruktur selber zu schreiben, die im .NET Framework bereits vorhanden ist. Du schreibst Dir ja auch nicht Deinen eigenen String-Datentypn, oder?

Auch hier liegt das vielleicht nur an meiner Unwissenheit. Der Client wird wohl in C++ und ohne .NET implementiert werden, falls irgendwie machbar als Crossplattform-Client für Windows/Linux. Sollte ich da mit WCF oder anderen Bordmitteln weiter kommen, würde ich das sicherlich vorziehen. Das scheint allerdings eher unwahrscheinlich, auch wenn ich mich mit der Thematik noch nicht ausführlich beschäftigt habe.

Das mit dem Threadpool war wohl unglücklich formuliert. Es spricht sicherlich nichts gegen die Nutzung des .NET Threadpools, ich bezog mich eher darauf, die Threads zur Anfragebearbeitung auf separaten "Manager"klassen zu starten, welche die für Anfragen notwendigen Connections und Command Objekte halten. Dies scheint mir bisher wegen den in Punkt 1 erwähnten Prepared Statements nötig.

Caching würde ich nicht so implementieren, da die Synchronisierung des Caches vermutlich mehr kostet als nützt. Das RDBMS hat bereits ausgefeilte Caching-Implementierungen, so dass Du das Caching getrost dem RDBMS überlassen kannst. Es sei denn Du hast eine Daten aus verschiedenen Datenquellen bzw. Daten die aufwändig aufbereitet werden (Zellinhalte von Resultsets 1:1 in Objekte zu schreiben, ist keine Aufbereitung), dann würde ein AppServer-seitiges Cahing wieder Sinn machen.

Das dürfte sich in meinem Fall zumindest für Teilbereiche lohnen, da auf den Daten einiges an Berechnungen und Nachverarbeitung vorgenommen wird ohne dies in die DB zurück zu schreiben. Der größte Teil dürfte sich aber mittlerweile erledigt haben, siehe letzten Kommentar.

Wenn Datenbankzugriffe zu lahm sind, solltest Du die Indizes optimieren. Das ist in den allermeisten Fällen die Lösung.

Sind sie eigentlich. Zugriffe auf Datensätze erfolgen zumeist über eine ID-Spalte die als PRIMARY UNIQUE deklariert ist. Damit wird doch automatisch ein entsprechender Index erzeugt. Allerdings scheint ich da über einen Fehler in Npgsql gestolpert zu sein der beim Lesen von Array-Spalten auftritt. Vergrößert man die Arraylänge um den Faktor zwei, steigt die zum Schreiben benötigte Zeit ebenfalls um den Faktor 2, die zum Lesen benötigte Zeit aber um den Faktor 4. Lese ich also ein Array der Länge 1024 dauert das etwa 2-3 ms, bei einem Array der Länge 16384 allerdings schon um die 500 ms. Sollte das behoben werden relativiert sich wohl auch mein Geschwindigkeitsproblem wieder.

3.728 Beiträge seit 2005
vor 15 Jahren

Hallo Luth,

Der Npgsql-Provider unterstützt unter anderem Prepared Commands, bei welchen der Query Plan von der Datenbank aufgehoben wird. Insbesondere bei Selects auf einzelne Datensätze habe ich mit Prepared Statements 2-10 mal mehr Anfragen / Sekunde ausführen können als ohne. Problem ist, dass dies scheinbar nur funktioniert, solange die Connection zwischendurch nicht geschlossen wird. Das scheint eine Besonderheit dieses Providers zu sein. Als MSSQL-Hase habe ich da leider zu wenig praktische Erfahrung. Ich kann mir aber nicht vorstellen, dass der PostgreSQL Server die gecacheten Ausführungspläne verwirft, nur weil jemand in ADO.NET die Close-Methode einer Connection ausführt, was die Connection ja gar nicht wirklich schließt. Aber da kenne ich mich, wie gesagt, zu wenig mit PostgreSQL aus.

Ja. Wenn ich nicht irre, darf ich für den Entity-Framework Ansatz aber erst ein Visual Studio kaufen. Da sträube ich mich momentan noch dagegen, da ich hier an einem Hobbyprojekt rumbastel das in erster Linie dazu dient ein wenig über .NET zu lernen. Von daher wird es eher in Richtung Datareader und eigene Business-Objekte basteln gehen.

Das Entity Framework war ja nur ein Vorschlag. Es gibt aber genügend freie OR-Mapper, die Du einsetzen kannst. Am bekanntesten ist das wohl NHibernate.

Auch hier liegt das vielleicht nur an meiner Unwissenheit. Der Client wird wohl in C++ und ohne .NET implementiert werden, falls irgendwie machbar als Crossplattform-Client für Windows/Linux. Sollte ich da mit WCF oder anderen Bordmitteln weiter kommen, würde ich das sicherlich vorziehen. Das scheint allerdings eher unwahrscheinlich, auch wenn ich mich mit der Thematik noch nicht ausführlich beschäftigt habe.

Wenn Du WCF mit einem BasicHttpBinding verwendest, sollte damit grundsätzlich jeder Client auf jeder Plattform reden können, der die Möglichkeit hat, über HTTP bzw. SOAP zu kommunizieren. Da Du allerdings sehr hohen Wert auf Performanz zu legen scheinst, ist SOAP wiederum nicht unbedingt ideal, da viel Overhead. Wenn der Client in unmanaged Code geschrieben ist, macht ein eigenes binäres Protokoll wiederum Sinn.