一、为什么需要自定义RDD

       1. spark提供了很多方法读数据源,比如我们当前可以从hdfs文件、jdbc、mongo、hbase等等将数据包装成RDD供我们后续进行处理。如果我们想要读memcache中的数据恐怕就没有现成的了,需要我们自己实现自己的RDD。

       2. RDD是一种弹性分布式数据集,本质就是对数据的封装与抽象。讲道理我们可以将任何我们想要的数据按照我们的业务情况将数据进行分片,而不是对spark的API深度依赖。

二、搭建项目

本demo完成功能:

       1. scala与java混合编程,核心的部分交给scala,需要我们外部扩展的使用java编写

       2. 创建的RDD共有10个partition,每一个partition共有10000个整数

项目搭建:

1. maven配置

<dependencies>
        <!-- spark -->
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-core_2.11</artifactId>
            <version>2.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-sql_2.11</artifactId>
            <version>2.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-mllib_2.11</artifactId>
            <version>2.2.0</version>
        </dependency>
        <!-- log -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.25</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>3.2.0</version>
                <executions>
                    <execution>
                        <id>compile-scala</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>add-source</goal>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>test-compile-scala</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>add-source</goal>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

日志log4j配置:

# 所有Log信息输出到标准输出(System.out)和在下面指定的一个文件
# WARN是默认的logging级别
log4j.rootCategory=DEBUG, STDOUT

# 应用程序的logging级别是DEBUG
log4j.logger.org=INFO
log4j.logger.org.apache=INFO
log4j.logger.org.apache.hadoop=ERROR
log4j.logger.io.netty=ERROR
log4j.logger.org.spark_project=ERROR

# 配置标准输出Appender
log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender
log4j.appender.STDOUT.Threshold=DEBUG
log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout
log4j.appender.STDOUT.layout.ConversionPattern=%5p (%C:%L) %m%n

项目目录结构:

spark 自定义metrics spark 自定义rdd_自定义

主函数:

/**
 * 应用启动
 *
 * @author yufei.liu
 */
public class Application {

    public static void main(String[] args) throws InterruptedException {
        SparkConf sparkConf = new SparkConf().setMaster("local[*]").setAppName("spark-custom-RDD");
        JavaSparkContext sparkContext = new JavaSparkContext(sparkConf);

        sparkContext.close();
    }

}

项目编译:

mvn clean scala:compile package -Dmaven.test.skip=true

好了,到这里为止,项目可以进行编译运行,上述是java与scala混合编程通用做法。

三、自定义RDD

自定义RDD本质上就是实现RDD的三个接口,分别是getPartitions、compute、getPreferredLocations。RDD数据是分区存储,每一个分区可能分布在申请spark资源的任何位置,partition只需要将数据的迭代器给RDD就可以了。这三个接口可以描述RDD的全部信息,其中这个demo就不重载getPreferredLocations,该方法和计算本地化有关,作为demo先忽略掉。

package org.apache.spark.rdd

import org.apache.spark.internal.Logging
import org.apache.spark.util.NextIterator
import org.apache.spark.{Partition, SparkContext, TaskContext}

/**
  * 自定义的RDD
  *
  * @param sc spark上下文
  */
class IntegerRDD(sc: SparkContext) extends RDD[java.lang.Integer](sc, Nil) with Logging {

  override protected def getPartitions: Array[Partition] = {
    (0 until 10).map { i =>
      val start = i * 10000
      val end = (i + 1) * 10000 - 1
      new IntegerPartition(i, start.toInt, end.toInt)
    }.toArray
  }

  override def compute(split: Partition, context: TaskContext): Iterator[java.lang.Integer] = new NextIterator[Integer] {

    context.addTaskCompletionListener{ context => closeIfNeeded() }

    private val integerPartition = split.asInstanceOf[IntegerPartition]
    var integerData = new IntegerData(integerPartition.index, integerPartition.lower, integerPartition.upper)

    /**
      * 注意:
      * 1.
      * @return
      */
    override protected def getNext(): Integer = {
      if (integerData.hasNext) {
        integerData.next()
      } else {
        finished = true
        null.asInstanceOf[Integer]
      }
    }

    /**
      * 关闭资源,但是spark无法保证资源一定被关闭
      */
    override protected def close(): Unit = {
    }
  }

}

private[spark] class IntegerPartition(idx: Int, val lower: Int, val upper: Int) extends Partition {
  override def index: Int = idx
}

说明:

       1. getPartitions返回partition列表,每一个partition都负责10000条数据的捞取工作,为了保证10个partition有序工作,给每一个partition画一个圈子。

       2. compute是核心,它的目的是返回数据的迭代器。我们遇到的数据很大,都会分页拉取,所以看起来是一个连续的iterator本质上都是即连续有离散的数据。这个我们可以根据自己的实际情况进行扩展。

       3. NextIterator是spark提供的类,它帮助我们封装了一部分工作。值的注意的是,close方法会在迭代器到达最后一个元素时调用,但是spark无法保证一定成功,原因也很简单,因为异常任何时间都会出现,这需要我们在可能出现异常的地方自己处理掉。context.addTaskCompletionListener{ context => closeIfNeeded() }这一句很有必要

      4. 核心捞取数据部分使用java编写

package org.apache.spark.rdd;

/**
 * @author yufei.liu
 */
public class IntegerData {

    private int partitionId;

    private int start;

    private int end;

    private int currentIndex;

    public IntegerData(int partitionId, int start, int end) {
        this.partitionId = partitionId;
        this.start = start;
        this.end = end;
        currentIndex = start;
    }

    public boolean hasNext() {
        return currentIndex <= end;
    }

    public int next() throws Exception {
        if (currentIndex <= end) {
            return currentIndex++;
        }
        throw new Exception();
    }

    @Override
    public String toString() {
        return "IntegerData{" +
                "partitionId=" + partitionId +
                ", start=" + start +
                ", end=" + end +
                ", currentIndex=" + currentIndex +
                '}';
    }

}

四、测试

JavaRDD<Integer> javaRDD1 = new IntegerRDD(sparkContext.sc()).toJavaRDD();
JavaRDD<Integer> javaRDD2 = new IntegerRDD(sparkContext.sc()).toJavaRDD();
long count = javaRDD1.union(javaRDD2).distinct().count();
System.out.println(count);

执行代码:

spark 自定义metrics spark 自定义rdd_自定义_02

我们的逻辑很简单,创建两个RDD,然后将这两个RDD并起来,做一次去重和计数。这里面涉及到四个操作createRDD、union、distinct(map/reduceByKey)、count。所以真正会执行compute只有distinct和count。上面的截图是distinct的stage执行计划,每执行一次compute都会创建一个task,所以task的数量一共是2 * 10 = 20个task,当然distinct操作shuttle之后也是10个partition,所以count也会发起20个task任务,最终结果一定是100000。

五、总结

上一段时间需要使用spark streaming同步数据从sls到kudu,由于sls数据量太大,但是我需要的数据只有其中的很小一部分,所以采用了自定义receiver完成了该功能。这样具备了一种能力:使用spark streaming从任何数据源同步数据到任何数据源。并且自己和使用单机处理每秒百万级别数据的能力(只是本地mock数据)。并且这样操作对spark streaming理解深入了不少。