Pular para o conteúdo principal

Considerações sobre desempenho para políticas ABAC

As políticas de filtro de linha e máscara de coluna introduzem lógica que é executada no momento da consulta, portanto, o desempenho depende de como você projeta suas políticas. Não existe uma única abordagem correta para todas as cargas de trabalho. A melhor abordagem depende do volume de dados, dos padrões de consulta, da forma como os usuários interagem com as tabelas protegidas e do comportamento de mascaramento ou filtragem desejado. As seções a seguir abordam as considerações de desempenho mais comuns. Use-as como uma lista de verificação ao projetar suas políticas e teste com consultas representativas antes de implantá-las em produção.

Visão geral do desempenho

Consideração

Descrição

Reduzir a complexidade UDF

A lógica complexa UDF pode prejudicar o desempenho das consultas; funções simples têm melhor desempenho.

Abordagem para direcionar os diretores

Decida se deve implementar a lógica baseada em princípios nas cláusulas TO/EXCEPT da política ou dentro da UDF usando funções de identidade.

Utilize expressões determinísticas e à prova de erros.

Funções e expressões não determinísticas que podem gerar erros reduzem a capacidade do otimizador de armazenar resultados em cache e reordenar operações.

Evite UDFs em Python

Sempre que possível, utilize UDFs em SQL em vez de UDFs em Python.

Mantenha as tabelas de consulta pequenas

As UDFs que fazem referência a tabelas externas têm melhor desempenho quando essas tabelas são pequenas o suficiente para serem transmitidas (broadcast).

Entenda o pushdown de predicados em tabelas protegidas.

Consultas em tabelas protegidas podem não se beneficiar da eliminação de partições ou clustering líquido se os predicados tiverem efeitos colaterais.

Reutilize as máscaras de coluna sempre que possível.

Cada máscara distinta em uma tabela adiciona sobrecarga; reutilizar a mesma função em várias colunas pode reduzi-la.

Evite mascaramento por expressões regulares em campos de texto grandes.

A aplicação de máscaras baseadas em expressões regulares a documentos serializados força o mecanismo a analisar e reescrever toda a carga útil de cada linha.

Reduzir a complexidade UDF

A UDF em uma política ABAC é executada para cada linha (filtros de linha) ou para cada valor de coluna correspondente (máscaras de coluna) durante a execução da consulta. A complexidade da UDF afeta diretamente o desempenho da consulta.

Fazer:

  • Mantenha as UDFs simples. Dê preferência a declarações básicas CASE e expressões booleanas simples.
  • Utilize as UDFs (funções definidas pelo usuário) como referência apenas para colunas da tabela de destino, sempre que possível. Isso possibilita o pushdown de predicados.
  • Se sua UDF precisar fazer referência a tabelas externas, mantenha qualquer referência externa pequena o suficiente para ser transmitida. Garanta que as tabelas referenciadas estejam otimizadas e particionadas para corresponder ao padrão de acesso da política. Por exemplo, particione uma tabela de pesquisa de políticas por nome de usuário.
  • Evite aninhamento em vários níveis e chamadas de função desnecessárias. Utilize funções SQL integradas sempre que possível.

Evitar:

  • Chamadas de API externas ou consultas a outros bancos de dados em UDFs. As chamadas de rede podem introduzir latência adicional e tempos limite.
  • Subconsultas complexas ou junções em tabelas grandes. Essas opções impedem a junção por hash de transmissão e forçam a junção por loop aninhado.
  • Uso complexo de expressões regulares em campos de texto extensos. Veja Expressões regulares em campos de texto grandes.
  • Pesquisas de metadados por linha, por exemplo, consultando information_schema.

Abordagem para direcionar os diretores

Ao escrever uma política ABAC, você decide onde implementar a lógica baseada em principal: nas cláusulas TO/EXCEPT da política ou dentro da UDF usando funções de identidade como current_user() e is_account_group_member().

Em geral, use as cláusulas TO/EXCEPT da política para definir a quais principais uma política se aplica. Isso simplifica a definição da política e mantém a UDF focada em transformações de dados, filtragem ou mascaramento. A cláusula EXCEPT elimina completamente a política para usuários isentos, o que significa que não haverá execução de UDF para esses usuários.

Quando a lógica condicional é muito complexa para as cláusulas principais da política, as funções de identidade dentro da UDF (Função Definida pelo Usuário) são uma possível alternativa. Essas funções são resolvidas uma única vez durante a análise da consulta, e não por linha. Múltiplas chamadas a funções de identidade como is_account_group_member() com diferentes argumentos de grupo resultam em uma única chamada à API UC, portanto, o impacto no desempenho é normalmente mínimo.

A seguinte UDF é eficiente porque depende apenas de funções de identidade, que são resolvidas uma única vez durante a análise da consulta:

SQL
CREATE OR REPLACE FUNCTION rowfilter()
RETURNS BOOLEAN
RETURN
CASE
WHEN is_account_group_member('auditors') OR is_account_group_member('external-auditors') THEN true
WHEN is_account_group_member('low-privileged') THEN false
WHEN session_user() = 'admin@organization.com' THEN true
ELSE false
END;

Em contrapartida, a seguinte UDF é mais lenta porque codifica privilégios em uma tabela secundária, o que requer uma consulta adicional à tabela:

SQL
CREATE OR REPLACE FUNCTION rowfilter()
RETURNS BOOLEAN
RETURN
CASE WHEN EXISTS(SELECT 1 FROM access_lease WHERE user = session_user()) THEN true
ELSE false END;

Utilize expressões determinísticas e à prova de erros.

Utilize expressões determinísticas que não possam gerar erros em UDFs de política e em consultas contra tabelas protegidas.

Funções não determinísticas (funções que retornam resultados diferentes para a mesma entrada, como rand() ou now()) impedem o otimizador de armazenar resultados em cache ou aplicar a redução de constantes. Tanto as UDFs SQL quanto as Python suportam a palavra-chave DETERMINISTIC na instrução CREATE FUNCTION . Para UDFs SQL, o otimizador deriva o determinismo do corpo da função automaticamente, mas você também pode defini-lo explicitamente. Para UDFs em Python, o otimizador não consegue inspecionar o corpo da função, portanto, marcar explicitamente uma UDF em Python como determinística é importante para habilitar o armazenamento em cache de resultados para chamadas com argumentos idênticos.

Algumas expressões geram erros se as entradas não forem válidas, como a divisão ANSI com denominador zero. Quando o compilador SQL detecta essa possibilidade, ele não consegue adiar operações como filtros no plano de consulta. Fazer isso pode desencadear erros que revelem informações sobre valores antes que a filtragem ou o mascaramento entrem em vigor. Use alternativas seguras contra erros, como try_divide em vez de /, try_cast em vez de CAST e try_to_number em vez de to_number. Esses retornam NULL em caso de falha em vez de lançar uma exceção, o que permite que o otimizador reorganize e recolha expressões livremente.

Evite UDFs em Python

Evite, sempre que possível, o uso de UDFs em Python nas políticas ABAC. As UDFs em Python devem ser encapsuladas em uma UDF em SQL para serem usadas em políticas. Elas também costumam ser mais lentas do que as UDFs SQL porque o otimizador não consegue incorporá-las ou otimizá-las, e a função Python é executada para cada linha na tabela de destino.

Se uma UDF em Python for inevitável, consulte Expressões determinísticas e seguras contra erros para saber como marcá-la como DETERMINISTIC para habilitar o cache de resultados.

Mantenha as tabelas de consulta pequenas

Um padrão comum é verificar os direitos de acesso em relação a uma pequena tabela de consulta (por exemplo, uma tabela que mapeia usuários para níveis de prioridade permitidos). Se a tabela de pesquisa for significativamente menor que a tabela de destino, o otimizador converte a subconsulta em uma join hash de broadcast. A tabela de consulta é copiada para cada executor e armazenada na memória como um mapa hash, o que permite uma filtragem rápida durante a varredura da tabela. Para um exemplo de código, consulte Tabelas de pesquisa em UDFs de política ABAC.

  • Se a tabela de consulta for grande, o otimizador recorre a uma junção aleatória (shuffle join), que é mais lenta.
  • Se o predicado de pesquisa for complexo (e não uma simples verificação de igualdade), join por transmissão também poderá se tornar inelegível.
  • Mesmo com join hash de transmissão, cada linha ainda incorre no custo de uma consulta à tabela hash durante a execução.

Entenda o pushdown de predicados em tabelas protegidas.

O pushdown de predicados é uma otimização de desempenho na qual o mecanismo envia as condições do filtro para a camada de armazenamento. Isso permite que o mecanismo ignore partições inteiras de dados que não correspondem à sua consulta, reduzindo significativamente a E/S e acelerando a execução.

Para tabelas protegidas por filtros de linha e máscaras de coluna, essa otimização é mais complexa. Essa é a causa mais comum de problemas de desempenho com tabelas protegidas e a mais difícil de resolver, porque os autores das políticas não podem controlar quais consultas os usuários executam em tabelas protegidas.

Como a barreira SecureView afeta o pushdown de predicados

Tanto o ABAC quanto os filtros de linha e as máscaras de coluna em nível de tabela usam uma barreira SecureView para impedir que predicados com efeitos colaterais sejam enviados através do limite da política. Isso protege contra vazamento de dados por canais laterais, mas também pode bloquear a poda de partições e as otimizações clustering líquido, o que pode forçar varreduras completas da tabela. Isso se aplica mesmo quando a UDF da política resulta em uma constante true (o que significa que nenhuma linha é realmente filtrada). A presença de uma política em uma tabela introduz a barreira SecureView .

Filtros afetados pela barreira

Geralmente, o otimizador só pode passar pela barreira SecureView predicados sem efeitos colaterais.

  • Empurrado para baixo (rápido) : Comparações de igualdade simples (WHERE col = 'value') e comparações de intervalo básicas (WHERE col > 100). Esses produtos não apresentam efeitos colaterais e não oferecem risco de vazamento de dados.
  • Bloqueado (mais lento) : Predicados que chamam funções (WHERE date_format(col, 'yyyy-MM-dd') = '1995-07-29') ou introduzem conversões de tipo implícitas. Estes são mantidos acima da barreira SecureView , o que significa que o mecanismo deve examinar a tabela antes de aplicar o filtro.

O exemplo a seguir mostra a diferença. Considere uma tabela com uma key de partição em o_orderdate e uma consulta que filtra usando date_format:

SQL
EXPLAIN SELECT * FROM orders
WHERE date_format(o_orderdate, 'yyyy-MM-dd') = '1995-07-29'

Sem uma política, o predicado date_format aparece em PartitionFilters dentro do nó PhotonScan , o que significa que a poda de partições está ativa:

+- PhotonScan parquet orders[...]
PartitionFilters: [isnotnull(o_orderdate),
(date_format(cast(o_orderdate as timestamp), yyyy-MM-dd, ...))]

Com uma política (mesmo uma que sempre retorne true), a barreira SecureView bloqueia o predicado. Ele se move para um PhotonFilter acima da varredura em vez de permanecer em PartitionFilters, o que resulta em uma varredura completa da tabela:

+- PhotonFilter (date_format(cast(o_orderdate as timestamp),
yyyy-MM-dd, ...) = 1995-07-29)
+- PhotonSecureView orders
+- PhotonScan parquet orders[...]
PartitionFilters: [isnotnull(o_orderdate)]

Um predicado mais simples como WHERE o_orderdate = '1995-07-29' não tem efeitos colaterais e ainda pode ser rebaixado mesmo com a barreira SecureView em vigor:

+- PhotonSecureView orders
+- PhotonScan parquet orders[...]
PartitionFilters: [isnotnull(o_orderdate),
(o_orderdate = 1995-07-29)]

Use predicados de igualdade simples em tabelas protegidas sempre que possível. Para usuários isentos, use a cláusula EXCEPT na política para eliminar completamente a barreira SecureView , o que restaura o pushdown de predicado completo.

Reutilize as máscaras de coluna sempre que possível.

Aplicar várias máscaras de coluna distintas a uma única tabela aumenta o custo por coluna. Mascare apenas as colunas que contêm dados verdadeiramente sensíveis.

Quando várias colunas exigem as mesmas transformações (por exemplo, redigir para NULL ou substituir por strings fixas), reutilize a mesma função de mascaramento em vez de criar uma função separada para cada coluna.

O Databricks reconhece políticas que fazem referência à mesma UDF com os mesmos argumentos como a mesma máscara efetiva, portanto, a reutilização de funções evita sobrecarga desnecessária.

Evite mascaramento por expressões regulares em campos de texto grandes.

Usar regexp_replace dentro de uma máscara de coluna para ocultar elementos em um documento serializado (XML ou JSON armazenado como uma coluna de strings) é custoso. regexp_replace percorre as strings completas para cada linha. O otimizador trata a coluna de strings como um valor opaco e não consegue remover partes não utilizadas do documento. O mecanismo lê e reescreve toda a carga útil, mesmo quando a consulta requer apenas alguns campos.

SQL
-- Expensive: regex masking on serialized XML
CREATE FUNCTION mask_xml_pii(raw_xml STRING)
RETURNS STRING
RETURN CASE
WHEN is_account_group_member('sensitive_data_viewers') THEN raw_xml
ELSE regexp_replace(raw_xml, '<SSN>[^<]*</SSN>', '<SSN>***</SSN>')
END;

Em vez disso, materialize os campos sensíveis em colunas tipadas em uma tabela separada e, em seguida, aplique máscaras de coluna a essas colunas escalares. A função de máscara opera então em um único valor pequeno por linha, em vez de em todo o documento serializado.

SQL
-- Source table stores raw XML as STRING
-- Example XML: <person><SSN>123-45-6789</SSN><name>Alice</name><dob>1990-01-01</dob></person>

-- Recommended: extract fields into a table, then mask scalar values
CREATE TABLE person_data AS
SELECT
id,
xpath_string(raw_xml, 'person/SSN') AS ssn,
xpath_string(raw_xml, 'person/name') AS name,
xpath_string(raw_xml, 'person/dob') AS date_of_birth,
raw_xml
FROM raw_records;

-- Simple scalar mask, applied to each extracted column
CREATE FUNCTION redact(val STRING) RETURNS STRING
RETURN CASE
WHEN is_account_group_member('sensitive_data_viewers') THEN val
ELSE '***'
END;

Se você puder armazenar os dados como uma coluna de estrutura em vez de XML, use o padrão de mascaramento flexível VARIANT para ocultar campos individuais dentro da estrutura. Consulte Colunas de estrutura de máscara com VARIANT.

Teste o desempenho da UDF

Teste em escala

Teste o desempenho UDF em pelo menos 1 milhão de linhas antes de implantá-la em produção. Além dos testes de escalonamento sintéticos, execute consultas que representem a carga de trabalho real esperada na tabela protegida. Faça alterações incrementais nas funções de suas políticas e meça o efeito de cada alteração, em vez de testar apenas a versão final.

SQL
WITH test_data AS (
SELECT
id,
your_mask_function(id) AS masked_id,
current_timestamp() AS ts
FROM (
SELECT CONCAT('ID', LPAD(CAST(id AS STRING), 6, '0')) AS id
FROM range(1000000)
)
)
SELECT
COUNT(*) AS rows_processed,
MAX(ts) - MIN(ts) AS total_duration
FROM test_data;

Substitua your_mask_function pela UDF que você está testando. Compare os resultados com e sem a política aplicada para isolar a sobrecarga da política.