Enregistrement complet du dépannage et de la réparation de BUG selon les règles de Spark Optimizer

L'auteur de cet article : Zhu Mingliang, ingénieur en développement de Guanyuan Data Computing Engine, est diplômé de l'Université du Hunan en 2013 et possède plus de sept ans d'expérience dans la recherche et le développement de mégadonnées. Il a autrefois travaillé comme expert en recherche et développement de données dans une société Internet bien connue, et était responsable de la construction d'une plate-forme intermédiaire de données qui dessert différents secteurs d'activité au sein de l'entreprise.Il est l'un des principaux contributeurs au projet open source byzer- lang (un moteur informatique de plate-forme intermédiaire open source basé sur Spark).

arrière-plan

Les étudiants du test ont trouvé un problème sérieux dans le test de l'environnement interne. Le résultat de l'exécution ETL n'a pas répondu aux attentes. Selon le filtrage du champ Date des données brutes, les données filtrées sont égales à 2022-07-12 et les données filtrées sont 7.11 .vide.

Processus de dépannage

1. Logique simplifiée

Le processus ETL est trop volumineux. Tout d'abord, vous devez passer un peu de temps à simplifier l'ETL pour minimiser la récurrence des problèmes et faciliter le dépannage.

Manière simplifiée :

  • Si le temps d'exécution ETL est long, réduisez d'abord la taille de l'ensemble de données, enregistrez le nouvel ensemble de données après la limite et utilisez le nouvel ensemble de données pour les tests.

  • Réduisez la logique du début à la fin jusqu'à ce que le problème ne puisse plus être reproduit. La réduction en tête peut enregistrer le jeu de données temporaire, puis utiliser le jeu de données temporaire pour le calcul, et la réduction à la fin peut prévisualiser directement le résultat de l'étape précédente.

La logique simplifiée est constituée de deux ensembles de données, une table A avec un champ de type date a et une table B avec des champs de type date a et b, exprimées en sql comme :

select  to_date(a) as a, to_date(b) as b from 
(select to_date(a) as a, to_date(A)  as b from 
(select to_date(a) as a from A group by to_date(a)) t1
union all 
select to_date(a) as a, to_date(b) as b from B group by to_date(a), to_date(b)) t2
group by  to_date(a), to_date(b)

Bien sûr, ce que le moteur de travail obtient est un fichier de script. Ici, il est plus intuitif de voir la logique écrite en sql. En vérifiant le script, il est confirmé que la logique générée par BI est correcte, et le problème réside dans le couche moteur.

2. Vérification de la récurrence locale Spark

reproduction locale

Convertissez le script simplifié en opérateur spark ou spark sql. L'environnement de test est spark 3.2.1. Le problème est reproduit dans la version spark 3.2.1. Après reproduction locale, il convient de déboguer.

  test("test1") {
    val sqlText =
      """
        |select to_date(a) a, to_date(b) b from
        |(select  to_date(a) a, to_date(a) as b from
        |(select to_date(a) a from
        | values ('2020-02-01') as t1(a)
        | group by to_date(a)) t3
        |union all
        |select to_date(a) a, to_date(b) b from
        |(select to_date(a) a, to_date(b) b from
        |values ('2020-01-01','2020-01-02') as t1(a, b)
        | group by to_date(a), to_date(b)) t4) t5
        |group by to_date(a), to_date(b)
        |""".stripMargin
    spark.sql(sqlText).show()
  }

Résultat attendu:

un b
2020-02-01 2020-02-01
2020-01-01 2020-01-02

Le résultat renvoyé :

un b
2020-02-01 2020-02-01
2020-01-01 2020-01-01

On peut voir que b de toutes les données prend la valeur de a, et l'étape suivante consiste à vérifier le plan d'exécution Spark.

Plan de mise en œuvre

Présentons brièvement l'optimiseur Spark Catalyst.

Une instruction SQL génère un programme reconnaissable par le moteur d'exécution, qui est indissociable des trois grands processus d'analyse (Parser), d'optimisation (Optimizer) et d'exécution (Execution). Lorsque l'optimiseur Catalyst effectue la génération et l'optimisation du plan, il ne peut pas se passer de ses cinq composants internes, comme suit :

  • Parser : utilisez Antlr4 pour analyser l'analyse lexicale et grammaticale des instructions sql ;

  • Analyseur : utilise principalement les informations du catalogue pour analyser le plan logique non résolu dans le plan logique analysé ;

  • Optimiseur : utilisez certaines règles (règles) pour analyser le plan logique analysé dans un plan logique optimisé. Il existe de nombreuses règles, telles que le pushdown de prédicat commun PushDownPredicate, l'élagage de colonne ColumnPruning, le remplacement constant ConstantPropagation, l'accumulation constante ConstantFolding, etc. ;

  • Planificateur : le plan logique précédent ne peut pas être exécuté par Spark, et ce processus consiste à convertir le plan logique en plusieurs plans physiques, puis à utiliser le modèle de coût (modèle de coût) pour sélectionner le meilleur plan physique ;

  • Génération de code : ce processus génère un bytecode Java à partir de requêtes SQL.

Le plan d'exécution de chaque étape de SQL peut être obtenu via la méthode EXPLAIN(true), comme suit :

== Parsed Logical Plan ==
'Aggregate ['to_date('a), 'to_date('b)], ['to_date('a) AS a#7, 'to_date('b) AS b#8]
+- 'SubqueryAlias t5
   +- 'Union false, false
      :- 'Project ['to_date('a) AS a#1, 'to_date('a) AS b#2]
      :  +- 'SubqueryAlias t3
      :     +- 'Aggregate ['to_date('a)], ['to_date('a) AS a#0]
      :        +- 'SubqueryAlias t1
      :           +- 'UnresolvedInlineTable [a], [[2020-02-01]]
      +- 'Project ['to_date('a) AS a#5, 'to_date('b) AS b#6]
         +- 'SubqueryAlias t4
            +- 'Aggregate ['to_date('a), 'to_date('b)], ['to_date('a) AS a#3, 'to_date('b) AS b#4]
               +- 'SubqueryAlias t1
                  +- 'UnresolvedInlineTable [a, b], [[2020-01-01, 2020-01-02]]

== Analyzed Logical Plan ==
a: date, b: date
Aggregate [to_date(a#1, None), to_date(b#2, None)], [to_date(a#1, None) AS a#7, to_date(b#2, None) AS b#8]
+- SubqueryAlias t5
   +- Union false, false
      :- Project [to_date(a#0, None) AS a#1, to_date(a#0, None) AS b#2]
      :  +- SubqueryAlias t3
      :     +- Aggregate [to_date(a#9, None)], [to_date(a#9, None) AS a#0]
      :        +- SubqueryAlias t1
      :           +- LocalRelation [a#9]
      +- Project [to_date(a#3, None) AS a#5, to_date(b#4, None) AS b#6]
         +- SubqueryAlias t4
            +- Aggregate [to_date(a#10, None), to_date(b#11, None)], [to_date(a#10, None) AS a#3, to_date(b#11, None) AS b#4]
               +- SubqueryAlias t1
                  +- LocalRelation [a#10, b#11]

== Optimized Logical Plan ==
Aggregate [_groupingexpression#16, _groupingexpression#16], [_groupingexpression#16 AS a#7, _groupingexpression#16 AS b#8]
+- Union false, false
   :- Aggregate [_groupingexpression#16], [_groupingexpression#16, _groupingexpression#16]
   :  +- LocalRelation [_groupingexpression#16]
   +- Aggregate [_groupingexpression#17, _groupingexpression#18], [_groupingexpression#17, _groupingexpression#18]
      +- LocalRelation [_groupingexpression#17, _groupingexpression#18]

== Physical Plan ==
AdaptiveSparkPlan isFinalPlan=false
+- HashAggregate(keys=[_groupingexpression#16, _groupingexpression#16], functions=[], output=[a#7, b#8])
   +- Exchange hashpartitioning(_groupingexpression#16, _groupingexpression#16, 200), ENSURE_REQUIREMENTS, [id=#40]
      +- HashAggregate(keys=[_groupingexpression#16, _groupingexpression#16], functions=[], output=[_groupingexpression#16, _groupingexpression#16])
         +- Union
            :- HashAggregate(keys=[_groupingexpression#16], functions=[], output=[_groupingexpression#16, _groupingexpression#16])
            :  +- Exchange hashpartitioning(_groupingexpression#16, 200), ENSURE_REQUIREMENTS, [id=#33]
            :     +- HashAggregate(keys=[_groupingexpression#16], functions=[], output=[_groupingexpression#16])
            :        +- LocalTableScan [_groupingexpression#16]
            +- HashAggregate(keys=[_groupingexpression#17, _groupingexpression#18], functions=[], output=[_groupingexpression#17, _groupingexpression#18])
               +- Exchange hashpartitioning(_groupingexpression#17, _groupingexpression#18, 200), ENSURE_REQUIREMENTS, [id=#35]
                  +- HashAggregate(keys=[_groupingexpression#17, _groupingexpression#18], functions=[], output=[_groupingexpression#17, _groupingexpression#18])
                     +- LocalTableScan [_groupingexpression#17, _groupingexpression#18]

Un examen plus approfondi du plan d'exécution montre que le plan logique analysé est normal et que le plan logique optimisé présente des problèmes évidents :

Agrégat [_groupingexpression#16, _groupingexpression#16], [_groupingexpression#16 AS a#7, _groupingexpression#16 AS b#8]

Les champs de résultat d'agrégation les plus externes a, b deviennent la même expression _groupingexpression#16, le problème réside dans la conversion d'expression de l'optimiseur.

3. Essayez de trouver des raccourcis

Il y a des dizaines de règles impliquées dans l'optimiseur. Il est trop lent à comprendre et à dépanner une par une. Essayez d'abord de trouver un raccourci.

Testez d'abord séparément sur spark 3.0.1, spark 3.2.2 et spark master respectivement, et a constaté que seul spark 3.0.1 est le résultat correct, et d'autres versions ont des résultats erronés, indiquant qu'il s'agit d'un nouveau bogue introduit après spark 3.0 .1 , et n'a pas encore été corrigé.

Utilisez ensuite des mots-clés tels que union, agrégat, attribut, alias pour rechercher des problèmes non résolus de spark, mais n'a pas trouvé le même bogue, n'a pas trouvé de raccourci, a signalé le problème à la communauté, puis a continué à analyser.

Adresse du problème : [SPARK-39887] Erreur de transformation d'expression - ASF JIRA

4. Analysez les règles de l'optimiseur

Dans l'optimiseur, l'activation des quatre règles PushProjectionThroughUnion, CollapseProject, SimplifyCasts et RemoveRedundantAliases via l'exclusion déclenchera un bogue. Grâce au débogage local, vous pouvez voir la nouvelle arborescence de plan logique obtenue après l'exécution de chaque règle.

Aggregate [_groupingexpression#14, _groupingexpression#15], [_groupingexpression#14 AS a#7, _groupingexpression#15 AS b#8]
+- Project [a#1, b#2, cast(a#1 as date) AS _groupingexpression#14, cast(b#2 as date) AS _groupingexpression#15]
   +- Union false, false
      :- Project [cast(a#0 as date) AS a#1, cast(a#0 as date) AS b#2]
      :  +- Aggregate [_groupingexpression#16], [_groupingexpression#16 AS a#0]
      :     +- Project [a#9, cast(a#9 as date) AS _groupingexpression#16]
      :        +- LocalRelation [a#9]
      +- Project [cast(a#3 as date) AS a#5, cast(b#4 as date) AS b#6]
         +- Aggregate [_groupingexpression#17, _groupingexpression#18], [_groupingexpression#17 AS a#3, _groupingexpression#18 AS b#4]
            +- Project [a#10, b#11, cast(a#10 as date) AS _groupingexpression#17, cast(b#11 as date) AS _groupingexpression#18]
               +- LocalRelation [a#10, b#11]

Avant PushProjectionThroughUnion

Aggregate [_groupingexpression#14, _groupingexpression#15], [_groupingexpression#14 AS a#7, _groupingexpression#15 AS b#8]
+- Union false, false
   :- Project [a#1, b#2, cast(a#1 as date) AS _groupingexpression#14, cast(b#2 as date) AS _groupingexpression#15]
   :  +- Project [cast(a#0 as date) AS a#1, cast(a#0 as date) AS b#2]
   :     +- Aggregate [_groupingexpression#16], [_groupingexpression#16 AS a#0]
   :        +- Project [a#9, cast(a#9 as date) AS _groupingexpression#16]
   :           +- LocalRelation [a#9]
   +- Project [a#5, b#6, cast(a#5 as date) AS _groupingexpression#19, cast(b#6 as date) AS _groupingexpression#20]
      +- Project [cast(a#3 as date) AS a#5, cast(b#4 as date) AS b#6]
         +- Aggregate [_groupingexpression#17, _groupingexpression#18], [_groupingexpression#17 AS a#3, _groupingexpression#18 AS b#4]
            +- Project [a#10, b#11, cast(a#10 as date) AS _groupingexpression#17, cast(b#11 as date) AS _groupingexpression#18]
               +- LocalRelation [a#10, b#11]

Après PushProjectionThroughUnion

Aggregate [_groupingexpression#14, _groupingexpression#15], [_groupingexpression#14 AS a#7, _groupingexpression#15 AS b#8]
+- Union false, false
   :- Aggregate [_groupingexpression#16], [cast(_groupingexpression#16 as date) AS a#1, cast(_groupingexpression#16 as date) AS b#2, cast(cast(_groupingexpression#16 as date) as date) AS _groupingexpression#14, cast(cast(_groupingexpression#16 as date) as date) AS _groupingexpression#15]
   :  +- Project [a#9, cast(a#9 as date) AS _groupingexpression#16]
   :     +- LocalRelation [a#9]
   +- Aggregate [_groupingexpression#17, _groupingexpression#18], [cast(_groupingexpression#17 as date) AS a#5, cast(_groupingexpression#18 as date) AS b#6, cast(cast(_groupingexpression#17 as date) as date) AS _groupingexpression#19, cast(cast(_groupingexpression#18 as date) as date) AS _groupingexpression#20]
      +- Project [a#10, b#11, cast(a#10 as date) AS _groupingexpression#17, cast(b#11 as date) AS _groupingexpression#18]
         +- LocalRelation [a#10, b#11]

Après l'effondrement du projet

Aggregate [_groupingexpression#14, _groupingexpression#15], [_groupingexpression#14 AS a#7, _groupingexpression#15 AS b#8]
+- Union false, false
   :- Aggregate [_groupingexpression#16], [_groupingexpression#16 AS a#1, _groupingexpression#16 AS b#2, cast(_groupingexpression#16 as date) AS _groupingexpression#14, cast(_groupingexpression#16 as date) AS _groupingexpression#15]
   :  +- Project [a#9, cast(a#9 as date) AS _groupingexpression#16]
   :     +- LocalRelation [a#9]
   +- Aggregate [_groupingexpression#17, _groupingexpression#18], [_groupingexpression#17 AS a#5, _groupingexpression#18 AS b#6, cast(_groupingexpression#17 as date) AS _groupingexpression#19, cast(_groupingexpression#18 as date) AS _groupingexpression#20]
      +- Project [a#10, b#11, cast(a#10 as date) AS _groupingexpression#17, cast(b#11 as date) AS _groupingexpression#18]
         +- LocalRelation [a#10, b#11]

Après SimplifyCasts

Aggregate [_groupingexpression#14, _groupingexpression#15], [_groupingexpression#14 AS a#7, _groupingexpression#15 AS b#8]
+- Union false, false
   :- Aggregate [_groupingexpression#16], [_groupingexpression#16 AS a#1, _groupingexpression#16 AS b#2, cast(_groupingexpression#16 as date) AS _groupingexpression#14, cast(_groupingexpression#16 as date) AS _groupingexpression#15]
   :  +- Project [a#9, cast(a#9 as date) AS _groupingexpression#16]
   :     +- LocalRelation [a#9]
   +- Aggregate [_groupingexpression#17, _groupingexpression#18], [_groupingexpression#17 AS a#5, _groupingexpression#18 AS b#6, cast(_groupingexpression#17 as date) AS _groupingexpression#19, cast(_groupingexpression#18 as date) AS _groupingexpression#20]
      +- Project [a#10, b#11, cast(a#10 as date) AS _groupingexpression#17, cast(b#11 as date) AS _groupingexpression#18]
         +- LocalRelation [a#10, b#11]

Après la suppression des alias redondants

Combiné avec le code source, comprenez le rôle de chaque règle :

  • PushProjectionThroughUnion : Poussez l'opérateur Project vers les deux côtés de l'opérateur Union all ;

  • CollapseProject : combine deux opérateurs Project en un seul et effectue une substitution d'alias, en fusionnant des expressions en une seule expression dans les cas suivants : 1. lorsque deux opérateurs Project sont adjacents ; 2. lorsque deux opérateurs Project Il existe des opérateurs LocalLimit/Sample/Repartition et le le projet de niveau se compose du même nombre de colonnes, qui sont égales ou ont des alias ;

  • SimplifyCasts : supprimez les conversions inutiles, car l'entrée est déjà du type correct ;

  • RemoveRedundantAliases : supprimez les alias redondants du plan de requête. Un alias redondant est un alias qui ne modifie pas le nom ou les métadonnées de la colonne.

Les trois premières visent à simplifier le plan d'exécution et à préparer d'autres optimisations, notamment RemoveRedundantAliases. L'accent est mis sur RemoveRedundantAliases et Union.output. Union.output réutilise la sortie du premier enfant, mais évidemment Union et son premier enfant produisent des valeurs différentes. RemoveRedundantAliases supprimera les alias Union, y compris ce cas Union(Project(a#1, a#1 as a#2, ...), ...), provoquant ainsi une erreur.

5. Corrigez les bogues

Connaissant la raison, commençons à le réparer.Ma solution est de ne pas effectuer l'opération RemoveRedundantAliases sur le premier enfant de l'union, ce qui résout le problème :

case _: Union =>
        var first = true
        plan.mapChildren { child =>
          if (first) {
            first = false
            removeRedundantAliases(child, excluded ++ child.outputSet)
          } else {
            removeRedundantAliases(child, excluded)
          }
        }

Cependant, lorsque le premier enfant de l'union ne contient pas d'attributs en conflit, cela affectera l'optimisation RemoveRedundantAliases du premier enfant, affectant légèrement les performances mais pas la fonction. Visant à résoudre ce problème, les leaders de la communauté le résolvent : [SPARK-39887][SQL] RemoveRedundantAliases devrait conserver les alias qui rendent la sortie des nœuds de projection unique par peter-toth · Pull Request #37334 · apache/spark · GitHub .

Résumer

Cet article présente principalement l'ensemble du processus de dépannage et de réparation du bogue causé par la règle RemoveRedundantAliases de Spark Optimizer. Il comporte principalement plusieurs étapes. La première étape consiste à simplifier la logique et à faciliter le dépannage ; la deuxième étape consiste à le reproduire localement. Il est déterminé qu'il s'agit d'un bogue de spark lui-même et localise grossièrement le problème. Le module auquel il appartient ; la troisième étape consiste à essayer de trouver un raccourci ; si la troisième partie échoue, passez à la quatrième étape pour analyser en profondeur et corriger les bogues .

Je suppose que tu aimes

Origine blog.csdn.net/GUANDATA_/article/details/126526150
conseillé
Classement