• Java基础部分-面试题


    1.java中的数据类型有哪些?

    数据类型主要分为基本数据类型和引用数据类型。

    基本数据类型主要包括:

    整数类型: byte、short、int、long

    浮点数:float、double

    布尔类型:boolean

    字符类型:char

    引用数据类型主要包括:

    类(String)、接口、数组、枚举、标注。

    2.java中关键字与保留字的区别

    关键字(keywords):

    关键字对java的编译器有特殊的意义,他们用来表示一种数据类型,或者表示程序的结构等。

    保留字(reserved words):

    保留字是java语言已经定义过的字,保留字暂时没有相对应的语法,但考虑到扩展性,为了向后兼容不能再将其作为变量名。

    const和goto是java的保留字

    3.switch语句能否作用在String上?

    switch语句可以作用在byte,short,char ,int和相对应的包装类上以及枚举(enum)

    在jdk1.7版本之后也可以作用在String上:

    原理:利用String的hash值,本质上是switch-int结构。并且利用到了equals方法来防止hash冲突的问题。

    最后利用switch-byte结构,精确匹配。

    4.重写和重载的区别

    方法重载(overload):

    1)同一个类中

    2)方法名相同,参数列表不同(参数顺序、个数、类型)

    3)方法返回值、访问修饰符任意

    4)与方法的参数名无关

    方法重写(override):

    1)发生在具有继承关系的两个类中

    2)方法名相同,参数列表相同(参数顺序、个数、类型)

    (3)方法返回值类型必须小于或者等于父类的访问权限

    4)访问修饰符,子类的访问权限要大于或者等于父类的访问权限

    5)与方法的参数名无关

    (6)子类抛出的异常类型要小于或者等于父类抛出的异常类型

    #如果某一方法在父类中是访问权限是priavte,那么就不能在子类对其进行重写,如果定义的话,也只是定义了一个新方法,

    而不会达到重写的效果(private只允许在本类使用)。

    5.访问修饰符public,protected,friendly,private的作用域

     
    访问修饰符  本类 同包 子类 其他
      同一类中 同一包下 有继承关系 不同包下没继承关系
    private(私有的)     √      
    friendly(默认)    √    √    
    protected(受保护的)    √  
    public(公共的)

     

    6.一个”.java”源文件中是否可以包含多个类(不是内部类)?有什么限制

    一个“.java”源文件里面可以包含多个类,但是只允许有一个public类,并且类名必须和文件名一致。

    每个编译单元只能有一个public类,控制了每个编译单元只能有一个公开的接口,而这个接口就由其public 类来表示。

    你可以根据需要,往这个文件里面添加任意多个提供辅助功能的package 权限的类。但是如果这个编译单元里面有两个或两

    个以上的public 类的话,程序就不知道从哪里导入了,编译器就会报错。 

    7."=="和equals方法究竟有什么区别?

    "=="操作符专门用来比较两个变量的内存地址是否相等,也就是用于比较变量所对应的内存中所存储的数值是否相同,用来比较

    两个基本类型的数据或者两个引用变量是否相等

    equals方法是用于比较两个独立对象的内容是否相同

    8.nteger与int的区别

    int是java提供的8种原始数据类型之一。Java为每个原始类型提供了封装类,Integer是java为int提供的封装类。int的默认值为0,而Integer的默认值为null,即Integer可以区分出未赋值和值为0的区别,int则无法表达出未赋值的情况,例如,要想表达出没有参加考试和考试成绩为0的区别,则只能使用Integer。在JSP开发中,Integer的默认为null,所以用el表达式在文本框中显示时,值为空白字符串,而int默认的默认值为0,所以用el表达式在文本框中显示时,结果为0,所以,int不适合作为web层的表单数据的类型。

    在Hibernate中,如果将OID定义为Integer类型,那么Hibernate就可以根据其值是否为null而判断一个对象是否是临时的,如果将OID定义为了int类型,还需要在hbm映射文件中设置其unsaved-value属性为0。

    另外,Integer提供了多个与整数相关的操作方法,例如,将一个字符串转换成整数,Integer中还定义了表示整数的最大值和最小值的常量。

    9.&&&的区别

    &和&&都可以用作逻辑与的运算符,表示逻辑与(and),当运算符两边的表达式的结果都为true时,整个运算结果才true,否则,只要有一方为false,则结果为false。

    &&还具有短路的功能,即如果第一个表达式为false,则不再计算第二个表达式,例如,对于if(str != null && !str.equals(“”))表达式,当str为null时,后面的表达式不会执行,所以不会出现NullPointerException如果将&&改为&,则会抛出NullPointerException异常。If(x==33 & ++y>0) y会增长,If(x==33 && ++y>0)不会增长

    &还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作,我们通常使用0x0f来与一个整数进行&运算,来获取该整数的最低4个bit位,例如,0x31 & 0x0f的结果为0x01。

    10.char型变量中能不能存贮一个中文汉字?为什么? 

    char型变量是用来存储Unicode编码的字符的,unicode编码字符集中包含了汉字,所以,char型变量中当然可以存储汉字啦。不过,如果某个特殊的汉字没有被包含在unicode编码字符集中,那么,这个char型变量中就不能存储这个特殊汉字。补充说明:unicode编码占用两个字节,所以,char类型的变量也是占用两个字节。

    备注:后面一部分回答虽然不是在正面回答题目,但是,为了展现自己的学识和表现自己对问题理解的透彻深入,可以回答一些相关的知识,做到知无不言,言无不尽。 

    11.abstract的method是否可同时是static,是否可同时是native,是否可同时是synchronized?

    abstract的method 不可以是static的,因为抽象的方法是要被子类实现的,而static与子类扯不上关系!

    native方法表示该方法要用另外一种依赖平台的编程语言实现的,不存在着被子类实现的问题,所以,它也不能是抽象的,不能与abstract混用。例如,FileOutputSteam类要硬件打交道,底层的实现用的是操作系统相关的api实现,例如,在windows用c语言实现的,所以,查看jdk 的源代码,可以发现FileOutputStream的open方法的定义如下:

    private native void open(String name) throws FileNotFoundException;

    如果我们要用java调用别人写的c语言函数,我们是无法直接调用的,我们需要按照java的要求写一个c语言的函数,又我们的这个c语言函数去调用别人的c语言函数。由于我们的c语言函数是按java的要求来写的,我们这个c语言函数就可以与java对接上,java那边的对接方式就是定义出与我们这个c函数相对应的方法,java中对应的方法不需要写具体的代码,但需要在前面声明native。

    关于synchronized与abstract合用的问题,我觉得也不行,因为在我几年的学习和开发中,从来没见到过这种情况,并且我觉得synchronized应该是作用在一个具体的方法上才有意义。而且,方法上的synchronized同步所使用的同步锁对象是this,而抽象方法上无法确定this是什么。

     

      

     

    #谈谈你对java面向对象的理解。

    封装:核心思想就是“隐藏细节”、“数据安全”,将对象不需要让外界访问的成员变量和方法私有化,只提供符合开发者意愿的公有方法来访问这些数据和逻辑,保证了数据的安全和程序的稳定。

    继承:子类可以继承父类的属性和方法,并对其进行拓展。

    多态:同一种类型的对象执行同一个方法时可以表现出不同的行为特征。通过继承的上下转型、接口的回调以及方法的重写和重载可以实现多态。

    #List、Set、Map的区别。

    List

    1)可以允许重复的对象。

    2)可以插入多个null元素。

    3)是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。

    4)常用的实现类有ArrayList、LinkedList和Vector。ArrayList最为流行,它提供了使用索引的随意访问,而LinkedList则对于经常需要从List中添加或删除元素的场合更为合适。

    Set

    1)不允许重复对象

    2)无序容器,你无法保证每个元素的存储顺序,TreeSet通过Comparator或者Comparable维护了一个排序顺序。

    3)只允许一个null元素

    4)Set接口最流行的几个实现类是HashSet、LinkedHashSet以及TreeSet。最流行的是基于HashMap实现的HashSet;TreeSet还实现了SortedSet接口,因此TreeSet是一个根据其compare()和compareTo()的定义进行排序的有序容器。

    map

    1)Map不是collection的子接口或者实现类。Map是一个接口。

    2)Map的每个Entry都持有两个对象,也就是一个键一个值,Map可能会持有相同的值对象但键对象必须是唯一的。

    3)TreeMap也通过Comparator或者Comparable维护了一个排序顺序。

    4)Map里你可以拥有随意个null值但最多只能有一个null键。

    5)Map接口最流行的几个实现类是HashMap、LinkedHashMap、Hashtable和TreeMap。(HashMap、TreeMap最常用)

    #HashMap和HashTable的区别。

    HashMap和Hashtable都实现了Map接口。主要的区别有:线程安全性,同步(synchronization),以及速度。

    1)HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。

    2)HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。

    3)HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。

    4)由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。

    5)HashMap不能保证随着时间的推移Map中的元素次序是不变的。

    #线程安全的方法有哪些。

    1)内置锁

    2)使用java.util.concurrent包中定义的并发类如:

    ConcurrentHashMap

    ConcurrentLinkedQueue

    ConcurrentSkipListMap

    但是他们支持的并发实现并不一定意味着操作的原子性,他们只是保证数据结构不被破坏。

    3)添加volatile关键字

    volatile:内存可见性,即线程A对volatile变量的修改,其他线程获取的volatile变量都是最新的;可以禁止指令重排序。

    4)同步语句的注意

    你可以使用一个对象来标记同步块,不要使用this,因为this可能代表当前的类,this造成同步的区域是整个类,其他对象就无法调用类中不是同步的方法了,需要等待锁从this指的类中释放才能进行了。所以你可以定义一个对象,然后让同步块的锁指向整个对象来缩小同步块的锁影响范围。

    5)不要在同步块中调用其他的同步块。

    这句话不是绝对的,如果你很了解代码的同步,锁等信息,你可以大胆的这么做。

    #数据结构有哪些

    常见的数据结构如下:

    数组 (Array)

    在程序设计中,为了处理方便, 把具有相同类型的若干变量按有序的形式组织起来,这些按序排列的同类数据元素的集合称为数组。一个数组可以分解为多个数组元素,这些数组元素可以是基本数据类型或是构造类型。因此按数组元素的类型不同,数组又可分为数值数组、字符数组、指针数组、结构数组等各种类别。 

    (Stack)

    是只能在某一端插入和删除的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。

    队列 (Queue)

    一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为队空。

    链表 (Linked List)

    是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

    (Tree)

    是包含n(n>0)个结点的有穷集合K,且在K中定义了一个关系N,N满足以下条件:

    1)有且仅有一个结点K0,他对于关系N来说没有前驱,称K0为树的根结点。简称为根(root)。

    2)除K0外,K中的每个结点,对于关系N来说有且仅有一个前驱。

    3)K 中各结点,对关系N来说可以有m个后继(m>=0)。

    (Graph)

    图是由结点的有穷集合V和边的集合E组成。其中,为了与树形结构加以区别,在图结构中常常将结点称为顶点,边是顶点的有序偶对,若两个顶点之间存在一条边,就表示这两个顶点具有相邻关系。

    (Heap)

    在计算机科学中,堆是一种特殊的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。

    散列表 (Hash)

    若结构中存在关键字和K相等的记录,则该记录必定在f(K)的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系f为散列函数(Hash function),按这个思想建立的表为散列表。

    数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。

    #如何减少Spark中的网络IO。

    使用广播变量、累加变量、使用压缩、使用序列化、尽量不使用shuffle。

    1)高性能序列化类库

    Spark中,默认是使用Java自带的序列化机制——基于ObjectInputStream和ObjectOutputStream的序列化机制,这是为了提高便捷性和适用性,毕竟是Java原生的嘛。然鹅,自带的东西往往考虑的东西比较多,没法做到样样俱全,比如内序列化后占据的内存还是较大,但是Spark是基于内存的大数据框架,对内存的要求很高。所以,在Spark应用程序中,Java自带的序列化库的效率有点差强人意。需求是从实际出发的嘛,最终Spark也提供了另外一种序列化机制——Kryo序列化机制。

    Kryo序列化机制比Java序列化机制更快,序列化后的数据占的内存更小。那么Kryo序列化机制这么好,为什么不选用它是默认序列化库呢?这里提一句话“人无完人,谁能无错”,Kryo序列化机制也样,之所以不选用它为默认序列化机制是因为有些类型虽然实现了Seriralizable接口,但是不一定能够进行序列化;此外,如果要得到最佳的性能,需要在Spark应用程序中,对所有 需要序列化的类型都进行注册。

    使用Kryo序列化机制的方法:

    1、给SparkConf加入一个参数 SparkConf().set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")

    2、对需要序列化的类自行进行注册(因为如果不注册,Kryo必须一直保存类型的全限定名,会占用内存。Spark默认是对Scala中常用的类型自动注册了Kryo的,都在AllScalaRegistry类中)

    SparkConf conf = new SparkConf().setMaster(...).setAppName(...)

    conf.registerKryoClasses(Counter.class)

    JavaSparkContext sc = new JavaSparkContext(conf)

    Kryo序列化机制进行优化达到更优的效果。

    1、优化缓存大小。如果注册的要序列化的自定义的类型,本身很大大,比如包含了超过100个field。会导致要序列化的对象过大。此时需要对Kryo本身进行优化。因为Kryo内部的缓存可能不够存放这么大的class对象。此时需要调用SparkConf.set()方法,设置spark.kryoserializer.buffer.mb参数的值,将其调大以适用。默认情况下spark.kryoserializer.buffer.mb是2,即最大能缓存2M的对象,然后进行序列化。可以在必要时将其调大。比如设置为10。

    2、预先注册自定义类型。虽然不注册自定义类型,Kryo类库也能正常工作,但是那样对于它要序列化的每个对象,都会保存一份它的全限定类名。反而会耗费大量内存。因此通常都预先注册好要序列化的自定义的类。

    总结,需要用到Kryo序列化机制的场景,算子内部使用了外部的大对象或者大数据结构。那么可以切换到Kryo序列化,序列化速度更快,和获得更小的序列化数据,减少内存的消耗。

    2)优化数据结构

    对数据结构的优化,主要是针对Java数据结构(如果用scala开发的话,其实原理也一样的)。其实就是算子里面的局部变量或者算子函数外部的数据结构。比如基于链式结构的数据结构、包装类型的数据结构等,它们在除了本身的数据之外,还会有额外的数据信息来维持它们的数据类型,这样就会比预想占有更大的内存。

    以下是一些优化建议:

    1、能使用数组或字符串就不要用集合类。即优先使用Array,退而求次才是ArrayList、LinkedList、HashMap、HashTable等。熟悉Java语言的都知道集合类一般是泛型的,然鹅泛型的类型是包装类,比如List list = new ArrayList(),就会因为包装类而占有额外的内存,最后占有更多的额外开销。在生产开发中的做法是,对于HashMap、List这种数据,统一用String拼接成特殊格式的字符串。如Map<Integer, Person> persons = new HashMap<Integer, Person>()。可以优化为,特殊的字符串格式:id:name,address|id:name,address...

    2、避免使用多层嵌套的对象结构。public class Teacher { private List students = new ArrayList() }。就是非常不好的例子。因为Teacher类的内部又嵌套了大量的小Student对象。比如说,对于上述例子,也完全可以使用特殊的字符串来进行数据的存储。比如,用json字符串来存储数据,就是一个很好的选择。{"teacherId": 1, "teacherName": "leo", students:[{"studentId": 1, "studentName": "tom"},{"studentId":2, "studentName":"marry"}]}

    3、能用int就不用String。虽然String比集合咧更高效,但是之前说过Java的String是占2个字节的,使用int会优化内存。

    总结,在写Spark程序的时候,要牢牢记住,尽量压榨因语言带来的内存开销,达到节约内存的目的。

    3)广播共享数据

    RDD实质是弹性分布式数据集,在每个节点中的每个task(一个节点可以有很多个task)操作的只是RDD的一部分数据,如果RDD算子操作使用到了算子函数外部的一份大数据的时候,实际上是Spark应用程序把数据文件通过driver发送给每一个节点的每一个task,很明显,这样会造成大量的网络IO操作,大量消耗节点上的内存。其实很容易想到,把一份大数据文件发送给每个节点就OK了,单个节点的所有task共享一份数据,这样就会节省大量的网络IO操作和节省大量内存消耗。

    如果算子函数中,使用到了特别大的数据(比如一份大的配置文件)供每个节点的所有task使用,可以借助Spark提供的共享变量。共享变量有两种,一是广播变量,一是累加器。广播变量是只读的,通常用来提供一份数据给所有的节点,每个节点的task访问访问同一份数据。而累加器是可写可读的,一个累加器一般是用于所有节点对用一个简单的整型变量进行共享累加,共同维护一份数据。这样的话,就不至于将一个大数据拷贝到每一个task上去。而是给每个节点拷贝一份,然后节点上的task共享该数据。

    4)数据本地化

    Spark数据本地化的基本原理

    Spark和MapReduce是如今两个最流行的大数据框架,它们的原理都是计算移动,而数据不移动,计算找数据。这样做的创新性是避免了大量数据的网络传输造成网络IO和内存的消耗。因此引出一个叫“数据本地化”的概念。

    数据本地化对于Spark Job性能有着巨大的影响。如果数据以及要计算它的代码是在同一个节点,性能会非常高。但是,如果数据和计算它的代码是位于不同的节点,那么其中之一必须到另外一方的机器上。通常来说,移动代码到其他节点,会比移动数据到代码所在的节点上去,速度要快得多,因为代码比较小。Spark也正是基于这个数据本地化的原则来构建task调度算法的。

    数据本地化,指的是,数据离计算它的代码有多近。基于数据距离代码的距离,有几种数据本地化级别:

    1、PROCESS_LOCAL:数据和计算它的代码在同一个JVM进程中。

    2、NODE_LOCAL:数据和计算它的代码在一个节点上,但是不在一个进程中,比如在不同的executor进程中,或者是数据在HDFS文件的block中。

    3、NO_PREF:数据从哪里过来,性能都是一样的。

    4、RACK_LOCAL:数据和计算它的代码在一个机架上。

    5、ANY:数据可能在任意地方,比如其他网络环境内,或者其他机架上。

    Spark数据本地化的特点

    Spark倾向于使用最好的本地化级别来调度task,但并不是每次都会使用最好的本地化数据的。在实际中,如果没有任何未处理的数据在空闲的executor上,Spark会放低本地化级别。这时有两个选择:第一,driver等待,直到executor上的cpu释放出来,就分配task等资源给这个executor;第二,立即在任意一个executor上启动一个task。

    Spark会默认等待一段时间(这个事件可以通过参数来设置),来期望在task要处理的数据所在的节点上的executor空闲出一个cpu,从而为其分配task鞥资源。但只要超过了时间,Spark就会将task分配到其他任意一个空闲的executor上。

    可以设置参数,spark.locality系列参数,来调节Spark等待task可以进行数据本地化的时间。spark.locality.wait(3000毫秒)、spark.locality.wait.node、spark.locality.wait.process、spark.locality.wait.rack

    针对以上的分析,我们可以这样调优,增大查找本地化数据的超时时间和重试次数,因为时间更长更利于查找本地化数据的节点的executor,重试次数越多,更多机会尝试查找本地化数据的节点的executor。

    调优方式,主要是spark.locality.wait(3000毫秒)、spark.locality.wait.node、spark.locality.wait.process、spark.locality.wait.rack这些参数,具体的根据实际的业务需求来控制参数就OK了。

    5)reduceByKey和groupByKey的选择

    以下两种方式是等价的,但是实现的原理却不相同。reduceByKey,因为它会在map端,先进行本地combine,可以大大减少要传输到reduce端的数据量,减小网络传输的开销。而groupByKey算子却不会这样优化。所以只有在reduceByKey处理不了时,才用groupByKey().map()来替代。

    scala实现代码如下:

    val counts = pairs.reduceByKey(_ + _)

    val counts = pairs.groupByKey().map(wordCounts => (wordCounts._1, wordCounts._2.sum))

    6)shuffle性能优化

    无论是MapReduce还是Spark,Shuffle阶段是最重要的阶段,它的好坏影响着整个Spark的性能。其实Shuffle阶段的调优,可以从以下的参数入手:

    new SparkConf().set("spark.shuffle.consolidateFiles", "true")

    spark.shuffle.consolidateFiles:是否开启shuffle block file的合并,默认为false。

    spark.reducer.maxSizeInFlight:reduce task的拉取缓存,默认48m。

    spark.shuffle.file.buffer:map task的写磁盘缓存,默认32k。

    spark.shuffle.io.maxRetries:拉取失败的最大重试次数,默认3次。

    spark.shuffle.io.retryWait:拉取失败的重试间隔,默认5s。

    spark.shuffle.memoryFraction:用于reduce端聚合的内存比例,默认0.2,超过比例就会溢出到磁盘上。

    #Spark中常用的算子。

    Transformation:

    1)map

    map的输入变换函数应用于RDD中所有元素,而mapPartitions应用于所有分区。区别于mapPartitions主要在于调用粒度不同。如parallelize(1 to 10, 3),map函数执行10次,而mapPartitions函数执行3次。

    2)filter(function)

    过滤操作,满足filter内function函数为true的RDD内所有元素组成一个新的数据集。如:filter(a == 1)。

    3)flatMap(function)

    map是对RDD中元素逐一进行函数操作映射为另外一个RDD,而flatMap操作是将函数应用于RDD之中的每一个元素,将返回的迭代器的所有内容构成新的RDD。

    flatMap与map区别在于map为“映射”,而flatMap“先映射,后扁平化”,map对每一次(func)都产生一个元素,返回一个对象,而flatMap多一步就是将所有对象合并为一个对象。

    4)mapPartitions(function)

    区于foreachPartition(属于Action,且无返回值),而mapPartitions可获取返回值。与map的区别前面已经提到过了,但由于单独运行于RDD的每个分区上(block),所以在一个类型为T的RDD上运行时,(function)必须是Iterator<T> => Iterator<U>类型的方法(入参)。

    5)mapPartitionsWithIndex(function)

    mapPartitions类似,但需要提供一个表示分区索引值的整型值作为参数,因此function必须是(int, Iterator<T>)=>Iterator<U>类型的。

    6)sample(withReplacement, fraction, seed)

    采样操作,用于从样本中取出部分数据。withReplacement是否放回,fraction采样比例,seed用于指定的随机数生成器的种子。(是否放回抽样分true和false,fraction取样比例为(0, 1]。seed种子为整型实数。)

    7)union(otherDataSet)

    对于源数据集和其他数据集求并集,不去重。

    8)intersection(otherDataSet)

    对于源数据集和其他数据集求交集,并去重,且无序返回。

    9)distinct([numTasks])

    返回一个在源数据集去重之后的新数据集,即去重,并局部无序而整体有序返回。

    10)groupByKey([numTasks])

    在一个PairRDD或(k,v)RDD上调用,返回一个(k,Iterable<v>)。主要作用是将相同的所有的键值对分组到一个集合序列当中,其顺序是不确定的。groupByKey是把所有的键值对集合都加载到内存中存储计算,若一个键对应值太多,则易导致内存溢出。

    在此,用之前求并集的union方法,将pair1,pair2变为有相同键值的pair3,而后进行groupByKey

    11)reduceByKey(function,[numTasks])

    groupByKey类似,却有不同。如(a,1), (a,2), (b,1), (b,2)。groupByKey产生中间结果为( (a,1), (a,2) ), ( (b,1), (b,2) )。而reduceByKey为(a,3), (b,3)。

    reduceByKey主要作用是聚合,groupByKey主要作用是分组。(function对于key值来进行聚合)

    12)aggregateByKey(zeroValue)(seqOp, combOp, [numTasks])

    类似reduceByKey,对pairRDD中想用的key值进行聚合操作,使用初始值(seqOp中使用,而combOpenCL中未使用)对应返回值为pairRDD,而区于aggregate(返回值为非RDD)

    13)sortByKey([ascending], [numTasks])

    同样是基于pairRDD的,根据key值来进行排序。ascending升序,默认为true,即升序;numTasks

    14)join(otherDataSet,[numTasks])

    加入一个RDD,在一个(k,v)和(k,w)类型的dataSet上调用,返回一个(k,(v,w))的pair dataSet。

    15)cogroup(otherDataSet,[numTasks])

    合并两个RDD,生成一个新的RDD。实例中包含两个Iterable值,第一个表示RDD1中相同值,第二个表示RDD2中相同值(key值),这个操作需要通过partitioner进行重新分区,因此需要执行一次shuffle操作。(若两个RDD在此之前进行过shuffle,则不需要)

    16)cartesian(otherDataSet)

    求笛卡尔乘积。该操作不会执行shuffle操作。

    17)pipe(command,[envVars])

    通过一个shell命令来对RDD各分区进行“管道化”。通过pipe变换将一些shell命令用于Spark中生成的新RDD

    18)coalesce(numPartitions)

    重新分区,减少RDD中分区的数量到numPartitions。

    19)repartition(numPartitions)

    repartition是coalesce接口中shuffle为true的简易实现,即Reshuffle RDD并随机分区,使各分区数据量尽可能平衡。若分区之后分区数远大于原分区数,则需要shuffle。

    20)repartitionAndSortWithinPartitions(partitioner)

    该方法根据partitioner对RDD进行分区,并且在每个结果分区中按key进行排序。

    Action:

    1)reduce(function)

    reduce将RDD中元素两两传递给输入函数,同时产生一个新值,新值与RDD中下一个元素再被传递给输入函数,直到最后只有一个值为止。

    2)collect()

    将一个RDD以一个Array数组形式返回其中的所有元素。

    3)count()

    返回数据集中元素个数,默认Long类型。

    4)first()

    返回数据集的第一个元素(类似于take(1))

    5)takeSample(withReplacement, num, [seed])

    对于一个数据集进行随机抽样,返回一个包含num个随机抽样元素的数组,withReplacement表示是否有放回抽样,参数seed指定生成随机数的种子。

    该方法仅在预期结果数组很小的情况下使用,因为所有数据都被加载到driver端的内存中。

    6)take(n)

    返回一个包含数据集前n个元素的数组(从0下标到n-1下标的元素),不排序。

    7)takeOrdered(n,[ordering])

    返回RDD中前n个元素,并按默认顺序排序(升序)或者按自定义比较器顺序排序。

    8)saveAsTextFile(path)

    dataSet中元素以文本文件的形式写入本地文件系统或者HDFS等。Spark将对每个元素调用toString方法,将数据元素转换为文本文件中的一行记录。

    若将文件保存到本地文件系统,那么只会保存在executor所在机器的本地目录。

    9)saveAsSequenceFile(path)(Java and Scala)

    dataSet中元素以Hadoop SequenceFile的形式写入本地文件系统或者HDFS等。(对pairRDD操作)

    10)saveAsObjectFile(path)(Java and Scala)

    将数据集中元素以ObjectFile形式写入本地文件系统或者HDFS等。

    11)countByKey()

    用于统计RDD[K,V]中每个K的数量,返回具有每个key的计数的(k,int)pairs的hashMap。

    12)foreach(function)

    对数据集中每一个元素运行函数function。

    #介绍一下RDD。

    RDD是什么

    RDD是Spark的基础数据结构。表现形式为不可变的分区元素的集合,并且可以在集群中并行操作。

    RDD解析

    Resilient(弹性):在DAG的帮助下,具有容错性。即在节点故障导致丢失或者损坏分区,可以重新计算数据。

    Distributed(分布式的):数据存储在多个节点上。

    Dataset(数据集):要处理的数据集。用户可以在外部数据源获取数据,这些数据源可以JSON文件、CSV文件、文本文件获取通过JDBC的形式获取数据库中没有特定数据结构的数据。

    RDD的5特属性

    1、获取分区列表(getPartitions):有一个数据分片列表,能够将数据进行切分,切分后的数据能够进行并行计算,是数据集的原子组成部分。

    2、可以在每一个分区上进行计算(compute):计算每个分区,得到一个可便利的结果,用于说明在父RDD上执行何种计算。

    3、获取每个RDD的依赖(getDependencies):计算每个RDD对父RDD的依赖列表,源RDD没有依赖,通过依赖关系描述血统。

    4、RDD的键值分区器( @transient val partitioner: Option[Partitioner] = None):描述分区模式和数据存放的位置,键值对的RDD根据哈希值进行分区。

    5、在那个分区上进行计算最好(getPreferredLocations):每一个分片的优先计算位置。

    RDD中的数据集是按照逻辑分不到集群中的节点上,这样可以在各个节点上并行计算。RDD具有容错性,在失败的情况下,可以自动恢复。

    RDD可以被缓存,并且可以手动分区。

    开发人员可以将需要再次使用的RDD进行持久化。Spark是默认将数据持久化到内存中,如果内存不足,会将数据写入到磁盘上。用户可以自定义持久化方式。

    为什么需要RDD

    提出RDD的动机有:

    迭代计算。

    交互式的数据挖掘工具。

    DSM(Distributed Shared Memory)是一个通用的抽象,这种通用性使得在集群上高效执行和容错性变得更难。

    RDD具有容错性,这是因为RDD是基于coarse-grained transformation而不是fine-grained updates来更新状态。

    RDD是lazy的,在需要的时候,这样可以节省很多时间并提高效率。

    RDD的特征

    1)In-memory Computation

    Spark RDD具有内存计算功能。RDD将中间的结果存储在内存中,如果内存中存不下的时候,将数据放在磁盘上。

    2)Lazy Evaluations

    Spark的所有转换操作都是Lazy的,如果遇到action操作时,才会执行之前的操作。

    3)Fault Tolerance

    Spark RDD具有容错能力。如果碰到故障时,RDD可以根据追踪数据的依赖关系,来重新生成丢失的数据。

    4)Immutability

    RDD中的数据都是不可变的,可以在进程之间共享使用。

    5)Partitioning

    分区是RDD并行计算的基本单元。每个分区都是一个可变数据的逻辑分区。在现有分区的基础上可以通过一些转换操作生成新的分区。

    6)Coarse-grained Operations

    将一个方法作用于数据集中的每一个元素。

    7)Location-Stickiness

    RDD可以定义计算分区的放置首选项。DAG Scheduler将尽量任务放到数据所在的节点上。

    8)Persistence

    RDD可以根据需要不同,选择相应的存储策略。

    RDD Operation(RDD操作)

    Transformations(转换)

    RDD的转换操作都是lazy的,分为两种narrow transformation, wide transformation。

    1)Narrow Transformations(窄依赖)

    在单个分区中计算记录所需的所有元素都存在父RDD的单个分区中。

    map、flatMap、MapPartition、filter、Sample、Union操作的结果就是Narrow transformation。

    一个父RDD的partition至少会被子RDD的某个partition使用一次。就是一个父类RDD的一个分区不可能对应一个子RDD的多个分区。

    2)Wide transformation(宽依赖)

    在单个分区中计算记录所需的所有元素都存在父RDD的多个分区中。

    intersection、distinct、reduceByKey、GroupByKey、join、Cartesian、repartition、colaesce操作的结果是Wide transformation。

    一个父RDD的partition会被子RDD的partition使用多次。就是一个父RDD的一个分区对应一个子RDD的多个分区。

    Join操作:窄依赖,两个数据集使用相同的分区器;宽依赖,使用不同的分区器。

    可以使用toDebugString,看到RDD的线性信息,如果出现ShuffleDependency,就是发生shuffle操作。

    依赖关系说明

    窄依赖的RDd可以通过相同的键进行联合分区,整个操作都可以在一个集群节点上进行,以流水线的方式计算所有父分区,不会造成网络之间的数据混合。

    宽依赖RDD涉及数据混合,宽依赖需要首先计算好所有父分区数据,然后在节点直接进行shuffle。

    窄依赖能够更有效的进行失效节点的恢复,重新计算丢失RDD分区的父分区,不同节点之间可以并行计算;

    对一个宽窄依赖的血统图,单个节点失效可能导致这个RDD的所有祖先丢失部分数据,因而需要整体重新计算(Shuffle执行时固化操作,以及采取persist缓存策略,可以在固化点或者缓存点重新计算)。

    执行时,调度程序检查依赖性的类型,将窄依赖的RDD划到一组处理当中,即Stage,宽依赖在一个跨越连续的Stage,同时需要显示指定多个子RDD的分区。

    RDD的局限性

    1)No input optimization engine

    RDD没有自动优化的规则,无法使用Spark高级优化器,例如catalyst优化器和Tungsten执行引擎。可以实现手动RDD优化。

    Dataset和DataFrame中不存在这个问题,DataSet和DataFrame可以使用catalyst来产生优化后的逻辑和物理执行计划,这样可以节省空间和提高运行速度。

    2)Runtime type safety

    RDD中没有静态类型和运行时类型安全,并且不允许在运行时检查错误。

    Dataset提供了编译时期类型安全来构建复杂数据工作流。

    3)Degrade when not enough memory

    在存储RDD时,如果没有足够的内存或者磁盘,将会使得RDD的性能下降特别厉害。

    4)Performance limitation & Overhead of serialization & garbage collection

    因为RDD是内存中的JVM对象,这就牵扯到GC和Java序列化,在数据增长时,会需要大量的内存或者磁盘空间。

    GC的成本与Java对象是成正比的,使用数据结构比较少的对象可以减少成本,或者将数据持久化。

    5)Handling structured data

    RDD不提供数据的schema信息。

    Dataset和DataFrame提供了数据的schema信息,可以每一列数据的含义。

    1、谈谈你对java面向对象的理解。

    封装:核心思想就是“隐藏细节”、“数据安全”,将对象不需要让外界访问的成员变量和方法私有化,只提供符合开发者意愿的公有方法来访问这些数据和逻辑,保证了数据的安全和程序的稳定。

    继承:子类可以继承父类的属性和方法,并对其进行拓展。

    多态:同一种类型的对象执行同一个方法时可以表现出不同的行为特征。通过继承的上下转型、接口的回调以及方法的重写和重载可以实现多态。

    4、List、Set、Map的区别。

    List

    1)可以允许重复的对象。

    2)可以插入多个null元素。

    3)是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。

    4)常用的实现类有ArrayList、LinkedList和Vector。ArrayList最为流行,它提供了使用索引的随意访问,而LinkedList则对于经常需要从List中添加或删除元素的场合更为合适。

    Set

    1)不允许重复对象

    2)无序容器,你无法保证每个元素的存储顺序,TreeSet通过Comparator或者Comparable维护了一个排序顺序。

    3)只允许一个null元素

    4)Set接口最流行的几个实现类是HashSet、LinkedHashSet以及TreeSet。最流行的是基于HashMap实现的HashSet;TreeSet还实现了SortedSet接口,因此TreeSet是一个根据其compare()和compareTo()的定义进行排序的有序容器。

    map

    1)Map不是collection的子接口或者实现类。Map是一个接口。

    2)Map的每个Entry都持有两个对象,也就是一个键一个值,Map可能会持有相同的值对象但键对象必须是唯一的。

    3)TreeMap也通过Comparator或者Comparable维护了一个排序顺序。

    4)Map里你可以拥有随意个null值但最多只能有一个null键。

    5)Map接口最流行的几个实现类是HashMap、LinkedHashMap、Hashtable和TreeMap。(HashMap、TreeMap最常用)

    5、HashMap和HashTable的区别。

    HashMap和Hashtable都实现了Map接口。主要的区别有:线程安全性,同步(synchronization),以及速度。

    1)HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。

    2)HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。

    3)HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。

    4)由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。

    5)HashMap不能保证随着时间的推移Map中的元素次序是不变的。

    6、实现线程安全的方法有哪些。

    1)内置锁

    2)使用java.util.concurrent包中定义的并发类如:

    ConcurrentHashMap

    ConcurrentLinkedQueue

    ConcurrentSkipListMap

    但是他们支持的并发实现并不一定意味着操作的原子性,他们只是保证数据结构不被破坏。

    3)添加volatile关键字

    volatile:内存可见性,即线程A对volatile变量的修改,其他线程获取的volatile变量都是最新的;可以禁止指令重排序。

    4)同步语句的注意

    你可以使用一个对象来标记同步块,不要使用this,因为this可能代表当前的类,this造成同步的区域是整个类,其他对象就无法调用类中不是同步的方法了,需要等待锁从this指的类中释放才能进行了。所以你可以定义一个对象,然后让同步块的锁指向整个对象来缩小同步块的锁影响范围。

    5)不要在同步块中调用其他的同步块。

    这句话不是绝对的,如果你很了解代码的同步,锁等信息,你可以大胆的这么做。

    7、数据结构有哪些?

    常见的数据结构如下:

    数组 (Array)

    在程序设计中,为了处理方便, 把具有相同类型的若干变量按有序的形式组织起来,这些按序排列的同类数据元素的集合称为数组。一个数组可以分解为多个数组元素,这些数组元素可以是基本数据类型或是构造类型。因此按数组元素的类型不同,数组又可分为数值数组、字符数组、指针数组、结构数组等各种类别。 

    (Stack)

    是只能在某一端插入和删除的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。

    队列 (Queue)

    一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为队空。

    链表 (Linked List)

    是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

    (Tree)

    是包含n(n>0)个结点的有穷集合K,且在K中定义了一个关系N,N满足以下条件:

    1)有且仅有一个结点K0,他对于关系N来说没有前驱,称K0为树的根结点。简称为根(root)。

    2)除K0外,K中的每个结点,对于关系N来说有且仅有一个前驱。

    3)K 中各结点,对关系N来说可以有m个后继(m>=0)。

    (Graph)

    图是由结点的有穷集合V和边的集合E组成。其中,为了与树形结构加以区别,在图结构中常常将结点称为顶点,边是顶点的有序偶对,若两个顶点之间存在一条边,就表示这两个顶点具有相邻关系。

    (Heap)

    在计算机科学中,堆是一种特殊的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。

    散列表 (Hash)

    若结构中存在关键字和K相等的记录,则该记录必定在f(K)的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系f为散列函数(Hash function),按这个思想建立的表为散列表。

    数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。

    8、如何减少Spark中的网络IO。

    使用广播变量、累加变量、使用压缩、使用序列化、尽量不使用shuffle。

    1)高性能序列化类库

    Spark中,默认是使用Java自带的序列化机制——基于ObjectInputStream和ObjectOutputStream的序列化机制,这是为了提高便捷性和适用性,毕竟是Java原生的嘛。然鹅,自带的东西往往考虑的东西比较多,没法做到样样俱全,比如内序列化后占据的内存还是较大,但是Spark是基于内存的大数据框架,对内存的要求很高。所以,在Spark应用程序中,Java自带的序列化库的效率有点差强人意。需求是从实际出发的嘛,最终Spark也提供了另外一种序列化机制——Kryo序列化机制。

    Kryo序列化机制比Java序列化机制更快,序列化后的数据占的内存更小。那么Kryo序列化机制这么好,为什么不选用它是默认序列化库呢?这里提一句话“人无完人,谁能无错”,Kryo序列化机制也样,之所以不选用它为默认序列化机制是因为有些类型虽然实现了Seriralizable接口,但是不一定能够进行序列化;此外,如果要得到最佳的性能,需要在Spark应用程序中,对所有 需要序列化的类型都进行注册。

    使用Kryo序列化机制的方法:

    1、给SparkConf加入一个参数 SparkConf().set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")

    2、对需要序列化的类自行进行注册(因为如果不注册,Kryo必须一直保存类型的全限定名,会占用内存。Spark默认是对Scala中常用的类型自动注册了Kryo的,都在AllScalaRegistry类中)

    SparkConf conf = new SparkConf().setMaster(...).setAppName(...)

    conf.registerKryoClasses(Counter.class)

    JavaSparkContext sc = new JavaSparkContext(conf)

    Kryo序列化机制进行优化达到更优的效果。

    1、优化缓存大小。如果注册的要序列化的自定义的类型,本身很大大,比如包含了超过100个field。会导致要序列化的对象过大。此时需要对Kryo本身进行优化。因为Kryo内部的缓存可能不够存放这么大的class对象。此时需要调用SparkConf.set()方法,设置spark.kryoserializer.buffer.mb参数的值,将其调大以适用。默认情况下spark.kryoserializer.buffer.mb是2,即最大能缓存2M的对象,然后进行序列化。可以在必要时将其调大。比如设置为10。

    2、预先注册自定义类型。虽然不注册自定义类型,Kryo类库也能正常工作,但是那样对于它要序列化的每个对象,都会保存一份它的全限定类名。反而会耗费大量内存。因此通常都预先注册好要序列化的自定义的类。

    总结,需要用到Kryo序列化机制的场景,算子内部使用了外部的大对象或者大数据结构。那么可以切换到Kryo序列化,序列化速度更快,和获得更小的序列化数据,减少内存的消耗。

    2)优化数据结构

    对数据结构的优化,主要是针对Java数据结构(如果用scala开发的话,其实原理也一样的)。其实就是算子里面的局部变量或者算子函数外部的数据结构。比如基于链式结构的数据结构、包装类型的数据结构等,它们在除了本身的数据之外,还会有额外的数据信息来维持它们的数据类型,这样就会比预想占有更大的内存。

    以下是一些优化建议:

    1、能使用数组或字符串就不要用集合类。即优先使用Array,退而求次才是ArrayList、LinkedList、HashMap、HashTable等。熟悉Java语言的都知道集合类一般是泛型的,然鹅泛型的类型是包装类,比如List list = new ArrayList(),就会因为包装类而占有额外的内存,最后占有更多的额外开销。在生产开发中的做法是,对于HashMap、List这种数据,统一用String拼接成特殊格式的字符串。如Map<Integer, Person> persons = new HashMap<Integer, Person>()。可以优化为,特殊的字符串格式:id:name,address|id:name,address...

    2、避免使用多层嵌套的对象结构。public class Teacher { private List students = new ArrayList() }。就是非常不好的例子。因为Teacher类的内部又嵌套了大量的小Student对象。比如说,对于上述例子,也完全可以使用特殊的字符串来进行数据的存储。比如,用json字符串来存储数据,就是一个很好的选择。{"teacherId": 1, "teacherName": "leo", students:[{"studentId": 1, "studentName": "tom"},{"studentId":2, "studentName":"marry"}]}

    3、能用int就不用String。虽然String比集合咧更高效,但是之前说过Java的String是占2个字节的,使用int会优化内存。

    总结,在写Spark程序的时候,要牢牢记住,尽量压榨因语言带来的内存开销,达到节约内存的目的。

    3)广播共享数据

    RDD实质是弹性分布式数据集,在每个节点中的每个task(一个节点可以有很多个task)操作的只是RDD的一部分数据,如果RDD算子操作使用到了算子函数外部的一份大数据的时候,实际上是Spark应用程序把数据文件通过driver发送给每一个节点的每一个task,很明显,这样会造成大量的网络IO操作,大量消耗节点上的内存。其实很容易想到,把一份大数据文件发送给每个节点就OK了,单个节点的所有task共享一份数据,这样就会节省大量的网络IO操作和节省大量内存消耗。

    如果算子函数中,使用到了特别大的数据(比如一份大的配置文件)供每个节点的所有task使用,可以借助Spark提供的共享变量。共享变量有两种,一是广播变量,一是累加器。广播变量是只读的,通常用来提供一份数据给所有的节点,每个节点的task访问访问同一份数据。而累加器是可写可读的,一个累加器一般是用于所有节点对用一个简单的整型变量进行共享累加,共同维护一份数据。这样的话,就不至于将一个大数据拷贝到每一个task上去。而是给每个节点拷贝一份,然后节点上的task共享该数据。

    4)数据本地化

    Spark数据本地化的基本原理

    Spark和MapReduce是如今两个最流行的大数据框架,它们的原理都是计算移动,而数据不移动,计算找数据。这样做的创新性是避免了大量数据的网络传输造成网络IO和内存的消耗。因此引出一个叫“数据本地化”的概念。

    数据本地化对于Spark Job性能有着巨大的影响。如果数据以及要计算它的代码是在同一个节点,性能会非常高。但是,如果数据和计算它的代码是位于不同的节点,那么其中之一必须到另外一方的机器上。通常来说,移动代码到其他节点,会比移动数据到代码所在的节点上去,速度要快得多,因为代码比较小。Spark也正是基于这个数据本地化的原则来构建task调度算法的。

    数据本地化,指的是,数据离计算它的代码有多近。基于数据距离代码的距离,有几种数据本地化级别:

    1、PROCESS_LOCAL:数据和计算它的代码在同一个JVM进程中。

    2、NODE_LOCAL:数据和计算它的代码在一个节点上,但是不在一个进程中,比如在不同的executor进程中,或者是数据在HDFS文件的block中。

    3、NO_PREF:数据从哪里过来,性能都是一样的。

    4、RACK_LOCAL:数据和计算它的代码在一个机架上。

    5、ANY:数据可能在任意地方,比如其他网络环境内,或者其他机架上。

    Spark数据本地化的特点

    Spark倾向于使用最好的本地化级别来调度task,但并不是每次都会使用最好的本地化数据的。在实际中,如果没有任何未处理的数据在空闲的executor上,Spark会放低本地化级别。这时有两个选择:第一,driver等待,直到executor上的cpu释放出来,就分配task等资源给这个executor;第二,立即在任意一个executor上启动一个task。

    Spark会默认等待一段时间(这个事件可以通过参数来设置),来期望在task要处理的数据所在的节点上的executor空闲出一个cpu,从而为其分配task鞥资源。但只要超过了时间,Spark就会将task分配到其他任意一个空闲的executor上。

    可以设置参数,spark.locality系列参数,来调节Spark等待task可以进行数据本地化的时间。spark.locality.wait(3000毫秒)、spark.locality.wait.node、spark.locality.wait.process、spark.locality.wait.rack

    针对以上的分析,我们可以这样调优,增大查找本地化数据的超时时间和重试次数,因为时间更长更利于查找本地化数据的节点的executor,重试次数越多,更多机会尝试查找本地化数据的节点的executor。

    调优方式,主要是spark.locality.wait(3000毫秒)、spark.locality.wait.node、spark.locality.wait.process、spark.locality.wait.rack这些参数,具体的根据实际的业务需求来控制参数就OK了。

    5)reduceByKey和groupByKey的选择

    以下两种方式是等价的,但是实现的原理却不相同。reduceByKey,因为它会在map端,先进行本地combine,可以大大减少要传输到reduce端的数据量,减小网络传输的开销。而groupByKey算子却不会这样优化。所以只有在reduceByKey处理不了时,才用groupByKey().map()来替代。

    scala实现代码如下:

    val counts = pairs.reduceByKey(_ + _)

    val counts = pairs.groupByKey().map(wordCounts => (wordCounts._1, wordCounts._2.sum))

    6)shuffle性能优化

    无论是MapReduce还是Spark,Shuffle阶段是最重要的阶段,它的好坏影响着整个Spark的性能。其实Shuffle阶段的调优,可以从以下的参数入手:

    new SparkConf().set("spark.shuffle.consolidateFiles", "true")

    spark.shuffle.consolidateFiles:是否开启shuffle block file的合并,默认为false。

    spark.reducer.maxSizeInFlight:reduce task的拉取缓存,默认48m。

    spark.shuffle.file.buffer:map task的写磁盘缓存,默认32k。

    spark.shuffle.io.maxRetries:拉取失败的最大重试次数,默认3次。

    spark.shuffle.io.retryWait:拉取失败的重试间隔,默认5s。

    spark.shuffle.memoryFraction:用于reduce端聚合的内存比例,默认0.2,超过比例就会溢出到磁盘上。

    9、Spark中常用的算子。

    Transformation:

    1)map

    map的输入变换函数应用于RDD中所有元素,而mapPartitions应用于所有分区。区别于mapPartitions主要在于调用粒度不同。如parallelize(1 to 10, 3),map函数执行10次,而mapPartitions函数执行3次。

    2)filter(function)

    过滤操作,满足filter内function函数为true的RDD内所有元素组成一个新的数据集。如:filter(a == 1)。

    3)flatMap(function)

    map是对RDD中元素逐一进行函数操作映射为另外一个RDD,而flatMap操作是将函数应用于RDD之中的每一个元素,将返回的迭代器的所有内容构成新的RDD。

    flatMap与map区别在于map为“映射”,而flatMap“先映射,后扁平化”,map对每一次(func)都产生一个元素,返回一个对象,而flatMap多一步就是将所有对象合并为一个对象。

    4)mapPartitions(function)

    区于foreachPartition(属于Action,且无返回值),而mapPartitions可获取返回值。与map的区别前面已经提到过了,但由于单独运行于RDD的每个分区上(block),所以在一个类型为T的RDD上运行时,(function)必须是Iterator<T> => Iterator<U>类型的方法(入参)。

    5)mapPartitionsWithIndex(function)

    mapPartitions类似,但需要提供一个表示分区索引值的整型值作为参数,因此function必须是(int, Iterator<T>)=>Iterator<U>类型的。

    6)sample(withReplacement, fraction, seed)

    采样操作,用于从样本中取出部分数据。withReplacement是否放回,fraction采样比例,seed用于指定的随机数生成器的种子。(是否放回抽样分true和false,fraction取样比例为(0, 1]。seed种子为整型实数。)

    7)union(otherDataSet)

    对于源数据集和其他数据集求并集,不去重。

    8)intersection(otherDataSet)

    对于源数据集和其他数据集求交集,并去重,且无序返回。

    9)distinct([numTasks])

    返回一个在源数据集去重之后的新数据集,即去重,并局部无序而整体有序返回。

    10)groupByKey([numTasks])

    在一个PairRDD或(k,v)RDD上调用,返回一个(k,Iterable<v>)。主要作用是将相同的所有的键值对分组到一个集合序列当中,其顺序是不确定的。groupByKey是把所有的键值对集合都加载到内存中存储计算,若一个键对应值太多,则易导致内存溢出。

    在此,用之前求并集的union方法,将pair1,pair2变为有相同键值的pair3,而后进行groupByKey

    11)reduceByKey(function,[numTasks])

    groupByKey类似,却有不同。如(a,1), (a,2), (b,1), (b,2)。groupByKey产生中间结果为( (a,1), (a,2) ), ( (b,1), (b,2) )。而reduceByKey为(a,3), (b,3)。

    reduceByKey主要作用是聚合,groupByKey主要作用是分组。(function对于key值来进行聚合)

    12)aggregateByKey(zeroValue)(seqOp, combOp, [numTasks])

    类似reduceByKey,对pairRDD中想用的key值进行聚合操作,使用初始值(seqOp中使用,而combOpenCL中未使用)对应返回值为pairRDD,而区于aggregate(返回值为非RDD)

    13)sortByKey([ascending], [numTasks])

    同样是基于pairRDD的,根据key值来进行排序。ascending升序,默认为true,即升序;numTasks

    14)join(otherDataSet,[numTasks])

    加入一个RDD,在一个(k,v)和(k,w)类型的dataSet上调用,返回一个(k,(v,w))的pair dataSet。

    15)cogroup(otherDataSet,[numTasks])

    合并两个RDD,生成一个新的RDD。实例中包含两个Iterable值,第一个表示RDD1中相同值,第二个表示RDD2中相同值(key值),这个操作需要通过partitioner进行重新分区,因此需要执行一次shuffle操作。(若两个RDD在此之前进行过shuffle,则不需要)

    16)cartesian(otherDataSet)

    求笛卡尔乘积。该操作不会执行shuffle操作。

    17)pipe(command,[envVars])

    通过一个shell命令来对RDD各分区进行“管道化”。通过pipe变换将一些shell命令用于Spark中生成的新RDD

    18)coalesce(numPartitions)

    重新分区,减少RDD中分区的数量到numPartitions。

    19)repartition(numPartitions)

    repartition是coalesce接口中shuffle为true的简易实现,即Reshuffle RDD并随机分区,使各分区数据量尽可能平衡。若分区之后分区数远大于原分区数,则需要shuffle。

    20)repartitionAndSortWithinPartitions(partitioner)

    该方法根据partitioner对RDD进行分区,并且在每个结果分区中按key进行排序。

    Action:

    1)reduce(function)

    reduce将RDD中元素两两传递给输入函数,同时产生一个新值,新值与RDD中下一个元素再被传递给输入函数,直到最后只有一个值为止。

    2)collect()

    将一个RDD以一个Array数组形式返回其中的所有元素。

    3)count()

    返回数据集中元素个数,默认Long类型。

    4)first()

    返回数据集的第一个元素(类似于take(1))

    5)takeSample(withReplacement, num, [seed])

    对于一个数据集进行随机抽样,返回一个包含num个随机抽样元素的数组,withReplacement表示是否有放回抽样,参数seed指定生成随机数的种子。

    该方法仅在预期结果数组很小的情况下使用,因为所有数据都被加载到driver端的内存中。

    6)take(n)

    返回一个包含数据集前n个元素的数组(从0下标到n-1下标的元素),不排序。

    7)takeOrdered(n,[ordering])

    返回RDD中前n个元素,并按默认顺序排序(升序)或者按自定义比较器顺序排序。

    8)saveAsTextFile(path)

    dataSet中元素以文本文件的形式写入本地文件系统或者HDFS等。Spark将对每个元素调用toString方法,将数据元素转换为文本文件中的一行记录。

    若将文件保存到本地文件系统,那么只会保存在executor所在机器的本地目录。

    9)saveAsSequenceFile(path)(Java and Scala)

    dataSet中元素以Hadoop SequenceFile的形式写入本地文件系统或者HDFS等。(对pairRDD操作)

    10)saveAsObjectFile(path)(Java and Scala)

    将数据集中元素以ObjectFile形式写入本地文件系统或者HDFS等。

    11)countByKey()

    用于统计RDD[K,V]中每个K的数量,返回具有每个key的计数的(k,int)pairs的hashMap。

    12)foreach(function)

    对数据集中每一个元素运行函数function。

    10、介绍一下RDD。

    RDD是什么

    RDD是Spark的基础数据结构。表现形式为不可变的分区元素的集合,并且可以在集群中并行操作。

    RDD解析

    Resilient(弹性):在DAG的帮助下,具有容错性。即在节点故障导致丢失或者损坏分区,可以重新计算数据。

    Distributed(分布式的):数据存储在多个节点上。

    Dataset(数据集):要处理的数据集。用户可以在外部数据源获取数据,这些数据源可以JSON文件、CSV文件、文本文件获取通过JDBC的形式获取数据库中没有特定数据结构的数据。

    RDD的5特属性

    1、获取分区列表(getPartitions):有一个数据分片列表,能够将数据进行切分,切分后的数据能够进行并行计算,是数据集的原子组成部分。

    2、可以在每一个分区上进行计算(compute):计算每个分区,得到一个可便利的结果,用于说明在父RDD上执行何种计算。

    3、获取每个RDD的依赖(getDependencies):计算每个RDD对父RDD的依赖列表,源RDD没有依赖,通过依赖关系描述血统。

    4、RDD的键值分区器( @transient val partitioner: Option[Partitioner] = None):描述分区模式和数据存放的位置,键值对的RDD根据哈希值进行分区。

    5、在那个分区上进行计算最好(getPreferredLocations):每一个分片的优先计算位置。

    RDD中的数据集是按照逻辑分不到集群中的节点上,这样可以在各个节点上并行计算。RDD具有容错性,在失败的情况下,可以自动恢复。

    RDD可以被缓存,并且可以手动分区。

    开发人员可以将需要再次使用的RDD进行持久化。Spark是默认将数据持久化到内存中,如果内存不足,会将数据写入到磁盘上。用户可以自定义持久化方式。

    为什么需要RDD

    提出RDD的动机有:

    迭代计算。

    交互式的数据挖掘工具。

    DSM(Distributed Shared Memory)是一个通用的抽象,这种通用性使得在集群上高效执行和容错性变得更难。

    RDD具有容错性,这是因为RDD是基于coarse-grained transformation而不是fine-grained updates来更新状态。

    RDD是lazy的,在需要的时候,这样可以节省很多时间并提高效率。

    RDD的特征

    1)In-memory Computation

    Spark RDD具有内存计算功能。RDD将中间的结果存储在内存中,如果内存中存不下的时候,将数据放在磁盘上。

    2)Lazy Evaluations

    Spark的所有转换操作都是Lazy的,如果遇到action操作时,才会执行之前的操作。

    3)Fault Tolerance

    Spark RDD具有容错能力。如果碰到故障时,RDD可以根据追踪数据的依赖关系,来重新生成丢失的数据。

    4)Immutability

    RDD中的数据都是不可变的,可以在进程之间共享使用。

    5)Partitioning

    分区是RDD并行计算的基本单元。每个分区都是一个可变数据的逻辑分区。在现有分区的基础上可以通过一些转换操作生成新的分区。

    6)Coarse-grained Operations

    将一个方法作用于数据集中的每一个元素。

    7)Location-Stickiness

    RDD可以定义计算分区的放置首选项。DAG Scheduler将尽量任务放到数据所在的节点上。

    8)Persistence

    RDD可以根据需要不同,选择相应的存储策略。

    RDD Operation(RDD操作)

    Transformations(转换)

    RDD的转换操作都是lazy的,分为两种narrow transformation, wide transformation。

    1)Narrow Transformations(窄依赖)

    在单个分区中计算记录所需的所有元素都存在父RDD的单个分区中。

    map、flatMap、MapPartition、filter、Sample、Union操作的结果就是Narrow transformation。

    一个父RDD的partition至少会被子RDD的某个partition使用一次。就是一个父类RDD的一个分区不可能对应一个子RDD的多个分区。

    2)Wide transformation(宽依赖)

    在单个分区中计算记录所需的所有元素都存在父RDD的多个分区中。

    intersection、distinct、reduceByKey、GroupByKey、join、Cartesian、repartition、colaesce操作的结果是Wide transformation。

    一个父RDD的partition会被子RDD的partition使用多次。就是一个父RDD的一个分区对应一个子RDD的多个分区。

    Join操作:窄依赖,两个数据集使用相同的分区器;宽依赖,使用不同的分区器。

    可以使用toDebugString,看到RDD的线性信息,如果出现ShuffleDependency,就是发生shuffle操作。

    依赖关系说明

    窄依赖的RDd可以通过相同的键进行联合分区,整个操作都可以在一个集群节点上进行,以流水线的方式计算所有父分区,不会造成网络之间的数据混合。

    宽依赖RDD涉及数据混合,宽依赖需要首先计算好所有父分区数据,然后在节点直接进行shuffle。

    窄依赖能够更有效的进行失效节点的恢复,重新计算丢失RDD分区的父分区,不同节点之间可以并行计算;

    对一个宽窄依赖的血统图,单个节点失效可能导致这个RDD的所有祖先丢失部分数据,因而需要整体重新计算(Shuffle执行时固化操作,以及采取persist缓存策略,可以在固化点或者缓存点重新计算)。

    执行时,调度程序检查依赖性的类型,将窄依赖的RDD划到一组处理当中,即Stage,宽依赖在一个跨越连续的Stage,同时需要显示指定多个子RDD的分区。

    RDD的局限性

    1)No input optimization engine

    RDD没有自动优化的规则,无法使用Spark高级优化器,例如catalyst优化器和Tungsten执行引擎。可以实现手动RDD优化。

    Dataset和DataFrame中不存在这个问题,DataSet和DataFrame可以使用catalyst来产生优化后的逻辑和物理执行计划,这样可以节省空间和提高运行速度。

    2)Runtime type safety

    RDD中没有静态类型和运行时类型安全,并且不允许在运行时检查错误。

    Dataset提供了编译时期类型安全来构建复杂数据工作流。

    3)Degrade when not enough memory

    在存储RDD时,如果没有足够的内存或者磁盘,将会使得RDD的性能下降特别厉害。

    4)Performance limitation & Overhead of serialization & garbage collection

    因为RDD是内存中的JVM对象,这就牵扯到GC和Java序列化,在数据增长时,会需要大量的内存或者磁盘空间。

    GC的成本与Java对象是成正比的,使用数据结构比较少的对象可以减少成本,或者将数据持久化。

    5)Handling structured data

    RDD不提供数据的schema信息。

    Dataset和DataFrame提供了数据的schema信息,可以每一列数据的含义。

  • 相关阅读:
    vue mock数据设置
    vue 的全局拦截器
    vue-resource基础介绍
    快速排序
    Node 中的 stream (流)
    v8垃圾回收和js垃圾回收机制
    Node内存限制与垃圾回收
    ReactNative http网络通讯
    luogu1829 [国家集训队]Crash的数字表格
    luogu2870 [USACO07DEC]最佳牛线,黄金Best Cow Line, Gold
  • 原文地址:https://www.cnblogs.com/huoyuer/p/11161593.html
Copyright © 2020-2023  润新知