Stolperfalle Connection Pooling von Nocturne zum Thema Code To Joy - Fr, 09.03.2007

Heute wurde ich von unserem Webserver beim Aufruf einer unserer Webseiten, mit folgender Fehlermeldung begrüßt:
Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.

Ich mag ja prinzipiell Fehlermeldungen mit einem "das könnte daran liegen" oder einem "möglicherweise ist die Ursache" ganz besonders. Schließlich geht man ein nicht geringes Risiko ein, der mir entgegengebrachten Mutmaßung glauben zu schenken, den Fehler dann fieberhaft zu suchen, nur um am Ende feststellen zu müssen, dass es an was ganz anderem lag. Und nicht selten wäre man da zuweilen auch viel eher drauf gekommen, wenn man nicht durch diverse "Hinweise" in die Irre geführt worden wäre *g*

Und so blieb mir auch heute nix anderes übrig, als anzunehmen, dass es an der Größe des Connection-Pools liegt. Moment mal...dem was? Dem "Connection-Pool"? Was ist das denn? So recherchierte ich und stelle mal wieder fest: wenn's funktioniert, dann ist das ne ganz feine Sache

Hintergrund

Wenn eine Webseite Informationen aus einer Datenbank abruft, so muss sie grob gesagt zunächst eine Verbindung zu ihr aufbauen, wird dann mit Benutzername und Passwort authentifiziert und darf dann die gewünschten Informationen erfragen. So ein Verbindungsaufbau inklusive Authentifizierung und dem ganzen Kram, ist dabei verhältnismäßig Ressourcen raubend und zeitintensiv.
Und genau an dieser Stelle kommt der Connection-Pool ins Spiel: er erhöht die Leistung einer solchen Webanwendung, indem er geöffnete Verbindungen wiederverwendet, anstatt bei jeder Anfrage eine neue aufzubauen.

Das Connection-Objekt verwendet standardmäßig einen Pool mit maximal 100 geöffneten Verbindungen. Eine Verbindung wird immer dann geöffnet, wenn die Open() Methode der Connection aufgerufen wird. Wird anschließend jedoch erneut ein Open() mit dem selben ConnectionString angefordert, so weist der Pool-Manager eine bereits geöffnete Verbindung zu, insofern diese gerade nicht aktiv ist. Und tatsächlich aktiv ist eine Verbindung zuweilen nur sehr kurz und verschwendet dann im "Sleeping"-Mode einfach nur Ressourcen. Noch extremer wirkt sich dies aus, wenn die Connection vom Programm, aufgrund unsauberen Codes, nicht korrekt geschlossen wird. Dann ist die "Leitung" quasi solange belegt, bis die Garbage Collection den Timeout ausruft und das Schließen übernimmt. Standardmäßig passiert das nach 20 Minuten - in einer produktiven Anwendung untragbar.

Durch das Wiederverwenden von offenen Verbindungen kann die Anzahl gleichzeitig geöffneter Datenbankverbindungen sehr klein gehalten werden. In mittelgroßen Anwendungen sind ca. 40 Verbindungen ausreichend. Sind die maximal zulässigen 100 Verbindungen erreicht, so wird eine eingehende Verbindungsanfrage vom Pool-Manager in die Warteschlange gesetzt. Dort hat sie 15 Sekunden (Standardwert) Zeit, bis sie komplett abgelehnt wird. In einer Umgebung von 100 möglichen Verbindungen, sind 15 Sekunden eine mehr als angemessene Zeit. Man stelle sich vor, man stünde in einem Supermarkt mit 100 Kassen: Dass wirklich mal alle belegt sind, ist schon sehr unwahrscheinlich. Und tritt dieser Fall dann doch mal ein, so kann dieser Zustand nur sehr kurz dauern. Dauert es länger, so ist die Ursache womöglich ein Programmierfehler, der sich schließlich durch die eingangs genannte Fehlermeldung äußert.

Die Stolperfalle

Eine einfache Datenbankabfrage erfolgt nach folgendem Schema:

  1. Verbindungsobjekt mit ConnectionString instanzieren
  2. open()
  3. Abfrage
  4. close()
Doch was passiert, wenn in Schritt 3 eine Exception ausgelöst wird? Der Fehler ist insofern tückisch, als dass er lange Zeit verborgen bleiben und erst später während der Laufzeit eintreten kann, wenn verschiedene Bedingungen erfüllt sind. In diesem Fall stoppt die Ausführung des Programms, der Fehlertext der Ausnahme wird angezeigt und das Wichtigste: close() wird nie aufgerufen. Findet das Ganze dann noch in einem try-catch-Block statt und behandelt man die ausgelöste Ausnahme im Catch-Block explizit nicht, dann ist das Chaos perfekt: die Anwendung läuft dann nämlich erstmal scheinbar problemlos. Die Exception bleibt aus, aber die Verbindung wird auch hier nicht abgebrochen. Lediglich die ausbleibende Funktionalität, bietet dann einen Hinweis darauf, dass hier etwas nicht stimmt. Doch nicht immer ist das Ergebnis einer Datenbankabfrage für den Benutzer direkt sichtbar.

Ein Programmierer kann demzufolge niemals davon ausgehen, dass seine Anwendung vollständig Zeile für Zeile durchlaufen wird. Für solch kritische Blöcke, wie das Schließen von Datenbankverbindungen, aber auch das Freigeben von Dateihandles beim Schreiben oder Lesen auf dem Datenträger, gibt es daher spezielle Behandlungsmethoden:

try-finally

C#
1 SqlConnection conn = new SqlConnection(myConnectionString); 2 try 3 { 4 conn.Open(); 5 //Abfrage durchführen 6 } 7 finally 8 { 9 conn.Close(); 10 }

Im kritischen Block (try) wird die Verbindung geöffnet und die Abfrage durchgeführt. Anweisungen im finally-Block werden vom Compiler immer ausgeführt, sogar dann, wenn im kritischen Block ein Fehler auftritt.

Die using-Anweisung

C#
1 using (SqlConnection conn = new SqlConnection(myConnectionString)) 2 { 3 conn.Open(); 4 //Abfrage durchführen 5 }

Die using-Anweisung bietet genau das selbe, ist dabei jedoch einfacher zu benutzen. Using definiert einen Gültigkeitsbereich für ein Objekt. Das Objekt existiert nur innerhalb der geschweiften Klammern. Setzt das Programm die Abarbeitung außerhalb der Klammern fort, so garantiert der Compiler, dass das Objekt zerstört wurde bzw. wie in diesem Fall, dass die Verbindung zur Datenbank geschlossen wurde. Tritt innerhalb der using-Anweisung eine Ausnahme ein, so wird auch in dem Fall die "automatische Bereinigung" durchgeführt. Insgesamt eine sehr angenehme Sache.

Im Fall unserer Website heute, stellte sich heraus, dass genau so eine unbehandelte Ausnahme dafür sorgte, dass die Verbindung nie geschlossen wurde. Noch dazu befand sich diese Abfrage in einem User-Control, wodurch dieser Fehler von jeder Seite verursacht wurde, in der das Control verwendet wird. Somit dauerte es nicht lange, bis der Connection-Pool erschöpft war.
Im Systemmonitor des SQL-Servers konnte man das traurige Schauspiel sehr gut beobachten: Bei jedem Aufruf der Webseite wurde eine Verbindung geöffnet, die nicht geschlossen und somit nicht wiederverwendet werden konnte. Sie belegten ab dann jeweils 20 Minuten lang sinnlos Ressourcen. Durch jeden Druck auf F5, stieg die Anzahl an Benutzerverbindungen zum Server ... solange bis der Pool voller ungenutzter ("schlafender") Verbindungen war und Neue abgewiesen wurden.
Nachdem sämtliche Datenbankverbindungen auf eine der beiden o.g. Methoden umgestellt waren, war das Problem vom Tisch, ich um einige Erfahrungen reicher und mein Gewissen beruhigt, weil das System dadurch wieder ein Stückchen stabiler wurde

Weitere Informationen zum Thema:
[1] http://www.15seconds.com/issue/040830.htm

zuletzt geändert am 13.03.2007

Validierung direkt während der EingabeCode aus XML generieren lassen