Il problema che il Pool di Thread risolve

L'obiettivo di avere un server software scalabile (e un DBMS è un esempio di tali software) comporta mantenere le migliori performance quando il numero di client aumenta. MySQL tradizionalmente assegnava un thread ad ogni connessione da parte dei client, così con l'aumentare degli utenti concorrenti le performance calavano. Molti thread attivi sembrano essere un grosso problema per le performance, perché l'aumento del numero di connessioni comporta un context switching (cambiamento di contesto) sempre più intensivo e un peggior incapsulamento delle cache della CPU, mentre i lock aumentano. Perciò la soluzione ideale che aiuta a ridurre il cambiamento di contesto è mantenere il numero dei thread inferiore alla quantità di client, anche se questa quantità non deve essere troppo bassa, perché al contempo è desiderabile usare le CPU al meglio - idealmente, c'è un singolo thread attivo per ogni CPU di cui la macchina è dotata.

Cosa c'è di nuovo nel Pool di Thread di MariaDB 5.5

MariaDB era in grado di eseguire i thread in un pool anche nella versione 5.1. Lo svantaggio principale della precedente soluzione era che il pool era statico, cioè il numero di thread era fisso. I pool di thread statici hanno dei meriti, perché vi è un limitato numero di casi in cui le callback eseguite dal pool non si bloccano mai reciprocamente e non dipendono l'una dall'altra (si immagini qualcosa come un server echo). Ma i client dei DBMS sono più sofisticati, perché le azioni eseguite da uno devono attendere il completamento delle operazioni di un altro, si bloccano con diversi lock e con l'I/O, pertanto è impossibile o molto difficile predire quale sia il numero ideale (o anche solo sufficiente) di thread per prevenire i deadlock. La versione 5.5 implementa un pool dinamico/adattivo che si occupa da solo della creazione di nuovi thread quando la domanda aumenta, e di ritirarli quando non hanno niente da fare. Si tratta di una re-implementazione completa del vecchio pool-of-threads, che si pone i seguenti obiettivi:

  • Rendere il pool dinamico (cresce e diminuisce al bisogno)
  • Minimizzare il carico di lavoro per mantenere il pool stesso
  • Ottimizzare l'uso delle capacità dell'OS (usando il threadpool nativo se esiste, altrimenti usando il multiplexing dell'IO)
  • Limitare l'uso delle risorse da parte dei thread (thread_pool_max_threads)

Attualmente vi sono due differenti implementazioni di basso livello, che dipendono dall'OS – una per Windows, che usa il threadpool nativo, l'altra per i sistemi Unix-like. Per questa ragione, alcuni parametri di ottimizzazione differiscono da Windows a Unix.

Quando usare il pool di thread

I threadpool sono più efficienti in quelle situazioni in cui le query sono relativamente brevi e il carico è legato soprattutto alla CPU (OLTP). Se il carico di lavoro non è sulla CPU, si potrebbe preferire limitare il numero di thread per risparmiare memoria per i buffer del database.

Quando il pool di thread è meno efficiente

Vi sono casi particolari in cui il threadpool è probabilmente meno efficiente.

  • Carico di lavoro con crescita molto intensa (lunghi periodi di inattività che si alternano con brevi periodi di intense attività da parte di molti utenti), e inoltre non si possono tollerare i ritardi che derivano dalla creazione di thread. Anche in questa situazione, le prestazioni si possono migliorare modificando il modo in cui i thread vengono ritirati, tramite thread_pool_idle_timeout su Unix, o thread_pool_min_threads su Windows.
  • Molte query lunghe, senza attese. Senza attese significa che il thread non deve mai aspettare per eseguire la query, o non indica attese al threadpool. Tali carichi di lavoro sono usati soprattutto nei data warehouse. In questo caso le query che impiegano molto tempo ritardano l'esecuzione delle altre, e il ritardo si elimina con un meccanismo di rilevamento del blocco (si veda la descrizione di thread_pool_stall_limit). E' comunque possibile connettersi utilizzando la porta TCP/IP extra-port.
  • Si è certi che le query più semplici si eseguano sempre velocemente, indipendentemente dal carico di lavoro sul server di database. Con un threadpool carico di lavoro, le query potrebbero essere accodate per essere eseguite successivamente, così che anche una semplice SELECT 1, potrebbe richiedere più tempo che con il metodo thread-per-connection.

Usare lo scheduler del threadpool

Su Unix, si aggiunga thread_handling=pool-of-threads al file di configurazione my.cnf.

Su Windows, il valore predefinito di thread_handling è preimpostato a pool-of-threads, quindi non è necessaria alcuna modifica. Se si vuole usare un thread per connessione si aggiunga thread_handling=one-thread-per-connection al file my.cnf.

Per le versioni più vecchie di Windows, come XP e 2003, pool-of-threads non è implementato e il server passa silenziosamente al metodo thread-per-connection.

Variabili server

Generalmente non è necessario cambiare i parametri, perché l'obiettivo è quello di fornire buone prestazioni nel mondo reale. Si prega di aprire un bug, se i valori di default non funzionano bene nel proprio ambiente. Tuttavia, i parametri non sono inseriti in modo fisso nel codice, anzi tutto ciò che l'implementazione sottostante permette di esporre è esposto, pertanto i valori possono essere modificati. Tutte le variabili server documentate sotto sono dinamiche, cioè possono essere modificate a runtime.

Ottimizzare le variabili server su Windows

L'implementazione dinamica del threadpool permette di impostato il numero minimo e il numero massimo di thread. Sono quindi esposte le seguenti variabili:

  • thread_pool_min_threads - Numero minimo di thread nel pool. Il default è 1. Questo si applica per carichi di lavoro con crescite improvvise. Si immagini di avere lunghi periodi di inattività dopo periodi di attività intensa. Mente il threadpool è in attesa, Windows dediderebbe di ritirare i thread (in base agli esperimenti, sembra che questo accada dopo che i thread sono rimasti inattivi per 1 minuto). Quando torna un carico intenso, ci vorrebbero alcuni millisecondi o secondi prima che le dimensioni del threadpool si stabilizzino di nuovo al valore ottimale. Per evitare il ritiro dei thread, si può impostare questo parametro a un valore più alto.
  • thread_pool_max_threads – Numero massimo di thread nel pool. Quando viene raggiunto, i thread non vengono più creati. Il default è 500. Questo parametro può essere utilizzato per impedire la creazione di nuovi thread se possono esserci brevi periodi in cui tutti i client sono bloccati (per esempio con “FLUSH TABLES WITH READ LOCK”, un uso elevato dei lock sulle righe, o simili). Il pool crea nuovi thread se si verifica una situazione bloccante (dopo un certo intervallo), ma a volte si vuole limitare il numero di thread, se si ha familiarità con l'applicazione, per risparmiare memoria. Se l'applicazione si stabilizza a 500 thread, ciò potrebbe indicare che vi sono troppi lock, e il threadpool non aiuta molto.

Ottimizzare le variabili server su Unix

Le seguenti variabili sono esposte dall'implementazione sottostante:

  • thread_pool_size – Numero dei gruppi di thread. Il default è il numero dei processori. Questo parametro è quello che ha effetti più notevoli sulle prestazioni. E' più o meno equivalente al numero di thread che possono essere eseguiti allo stesso tempo (cioè possono usare la CPU, piuttosto che dormire o aspettare). L'implementazione suddivide i client in gruppi, con l'obiettivo di avere un thread eseguibile per ogni gruppo. Un possibile motivo per abbassare questo numero, è che si vuole eseguire il server su un processore dedicato (per esempio con una taskset utility su Linux). Va invece incrementato se, nonostante il carico di lavoro sia legato alla CPU, i processori sono ancora sottoutilizzati.

Il sottoutilizzo delle CPU non dovrebbe accadere in un mondo ideale, ma in questo mondo succede. Solitamente, il pool viene informato quando un thread sta per essere in attesa, però vi sono attese come i page fault o un miss nella cache del buffer di sistema, che non possono essere individuati,mente il rilevamento di certe attese (come quelle legate alla rete) non è attualmente implementato (ma lo sarà in MariaDB 5.6).

  • thread_pool_stall_limit – Numero di millisecondi prima che un thread in esecuzione sia considerato in stallo. Il default è 500. Il threadpool lo sveglierà o creerà un altro thread se non è possibile comunicare. Questo è il meccanismo che impedisce alle query di lunga durata di monopolizzare il pool, e permette temporaneamente a diverse query di operare in parallelo.
  • thread_pool_max_threads – Numero massimo di thread nel pool. Quando questo valore viene raggiunto, non vengono più creati nuovi thread. Il default è 500. Questa variabile ha lo stesso significato che ha su Windows.
  • thread_pool_idle_timeout- Numero di secondi prima che un worker thread che è in attesa termini. Il default è 60. Se non c'è lavoro da svolgere, per quanto tempo un thread deve restare in attesa prima di terminare?
  • thread_pool_oversubscribe – Parametro interno. Modificarlo a proprio rischio. Il default è 3. Ecco una spiegazione approssimativa del suo comportamento: esiste un compromesso tra permettere l'esecuzione di più di 1 thread per ogni CPU oppure mettere il thread in attesa e svegliarlo quasi istantaneamente quando server. Più è alto il valore di questo parametro, più thread possono essere eseguiti insieme. Più è basso, più saranno frequenti le attese e i risvegli.

Monitorare l'attività del pool

Attualmente vi sono due variabili di stato per monitorare l'attività del pool:

VariabileSpiegazione
threadpool_threadsNumero di thread nel pool
threadpool_idle_threadsNumero di thread inattivi nel pool. Ha senso solo sui sistemi Unix, non viene valorizzata su Windows. Un thread va in idle se aspetta che abbia compiti da svolgere,o se è bloccato dall'io su disco, lock di tabella o di riga, etc.

Risolvere le situazioni bloccanti

Anche impostando thread_pool_max_threads a un valore elevato (si ricordi che il default è 500), se vi sono dei lock globali, è possibile che l'intero pool rimanga bloccato. Si immagini una situazione in cui un client esegue FLUSH TABLES WITH READ LOCK e si fermi. Se altri 500 client iniziano un'operazione di scrittura, viene raggiunto il massimo dei thread consentiti e diventa impossibile lanciare un UNLOCK TABLES. Un modo per risolvere il problema potrebbe essere impostare thread_pool_max_threads a un valore molto alto, ma non è certo l'ideale e avrebbe conseguenze negative sulle performance.

Per aggirare il problema, MariaDB consente di utilizzare una connessione amministrativa dedicata. Per poterla usare, si imposti la variabile server extra_port con la porta TCP che si desidera associare alla connessione (non può essere la stessa porta che si usa normalmente) e ci si connetta come superutente usando questa porta. Una volta effettuata la connessione, si può incrementare thread_pool_max_threads o terminare la sessione che crea problemi - nell'esempio sopra è quella che ha acquisito un lock globale (tale connessione dovrebbe trovarsi nello stato 'sleep').

Il threadpool di MariaDB a confronto con quello di Oracle MySQL Enterprise

Le edizioni commerciali di MySQL, a partire dalla versione 5.5 includono Oracle MySQL Enterprise threadpool implementato come plugin, che aggiunge funzionalità simili. La documentazione ufficiale di questa caratteristica si trova nel Reference Manual e una descrizione dettagliata del suo design è reperibile nel blog di Mikael Ronstrom. Segue un sommario delle analogie e delle differenze, basato sulle fonti citate.

Analogie

  • Su Unix, sia MariaDB sia Oracle MySQL Enterprise Threadpool dividono in gruppi le connessioni dei client. Il parametro thread_pool_size ha perciò lo stesso significato in MySQL e MariaDB.
  • Inoltre, le due implementazioni utilizzano un metodo simile per rilevarei thread in stallo, ed entrambe hanno un parametro chiamato thread_pool_stall_limit (tuttavia in MariaDB si misura in milliseconi, non in unità di 10ms come in Oracle MySQL).

Differenze

  • L'implementazione di Windows è completamente diversa - quella di MariaDB utilizza il threadpool nativo di Windows, mentre quella di Oracle ha una funzione WSAPoll() (creata per facilitare il port delle applicazioni da Unix). Una conseguenza dell'uso di WSAPoll() è che l'implementazione di Oracle non supporta le connessioni attraverso i named pipe e la memoria condivisa.
  • MariaDB usa i meccanismi più efficienti di IO multiplexing per tutti i sistemi Windows (internamente, il threadpool nativo usa la porta per l'IO completion), Linux (epoll), Solaris (event ports), FreeBSD e OSX (kevent). Oracle usa l'IO multiplexing solo su Linux, con epoll, mentre sugli altri sistemi usa poll().
  • Diversamente da Oracle MySQL Enterprise Threadpool, MariaDB non cerca di minimizzare la quantità delle transazioni concorrenti.
  • Diversamente da Oracle MySQL Enterprise Threadpool, il threadpool di MariaDB è builtin, non un plugin.

Gli interni del threadpool

I dettagli di basso livello dell'implementazione sono documentati nel WL#246

Eseguire i benchmark

Quando si eseguono sysbench o altri benchmarks, questi creano molti thread sulla stessa macchina come server, ed è consigliabile eseguire il benchmark driver e il server su CPU differenti, per ottenere risultati rialistici. Eseguire molti thread driver e molti thread server sulle stesse CPU fa sì che lo scheduler dell'OS scheduler esegua i thread driver con probabilità molto più alte rispetto ai thread server. Per separare i thread driver e server del benchmark, si può usare "taskset –c" su Linux, e "set /affinity" su Windows.

Commenti

Sto caricando i commenti......