Spark Core笔记

Spark Core笔记

Spark


一、What is Spark

Apache Spark is a fast and general engine for large-scale data processing

二、Spark框架优势

数据结构:RDD,用于存储和管理数据

  • DAG 调度
    Spark 中每个Job运行的调度都是DAG调度
    DAG:有向无环图
  • 计算过程中中间结果临时数据(RDD)
    保存在内存中(除去产生Shuffle的RDD)

三、Ways to run Spark

  • Local Mode
  • Spark Standalone
  • Hadoop Yarn
  • Apache Mesos

1、大家喜欢Spark编程 ,如下四个方面:

  • 代码:
    • 很少、很少,在业务逻辑上 函数式编程,简洁
  • 开发:
    • 开发测试很简单
    • 由于Spark框架使用Scala语言编写,提供Scala Shell交互式命令,此外也支持Python语言,也提供命令行
      在Windows系统上直接开发测试,不需要配置任何插件
  • 监控:
    运行每一个程序,都有自己的一个监控页面,端口号4040
  • 运速快:
    比MapReduce快很多

2、Runs Everywhere

  • a. Spark 框架开发的程序运行在哪里???
    如果使用Java/Scala语言编程,打成 JAR包
    • 本地模式
      Local Mode - 用于开发测试
    • 集群模式
      Hadoop YARN、Apache Mesos、Spark Standalone
  • b. Spark 处理数据在哪里???
    HDFS、Hive、HBase、ORC、Parquet、CSV、TSV、JDBC、Redis、MongoDB、ES、。。。。。

词频统计演示

val inputRDD = sc.textFile("/datas/wordcount.data")
val wordCountRDD = inputRDD.flatMap(_.split("\\s+")
    .map(_.trim))
    .map(word => (word, 1)).reduceByKey(_ + _)
wordCountRDD.saveAsTextFile("/datas/spark-wc-output")
wordCountRDD.foreach(println)

3、Spark Standalone Cluster

  • Master
    • 接收Worker的注册请求,统筹记录所有Worker的CPU、Memory等资源,并跟踪Worker节点的活动状态
    • 接受Driver中App的注册请求(这个请求有Driver端的client发出),为App在Worker上分配CPU、Memory等资源,生成后台Executor进程,之后跟踪Executor和App的活动状态
  • Worker
    • 负责接收Master的指示,为App创建Executor进程。Worker在Master和Executor之间起着桥梁作用,实际不会参与计算工作
  • Driver
    • 负责用户侧逻辑处理
  • Executor
    • 负责计算,接收并执行由App划分的Task任务,并将结果缓存在本地内存或磁盘

四、RDD(Resilient Distributed Dataset 弹性分布式数据集)

1、就是一个集合

  • 从使用的角度来看
    • 当做Scala语言集合类中的列表List
  • 实质
    • 分布式、存储数据、集合
    • Represents an immutable,partitioned collection of elements that can be operated on parallel(将集合中的数据划分为很多分区(partition),不同分区的数据存储在不同的机器上,每个分区的数据可以被一个Task进行处理)
  • 如果从HDFS上读取数据,Spark程序运行在集群模式下,一个block数据对应一个分区Partition数据
  • RDD 五个主要特征(特征)
    • A list of partitions
      • RDD = List
    • A function for computing each split
      • split = partition
      • 每个分区的数据都可以应用函数进行计算
    • A list of dependencies on other RDDs
      • List

2、创建RDD

将要处理的数据转换为RDD

  • 从HDFS/LocalFS 读取数据
    sc.textFile("…")/spark.read.textFile("…")
  • 并行化Scala中集合
    seq: Seq[T],
    numSlices: Int = defaultParallelism
    ): RDD[T] ```
    
  • 应用RDD中的Transformation转换函数
    • 将一个RDD转换为另外一个RDD

示例(Spark开发经典案例WordCount):

package com.erongda.bigdata.spark.core

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * Spark 开发大数据经典案例,词频统计Wordcount
  */
object SparkWordCount {

  def main(args: Array[String]): Unit = {

    // Spark Application运行时的相关配置信息,比如AppName,Master
    val sparkConf = new SparkConf()
      .setAppName("Spark Application")
      //设置应用运行在哪里,是本地模式还是集群(具体制定的地址)
      .setMaster("local[2]")

    // 创建SparkContext对象,主要用于读取处理的数据,封装在集合RDD中,调度Job
    val sc = new SparkContext(sparkConf)
    sc.setLogLevel("WARN")

    /**
      * 第一步 数据的读取(输入input)
      */
    val inputRDD: RDD[String] = sc.textFile("/datas/wordcount.data")
    //查看样本数据
    println(s"count = ${inputRDD.count()}")
    println(s"first = \n\t ${inputRDD.first()}")

    /**
      * 第二步 数据的处理
      */
    // 内功
    val wordCountRDD: RDD[(String, Int)] = inputRDD.flatMap(_.split("\\s+")).map((_, 1)).reduceByKey(_ + _)

    // 基本
    inputRDD
      .flatMap(line => line.split("\\s+"))
      .map(word => (word, 1))
      .reduceByKey((tmp, item) => tmp + item)

    // 按照统计词频count进行降序排序
    /**
      * def sortByKey(ascending: Boolean = true, numPartitions: Int = self.partitions.length)
      *     从函数的名称sortByKey:
      *       按照Key进行排序的
      *     第一个参数“
      *       ascending = true 默认值,表示暗战升序排序
      */
    println("======== sortByKey ========")
    val  sortedWordCountRDD = wordCountRDD.map(_.swap).sortByKey(ascending = false)
    sortedWordCountRDD.take(3).foreach(println)

    println("========= top ============")
    /**
      * def top(num: Int)(implicit ord: Ordering[T]: Array[T])
      *     takeOrdered(num)(ord.reverse)
      */
    wordCountRDD.top(3)(OrderUtils.SecondValueOrdering).foreach(println)

    /**
      * TODO:
      *     在企业中使用SparkCore RDD 来分析数据
      *       -a. 如果获取最大的前几个TopKey
      *         rdd#top
      *       -b. 如果获取最小的前几个BottomKey
      *         rdd#takeOrdered()
      */

    /**
      * 第三步 数据的保存(输出output)
      */
    // 查看处理后的数据
    println("=========== 原始数据 ============")
    wordCountRDD.foreach(println)

    Thread.sleep(1000000)
    // 关闭资源
    sc.stop()
  }
}

/**
  * 自定义的排序规则,依据实际需求定义
  */
object OrderUtils{
  object SecondValueOrdering extends scala.math.Ordering[(String, Int)]{
    override def compare(x: (String, Int), y: (String, Int)): Int = {
      x._2 - y._2
    }
  }
}

五、RDD集合类中的函数(80多种,可以分为三类)

1、转换函数(Transformations)重点

特点

  • return a new RDD
  • 一个RDD调用函数以后转换为另外一个RDD

2、Action函数

特点

  • launch a job to return a value to the user program
  • 当一个RDD调用函数以后,就会触发一个Job的执行,不会转换为RDD

如:
count -> Long, first -> 集合中第一条数据
take -> 获取集合中前N条数据
foreach -> 对每条进行操作,比如答应数据,无返回值

3、Persistence()

持久化函数:将RDD集合中的数据缓存到内存或磁盘中

如:


def cache(): this.type = persist()
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
StorageLevel:
    表示的将RDD数据存储的地方(存储级别)
class StorageLevel private(
    // 将数据存储到磁盘中
    private var _useDisk: Boolean,
    // 将数据存储到内存中
    private var _useMemory: Boolean,
    // JVM 内存中,Alluxio
    private var _useOffHeap: Boolean,
    // 数据存储的时候是否反序列化
    private var _deserialized: Boolean,
    // 持久化数据的副本数,默认值为1
    private var _replication: Int = 1
)
  • Alluxio:分布式内存文件系统
    类似于HDFS分布式文件系统,不过Ta将数据存储在内存中

  • 在什么情况 ,将RDD数据持久化呢???

    • 某个RDD数据,被使用多次重复使用
    • 某个RDD的数据来之不易,使用超多一次,经过复杂处理获取的RDD
    • 通常选择缓存数据策略
      • MEMORY_ONLY_2 如果集群内存足够充分
      • MEMORY_AND_DISK_SER_2 先内存后磁盘,2副本数
  • 释放存储数据

    • def unpersist(blocking: Boolean = true): this.type
  • RDD 中Aciton函数

    • 调用函数以后,返回一个非RDD的值Driver
      count/first/take/collect
    • 调用函数以后,没有返回值
      • 打印、存储外部系统 foreach:
        • 针对RDD中每条数据进行操作的
      • 但是我们建议使用foreachPartition:
        • 针对RDD中每个分区的数据进行操作的
  • 在RDD中调整分区数的大小函数:

    • coalesce:调整分区数,不进行Shuffle,性能较好
      最原始的,源码如下:
     numPartitions: Int, 
     shuffle: Boolean = false
 ): RDD[T] ```
   - repartition:进行Shuffle操作,性能所有消耗,不建议使用
   ```def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]```
       - 底层:````coalesce(numPartitions, shuffle = true)```
      
示例(SparkSessionWordCount):
```spark
package com.erongda.bigdata.spark.core

import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession

/**
 * Created by Administrator on 2018/7/16.
 */
object SparkSessionWordCount {

 def main(args: Array[String]): Unit = {
   // 创建SparkSession实例对象
   val spark: SparkSession = SparkSession.builder()
     .appName("SparkSessionWordCount")
     .master("local[2]")
     .getOrCreate()

   // 设置日志级别
   spark.sparkContext.setLogLevel("WARN")

   /**
     * 使用SparkSession读取数据
     */
   val inputRDD: RDD[String] = spark.read.textFile("/datas/wordcount.data").rdd
   // inputRDD.cache()  数据缓存
   // inputRDD.unpersist() 释放缓存
   // 数据处理
   val wordCount: RDD[(String, Int)] = inputRDD.flatMap(_.split("\\s+").map((_, 1)))  // 性能高
     .reduceByKey(_ + _)

   wordCount.foreach(println)

   // 为了4040
   Thread.sleep(1000000)

   // 关闭资源
   spark.close()

 }

}

示例(SessionTimeCountSpark):

package com.erongda.bigdata.spark.core

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * Created by Administrator on 2018/7/16.
  * 分析用户 Session会话时长, 各个时段占比
  */
object SessionTimeCountSpark {

  // 会话时长:0 - 10 秒
  val TIME_LENGTH_0010: String = "0-10"
  // 会话时长: 11 - 20 秒
  val TIME_LENGTH_1120: String = "11-20"
  // 会话时长: 21 - 30 秒
  val TIME_LENGTH_2130: String = "21-30"
  // 会话时长: 30+ 秒
  val TIME_LENGTH_3000: String = "30+"

  def main(args: Array[String]): Unit = {

    // Spark Application运行时的相关配置信息,比如AppName, Master
    val sparkConf = new SparkConf()
      .setAppName("SessionTimeCountSpark")
      .setMaster("local[2]")

    // 创建SparkContext实例对象,主要用于读取处理的数据,封装集合到RDD中,调度Job
    val sc = new SparkContext(sparkConf)
    sc.setLogLevel("WARN")

    // TODO: 1. 读取数据,从本地系统LocalFileSystem读取数据
    val pageViewRDD = sc.textFile("file:///C:/spark-learning/datas/page_views.data")

    // 将RDD集合的数据存储到内存中,属于lazy方法,需要一个Action函数触发,才会将数据真正的缓存
    pageViewRDD.cache()

    // 采用数据,获取第一条数据和总的条目数
    println(s"count = ${pageViewRDD.count()} \nFirst: \n\t${pageViewRDD.first()} ")

    /**
      * 2.需求分析:
      *   统计 各个会话时长段 0-10,11-20,21-30,30+
      * 关键点:
      *   1. 统计各个会话时长
      *     按照session_id进行分组,得到每个会话中的所有的track_time, 使用最后一个track_time - 第一个track_time获取时长
      *   2. 判断会话时长属于哪个 时间段
      */
    val timeLengthCountRDD: RDD[(String, Int)] = pageViewRDD
      .map(line => {
        //分割单词
        val arr = line.split("\t")
        // 将track_time日期类型的数据(2013-05-19 13:00:00)转换为Long类型,以便后续操作
        import java.text.SimpleDateFormat
        val time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(arr(0)).getTime
        //返回
        (arr(2), time)
      })
      // TODO: b. 按照session_id进行分组,得到每个session会话中的所有页面的访问时间
      .groupByKey()  // RDD[(String, Iterable[Long])]
      // TODO: c.计算每个会话的时长
      .map{case (session_id: String, iter: Iterable[Long]) =>
          // 最大的和最小的track_time
        val maxTrackTime = iter.max
        val minTrackTime = iter.min
        // 计算会话时长
        val timeLength = (maxTrackTime - minTrackTime) / 1000.0

        // 判断时长属于哪个时长段
        if (30 < timeLength){
          (TIME_LENGTH_3000, 1)
        }else if(20 < timeLength){
          (TIME_LENGTH_2130, 1)
        }else if(10 < timeLength){
          (TIME_LENGTH_1120, 1)
        }else{
          (TIME_LENGTH_0010, 1)
        }
      }
      // TODO: d. 聚合统计,计算出各个时长段的会话个数
      .reduceByKey(_ + _)

    // 显示结果
    timeLengthCountRDD.foreach(println)

    // 将RDD的数据从缓存中释放出来
    pageViewRDD.unpersist()

    // 为了开发测试 ,对每Application运行左监控,所以当前线程休眠
    Thread.sleep(100000)

    // 关闭资源
    sc.stop()

  }
}

示例(SparkWordCountToMySQL):

package com.erongda.bigdata.spark.core

import java.sql.{Connection, DriverManager}

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * Spark 开发大数据经典案例,词频统计Wordcount
  */
object SparkWordCountToMySQL {

  def main(args: Array[String]): Unit = {

    // Spark Application运行时的相关配置信息,比如AppName,Master
    val sparkConf = new SparkConf()
      .setAppName("Spark Application")
      //设置应用运行在哪里,是本地模式还是集群(具体制定的地址)
      .setMaster("local[2]")

    // 创建SparkContext对象,主要用于读取处理的数据,封装在集合RDD中,调度Job
    val sc = new SparkContext(sparkConf)
    sc.setLogLevel("WARN")

    /**
      * 第一步 数据的读取(输入input)
      */
    val inputRDD: RDD[String] = sc.textFile("/datas/wordcount.data")

    val wordCount: RDD[(String, Int)] = inputRDD.flatMap(_.split("\\s+").map((_, 1)))  // 性能高
      .reduceByKey(_ + _)

    wordCount.coalesce(1)
      .foreachPartition(iter => {
        Class.forName("com.mysql.jdbc.Driver")

        val url ="jdbc:mysql://bigdata-training01.erongda.com:3306/test"
        val username = "root"
        val password = "123456"

        var conn: Connection = null
        try{
          conn = DriverManager.getConnection(url, username, password)

          val sql = "INSERT INTO rdd_word_count (word, count) values(?,?)"
          val pst = conn.prepareStatement(sql)

          iter.foreach{
            case (word: String, count: Int) =>
              println(s"word = $word, count = $count")

              pst.setString(1, word)
              pst.setInt(2, count)

              pst.executeUpdate()
          }

        }catch{
          case e: Exception => e.printStackTrace()
        }finally {
          if(conn != null) conn.close()
        }
      })

    Thread.sleep(1000000)
    // 关闭资源
    sc.stop()
  }
}

六、Spark Standalone

Spark框架自身带的分布式集群资源管理和任务调度的框架,类似Hadoop YARN框架

1、如何配置Spark Standalone Cluster (伪分布式)

  • 配置
    conf/spark-env.sh 增加如下内容:
        SPARK_MASTER_HOST=bigdata-training01.erongda.com
        SPARK_MASTER_PORT=7077
        SPARK_MASTER_WEBUI_PORT=8080
        SPARK_WORKER_CORES=2
        SPARK_WORKER_MEMORY=2g
        SPARK_WORKER_PORT=7078
        SPARK_WORKER_WEBUI_PORT=8081
  • 配置从节点Workers
    conf/slaves 内容:
    bigdata-training01.erongda.com
  • 启动服务
    • 启动主节点Master服务
      $ sbin/start-master.sh
    • 启动从节点Workers服务 - 启动所有的Workers服务
      $ start-slaves.sh
      • 注意:
        此命令必须在Master主节点上执行,并且需要配置Master节点到所有Worker节点SSH无秘钥登录
        $ ssh-keygen -r rsa
        $ ssh-copy-id bigdata-training01.erongda.com
  • 测试
    运行一个spark-shell
    到SparkStandalone Cluster上运行
    bin/spark-shell --master spark://bigdata-training01.erongda.com:7077

七、大数据集群架构和数据规模

1、集群规模

  • 数据量:每天增量

    • 记录数:
      千万级别以上:单个访客访问
      亿级别记录数
      百万级别:
      千万级别记录数
      几十万:
      一千万级别
    • 大小值:
      记录数*每条记录的大小
  • 设计集群规模
    每天增量3653*3 / 12 = 从节点个数
    中小型规模:20台
    中型规模:50台
    大型规模:100台以上

  • 硬件选型

    • 主节点:
      • CPU:32核
      • 内存:128GB
      • 硬盘:SATA,SCSI,推荐SAS/SSD
        • RAID0:
          机器:disk1 2T disk2 2T
          系统:disk 4T
          写数据时,随机写入两块硬盘
        • RAID1
          机器:disk1 2T disk2 2T
          系统:disk 2T
          写数据时,写两份
        • JBOD:不属于raid
          机器:disk1 2T disk2 2T
          系统:disk 4T

写数据时,先写第一块硬盘,直到第一块满了,才写第二块

2、大数据运维部署

  • 部署运维
    • 手动分发
      同步服务:rsync

    • CM:cloudera Manager

      • 分布式架构:主从
      • 安装cm
        • 安装一台机器的cm
        • 分发给其他机器
        • 启动cm
        • 勾选当前哪些机器构建集群
        • 勾选需要安装的程序
        • 选择每个进程在那台机器上
        • 下一步,根据用户的选择,自动实现安装
    • 技术选型

      • 因素:
        • 业务需求
        • 参考
      • 业务需求
        • 架构
          • 离线批处理
          • 实时
        • 应用
          • 数据分析
          • 用户画像
          • 推荐系统
          • 推荐预测
          • 数据挖掘
          • 机器学习
        • 业务流程
          • 数据采集
          • 数据存储:
            • 持久性存储
            • 缓存
            • 数据仓库
          • 数据处理
            • 离线批处理
            • 实时处理
            • 交互式处理:spark SQL 、hive、impala、presto、kylin

八、Spark Application应用在集群上

1、Driver Program - JVM Process

相当于AppMaster,应用管理者,主要调度Job执行
就是每个程序的main方法,必须创建SparkContext实例对象
端口号:4040 ,提供应用的监控

2、Executors

每个Executor是一个JVM Process(JVM 进程,相当于线程池),包含Memory和CPU Core,运行Tasks。
Spark中Task是以线程Thread方式运行的,每个Task执行需要1 Core CPU。

九、Spark Application与MapReduce Application 区别

每个Job(作业)都有很多Task(任务)进行计算

1、对于MapReduce Application来说

  • bin/yarn jar …运行的一个MapReduce程序,其实就是一个MapReduce Application在运行。一个MR App就是一个MR Job(一个应用只有一个Job)。
  • 无论是MapTask还是ReduceTask运行在JVM Process。

2、对于Spark Application来说

  • 一个Application中有很多Job。
  • 每个Task运行在一个线程Thread中,都需要1 Core CPU。

十、RDD 中数据持久化操作(属于lazy操作)

需要RDD#Action函数调用(Job 触发)才能进行持久化操作

注意:
Action函数必须对RDD中所有分区的数据进行操作,才能将RDD中所有分区的数据进行缓存。
-a. first()函数仅仅获取RDD中某个分区的一条数据,所以仅仅 将一个分区中的数据进行缓存
-b. 可以使用count()函数对RDD中所有的分区数据进行统计个数,来进行缓存数据(触发缓存)

十一、对于RDD中某些函数的使用注意:(优化)

  • 能不使用groupByKey函数就不要使用,除非不得已
    • 知道:
      • groupByKey + map = reduceByKey: combiner
    • reduceByKey:
      • 包含分组,在聚合的时候,先进行本地聚合,然后在进行分组中的聚合。
  • 尽量使用xxParition函数代替xx函数
    • 比如:
      • foreach 与 foreachPartition
      • map 与 mapPartition
        def map[U: ClassTag](f: T => U): RDD[U] f -> 针对RDD中每个元素进行操作 def mapPartitions[U: ClassTag](f: Iterator[T] => Iterator[U]): RDD[U] f -> 针对RDD中每个分区中的元素进行操作的
  • 适当调整RDD的分区数
    • 在Spark 程序的运行中,一个Thread线程运行一个Task任务,一个Task处理一个分区Parition的数据。
      • Thread = Task = Partition
    • 方式一:
      在读取数据的时候,就可以调整分区,通常加大分区数目
      sc.textFile("", minPartitions = number)
      sc.parallelize(seq, numSlices = number)
    • 方式二:
      可以通过RDD直接调整分区数
      rdd.coalesce(numPartitions = number)
    • 关键点:
      对结果RDD或者数据预处理以后的RDD降低分区数
  • 数据多次使用,进行缓存

十二、数据倾斜

1、何为数据倾斜

  • 某些Task分析数据过多,某个Task分析数据过少,导致处理过程中运行的速度和效率是不一样,或者出现内存不足溢出情况。

2、回顾Hive中数据倾斜的处理:

  • Hive 中某个MapReduce Job运行的发生数据倾斜方案:
    • 将一个MapReduce Job程序变成两个MapReduce Job程序来执行
    • 具体方案:
      http://www.cnblogs.com/ggjucheng/archive/2013/01/03/2842860.html
    • 分阶段聚合:
      • set hive.groupby.skewindata = true;
      • 第一个MapReduce Job进行 局部聚合
        给Key加上 指定范围 的随机数
      • 第二MapReduce Job进行全局聚合
        将Key前缀随机数去除,全局聚合

3、RDD中四大聚合函数,源码分析,基本参数

  • 第一个"聚合"函数:

    • def groupByKey(): RDD[(K, Iterable[V])] - 643 行代码
      • def combineByKeyWithClassTag[C] - 74行
  • 第二个"聚合"函数:

    • def reduceByKey(func: (V, V) => V): RDD[(K, V)] - 328

      • def combineByKeyWithClassTag[C] - 74
    • "聚合"函数 , 类似Scala中Listfold聚合函数 -243

      • def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]
        • def combineByKeyWithClassTag[C] - 74
  • 第三个"聚合"函数:

    • def aggregateByKey[U: ClassTag](zeroValue: U) - 204 (zeroValue: U) // 聚合中间临时变量初始化 ( // 每个分区的聚合操作 seqOp: (U, V) => U, // 合并所有分区的聚合结果 combOp: (U, U) => U ): RDD[(K, U)]
      • def combineByKeyWithClassTag[C] - 74
  • 第四个"聚合"函数:

    • def combineByKey[C] - 613 行 ( createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C ): RDD[(K, C)]
      • def combineByKeyWithClassTag[C] - 74

4、最最底层"聚合"函数的含义

def combineByKeyWithClassTag[C] ( // 创建 合并器:确定如何聚合合并Value的值, 初始化操作,聚合中间临时变量的数据类型 createCombiner: V => C, // 针对每个分区中V的值进行聚合操作 mergeValue: (C, V) => C, // 合并 各个分区聚合的结果 mergeCombiners: (C, C) => C, partitioner: Partitioner, // 指定分区器 mapSideCombine: Boolean = true, // 默认不问 serializer: Serializer = null // 默认值,不问 ) (implicit ct: ClassTag[C]): RDD[(K, C)]

十三、如何提交运行Spark Application呢???

${SPARK_HOME}/bin/spark-submit 脚本运行提交所有的应用

  • 如何使用spark-sumbit:
    spark-submit [options] [app arguments]
    • 第一点:
      如果是Scala语言或者Python语言开发的程序,需要打成JAR包运行;如果是Python语言开发,直接指定脚本运行即可。
    • 第二点:
      • options可选项,主要用于配置应用运行时相关参数设置,如
        • a. 应用运行在哪里????
          --master MASTER_URL
          • 值:local[2], spark://…:7077, yarn,…
        • b. 指定Driver Program
          • 无论程序是运行在本地模式还是运行在集群上,都有Driver,指定资源(Memory和CPU Core)
        • c. 如果运行在集群上,指定Executor相关信息
          Executor个数,每个Executor资源配置
    • 第三点:
      app arguments,表示的是指定应用程序运行时需要传递的参数

十四、Spark 框架中的历史服务器

1、监控运行完成的Spark Application

2、针对Spark HistoryServer来说,如何配置

  • 将SparkApplication运行的EventLog存储到HDFS上
    http://spark.apache.org/docs/2.2.0/configuration.html#spark-ui
    • spark.eventLog.enabled:
      表示是否存储EventLog,设置为true
    • spark.eventLog.compress:
      表示是否进行压缩存储的EventLog,使用压缩算法lz4
    • spark.eventLog.dir:
      表示存储EventLog目录
      hdfs://bigdata-training01.erongda.com:8020/datas/spark/eventLogs/
      • 备注:
        /datas/spark/eventLogs/必须创建出来
    • 配置以上三种属性有多种方式
      • 针对某个应用来配置属性
        • 通过提交应用参数属性–命令行配置
        • --conf spark.eventLog.enabled=true \ --conf spark.eventLog.compress=true \ --conf spark.eventLog.dir=hdfs://bigdata-training01.erongda.com:8020/datas/spark/eventLogs/ \
        • 程序编码配置
        • sparkConf.set("spark.eventLog.enabled", "true") sparkConf.set("spark.eventLog.compress", "true") sparkConf.set("spark.eventLog.dir", "hdfs://bigdata-training01.erongda.com:8020/datas/spark/eventLogs/")
      • 针对所有应用配置属性
        • 只要运行SparkApplication,就会自动保存EventLog
          ${SPARK_HOME}/spark-default.conf
  • 启动HistoryServer历史服务
    • ${SPARK_HOME}/conf/spark-env.sh 中配置:
      SPARK_HISTORY_OPTS="-Dspark.history.fs.logDirectory=hdfs://bigdata-training01.erongda.com:8020/datas/spark/eventLogs/ -Dspark.history.fs.cleaner.enabled=true"

十五、Spark Application运行在YARN集群上

http://spark.apache.org/docs/2.2.0/running-on-yarn.html

1、运行在YARN集群上

  • 启动Hadoop YARN集群
    • ResourceManager/NodeManagers
    • 向YARN集群提交应用,找到的RM服务(端口8032),告知RM所在主机
  • 配置Spark
    • conf/spark-env.sh
    • JAVA_HOME
    • SCALA_HOME
    • HADOOP_CONF_DIR: HADOOP框架配置文件所在的目录
      • 提交应用的时候,将会读取该目录下所有的属性文件
        • HDFS集群位置,读物HDFS上数据
          hdfs-site.xml core-site.xml
        • YARN集群位置(RM),提交应用到YARN上运行
          yarn-site.xml
  • 提交应用${SPARK_HOME}/bin/spark-submit
    • 以最简单的方式来演示:提交spark-shell运行在yarn上
    • SPARK_HOME=/opt/cdh-5.7.6/spark-2.2.0-bin-2.6.0-cdh5.7.6 ${SPARK_HOME}/bin/spark-shell \ --master yarn \ --driver-memory 512M \ --executor-memory 1g \ --executor-cores 2 \ --num-executors 3

2、额外回顾YARN知识

对于YARN集群来说,以下两点清楚

  • 默认情况下每个NodeManager节点服务管理的资源
    yarn-site.xml
    • 内存大小:默认值 8G = 8192M
      yarn.nodemanager.resource.memory-mb
    • CPU Core核数:默认值8核
      yarn.nodemanager.resource.cpu-vcores
      • 虚拟核数考虑:
        2 Core(i7) = 3 Core(i5)
  • YARN资源调度方式
    • 按照队列方式进行资源调度
    • 将集群资源划分为很多队列Queue方式进行调度,用户提交应用的时候,将应用提交到对应队列上,然后进行资源调度

十六、deploy-mode DEPLOY_MODE

表示的是Driver Program(JVM Process)运行在地方,如果运行在本地Localy称为client,也可以运行在集群的从节点(Worker节点或者NodeManager节点)上的某台机器上。
注意两点

  • 在企业的实际开发环境中,Spark Application提交运行,采用的是“cluster”模式运行。
  • spark-shell不能运行在Cluster Mode下
    • Driver就是REPL(SCALA)交互式的命令行

1、Spark on YARN:

  • Spark Application
    • Driver Program
      应用的管理者(获取资源和调度Job执行)
    • Executors
      运行Tasks任务和Cache缓存数据
    • YARN Application
      • 框架设计:如果一个应用运行在YARN上,首先给每个应用分区一个AppMaster(应用管理者):获取资源和调度job执行
      • Container容器:JVM Process
    • 如何设计Spark Application运行在YARN上呢??
      • Driver 运行在本地Client
        • 申请资源
          driver -> AppMaster -> RM
      • Driver 运行在集群上Cluster
        将AppMaster与Driver合并在一起
        AppMaster(Driver) -> RM

2、企业中实际问题:

CDH 版本的HADOOP 框架使用的JDK 1.7,将Spark 2.x开发应用运行在YARN上,如何处理解决JDK 版本问题呢???

解决方案:

  • 在提交应用运行的时候,指定Driver和Executor使用的JDK版本
    • 针对AppMaster(Driver)
      spark.yarn.appMasterEnv.JAVA_HOME=/opt/modules/jdk1.8.0_91
    • 针对Executors
      spark.executorEnv.JAVA_HOME=/opt/modules/jdk1.8.0_91

十七、Spark 如何与HBase进行交互

1、SparkCore如何读写HBase表中的数据????

RDD <-> HFile(StoreFile)

返璞归真:

  • SparkCore如何从HDFS上读取文本文件数据的??
    sc.textFile("/datas/wordcount") -> hadoopFile
    • 底层:调用MapReduce框架中如何从HDFS上读取数据的API
      • TextInputFormat
        • LongWritable
        • Text
  • SparkCore如何将RDD保存到HDFS文件系统上呢??
    rdd.saveAsTextFile("/datas/xxx") -> saveAsHadoopFile
    • 底层:
      - TextOutputFormat
      - NullWritable
      - Text
  • 触发思考:
    • Spark读写HBase表中的数据,本质:MapReduce从HBase表中读写数据的

2、MapReduce 读写HBase表中的数据,使用底层类

  • pom.xml加入依赖
    org.apache.hbase hbase-server 1.2.0-cdh5.7.6 org.apache.hbase hbase-hadoop2-compat 1.2.0-cdh5.7.6 org.apache.hbase hbase-client 1.2.0-cdh5.7.6

3、读取数据

  • 一条一条的读取数据,每条数据变为KeyValue对
    • TableInputFormat
      • ImmutableBytesWritable
      • Result
  • 对于Spark来说:
    def newAPIHadoopRDD[K, V, F <: NewInputFormat[K, V]]( conf: Configuration = hadoopConfiguration, fClass: Class[F], kClass: Class[K], vClass: Class[V]): RDD[(K, V)]

示例:

package com.erongda.bigdata.spark.hbase

import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase.{CellUtil, HBaseConfiguration}
import org.apache.hadoop.hbase.client.Result
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.hadoop.hbase.util.Bytes
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * Spark Core从HBase表中读取数据:
  *     表的名称: ns1:sale_orders
  */
object ReadSaleOrdersSpark {

  def main(args: Array[String]): Unit = {

    // Spark Application运行时的相关配置信息,比如AppName, Master
    val sparkConf = new SparkConf()
      .setAppName("ReadSaleOrdersSpark")
      // 设置应用运行在哪里,是本地模式还是集群(具体指定的地址)
      .setMaster("local[5]")

    /**
      * 设置Spark Application 序列化方式使用Kryo
      *   默认情况下,对simple types, arrays of simple types, or string type 使用Kryo方式序列化
      */
    sparkConf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
    // 告知哪些类型进行序列化
    sparkConf.registerKryoClasses(Array(classOf[ImmutableBytesWritable], classOf[Result]))

    // 创建SparkContext实例对象, 主要用于读取处理的数据,封装集合RDD中,调度Job
    // val sc = new SparkContext(sparkConf)

    val sc: SparkContext = SparkContext.getOrCreate(sparkConf)
    /**
      * def newAPIHadoopRDD[K, V, F <: NewInputFormat[K, V]](
          conf: Configuration = hadoopConfiguration,
          fClass: Class[F],
          kClass: Class[K],
          vClass: Class[V]
        ): RDD[(K, V)]
      */
    // a. 读取配置信息
    val conf: Configuration = HBaseConfiguration.create()

    // b. 设置从HBase哪张表读取数据
    conf.set(TableInputFormat.INPUT_TABLE, "ns1:sale_orders")

    // c. 调用SparkContext中newAPIHadoopRDD读取HBase表中的数据
    val resultRDD: RDD[(ImmutableBytesWritable, Result)] = sc.newAPIHadoopRDD(
      conf, // Configuration
      classOf[TableInputFormat], // storage format of the data to be read
      classOf[ImmutableBytesWritable], //
      classOf[Result]
    )

    // 测试获取的数据
    println(s"Count = ${resultRDD.count()}")

    /**
      * 当使用RDD.take(3).foreach() 报如下异常:ImmutableBytesWritable不能进行序列化
      *   java.io.NotSerializableException: org.apache.hadoop.hbase.io.ImmutableBytesWritable
      * Serialization stack:
      * 原因在于:
      *     RDD.take(N) 将数据从Executor中返回给Driver端,需要经过网络传输,所以需要对数据进行序列化,然而
      *   ImmutableBytesWritable 和 Result 类型都没有实现Java中序列化接口Serializable,所以出错。
      * 如何解决问题呢????
      *   Spark 大数据分析计算框架,默认情况下使用Java Serializable对数据进行序列化,设置其他序列化方式。
      */
    //
    resultRDD.take(3).foreach{ case (key, result) =>
      // 获取RowKey
      val rowKey = Bytes.toString(key.get)
      println(s"RowKey = $rowKey")

      // 获取每条数据
      for(cell <- result.rawCells()){
        // 获取列簇
        val cf = Bytes.toString(CellUtil.cloneFamily(cell))
        // 获取列
        val column = Bytes.toString(CellUtil.cloneQualifier(cell))
        // 获取值
        val value = Bytes.toString(CellUtil.cloneValue(cell))
        // 打印
        println(s"\t $cf:$column = $value -> ${cell.getTimestamp}")
      }
    }

    // 为了开发测试,对每个Application运行做监控,所以当前线程休眠
    Thread.sleep(10000000)

    // 关闭资源
    sc.stop()
  }

}

4、写入数据

  • TableOutputFormat
    • ImmutableBytesWritable
    • Put
  • 对于Spark来说:
    def saveAsNewAPIHadoopDataset(conf: Configuration): Unit

示例:

package com.erongda.bigdata.spark.hbase

import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase.HBaseConfiguration
import org.apache.hadoop.hbase.client.Put
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.TableOutputFormat
import org.apache.hadoop.hbase.util.Bytes
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * 将RDD数据存储到HBase表中,使用SparkCore中API完成
  */
object WriteDataToHBaseSpark {

  def main(args: Array[String]): Unit = {

    // Spark Application运行时的相关配置信息,比如AppName, Master
    val sparkConf = new SparkConf()
      .setAppName("SparkAppModule")
      // 设置应用运行在哪里,是本地模式还是集群(具体指定的地址)
      .setMaster("local[2]")

    // 创建SparkContext实例对象, 主要用于读取处理的数据,封装集合RDD中,调度Job
    val sc = new SparkContext(sparkConf)


    /**
      * 模拟数据
      *   将词频统计的结果RDD存储到HBase表中
      *   设计表:
      *      表的名称:ht_wordcount
      *      RowKey: word
      *      列簇:info
      *      列名:count
      */
    // 创建Scala中集合类列表 List
    val list = List(("hadoop", 234), ("spark", 4356), ("ml", 456), ("SQL", 1235))
    // 通过并行化集合创建RDD
    val wordCountRDD: RDD[(String, Int)] = sc.parallelize(list, numSlices = 1)


    /**
      * TableOutputFormat 向HBase表中写入数据,要求(Key, Value), 所以需要将RDD中的数据数据类型转换为
      *     RDD[(ImmutableBytesWritable, Put)]
      */
    val putsRDD: RDD[(ImmutableBytesWritable, Put)] = wordCountRDD
      // 数据类型转换
      .map{ case(word, count) =>
        // RowKey
        val rowKey: ImmutableBytesWritable = new ImmutableBytesWritable(Bytes.toBytes(word))
        // 创建Put对象
        val put: Put = new Put(rowKey.get())
        // 增加列
        put.addColumn(
          Bytes.toBytes("info"), // cf
          Bytes.toBytes("count"), // column
          Bytes.toBytes(count.toString)
        )
        // 返回二元组
        (rowKey, put)
      }

    // TODO: 读取配置信息
    val conf: Configuration = HBaseConfiguration.create()


    // a. 设置数据保存表的名称
    conf.set(TableOutputFormat.OUTPUT_TABLE, "ht_wordcount")
    // b.  设置OutputFormat
    conf.set("mapreduce.job.outputformat.class",
      "org.apache.hadoop.hbase.mapreduce.TableOutputFormat")
    // c. 设置输出目录
    conf.set("mapreduce.output.fileoutputformat.outputdir",
      "/datas/spark/hbase/htwc-" + System.currentTimeMillis())

    // TODO: 调用RDD中方法,将数据保存到HBase表中
    putsRDD.saveAsNewAPIHadoopDataset(conf)


    // 为了开发测试,对每个Application运行做监控,所以当前线程休眠
    Thread.sleep(10000000)

    // 关闭资源
    sc.stop()
  }

}

5、对于MapReduce框架来说,新旧API,以hadoop-0.20.0版本

  • 旧API
    Mapper、Reducer都是接口
    org.apache.hadoop.mapred
  • 新API
    Mapper、Reducer都是类
    org.apache.hadoop.mapreduce

十八、基于Spark电商用户行为分析(驴妈妈)

1、基于HADOOP生态系统框架离线数据分析

  • 项目架构:分为三层架构
    • 基于CDH 5.x版本HADOOP生态组件,8台机器
  • 数据收集
    • 定制化收集数据,自定义SDK(Java SDK、JsSDK或Ios SDK、Android SDK)
    • SDK -> Nginx -> Flume -> HDFS
    • 每天一个日志文件,针对每台Nginx服务器,15GB
  • 数据处理
    • 数据预处理 - 数据ETL
      • 过滤清洗垃圾数据
      • 转换数据格式
      • hdfs -> mapreduce/spark -> hbase
    • 数据分析
      • 基于MR/Spark 分析 -> 75%
        • hbase -> mr/spark -> mysql
      • 基于Hive/SparkSQL分析 -> 25%
        • hbase -> hive -> HiveQL -> Sqoop -> MySQL
        • hbase -> SparkSQL -> MySQL
  • 分析结果展示
    • SSM + MySQL + Maven + HighCharts(ECharts)

2、HBase 数据库

  • 基于HDFS之上NoSQL、面向列存储的、多版本的海量数据存的数据库
    • 数十亿行数据 * 数百万列
  • 两个功能
    • 存储数据
      • 每条数据字段类型不一样,数据量很大
    • 检索数据
      • RowKey检索
  • 关键点
    • RowKey 设计与业务

3、项目准备工作

  • 日志数据存储到HDFS
    • HDFS 服务启动起来
  • ETL数据存储到HBase表中
    • Zookeeper 启动
    • HBase 服务启动
      • Java API -> 批量读数据、写数据到表中(MapReduce)
    • 游戏公司,往往使用Python语言开发代码
      • 将游戏数据写入到HBase表中或从HBase表中读取数据
      • bin/hbase-daemon.sh start thrift
  • Spark 本地开发测试

4、在Spark程序运行中每个Executor内存的使用

http://spark.apache.org/docs/2.2.0/tuning.html#memory-management-overview

  • 内存:计算数据
    • Tasks运行需要内存
  • 内存:存数据
    • RDD缓存、广播变量缓存

比例的划分,在不同的应用运行的时候,适当考虑分配。

比如:
(默认情况下: 属于值的比例限定)

  • 计算数据的内存:
    • spark.memory.fraction = 0.6 -> 6G
  • 存储数据的内存:
    • spark.memory.storageFraction = 0.5 -> 5G

代码(ETL):


package com.erongda.bigdata.project.etl

import java.util
import java.util.zip.CRC32

import com.erongda.bigdata.project.common.EventLogConstants
import com.erongda.bigdata.project.common.EventLogConstants.EventEnum
import com.erongda.bigdata.project.util.{LogParser, TimeUtil}
import org.apache.commons.lang.StringUtils
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase.{HBaseConfiguration, HColumnDescriptor, HTableDescriptor, TableName}
import org.apache.hadoop.hbase.client.{Connection, ConnectionFactory, HBaseAdmin, Put}
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.io.compress.Compression
import org.apache.hadoop.hbase.mapreduce.TableOutputFormat
import org.apache.hadoop.hbase.util.Bytes
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * 基于Spark框架读取HDFS上日志文件数据,进行ETL操作,最终将数据插入到HBase表中
  *   -1. 为什么选择ETL数据到HBase表中??
  *     采集的数据包含很多Event类型的数据,不同Event时间类型的数据字段不一样,数据量相当比较大
  *   -2. HBase 表的设计???
  *     -a. 每天的日志数据,ETL到一张表中
  *         本系统,主要针对日志数据进行分析的,基本上每天的数据分析一次,为了分析更快,加载更少的数据
  *     -b. 每次ETL数据的时候,创建一张表
  *         -i. 创建表的预分区,使用的数据在不同Region中,减少些热点,避免Region Split
  *         -ii. 可以考虑表中数据的压缩,使用snappy压缩或lz4压缩
  *     -v. RowKey设计原则:
  *         - 唯一性(不重复)
  *         - 结合业务考虑
  *           某个EventType数据的分析,某个时间段数据的分析
  *         RowKey = 采用服务器时间戳  +  crc32(用户ID、会员ID、事件名称)
  */
object EtlToHBaseSpark {


  /**
    * RowKey 创建
    * @param time
    *             服务器时间
    * @param uUID
    *             用户ID
    * @param uMD
    *            用户会员ID
    * @param eventAlias
    *                   事件Event名称
    * @return
    */
  def createRowKey(time: Long, uUID: String, uMD: String, eventAlias: String): String = {
    // 创建StringBuilder实例对象,用于拼接字符串
    val sBuilder = new StringBuilder()
    sBuilder.append(time + "_")

    // 创建CRC32实例对象,进行字符串编码,将字符串转换为Logn类型数字
    val crc32 = new CRC32()
    // 重置
    crc32.reset()
    if(StringUtils.isNotBlank(uUID)){
      crc32.update(Bytes.toBytes(uUID))
    }
    if(StringUtils.isNotBlank(uMD)){
      crc32.update(Bytes.toBytes(uMD))
    }
    if(StringUtils.isNotBlank(eventAlias)){
      crc32.update(Bytes.toBytes(eventAlias))
    }

    sBuilder.append(crc32.getValue % 100000000L)
    // return
    sBuilder.toString()
  }

  /**
    * 创建HBase表,创建的时候判断是否已经存在,存在的话先删除后创建
    * @param processDate
    *                    要处理哪天数据的日期,格式:2018-07-22
    * @param conf
    *             HBase Client 要读取的配置信息
    * @return
    *         表的名称
    */
  def createHBaseTable(processDate: String, conf: Configuration): String = {
    // create 'event_logs20151220', 'info'
    val time = TimeUtil.parseString2Long(processDate)
    val date = TimeUtil.parseLong2String(time, "yyyyMMdd")

    // table name
    val tableName = EventLogConstants.HBASE_NAME_EVENT_LOGS + date

    // 创建表,先判断是否存在
    var conn: Connection = null
    var admin: HBaseAdmin = null

    try{
      // 获取连接
      conn = ConnectionFactory.createConnection(conf)
      // 获取HBaseAdmin实例对象
      admin = conn.getAdmin.asInstanceOf[HBaseAdmin]

      // 判断表是否存在
      if(admin.tableExists(tableName)){
        // 表存在,先禁用,后删除
        admin.disableTable(tableName)
        admin.deleteTable(tableName)
      }

      // 创建TableDesc描述符
      val desc = new HTableDescriptor(TableName.valueOf(tableName))
      // 创建表的列簇描述符
      val familyDesc = new HColumnDescriptor(EventLogConstants.BYTES_EVENT_LOGS_FAMILY_NAME)

      /**
        * 针对列簇设置属性值
        */
      // 设置数据压缩
      familyDesc.setCompressionType(Compression.Algorithm.SNAPPY)
      // 设置读取数据不缓存
      familyDesc.setBlockCacheEnabled(false)
      // 向表中添加列簇
      desc.addFamily(familyDesc)

      // 设置表的预分区,针对整个表来说的,不是针对某个列簇
      // TODO: createTable(desc: HTableDescriptor, splitKeys: Array[Array[Byte]])
      admin.createTable(desc,
        Array(
          Bytes.toBytes("1450570500000_"), Bytes.toBytes("1450571100000_"),
          Bytes.toBytes("1450571700000_")
        )
      )
    }catch {
      case e: Exception => e.printStackTrace()
    }finally {
      if(null != admin) admin.close()
      if(null != conn) conn.close()
    }
    // 返回表的名称
    tableName
  }

  /**
    * Spark Application 运行的入口,就是Driver Program
    * @param args
    *             程序的参数,实际业务需要传递 处理哪天你的数据(processDate)
    */
  def main(args: Array[String]): Unit = {

    // 需求传递一个参数,表明ETL处理的数据时哪一天的
    if(args.length < 1){
      println("Usage: EtlToHBaseSpark process_date")
      System.exit(1)
    }

    /**
      * 1. 创建SparkContext实例对象,读取数据,调度Job
      */
    val sparkConf = new SparkConf()
      .setMaster("local[3]").setAppName("EtlToHBaseSpark Application")
      // 设置使用kryo序列化
      .set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
      // 告知序列化数据类型,看RDD中数据类型,除了simple types, arrays of simple types, or string type
      .registerKryoClasses(Array(classOf[ImmutableBytesWritable], classOf[Put]))

    // 创建SparkContext
    val sc = SparkContext.getOrCreate(sparkConf)
    sc.setLogLevel("WARN")


    /**
      * TODO: a. 读取日志数据,从本地读取
      */
    val eventLogsRDD: RDD[String] = sc
      .textFile("file:///C:/spark-learning/datas/20151220.log", minPartitions = 3)

    // println(s"Count = ${eventLogsRDD.count()}")
    // println(eventLogsRDD.first())

    /**
      * TODO:b. 解析每条日志数据
      */
    val parseEventLogsRDD: RDD[(String, util.Map[String, String])] = eventLogsRDD
      // 通过解析工具类解析每条数据
      .map(line => {
        // 调用工具类进行解析得到Map集合
        val logInfo: util.Map[String, String] = new LogParser().handleLogParser(line)
        // 获取事件的类型
        val eventAlias = logInfo.get(EventLogConstants.LOG_COLUMN_NAME_EVENT_NAME)
        // 以二元组的形式返回
        (eventAlias, logInfo)
      })
    // println(parseEventLogsRDD.first())

    // 存储事件EventType类型
    val eventTypeList = List(EventEnum.LAUNCH, EventEnum.PAGEVIEW, EventEnum.CHARGEREQUEST,
      EventEnum.CHARGESUCCESS, EventEnum.CHARGEREFUND, EventEnum.EVENT)
    // TODO: 定义广播变量,将事件类型列表广播出去,广播给所有Executor
    val eventTypeListBroadcast: Broadcast[List[EventEnum]] = sc.broadcast(eventTypeList)

    /**
      * TODO:c. 过滤数据
      */
    val eventPutsRDD: RDD[(ImmutableBytesWritable, Put)] = parseEventLogsRDD
      // 过滤条件:解析Map集合不能为空; 事件类型EventType必须存在
      // TODO: 性能优化点:将集合列表 拷贝到每个Executor中一份数据,而不是每个Task中一份数据
      .filter{ case(eventAlias, logInfo) =>
        // logInfo.size() != 0 && eventTypeList.contains(EventEnum.valueOfAlias(eventAlias))
        logInfo.size() != 0 && eventTypeListBroadcast.value.contains(EventEnum.valueOfAlias(eventAlias))
      }
      // 数据转换,准备RDD数据库,将数据保存到HBASE表中RDD[(ImmtableBytesWritable, Put)]
      .map{ case(eventAlias, logInfo) =>
        // -i. RowKey 表的主键
        val rowKey = createRowKey(
          TimeUtil.parseNginxServerTime2Long(logInfo.get(EventLogConstants.LOG_COLUMN_NAME_SERVER_TIME)),
          logInfo.get(EventLogConstants.LOG_COLUMN_NAME_UUID), // 用户ID
          logInfo.get(EventLogConstants.LOG_COLUMN_NAME_MEMBER_ID), // 会员ID
          eventAlias // 事件类型别名
        )

        // -ii. 创建Put对象
        val put = new Put(Bytes.toBytes(rowKey))
        // add columns
        // TODO:  注意此处需要将Java中Map集合转换为Scala中Map集合,方能进行操作
        import scala.collection.JavaConverters._
        for((key, value) <- logInfo.asScala){
          put.addColumn(
            EventLogConstants.BYTES_EVENT_LOGS_FAMILY_NAME , // cf
            Bytes.toBytes(key), // column
            Bytes.toBytes(value)
          )
        }

        // iii. 返回二元组
        (new ImmutableBytesWritable(put.getRow), put)
    }

    /**
      * TODO: d. 将RDD保存到HBase表中
      */
    // d.1 获取配置信息, 需要将hbase-site.xml放入CLASSPATH下面
    val conf = HBaseConfiguration.create()

    /**
      * 由于ETL每天执行一次(ETL失败,再次执行),对原始的数据进行处理,将每天的数据存储HBase表中
      *     表的名称:
      *       create 'event_logs20151220', 'info'
      */
    val tableName = createHBaseTable(args(0), conf)

    // d.2 设置表的名称
    conf.set(TableOutputFormat.OUTPUT_TABLE, tableName)

    // d.3 设置OutputFort
    conf.set("mapreduce.job.outputformat.class",
      "org.apache.hadoop.hbase.mapreduce.TableOutputFormat")

    // d.4. 设置输出目录
    conf.set("mapreduce.output.fileoutputformat.outputdir",
      "/datas/spark/hbase/etl-" + System.currentTimeMillis())

    /**
      * TODO: 真正的保存数据到HBase表中
      */
    eventPutsRDD.saveAsNewAPIHadoopDataset(conf)

    // println(eventPutsRDD.count())
    // println(eventPutsRDD.first())

    // 为了开发测试,线程休眠, WEB UI监控查看
    Thread.sleep(1000000)

    // 关闭资源
    sc.stop()
  }
}

5、ETL 日志数据到HBase表中,程序代码优化点

  • 创建表的时候
    • 设置表的数据压缩
    • 创建预分区
      • 最好分区数多一点,均匀一点,减少Region分割
    • 设置读取表中的数据不缓存
      • cache blockfamilyDesc.setBlockCacheEnabled(false)
  • 事件类型EventType过滤优化
    • 使用广播变量 ,将集合列表广播出去 ,将数据发送到每个Executor一份,而不是每个Task一份数据
    • val eventTypeListBroadcast: Broadcast[List[EventEnum]] = sc.broadcast(eventTypeList)
    • eventTypeListBroadcast.value
  • 使用HFileOutputFormat
    • 向HBase表中存储数据的时候,方式
      • Put方式
        • PutData -> WAL -> MemStore -> StoreFile(HFile)
      • HFile方式
        • Data -> HFile -> Load Table

6、分析需求

新增用户 统计分析

  • 第一次访问网站(这一天)就是一个新增用户
    • 触发一个事件Event: launch事件 en=e_l

分析指标:需要结合维度来分析

  • 时间维度
    • 每天进行统计
  • 平台维度
    • 浏览网站所使用的的平台(网站、iOS APP,Android App等)
  • 浏览器维度
    • 使用浏览器的类型
  • 定义维度
    • 基本维度分析
      • 时间维度 + 平台维度
    • 基本维度 + 浏览器维度
      • 时间维度 + 平台维度 + 浏览器维度

7、shared variables (共享变量)

  • broadcast variables
    • 广播变量,变量的值是不可变,被存储到Executor中,以便Task使用的。
  • accumulators
    • 累加器:类似MapReduce程序中的计数器Counters,此变量的值只能增加,不能其他操作

累加器示例:

val accum = sc.longAccumulator("Input Lines Accumulator")
val inputRDD = sc.textFile("/datas/wordcount.data")
val filterRDD = inputRDD.filter(line => {
    accum.add(1L)
    line.trim.length > 0
})
filterRDD.count()

你可能感兴趣的:(笔记)