perl e memcached

Creaiamo un ambiente di test  per memcached e testiamolo con perl.

Utilizzeremo:

  • memcahced
  • perl
  • il modulo perl Cache::Memcached
  • il modulo perl String::Random
  • il comando di monitoraggio memcache-top

Do per scontato che perl sia installato. Questo è, credo, praticamente sempre vero in ambiente UNIX/LInux.

Potrebbe non essere invece presente il modulo perl per la gestione della cache con memcached. Lo si può istallare o come pacchetto dell distribuzione (es. su Debian/Ubuntu qualche cosa come sudo aptitude install libcache-memcached-perl)  o come modulo cpan:

$ cpan
cpan[1]> install Cache::Memcached

Analogamente istalliamo il modulo String::Random (libstring-random-perl).

Passiamo ora al daemone.memcached. Consideriamo come sempre un sistema Debian/Ubuntu dove basterà:

# aptitude update
# aptitude install memcached
# update-rc.d -f memcached remove
# /etc/init.d/memcached stop

Il primo comando aggiorna le liste dei pacchetti, il secondo istalla il demone. Dato che stiamo lavorando su di un ambiente di test, con il terzo comando si facciamo in modo che il demone non venga avviato all’avvio del pc e con l’ultimo fermiamo il demone lanciato con le configurazioni di default.

Memcached è presente in quasi tutte le distribuzioni e se non si utilizza un sistema Debian o derivato si può far ricorso al sistema di pacchettizzazione della propria distribuzione o anche ai sorgenti del demone recuperabili dal sito ufficiale.

L’ultimo elemento per l’ambiente di test è memcache-top, che può essere scaricato dal sito ufficiale.

wget -O memcache-top  http://memcache-top.googlecode.com/files/memcache-top-v0.6

Ora che abbiamo tutti gli elementi possiamo aprire una shell e avviare 3 demoni per avere un pool non banale. Li metteremo in ascolto su localhost ma, ovviamente, su porte differenti.

/usr/bin/memcached -d -m 64 -p 11211 -u memcache -l 127.0.0.1
/usr/bin/memcached -d -m 64 -p 11212 -u memcache -l 127.0.0.1
/usr/bin/memcached -d -m 64 -p 11213 -u memcache -l 127.0.0.1

possimao poi avviare in un’altra shell memcache-top con cui monitereremo il nostro pool di server

memcache-top --commands --instance=127.0.0.1:11211,127.0.0.1:11212,127.0.0.1:11213

Vediamo ora un primo semplice script perl che chiameremo memcahced.pl:

#!/usr/bin/perl -w

use strict;
use Cache::Memcached;

my $memd = new Cache::Memcached {
        'servers' => [ "localhost:11211", "localhost:11212", "localhost:11213" ],
        'debug' => 0,
        'compress_threshold' => 10_000,
};

$memd->set("my_key", "Some Value\n");
my $val = $memd->get("my_key");
if ($val) { print $val; }

In dettaglio:

#!/usr/bin/perl -w

use strict;
use Cache::Memcached;

Richiama l’eseguibile attivando i warning e i moduli strict e Memcached.

my $memd = new Cache::Memcached {
        'servers' => [ "localhost:11211", "localhost:11212", "localhost:11213" ],
        'debug' => 0,
        'compress_threshold' => 10_000,
};

definisce l’oggetto di connessione a memcached inserendo una lista di server.

$memd->set("my_key", "Some Value\n");
my $val = $memd->get("my_key");
if ($val) { print $val; }

scrive una chiave su memcached e la rilegge andando poi a scrivere a video il risultato.
Per eseguire questo script non bisogna dimenticare di rendere eseguibile il file o richiamarlo esplicitamente con l’interprete perl.

Con una singola esecuzione memcache-top non dice gran che ma se si esegue ripetutamente lo script ad esempio con

while [ true ]; do ./memcached.pl ; done

si vedranno numerosi accessi ad un singolo server; come ci si sarebbe dovuti aspettare trattandosi di una sola chiave.
Nel mio caso memcahce-top riporta:

memcache-top v0.6	(default port: 11211, color: on, refresh: 3 seconds)

INSTANCE		USAGE	HIT %	CONN	TIME	EVICT/s GETS/s	SETS/s	READ/s	WRITE/s	
127.0.0.1:11211		0.0%	0.0%	5	1.0ms	0.0	0	0	2	344	
127.0.0.1:11212		0.0%	0.0%	5	0.8ms	0.0	0	0	2	344	
127.0.0.1:11213		0.0%	0.0%	5	0.6ms	0.0	14	14	647	992	

AVERAGE:		0.0%	0.0%	5	0.8ms	0.0	5	5	217	560	

TOTAL:		83B/	0.2GB		15	2.4ms	0.0	14	14	651	1680	
(ctrl-c to quit.)

ma soprattutto dopo aver ucciso il demone in ascolto sulla porta 11213

memcache-top v0.6	(default port: 11211, color: on, refresh: 3 seconds)

INSTANCE		USAGE	HIT %	CONN	TIME	EVICT/s GETS/s	SETS/s	READ/s	WRITE/s	
127.0.0.1:11211		0.0%	0.0%	5	0.6ms	0.0	0	0	2	344	
127.0.0.1:11212		0.0%	0.0%	5	0.4ms	0.0	15	15	662	1007	
127.0.0.1:11213 is DOWN.

AVERAGE:		0.0%	0.0%	3	0.3ms	0.0	5	5	221	450	

TOTAL:		83B/	0.1GB		10	1.0ms	0.0	15	15	664	1351	
(ctrl-c to quit.)

dove so vede che le connessioni si sono spostate su di un’altro demone. Su questo aspetto bisogna fare molta attenzione dato che in un sistema di produzione la gestione dei fault  è cruciale e dato che questo aspetto è demandato alla libreria del linguaggio che si utilizza  bisogna averne molto chiaro il comportamento. Vale comunque in generale che bisognerebbe avere in numero sufficiente di server per cui se se ne dovesse perdere uno i restanti dovrebbero essere sufficienti a svolgere i loro compiti; in altre parole è molto meglio distribuire il servizio su molti serevr che averne uno o due dedicati.

Per vedere in che modo le chiavi si distribuiscono sul cluster ora inseriamo un valore fisso in una chiave di nome casuale (la distribuzione è legata al nome della chiave). Per farlo utilizziamo un altro script perl che chiameremo memcahced_random.pl:

#!/usr/bin/perl -w

use strict;
use Cache::Memcached;
use String::Random;

my $memd = new Cache::Memcached {
        'servers' => [ "localhost:11211", "localhost:11212", "localhost:11213" ],
        'debug' => 0,
        'compress_threshold' => 10_000,
};

my $foo = new String::Random;
my $random_key = $foo->randpattern("...");
my $value = "chiave casuale";

$memd->set("$random_key", "$value\n");
my $val = $memd->get("$random_key");
if ($val) { print $val; }

Non è molto diverso dal precedente tranne nella riga:

my $random_key = $foo->randpattern("...");

in cui viene generata casualmente una stringa di 3 caratteri da utilizzare come nome della chiave.
Anche in questo caso conviene inserire lo script in un loop per visualizzarne meglio gli effetti con memcache-top:

while [ true ]; do ./memcached_random.pl; done

Le connessioni si andranno a distribuire sui vari nodi in modo ragionevolmente equilibrato

memcache-top v0.6	(default port: 11211, color: on, refresh: 3 seconds)

INSTANCE		USAGE	HIT %	CONN	TIME	EVICT/s GETS/s	SETS/s	READ/s	WRITE/s	
127.0.0.1:11211		0.1%	0.0%	5	0.9ms	0.0	7	7	317	686	
127.0.0.1:11212		0.1%	0.0%	5	0.6ms	0.0	9	9	375	748	
127.0.0.1:11213		0.1%	0.0%	5	0.6ms	0.0	9	9	403	779	

AVERAGE:		0.1%	0.0%	5	0.7ms	0.0	8	8	365	738	

TOTAL:		132.7KB/	0.2GB		15	2.2ms	0.0	25	25	1095	2213	
(ctrl-c to quit.)

Cluster Mysql

Una delle caratteristiche più interessanti di mysql è la possibilità di replicare le modifiche al database su di un altro nodo che verrà mantenuto quindi aggiornato e al tempo stesso sarà disponibile per effettuare query in lettura,  backup o altro.

Questa caratteristica da luogo alla possibilità di implementare cluster con configurazioni (topologie) più o meno articolate che, avendo caratteristiche differenti, potranno tornare utili in situazioni diverse.

Quello che si cerca di ottenere in genere con queste configurazioni è in genere ridondanza per gestire fault dei server minimizzando il disservizio, scalabilità orizzontale nelle prestazioni o entrambe. Per scalabilità orizzontale si intende la possibilità di aggiungere server al sistema per aumentarne le prestazioni e si contrappone alla scalabilità verticale in cui per far crescere le prestazioni bisogna sostituire l’hardware precedente con altro in genere molto più costoso.

Altre volte quello che si vuole è un nodo disponibile per attività non standard: la possibilità di effettuare dei backup fuori linea, un ambiente su cui generare reportistica senza impattare sulle prestazioni del sistema di produzione, un ambiente su cui fare debugging o altro.

La configurazione più semplice possibile è una configurazione master-slave. In questa configurazione un solo nodo del cluster riceve le chiamate che modificano i dati mentre l’altro è disponibile per altre attività come i backup, attività di reporting o anche l’esecuzione di query di sola lettura di dati.

Sono utili alcune precisazioni:

  • mysql implementa la replica ma nessun meccanismo di distribuzione delle query. E’ demandato all’applicativo l’onere di inviare le query al server giusto. Questo si può fare sia tramite logica interna al client, sia con strumenti che si interpongono tra client e server quali proxy, pooling o loadbalancing.
  • mysql implementa la replica ma non implementa nessun meccanismo di tackeover: se il master dovesse morire il cluster perderebbe il nodo su cui vengono eseguite le query le query in scrittura. Anche in questo caso è demandato a strumenti esterni quali script, load balancing o lo stesso applicativo client l’onere di implementare la ridondanza. C’è da dire poi che questa non è la tipologia di cluster migliore per implementare la ridondanza data la sua forte asimmetria.
  • Se si utilizza il nodo mysql slave per fare dei backup si può avere un dump coerente con strumenti standard anche solo interrompendo i processi di replica; in questo modo infatti si sono sospese tutte le scritture sul nodo in esame.
  • se si utilizza il nodo mysql slave per fare dei backup e si vuole poter sfruttare il point in time recovery, bisogna abilitare i binary log anche sul nodo slave dato che sono questi che dovranno essere associati al backup stesso. O, meglio, bisogna associare al backup l’informazione dello stato della replica rispetto ai binary log del master; informazioni disponibili sullo stato della replica.

Una topologia di replica secondo me molto più interessante e flessibile è quella master-master. In questa configurazione si ha una coppia di server mysql ognuno dei quali replica le modifiche dell’altro.

Questa configurazione è estremamente flessibile perché, data la sua simmetria, è molto facile realizzare un sistema in High Havailability in cui un nodo venga sostituito dall’altro. E’ possibile farlo con diversi struementi, ad esempio heartbeat o un bilanciatore. Inoltre si può sia sfruttare entrambi i nodi per le scritture, sia utilizzare il cluster master-master come se fosse un sistema master-slave sia sfrutatrlo solamente per la ridondanza accedendo in condizioni normali ad un solo nodo. In tutti i casi però la simmetria del sistema fa si che nell’automatizzare lo scambio dei ruoli dei server non sia necessario intervenire sulla configurazione del database.

Ovviamente questa semplicità ha un costo, o se si preferisce i problemi vengono spostati altrove. In questa configurazione ci sono molte sottigliezze legate alla replica di cui l’applicazione deve tenere conto per non dare risultati sbagliati; sottiglizze legate soprattutto agli autoincrement, agli ordinamenti e a considerazioni di ACID compliance.

Se i nodi sono più di due le possibilità sembrano moltiplicarsi ma le vie ragionevolmente percorribili non sono poi così tante.

La prima possibilità è quella di avere un master e molti slave: questa configurazione, molto utile per i siti web in quanto permette di far crescere quasi a piacere la capacità di gestire query in lettura, presenta il non piccolo problema di non essere ridondata sul nodo master. L’implementazione di script che promuovono uno dei nodi slave al ruolo di master nel caso di fault del master precedente presenta moltissimi problemi: si pensi solo alla difficoltà di determinare qual’è lo slave più avanti con la replica e quindi più adatto a prendere il ruolo di master, per non parlare dei problemi nel riattestare tutti gli slave sul nuovo master.

Più praticabile, ma ovviamente molto più costosa, è la possibilità di implementare un cluster classico active-passive con disco condiviso per il solo master. Anche in questo caso comunque alcune attività possono rendersi necessarie sui nodi slave in caso di takeover del master.

Si può poi pensare di estender il caso di configurazione master-master  in una topologia ad anello in cui ogni nodo è replica del precedente e il primo è replica dell’ultimo. Questa configurazione nella pratica non è una grande idea per due ragioni:

  • il sistema nel suo insieme è sempre meno affidabile al crescere del numero dei nodi perché la rottura di un elemento qualsiasi della catena interrompe la propagazione delle modifice e bisogna quindi implementare una logica tutt’altro che banale che automatizzi l’esclusione di nodi rotti. Nodi che soarà poi comunque difficile reinserire senza dare disservizi.
  • ci sono potenziali problemi insidiosi legati al meccanismo di replica di mysql; questo infatti legge tutte le query nel binary log che ha in ingresso e se trova che il primo esecutore di una query è stato un altro nodo la esegue e la scrive nel proprio binary log altrimenti, se cioè riconosce di essere stato il primo esecutore di una query, la scarta. Tutto funziona bene fino a che non muore un nodo. Si pensi a cosa succede se in un sistema di tre server mysql A, B e C configurati ad anello: A master di  B master di C master di A si verifica la seguente situazione. Il nodo A esegue una query di insert e poi muore ad esempio per la rottura di un disco ma solo dopo aver passato l’insert al nodo B. Gli script del sistema riconoscono il problema ed escludono il nodo A rendendo C master di B. B riconosce come non propria la query ricevuta da A, la esegue e la passa a C; C riconosce la query come non propria, la esegue e la passa a B etc in un loop infinito. Per la verità le versioni più recenti di mysql hanno introdotto funzionalità per controllare questo problema ma la situazione rimane delicata.

Un’ultima configurazione che si può pensare di implementare è master-master ( o anche un anello con n master) su cui attestare un gruppo di slave. Gli slave andranno ovviamente divisi tra i master. Il problema di questa configurazione è che se uno dei master viene meno, si perde la possibilità di utilizzare tutti gli slave che a lui fanno capo a meno di non aver implementato una logica che permetta di riattestare gli slave sul master residuo.

awk e linea di comando

La shell di UNIX/Linux è uno strumento potentissimo che permette di velocizzare ed automatizzare le più svariate operazione. Dato che in UNIX ogni cosa è un file, sono di fondamentale importanza quegli strumenti che permettono di analizzare e gestire i file.

Spessissimo i file con cui si ha a che fare nelle attività sistemistiche sono dei rudimentali database strutturati con i record in righe e i campi in colonne. In questa categoria rientrano ad esempio i file passwd e moltissimi file di log.

Per poter automatizzare operazioni su questo tipo di file è necessario uno strumento che dia accesso a specifici campi nei vari record. Come sempre in ambiente UNIX/Linux è possibile procedere in diversi modi ma uno strumento di sicuoro interesse è awk. Si tratta di uno di quei tool la cui conoscenza non dovrebbe mancare a nessun sistemista.

Nella sua forma più semplice awk accetta in stdin un file, e permette di estrarre dei campi da ogni riga.

Ed esempio se volessi estrarre gli utenti che hanno script schedulati nel seguente file crontab:

17 *	* * *	root    cd / && run-parts --report /etc/cron.hourly
25 6	* * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6	* * 7	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6	1 * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )

potrei utilizzare la seguente righa di comando:

cat /etc/crontab | awk '{print $6}'

all’interno di awk ogni campo, infatti, è contenuto in una variabile $n, dove n è il numero del campo ed i campi sono separati de default da spazi. L’intera riga è contenuta nella variabile $0. Ci sono molte altre variabili predefinite in awk e la lista completa è riportata nel suo manuale (man awk). Una menzione la merita $NR che contiene il numero della riga che si sta elaborando.

Non in tutti i file il separatore è lo spazio; l’opzione -F permette di scegliere un separatore differente.

awk può essere utilizzato come filtro sfruttando il costrutto if al suo interno:

Ad esempio se voglio sapere quali utenti non hanno una shell reale posso utilizzare:

cat /etc/passwd|awk -F: '{if ($7=="/bin/false") print $1}'

Di grande interesse anche i costrutti BEGIN ed END che permettono di definire blocchi di operazioni da eseguire prima o dopo l’esame del file. Posso ad esempio riscrivere lo stesso esempio di prima come:

cat /etc/passwd|awk 'BEGIN{FS=":"}{if ($7=="/bin/false") print $1}'

awk è un vero e proprio linguaggio di programmazione anche se abbastanza semplice, e le possibilità che offre sono molto ampie; nondimeno la sua utilità è soprattutto nelle sue forme più semplici dove, con molta semplicità, permette di improvvisare filtri o operazioni anche molto sofisticati.

Concludo con un esempio dove mostro come awk può essere utilizzato per calcolare la durata media delle slow query a partire da un file di log di mysql:

cat mysql-slow.log |grep Query_time|awk '{cont++;totale+=$3}END{media=totale/cont; print "media = " media}'