ניהול יחסי תלות של Java ו-Scala ב-Apache Spark

אפליקציות Spark מסתמכות לעיתים קרובות על ספריות Java או Scala של צד שלישי. אלה הגישות המומלצות לכלול את התלות האלה כששולחים משימת Spark לאשכול Managed Service for Apache Spark:

  1. כששולחים משימה מהמחשב המקומי באמצעות הפקודה gcloud dataproc jobs submit, משתמשים בדגל --properties spark.jars.packages=[DEPENDENCIES].

    לדוגמה:

    gcloud dataproc jobs submit spark \
        --cluster=my-cluster \
        --region=region \
        --properties=spark.jars.packages='com.google.cloud:google-cloud-translate:1.35.0,org.apache.bahir:spark-streaming-pubsub_2.11:2.2.0'
    

  2. כששולחים עבודה ישירות באשכול, משתמשים בפקודה spark-submit עם הפרמטר --packages=[DEPENDENCIES].

    לדוגמה:

    spark-submit --packages='com.google.cloud:google-cloud-translate:1.35.0,org.apache.bahir:spark-streaming-pubsub_2.11:2.2.0'
    

  3. כשכוללים חבילות או קובצי JAR מותאמים אישית:

    • תלות ספציפית בעבודה: מציינים את הנתיב ב-Cloud Storage לקובץ ה-JAR באמצעות הדגל --jars, השדה Jar files במסוף Cloud de Confiance או השדה jarFileUris ב-API כששולחים עבודה.
    • תלויות ברמת האשכול: משתמשים בspark:spark.jars מאפיין האשכול במהלך יצירת האשכול (--properties="spark:spark.jars=gs://<var>your-bucket</var>/<var>your-jarfile</var>.jar").

מניעת התנגשויות בין תלויות

הגישות הקודמות עלולות להיכשל אם יש התנגשות בין התלויות של אפליקציית Spark לבין התלויות של Hadoop. התנגשות כזו יכולה להתרחש כי Hadoop מחדיר את התלויות שלו לנתיב המחלקה של האפליקציה, ולכן התלויות שלו מקבלות עדיפות על פני התלויות של האפליקציה. כשמתרחשת התנגשות, יכולות להיווצר NoSuchMethodError או שגיאות אחרות.

דוגמה:
Guava היא ספריית הליבה של Google ל-Java, שמשמשת ספריות ומסגרות רבות, כולל Hadoop. יכול להיות שיהיה ניגוד בין תלות אם משימה או התלויות שלה דורשות גרסה של Guava שהיא חדשה יותר מהגרסה שבה נעשה שימוש ב-Hadoop.

‫Hadoop v3.0 resolved

הבעיה הזו, אבל אפליקציות שמסתמכות על גרסאות קודמות של Hadoop דורשות פתרון עקיף שמורכב משני חלקים כדי למנוע התנגשויות אפשריות בין תלויות.

  1. יוצרים קובץ JAR יחיד שמכיל את חבילת האפליקציה ואת כל התלויות שלה.
  2. כדי למנוע התנגשות בין שמות הנתיבים של חבילות התלות לבין אלה של חבילות התלות של Hadoop, צריך להעביר את חבילות התלות המתנגשות בתוך קובץ ה-JAR הגדול. במקום לשנות את הקוד, אפשר להשתמש בתוסף (ראו בהמשך) כדי לבצע את ההעברה הזו (שנקראת גם 'הצללה') באופן אוטומטי כחלק מתהליך האריזה.

יצירת קובץ JAR גדול עם הצללה באמצעות Maven

‫Maven הוא כלי לניהול חבילות שמשמש לפיתוח אפליקציות ב-Java. אפשר להשתמש בפלאגין Maven scala כדי ליצור אפליקציות שנכתבו ב-Scala, השפה שבה משתמשים באפליקציות Spark. ‫Maven shade

אפשר להשתמש בפלאגין כדי ליצור קובץ JAR מוצלל.

הקובץ הבא הוא קובץ הגדרות pom.xml לדוגמה שבו מוגדרת הצללה לספריית Guava, שנמצאת בחבילה com.google.common. ההגדרה הזו מורה ל-Maven לשנות את השם של חבילת com.google.common ל-repackaged.com.google.common ולעדכן את כל ההפניות למחלקות מהחבילה המקורית.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <groupId><!-- YOUR_GROUP_ID --></groupId>
  <artifactId><!-- YOUR_ARTIFACT_ID --></artifactId>
  <version><!-- YOUR_PACKAGE_VERSION --></version>

  <dependencies>

    <dependency>
      <groupId>org.apache.spark</groupId>
      <artifactId>spark-sql_2.11</artifactId>
      <version><!-- YOUR_SPARK_VERSION --></version>
      <scope>provided</scope>
    </dependency>

    <!-- YOUR_DEPENDENCIES -->

  </dependencies>

  <build>
    <plugins>

      <plugin>
        <groupId>net.alchim31.maven</groupId>
        <artifactId>scala-maven-plugin</artifactId>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>testCompile</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <scalaVersion><!-- YOUR_SCALA_VERSION --></scalaVersion>
        </configuration>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass><!-- YOUR_APPLICATION_MAIN_CLASS --></mainClass>
                </transformer>
                <!-- This is needed if you have dependencies that use Service Loader. Most Google Cloud client libraries do. -->
                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
              </transformers>
              <filters>
                <filter>
                  <artifact>*:*</artifact>
                  <excludes>
                    <exclude>META-INF/maven/**</exclude>
                    <exclude>META-INF/*.SF</exclude>
                    <exclude>META-INF/*.DSA</exclude>
                    <exclude>META-INF/*.RSA</exclude>
                  </excludes>
                </filter>
              </filters>
              <relocations>
                <relocation>
                  <pattern>com</pattern>
                  <shadedPattern>repackaged.com.google.common</shadedPattern>
                  <includes>
                    <include>com.google.common.**</include>
                  </includes>
                </relocation>
              </relocations>
            </configuration>
          </execution>
        </executions>
      </plugin>

    </plugins>
  </build>

</project>

כדי להריץ את הבנייה:

mvn package

הערות לגבי pom.xml:

  • ‫ManifestResourceTransformer מעבד מאפיינים בקובץ המניפסט של ה-uber JAR ‏ (MANIFEST.MF). המניפסט יכול גם לציין את נקודת הכניסה לאפליקציה.
  • ההיקף של Spark הוא provided, כי Spark מותקן ב-Managed Service for Apache Spark.
  • מציינים את הגרסה של Spark שמותקנת באשכול Managed Service for Apache Spark (ראו רשימת הגרסאות של Managed Service for Apache Spark). אם האפליקציה שלכם דורשת גרסת Spark ששונה מהגרסה שמותקנת באשכול Managed Service for Apache Spark, אתם יכולים לכתוב פעולת אתחול או ליצור תמונה בהתאמה אישית שמתקינה את גרסת Spark שבה האפליקציה משתמשת.
  • הערך <filters> מחריג קובצי חתימה מהספריות של התלויות שלכם META-INF. בלי הרשומה הזו, יכולה להתרחש חריגה בזמן הריצה של java.lang.SecurityException: Invalid signature file digest for Manifest main attributes כי קובצי החתימה לא תקפים בהקשר של קובץ ה-JAR הגדול.
  • יכול להיות שתצטרכו להצל את כל הספריות. כדי לעשות זאת, צריך לכלול כמה נתיבים. בדוגמה הבאה מוצגות ספריות Guava ו-Protobuf.
    <relocation>
      <pattern>com</pattern>
      <shadedPattern>repackaged.com</shadedPattern>
      <includes>
        <include>com.google.protobuf.**</include>
        <include>com.google.common.**</include>
      </includes>
    </relocation>

יצירת קובץ JAR גדול עם הצללה באמצעות SBT

‫SBT הוא כלי לבניית אפליקציות Scala. כדי ליצור קובץ JAR מוצלל באמצעות SBT, מוסיפים את התוסף sbt-assembly

plugin להגדרת ה-build, קודם יוצרים קובץ בשם assembly.sbt בתיקייה project/:

├── src/
└── build.sbt
└── project/
    └── assembly.sbt

‫… ואז מוסיפים את השורה הבאה בקובץ assembly.sbt:

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")

הדוגמה הבאה היא קובץ הגדרות build.sbt שמסתיר את ספריית Guava שנמצאת ב-com.google.common package:

lazy val commonSettings = Seq(
 organization := "YOUR_GROUP_ID",
 name := "YOUR_ARTIFACT_ID",
 version := "YOUR_PACKAGE_VERSION",
 scalaVersion := "YOUR_SCALA_VERSION",
)

lazy val shaded = (project in file("."))
 .settings(commonSettings)

mainClass in (Compile, packageBin) := Some("YOUR_APPLICATION_MAIN_CLASS")

libraryDependencies ++= Seq(
 "org.apache.spark" % "spark-sql_2.11" % "YOUR_SPARK_VERSION" % "provided",
 // YOUR_DEPENDENCIES
)

assemblyShadeRules in assembly := Seq(
  ShadeRule.rename("com.google.common.**" -> "repackaged.com.google.common.@1").inAll
)

כדי להריץ את הבנייה:

sbt assembly

הערות לגבי build.sbt:

  • יכול להיות שכלל ההצללה בדוגמה הקודמת לא יפתור את כל הבעיות שקשורות לתלות, כי SBT משתמש בשיטות מחמירות לפתרון בעיות שקשורות לתלות. לכן, יכול להיות שתצטרכו לספק כללים מפורטים יותר שממזגים באופן מפורש סוגים ספציפיים של קבצים סותרים באמצעות אסטרטגיות MergeStrategy.first, last, concat, filterDistinctLines, rename או discard. פרטים נוספים מופיעים במאמר בנושא אסטרטגיית המיזוג של sbt-assembly.
  • יכול להיות שתצטרכו להצל את כל הספריות. כדי לעשות זאת, צריך לכלול כמה נתיבים. בדוגמה הבאה מוצגות ספריות Guava ו-Protobuf.
    assemblyShadeRules in assembly := Seq(
      ShadeRule.rename("com.google.common.**" -> "repackaged.com.google.common.@1").inAll,
      ShadeRule.rename("com.google.protobuf.**" -> "repackaged.com.google.protobuf.@1").inAll
    )

שליחת קובץ ה-JAR הגדול אל Managed Service for Apache Spark

אחרי שיוצרים קובץ JAR מוצלל שמכיל את אפליקציית Spark ואת התלויות שלה, מעלים את קובץ ה-JAR ל-Cloud Storage ושולחים משימה ל-Managed Service for Apache Spark (באמצעות הדגל --jars ב-Google Cloud CLI, השדה Jar files במסוף Cloud de Confiance , השדה jarFileUris ב-API או על ידי ציון קובץ ה-JAR המוצלל כקובץ ה-JAR של האפליקציה הראשית).

המאמרים הבאים

  • אפשר לעיין ב-spark-translate, אפליקציית Spark לדוגמה שמכילה קובצי הגדרות גם ל-Maven וגם ל-SBT.
  • כתיבה והרצה של משימות Spark Scala ב-Managed Service for Apache Spark. במדריך לתחילת העבודה מוסבר איך לכתוב ולהריץ משימות Spark Scala באשכול Managed Service for Apache Spark.