Connection Pooling, ovvero come gestire gli accessi concorrenti a database con una coda.

Spread the love

Negoziare una connessione con un database è una operazione non banale che richiede una consistente attività per la realizzazione del collegamento sicuro, autenticato e la messa a disposizione delle risorse del database all’applicazione che ne fa uso. Ho esperienza di questo fatto soprattutto in ambito Java e Oracle ma le considerazioni da fare sono tutto sommato agnostiche e quindi indipendenti dalle piattaforme utilizzate. Tuttavia gli esempi sono in Java.

In ambito concorrente, come una applicazione web, questa operazione rischia presto di diventare un collo di bottiglia se si rimane nello schema 1 richiesta = 1 nuova connessione a DB.

L’idea è quella di utilizzare una coda (in questo ambito definita come pool) nella quale mantenere un numero finito di connessioni riutilizzabili che vengono in continuazione prelevate dalla coda quando servono e rimesse nella coda al termine del loro utilizzo.

Supponiamo che arrivino due richieste quasi simultanee

  1. Richiesta 1 (es. https://mysite.com/authenticate) la quale richiede collegamento al db per effettuare una autenticazione utente: in questa richiesta viene aperta una nuova connessione al database e viene lanciata la query sulla tabella utenti. Dopo aver acqusito il recordset, l’applicazione può dismettere la connessione invocando il metodo close()
  2. Richiesta 2 (es. https://mysite.com/articles/get/AB123) che deve eseguire una lettura di una tabella. L’applicazione apre una connessione ex novo (con il conseguente impegno di risorse e di tempo), lancia l’interrogazione e recupera il recordset.

Non sarebbe più efficiente per la Richiesta 2, qualora arrivasse dopo la conclusione della prima richiesta, riutilizzare la connessione aperta dalla Richiesta 1? Se la richiesta evitasse di chiamare la close() e invece “mettesse via” la connessione da qualche parte, la richiesta 2 potrebbe riutilizzarla senza dover negoziare un’altra volta username/password, caricamento driver con il DBMS.

Una struttura dati efficiente per mettere via dati che si ritiene di usare di nuovo a breve, può essere una coda, in particolare una coda FIFO. Che nel gergo dei database si chiama Connection Pool. Questa coda conterrà descrittori di risorse DBMS.

Il funzionamento di questa coda è il seguente. Partiamo da una coda vuota.

Arriva il primo processo (una richesta HTTP) che impone all’applicazione di collegarsi al database. Siccome la coda è vuota, viene giocoforza negoziata una connessione nuova con il database, chiamiamola Connessione 1. Quando l’interrogazione è stata fatta e quindi la login è eseguita, la connessione non viene chiusa, bensì viene salvato il descrittore con un push() nella coda, che finora era ancora vuota.

Arriva il secondo processo che necessita di una connessione al DBMS e quello che fa prima di tutto è controllare se nella coda ci sono connessioni disponibili. Supponiamo che la login sia già stata fatta e quindi nella coda ci sia la connessione 1 rilasciata dal primo processo. Tutto ciò che deve fare il processo 2 è lanciare la query senza autenticarsi di nuovo; quindi serviamo due processi con una sola connessione.

Se, viceversa, il processo 2 arriva quando la login è ancora in corso, quindi la connessione 1 è ancora impegnata, in questo caso il processo 2 dovrà aprire una connessione 2. Ma questo meccanismo fa sì che, dopo un po’ di tempo, la probabilità di trovare una connessione libera nella coda sia molto maggiore e quindi il numero di negoziazioni dirette è destinato a calare drasticamente. Ancora meglio, se all’inizializzazione del pool mettiamo già via un certo numero di connessioni nella coda, risparmiamo tempo ad applicazione avviata.

Ogni volta che un processo preleva una connessione dalla coda, fa un pull(), ovvero toglie la connessione dalla coda; quando ha terminato, opera un push() e la rimette nella coda. Se il numero di processi concorrenti è maggiore della dimensione della coda fino a quel momento, viene negoziata una nuova connessione, altrimenti si fa più velocemente un pull().

Questo meccanismo deve essere gestito; ossia si deve sollevare l’applicazione dal negoziare nuove connessioni, perché dovrà essere tutto supervisionato dal processo di gestione della coda, che è un oggetto della classe ConnectionPool. L’applicazione dovrà solo limitarsi a chiedere una connessione al pool, e il pool in ogni caso gliela concederà (sia essa una connessone riciclata o una nuova di zecca), senza che l’applicazione debba interloquire direttamente con il database.

Per rendere poi più efficiente l’uso della memoria, si può fare a meno di allocare una coda dinamica, ma un array di lunghezza fissa, in cui vengono impilate tante connessioni quante ne servono: all’inizio supponiamo che la coda sia vuota, viene fatto il push() di una nuova connessione, marcata con Active e consegnata al processo, che la potrà usare. Quando il processo che l’ha utilizzata la rilascia, il gestore del pool la dichiarerà Idle rimettendola a disposizione. Però non è che la inserisce nuovamente nel pool, semplicemente le cambia lo stato.

Tuttavia nell’implementazione di esempio che vedremo, viene utilizzata un’altra tecnica ancora: vengono impiegati due vettori; quello delle connessioni disponibili (o idle) e quello delle connessioni attive (active).

Il processo successivo che arriva a chiedere connessioni, si farà dare l’ultima connessione idle resasi disponibile (comportamento FIFO).

Esempio applicativo

Questo esempio è liberamente tratto (e semplificato) da Java Made So Easy.

Iniziamo dunque definendo la classe ConnectionPool:

package connectionPooling;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Vector;
 
public class ConnectionPool implements Runnable {
    // i parametri di connessione
    private String driver, url, username, password;
    // la dimensione massima del pool
    private int maxConnections; 
    // le 2 code delle connessioni idle e active
    public Vector<Connection> idleConnections, activeConnections; 

    // il costruttore
    public ConnectionPool(
                  String driver, String url, String username,
                  String password, int maxConnections, int initialConnections
           ) throws SQLException {
           this.driver = driver;
           this.url = url;
           this.username = username;
           this.password = password;
           this.initialConnections = initialConnections;
           this.maxConnections = maxConnections;

           // in questo esempio il pool è mantenuto su due vettori:
           // - quello delle connessioni disponibili (o idle: pigre, nullafacenti);
           // - quello delle connessioni impegnate (o active);
           idleConnections = new Vector<Connection>(initialConnections);
           activeConnections = new Vector<Connection>();
           // qui opzionalmente preparo un certo numero di connessioni già pronte
           for (int i = 0; i < initialConnections; i++) {
                  idleConnections.addElement(makeNewConnection());
           }
    }

Il metodo fondamentale qui è makeNewConnection() con il quale costruiamo una nuova connessione “vera”; ovviamente lo dichiariamo private per impedire al programma chiamante di negoziare connessioni direttamente con il DBMS attraverso di essa:

private Connection makeNewConnection() throws SQLException {
           try {
                  // Carica il database driver
                  Class.forName(this.driver);
                  // Apri la connessione al db
                  Connection connection = DriverManager.getConnection(this.url, this.username,
                               this.password); 
                  return (connection);
           } catch (Exception cnfe) {
                  cnfe.printStackTrace();
                  throw new SQLException(
                               "ConnectionPool:: SQLException encountered:: "
                                             + cnfe.getMessage());
           }
    }

Come visto sopra questo metodo viene chiamato al deploy del connectioonPool per inizializzare un certo numero di connessioni già pronte per l’uso (che passo al costruttore).

Ora dobbiamo gestire il pool, ovvero consegnare al processo che ne fa richiesta una connessione idle, toglierla dal vettore delle idle e metterla in quello delle active:

/**
     * Method to return Connections
     */
    public synchronized Connection getConnection() throws SQLException {
           if (!idleConnections.isEmpty()) {
                  Connection existingConnection = (Connection) idleConnections
                               .lastElement();
                  int lastIndex = idleConnections.size() - 1;
                  // toglie la connessione dal vettore delle pigre:
                  idleConnections.removeElementAt(lastIndex);
                  // ... e la aggiunge al vettore delle attive:
                  activeConnections.addElement(existingConnection);
                  return (existingConnection);
           } else {
                  // se non ci sono più connessioni pigre, ne creo una di nuova: 
                  return makeNewConnection();
           }
    }

Poniamo attenzione sul fatto che il metodo getConnection() è definito come synchronized, per cui molte richieste pressoché simultanee verranno arbitrate in modo che le connessioni siano consegnate distintamente secondo il loro ordine di arrivo.

Può accadere, alla fine, che i processi esauriscano tutte le connessioni del pool: in questo caso il vettore delle connessioni active sarà pieno e quello delle idle sarà vuoto; si potrebbe gestire anche questa situazione ma, in questa fase in cui cerchiamo di capire come funziona il meccanismo, semplicemente notifichiamo che il pool è pieno.

                  if ((totalConnections() >= maxConnections) {
                        throw new SQLException("Connection limit reached");
                  }

dove totalConnection() è un count degli elementi del vettore activeConnections più il count degli elementi del vettore idleConnections.

Alla fine il programma chiamante utilizzerà il metodo getConnection() per comunicare con il pool, mentre la classe ConnectionPool utilizzerà il metodo (privato) makeConnection() per comunicare con il DBMS.

Così abbiamo operato la separazione tra applicazione e DBMS.

Ci manca solo un metodo per liberare una connessione (cioè toglierla dal vettore delle connesioni active e rimetterla in quello delle connessione idle), lo chiamiamo free():

  /**
     * Method to free the Connections
     */
    public synchronized void free(Connection connection) {
           activeConnections.removeElement(connection);
           idleConnections.addElement(connection);
    }

Alla fine siamo pronti ad utilizzare questa infrastruttura nell’applicazione; dobbiamo creare un oggetto istanza della classe ConnectionPool che mettterà a disposizione connessioni già pronte prelevabili con il metodo getConnection():

package connectionPooling;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
 
/** Copyright (c), Ankit Mittal JavaMadeSoEasy.com */

public class PreparedStatementUseConnectionPooling {

    public static void main(String... arg) throws SQLException {
            ConnectionPool connectionPool = new ConnectionPool(
                        "oracle.jdbc.driver.OracleDriver",
                        "jdbc:oracle:thin:@localhost:1521:orcl", 
                        "foo", 
                        "bar",
                        10, 5);
 
           Connection dbConn = connectionPool.getConnection();
           System.out.println("Sono pronte le connessioni dalla classe ConnectionPool");
           
           PreparedStatement prepStmt = dbConn
                        .prepareStatement("select ID, NAME from EMPLOYEE");
           
           ResultSet rs = prepStmt.executeQuery();
           while (rs.next()) {
                  System.out.print(rs.getInt("ID") + " ");
                  System.out.println(rs.getString("NAME"));
           }
 
           if (rs != null)
                  rs.close(); // close resultSet
           if (prepStmt != null)
                  prepStmt.close(); // close PreparedStatement
 
           connectionPool.free(dbConn);
           System.out.println("Rilasciata la connessione dalla classe ConnectionPool");
    }
}
 

Si noti che il programma chiamante, dopo la connessione impartisce comandi direttamente al DBMS; il ConnectioPool si intromette tra di loro solo al momento dell’acquisizione di nuove connessioni (o della rimozione di un connessione che non serve più).

Per ulteriori dettagli implementativi si faccia riferimento all’articolo citato all’inizio.

Adesso il funzionamento di un pool dovrebbe essere almeno un pelino più chiaro. Almeno per me lo è stato.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.