
Mi chiamo Yuri, sono il responsabile del team di amministrazione di sistema in Citymobil. Oggi condividerò la mia esperienza con la tecnologia del thin provisioning dei file system Linux e spiegherò come può essere applicata nei processi CI/CD aziendali. Analizzeremo la situazione in cui, per i test automatici del codice durante il suo deploy in produzione, abbiamo urgentemente bisogno di copie di database MySQL, il più simili possibile alla versione "live", disponibili sia in lettura che in scrittura.
Introduzione: perché dare consigli dannosi?
Una domanda legittima, dato che esistono meccanismi collaudati per la migrazione degli schemi di database negli ambienti di test. Perché mai portare un database principale non sharded a tali volumi? E per i test non sono necessari tutti i dati. Proverò a spiegare.
Circa un anno fa, a seguito della crescita attiva del nostro aggregatore di taxi (nel 2018 siamo cresciuti di circa 15 volte per numero di corse completate), sono aumentati i volumi di dati, il carico sui server e la frequenza dei rollout. Ci siamo trovati nella seguente situazione:
- Il database principale MySQL è cresciuto fino a circa 1000 tabelle con un volume totale di 2,5 TB e continuava a crescere.
- Non c'era la possibilità di sharding rapido e di distribuzione del database. Questo era impedito dal vecchio approccio "scrivo nel database quello che voglio e come voglio", con una miriade di JOIN e dipendenze interne delle tabelle.
- Non c'era un meccanismo per la migrazione dello schema del database negli ambienti di test.
- Non c'era un testing automatico del codice durante il rollout in produzione.
Volevamo risolvere quest'ultima problematica nel minor tempo possibile. Erano già stati scritti test Postman per verificare il principale monolite PHP, ma mancava un database attuale. Inoltre, non potevamo creare una replica di notte, renderla master e metterla a disposizione durante il giorno: il grande numero di rollout e modifiche, incluse quelle ai dati e allo schema del database, avrebbe reso l'ambiente non funzionante già a metà giornata. Inoltre, limitare i rollout solo all'orario lavorativo non sarebbe stato efficace.
Tuttavia, l'obiettivo è stato raggiunto: abbiamo ottenuto il primo ambiente di lavoro già dopo due settimane. Nel corso dell'anno ha subito molte modifiche e continua a essere utilizzato.
Di seguito descriverò in dettaglio tutti i passaggi e le fasi di sviluppo della nostra soluzione. Vi assicurerò che questo metodo merita di esistere.
Che cos'è il 'thin provisioning'?
Si tratta di una tecnologia hardware o software (altro nome: sparse volumes), che consente di allocare una quantità di risorse necessarie superiore a quella effettivamente disponibile. La quantità allocata deve soddisfare i criteri di just-enough (tanto quanto serve) e just-in-time (nel tempo necessario). Il thin provisioning è principalmente utilizzato in diversi sistemi di archiviazione dati per fornire spazio su disco in volume necessario, superiore a quello effettivamente disponibile. La tecnologia è supportata da vari file system, come LVM2, ZFS, BTRFS. È ampiamente usata nei hypervisor di virtualizzazione. Per noi, il thin provisioning ha consentito di creare rapidamente, a partire dagli snapshot della partizione principale contenente i dati, così tante copie di questa partizione quante ne avevamo bisogno (la directory dei dati del database MySQL).
Primo ambiente, tecnologia Thin LVM
Questo capitolo potrebbe essere intitolato 'Come creare snapshot estremamente veloci di grandi volumi di dati utilizzando , riducendo la stabilità del file system e del database MySQL a livelli inaccettabili'.
Poiché avevamo già utilizzato LVM per costruire le partizioni principali del sistema operativo, abbiamo deciso di partire proprio da essa. Prima di tutto, avevamo bisogno di una macchina fisica dedicata: una replica del nostro database MySQL principale, dalla quale potevamo creare a richiesta uno snapshot della replica e avviarlo come un'istanza separata di MySQL. Durante il periodo di test, abbiamo autorizzato l'uso di operazioni modificative su questa istanza e al termine dei test la abbiamo tranquillamente eliminata. La configurazione del server era la seguente:
- 2 x Intel Silver 4114 (10×2,2 GHz HT)
- 8 x 32 GB DDR4
- 8 x 1920 GB Intel SSD in RAID con controller Adaptec in RAID-10
Si potrebbe scrivere un articolo a parte sulla scelta tra controller RAID e RAID software MD. Posso dire solo che la nostra scelta è stata influenzata da due fattori:
- Al momento in cui è stato posto il problema, avevamo installato tutti i database su controller RAID, quindi si può dire che sia stata una scelta storica.
- La differenza nelle prestazioni nei test sintetici del file system e nei test con diverse operazioni in MySQL era minima.
Abbiamo suddiviso il RAID-10 risultante: abbiamo creato un'unica Volume Group (VG) per l'intero spazio (con costi aggiuntivi di circa 6,7 GB) e abbiamo creato una partizione logica (Logical Volume, LV) di 50 GB per il sistema. In una situazione normale, utilizzeremmo il resto dello spazio per la partizione di MySQL. Tuttavia, avevamo bisogno di un backup sottile, quindi inizialmente abbiamo creato quello che viene chiamato un pool, all'interno del quale abbiamo creato una partizione per /var/lib/mysql di 3,5 TB (in base ai volumi previsti del DB):
lvcreate -l 100%FREE -T vga/thin
lvcreate -V 3.5T -T vga/thin -n mysqlAbbiamo formattato la partizione in ext4, l'abbiamo montata, registrato una replica e ottenuto l'ambiente iniziale. Poi abbiamo creato un'API che deve generare snapshot, avviare un'istanza di MySQL su una porta specificata ed eliminare l'istanza creata. Poiché utilizziamo esclusivamente chiamate di sistema, abbiamo scelto di scrivere gli script in bash standard, e per l'integrazione API HTTP → bash abbiamo implementato una soluzione open source , scritta in Go.
Un giorno condivideremo i nostri script bash in open source, ma per ora descriverò semplicemente l'algoritmo principale:
Creazione dello snapshot principale snapmain:
- Ferma la replica principale.
- Imposta un blocco sulle operazioni con lo snapshot snapmain.
- Crea un nuovo snapshot snapmain.
- Avvia MySQL e rimuovi il blocco.
Creazione di un DB su una porta casuale dallo snapmain:
- Imposta un blocco su un'istanza specifica di DB (porta).
- Controlla la presenza del blocco sulla creazione dello snapshot principale. Se è presente, aspetta e ricontrolla ogni 5 secondi.
- Verifica se esiste una vecchia partizione LV per l'istanza.
3.1 Se esiste, fermiamo l'istanza MySQL con kill -9 e eliminiamo la partizione LV. - Crea un nuovo istanza dallo snapmain.
- Prepara e monta le directory per quest'istanza.
- Rimuovi i segni di slave (file) e avvia l'istanza di MySQL.
- Trasformala in master.
- Rimuovi il blocco.
Eliminazione di un DB su una porta casuale:
- Imposta un blocco su un'istanza specifica di DB (porta).
- Fermiamo l'istanza MySQL usando kill -9.
- Smontiamo le directory.
- Eliminiamo la partizione LV e rimuoviamo il blocco.
Ecco alcuni comandi per clonare le partizioni della nuova istanza di DB:
lvcreate -n stage_3307 -s vga/snapmain
lvchange -ay -K vga/stage_3307
mount -o noatime,nodiratime,data=writeback /dev/mapper/vga-stage_3307 /mnt/stage_3307Ora parlerò del principale problema che abbiamo affrontato nell'uso del backup sottile. Ci siamo scontrati con le prestazioni degli SSD. Questo è successo a causa delle peculiarità di Thin LVM: opera a livello dispositivo con chunk di basso livello di default di 4 MB. Come si è presentato:
- Crea uno snapshot dalla partizione principale /var/lib/mysql.
- Avvia la replica per raggiungere il master.
- Qualsiasi modifica nelle tabelle della replica fa sì che vengano mantenuti i vecchi chunk di dati invariati nella partizione dello snapshot.
- Qualsiasi modifica nell'istanza di test avviata costringe a mantenere i vecchi chunk di dati invariati nella partizione dello snapshot clonata per quest'istanza.
- Otteniamo un carico delle operazioni di input-output al 100% sul dispositivo, rallentando qualsiasi operazione e causando il graduale ritardo della replica.
- Alla fine della giornata lavorativa, otteniamo un ambiente indietro di alcune ore.
Ecco come abbiamo combattuto per ottenere risultati più ragionevoli (punti principali):
Controller RAID:
- Disattivato di default tutti i tipi di caching.
- Impostato writeback (quando i dati vengono memorizzati nel buffer, la scrittura termina prima che il salvataggio effettivo su disco sia completato).
File system:
- Nella directory di montaggio /var/lib/mysql abbiamo specificato noatime,nodiratime,data=writeback
- Disattivato il journaling ext4 usando tune2fs.
MySQL:
- Impostato innodb_flush_method = O_DSYNC (aumentato la velocità di scrittura, riducendo così l'affidabilità).
- Disattivato il journaling, non ci servono i log.
- Impostato innodb_buffer_pool_size = 4G (più è piccolo il pool InnoDB, più velocemente MySQL si spegnerà durante l'arresto, e più velocemente creeremo lo snapshot).
Questa non è affatto una lista completa, soprattutto per MySQL. Tuttavia, le altre modifiche sono minori e spesso non applicabili in modo preciso. Ad esempio, nel tentativo di alleggerire i dischi, abbiamo anche spostato innodb_parallel_doublewrite_path in /dev/shm, il che in alcuni casi, all'avvio di un'istanza non correttamente terminata, ci ha permesso di risparmiare fino a 5 secondi.
Perché fermiamo MySQL prima di fare uno snapshot? Possiamo anche estrarlo da una replica funzionante. È vero, ma la nuova istanza di DB basata su questo snapshot sarà considerata danneggiata di default e richiederà una scansione completa all'avvio. Fermare la replica è sicuramente più veloce, anche se alla fine rappresenta l'operazione più lunga dell'intero processo.
Di conseguenza, abbiamo ottenuto tempistiche più accettabili e un ambiente pronto all'uso. Anche se, come dimostra il grafico più eloquente del ritardo della replica principale, la situazione è ancora lontana dall'essere ideale:

Tra gli altri svantaggi, va notata la quasi impossibilità di monitorare il pool Thin LVM: oltre alle funzioni standard di sistema come iostat, non è possibile capire, ad esempio, quale elemento del pool sta attualmente generando il maggior carico sul file system.
Va sottolineato un grande svantaggio legato all'ottimizzazione descritta sopra: abbiamo ottenuto un ambiente YOLO. Circa una volta ogni uno o due mesi, ext4 non ha retto tali maltrattamenti e si è rotta irreparabilmente, richiedendo una riformattazione e un nuovo caricamento della replica. Guadagnando in velocità, abbiamo compromesso irrimediabilmente la stabilità.
Quali metriche tenere sotto controllo durante l'uso del Thin LVM:
- Percentuale di dati del pool sottile
- Percentuale di metadati del pool sottile
Se lo spazio dati esaurito verrà gestito dal nostro stand (basta pulire i dischi), lo spazio esaurito per i metadati porterà al completo collasso del pool e alla necessità di ricrearlo da zero.
Il file system all'interno del pool si frammenta notevolmente nel tempo. Consiglio di eseguire quotidianamente il comando tramite cron fstrim -v /var/lib/mysql.
Risultati intermedi:
- La tecnologia è facilmente applicabile, proprio come l'LVM stesso, e non richiede particolare qualificazione dell'ingegnere.
- Si adatta bene a database di piccole dimensioni e non eccessivamente carichi. Maggiore è la dimensione del database, più chunk vengono spostati nel file system all'interno del pool, aumentando il carico sui dischi.
- Per la nostra esigenza, abbiamo iniziato a cercare altre soluzioni, di cui si parlerà nel prossimo capitolo.
Secondo stand, tecnologia ZFS
Un tempo ebbi a che fare con il file system ZFS, ma allora ZFS funzionava bene solo sul suo sistema operativo nativo Solaris. Esisteva una versione portata su FreeBSD con un livello di implementazione sufficientemente buono. C'era anche un port incompleto su Linux, che pochi usavano. A causa della struttura di archiviazione dei dati B-tree (che è la stessa utilizzata anche da InnoDB di MySQL), ZFS mostrava scarse performance su installazioni con un numero elevato di file. Questo, unito alla necessità di studiare la tecnologia prima dell'uso, ha a lungo escluso questa filesystem dalla mia pratica. Sono emersi ext4 e xfs, che sono diventati lo standard. Tuttavia, considerando che per il nostro scopo ZFS si adatta perfettamente, e che la versione per Linux, a giudicare dalle recensioni, è diventata un prodotto assolutamente valido (anche se non è completamente supportata, motivo per cui installare un sistema su ZFS da zero richiede varie magie), abbiamo deciso di provarla.
Per ovvi motivi, abbiamo scelto uno stand con configurazione analoga (eccetto il controller RAID). Abbiamo installato otto dischi SSD da 1920 GB. Non avevamo voglia di scrivere la nostra immagine di rete per il caricamento del server su una ZFS 'nuda', quindi abbiamo sottratto 50 GB da ogni disco e creato un MD RAID-10 per il sistema. Gli altri 1950 GB di ciascun disco sono stati uniti in un ZFS-analogo di RAID-10:
zpool create zpool mirror /dev/sda2 /dev/sdb2 mirror /dev/sdc2 /dev/sdd2 mirror /dev/sde2 /dev/sdf2 mirror /dev/sdg2 /dev/sdh2Abbiamo creato partizioni per MySQL:
zfs create zpool/mysql
zfs set compression=gzip zpool/mysql
zfs set recordsize=128k zpool/mysql
zfs set atime=off zpool/mysql
zfs create zpool/mysql/data
zfs set recordsize=16k zpool/mysql/data
zfs set primarycache=metadata zpool/mysql/data
zfs set mountpoint=/var/lib/mysql zpool/mysql/dataNota che abbiamo attivato la compressione dati gzip di default. Abbiamo molte risorse di CPU sul server e non vengono utilizzate completamente. Di conseguenza, 3 TB del nostro database si sono ridotti a 1,6 TB, e poiché il collo di bottiglia, come nel caso precedente, è rappresentato dalla massima performance dei dischi, meno dati abbiamo — meglio è, ottenendo fin dall'inizio un ottimo bonus da ZFS! Durante i picchi di carico, fino a 4 core vengono utilizzati per mantenere in funzione gzip, ma non ce ne preoccupiamo.
Successivamente, l'implementazione è proceduta più rapidamente. Abbiamo replicato le impostazioni della replica MySQL dallo stand LVM. Abbiamo dovuto investire del tempo nella riscrittura degli script tramite comandi ZFS, ma in generale gli algoritmi sono rimasti gli stessi. Ecco un esempio di creazione di uno snapshot:
zfs set snapdir=visible zpool/mysql/data
zfs create zpool/stage_3307
zfs clone zpool/mysql/data@snapmain zpool/stage_3307/data
zfs set mountpoint=/mnt/stage_3307 zpool/stage_3307/dataPer un ulteriore tuning: abbiamo spostato in memoria le partizioni ZFS con i metadati e i log l2arc e zil. Per la nostra esigenza, come si è rivelato poi, questo era sovrabbondante, ma al momento abbiamo mantenuto questa ottimizzazione, cambiarla in futuro non è difficile. Tra gli effetti negativi, occorre ricreare le aree di memoria corrispondenti dopo il riavvio del server. In questo modo, i dati non vengono persi. Estratto dello stato di zpool:
logs
/dev/shm/zil_slog.img ONLINE 0 0 0
cache
/dev/shm/l2arc.img ONLINE 0 0 0In questa configurazione abbiamo iniziato a testare lo stand e abbiamo ottenuto risultati eccellenti: con due istanze di database in funzione contemporaneamente (e una replica principale attiva) sugli snapshot abbiamo ottenuto un carico sui dischi del 50-60%.
Abbiamo risolto il nostro problema principale, come evidenziato nel grafico del ritardo di replica (rispetto al precedente grafico nella sezione Thin LVM):

Oltre a questo, ci siamo notevolmente accelerati in tutte le operazioni: la creazione completa di uno snapshot con l'arresto e il riavvio della replica richiede fino a 40 secondi, mentre il deployment da uno snapshot di una nuova istanza MySQL richiede fino a 20 secondi. Questo soddisfa più che adeguatamente noi e i nostri test di codice.
Risultati intermedi:
- I risultati hanno pienamente soddisfatto la nostra necessità di ottenere una copia del database di produzione per il test del codice.
- La tecnologia richiede un apprendimento: è necessario comprendere che cos'è ZFS e come lavorarci.
- Non abbiamo verificato lo stato attuale di ZFS con un gran numero (da 1 milione) di piccoli file. Tuttavia, supponiamo che il problema persista, quindi non consiglierei questo file system per archiviazioni di file.
E ora?
Non abbiamo bisogno di fare null'altro nel contesto dell'ambiente di test, il risultato ci soddisfa. Potremmo in futuro aggiungere al setup della replica dell'ambiente delle eccezioni di tabelle non necessarie per il test, riducendo ulteriormente il volume del database. Non abbiamo testato il sistema BTRFS e la sua implementazione della tecnologia thin provisioning. Tuttavia, questo non è più prioritario, poiché l'obiettivo principale è stato raggiunto. In generale, naturalmente, vogliamo allontanarci dall'approccio descritto sopra: implementare migrazioni funzionanti del database nell'ambiente di test, creare un contorno di test del database separato e occuparci dello sharding del database principale. Molte di queste cose stiamo già concretizzando, di cui vi parleremo certamente in articoli futuri.
Risultati
L'obiettivo iniziale è stato raggiunto, sebbene in modo inusuale. Nelle conclusioni intermedie sono stati descritti i vantaggi e gli svantaggi di ciascuna delle tecnologie applicate, quindi decidiamo quale tecnologia utilizzare e quando:
- Thin LVM — su database piccoli e quando non si vuole o non si ha tempo di studiare ZFS.
- ZFS — se si ha esperienza con essa o la possibilità di dedicare tempo allo studio in qualsiasi situazione.
A un livello più alto, questo articolo non è solo un confronto tra le tecnologie di due file system. L'idea principale che vorrei trasmettere e consolidare è che non bisogna avere paura di pensare in modo non standard in situazioni critiche per il business e di adottare solo ricette pronte. Una volta avremmo potuto scuotere la testa a gran voce nel dipartimento tecnico e dire che l'obiettivo di creare copie di database da tre terabyte in meno di un minuto era impossibile, e che non avevamo bisogno di tecnologie rischiose, facciamo come si deve. Era possibile, ma avremmo perso circa sei mesi - un anno e molte trasferte di clienti (le trasferte sono il nostro principale indicatore di business) senza test e durante l'implementazione. Adottando un approccio non standard, abbiamo perso poco tempo sull'implementazione, acquisito esperienza in tecnologie nuove e dimenticate, e fornito test proprio nel momento in cui ne avevamo molto bisogno. Questo ha senza dubbio avuto un effetto positivo su tutti i nostri indicatori. La scelta è sempre vostra, mentre noi continueremo a raccontare nel nostro blog delle attuali e future conquiste interessanti.
Fonte: habr.com
