Spark-Core(二) - LogApp日志数据的解析&&Spark的运行架构

一、Spark-Core基础篇回顾

二、Spark如何进行大数据的逻辑处理

  • 2.1、入门代码遇到的一个简单错误
  • 2.2、求得买个域名下的流量之和
  • 2.3、每个省份访问次数的TopN(生产上边界值的处理)

三、Spark的运行架构(重要指数五颗星)

  • 3.1 总结
  • 3.2 Cluster Mode Overview

一、Spark-Core基础篇回顾

1、为什么选择Spark?

  • Fast:10倍于disk磁盘、100倍于memory内存
  • Easy code:编码容易、交互式的命令行interactive shell
  • Unified Stack:不管是批处理、流处理、机器学习、图计算都okay
  • Deployment:local、standalone、yarn、k8s
  • Multi Language:支持多语言的:Java、Scala、Python、R

2、RDD的两种创建方式:
textFile:local(本地)、只要是兼容hdfs的都可以
Parallelize:仅适用于本地测试

3、Transformation:
特点:Lazy延迟执行,写一堆代码并不会马上执行

4、Action算子
return a value to Driver(返回结果到Driver)

典型的Action算子:

  • collect、reduce、count、take

二、Spark进行大数据的逻辑处理-日志解析App的开发

2.1、开发Spark代码遇到的典型错误

IDEA下的一个常见错误: A master URL must be set in your configuration;有人不经会问,我在sparkConf中不是已经设置了AppName和Master么;

查看SparkContext的源码:
def this() = this(new SparkConf()) //在未传入参数时,它会new一个SparkConf,使得我们原先定义的sparkConf失效:

1、点击val sc = new SparkContext()中的SparkContext中去:
  /**
   * Create a SparkContext that loads settings from system properties (for instance, when
   * launching with ./bin/spark-submit).
   */
  def this() = this(new SparkConf())

2、再点击这个SparkConf中去:
  /** Create a SparkConf that loads defaults from system properties and the classpath */
  def this() = this(true)

//此处又创建了一个SparkConf使得我们的原先new出来sparkConf不生效了:所以需要把sparkConf传值传进去。

3、如下代码就会出现报错:
import org.apache.spark.{SparkConf,SparkContext}

object TestApp {
  def main(args: Array[String]): Unit = {
      val sparkConf = new SparkConf().setAppName("TestApp").setMaster("local[2]")
      val sc = new SparkContext()

    sc.parallelize(List(1,2,3,4,5,6,7,8)).foreach(println)
      sc.stop()
  }
}

4、在3的基础上把SparkConf传值传进去即可:
val sparkConf = new SparkConf().setAppName("TestApp").setMaster("yarn")
val sc = new SparkContext(sparkConf)

2.2、求每个域名下的流量之和

数据来源:自行准备的日志
主要字段:traffic(流量位于第20个字段)、domain(域名位于第11个字段)
需求分析:按照域名进行分组,然后组内求流量之和
求和:主要使用reduceByKey算子

第一次编码:
import org.apache.spark.{SparkConf,SparkContext}

object LogApp {
    def main(args: Array[String]): Unit = {
      val sparkConf = new SparkConf().setAppName("LogApp").setMaster("local[2]")
      val sc = new SparkContext(sparkConf)

      val lines = sc.textFile("file:///d:/baidu.log")

      val result  = lines.map( x => {
        val splits = x.split("\t")
        val domain = splits(10)
        val traffic = splits(19)
        (domain,traffic)
      }).reduceByKey(_+_).take(10).foreach(println)

      sc.stop()
    }
第二次编码:
  • 对于打进来的日志:不要默认它就是正确的,虽然说日志中的每个字段的含义及分隔符是什么都是实现定义好的,但是我们不能保证前段采集的日志或者nginx打的日志就是正确的?
  • 对于这些有问题的脏数据,我们应该怎么处理?

改代码:以增强代码的健壮性

从打进来的字符长度进行判断,是不是分隔符分割完后就是72个字符长度,是的话就没问题了:

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

object LogApp {
    def main(args: Array[String]): Unit = {
      val sparkConf = new SparkConf().setAppName("LogApp").setMaster("local[2]")
      val sc = new SparkContext(sparkConf)

      val lines = sc.textFile("file:///d:/baidu.log")

      val result  = lines.map( x => {
        val splits = x.split("\t")
        if (splits.length == 72) {
          val domain = splits(10)
          val traffic = splits(19).toLong
          (domain, traffic)
        } else {
          ("null",0L)
        }
      }).reduceByKey(_+_).take(10).foreach(println)

      sc.stop()
    }

}
第三次修改:

万一打进来的这两个字段不是string类型和long类型,是别的类型,那这段代码是不是就会报错了:所以需要try catch一下:

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

object LogApp {
    def main(args: Array[String]): Unit = {
      val sparkConf = new SparkConf().setAppName("LogApp").setMaster("local[2]")
      val sc = new SparkContext(sparkConf)

     var lines = sc.textFile("file:///d:/baidu.log")

      val result = lines.map( x => {
        val splits = x.split("\t")
        var traffic = 0L

        if (splits.length == 72){
          val domain = splits(10)
          
          try{
           traffic = splits(19).toLong
          } catch {
            case e:Exception => 0L
          }
          (domain,traffic)
        } else
          {
            ("null",0L)
          }
      }).reduceByKey(_+_).take(10).foreach(println)

      sc.stop()
    }

}

catch中不写case e:Exception =>0L,这个traffic还是0;此时的代码才显的稍稍有些健壮性可言。

2.3、求每个省份下访问次数的TopN

数据准备:

1、如下这样的一份数据,第一列数据可能是单独的ip或者ip:port,第二列是访问用户,第三列可有可无用来测试:
121.228.247.192:3306	john
121.28.247.191	sail
121.228.247.192	john
121.28.247.191:80	sail
121.228.247.192	john
121.28.247.191:954	sail
121.228.247.192	john
121.28.247.191	sail
121.28.247.191	ron	白领

2.3.1、引入纯真库解析

引入纯真IP库进行解析,参考博客:https://blog.csdn.net/adayan_2015/article/details/88580988

1、进入如下的github网址下载项目:

  • https://github.com/wzhe06/ipdatabase

2、项目下载到本地,进入cmd控制台对这个项目进行编译打jar包,需要进入到ipdatabase这个目录下面;

  • mvn clean package -DskipTests=true

3、cmd下执行命令,将这个jar包上传到本地maven仓库上去:

mvn install:install-file -Dfile:C:\Users\Administrator\Desktop\ipdatabase-master\ipdatabase-master\target\ipdatabase-1.0-SNAPSHOT.jar	\
-DgroupId=com.ggstar \
-DartifactId=ipdatabase \
-Dversion=1.0 \
-Dpackaging=jar

去到maven的仓库目录下就能看见这个包已经被打了进去,下次可以直接使用,命令解析:
-Dfile	后面跟着的是打包编译后jar包位置所在的全路径
-DgroupId	后面跟着的是包的名称
-DartifactId 项目的名称
-Dversion	版本名字

4、此时在Spark项目中引入依赖:

<dependency>
      <groupId>com.ggstar</groupId>
      <artifactId>ipdatabase</artifactId>
      <version>1.0</version>
    </dependency>

    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-ooxml</artifactId>
      <version>3.14</version>
    </dependency>

5、需要新建一个resources目录,把源代码中国的两个文件"ipDatabase.csv"和"ipRegion.xlsx"复制到resources目录下;并且在project structure中把这个目录设置为资源文件(file --> Project Structure --> Modules):

2.3.2、编写测试代码

1、使用别人写好的工具测试一个基础的可以解析出省份:

import com.ggstar.util.ip.IpHelper

object ParseIpApp {
    def main(args: Array[String]):Unit = {
      println(getCity("112.80.63.242"))
  }
    def getCity(ip:String) = {
      IpHelper.findRegionByIp(ip)
    }

}

2、用来判断字符长度是否等于2,等于2进行读取解析,字符长度大于2的默认是脏数据,不进行读取;

import com.ggstar.util.ip.IpHelper
import org.apache.spark.{SparkConf,SparkContext}

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

      val sparkConf = new SparkConf().setAppName("ParseIpTest").setMaster("local[2]")
      val sc = new SparkContext(sparkConf)

      val lines = sc.textFile("file:///D:/iptest.log")

      val result = lines.map( x => {
        val splits = x.split("\t")

        if (splits.length == 2) {
        val ip = splits(0)
        val province = IpHelper.findRegionByIp(ip)
        (province, 1)
      } else {
        ("null",1)
      }
      }).reduceByKey(_+_).take(5).foreach(println)

      sc.stop()
  }

}

//输出结果:
(鏈煡,3)
(null,1)
(江苏省,3)
(河北省,2)

3、在2的基础上进行修改,因为输入的第一个字段可能是ip也有可能是ip:port的形式,所以还需要对第一个字段进行解析:

package com.ruozedata.bigdata.SparkCore02

import com.ggstar.util.ip.IpHelper
import org.apache.spark.{SparkConf,SparkContext}

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

      val sparkConf = new SparkConf().setAppName("ParseIpTest").setMaster("local[2]")
      val sc = new SparkContext(sparkConf)
		
		//读取文件
      val lines = sc.textFile("file:///D:/iptest.log")
		
		
      val result = lines.map( x => {
      //tab键进行分割
        val splits = x.split("\t")
        var ip = ""
		
		//对长度做判断,对于长度不等于2的默认是脏数据,不进行操作
        if (splits.length == 2) {
        //文件中第一个字段是ip字段,但是采集过来的数据有些是纯IP,有些是ip:port
        val args0 = splits(0)
        //对第一列使用:进行分割
          val serverport = args0.split(":")
		//判断serverport拆分后的字符长度是几个,如果是长度为2,则取前面的ip部分;长度不为2,ip就等于它本身;
          if (serverport.length == 2){
            ip = serverport(0)
          } else {
            ip = args0
          }
		
		//定义省份,使用ipdatabase库中的findRegionByIp方法
        val province = IpHelper.findRegionByIp(ip)
        (province, 1)
        //这边的(province,1)解析出省份赋上1个1,做reduceByKey操作;下面的else对应的是总长度,总长度不为2的数据,key字段赋值"null",value字段赋值1
      } else {
        ("null",1)
      }
      }).reduceByKey(_+_).take(5).foreach(println)

      sc.stop()
  }


}

//运行出的结果:
(null,1)
(江苏省,4)
(河北省,4)

如何进行降序排序:
.reduceByKey(_+_).sortBy(_._2,false).take(10).foreach(println)

//sortBy源码的定义:
  /**
   * Return this RDD sorted by the given key function.
   */
  def sortBy[K](
      f: (T) => K,
      ascending: Boolean = true,
      numPartitions: Int = this.partitions.length)
      (implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T] = withScope {
    this.keyBy[K](f)
        .sortByKey(ascending, numPartitions)
        .values
  }

如上代码的作用:
1、需求的拆分,架构怎么设计合理,这些代码写出来一点意义都没有
2、仅仅了解到Spark是如何处理业务场景的;
3、掌握生产上边界值的处理 -->能够使得代码的健壮性有保障

三、Spark的运行架构(重要指数五颗星)

  • 官网地址:http://spark.apache.org/docs/latest/cluster-overview.html
  1. Spark Application:a driver program + executors on the cluster

  2. Application jar:

  • IDEA --> Maven Project --> Lifecycle --> package --> Run Maven Build;这个生成出来可以在服务器上跑的包就叫Application jar
  1. Driver Program:
  • The process running the main() function of the application and create the SparkContext
  • 这是一个进程运行比如LogApp中的main方法,并且创建了一个SparkContext
  1. Cluster Manager:集群管理器
  • An external service for acquiring resources on the cluster(eg: standalone manager,Mesos,YARN)
  • 代码都是一样的,通过cluster manager这个服务去申请资源跑在不同的平台上
  1. Deploy mode:部署模式
    用来区分Driver跑在什么地方;
    –deploy-mode DEPLOY_MODE Whether to launch the driver program locally (“client”) or
    on one of the worker machines inside the cluster (“cluster”)
    (Default: client).

client:driver run local
cluster:driver run cluster

  1. Worker node:any node that can run application node in the cluster.
    运行executor进程的,对于yarn来说就是在NodeManager上运行一个Container

  2. Executor:
    a process launched for an application on a worker node,(一个应用程序的进程)
    that runs tasks and keeps data in memory or a disk storage across them(把数据放在内存或磁盘上面存储,每一个executor可以跑多个tasks)
    Each application has its own executors :每一个应用程序有他自己的executors
    如下图所示:NodeManager上跑container,Application和App虽然是跑在一个NodeManager上的,上面三个和下面三个是相互独立的。

  3. Task:A Unit of work that will be sent to one executor
    每一个executor中运行多个task

  4. Job:
    只要spark遇到一个action就是一个job

  5. stage
    Stage: Each job gets divided into smaller sets of tasks called stages that depend on each other (similar to the map and reduce stages in MapReduce); you will see this term used in Driver’s logs

Spark-Core(二) - LogApp日志数据的解析&&Spark的运行架构_第1张图片
图片解析:
一个application应用程序跑在yarn上的3个executor上;另外一个application跑在另外3个executor上,虽然2个application的executor是运行在相同的NodeManager上,但是它们是独立的,不起冲突。

Components

Spark应用程序是一组独立的进程在集群上的:有一个Driver和多个executor,通过SparkContext对象在你的主程序中,也叫作Driver Program

为了让你的作业能够运行到集群上,SparkContext需要通过Cluster Manager去到集群上申请资源,CM去申请资源,此时就会启动Executor在集群上运行;executor是一=一堆进程去运行我们的计算和存储我们的数据;Driver Program会发送你的应用程序代码到executor上去;最终SparkContext会发送task去到executor上去。

关于这个架构有一些需要注意的东西

1、每一个应用程序都有多个独立的进程

  • 不管是从调度层面还是资源隔离层面,都是okay的;但是这并不意味着数据能够跨Spark应用程序共享,因为每一个Spark应用程序都有独立的多个executor;除非把多个共享的应用程序写到外部存储中去 --> 引申出了新的框架alluxio框架(分布式内存的存储框架)

2、Spark不关心底层的集群管理:
Spark is agnotic to the underlying cluster manager(底层的集群管理)
如何理解:代码都是一样的,可以跑在不同的地方,只是一个参数设置的问题:setMaster;一旦Spark拿到执行进程,进程之间就会进行通信;Spark运行在集群上相对容易,它也能运行在Mesos、Yarn上。

3、Driver和executor网路一定要通:

  • driver program必须要监听和接收一些连接从它的executors,网络一定要通

4、Driver调度作业要尽可能的靠近NodeManager

  • Driver是在集群上调度作业的,driver应该尽可能的运行在相近的工作节点,尽可能让他们运行在相同区域的本地网络.
  • 如果你想要发送一些请求到远端集群,最好使用RPC的方式发送

总结:

1、每一个应用程序都有其独立的进程
2、Spark不关心底层的集群管理
3、Driver 和executor的网络一定要通
4、driver调度作业要尽可能靠近NodeManager

你可能感兴趣的:(Spark-Core实战班)