Connection pooling  
Downloads til denne artikel finder du her !
ConnectionPoolingDemo viser hvorledes connection pooling fungerer når henholdsvis samme og forskellige connection strings benyttes i en applikation.
ConnectionPoolingDemo_vb.zip
Connection pooling demo i VB version
ConnectionPoolingDemo_cs.zip
Connection pooling demo i C# version
For yderligere information om demoen; læs kommentarer i koden.

Connection.Reinkarnation

At skabe en ny databaseforbindelse er ganske simpelt, men det er absolut ikke ligegyldigt, hvordan det gøres. I denne artikel vil vi se på, hvordan brug af connection pooling kan gøre applikationer skalerbare uden, at det af den grund har svangre performancemæssige implikationer.

I de gode gamle dage...

... da store applikationer var client/server applikationer, og mere distribuerede applikationsarkitekturer ikke var særligt udbredte, blev en eller flere connections typisk åbnet under applikationens opstart (f.eks. i forbindelse med at brugeren blev valideret). Og de således åbnede connections levede lykkeligt til deres applikations ende. En af grundene til, at man i disse sorgløse dage som udgangspunkt ønskede, at lade sine connections leve længe var, at det kan tage forfærdeligt lang tid at åbne en connection - ja faktisk op til flere sekunder. Havde man én connection per bruger og 100 samtidige brugere (forstået som 100 brugere der samtidigt havde applikationen åben), havde man dermed 100 samtidigt åbne connnections. At have sådanne database connections med næsten evigt liv kan være en udmærket løsning, hvis man taler traditionelle client/server applikationer, hvor man har har et begrænset antal brugere.

Løse forbindelser får konsekvenser

Men de senere års fokus på distribuerede og løst koblede applikationsarkitekturer stiller nye krav til skalerbarhed. Som det mest ekstreme eksempel på dette har fremkomsten af browserbaserede applikationer midt i 90erne betydet, at man potentielt set har haft ønske om at kunne håndtere tusindvis - ja måske endda titusindvis af samtidige brugere. Begrebet samtidige brugere skal dog opfattes noget anderledes end for de traditionelle client/server applikationer. Samtidige brugere for en web applikation vil kunne opfattes, som de brugere for hvilke, der på et givet tidspunkt eksisterer aktive sessioner på web serveren, om end det reelle antal aktive brugere ofte vil være en hel del mindre end antallet af disse.

Kritiske ressourcer er ... kritiske

I sagens natur er håndteringen af kritiske og sparsomme ressourcer af afgørende betydning for i hvor høj grad et givet system er i stand til at håndtere tunge belastninger. Databasen er for mange systemer en af de mest kritiske ressourcer - jo mindre man belaster den med f.eks. åbne connections, jo lettere er det alt andet lige at skabe skalerbare systemer. Derfor sørger man for kun at holde en connection åben så længe, som det er absolut nødvendigt. Det kan i den yderste konsekvens betyde, at man åbner og lukker connections for hver eneste database transaktion, der eksekveres. Men selvom en sådan strategi isoleret set bidrager til at skabe et skalerbart system, så er det ud fra et performancemæssigt synspunkt noget nær en katastrofe, hvis hver eneste databasetransaktion skal bære byrden af at åbne en ny connection.

Brian: Look. You've got it all wrong. You don't need to follow me. You don't need to follow anybody. You've got to think for yourselves. You're all individuals.
Folkemængden: YES! WE'RE ALL INDIVIDUALS!
Brian: You're all different.
Folkemængden: YES. WE ARE ALL DIFFERENT
En enkelt i mængden: I'm not.
Folkemængden: Ssssh. Sssh.
Brian: You've all got to work it out for yourselves.
Folkemængden: YES. WE'VE GOT TO WORK IT OUT FOR OURSELVES.
Brian: Exactly.
Fra Monty Pythons "Life of Brian"

Connection.Reinkarnation

Har database connections et liv efter close? Ja, hvis de tror på connection pooling.  Benytter man sig af connection pooling bliver connections ikke nedlagt i samme øjeblik, man lukker forbindelsen til databasen. I stedet foretages der en "rengøring" af connection objektet (tilknyttede prepared statements nedlægges og evt. igangværende transaktioner rulles tilbage), hvorefter det placeres i den relevante connection pools kø af ledige connections. Hvert connection objekt er tilknyttet præcist en connection pool.  Har 2 connection objekter præcist den samme connection streng, er de tilknyttet samme pool.
Når der på et senere tidspunkt efterspørges en "ny" databaseforbindelse checkes det, hvorvidt der findes et ledigt connection objekt i den connection pool, der matcher den specificerede connection streng. Hvis dette er tilfældet "genbruges" det fundne connection objekt og kun, hvis der ikke allerede findes et passende ledigt connection objekt, oprettes der et nyt.

Da en given connection pool findes ved at slå connection strengen op i en hashtabel (af typen System.Collections.HashTable), der indeholder alle connection pools, kræver genbrug af connections, at det poolede connection objekts connection streng og den specificerede connection streng er helt ens. Et enkelt mellemrum i en connection streng kan være nok til, at der ikke sker et match, og at der dermed oprettes 2 connection pools med hvert deres connection objekt i stedet for 1 pool med et connection objekt, der genbruges.

Følgende tre connection strenge er at opfatte som forskellige og medfører altså, at de tilsvarende connection objekter placeres i hver deres connection pool.

   Data Source=localhost;User Id=sa;Password=;Initial Catalog=NorthWind
   Data Source=localhost;User Id=sa; Password=;Initial Catalog=NorthWind
   Data Source=localhost;User Id=lykke;Password=;Initial Catalog=NorthWind

Skal man effektivt kunne udnytte connection pooling i ens applikationer, må man ikke lade de enkelte slutbrugeres identitet slå igennem, når man laver login imod databasen. Det betyder, at der er en række login-scenarier, man bør se bort fra, hvis man ønsker at få en effektiv udnyttelse af connection pooling:

  • Man må ikke foretage eksplicit login baseret på de enkelte slutbrugeres identitet (som i eksemplerne ovenfor)
  • Man må ikke benytte sig af integrated security for den enkelte slutbruger
  • Man må ikke benytte sig af delegation (eller andre former for impersonation) af slutbruger identiteter
  • Man må ikke benytte sig af Microsoft SQL Serverens applikationsroller med en bagvedliggende slutbruger identitet

Connectionpooling benyttes som udgangspunkt altid, så forbryder man sig mod ovenstående retningslinjer er det ikke sådan at connection pooling slås fra, men der er derimod tale om en ret ineffektiv connection pooling, fordi den baseres på hver enkelt aktuel security id (altså den brugers identitet på hvis vegne login sker).
Konceptuelt kan man tænke på det på den måde, at m.h.t. matchning af connections udvides connection strengen med slutbrugerens identitet i de tilfælde, hvor den ikke fremgår eksplicit af connection strengen.

For at udnytte connection pooling maksimalt skal man nøjes med nogle få (eksempelvis en for hver rolle) eller måske blot en enkelt fast bruger id per applikation. Denne bruger id kan enten være en eksplicit del af connection strengen, eller det kan være identiteten af en bruger, på vegne af  hvilken der eksekveres en service. Udover at understøtte connection pooling giver den faste database user id også en dejlig simpel administration af brugere på databasen ;^) om end der også findes andre udmærkede måder at opnå en sådan simpel databaseadministration på. På den anden side har denne simple administration så den ulempe, at man i databasen ikke umiddelbart har adgang til information om, hvilken slutbruger der tilgår den - og at man derfor om nødvendigt så selv skal levere og håndtere denne information.

For at sikre den maksimale effekt af connection pooling må det derfor på det kraftigste anbefales, at definitionen af connection strenge centraliseres, uanset om det så gøres som en konstant eller via en funktion, der genererer strengen.

Og hva' så?

Hvad betyder det så for vores kode, at vi har connection pooling? Det betyder, at vi kan skabe skalerbare systemer ved at nøjes med at holde connections åbne i forbindelse med selve afviklingen af de enkelte databasetransaktioner uden at det får reelle performancemæssige konsekvenser.

Nedenstående kode er et eksempel på, hvordan kode i en DAL-klasse (Data Access Layer) kan se ud. Bemærk at der udelukkende er tale om et eksempel til illustration af connection pooling. I produktionskode bruger jeg naturligvis ;^) et generisk database framework. Men uanset at der kun er tale om et eksempel til illustration af connection pooling, så fremgår en anden mere strukturel pointe også: At interfacet til funktionen reelt set er uafhængigt af den valgte managed data provider. Så denne simple indpakning af den database specifikke kode kan være et første skridt mod at skabe en uafhængighed af den konkrete database - ja faktisk behøver koden i en eventuel reimplementation af funktionen jo slet ikke at tilgå en egentlig database...

  Private Function GetCustomers(ByVal connString As String) _
      As System.Data.DataTable
    Dim sql As String = "SELECT * FROM Customers"
    Dim tbl As New System.Data.DataTable("Customers")
    Dim conn As New System.Data.SqlClient.SqlConnection(connString)
    conn.Open()
    Dim adap As New System.Data.SqlClient.SqlDataAdapter(sql, conn)
    adap.Fill(tbl)
    conn.Close()
    Return tbl
  End Function 
  private  System.Data.DataTable GetCustomers(string connString)
  {
    string sql = "SELECT * FROM Customers";
    System.Data.DataTable tbl = new System.Data.DataTable("Customers");
    System.Data.SqlClient.SqlConnection conn = _
new System.Data.SqlClient.SqlConnection(connString);


conn.Open(); System.Data.SqlClient.SqlDataAdapter adap =
new System.Data.SqlClient.SqlDataAdapter(sql, conn); adap.Fill(tbl); conn.Close(); return tbl; }

For at udnytte connection pooling optimalt bør man altid lukke connections eksplicit v.h.a. Close (eller Dispose) og ikke nøjes med at basere sig på, at lokale connection variable går ud af scope.

Styring af connection pooling

Hver enkelt managed data provider har sin måde at håndtere connection pooling på - nærværende artikel er baseret på System.Data.SqlClient provideren til Microsofts SQL Server database, men andre managed data providere så som System.Data.OleDb, System.Data.Odbc, System.Data.OracleClient o.s.v. har lignende funktionalitet om end den konkrete implementering som sagt varierer. Faktisk kan håndteringen af connection pooling for forskellige versioner af en given provider variere. Eksempelvis har Microsoft ændret på implementeringen af connection pooling for SqlClient fra version 1.0 til version 1.1 af .NET frameworket.

Connection pooling er som default slået til ved brug af SqlClient connections, men om ønsket kan vi vælge at slå pooling helt fra eller måske blot styre, hvordan connection pooling skal foregå for en given connection - man kan eksempelvis angive, hvor mange connections der skal være plads til i poolen. Det gøres ved at angive parametre i connection strengen. Disse parametre er som udgangspunkt specifikke for den enkelte managed data provider, men dog ens for f.eks. SqlClient og OracleClient.

Et eksempel på en connection streng med pooling information er følgende, der slår connection pooling fra:

  Data Source=localhost;User Id=sa;Password=;Initial Catalog=NorthWind;Pooling=false

Ælle, bælle, connections tælle

Eksperimenterer man med connection pooling eller ønsker man at skrue på måden det virker på, kan det være ganske nyttigt at benytte Performance Monitoren (perfmon) i Windows. Ved installationen af .NET frameworket på en maskine bliver der tilføjet en række performance counters, som alle tilhører performance objektet ".NET CLR Data":

  • SqlClient: Current # connection pools - angiver hvor mange connection pools, der findes - hvilket er nyttigt til at afgøre, om man (ved et uheld) har fået specificeret forskellige connection strenge
  • SqlClient: Current # pooled connections - angiver hvor mange poolede connections, der findes i alle pools tilsammen
  • SqlClient: Current # of pooled and non pooled connections - angiver hvor mange connections, der findes uanset, om de er poolede eller ej
  • SqlClient: Peak # pooled connections - angiver det over tid største antal samtidigt poolede connections (i alle pools tilsammen)
  • SqlClient: Total # failed commands - angiver det samlede antal fejlede kommandoer, der er forsøgt eksekveret
  • SqlClient: Total # failed connects -  angiver det samlede antal gange åbning af en connection er fejlet

Summa summarum

Som et led i at sikre at nutidens distribuerede systemer skal kunne skalere, kan det være nyttigt at benytte sig af connection pooling. Connection pooling gør det muligt at åbne og lukke connections gang på gang uden at ens applikation går i knæ rent performancemæssigt. For at connection pooling skal være effektiv, skal man sørge for, at de anvendte connection strenge ikke varierer unødigt, og man skal især passe på med login identiteten, som benyttes mod databasen. Det er i denne forbindelse vigtigt, at implicitte login identiteter (opnået via integrated security) ikke baserer sig på slutbruger identiteter. Connection pooling betyder, at man kan tillade sig at åbne og lukke connections for hver eneste databasetransaktion, der eksekveres.

Jeg har lavet en lille applikation til demonstration af connection pooling, som du kan downloade - se "side"-baren først i dokumentet.