Google Cloud Spanner: buono, cattivo, brutto

Salve, Khabroviti. Tradizionalmente, continuiamo a condividere materiale interessante alla vigilia dell'inizio di nuovi corsi. Oggi, appositamente per te, abbiamo tradotto un articolo su Google Cloud Spanner, programmato per coincidere con il lancio del corso "AWS per sviluppatori".

Google Cloud Spanner: buono, cattivo, brutto

Originariamente pubblicato in Blog del quartier generale della velocità della luce.

In qualità di azienda che offre una varietà di soluzioni POS basate su cloud per rivenditori, ristoratori e commercianti online in tutto il mondo, Lightspeed utilizza diversi tipi di piattaforme di database per una varietà di casi d'uso transazionali, analitici e di ricerca. Ognuna di queste piattaforme di database ha i propri punti di forza e di debolezza, quindi, quando Google ha introdotto sul mercato Cloud Spanner, caratteristiche promettenti mai viste prima nel mondo dei database relazionali, come la scalabilità orizzontale praticamente illimitata e un accordo sul livello di servizio (SLA) del 99,999%. , Non potevamo lasciarci sfuggire l'occasione di averla tra le nostre mani!

Per fornire una panoramica completa della nostra esperienza con Cloud Spanner, nonché dei criteri di valutazione che abbiamo utilizzato, tratteremo i seguenti argomenti:

  1. I nostri criteri di valutazione
  2. Cloud Spanner in poche parole
  3. La nostra valutazione
  4. I nostri risultati

Google Cloud Spanner: buono, cattivo, brutto

1. I nostri criteri di valutazione

Prima di approfondire le specifiche di Cloud Spanner, le sue somiglianze e differenze con altre soluzioni sul mercato, parliamo innanzitutto dei principali casi d'uso che avevamo in mente quando abbiamo considerato dove implementare Cloud Spanner nella nostra infrastruttura:

  • In sostituzione della (prevalente) tradizionale soluzione di database SQL
  • Come soluzione OLTP abilitata per OLAP

Nota: Per facilitare il confronto, questo articolo confronta Cloud Spanner con le varianti MySQL delle famiglie di soluzioni GCP Cloud SQL e Amazon AWS RDS.

Utilizzo di Cloud Spanner in sostituzione di una soluzione di database SQL tradizionale

Nell'ambiente tradizionale database, quando il tempo di risposta per una query del database si avvicina o addirittura supera soglie applicative predefinite (principalmente a causa di un aumento del numero di utenti e/o richieste), esistono diversi modi per ridurre il tempo di risposta a livelli accettabili. Tuttavia, la maggior parte di queste soluzioni comporta un intervento manuale.

Ad esempio, il primo passaggio da eseguire consiste nell'esaminare le varie impostazioni del database relative alle prestazioni e ottimizzarle in modo che corrispondano al meglio ai modelli dello scenario di utilizzo dell'applicazione. Se questo non è sufficiente, puoi scegliere di ridimensionare il database verticalmente o orizzontalmente.

Il ridimensionamento di un'applicazione comporta l'aggiornamento dell'istanza del server, in genere aggiungendo più processori/core, più RAM, storage più veloce, ecc. L'aggiunta di più risorse hardware comporta un aumento delle prestazioni del database, misurate principalmente in transazioni al secondo, e latenza delle transazioni per i sistemi OLTP. I sistemi di database relazionali (che utilizzano un approccio multi-thread) come MySQL scalano bene verticalmente.

Ci sono diversi inconvenienti in questo approccio, ma il più ovvio è la dimensione massima del server sul mercato. Una volta raggiunto il limite massimo di istanze del server, rimane solo un percorso: ridimensionare.

La scalabilità orizzontale è un approccio che aggiunge più server a un cluster per aumentare idealmente le prestazioni in modo lineare man mano che vengono aggiunti più server. Maggioranza tradizionale i sistemi di database non si adattano bene o non si adattano affatto. Ad esempio, MySQL può ridimensionare per le letture aggiungendo lettori slave, ma non può ridimensionare per le scritture.

D'altra parte, per sua natura, Cloud Spanner può facilmente scalare orizzontalmente con un intervento minimo.

Funzionalità complete DBMS come servizio devono essere valutati da diverse prospettive. Come base, abbiamo preso il DBMS più popolare nel cloud: per Google, GCP Cloud SQL e per Amazon, AWS RDS. Nella nostra valutazione, ci siamo concentrati sulle seguenti categorie:

  • Mappatura delle caratteristiche: estensione SQL, DDL, DML; librerie/connettori di connessione, supporto delle transazioni e così via.
  • Supporto allo sviluppo: facilità di sviluppo e test.
  • Supporto amministrativo: gestione delle istanze come aumento/riduzione delle istanze e aggiornamento delle istanze; SLA, backup e ripristino; sicurezza/controllo accessi.

Utilizzo di Cloud Spanner come soluzione OLTP abilitata per OLAP

Sebbene Google non dichiari esplicitamente che Cloud Spanner è per l'analisi, condivide alcuni attributi con altri motori come Apache Impala & Kudu e YugaByte progettati per i carichi di lavoro OLAP.

Anche se ci fosse solo una piccola possibilità che Cloud Spanner includesse un motore HTAP (Hybrid Transactional/Analytic Processing) a scalabilità orizzontale coerente con un set di funzionalità OLAP (più o meno) utilizzabile, riteniamo che meriterebbe la nostra attenzione.

Con questo in mente, abbiamo esaminato le seguenti categorie:

  • Caricamento dei dati, indici e supporto per il partizionamento
  • Prestazioni delle query e DML

2. Cloud Spanner in poche parole

Google Spanner è un sistema di gestione di database relazionali in cluster (RDBMS) che Google utilizza per molti dei propri servizi. Google lo ha reso pubblicamente disponibile agli utenti di Google Cloud Platform all'inizio del 2017.

Ecco alcuni degli attributi di Cloud Spanner:

  • Cluster RDBMS altamente coerente e scalabile: utilizza la sincronizzazione dell'ora hardware per garantire la coerenza dei dati.
  • Supporto per transazioni tra tabelle: le transazioni possono estendersi su più tabelle, non necessariamente limitate a una singola tabella (a differenza di Apache HBase o Apache Kudu).
  • Tabelle basate su chiave primaria: tutte le tabelle devono avere una chiave primaria (PC) dichiarata, che può essere costituita da più colonne della tabella. I dati tabulari vengono archiviati nell'ordine del PC, il che lo rende molto efficiente e veloce per le ricerche su PC. Come con altri sistemi basati su PC, l'implementazione deve essere modellata in base a casi d'uso precostituiti per ottenere risultati la prestazione migliore.
  • Tabelle a strisce: le tabelle possono avere dipendenze fisiche l'una dall'altra. Le righe della tabella figlio possono essere abbinate alle righe della tabella padre. Questo approccio accelera la ricerca di relazioni che possono essere determinate nella fase di modellazione dei dati, ad esempio, quando si mettono insieme i clienti e le loro fatture.
  • Indici: Cloud Spanner supporta gli indici secondari. Un indice è costituito da colonne indicizzate e da tutte le colonne PC. Facoltativamente, l'indice può contenere anche altre colonne non indicizzate. L'indice può essere intercalato con la tabella padre per velocizzare le query. Diverse restrizioni si applicano agli indici, ad esempio il numero massimo di colonne aggiuntive che possono essere archiviate in un indice. Inoltre, le query tramite indici potrebbero non essere così semplici come in altri RDBMS.

“Cloud Spanner seleziona automaticamente un indice solo in rari casi. In particolare, Cloud Spanner non seleziona automaticamente un indice secondario se la query richiede colonne non archiviate indice '.

  • Accordo sul livello di servizio (SLA): distribuzione in un'unica regione con SLA del 99,99%; implementazioni in più regioni con SLA del 99,999%. Sebbene lo stesso SLA sia solo un accordo e non una garanzia di alcun tipo, credo che le persone di Google abbiano alcuni dati concreti per fare un'affermazione così forte. (Per riferimento, 99,999% significa 26,3 secondi di inattività del servizio al mese.)
  • Altro: https://cloud.google.com/spanner/

Nota: Il progetto Apache Tephra aggiunge il supporto avanzato delle transazioni ad Apache HBase (ora implementato anche in Apache Phoenix come versione beta).

3. La nostra valutazione

Quindi, abbiamo tutti letto le dichiarazioni di Google sui vantaggi di Cloud Spanner: scalabilità orizzontale praticamente illimitata pur mantenendo un'elevata coerenza e uno SLA molto elevato. Sebbene queste affermazioni siano, in ogni caso, estremamente difficili da realizzare, il nostro obiettivo non era quello di confutarle. Invece, concentriamoci su altre cose che interessano alla maggior parte degli utenti di database: parità e usabilità.

Abbiamo valutato Cloud Spanner come sostituto di Sharded MySQL

Google Cloud SQL e Amazon AWS RDS, due dei database OLTP più popolari nel mercato del cloud, hanno un set di funzionalità molto ampio. Tuttavia, per ridimensionare questi database oltre le dimensioni di un singolo nodo, è necessario eseguire la suddivisione dell'applicazione. Questo approccio crea ulteriore complessità sia per le applicazioni che per l'amministrazione. Abbiamo esaminato come Spanner si inserisce nello scenario di combinazione di più shard in un'unica istanza e quali funzionalità (se presenti) potrebbero dover essere sacrificate.

Supporto per SQL, DML e DDL, oltre al connettore e alle librerie?

Innanzitutto, quando si inizia con qualsiasi database, è necessario creare un modello di dati. Se pensi di poter connettere JDBC Spanner al tuo strumento SQL preferito, scoprirai che puoi interrogare i tuoi dati con esso, ma non puoi usarlo per creare una tabella o aggiornare (DDL) o qualsiasi inserimento/aggiornamento/cancellazione operazioni (DML). Anche il JDBC ufficiale di Google non lo supporta.

"I driver attualmente non supportano le istruzioni DML o DDL."
Documentazione della chiave inglese

La situazione non è migliore con la console GCP: puoi inviare solo query SELECT. Fortunatamente esiste un driver JDBC con supporto DML e DDL da parte della community, comprese le transazioni github.com/olavloite/spanner-jdbc. Sebbene questo driver sia estremamente utile, l'assenza del driver JDBC di Google è sorprendente. Fortunatamente, Google offre un supporto di libreria client abbastanza ampio (basato su gRPC): C#, Go, Java, node.js, PHP, Python e Ruby.

L'uso quasi obbligatorio delle API personalizzate di Cloud Spanner (a causa della mancanza di DDL e DML in JDBC) comporta alcune limitazioni per le aree di codice correlate come pool di connessioni o framework di binding di database (come Spring MVC). Generalmente, quando si utilizza JDBC, si è liberi di scegliere il pool di connessioni preferito (ad es. HikariCP, DBCP, C3PO, ecc.) che è testato e funziona bene. Nel caso di API Spanner personalizzate, dobbiamo fare affidamento su framework/binding/pool di sessione che abbiamo creato noi stessi.

Il design orientato alla chiave primaria (PC) consente a Cloud Spanner di essere molto veloce durante l'accesso ai dati tramite il PC, ma introduce anche alcuni problemi di query.

  • Non è possibile aggiornare il valore di una chiave primaria; È necessario prima eliminare la voce PC originale e reinserirla con il nuovo valore. (Questo è simile ad altri motori di database/archiviazione orientati al PC.)
  • Qualsiasi istruzione UPDATE e DELETE deve specificare il PC in WHERE, quindi non possono esserci istruzioni DELETE all vuote - deve sempre esserci una sottoquery, ad esempio: UPDATE xxx WHERE id IN (SELECT id FROM table1)
  • Mancanza di un'opzione di incremento automatico o qualcosa di simile che imposti la sequenza per il campo PC. Affinché ciò funzioni, è necessario creare il valore corrispondente sul lato dell'applicazione.

Indici secondari?

Google Cloud Spanner ha il supporto integrato per gli indici secondari. Questa è una caratteristica molto interessante che non è sempre presente in altre tecnologie. Apache Kudu attualmente non supporta affatto gli indici secondari e Apache HBase non supporta direttamente gli indici, ma può aggiungerli tramite Apache Phoenix.

Gli indici in Kudu e HBase possono essere modellati come una tabella separata con diversa composizione di chiavi primarie, ma l'atomicità delle operazioni eseguite sulla tabella padre e sulle relative tabelle degli indici deve essere eseguita a livello di applicazione e non è banale da implementare correttamente.

Come accennato nella recensione di Cloud Spanner, i suoi indici possono differire dagli indici MySQL. Pertanto, è necessario prestare particolare attenzione nella creazione di query e nella creazione di profili per garantire che venga utilizzato l'indice corretto dove è necessario.

Rappresentazione?

Un oggetto molto popolare e utile in un database sono le visualizzazioni. Possono essere utili per un gran numero di casi d'uso; i miei due preferiti sono il livello di astrazione logica e il livello di sicurezza. Purtroppo Cloud Spanner NON supporta le visualizzazioni. Tuttavia, questo ci limita solo parzialmente, poiché non esiste una granularità a livello di colonna per le autorizzazioni di accesso in cui le visualizzazioni possono essere una soluzione accettabile.

Consulta la documentazione di Cloud Spanner per una sezione che descrive in dettaglio quote e limiti (chiave/quote), ce n'è uno in particolare che può essere problematico per alcune applicazioni: Cloud Spanner ha un massimo di 100 database per istanza. Ovviamente, questo può rappresentare un grosso ostacolo per un database progettato per scalare fino a oltre 100 database. Fortunatamente, dopo aver parlato con il nostro rappresentante tecnico di Google, abbiamo scoperto che questo limite può essere aumentato a quasi qualsiasi valore tramite l'assistenza di Google.

Supporto allo sviluppo?

Cloud Spanner offre un supporto del linguaggio di programmazione abbastanza decente per lavorare con la sua API. Le librerie ufficialmente supportate sono nell'area C#, Go, Java, node.js, PHP, Python e Ruby. La documentazione è abbastanza dettagliata, ma come con altre tecnologie all'avanguardia, la comunità è piuttosto piccola rispetto alle tecnologie di database più popolari, il che può comportare più tempo dedicato a casi d'uso o problemi meno comuni.

E per quanto riguarda il sostegno allo sviluppo locale?

Non abbiamo trovato un modo per creare un'istanza Cloud Spanner on-premise. Il più vicino che abbiamo ottenuto è un'immagine Docker scarafaggioDBche è simile in linea di principio, ma molto diverso in pratica. Ad esempio CockroachDB può utilizzare PostgreSQL JDBC. Poiché l'ambiente di sviluppo dovrebbe essere il più vicino possibile all'ambiente di produzione, Cloud Spanner non è l'ideale perché devi fare affidamento su un'istanza completa di Spanner. Per risparmiare sui costi, puoi selezionare una singola istanza di regione.

Supporto amministrativo?

La creazione di un'istanza di Cloud Spanner è molto semplice. Devi solo scegliere tra la creazione di un'istanza multi-regione o singola, specificare la regione o le regioni e il numero di nodi. In meno di un minuto, l'istanza sarà attiva e funzionante.

Diverse metriche elementari sono direttamente disponibili nella pagina Spanner in Google Console. Visualizzazioni più dettagliate sono disponibili tramite Stackdriver, dove puoi anche impostare soglie metriche e criteri di avviso.

Accesso alle risorse?

MySQL offre impostazioni di autorizzazione/ruolo utente estese e molto granulari. Puoi personalizzare facilmente l'accesso a una tabella specifica o anche solo a un sottoinsieme delle sue colonne. Cloud Spanner utilizza lo strumento Google Identity & Access Management (IAM), che ti consente di impostare criteri e autorizzazioni solo a un livello molto elevato. L'opzione più granulare è l'autorizzazione a livello di database, che non si adatta alla maggior parte dei casi di produzione. Questa restrizione obbliga ad aggiungere ulteriori misure di sicurezza al codice, all'infrastruttura o a entrambi per impedire l'uso non autorizzato delle risorse di Spanner.

Backup?

Per dirla semplicemente, non ci sono backup in Cloud Spanner. Mentre gli elevati requisiti SLA di Google possono garantire che non si perdano dati a causa di errori hardware o del database, errori umani, difetti dell'applicazione e così via. Conosciamo tutti la regola: l'elevata disponibilità non può sostituire una strategia di backup intelligente. Attualmente, l'unico modo per eseguire il backup dei dati consiste nel trasmetterli in modo programmatico dal database a un ambiente di archiviazione separato.

Prestazioni della query?

Abbiamo utilizzato Yahoo! per caricare i dati e testare le richieste. Benchmark di servizi cloud. La tabella seguente mostra il carico di lavoro B YCSB con un rapporto di lettura del 95% rispetto al 5% di scrittura.

Google Cloud Spanner: buono, cattivo, brutto

* Il test di carico è stato eseguito su n1-standard-32 Compute Engine (CE) (32 vCPU, 120 GB di memoria) e l'istanza di test non è mai stata il collo di bottiglia nei test.
** Il numero massimo di thread in un'istanza YCSB è 400. In totale, è stato necessario eseguire sei istanze parallele di test YCSB per ottenere un totale di 2400 thread.

Osservando i risultati del benchmark, in particolare la combinazione di carico della CPU e TPS, possiamo vedere chiaramente che Cloud Spanner scala abbastanza bene. Il carico elevato creato da un numero elevato di thread è compensato da un numero elevato di nodi nel cluster Cloud Spanner. Sebbene la latenza sembri piuttosto elevata, specialmente quando si esegue a 2400 thread, potrebbe essere necessario ripetere il test con 6 istanze più piccole del motore di calcolo per ottenere numeri più accurati. Ogni istanza eseguirà un test YCSB anziché un'istanza CE di grandi dimensioni con 6 test paralleli. In questo modo è più semplice distinguere tra i ritardi delle richieste di Cloud Spanner e i ritardi aggiunti dalla connessione di rete tra Cloud Spanner e l'istanza CE che esegue il test.

Come si comporta Cloud Spanner come OLAP?

Partizionamento?

Dividere i dati in segmenti fisicamente e/o logicamente indipendenti, chiamati partizioni, è un concetto molto popolare che si trova nella maggior parte dei motori OLAP. Le partizioni possono migliorare notevolmente le prestazioni delle query e la manutenibilità del database. Ulteriori approfondimenti sul partizionamento sarebbero articoli separati, quindi menzioniamo solo l'importanza di avere uno schema di partizionamento e un sottopartizionamento. La possibilità di suddividere i dati in partizioni e ancora di più in sottopartizioni è fondamentale per le prestazioni delle query analitiche.

Cloud Spanner non supporta le partizioni di per sé. Separa i dati internamente nei cosiddetti dividere-s basato su intervalli di chiavi primarie. Il partizionamento viene eseguito automaticamente per bilanciare il carico sul cluster Cloud Spanner. Una funzionalità molto utile di Cloud Spanner è la suddivisione del carico di base di una tabella padre (una tabella che non è intercalata con un'altra). Spanner rileva automaticamente se contiene dividere dati che vengono letti più frequentemente rispetto ai dati in altri dividere-ah, e può decidere un'ulteriore separazione. Pertanto, più nodi possono essere coinvolti in una richiesta, il che aumenta anche efficacemente il throughput.

Caricamento dati?

Il metodo di Cloud Spanner per i dati in blocco è lo stesso di un normale caricamento. Per ottenere le massime prestazioni, è necessario seguire alcune linee guida, tra cui:

  • Ordina i tuoi dati per chiave primaria.
  • Dividili per 10*numero di nodi singole sezioni.
  • Crea una serie di attività di lavoro che caricano i dati in parallelo.

Questo caricamento di dati utilizza tutti i nodi Cloud Spanner.

Abbiamo utilizzato il carico di lavoro A YCSB per generare un set di dati di 10 milioni di righe.

Google Cloud Spanner: buono, cattivo, brutto

* Il test di carico è stato eseguito sul motore di calcolo n1-standard-32 (32 vCPU, 120 GB di memoria) e l'istanza di test non è mai stata il collo di bottiglia nei test.
** Una configurazione a 1 nodo non è consigliata per nessun carico di lavoro di produzione.

Come accennato in precedenza, Cloud Spanner elabora automaticamente le suddivisioni in base al loro carico, quindi i risultati migliorano dopo diverse iterazioni consecutive del test. I risultati presentati qui sono i migliori risultati che abbiamo ricevuto. Guardando i numeri sopra, possiamo vedere come Cloud Spanner scala (bene) con l'aumentare del numero di nodi nel cluster. I numeri che spiccano sono una latenza media estremamente bassa, che contrasta con i risultati di carichi di lavoro misti (95% in lettura e 5% in scrittura) come descritto nella sezione precedente.

Ridimensionamento?

Aumentare e diminuire il numero di nodi Cloud Spanner è un'attività che richiede un solo clic. Se desideri caricare i dati rapidamente, potresti prendere in considerazione l'idea di potenziare l'istanza al massimo (nel nostro caso erano 25 nodi nella regione US-EAST) e quindi ridurre il numero di nodi adatti al tuo carico normale dopo tutti i dati nel database , tenendo presente il limite di 2 TB/nodo.

Ci è stato ricordato questo limite anche con un database molto più piccolo. Dopo diverse esecuzioni di test di carico, il nostro database aveva una dimensione di circa 155 GB e, quando è stato ridotto a un'istanza di 1 nodo, abbiamo ricevuto il seguente errore:

Google Cloud Spanner: buono, cattivo, brutto

Siamo stati in grado di ridimensionare da 25 a 2 istanze, ma siamo bloccati su due nodi.

L'aumento e la diminuzione del numero di nodi in un cluster Cloud Spanner possono essere automatizzati utilizzando l'API REST. Ciò può essere particolarmente utile per ridurre l'aumento del carico sul sistema durante le ore di punta.

Prestazioni delle query OLAP?

Inizialmente avevamo programmato di dedicare molto tempo alla nostra valutazione di Spanner su questa parte. Dopo alcuni SELECT COUNT, ci siamo subito resi conto che il test sarebbe stato breve e che Spanner NON sarebbe stato un motore adatto per OLAP. Indipendentemente dal numero di nodi nel cluster, la semplice scelta del numero di righe in una tabella di 10 milioni di righe richiedeva dai 55 ai 60 secondi. Inoltre, qualsiasi query che richiedeva più memoria per archiviare i risultati intermedi non è riuscita con un errore OOM.

SELECT COUNT(DISTINCT(field0)) FROM usertable; — (10M distinct values)-> SpoolingHashAggregateIterator ran out of memory during new row.

Alcuni numeri per le query TPC-H possono essere trovati nell'articolo di Todd Lipcon nosql-kudu-spanner-slides.html, diapositive 42 e 43. Questi numeri sono coerenti con i nostri risultati (purtroppo).

Google Cloud Spanner: buono, cattivo, brutto

4. I nostri risultati

Dato lo stato attuale delle funzionalità di Cloud Spanner, è difficile vederlo come un semplice sostituto di una soluzione OLTP esistente, soprattutto quando le tue esigenze lo superano. Ci vorrebbe molto tempo per creare una soluzione attorno alle carenze di Cloud Spanner.

Quando abbiamo iniziato a valutare Cloud Spanner, ci aspettavamo che le sue funzionalità di gestione fossero alla pari, o almeno non lontane, da altre soluzioni Google SQL. Ma siamo rimasti sorpresi dalla totale mancanza di backup e dal controllo di accesso molto limitato alle risorse. Per non parlare di nessuna vista, nessun ambiente di sviluppo locale, sequenze non supportate, JDBC senza supporto DML e DDL e così via.

Quindi, dove andare per qualcuno che ha bisogno di scalare un database transazionale? Non sembra esserci ancora un'unica soluzione sul mercato che si adatti a tutti i casi d'uso. Esistono molte soluzioni chiuse e open source (alcune delle quali sono menzionate in questo articolo), ciascuna con i propri punti di forza e di debolezza, ma nessuna di esse offre SaaS con uno SLA del 99,999% e un alto grado di coerenza. Se uno SLA elevato è il tuo obiettivo principale e non sei propenso a creare la tua soluzione per più cloud, Cloud Spanner potrebbe essere la soluzione che stai cercando. Ma dovresti essere consapevole di tutti i suoi limiti.

Per essere onesti, Cloud Spanner è stato rilasciato al pubblico solo nella primavera del 2017, quindi è ragionevole aspettarsi che alcuni dei suoi difetti attuali possano eventualmente scomparire (si spera) e, quando lo farà, potrebbe essere un punto di svolta. Dopotutto, Cloud Spanner non è solo un progetto parallelo per Google. Google lo utilizza come base per altri prodotti Google. E quando Google ha recentemente sostituito Megastore in Google Cloud Storage con Cloud Spanner, ha consentito a Google Cloud Storage di diventare altamente coerente per gli elenchi di oggetti su scala globale (che non è ancora il caso per Amazon S3).

Quindi, c'è ancora speranza... speriamo.

È tutto. Come l'autore dell'articolo, anche noi continuiamo a sperare, ma voi cosa ne pensate? Scrivi nei commenti

Invitiamo tutti a visitare il ns webinar gratuito in cui ti parleremo in dettaglio del corso "AWS per sviluppatori" da OTUS.

Fonte: habr.com

Aggiungi un commento