Quando hai le squame Sber. Utilizzo di Ab Initio con Hive e GreenPlum

Qualche tempo fa ci siamo trovati di fronte alla questione della scelta di uno strumento ETL per lavorare con i Big Data. La soluzione Informatica BDM utilizzata in precedenza non era adatta a noi a causa delle funzionalità limitate. Il suo utilizzo è stato ridotto a un framework per il lancio di comandi spark-submit. Sul mercato non c'erano molti analoghi che, in linea di principio, fossero in grado di lavorare con il volume di dati con cui abbiamo a che fare ogni giorno. Alla fine abbiamo scelto Ab Initio. Durante le dimostrazioni pilota, il prodotto ha mostrato una velocità di elaborazione dei dati molto elevata. Non ci sono quasi informazioni su Ab Initio in russo, quindi abbiamo deciso di parlare della nostra esperienza su Habré.

Ab Initio presenta molte trasformazioni classiche e insolite, il cui codice può essere esteso utilizzando il proprio linguaggio PDL. Per una piccola impresa, uno strumento così potente sarà probabilmente eccessivo e la maggior parte delle sue funzionalità potrebbero essere costose e inutilizzate. Ma se la tua scala è vicina a quella di Sberov, allora Ab Initio potrebbe interessarti.

Aiuta un'azienda ad accumulare conoscenze a livello globale e sviluppare un ecosistema, e uno sviluppatore a migliorare le sue competenze in ETL, migliorare le sue conoscenze nella shell, offre l'opportunità di padroneggiare il linguaggio PDL, fornisce un'immagine visiva dei processi di caricamento e semplifica lo sviluppo a causa dell'abbondanza di componenti funzionali.

In questo post parlerò delle capacità di Ab Initio e fornirò le caratteristiche comparative del suo lavoro con Hive e GreenPlum.

  • Descrizione del framework MDW e lavoro sulla sua personalizzazione per GreenPlum
  • Confronto ab initio delle prestazioni tra Hive e GreenPlum
  • Lavorare Ab Initio con GreenPlum in modalità Near Real Time


La funzionalità di questo prodotto è molto ampia e richiede molto tempo per studiarla. Tuttavia, con le competenze lavorative adeguate e le giuste impostazioni delle prestazioni, i risultati dell’elaborazione dei dati sono davvero impressionanti. L'utilizzo di Ab Initio per uno sviluppatore può fornire un'esperienza interessante. Si tratta di una nuova interpretazione dello sviluppo ETL, un ibrido tra un ambiente visivo e lo sviluppo di download in un linguaggio simile a script.

Le aziende stanno sviluppando i propri ecosistemi e questo strumento è più utile che mai. Con Ab Initio, puoi accumulare conoscenze sulla tua attività attuale e utilizzare queste conoscenze per espandere vecchie attività e aprire nuove attività. Le alternative ad Ab Initio includono gli ambienti di sviluppo visivo Informatica BDM e gli ambienti di sviluppo non visivo Apache Spark.

Descrizione di Ab Initio

Ab Initio, come altri strumenti ETL, è una raccolta di prodotti.

Quando hai le squame Sber. Utilizzo di Ab Initio con Hive e GreenPlum

Ab Initio GDE (Graphical Development Environment) è un ambiente per lo sviluppatore in cui configura le trasformazioni dei dati e le collega ai flussi di dati sotto forma di frecce. In questo caso, tale insieme di trasformazioni è chiamato grafico:

Quando hai le squame Sber. Utilizzo di Ab Initio con Hive e GreenPlum

Le connessioni di input e output dei componenti funzionali sono porte e contengono campi calcolati all'interno delle trasformazioni. Diversi grafici collegati da flussi sotto forma di frecce nell'ordine della loro esecuzione sono chiamati piano.

Esistono diverse centinaia di componenti funzionali, il che è molto. Molti di loro sono altamente specializzati. Le capacità delle trasformazioni classiche in Ab Initio sono più ampie rispetto ad altri strumenti ETL. Ad esempio, Join ha più output. Oltre al risultato della connessione dei set di dati, è possibile ottenere record di output dei set di dati di input le cui chiavi non possono essere connesse. È inoltre possibile ottenere scarti, errori e un log dell'operazione di trasformazione, che può essere letto nella stessa colonna di un file di testo ed elaborato con altre trasformazioni:

Quando hai le squame Sber. Utilizzo di Ab Initio con Hive e GreenPlum

Oppure, ad esempio, puoi materializzare un ricevitore dati sotto forma di tabella e leggerne i dati nella stessa colonna.

Ci sono trasformazioni originali. Ad esempio, la trasformazione Scansione presenta funzionalità simili alle funzioni analitiche. Esistono trasformazioni con nomi autoesplicativi: Crea dati, Leggi Excel, Normalizza, Ordina all'interno di gruppi, Esegui programma, Esegui SQL, Unisci con DB, ecc. I grafici possono utilizzare parametri di runtime, inclusa la possibilità di passare parametri da o a il sistema operativo. I file con un set di parametri già pronto passato al grafico sono chiamati set di parametri (pset).

Come previsto, Ab Initio GDE ha un proprio repository chiamato EME (Enterprise Meta Environment). Gli sviluppatori hanno l'opportunità di lavorare con versioni locali del codice e di archiviare i propri sviluppi nel repository centrale.

È possibile, durante l'esecuzione o dopo aver eseguito il grafico, fare clic su qualsiasi flusso che collega la trasformazione e guardare i dati passati tra queste trasformazioni:

Quando hai le squame Sber. Utilizzo di Ab Initio con Hive e GreenPlum

È anche possibile fare clic su qualsiasi flusso e vedere i dettagli di tracciamento: in quanti paralleli ha funzionato la trasformazione, quante linee e byte sono stati caricati in quale dei paralleli:

Quando hai le squame Sber. Utilizzo di Ab Initio con Hive e GreenPlum

È possibile dividere l'esecuzione del grafico in fasi e segnare che alcune trasformazioni devono essere eseguite prima (nella fase zero), le successive nella prima fase, le successive nella seconda fase, ecc.

Per ogni trasformazione è possibile scegliere il cosiddetto layout (dove verrà eseguito): senza paralleli o in thread paralleli, di cui è possibile specificare il numero. Allo stesso tempo, i file temporanei creati da Ab Initio durante l'esecuzione delle trasformazioni possono essere posizionati sia nel file system del server che in HDFS.

In ogni trasformazione, in base al modello predefinito, puoi creare il tuo script in PDL, che è un po' come una shell.

Con PDL è possibile estendere le funzionalità delle trasformazioni e, in particolare, generare dinamicamente (in fase di runtime) frammenti di codice arbitrari in base ai parametri di runtime.

Ab Initio ha anche un'integrazione ben sviluppata con il sistema operativo tramite shell. Nello specifico, Sberbank utilizza Linux KSH. Puoi scambiare variabili con la shell e usarle come parametri del grafico. È possibile richiamare l'esecuzione dei grafici Ab Initio dalla shell e amministrare Ab Initio.

Oltre ad Ab Initio GDE, nella consegna sono inclusi molti altri prodotti. Esiste un proprio Co>Operation System che pretende di essere chiamato sistema operativo. È presente un Controllo>Centro in cui è possibile pianificare e monitorare i flussi di download. Esistono prodotti per eseguire lo sviluppo a un livello più primitivo di quello consentito da Ab Initio GDE.

Descrizione del framework MDW e lavoro sulla sua personalizzazione per GreenPlum

Insieme ai suoi prodotti, il fornitore fornisce il prodotto MDW (Metadata Driven Warehouse), che è un configuratore di grafici progettato per aiutare con le attività tipiche di popolamento di data warehouse o depositi di dati.

Contiene parser di metadati personalizzati (specifici del progetto) e generatori di codice già pronti.

Quando hai le squame Sber. Utilizzo di Ab Initio con Hive e GreenPlum
Come input, MDW riceve un modello di dati, un file di configurazione per impostare una connessione a un database (Oracle, Teradata o Hive) e alcune altre impostazioni. La parte specifica del progetto, ad esempio, distribuisce il modello in un database. La parte pronta all'uso del prodotto genera grafici e file di configurazione caricando i dati nelle tabelle del modello. In questo caso, vengono creati grafici (e pset) per diverse modalità di inizializzazione e lavoro incrementale sull'aggiornamento delle entità.

Nei casi di Hive e RDBMS, vengono generati grafici diversi per l'inizializzazione e gli aggiornamenti incrementali dei dati.

Nel caso di Hive, i dati delta in entrata sono collegati tramite Ab Initio Join con i dati presenti nella tabella prima dell'aggiornamento. I caricatori di dati in MDW (sia in Hive che in RDBMS) non solo inseriscono nuovi dati dal delta, ma chiudono anche i periodi di rilevanza dei dati le cui chiavi primarie hanno ricevuto il delta. Inoltre, è necessario riscrivere la parte invariata dei dati. Ma questo deve essere fatto perché Hive non dispone di operazioni di eliminazione o aggiornamento.

Quando hai le squame Sber. Utilizzo di Ab Initio con Hive e GreenPlum

Nel caso dell'RDBMS, i grafici per l'aggiornamento incrementale dei dati sembrano più ottimali, poiché gli RDBMS hanno reali capacità di aggiornamento.

Quando hai le squame Sber. Utilizzo di Ab Initio con Hive e GreenPlum

Il delta ricevuto viene caricato in una tabella intermedia nel database. Successivamente, il delta viene collegato ai dati presenti nella tabella prima dell'aggiornamento. E questo viene fatto utilizzando SQL utilizzando una query SQL generata. Successivamente, utilizzando i comandi SQL delete+insert, i nuovi dati del delta vengono inseriti nella tabella di destinazione e i periodi di rilevanza dei dati le cui chiavi primarie hanno ricevuto il delta vengono chiusi.
Non è necessario riscrivere i dati invariati.

Quindi siamo arrivati ​​alla conclusione che nel caso di Hive, MDW deve andare a riscrivere l'intera tabella perché Hive non ha una funzione di aggiornamento. E niente di meglio che riscrivere completamente i dati una volta inventato l'aggiornamento. Nel caso di RDBMS, al contrario, gli ideatori del prodotto hanno ritenuto necessario affidare il collegamento e l'aggiornamento delle tabelle all'utilizzo di SQL.

Per un progetto presso Sberbank, abbiamo creato una nuova implementazione riutilizzabile di un caricatore di database per GreenPlum. Questa operazione è stata eseguita in base alla versione generata da MDW per Teradata. È stato Teradata, e non Oracle, ad avvicinarsi di più e a farlo meglio, perché... è anche un sistema MPP. I metodi di lavoro, così come la sintassi, di Teradata e GreenPlum si sono rivelati simili.

Di seguito sono riportati esempi di differenze critiche per l'MDW tra diversi RDBMS. In GreenPlum, a differenza di Teradata, quando si creano tabelle è necessario scrivere una clausola

distributed by

Teradata scrive:

delete <table> all

, e in GreenPlum scrivono

delete from <table>

In Oracle, per scopi di ottimizzazione scrivono

delete from t where rowid in (<соединение t с дельтой>)

, e scrivono Teradata e GreenPlum

delete from t where exists (select * from delta where delta.pk=t.pk)

Notiamo inoltre che affinché Ab Initio funzioni con GreenPlum, era necessario installare il client GreenPlum su tutti i nodi del cluster Ab Initio. Questo perché ci siamo collegati a GreenPlum simultaneamente da tutti i nodi del nostro cluster. E affinché la lettura da GreenPlum fosse parallela e ciascun thread Ab Initio parallelo potesse leggere la propria porzione di dati da GreenPlum, abbiamo dovuto inserire una costruzione compresa da Ab Initio nella sezione "dove" delle query SQL

where ABLOCAL()

e determinare il valore di questa costruzione specificando il parametro letto dal database di trasformazione

ablocal_expr=«string_concat("mod(t.", string_filter_out("{$TABLE_KEY}","{}"), ",", (decimal(3))(number_of_partitions()),")=", (decimal(3))(this_partition()))»

, che viene compilato in qualcosa di simile

mod(sk,10)=3

, cioè. devi richiedere a GreenPlum un filtro esplicito per ogni partizione. Per altri database (Teradata, Oracle), Ab Initio può eseguire automaticamente questa parallelizzazione.

Confronto ab initio delle prestazioni tra Hive e GreenPlum

Sberbank ha condotto un esperimento per confrontare le prestazioni dei grafici generati da MDW in relazione a Hive e in relazione a GreenPlum. Nell'ambito dell'esperimento, nel caso di Hive c'erano 5 nodi sullo stesso cluster di Ab Initio e nel caso di GreenPlum c'erano 4 nodi su un cluster separato. Quelli. Hive aveva qualche vantaggio hardware rispetto a GreenPlum.

Abbiamo considerato due coppie di grafici che eseguono lo stesso compito di aggiornare i dati in Hive e GreenPlum. Contestualmente sono stati lanciati i grafici generati dal configuratore MDW:

  • carico iniziale + carico incrementale di dati generati casualmente in una tabella Hive
  • carico iniziale + carico incrementale di dati generati casualmente nella stessa tabella GreenPlum

In entrambi i casi (Hive e GreenPlum) hanno eseguito caricamenti su 10 thread paralleli sullo stesso cluster Ab Initio. Ab Initio ha salvato i dati intermedi per i calcoli in HDFS (in termini di Ab Initio, è stato utilizzato il layout MFS utilizzando HDFS). Una riga di dati generati casualmente occupava 200 byte in entrambi i casi.

Il risultato è stato così:

Alveare:

Caricamento iniziale in Hive

Righe inserite
6 000 000
60 000 000
600 000 000

Durata dell'inizializzazione
download in pochi secondi
41
203
1/601

Caricamento incrementale in Hive

Numero di righe disponibili in
tabella di destinazione all'inizio dell'esperimento
6 000 000
60 000 000
600 000 000

Numero di linee delta applicate
tabella di destinazione durante l'esperimento
6 000 000
6 000 000
6 000 000

Durata dell'incremento
download in pochi secondi
88
299
2/541

VerdePrugna:

Caricamento iniziale in GreenPlum

Righe inserite
6 000 000
60 000 000
600 000 000

Durata dell'inizializzazione
download in pochi secondi
72
360
3/631

Caricamento incrementale in GreenPlum

Numero di righe disponibili in
tabella di destinazione all'inizio dell'esperimento
6 000 000
60 000 000
600 000 000

Numero di linee delta applicate
tabella di destinazione durante l'esperimento
6 000 000
6 000 000
6 000 000

Durata dell'incremento
download in pochi secondi
159
199
321

Vediamo che la velocità del caricamento iniziale sia in Hive che in GreenPlum dipende linearmente dalla quantità di dati e, per ragioni di hardware migliore, è leggermente più veloce per Hive che per GreenPlum.

Anche il caricamento incrementale in Hive dipende linearmente dal volume dei dati caricati in precedenza disponibili nella tabella di destinazione e procede piuttosto lentamente man mano che il volume cresce. Ciò è causato dalla necessità di riscrivere completamente la tabella di destinazione. Ciò significa che applicare piccole modifiche a tabelle di grandi dimensioni non è un buon caso d'uso per Hive.

Il caricamento incrementale in GreenPlum dipende debolmente dal volume dei dati precedentemente caricati disponibili nella tabella di destinazione e procede abbastanza rapidamente. Ciò è avvenuto grazie agli SQL Join e all'architettura GreenPlum, che permette l'operazione di cancellazione.

Pertanto, GreenPlum aggiunge il delta utilizzando il metodo delete+insert, ma Hive non dispone di operazioni di eliminazione o aggiornamento, quindi l'intero array di dati è stato costretto a essere riscritto interamente durante un aggiornamento incrementale. Il confronto tra le celle evidenziate in grassetto è molto rivelatore, poiché corrisponde all'opzione più comune per l'utilizzo di download ad alta intensità di risorse. Vediamo che GreenPlum ha battuto Hive in questo test di 8 volte.

Lavorare Ab Initio con GreenPlum in modalità Near Real Time

In questo esperimento, testeremo la capacità di Ab Initio di aggiornare la tabella GreenPlum con blocchi di dati generati casualmente quasi in tempo reale. Consideriamo la tabella GreenPlum dev42_1_db_usl.TESTING_SUBJ_org_finval, con la quale lavoreremo.

Utilizzeremo tre grafici Ab Initio per lavorarci:

1) Graph Create_test_data.mp: crea file di dati in HDFS con 10 di righe in 6 thread paralleli. Il dato è casuale, la sua struttura è organizzata per l'inserimento nella nostra tabella

Quando hai le squame Sber. Utilizzo di Ab Initio con Hive e GreenPlum

Quando hai le squame Sber. Utilizzo di Ab Initio con Hive e GreenPlum

2) Grafico mdw_load.day_one.current.dev42_1_db_usl_testing_subj_org_finval.pset – Grafico generato da MDW inizializzando l'inserimento dei dati nella nostra tabella in 10 thread paralleli (vengono utilizzati i dati di test generati dal grafico (1))

Quando hai le squame Sber. Utilizzo di Ab Initio con Hive e GreenPlum

3) Grafico mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset – un grafico generato da MDW per l'aggiornamento incrementale della nostra tabella in 10 thread paralleli utilizzando una porzione di dati appena ricevuti (delta) generati dal grafico (1)

Quando hai le squame Sber. Utilizzo di Ab Initio con Hive e GreenPlum

Eseguiamo lo script seguente in modalità NRT:

  • generare 6 di linee di test
  • eseguire un caricamento iniziale inserire 6 di righe di test in una tabella vuota
  • ripetere il download incrementale 5 volte
    • generare 6 di linee di test
    • eseguire un inserimento incrementale di 6 di righe di test nella tabella (in questo caso, la scadenza valid_to_ts è impostata sui vecchi dati e vengono inseriti dati più recenti con la stessa chiave primaria)

Questo scenario emula la modalità di funzionamento reale di un determinato sistema aziendale: una porzione abbastanza ampia di nuovi dati appare in tempo reale e viene immediatamente riversata in GreenPlum.

Ora diamo un'occhiata al registro dello script:

Inizia Create_test_data.input.pset alle 2020-06-04 11:49:11
Termina Create_test_data.input.pset alle 2020-06-04 11:49:37
Avvia mdw_load.day_one.current.dev42_1_db_usl_testing_subj_org_finval.pset alle 2020-06-04 11:49:37
Termina mdw_load.day_one.current.dev42_1_db_usl_testing_subj_org_finval.pset alle 2020-06-04 11:50:42
Inizia Create_test_data.input.pset alle 2020-06-04 11:50:42
Termina Create_test_data.input.pset alle 2020-06-04 11:51:06
Avvia mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset alle 2020-06-04 11:51:06
Termina mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset alle 2020-06-04 11:53:41
Inizia Create_test_data.input.pset alle 2020-06-04 11:53:41
Termina Create_test_data.input.pset alle 2020-06-04 11:54:04
Avvia mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset alle 2020-06-04 11:54:04
Termina mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset alle 2020-06-04 11:56:51
Inizia Create_test_data.input.pset alle 2020-06-04 11:56:51
Termina Create_test_data.input.pset alle 2020-06-04 11:57:14
Avvia mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset alle 2020-06-04 11:57:14
Termina mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset alle 2020-06-04 11:59:55
Inizia Create_test_data.input.pset alle 2020-06-04 11:59:55
Termina Create_test_data.input.pset alle 2020-06-04 12:00:23
Avvia mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset alle 2020-06-04 12:00:23
Termina mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset alle 2020-06-04 12:03:23
Inizia Create_test_data.input.pset alle 2020-06-04 12:03:23
Termina Create_test_data.input.pset alle 2020-06-04 12:03:49
Avvia mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset alle 2020-06-04 12:03:49
Termina mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset alle 2020-06-04 12:06:46

Si scopre questa immagine:

Grafico
Ora di inizio
Tempo finale
Lunghezza

Crea_dati_di_prova.input.pset
04.06.2020 11: 49: 11
04.06.2020 11: 49: 37
00:00:26

mdw_load.day_one.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 11: 49: 37
04.06.2020 11: 50: 42
00:01:05

Crea_dati_di_prova.input.pset
04.06.2020 11: 50: 42
04.06.2020 11: 51: 06
00:00:24

mdw_load.regular.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 11: 51: 06
04.06.2020 11: 53: 41
00:02:35

Crea_dati_di_prova.input.pset
04.06.2020 11: 53: 41
04.06.2020 11: 54: 04
00:00:23

mdw_load.regular.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 11: 54: 04
04.06.2020 11: 56: 51
00:02:47

Crea_dati_di_prova.input.pset
04.06.2020 11: 56: 51
04.06.2020 11: 57: 14
00:00:23

mdw_load.regular.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 11: 57: 14
04.06.2020 11: 59: 55
00:02:41

Crea_dati_di_prova.input.pset
04.06.2020 11: 59: 55
04.06.2020 12: 00: 23
00:00:28

mdw_load.regular.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 12: 00: 23
04.06.2020 12: 03: 23
00:03:00

Crea_dati_di_prova.input.pset
04.06.2020 12: 03: 23
04.06.2020 12: 03: 49
00:00:26

mdw_load.regular.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 12: 03: 49
04.06.2020 12: 06: 46
00:02:57

Vediamo che 6 di righe di incremento vengono elaborate in 000 minuti, il che è abbastanza veloce.
I dati nella tabella di destinazione risultano essere distribuiti come segue:

select valid_from_ts, valid_to_ts, count(1), min(sk), max(sk) from dev42_1_db_usl.TESTING_SUBJ_org_finval group by valid_from_ts, valid_to_ts order by 1,2;

Quando hai le squame Sber. Utilizzo di Ab Initio con Hive e GreenPlum
Puoi vedere la corrispondenza dei dati inseriti agli orari in cui sono stati lanciati i grafici.
Ciò significa che è possibile eseguire il caricamento incrementale dei dati in GreenPlum in Ab Initio con una frequenza molto elevata e osservare un'elevata velocità di inserimento di questi dati in GreenPlum. Naturalmente, non sarà possibile avviarlo una volta al secondo, poiché Ab Initio, come qualsiasi strumento ETL, richiede tempo per “avviarsi” una volta avviato.

conclusione

Ab Initio è attualmente utilizzato presso Sberbank per costruire un Unified Semantic Data Layer (ESS). Questo progetto prevede la costruzione di una versione unificata dello stato di varie entità bancarie. Le informazioni provengono da diverse fonti, le cui repliche vengono preparate su Hadoop. In base alle esigenze aziendali, viene preparato un modello di dati e vengono descritte le trasformazioni dei dati. Ab Initio carica le informazioni nell'ESN e i dati scaricati non sono solo di interesse per l'azienda in sé, ma servono anche come fonte per la creazione di data mart. Allo stesso tempo, la funzionalità del prodotto consente di utilizzare diversi sistemi come ricevitore (Hive, Greenplum, Teradata, Oracle), il che rende possibile preparare facilmente i dati per un'azienda nei vari formati richiesti.

Le capacità di Ab Initio sono ampie; ad esempio, il framework MDW incluso consente di creare dati storici tecnici e aziendali fuori dagli schemi. Per gli sviluppatori, Ab Initio consente non di reinventare la ruota, ma di utilizzare molti componenti funzionali esistenti, che sono essenzialmente librerie necessarie quando si lavora con i dati.

L'autore è un esperto nella comunità professionale di Sberbank SberProfi DWH/BigData. La comunità professionale SberProfi DWH/BigData è responsabile dello sviluppo di competenze in aree come l'ecosistema Hadoop, Teradata, Oracle DB, GreenPlum, nonché gli strumenti BI Qlik, SAP BO, Tableau, ecc.

Fonte: habr.com

Aggiungi un commento