Operacionalizando Snowpark Python: Parte Um

Dec 06 2022
Na primeira parte desta postagem, descreveremos alguns dos desafios exclusivos associados à colocação do código Snowpark Python em produção, discutiremos os componentes com mais detalhes e descreveremos alguns princípios de design de código a serem seguidos ao criar com o Snowpark Python. Em uma futura parte dois, discutiremos conceitos relevantes de CI/CD para desenvolvedores Python que trabalham com o Snowpark Python.

Na primeira parte desta postagem, descreveremos alguns dos desafios exclusivos associados à colocação do código Snowpark Python em produção, discutiremos os componentes com mais detalhes e descreveremos alguns princípios de design de código a serem seguidos ao criar com o Snowpark Python.

Em uma futura parte dois, discutiremos conceitos relevantes de CI/CD para desenvolvedores Python que trabalham com o Snowpark Python.

Principais desafios

Por que o gerenciamento do código do Snowpark em produção é diferente do gerenciamento de código tradicional/legado na produção? De muitas maneiras, não é. Na verdade, se você já está familiarizado com a criação de aplicativos e recursos desenvolvidos pelo Snowflake e utilizou coisas como procedimentos armazenados, funções definidas pelo usuário etc. (seja SQL ou Snowpark em uma linguagem diferente), gerenciar o código Snowpark Python é não é fundamentalmente diferente de como você está fazendo o mesmo trabalho hoje. Existem algumas restrições ambientais para o tempo de execução do Python do lado do servidor no Snowflake, mas elas são semelhantes a como os recursos de Javascript e Java são limitados no Snowpark e, portanto, os mesmos princípios e práticas que você está seguindo para os recursos existentes do Snowflake se aplicarão a Parque de neve Python.

As diferenças e os desafios se tornam mais aparentes para equipes e aplicativos criados em Python, mas não em Snowflake. Mesmo que o Snowflake atue como uma fonte de dados e/ou coletor em sua arquitetura existente, os recursos do Python executados externamente ao Snowflake (em VMs sem servidor ou hospedadas na nuvem etc.) estar ciente de. Fundamentalmente, os recursos criados no Snowpark são apenas código Python, e muitas das mesmas práticas e ferramentas que a equipe usa hoje para gerenciar suas bases de código existentes ainda são relevantes. As principais considerações associadas à construção no Snowpark decorrem do fato de que o código final e o aplicativo são implantados em uma plataforma SaaS totalmente gerenciada, e, portanto, a infraestrutura e as estruturas de computação nas quais seu código deve funcionar não estão totalmente abertas ao seu controle e discrição. Além disso, o aplicativo é totalmente integrado à sua plataforma de dados, o que gera muitos benefícios, mas é um paradigma diferente daquele em que a maioria dos aplicativos é desenvolvida e, portanto, os padrões de design recomendados diferem. A implantação do código Python em um ambiente SQL também coloca limitações sobre quais tipos de dados e como os dados podem ser passados ​​entre módulos, funções, etc. Os aplicativos Snowpark também são diferentes de outros aplicativos que podem ser projetados para funcionar continuamente, 24 /7. O Snowpark é mais adequado para trabalhos e aplicativos sob demanda e/ou agendados/orquestrados.

Componentes do parque de neve

O termo amplo Snowpark Python refere-se a dois componentes fundamentalmente diferentes, mas também fortemente acoplados. Uma é uma API de dataframe do cliente, que está disponível como um pacote Python e pode ser instalada em qualquer ambiente Python (o suporte para Python 3.8 é GA a partir de 7 de novembro de 2022), aproveitado por qualquer aplicativo Python etc. na computação Snowflake. Em contraste com isso, o Snowpark também possui um tempo de execução do lado do servidor que permite implantar um código Python mais arbitrário em um tempo de execução gerenciado pelo Snowflake, para ser executado junto com seus dados. Existem vários mecanismos diferentes de como o código Python pode ser implantado e executado no tempo de execução do lado do servidor, que será abordado com mais detalhes abaixo. Essa distinção é importante porque, embora existam desafios compartilhados que devem ser abordados para Snowpark Python de forma ampla, cada um desses componentes também tem desafios específicos e únicos que pretendemos abordar ao longo deste documento. Em geral, nos referiremos ao Snowpark Python para abranger ambos os componentes em geral, o cliente e/ou a API do dataframe para se referir à biblioteca Python instalável e o tempo de execução do lado do servidor para se referir ao ambiente de execução do Python gerenciado pelo Snowflake. Neste post vamos resumir os componentes principais, mas você deve consultar o e o tempo de execução do lado do servidor para se referir ao ambiente de execução do Python gerenciado pelo Snowflake. Neste post vamos resumir os componentes principais, mas você deve consultar o e o tempo de execução do lado do servidor para se referir ao ambiente de execução do Python gerenciado pelo Snowflake. Neste post vamos resumir os componentes principais, mas você deve consultar oGuia do desenvolvedor do Snowpark para Python para obter detalhes adicionais, exemplos de código, documentação e muito mais.

API de dataframe do cliente

A API Snowpark Client Dataframe é uma biblioteca Python que pode ser instalada em qualquer ambiente onde você pode executar um kernel Python e tem conectividade com sua conta Snowflake (observação: atualmente a API dataframe é compatível apenas com Python 3.8). A API Dataframe fornece sintaxe Python estilo dataframe para realizar push-down de consultas, transformações e muito mais no ambiente de computação gerenciado do Snowflake.

Fundamentalmente, a API Snowpark fornece métodos Python correspondentes para operações SQL comuns, em uma sintaxe familiar à API PySpark Dataframe, com considerável sobreposição de funcionalidade para o que você pode fazer no PySpark hoje. É importante observar que o Snowpark usa um mecanismo de computação de back-end fundamentalmente diferente em comparação com o PySpark, mas sintaticamente e funcionalmente parecerá muito semelhante à API PySpark Dataframe. A API opera em Snowpark Dataframes, que são ponteiros para tabelas, visualizações, etc. dentro do Snowflake. As operações correspondentes para uma chamada de API do Snowpark são executadas lentamente em um armazém virtual do Snowflake. Isso significa que nãoAs operações de dataframe do Snowpark são executadas diretamente no ambiente de computação do cliente Python e, portanto, os requisitos computacionais do cliente podem ser extremamente baixos. O Snowpark também fornece métodos convenientes para trazer quadros de dados do Snowpark na memória do cliente como quadros de dados do pandas e vice-versa (escrever quadros de dados do pandas de volta para o Snowflake). A API do Snowpark requer que os dados de origem estejam localizados no Snowflake para realizar push-down de transformações em um Snowflake Virtual Warehouse. Além dos métodos correspondentes às operações SQL, a API do Snowpark contém outras funções auxiliares, juntamente com a capacidade de invocar objetos Snowpark Python do lado do servidor (UD(T)Fs e Sprocs, descritos com mais detalhes abaixo) de um cliente Python tempo de execução.

UD(T)Fs e Procedimentos Armazenados

O tempo de execução Snowpark Python do lado do servidor torna possível escrever Procedimentos Armazenados Python e Funções Definidas pelo Usuário (Tabela) (UD(T)Fs) que são implantadas no Snowflake, disponíveis para serem invocadas de qualquer interface do Snowflake e executadas de forma segura, Sandbox Python em armazéns virtuais Snowflake.

Os UDFs e UDTFs do Python expandem o processamento associado ao código Python subjacente para ocorrer em paralelo em todos os encadeamentos e nós que compõem o armazém virtual no qual a função está sendo executada. Existem três mecanismos diferentes do tipo UDF com o Snowpark Python:

  • As funções definidas pelo usuário (UDF) são operações escalares um-para-um: para uma linha de dados de entrada passados ​​para a função, uma única saída é produzida. Linhas de dados são processadas em paralelo nos processos Python em cada nó dentro de um armazém virtual.
  • UDFs vetorizadas/em lote são operações escalares um-para-um semelhantes às UDFs descritas acima. A diferença é que as UDFs paralelizam as operações da UDF em linhas individuais de dados. UDFs vetorizados paralelizam a operação UDF em lotesde dados (várias linhas). Funcionalmente, as UDFs vetorizadas ainda produzem uma única saída para cada linha de entrada de dados, no entanto, os dados são agrupados em instâncias individuais da UDF (muitas linhas passadas simultaneamente e muitos lotes processados ​​em paralelo). A razão para isso é que muitas operações Python baseadas em Pandas, Numpy, scipy, etc. que podem ser usadas em UDFs Python são otimizadas para serem executadas como operações de estilo vetorial. Quando linhas individuais são processadas, como em uma UDF padrão, a UDF não está aproveitando totalmente as otimizações baseadas em array que são incorporadas às bibliotecas Python subjacentes. UDFs vetorizados permitem que você faça isso; eles são funcionalmente iguais aos UDFs normais do Python, no entanto, os dados são agrupados e operados em massa para aproveitar as otimizações baseadas em array em várias bibliotecas comuns do Python.
  • Funções de tabela definidas pelo usuário (UDTFs) são funções do Python que exigem operações com informações de estado em lotes de dados. UDFs vetorizados agrupam dados aleatoriamente para um processamento mais otimizado e acelerado, no entanto, eles não permitem que o usuário/desenvolvedor determine quais dados são agrupados e como todo o lote de dados é processado: apenas linhas individuais. UDTFs permitem o processamento de linha muitos-para-muitos e muitos-para-um. Cada UDTF tem um método de processo e um método endPartition. O método de processo define qual trabalho é feito como linhas individuaisem um lote são processados ​​(você pode ou não retornar algum tipo de saída por linha, dependendo da funcionalidade subjacente). O método endPartition define qual trabalho é feito em todo o lote de dados após o processamento de todas as linhas individuais e pode incluir algum tipo de trabalho de estado que foi criado conforme o lote foi processado. Quando UDTFs são chamados, a partição por expressão permite que você especifique quais campos nos dados subjacentes são usados ​​para agrupar os dados, ou seja, se você particionar por COUNTRY , todos os registros com COUNTRY=US serão processados ​​no mesmo lote, todos os registros com COUNTRY=CHINA são processados ​​no mesmo lote, etc.

Além de UD(T)Fs, o tempo de execução do lado do servidor do Snowpark Python fornece suporte para procedimentos armazenados do Python. Os procedimentos armazenados podem ser considerados como um script mais arbitrário que é executado em um warehouse virtual do Snowflake. Uma diferença importante é que os procedimentos armazenados são de nó único; portanto, para executar transformações ou análises de dados em escala dentro de um procedimento armazenado, os procedimentos armazenados devem aproveitar a API do dataframe do cliente ou outros UD(T)Fs implantados para dimensionar a computação correspondente em todos os nós de um armazém virtual. Isso é diferente de extrair todos os dados de uma consulta parao procedimento armazenado e manipulá-lo, por exemplo, com pandas. Em vez disso, dimensione a computação no warehouse virtual a partir de um procedimento armazenado, aproveitando a API do dataframe e os UD(T)Fs para um trabalho computacionalmente mais intensivo. Isso é ilustrado no princípio de design de código abaixo: você deve evitar puxar dados diretamente para um procedimento armazenado ao máximo possível (com algumas exceções: isso é detalhado mais detalhadamente posteriormente neste artigo). O que os procedimentos armazenados fornecem, no entanto, é uma maneira simples de implantar um fluxo de programa semelhante a um script no tempo de execução do lado do servidor do Snowpark, que pode ser “iniciado” como um trabalho por meio de tarefas ou instruções SQL “CALL sproc” diretas.

Design de código

Os recursos principais do Snowpark Python podem ser pensados ​​em cinco grupos.

Princípios de design

  1. O princípio orientador para desenvolver recursos operacionais com o Snowpark Python é não extrair dados do Snowflake e processá-los em um ambiente de cliente sempre que possível . Em vez disso, aproveite a API de dataframe do cliente Snowpark para push-down de operações semelhantes a SQL e o tempo de execução do lado do servidor para implantar mais código arbitrário como UD(T)Fs que podem ser dimensionados em armazéns virtuais Snowflake e processar dados com mais eficiência. Isso pode ser considerado uma prática de programação semelhante ao envio de SQL via JDBC em outras ferramentas ou aplicativos.
  2. Use a API de dataframe do cliente Snowpark em seus aplicativos ao consultar e transformar dados, em vez de buscar todos os dados em um aplicativo cliente e processá-los lá. Isso pode escalar efetivamente suas transformações de dados, independentemente de todo o aplicativo estar sendo executado dentro do ambiente de tempo de execução Snowpark do lado do servidor ou em um ambiente externo ao Snowflake Python. Vale a pena notar que você pode usar o SQL diretamente em vez de usar a API do dataframe, no entanto, muitos desenvolvedores acham complicado tentar criar e usar o SQL dentro de um aplicativo escrito em outra linguagem, enquanto a API do dataframe fornece uma sintaxe Pythonic familiar para alcançar os mesmos resultados em escala.
  3. O código que manipula, analisa ou transforma dados deve, na medida do possível, ser construído como UD(T)Fs (a decisão entre UDF vs. UDTF se resume em grande parte à funcionalidade do que o código/transformação está realmente fazendo) ou deve ser implementado usando a API de dataframe do cliente Snowpark. Isso ocorre porque a API do dataframe e os UD(T)Fs permitem dimensionar e paralelizar o trabalho computacional que está sendo executado no armazém virtual.
  4. Python Sprocs são mais adequados para controlar o fluxo de programas Python. Pense no sproc como o script ou aplicativo principal - ele inicializa objetos, mantém o estado, etc. Dentro do sproc, qualquer computação intensiva de dados deve chamar a API do dataframe ou usar UD(T)Fs que foram construídos e implantados separadamente. Semelhante ao princípio acima de “não extrair dados para o cliente”, geralmente você deve evitar puxar dados para a memória do procedimento armazenado, pois isso limita sua capacidade de dimensionamento. Em vez disso, envie o trabalho executado no sproc para o armazém virtual usando a API de dataframe e UD(T)Fs. O sproc pode ser pensado como uma “unidade” de trabalho em seu aplicativo, que você pode orquestrar usando tarefas ou um serviço de orquestração externo (por exemplo, Airflow).
  5. Apenas para enfatizar novamente: você deve evitar puxar dados diretamente para o Sproc ao máximo possível. Os Sprocs estão restritos à execução em um único nó no armazém virtual e, portanto, estão restritos aos recursos computacionais desse nó. Os armazéns otimizados para o Snowpark (agora em Public Preview) estenderão a capacidade dos sprocs de executar mais trabalhos com uso intensivo de memória/dados, mas, como regra geral, os sprocs não devem ser encarregados de realizar cálculos significativos sozinhos e, dentro de um sproc, você deve descarregar o trabalho para a API do Snowpark e/ou UD(T)Fs.
  6. A exceção ao princípio 5 é a computação que não pode ser realizada de maneira distribuída e requer acesso a um conjunto de dados inteiro (ou grande amostra) para ser executada. Por exemplo, o treinamento do modelo de aprendizado de máquina de nó único geralmente deve ser executado no contexto de um procedimento armazenado, mas este é um exemplo raro em que a computação intensiva de dados deve ser executada em um procedimento armazenado. Sem entrar em muitos detalhes, pode haver casos de uso em torno do treinamento de modelo especificamente onde a paralelização via UD(T)Fs faz sentido, mas esse é um conjunto pequeno e específico de casos de uso.
  7. Os princípios padrão de design de código Python devem ser seguidos em relação à modularidade do código, reutilização etc. potencialmente aproveitado em todo o seu ecossistema de aplicativos Snowpark.
  8. Classes e objetos personalizados que você usa em todo o aplicativo precisam ser avaliados quanto à compatibilidade com UD(T)Fs. Por exemplo: suponha que você tenha uma parte de seu aplicativo que receba um monte de dados, faça algumas análises neles e construa um objeto de classe Python personalizado com base na saída de sua análise. Isso é potencialmente um bom ajuste para um UDTF, no entanto, não oferecemos suporte ao retorno de objetos Python arbitrários das funções do Snowpark. Como tal, você precisará considerar métodos de serialização/desserialização de seu objeto para tipos de dados SQL suportados, por exemplo, implementando construtores/serializadores to_json() e from_json() para que você possa inicializar seus objetos de classe a partir dos dados da tabela Snowflake. Você também pode considerar a serialização binária - independentemente da abordagem, isso precisa ser considerado.
  9. As instâncias de UD(T)Fs serão reutilizadas em um único conjunto de consultas. Você pode tirar proveito disso movendo o código de inicialização ou variáveis/estado temporário que podem ser reutilizados em execuções fora do método de função e na parte global/estática do código. Isso significa que, em execuções subsequentes, essas variáveis ​​ou estado temporário podem ser reutilizados sem a necessidade de reinicializar toda e qualquer execução. Isso será especialmente importante quando coisas como acesso externo se tornarem disponíveis, onde você desejará colocar clientes HTTP ou pools de conexão fora da declaração de função (consulte AWS Lambda/Azure Functions para obter padrões de design e práticas recomendadas semelhantes).
  10. UDFs vetorizados (lote) permitem que você execute as mesmas operações que UDFs, aproveitando as próprias otimizações baseadas em matriz internas do Python em objetos do tipo Pandas e Numpy (isso é descrito com mais detalhes acima). Como regra geral, geralmente é uma prática recomendada implantar UDFs como UDFs vetorizadas se qualquer uma das operações subjacentes nos dados depender de Numpy/Pandas ou for implementada usando operações vetoriais. Isso simplesmente otimiza a distribuição de dados para o tempo de execução do lado do servidor e permite que o Snowpark aproveite as otimizações integradas do Python.
  11. Além das próprias camadas de cache do Snowflake, o código Python do Snowpark geralmente deve seguir as práticas recomendadas de cache do Python, com exceção da necessidade de armazenar em cache os resultados da consulta (já que o Snowflake fará isso para você em várias camadas da arquitetura). Mas, por exemplo, se você tem uma UDF que realiza tokenização de texto em sua aplicação, e para utilizá-la você precisa carregar um tokenizer construído do stage, você deve agrupar uma função que carregue o tokenizer na UDF usando cachetools, para evitar carregando o tokenizer do palco para o UDF. Este é o caso de uso mais comum para armazenamento em cache no código do Snowpark - quando um artefato precisa ser carregado em um UD(T)F a partir do palco.

Portabilidade de aplicativos existentes para o Snowpark

Muitos clientes têm se perguntado como podem portar recursos e aplicativos existentes para serem executados no Snowpark, tanto para melhorar a escalabilidade, desempenho, governança e segurança quanto simplificar sua arquitetura e remover a complexidade associada à propriedade e gerenciamento da infraestrutura. Além da avaliação inicial de “este aplicativo e/ou código pode ser suportado pelo Snowpark”, deve haver uma extensa discussão e conversa sobre como mover o código da maneira mais otimizada. No caso de migrar aplicativos PySpark especificamente para o Snowpark (em comparação com outros aplicativos Python mais genéricos), o Snowflake fez uma parceria com a Mobilize.netpara fornecer uma ferramenta gratuita de análise de código PySpark para Snowpark que pode ajudar a determinar se sua base de código existente é uma boa candidata para migração para Snowpark. Além disso, as equipes de serviços profissionais da Snowflake e os parceiros SI podem oferecer suporte total à migração.

No caso de aplicativos Python mais genéricos, os scripts Python podemapenas serão colocados em sprocs do Python e provavelmente mais ou menos "funcionarão", mas é provável que seja uma implementação extremamente ineficiente, a menos que o script específico seja computacionalmente leve. Em particular, esses aplicativos existentes provavelmente estão extraindo dados do Snowflake, que, como enfatizamos acima, não é o padrão recomendado para criar aplicativos no Snowpark. As duas questões principais a serem consideradas são (1) como o código que extrai dados do Snowflake precisa ser refatorado para aproveitar mais push-down de computação. Além disso, (2) Aplicativos intensivos em dados precisam ser avaliados especialmente para determinar quais funções, métodos, classes etc. são mais bem atendidos aproveitando Python UD(T)Fs e aproveitando a escalabilidade e o desempenho em armazéns virtuais.

Além da pura análise de código, outra abordagem para realizar essa avaliação de migração é começar com uma base de código e produzir um diagrama de fluxo lógico do aplicativo/script, dividido em componentes computacionais individuais. Muitas equipes já podem ter esse tipo de diagrama de design/arch de software disponível, mas, se não, é um bom começo para entender como os dados e o trabalho computacional são distribuídos em seu aplicativo. Esse diagrama também pode incluir objetos de classe personalizados, etc. Para cada função, classe, método usado, você deve avaliar quais dados devem ser fornecidos e qual saída é produzida. A saída é compatível com os tipos de dados Snowflake? O que consumirá a produção e como ela será usada? A computação realizada é intensa o suficiente para justificar a execução em um cluster de nós de computação em um armazém virtual? Quanta configuração personalizada é necessária em cada iteração do uso da função/objeto?

Ao preencher este diagrama, começará a ficar claro o que deve existir na camada de fluxo lógico do aplicativo (que pode ser uma boa opção para um Snowpark Python sproc) e o que precisa ser descarregado para um UD(T)F e, portanto, potencialmente refatorado. Isso também indicará quais classes e objetos personalizados precisam ter métodos de serialização para gravar/inicializar a partir da estrutura da tabela do Snowpark, conforme descrito nos princípios de design acima.

A partir deste ponto, você pode começar a entender qual código pode ser levantado e alterado, versus o que precisa ser refatorado, reimplementado ou modificado. Além disso, o Snowflake Professional Services oferece mais assistência prática na migração de aplicativos para o Snowpark, que vai além dos princípios e práticas recomendadas.

Conclusão

Nesta postagem, detalhamos o que é o Snowpark Python (API de dataframe do cliente e tempo de execução do lado do servidor) e como ele pode ser melhor usado em aplicativos Python. Descrevemos os princípios básicos de design que devem ser seguidos ao projetar ou migrar aplicativos para o Snowpark Python. Na parte dois, veremos como incorporar esses novos recursos às práticas existentes de CI/CD e DevOps, incluindo como o Snowpark Python pode ser semelhante e bastante diferente de outras estruturas de desenvolvimento.