Tạo Fat Jar cho chương trình Apache Spark

Các bài viết về Apache Spark

Phần 1: Hướng Dẫn Cài Apache Spark Trên Ubuntu
Phần 2: Một chương trình Apache Spark siêu đơn giản
Phần 3: Tạo Fat Jar cho chương trình Apache Spark


Mình sẽ hướng dẫn các bạn tạo fat jar cho ứng dụng Apache Spark bằng ngôn ngữ Scala.

Chúng ta sẽ đặt biến môi trường SPARK_HOME để tiện việc gọi spark-submit không phải ghi đường dẫn dài.

export SPARK_HOME=/home/tt/Software/spark-3.0.1-bin-hadoop2.7

1. Giới thiệu

1.1 Fat jar là gì ?

Fat JAR là một tập tin .jar chứa tất cả các thư viện cần thiết cho chương trình của bạn.

1.2 Khi nào bạn nên tạo fat jar ?

Khi bạn thấy lỗi NoClassDefFoundError

1.3 Một tình huống cụ thể

Bạn sử dụng lại ví dụ trong Phần 2: Một chương trình Apache Spark siêu đơn giản. Bạn nên copy ví dụ ra chỗ khác để tiện việc so sánh đối chiếu. Chúng ta dùng thêm thư viện Joda Money. Các bạn cần sửa lại mã nguồn như sau.

1.3.1 build.sbt

Thêm thư viện joda money vào build.sbt

// https://mvnrepository.com/artifact/org.joda/joda-money
libraryDependencies += "org.joda" % "joda-money" % "1.0.1"

1.3.2 Chương trình

Sửa tên class lại thành JodaSimpleApp, sửa tên file thành JodaSimpleApp.scala. Import thêm thư viện

import org.joda.money.Money
import org.joda.money.CurrencyUnit

Phần thân chương trình, thêm vào

// create a monetary value
val m1 = Money.parse("USD 23.87");
val usd = CurrencyUnit.of("USD");
val m2 = m1.plus(Money.of(usd, 12.43d));
println(s"Money m1 $m1")
println(s"Money m2 $m2")

Nội dung JodaSimpleApp.scala

/* JodaSimpleApp.scala */
import org.apache.spark.sql.SparkSession
import org.joda.money.Money
import org.joda.money.CurrencyUnit


/*
* We include joda time in this spark application 
*/
object JodaSimpleApp {
  def main(args: Array[String]) {
    var mySparkHome = "/zserver/spark/spark-3.0.1-bin-hadoop2.7"
    val logFile = mySparkHome + "/README.md" // Should be some file on your system
    val spark = SparkSession.builder.appName("Simple Application").getOrCreate()
    val logData = spark.read.textFile(logFile).cache()
    val numAs = logData.filter(line => line.contains("a")).count()
    val numBs = logData.filter(line => line.contains("b")).count()
    println(s"log file dir is $logFile")
    println(s"Lines with a: $numAs, Lines with b: $numBs")

    // create a monetary value
    val m1 = Money.parse("USD 23.87");
    val usd = CurrencyUnit.of("USD");
    val m2 = m1.plus(Money.of(usd, 12.43d));
    println(s"Money m1 $m1")
    println(s"Money m2 $m2")

    spark.stop()
  }
}

1.3.3 Biên dịch và thực thi

sbt package 
$SPARK_HOME/bin/spark-submit \
--class "JodaSimpleApp" \
--master local[4]  \
target/scala-2.12/fatjar-simple-project_2.12-1.0.jar

Khi chương trình chạy, chúng ta sẽ thấy lỗi như sau:

Exception in thread "main" java.lang.NoClassDefFoundError: org/joda/money/Money
        at JodaSimpleApp$.main(JodaSimpleApp.scala:22)
        at JodaSimpleApp.main(JodaSimpleApp.scala)

Bởi vì sbt package không bỏ những thư viện libraryDependencies vào gói jar.

Tuy nhiên, ở ví dụ Phần 2 thư viện spark-sql vẫn chạy được, vì trong môi trường thực thi spark-submit đã có sẵn.

Để sửa lỗi này, ta cần biên dịch fat jar và nhét Joda Money vào gói jar đó.

2. Tạo fat jar với sbt plugin assembly

2.1 Gắn plugin

Tạo tập tin project/plugins.sbt với nội dung

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

2.2 Sửa build.sbt

Thêm provided vào thư viện spark để nó không nhét vào jar. Bởi vì môi trường spark-submit đã có sẵn các thư viện spark, nên bạn không cần nhét vào jar. Nó sẽ giảm rất nhiều dung lượng gói jar của bạn.

libraryDependencies += "org.apache.spark" %% "spark-sql" % "3.0.1" % "provided"

Trong quá trình gộp các thư viện vào cùng 1 jar, sẽ gặp vấn đề khi các phần nội dung cùng tên mà khác nội dung. Bạn cần phải định nghĩa assemblyMergeStrategy để xử lý khi gặp tình huống này. Đoạn code sau phù hợp với đa số trường hợp phổ biến mình vẫn thường dùng.

assemblyMergeStrategy in assembly := {
    case PathList("javax", "servlet", xs @ _*) => MergeStrategy.last
    case PathList("javax", "activation", xs @ _*) => MergeStrategy.last
    case PathList("org", "joda", xs @ _*) => MergeStrategy.last
    case PathList("org", "apache", xs @ _*) => MergeStrategy.last
    case PathList("com", "google", xs @ _*) => MergeStrategy.last
    case PathList("com", "esotericsoftware", xs @ _*) => MergeStrategy.last
    case PathList("com", "codahale", xs @ _*) => MergeStrategy.last
    case PathList("com", "yammer", xs @ _*) => MergeStrategy.last
    case "about.html" => MergeStrategy.rename
    case "META-INF/ECLIPSEF.RSA" => MergeStrategy.last
    case "META-INF/mailcap" => MergeStrategy.last
    case "META-INF/mimetypes.default" => MergeStrategy.last
    case "plugin.properties" => MergeStrategy.last
    case "log4j.properties" => MergeStrategy.last

    //https://stackoverflow.com/questions/54834125/sbt-assembly-deduplicate-module-info-class
    case "module-info.class" => MergeStrategy.discard  // for jdk8 only
    case x =>
        val oldStrategy = (assemblyMergeStrategy in assembly).value
        oldStrategy(x)
}

Toàn bộ nội dung build.sbt như sau:

name := "Fatjar-Simple-Project"

version := "1.0"

scalaVersion := "2.12.10"

libraryDependencies += "org.apache.spark" %% "spark-sql" % "3.0.1" % "provided"

// https://mvnrepository.com/artifact/org.joda/joda-money
libraryDependencies += "org.joda" % "joda-money" % "1.0.1"


assemblyMergeStrategy in assembly := {
    case PathList("javax", "servlet", xs @ _*) => MergeStrategy.last
    case PathList("javax", "activation", xs @ _*) => MergeStrategy.last
    case PathList("org", "joda", xs @ _*) => MergeStrategy.last
    case PathList("org", "apache", xs @ _*) => MergeStrategy.last
    case PathList("com", "google", xs @ _*) => MergeStrategy.last
    case PathList("com", "esotericsoftware", xs @ _*) => MergeStrategy.last
    case PathList("com", "codahale", xs @ _*) => MergeStrategy.last
    case PathList("com", "yammer", xs @ _*) => MergeStrategy.last
    case "about.html" => MergeStrategy.rename
    case "META-INF/ECLIPSEF.RSA" => MergeStrategy.last
    case "META-INF/mailcap" => MergeStrategy.last
    case "META-INF/mimetypes.default" => MergeStrategy.last
    case "plugin.properties" => MergeStrategy.last
    case "log4j.properties" => MergeStrategy.last

    //https://stackoverflow.com/questions/54834125/sbt-assembly-deduplicate-module-info-class
    case "module-info.class" => MergeStrategy.discard  // for jdk8 only
    case x =>
        val oldStrategy = (assemblyMergeStrategy in assembly).value
        oldStrategy(x)
}

2.3 Biên dịch và thực thi

sbt clean
sbt assembly 

Nó sẽ tạo ra một gói jar tên có phần đuôi là assembly-x.x.jar. Tránh nhầm lẫn với gói jar tạo ra bởi sbt package.

Rồi, ta thực thi gói jar này

$SPARK_HOME/bin/spark-submit \
--class "JodaSimpleApp" \
--master local[4]  \
target/scala-2.12/Fatjar-Simple-Project-assembly-1.0.jar

Sau khi chạy bạn sẽ thấy kết quả của đoạn mã sử dụng thư viện Joda Money

Money m1 USD 23.87
Money m2 USD 36.30

Ví dụ hoàn chỉnh trên Github

https://github.com/ttpro1995/ApacheSparkFatJarSimpleApp

Leave a Reply