Rendere operativo Snowpark Python: prima parte

Dec 06 2022
Nella prima parte di questo post, delineeremo alcune delle sfide uniche associate alla messa in produzione del codice Snowpark Python, discuteremo i componenti in modo più dettagliato e delineeremo alcuni principi di progettazione del codice da seguire durante la creazione con Snowpark Python. In una futura parte due, discuteremo i concetti CI/CD rilevanti per gli sviluppatori Python che lavorano con Snowpark Python.

Nella prima parte di questo post, delineeremo alcune delle sfide uniche associate alla messa in produzione del codice Snowpark Python, discuteremo i componenti in modo più dettagliato e delineeremo alcuni principi di progettazione del codice da seguire durante la creazione con Snowpark Python.

In una futura parte due, discuteremo i concetti CI/CD rilevanti per gli sviluppatori Python che lavorano con Snowpark Python.

Sfide fondamentali

Perché la gestione del codice Snowpark in produzione è diversa dalla gestione del codice tradizionale/legacy in produzione? In molti modi, non lo è. Infatti, se hai già familiarità con la creazione di applicazioni e funzionalità basate su Snowflake e utilizzi cose come stored procedure, funzioni definite dall'utente, ecc. (SQL o Snowpark in una lingua diversa), allora la gestione del codice Snowpark Python è non fondamentalmente diverso da come stai facendo lo stesso lavoro oggi. Ci sono alcuni vincoli ambientali per il runtime Python lato server in Snowflake, ma questi sono simili a come le funzionalità Javascript e Java sono vincolate in Snowpark, quindi gli stessi principi e pratiche che stai seguendo per le tue funzionalità esistenti basate su Snowflake si applicheranno a Pitone da snowpark.

Le differenze e le sfide diventano più evidenti per i team e le applicazioni basate su Python, ma non su Snowflake. Anche se Snowflake funge da origine dati e/o sink all'interno dell'architettura esistente, le funzionalità di Python che vengono eseguite esternamente a Snowflake (in VM serverless o ospitate su cloud, ecc.) rispetto a quelle basate su Snowpark Python presentano alcune differenze fondamentali rispetto a fare attenzione a. Fondamentalmente, le funzionalità integrate in Snowpark sono solo codice Python e molte delle stesse pratiche e strumenti che il team utilizza oggi per gestire le basi di codice esistenti sono ancora rilevanti. Le considerazioni principali associate alla creazione su Snowpark derivano dal fatto che il codice finale e l'applicazione sono distribuiti in una piattaforma SaaS completamente gestita, e quindi l'infrastruttura e i framework di calcolo in cui il tuo codice deve funzionare non sono interamente aperti al tuo controllo e alla tua discrezione. Inoltre, l'applicazione è completamente integrata con la tua piattaforma dati, il che offre molti vantaggi, ma è un paradigma diverso rispetto a quello in cui viene sviluppata la maggior parte delle applicazioni e quindi i modelli di progettazione consigliati differiscono. La distribuzione del codice Python in un ambiente SQL-first pone anche limitazioni su quali tipi di dati e su come i dati possono essere passati tra moduli, funzioni, ecc. /7. Snowpark è più adatto per lavori e applicazioni su richiesta e/o programmati/orchestrati.

Componenti dello snowpark

Il termine generico Snowpark Python si riferisce a due componenti fondamentalmente diversi, ma anche strettamente collegati. Una è un'API di dataframe client, che è disponibile come pacchetto Python e può essere installata in qualsiasi ambiente Python (il supporto per Python 3.8 è GA a partire dal 7 novembre 2022), sfruttata da qualsiasi applicazione Python, ecc. per ridurre le trasformazioni di dataframe nel calcolo del fiocco di neve. Al contrario, Snowpark ha anche un runtime lato server che ti consente di distribuire codice Python più arbitrario in un runtime gestito da Snowflake, da eseguire insieme ai tuoi dati. Esistono diversi meccanismi per il modo in cui il codice Python può essere distribuito ed eseguito nel runtime lato server, che verrà approfondito più in dettaglio di seguito. Questa distinzione è importante perché, mentre ci sono sfide condivise che dovrebbero essere affrontate per Snowpark Python in generale, ognuno di questi componenti ha anche sfide specifiche e uniche che cercheremo di affrontare in questo documento. In generale, faremo riferimento a Snowpark Python per comprendere entrambi i componenti in generale, l'API client e/o dataframe per fare riferimento alla libreria Python installabile e il runtime lato server per fare riferimento all'ambiente di esecuzione Python gestito da Snowflake. In questo post riassumeremo i componenti principali, ma dovresti fare riferimento al e il runtime lato server per fare riferimento all'ambiente di esecuzione Python gestito da Snowflake. In questo post riassumeremo i componenti principali, ma dovresti fare riferimento al e il runtime lato server per fare riferimento all'ambiente di esecuzione Python gestito da Snowflake. In questo post riassumeremo i componenti principali, ma dovresti fare riferimento alSnowpark Developer Guide per Python per ulteriori dettagli, esempi di codice, documentazione e altro.

API del frame di dati del cliente

L'API Snowpark Client Dataframe è una libreria Python che può essere installata in qualsiasi ambiente in cui è possibile eseguire un kernel Python e dispone di connettività al tuo account Snowflake (nota: attualmente l'API dataframe è compatibile solo con Python 3.8). L'API Dataframe fornisce una sintassi Python in stile dataframe per eseguire il pushdown di query, trasformazioni e altro nell'ambiente di elaborazione gestito di Snowflake.

Fondamentalmente, l'API Snowpark fornisce i metodi Python corrispondenti per operazioni SQL comuni, in una sintassi familiare all'API PySpark Dataframe, con una notevole sovrapposizione di funzionalità rispetto a ciò che puoi fare oggi in PySpark. È importante notare che Snowpark utilizza un motore di calcolo back-end fondamentalmente diverso rispetto a PySpark, ma dal punto di vista sintattico e funzionale sarà molto simile all'API PySpark Dataframe. L'API opera su Snowpark Dataframes, che sono puntatori a tabelle, viste, ecc. all'interno di Snowflake. Le operazioni corrispondenti per una chiamata API Snowpark vengono eseguite in modo pigro su un magazzino virtuale Snowflake. Ciò significa che nLe operazioni del dataframe di Snowpark vengono eseguite direttamente nell'ambiente di calcolo del client Python, pertanto i requisiti di calcolo del client possono essere estremamente bassi. Snowpark fornisce anche metodi convenienti per portare i dataframe di Snowpark in memoria sul client come dataframe di panda e viceversa (riscrivendo i dataframe di panda su Snowflake). L'API Snowpark richiede che i dati di origine si trovino in Snowflake per eseguire il pushdown delle trasformazioni in un magazzino virtuale Snowflake. Oltre ai metodi corrispondenti alle operazioni SQL, l'API Snowpark contiene altre funzioni di supporto, oltre alla possibilità di richiamare oggetti Python Snowpark lato server (UD(T)Fs e Sprocs, descritti più dettagliatamente di seguito) da un client Python tempo di esecuzione.

UD(T)F e stored procedure

Il runtime lato server di Snowpark Python consente di scrivere stored procedure Python e funzioni (tabella) definite dall'utente (UD(T)F) che vengono distribuite in Snowflake, disponibili per essere richiamate da qualsiasi interfaccia Snowflake ed eseguite in modo protetto, Sandbox Python sui magazzini virtuali Snowflake.

Gli UDF e gli UDTF Python scalano l'elaborazione associata al codice Python sottostante in modo che avvenga in parallelo su tutti i thread e i nodi che comprendono il magazzino virtuale su cui è in esecuzione la funzione. Esistono tre diversi meccanismi di tipo UDF con Snowpark Python:

  • Le funzioni definite dall'utente (UDF) sono operazioni scalari uno a uno: per una riga di dati di input passati alla funzione, viene prodotto un singolo output. Righe di dati vengono elaborate in parallelo attraverso i processi Python su ciascun nodo all'interno di un magazzino virtuale.
  • Le UDF vettorializzate/batch sono analogamente operazioni scalari uno a uno come le UDF sopra descritte. La differenza è che le UDF parallelizzano le operazioni dell'UDF su singole righe di dati. Le UDF vettorizzate parallelizzano l'operazione UDF sui batchdi dati (più righe). Funzionalmente, le UDF vettorizzate producono ancora un singolo output per ogni riga di dati di input, tuttavia i dati vengono raggruppati in singole istanze dell'UDF (molte righe passate contemporaneamente e molti batch elaborati in parallelo). La ragione di ciò è che molte operazioni Python basate su Pandas, Numpy, scipy, ecc. che possono essere utilizzate nelle UDF Python sono ottimizzate per essere eseguite come operazioni in stile vettoriale. Quando vengono elaborate singole righe, come in un UDF standard, l'UDF non sfrutta appieno le ottimizzazioni basate su array incorporate nelle librerie Python sottostanti. Le UDF vettorizzate ti consentono di farlo; sono funzionalmente uguali alle normali UDF Python, tuttavia i dati vengono raggruppati e gestiti in blocco per sfruttare le ottimizzazioni basate su array in varie librerie Python comuni.
  • Le funzioni di tabella definite dall'utente (UDTF) sono funzioni Python che richiedono operazioni stateful su batch di dati. Le UDF vettorizzate raggruppano in modo casuale i dati per un'elaborazione più ottimale e accelerata, tuttavia non consentono all'utente/sviluppatore di determinare quali dati vengono raggruppati e come viene elaborato l'intero batch di dati: solo singole righe. Gli UDTF consentono l'elaborazione di righe da molti a molti e da molti a uno. Ogni UDTF ha un metodo di processo e un metodo endPartition. Il metodo di processo definisce quale lavoro viene eseguito come singole righein un batch vengono elaborati (è possibile o meno restituire una sorta di output per riga, a seconda della funzionalità sottostante). Il metodo endPartition definisce quale lavoro viene eseguito sull'intero batch di dati dopo l'elaborazione di tutte le singole righe e può includere una sorta di lavoro con stato che è stato creato durante l'elaborazione del batch. Quando vengono richiamati gli UDTF, la partizione per espressione consente di specificare quali campi nei dati sottostanti vengono utilizzati per eseguire il batch dei dati, ad esempio se si esegue la partizione per COUNTRY , tutti i record con COUNTRY=US vengono elaborati nello stesso batch, tutti i record con PAESE=CINA vengono elaborati nello stesso batch, ecc.

Oltre a UD(T)Fs, il runtime lato server di Snowpark Python fornisce supporto per le stored procedure Python. Le stored procedure possono essere considerate come uno script più arbitrario che viene eseguito su un magazzino virtuale Snowflake. Una differenza fondamentale è che le stored procedure sono a nodo singolo; quindi, per eseguire trasformazioni o analisi dei dati su larga scala all'interno di una procedura archiviata, i processi archiviati dovrebbero sfruttare l'API del dataframe del client o altri UD(T)F distribuiti per ridimensionare il calcolo corrispondente su tutti i nodi di un magazzino virtuale. Ciò è in contrasto con l'estrazione di tutti i dati da una query inla procedura memorizzata e manipolandola ad esempio con i panda. Invece, ridimensiona il calcolo nel magazzino virtuale da una stored procedure sfruttando l'API dataframe e UD(T)F per un lavoro più intensivo dal punto di vista computazionale. Questo è ulteriormente illustrato nel seguente principio di progettazione del codice: dovresti evitare di inserire i dati direttamente in una stored procedure nella massima misura possibile (con alcune eccezioni: questo è descritto in modo più dettagliato più avanti in questo articolo). Ciò che le stored procedure forniscono, tuttavia, è un modo semplice per distribuire un flusso di programma simile a uno script nel runtime lato server di Snowpark, che può essere "avviato" come un lavoro tramite attività o istruzioni SQL dirette "CALL sproc".

Progettazione del codice

Le funzionalità principali di Snowpark Python possono essere pensate in cinque bucket.

Principi di progettazione

  1. Il principio guida per la creazione di capacità operative con Snowpark Python è di non estrarre i dati da Snowflake ed elaborarli in un ambiente client ove possibile . Sfrutta invece l'API dataframe del client Snowpark per il pushdown di operazioni simili a SQL e il runtime lato server per distribuire codice più arbitrario come UD(T)F in grado di scalare tra i magazzini virtuali Snowflake ed elaborare i dati in modo più efficiente. Questa può essere considerata una pratica di programmazione simile al push di SQL tramite JDBC in altri strumenti o applicazioni.
  2. Utilizza l'API del dataframe del client Snowpark nelle tue applicazioni durante l'esecuzione di query e la trasformazione dei dati, invece di recuperare tutti i dati in un'applicazione client ed elaborarli lì. Questo può scalare efficacemente le trasformazioni dei dati indipendentemente dal fatto che l'intera applicazione sia in esecuzione all'interno dell'ambiente di runtime lato server di Snowpark o in un ambiente esterno a Snowflake Python. Vale la pena notare che è possibile utilizzare SQL direttamente invece di utilizzare l'API dataframe, tuttavia molti sviluppatori trovano ingombrante provare a creare e utilizzare SQL all'interno di un'applicazione scritta in un'altra lingua, mentre l'API dataframe fornisce una sintassi Pythonic familiare da raggiungere gli stessi risultati su larga scala.
  3. Il codice che manipola, analizza o trasforma i dati dovrebbe, nella massima misura possibile, essere costruito come UD(T)F (la decisione tra UDF e UDTF si riduce in gran parte a funzionalmente ciò che il codice/trasformazione sta effettivamente facendo) o dovrebbe essere implementato utilizzando l'API dataframe del client Snowpark. Questo perché l'API dataframe e UD(T)Fs consentono di ridimensionare e parallelizzare il lavoro computazionale eseguito attraverso il magazzino virtuale.
  4. Python Sprocs è più adatto per il flusso di controllo dei programmi Python. Pensa a sproc come allo script o all'applicazione principale: inizializza oggetti, mantiene lo stato, ecc. All'interno di sproc, qualsiasi calcolo ad alta intensità di dati dovrebbe chiamare l'API dataframe o utilizzare UD(T)F che sono stati creati e distribuiti separatamente. Analogamente al principio precedente di "non estrarre i dati per il client", in genere dovresti evitare di inserire i dati nella memoria della stored procedure, poiché ciò limita la tua capacità di ridimensionamento. Invece, invia il lavoro eseguito nello sproc al magazzino virtuale utilizzando l'API dataframe e UD(T)Fs. Lo sproc può essere pensato come una "unità" di lavoro all'interno della tua applicazione, che potresti orchestrare utilizzando attività o un servizio di orchestrazione esterno (ad esempio Airflow).
  5. Solo per sottolineare nuovamente: dovresti evitare di inserire i dati direttamente in Sproc nella massima misura possibile. Gli Sproc sono limitati all'esecuzione su un singolo nodo nel magazzino virtuale e quindi sono limitati alle risorse computazionali di quel nodo. I magazzini ottimizzati per lo snowpark (ora in anteprima pubblica) estenderanno la possibilità per gli sproc di eseguire più lavoro ad alta intensità di memoria/dati, ma come regola generale, gli sproc non dovrebbero essere incaricati di eseguire autonomamente calcoli significativi e all'interno di uno sproc dovresti scaricare il lavoro sull'API di Snowpark e/o sugli UD(T)F.
  6. L'eccezione al principio 5 è il calcolo che non può essere eseguito in modo distribuito e richiede l'accesso a un intero set di dati (o un campione di grandi dimensioni) per essere eseguito. Ad esempio, l'addestramento del modello di machine learning a nodo singolo dovrebbe generalmente essere eseguito nel contesto di una stored procedure, ma questo è un raro esempio in cui il calcolo a uso intensivo di dati deve essere eseguito in una stored procedure. Senza entrare troppo nei dettagli, potrebbero esserci casi d'uso relativi all'addestramento del modello, in particolare in cui la parallelizzazione tramite UD(T)F ha senso, ma questo è un insieme piccolo e specifico di casi d'uso.
  7. I principi standard di progettazione del codice Python dovrebbero essere seguiti per quanto riguarda la modularità del codice, la riusabilità, ecc. potenzialmente sfruttato in tutto il tuo ecosistema di applicazioni Snowpark.
  8. Le classi e gli oggetti personalizzati utilizzati nell'applicazione devono essere valutati per verificarne la compatibilità con UD(T)F. Ad esempio: supponiamo di avere una parte della tua applicazione che accetta una serie di dati, esegue alcune analisi su di essa, quindi costruisce un oggetto di classe Python personalizzato basato sull'output della tua analisi. Questo è potenzialmente un buon adattamento per un UDTF, tuttavia, non supportiamo la restituzione di oggetti Python arbitrari dalle funzioni di Snowpark. Pertanto, dovrai considerare i metodi di serializzazione/deserializzazione del tuo oggetto per i tipi di dati SQL supportati, ad esempio implementando to_json() e from_json() costruttori/serializzatori in modo da poter inizializzare i tuoi oggetti di classe dai dati della tabella Snowflake. Potresti anche considerare la serializzazione binaria, indipendentemente dall'approccio, questo deve essere considerato.
  9. Le istanze di UD(T)F verranno riutilizzate all'interno di un singolo set di query. Puoi trarne vantaggio spostando il codice di inizializzazione o le variabili/lo stato temporaneo che possono essere riutilizzati tra le esecuzioni al di fuori del metodo della funzione e nella parte globale/statica del codice. Ciò significa che nelle successive esecuzioni quelle variabili o lo stato temporaneo potrebbero essere riutilizzati senza dover reinizializzare ogni singola esecuzione. Ciò sarà particolarmente importante man mano che diventano disponibili cose come l'accesso esterno, in cui vorrai inserire client HTTP o pool di connessioni al di fuori della dichiarazione della funzione (vedi AWS Lambda/Funzioni di Azure per modelli di progettazione e best practice simili).
  10. Le UDF vettorializzate (batch) consentono di eseguire le stesse operazioni delle UDF, sfruttando al contempo le ottimizzazioni interne basate su array di Python su Panda e oggetti di tipo Numpy (questo è descritto più dettagliatamente sopra). Come regola generale, è generalmente consigliabile distribuire UDF come UDF vettoriali se una qualsiasi delle operazioni sottostanti sui dati si basa su Numpy/Panda o viene implementata utilizzando operazioni vettoriali. In questo modo si ottimizza semplicemente la distribuzione dei dati al runtime lato server e si consente a Snowpark di sfruttare le ottimizzazioni Python integrate.
  11. Oltre ai livelli di memorizzazione nella cache di Snowflake, il codice Python di Snowpark dovrebbe generalmente seguire le migliori pratiche di memorizzazione nella cache di Python, con l'eccezione della necessità di memorizzare nella cache i risultati delle query (poiché Snowflake lo farà per te a vari livelli dell'architettura). Ma, ad esempio, se hai un UDF che esegue la tokenizzazione del testo nella tua applicazione e per usarlo devi caricare un tokenizer compilato dallo stage, dovresti eseguire il wrapping di una funzione che carica il tokenizer nell'UDF usando cachetools, per evitare ripetutamente caricamento del tokenizer dallo stage nell'UDF. Questo è il caso d'uso più comune per la memorizzazione nella cache all'interno del codice Snowpark, quando un artefatto deve essere caricato in un UD(T)F dallo stage.

Porting di applicazioni esistenti su Snowpark

Molti clienti hanno chiesto in che modo possono trasferire le funzionalità e le applicazioni esistenti per l'esecuzione in Snowpark, sia per migliorare la scalabilità, le prestazioni, la governance e la sicurezza, sia semplificando la loro architettura ed eliminando la complessità associata alla proprietà e alla gestione dell'infrastruttura. Al di là della valutazione iniziale di "questa applicazione e/o codice può essere supportato da Snowpark", dovrebbero esserci discussioni e conversazioni approfondite su come spostare il codice in modo ottimale. Nel caso della migrazione delle applicazioni PySpark specificamente a Snowpark (rispetto ad altre app Python più generiche), Snowflake ha stretto una partnership con Mobilize.netper fornire uno strumento gratuito di analisi del codice da PySpark a Snowpark che può aiutare a determinare se la base di codice esistente è un buon candidato per la migrazione a Snowpark. Inoltre, i team dei servizi professionali di Snowflake e i partner SI possono fornire un supporto completo per la migrazione.

Nel caso di applicazioni Python più generiche, gli script Python potrebberoessere semplicemente inseriti in Python sprocs ed è probabile che "funzionino" più o meno, ma è probabile che si tratti di un'implementazione estremamente inefficiente, a meno che il particolare script non sia computazionalmente leggero. In particolare, è probabile che queste applicazioni esistenti estraggano dati da Snowflake, che, come abbiamo sottolineato in precedenza, non è il modello consigliato per la creazione di applicazioni su Snowpark. Le due domande principali da considerare sono (1) in che modo è necessario eseguire il refactoring del codice che estrae i dati da Snowflake per sfruttare più push-down di calcolo. Inoltre, (2) le applicazioni ad alta intensità di dati devono essere valutate in particolare per determinare quali funzioni, metodi, classi, ecc. sono meglio servite sfruttando gli UD(T)F di Python e sfruttando la scalabilità e le prestazioni nei magazzini virtuali.

Oltre alla semplice analisi del codice, un altro approccio per eseguire questa valutazione della migrazione consiste nell'iniziare con una base di codice e produrre un diagramma di flusso logico dell'applicazione/script, suddiviso in singoli componenti computazionali. Molti team potrebbero già disporre di questo tipo di diagramma di architettura/progettazione del software disponibile, ma in caso contrario, è un buon inizio per comprendere come i dati e il lavoro computazionale sono distribuiti all'interno dell'applicazione. Questo diagramma può includere anche oggetti di classe personalizzati, ecc. Per ogni funzione, classe, metodo utilizzato, è necessario valutare quali dati devono essere forniti e quale output viene prodotto. L'output è compatibile con i tipi di dati Snowflake? Cosa consumerà l'output e come verrà utilizzato? Il calcolo viene eseguito in modo sufficientemente intenso da giustificare l'esecuzione su un cluster di nodi di calcolo in un magazzino virtuale? Quanta configurazione personalizzata è richiesta per ogni iterazione dell'utilizzo della funzione/oggetto?

Compilando questo diagramma, inizierà a diventare chiaro cosa dovrebbe esistere nel livello di flusso logico dell'applicazione (che potrebbe essere adatto per uno sproc Python di Snowpark) e cosa deve essere scaricato su un UD(T)F , e quindi potenzialmente sottoposto a refactoring. Questo ti indicherà anche quali classi e oggetti personalizzati devono avere metodi di serializzazione per scrivere/inizializzare dalla struttura della tabella Snowpark, come descritto nei principi di progettazione sopra.

Da questo punto, puoi iniziare a capire quale codice può essere rimosso e spostato, rispetto a ciò che deve essere refactored, reimplementato o modificato. Inoltre, Snowflake Professional Services ha offerte per un'assistenza più pratica con la migrazione delle applicazioni a Snowpark che va oltre i principi e le best practice.

Conclusione

In questo post, abbiamo dettagliato cos'è Snowpark Python (API dataframe client e runtime lato server) e come può essere utilizzato al meglio nelle applicazioni Python. Abbiamo delineato i principi di progettazione fondamentali che dovrebbero essere seguiti durante la progettazione o la migrazione di applicazioni per Snowpark Python. Nella seconda parte, esamineremo come incorporare queste nuove funzionalità nelle pratiche CI/CD e DevOps esistenti, incluso il modo in cui Snowpark Python può essere sia simile, sia piuttosto diverso, da altri framework di sviluppo.