giovedì 23 settembre 2010

Un router di alto profilo a "quattro soldi"

Sono ormai anni che provo a cercare una soluzione decente per i miei clienti, qualsiasi sia la connettività.
A parte un paio di "sfortunati" che sono obbligati ad usare una connessione GSM/UMTS, la maggior parte usa una "semplice" ADSL, una manciata ha una connessione in fibra ottica e qualcuno la connettività HDSL/sHDSL.
I problemi principali da risolvere sono:

  1. il costo deve essere congruo con tutto il resto
  2. deve supportare i diversi tipi di connettività
  3. deve supportare: NAT, VPN e DNS dinamico
  4. deve essere gestibile da remoto in sicurezza
  5. deve essere supportato con continuità al livello di firmware
Per il punto n.2 sono pervenuto alla decisione di separare la porzione di routing da quella di connessione. Il router lavora solo su ethernet, niente modem ADSL o UMTS integrato. I motivi sono sostanzialmente tre.
Il primo è che i router con modem integrato sono di solito adatti ad applicazioni "small office". Basta una dozzina di client che facciano un po' di traffico e il router-modem diventa un collo di bottiglia.
Il secondo è che questi funzionano solo per ADSL e UMTS. Per HDSL e fibra serve comunque un router classico.
Il terzo è che diversità di firmware (e di marca) spesso sfociano nell'impossibilità di far dialogare le VPN, a meno di non riallineare firmware (e marcha e modelli).
Per i punti 3, 4 e 5,  ho dovuto sudare molto non solo con produttori (almeno in teoria) di alto livello, ma anche con i loro sviluppatori di firmware (quasi tutti indiani). Senza cavare alla fine un ragno da buco.
Almeno finché non sono incappato in un progetto opensource, OpenWRT e nel suo "fratello" a vocazione più commerciale DD-WRT.
Sapevo, già dai tempi del mio buon NetGear 384 della possibilità di alcune soluzioni alternative, ma di progetti così articolati e completi ero completamente all'oscuro, almeno fino ad un annetto fa.
I due progetti "WRT" prendono il nome dalla famosa quanto fortunata serie di router-access point della Linksys-CISCO, di cui il WRT54G è stato capostipite. Il fatto che il produttore, in ottemperanza ai dettami della licenza GPLv2, rilasci tutto il codice sorgente della distribuzione Linux usata, è il fattore che ha determinato la nascita dei due progetti.
Questi offrono, risorse a bordo del router permettendo, un ventaglio di funzionalità senza precedenti, soprattutto se è presente anche hardware per il WiFi.
OpenWRT abbarraccia la filosofia delle distribuzioni Linux. Il tutto è a riga di comando, tassativamente via SSHv2, con un'interfaccia web minimale e le funzioni possono essere estese con l'istallazione di pacchetti aggiuntivi, anche scaricati direttamente da internet. DD-WRT prevede una manciata di versioni, a seconda della quantità di memoria (flash e RAM) a bordo dei router e delle funzionalità che si ritengono necessarie.
I due progetti restano comunque compatibili, per cui è possibile installare i pacchetti di OpenWRT su un router "powered by" DD-WRT.
Un giro sui relativi siti web non può che illuminare.

Il mio primo esperimento s'è svolto, per conto di un cliente, con una coppia Linksys WRT320N. Con 200 EURO e un po' di lavoro, le due sedi sono in VPN su ADSL ad una frazione del costo preventivato dai fornitori "ufficiali" e "blasonati" (si cita il peccato, non il peccatore). Solo che, invece dell'OpenWRT ho dovuto utilizzare il DD-WRT, fortunatamente gratuito su questo modello.
Inutile dire che in mezza giornata tutto era funzionante alla perfezione.
Ma il vero colpo l'ho fatto con due "tranquilli " TP-Link WR1043ND, 60 EURO l'uno con caratteristiche hardware uguali se non superiori al precedente. Porta USB compresa. Anche qui ho installato il firmware di DD-WRT, ma il supporto di OpenWRT è totale e penso che in futuro passerò a questa. Quella porta USB può essere usata per realizzare un print server, un file server e, udite udite, per collegare un modem UMTS USB (le cosiddette "chiavette internet"). Le antenne staccabili consentiranno l'aggancio di un'antenna direttiva ad alto guadagno per realizzare un ponte radio con una terza sede a 2 chilometri di distanza.

La lista di hardware supportato da entrambi i progetti è molto vasta e, normalmente, aggiornata. per cui la scelta di un router di partenza è determinata dalle sue caratteristiche hardware e, ovviamente, dal costo.
Ma alla fine ciò che si acquista non sarà altro che "ferro" su cui far girare un firmware di alto profilo.

Concludo dicendo che, dal momento che il firmware usato è sostanzialmente lo stesso, non è più importante che i router acquistati siano della stessa marca o lo stesso modello. Importa solo che possano ospitare le stesse funzioni.
Non male, direi. E una donazione in denaro ai progetti è un obbligo morale.

mercoledì 22 settembre 2010

Divagazioni sul table partitioning in PostgreSQL - parte 2

Una prima soluzione per nulla insoddisfacente l'ho trovata definendo i nomi delle tabelle in modo opportuno. "Codificando" cioè nei loro nomi la condizione che fa sì che una riga vi appartenga. Prima che iniziate a storcere il naso tengo a precisare che neanche a me piace molto questa cosa, ma bisogna ammettere che è funzionale e ragionevolmente efficiente.
In questo modo il trigger che intrappola, ad esempio, le INSERT può:
  1. "calcolare" il nome della tabella figlia candidata all'operazione
  2. verificare se questa esiste
  3. crearla in caso negativo
  4. realizzare l'operazione di INSERT vera e propria
In questo modo la "tabella di supporto" di cui ho parlato non sarebbe altro che il catalogo di sistema. Non serve alcuna tabella extra e relative operazioni di DML. Le CREATE TABLE fanno già tutto.
Quanto poi ad inserire informazioni nel nome di una tabella, a guardare bene, corrisponde ad inserire informazioni in una colonna di una tabella del catalogo. Il che non è poi così inelegante: La condizione da testare corrisponde ad una parte del testo (il nome della tabella) da ricercare nel catalogo.
Semplice come bere un bicchiere d'acqua ... stando a testa ingiù.
Ad esempio, le partizioni della tabella

CREATE TABLE movimenti_magazzino (
  maga int8 not null,
  prod int8 not null,
  qnta numeric not null,
  data timestamp not null
);

suddivisa in base al magazzino e alle settimane di movimentazione potrebbero chiamarsi:
"moma maga=42,week=19,year=2010","moma maga=42,week=20,year=2010","moma maga=42,week=21,year=2010" ...

usando il quoting dei nomi come da manuale.
Questa soluzione, inoltre, è chiaramente migliore di quest'altra:
  1. "calcolare" il nome della tabella figlia candidata all'operazione
  2. provare a realizzare la INSERT
  3. intrappolare l'eventuale errore con BEGIN...EXCEPTION...WHEN per creare la tabella figlia e rieseguire la INSERT
I  questo caso, come noto, il costrutto di intrappolamento degli errori nasconde una penalità di performance che impatterebbe su ogni singola INSERT. Questa penalità è certamente superiore a quella di un "IF NOT FOUND THEN". Si tratta più o meno, dello stesso problema che si trova, ad esempio, in C++ con le eccezioni e i blocchi "try { ... }".

Abbiamo poi trovato un'altra possibilità ancora, che si discosta un po' dallo schema ufficiale e che fa completamente a meno dei TRIGGER. Ecco la sostanza della tecnica.
Tutte le operazioni avvengono direttamente (e solamente) sulla tabella madre, definita con tutti gli indici e le constraint del caso e senza alcun TRIGGER.
Quando, e se possibile, questa viene scorsa da una procedura che realizza lo smistamento delle righe nelle relative tabelle figlie, creandole "on demand". Questa procedura può essere scritta in modo da limitare il numero di linee da smistare per volta. In questo modo la si può eseguire in modo "schedulato" e incrementale, al limite anche durante le normale operazioni sulla tabella madre senza creare LOCK troppo estesi.
In questo modo le penalizzazioni delle normali operazioni sono nulle ed il costo di "smistamento" può essere frazionato e diluito nel tempo o, se l'applicazione lo consente, effettuato tutto in una volta nella fase di manutenzione del DB, poco prima cioè di eseguire la VACUUM [ANALYZE].

L'ultima, almeno per il momento, soluzione "alternativa" diverge in modo totale da tutte quelle vista in precedenza. Nessun trigger, come per la soluzione di prima, e nemmeno ereditarietà.
Alla prossima.

giovedì 16 settembre 2010

Divagazioni sul table partitioning in PostgreSQL - parte 1

Il table partitioning, italianamente reso come partizionamento delle tabelle, è una tecnica/tecnologia degli RDBMS per snellire le operazioni su tabelle molto grandi.
In sostanza una tabella madre viene suddivisa in un numero di tabelle figlie secondo dei criteri per i quali ogni riga non può che appartenere ad una ed una sola tabella figlia.
Tabelle più piccole e maneggevoli consentono operazioni di manutenzione (leggi VACUUM)  più brevi, comportano indici più piccoli che stiano più facilmente in memoria.
Ad esempio un caso tipico utilizza una colonna di tipo DATE o TIMESTAMP per suddividere la tabella madre in "sezioni temporali".
PostgreSQL di per sé non supporta il partizionamento ma supporta delle funzionalità di base con le quali farne un implementazione. La documentazione ufficiale dedica un intero capitolo, il 5.9, all'argomento con anche una proposta abbastanza dettagliata per l'implementazione.
Alla base vi sono alcuni meccanismi:

  1. l'ereditarietà gerarchica per definire una relazione di "figliolanza" fra la tabella madre e quelle figlie;
  2. le table constraint per garantire (e documentare al query planner) il criterio di partizionamento;
  3. la constraint exclusion per ottimizzare le query (SELECT) limitandole alle sole tabelle figlie "interessanti".

In pratica le SELECT sulla tabella madre (che di fatto resta vuota) vengono dirottate su una o più tabelle figlie in base al contenuto della condizione WHERE.
Le altre operazioni (INSERT, UPDATE, DELETE) vengono "catturate" al volo tramite delle funzioni TRIGGER e dirottate sulla giusta tabella figlia.
In questo modo, peraltro, la tabella madre resta sempre vuota, come solo riferimento (o "avatar") di tutta la gerarchia.
La tecnica ufficiale impone due importanti vincoli:

  1. la struttura gerarchica deve essere definita prima di ogni operazione;
  2. la struttura gerarchica deve poter ospitare tutte le possibili righe di ogni operazione
In generale il primo vincolo non è un grosso problema se non lo è anche il secondo. Ad esempio: si gestiscono movimentazioni di magazzino (in senso generico) appartenenti al solo anno solare in corso e si sono create partizioni (tabelle figlie) per ogni mese dell'anno.
Se ad esempio esistono magazzini "virtuali" per gli ordini schedulati nel futuro o se si effettuano movimenti correttivi "retrodatati", allora il vincolo n.2 potrebbe non essere soddisfatto a meno di poter estendere la struttura gerarchica nel passato e/o nel futuro.
E' facile pensare ad una prima variante. Invece di creare la gerarchia in modo preventivo, la si può creare "on demand", man mano che le tabelle figlie diventano necessarie.
Chiaramente se pensiamo al partizionamento per mesi, la cosa ha poco senso. Se pensiamo ad un partizionamento per settimane, magazzini e, magari, categorie di prodotti, la considerazione assume un'altro valore e tutt'altro peso.
Risparmiandoci la creazione statica e preventiva della gerarchia, risolviamo in un colpo solo tutti e due i vincoli imposti dalla soluzione ufficiale. Ogni movimento, sia esso passato o futuro, troverà certamente posto in una tabella figlia.
Questa soluzione impone però un radicale cambiamento nell'implementazione.
Innanzitutto si deve automatizzare tutta la parte DML che serve per creare dinamicamente le tabelle figlie. Compito non facile, ma certamente un buon esercizio, soprattutto in PL/PgSQL.
In seconda battuta, va notato come l'implementazione ufficiale si basa su TRIGGER in cui una cascata di IF viene usata per selezionare la tabella figlia giusta. Non discuto sull'efficienza della cosa, anche perché non so se l'interprete PL/PgSQL operi qualche ottimizzazione (non credo, però). Mi preoccupo del fatto che quella cascata di IF va ricreata ogni volta che si aggiungono (o tolgono) tabelle figlie. Per poterlo fare serve una tabella di supporto in cui riportare da un lato il criterio di selezione e dall'altro il nome della corrispondente tabella. E possibilmente una funzione per automatizzare la cosa: devono essere anche ridefinite le funzioni TRIGGER.
E questo ci porta al prossimo secondo passo.