Utilización de reglas Sigma para la detección de anomalías en registros de ciberseguridad: un estudio sobre optimización del rendimiento
Uno de los roles del Centro Canadiense de Seguridad Cibernética (CCCS) es detectar anomalías y emitir mitigaciones lo más rápido posible.
Mientras poníamos en producción nuestras detecciones de reglas Sigma, hicimos una observación interesante en nuestra aplicación de transmisión Spark. Ejecutar una sola declaración SQL grande que expresara reglas de detección de 1000 Sigma era más lento que ejecutar cinco consultas separadas, cada una de las cuales aplicaba reglas de 200 Sigma. Esto fue sorprendente, ya que ejecutar cinco consultas obliga a Spark a leer los datos de origen cinco veces en lugar de una. Para obtener más detalles, consulte nuestra serie de artículos:
Dada la gran cantidad de datos de telemetría y reglas de detección que debemos ejecutar, cada aumento en el rendimiento genera importantes ahorros de costos. Por lo tanto, decidimos investigar esta observación peculiar, con el objetivo de explicarla y potencialmente descubrir oportunidades adicionales para mejorar el desempeño. Aprendimos algunas cosas a lo largo del camino y queríamos compartirlas con la comunidad en general.
Introducción
Nuestra corazonada era que estábamos alcanzando un límite en la generación de código de Spark. Por lo tanto, se requiere un poco de historia sobre este tema. En 2014, Spark introdujo la generación de código para evaluar expresiones de la forma (id > 1 and id > 2) and (id < 1000 or (id + id) = 12)
. Este artículo de Databricks lo explica muy bien: Emocionantes mejoras de rendimiento en el horizonte para Spark SQL
Dos años más tarde, Spark introdujo la generación de código en toda la etapa. Esta optimización fusiona varios operadores en una única función Java. Al igual que la generación de código de expresión, la generación de código de etapa completa elimina las llamadas a funciones virtuales y aprovecha los registros de la CPU para datos intermedios. Sin embargo, en lugar de estar a nivel de expresión, se aplica a nivel de operador. Los operadores son los nodos de un plan de ejecución. Para saber más, lea Apache Spark como compilador: unir mil millones de filas por segundo en una computadora portátil
Para resumir estos artículos, generemos el plan para esta consulta simple:
explain codegen
select
id,
(id > 1 and id > 2) and (id < 1000 or (id + id) = 12) as test
from
range(0, 10000, 1, 32)
En esta consulta simple, usamos dos operadores: Rango para generar filas y Seleccionar para realizar una proyección. Vemos estos operadores en el plano físico de la consulta. Note el asterisco (codegen id : 1)
junto a los nodos y sus asociados
|== Physical Plan ==
* Project (2)
+- * Range (1)(1) Range (codegen id : 1)
Output (1): (id#36167L)
Arguments: Range (0, 10000, step=1, splits=Some(32))
(2) Project (codegen id : 1)
Output (2): (id#36167L, (((id#36167L > 1) AND (id#36167L > 2)) AND ((id#36167L < 1000) OR ((id#36167L + id#36167L) = 12))) AS test#36161)
Input (1): (id#36167L)
. Esto indica que estos dos operadores se fusionaron en una única función Java mediante la generación de código de etapa completa.
Generated code:
/* 001 */ public Object generate(Object() references) {
/* 002 */ return new GeneratedIteratorForCodegenStage1(references);
/* 003 */ }
/* 004 */
/* 005 */ // codegenStageId=1
/* 006 */ final class GeneratedIteratorForCodegenStage1 extends org.apache.spark.sql.execution.BufferedRowIterator {
/* 007 */ private Object() references;
/* 008 */ private scala.collection.Iterator() inputs;
/* 009 */ private boolean range_initRange_0;
/* 010 */ private long range_nextIndex_0;
/* 011 */ private TaskContext range_taskContext_0;
/* 012 */ private InputMetrics range_inputMetrics_0;
/* 013 */ private long range_batchEnd_0;
/* 014 */ private long range_numElementsTodo_0;
/* 015 */ private org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter() range_mutableStateArray_0 = new org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter(3);
/* 016 */
/* 017 */ public GeneratedIteratorForCodegenStage1(Object() references) {
/* 018 */ this.references = references;
/* 019 */ }
/* 020 */
/* 021 */ public void init(int index, scala.collection.Iterator() inputs) {
/* 022 */ partitionIndex = index;
/* 023 */ this.inputs = inputs;
/* 024 */
/* 025 */ range_taskContext_0 = TaskContext.get();
/* 026 */ range_inputMetrics_0 = range_taskContext_0.taskMetrics().inputMetrics();
/* 027 */ range_mutableStateArray_0(0) = new org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter(1, 0);
/* 028 */ range_mutableStateArray_0(1) = new org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter(1, 0);
/* 029 */ range_mutableStateArray_0(2) = new org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter(2, 0);
/* 030 */
/* 031 */ }
/* 032 */
/* 033 */ private void project_doConsume_0(long project_expr_0_0) throws java.io.IOException {
/* 034 */ // common sub-expressions
/* 035 */
/* 036 */ boolean project_value_4 = false;
/* 037 */ project_value_4 = project_expr_0_0 > 1L;
/* 038 */ boolean project_value_3 = false;
/* 039 */
/* 040 */ if (project_value_4) {
/* 041 */ boolean project_value_7 = false;
/* 042 */ project_value_7 = project_expr_0_0 > 2L;
/* 043 */ project_value_3 = project_value_7;
/* 044 */ }
/* 045 */ boolean project_value_2 = false;
/* 046 */
/* 047 */ if (project_value_3) {
/* 048 */ boolean project_value_11 = false;
/* 049 */ project_value_11 = project_expr_0_0 < 1000L;
/* 050 */ boolean project_value_10 = true;
/* 051 */
/* 052 */ if (!project_value_11) {
/* 053 */ long project_value_15 = -1L;
/* 054 */
/* 055 */ project_value_15 = project_expr_0_0 + project_expr_0_0;
/* 056 */
/* 057 */ boolean project_value_14 = false;
/* 058 */ project_value_14 = project_value_15 == 12L;
/* 059 */ project_value_10 = project_value_14;
/* 060 */ }
/* 061 */ project_value_2 = project_value_10;
/* 062 */ }
/* 063 */ range_mutableStateArray_0(2).reset();
/* 064 */
/* 065 */ range_mutableStateArray_0(2).write(0, project_expr_0_0);
/* 066 */
/* 067 */ range_mutableStateArray_0(2).write(1, project_value_2);
/* 068 */ append((range_mutableStateArray_0(2).getRow()));
/* 069 */
/* 070 */ }
/* 071 */
/* 072 */ private void initRange(int idx) {
/* 073 */ java.math.BigInteger index = java.math.BigInteger.valueOf(idx);
/* 074 */ java.math.BigInteger numSlice = java.math.BigInteger.valueOf(32L);
/* 075 */ java.math.BigInteger numElement = java.math.BigInteger.valueOf(10000L);
/* 076 */ java.math.BigInteger step = java.math.BigInteger.valueOf(1L);
/* 077 */ java.math.BigInteger start = java.math.BigInteger.valueOf(0L);
/* 078 */ long partitionEnd;
/* 079 */
/* 080 */ java.math.BigInteger st = index.multiply(numElement).divide(numSlice).multiply(step).add(start);
/* 081 */ if (st.compareTo(java.math.BigInteger.valueOf(Long.MAX_VALUE)) > 0) {
/* 082 */ range_nextIndex_0 = Long.MAX_VALUE;
/* 083 */ } else if (st.compareTo(java.math.BigInteger.valueOf(Long.MIN_VALUE)) < 0) {
/* 084 */ range_nextIndex_0 = Long.MIN_VALUE;
/* 085 */ } else {
/* 086 */ range_nextIndex_0 = st.longValue();
/* 087 */ }
/* 088 */ range_batchEnd_0 = range_nextIndex_0;
/* 089 */
/* 090 */ java.math.BigInteger end = index.add(java.math.BigInteger.ONE).multiply(numElement).divide(numSlice)
/* 091 */ .multiply(step).add(start);
/* 092 */ if (end.compareTo(java.math.BigInteger.valueOf(Long.MAX_VALUE)) > 0) {
/* 093 */ partitionEnd = Long.MAX_VALUE;
/* 094 */ } else if (end.compareTo(java.math.BigInteger.valueOf(Long.MIN_VALUE)) < 0) {
/* 095 */ partitionEnd = Long.MIN_VALUE;
/* 096 */ } else {
/* 097 */ partitionEnd = end.longValue();
/* 098 */ }
/* 099 */
/* 100 */ java.math.BigInteger startToEnd = java.math.BigInteger.valueOf(partitionEnd).subtract(
/* 101 */ java.math.BigInteger.valueOf(range_nextIndex_0));
/* 102 */ range_numElementsTodo_0 = startToEnd.divide(step).longValue();
/* 103 */ if (range_numElementsTodo_0 < 0) {
/* 104 */ range_numElementsTodo_0 = 0;
/* 105 */ } else if (startToEnd.remainder(step).compareTo(java.math.BigInteger.valueOf(0L)) != 0) {
/* 106 */ range_numElementsTodo_0++;
/* 107 */ }
/* 108 */ }
/* 109 */
/* 110 */ protected void processNext() throws java.io.IOException {
/* 111 */ // initialize Range
/* 112 */ if (!range_initRange_0) {
/* 113 */ range_initRange_0 = true;
/* 114 */ initRange(partitionIndex);
/* 115 */ }
/* 116 */
/* 117 */ while (true) {
/* 118 */ if (range_nextIndex_0 == range_batchEnd_0) {
/* 119 */ long range_nextBatchTodo_0;
/* 120 */ if (range_numElementsTodo_0 > 1000L) {
/* 121 */ range_nextBatchTodo_0 = 1000L;
/* 122 */ range_numElementsTodo_0 -= 1000L;
/* 123 */ } else {
/* 124 */ range_nextBatchTodo_0 = range_numElementsTodo_0;
/* 125 */ range_numElementsTodo_0 = 0;
/* 126 */ if (range_nextBatchTodo_0 == 0) break;
/* 127 */ }
/* 128 */ range_batchEnd_0 += range_nextBatchTodo_0 * 1L;
/* 129 */ }
/* 130 */
/* 131 */ int range_localEnd_0 = (int)((range_batchEnd_0 - range_nextIndex_0) / 1L);
/* 132 */ for (int range_localIdx_0 = 0; range_localIdx_0 < range_localEnd_0; range_localIdx_0++) {
/* 133 */ long range_value_0 = ((long)range_localIdx_0 * 1L) + range_nextIndex_0;
/* 134 */
/* 135 */ project_doConsume_0(range_value_0);
/* 136 */
/* 137 */ if (shouldStop()) {
/* 138 */ range_nextIndex_0 = range_value_0 + 1L;
/* 139 */ ((org.apache.spark.sql.execution.metric.SQLMetric) references(0) /* numOutputRows */).add(range_localIdx_0 + 1);
/* 140 */ range_inputMetrics_0.incRecordsRead(range_localIdx_0 + 1);
/* 141 */ return;
/* 142 */ }
/* 143 */
/* 144 */ }
/* 145 */ range_nextIndex_0 = range_batchEnd_0;
/* 146 */ ((org.apache.spark.sql.execution.metric.SQLMetric) references(0) /* numOutputRows */).add(range_localEnd_0);
/* 147 */ range_inputMetrics_0.incRecordsRead(range_localEnd_0);
/* 148 */ range_taskContext_0.killTaskIfInterrupted();
/* 149 */ }
/* 150 */ }
/* 151 */
/* 152 */ }
El código generado muestra claramente la fusión de los dos operadores. project_doConsume_0
El (id > 1 and id > 2) and (id < 1000 or (id + id) = 12)
La función contiene el código a evaluar.
. Observe cómo se genera este código para evaluar esta expresión específica. Esta es una ilustración de la generación de código de expresión. processNext
Toda la clase es un operador con un project_doConsume_0
método. Este operador generado realiza las operaciones de Proyección y Rango. Dentro del bucle while en la línea 117, vemos el código para producir filas y una llamada específica (no una función virtual) a
. Esto ilustra lo que hace la generación de código de etapa completa.
Desglosando el rendimiento Imagepath
Ahora que comprendemos mejor la generación de código de Spark, intentemos explicar por qué dividir una consulta que realiza reglas de 1000 Sigma en otras más pequeñas funciona mejor. Consideremos una declaración SQL que evalúa dos reglas Sigma. Estas reglas son sencillas: la regla 1 relaciona eventos con un Imagepath
termina en 'schtask.exe' y Rule2 coincide con un
select /* #3 */
Imagepath,
CommandLine,
PID,
map_keys(map_filter(results_map, (k,v) -> v = TRUE)) as matching_rules
from (
select /* #2 */
*,
map('rule1', rule1, 'rule2', rule2) as results_map
from (
select /* #1 */
*,
(lower_Imagepath like '%schtasks.exe') as rule1,
(lower_Imagepath like 'd:%') as rule2
from (
select
lower(PID) as lower_PID,
lower(CommandLine) as lower_CommandLine,
lower(Imagepath) as lower_Imagepath,
*
from (
select
uuid() as PID,
uuid() as CommandLine,
uuid() as Imagepath,
id
from
range(0, 10000, 1, 32)
)
)
)
)
comenzando con 'd:'. results_map
La selección etiquetada como #1 realiza las detecciones y almacena los resultados en nuevas columnas denominadas regla1 y regla2. La selección n.º 2 reagrupa estas columnas en una sola map_filter
y finalmente seleccionar el n.° 3 transforma el mapa en una serie de reglas coincidentes. Usa map_keys
para mantener sólo las entradas de las reglas que realmente coincidieron, y luego
se utiliza para convertir las entradas del mapa en una lista de nombres de reglas coincidentes.
== Physical Plan ==
Project (4)
+- * Project (3)
+- * Project (2)
+- * Range (1)...
(4) Project
Output (4): (Imagepath#2, CommandLine#1, PID#0, map_keys(map_filter(map(rule1, EndsWith(lower_Imagepath#5, schtasks.exe), rule2, StartsWith(lower_Imagepath#5, d:)), lambdafunction(lambda v#12, lambda k#11, lambda v#12, false))) AS matching_rules#9)
Input (4): (lower_Imagepath#5, PID#0, CommandLine#1, Imagepath#2)
Imprimamos el plan de ejecución de Spark para esta consulta:
Observe que el nodo Proyecto (4) no es código generado. El nodo 4 tiene una función lambda, ¿impide la generación de código de etapa completa? Más sobre esto más adelante.
+--------------------+--------------------+--------------------+--------------+
| Imagepath| CommandLine| PID| matched_rule|
+--------------------+--------------------+--------------------+--------------+
|09401675-dc09-4d0...|6b8759ee-b55a-486...|44dbd1ec-b4e0-488...| rule1|
|e2b4a0fd-7b88-417...|46dd084d-f5b0-4d7...|60111cf8-069e-4b8...| rule1|
|1843ee7a-a908-400...|d1105cec-05ef-4ee...|6046509a-191d-432...| rule2|
+--------------------+--------------------+--------------------+--------------+
Esta consulta no es exactamente lo que queremos. Nos gustaría generar una tabla de eventos con una columna que indique la regla que coincidió. Algo como esto: matching_rules
Eso es bastante fácil. Sólo necesitamos explotar el
select
Imagepath,
CommandLine,
PID,
matched_rule
from (
select
*,
explode(matching_rules) as matched_rule
from (
/* original statement */
)
)
columna.
== Physical Plan ==
* Project (7)
+- * Generate (6)
+- Project (5)
+- * Project (4)
+- Filter (3)
+- * Project (2)
+- * Range (1)...
(3) Filter
Input (3): (PID#34, CommandLine#35, Imagepath#36)
Condition : (size(map_keys(map_filter(map(rule1, EndsWith(lower(Imagepath#36),
schtasks.exe), rule2, StartsWith(lower(Imagepath#36), d:)),
lambdafunction(lambda v#47, lambda k#46, lambda v#47, false))), true) > 0)
...
(6) Generate (codegen id : 3)
Input (4): (PID#34, CommandLine#35, Imagepath#36, matching_rules#43)
Arguments: explode(matching_rules#43), (PID#34, CommandLine#35, Imagepath#36), false, (matched_rule#48)
(7) Project (codegen id : 3)
Output (4): (Imagepath#36, CommandLine#35, PID#34, matched_rule#48)
Input (4): (PID#34, CommandLine#35, Imagepath#36, matched_rule#48)
Esto produce dos operadores adicionales: Generar (6) y Proyecto (7). Sin embargo, también hay un nuevo Filtro (3). explode
El explode
La función genera filas para cada elemento de la matriz. Cuando la matriz está vacía,
no produce ninguna fila, filtrando efectivamente las filas donde la matriz está vacía. org.apache.spark.sql.catalyst.optimizer.InferFiltersFromGenerate
Spark tiene una regla de optimización que detecta la función de explosión y produce esta condición adicional. El filtro es un intento de Spark de cortocircuitar el procesamiento tanto como sea posible. El código fuente de esta regla, llamado
lo explica así:
Infiere filtros de Generar, de modo que las filas que se habrían eliminado mediante este Generar se pueden eliminar antes, antes de las uniones y en las fuentes de datos.
Para obtener más detalles sobre cómo Spark optimiza los planes de ejecución, consulte el artículo de David Vrba Mastering Query Plans in Spark 3.0.
Surge otra pregunta: ¿nos beneficiamos de este filtro adicional? Tenga en cuenta que este filtro adicional tampoco es un código de etapa completa generado, presumiblemente debido a la función lambda. Intentemos expresar la misma consulta pero sin utilizar una función lambda. map_filter
En su lugar, podemos poner los resultados de la regla en un mapa, expandir el mapa y filtrar las filas, evitando así la necesidad de
select
Imagepath,
CommandLine,
PID,
matched_rule
from (
select
*
from (
select
*,
explode(results_map) as (matched_rule, matched_result)
from (
/* original statement */
)
)
where
matched_result = TRUE
)
. matched_rule
La operación de selección n.º 3 explota el mapa en dos nuevas columnas. El matched_result
La columna contendrá la clave, que representa el nombre de la regla, mientras que la matched_result
La columna contendrá el resultado de la prueba de detección. Para filtrar las filas, simplemente mantenemos sólo aquellas con un valor positivo.
.
== Physical Plan ==
* Project (8)
+- * Filter (7)
+- * Generate (6)
+- * Project (5)
+- * Project (4)
+- * Filter (3)
+- * Project (2)
+- * Range (1)
El plan físico indica que todos los nodos son código de etapa completa generado en una única función Java, lo cual es prometedor. map_filter
Realicemos algunas pruebas para comparar el rendimiento de la consulta utilizando
y el que usa explotar y luego filtrar.
Realizamos estas pruebas en una máquina con 4 CPU. Generamos 1 millón de filas, cada una con 100 reglas y cada regla evalúa 5 expresiones. Estas pruebas se realizaron 5 veces.
- De media
- map_filter tardó 42,6 segundos
explode_then_filter tardó 51,2 segundos
Por lo tanto, map_filter es un poco más rápido aunque no utiliza la generación de código de etapa completa.
Caused by: org.codehaus.commons.compiler.InternalCompilerException: Code grows beyond 64 KB
Sin embargo, en nuestra consulta de producción, ejecutamos muchas más reglas Sigma: un total de 1000 reglas. Esto incluye 29 expresiones regulares, 529 iguales, 115 comienzan con, 2352 terminan con y 5838 contienen expresiones. Probemos nuestra consulta nuevamente, pero esta vez con un ligero aumento en el número de expresiones, usando 7 en lugar de 5 por regla. Al hacer esto, encontramos un error en nuestros registros: spark.sql.codegen.maxFields
Intentamos aumentar spark.sql.codegen.hugeMethodLimit
y
, pero fundamentalmente, las clases Java tienen un límite de tamaño de función de 64 KB. Además, el compilador JVM JIT se limita a compilar funciones de menos de 8 KB.
Sin embargo, la consulta aún funciona bien porque Spark recurre al modelo de ejecución de Volcano para ciertas partes del plan. WholeStageCodeGen es solo una optimización después de todo.
- Al ejecutar la misma prueba que antes pero con 7 expresiones por regla en lugar de 5, explode_then_filter es mucho más rápido que map_filter.
- map_filter tardó 68,3 segundos
explode_then_filter tardó 15,8 segundos org.apache.spark.sql.catalyst.optimizer.InferFiltersFromGenerate
Aumentar el número de expresiones hace que partes de explode_then_filter ya no sean código de etapa completa generado. En particular, el operador Filtro introducido por la regla
spark.sql("SET spark.sql.optimizer.excludedRules=org.apache.spark.sql.catalyst.optimizer.InferFiltersFromGenerate")
Es demasiado grande para incorporarlo a la generación de código de etapa completa. Veamos qué sucede si excluimos la regla InferFiltersFromGenerate:
== Physical Plan ==
* Project (6)
+- * Generate (5)
+- Project (4)
+- * Project (3)
+- * Project (2)
+- * Range (1)== Physical Plan ==
* Project (7)
+- * Filter (6)
+- * Generate (5)
+- * Project (4)
+- * Project (3)
+- * Project (2)
+- * Range (1)
Como era de esperarse, el plan físico de ambas consultas ya no cuenta con un operador de Filtro adicional.
- De hecho, eliminar la regla tuvo un impacto significativo en el rendimiento:
- map_filter tardó 22,49 segundos
explode_then_filter tardó 4,08 segundos
Ambas consultas se beneficiaron enormemente de la eliminación de la regla. Dado el rendimiento mejorado, decidimos aumentar el número de reglas Sigma a 500 y la complejidad a 21 expresiones:
- Resultados:
- map_filter tardó 195,0 segundos
explode_then_filter tardó 25,09 segundos
A pesar de la mayor complejidad, ambas consultas aún ofrecen un rendimiento bastante bueno, y explode_then_filter supera significativamente a map_filter.
Es interesante explorar los diferentes aspectos de la generación de código empleada por Spark. Si bien es posible que actualmente no nos beneficiemos de la generación de código de etapa completa, aún podemos obtener ventajas de la generación de expresiones. spark.sql.codegen.methodSplitThreshold
La generación de expresiones no enfrenta las mismas limitaciones que la generación de código de etapa completa. Los árboles de expresión muy grandes se pueden dividir en otros más pequeños, y Spark's
controla cómo se dividen. Aunque experimentamos con esta propiedad, no observamos mejoras significativas. La configuración predeterminada parece satisfactoria. spark.sql.codegen.factoryMode
Spark proporciona una propiedad de depuración llamada spark.sql.codegen.factoryMode=NO_CODEGEN
, que se puede configurar en FALLBACK, CODEGEN_ONLY o NO_CODEGEN. Podemos desactivar la generación de código de expresión configurando
lo que resulta en una drástica degradación del rendimiento:
- Con 500 reglas y 21 expresiones:
- map_filter tomó 1581 segundos
explode_then_filter tardó 122,31 segundos.
Aunque no todos los operadores participan en la generación de código de etapa completa, aún observamos beneficios significativos de la generación de código de expresión.
Imagen del autor
Con nuestro mejor caso de 25,1 segundos para evaluar 10.500 expresiones en 1 millón de filas, logramos una tasa muy respetable de 104 millones de expresiones por segundo por CPU. map_filter
La conclusión de este estudio es que al evaluar una gran cantidad de expresiones, nos beneficiamos al convertir nuestras consultas que usan org.apache.spark.sql.catalyst.optimizer.InferFiltersFromGenerate
a aquellos que usan un enfoque de explosión y luego filtrado. Además, el
La regla no parece beneficiosa en nuestro caso de uso, por lo que debemos excluir esa regla de nuestras consultas.
¿Explica nuestras observaciones iniciales?
La implementación de estas lecciones aprendidas en nuestros trabajos de producción produjo beneficios significativos. Sin embargo, incluso después de estas optimizaciones, dividir la consulta grande en varias consultas más pequeñas siguió brindando ventajas. Tras una mayor investigación, descubrimos que esto no se debía únicamente a la generación de código sino a una explicación más simple.
Spark streaming funciona ejecutando un microlote hasta su finalización y luego verifica su progreso antes de comenzar un nuevo microlote.
Imagen del autor
De hecho, esto arroja luz sobre un fenómeno diferente. El hecho de que Spark espere algunas tareas rezagadas durante cada microlote deja muchas CPU inactivas, lo que explica por qué dividir la consulta grande en varias consultas más pequeñas resultó en un rendimiento general más rápido.
Imagen del autor
Durante estos períodos de espera, Spark puede utilizar las CPU inactivas para abordar otras consultas, maximizando así la utilización de recursos y el rendimiento general.
Conclusión
En este artículo, brindamos una descripción general del proceso de generación de código de Spark y analizamos cómo las optimizaciones integradas pueden no siempre producir resultados deseables. Además, demostramos que refactorizar una consulta que utiliza funciones lambda a una que utiliza una operación de explosión simple resultó en mejoras de rendimiento. Finalmente, llegamos a la conclusión de que, si bien dividir una declaración grande generó mejoras en el rendimiento, el factor principal que impulsó estas ganancias fue la topología de ejecución en lugar de las consultas en sí.