大数据简单面试题

本文汇总了大数据面试中常见的100道问题,涉及HashMap与Hashtable的区别、Java垃圾回收机制、Kafka数据丢失的解决办法、Spark与Hadoop的优化策略等,覆盖了数据结构、并发编程、JVM、数据库、分布式系统等多个领域,旨在帮助求职者全面了解和准备大数据面试。
摘要由CSDN通过智能技术生成

面试题

1、HashMap 和 Hashtable 区别

HashMap和Hashtable的存储和遍历都是无序的!

  • 继承的类不同:HashMap继承的是AbstractMap类;Hashtable 继承Dictionary类。但是都实现了Map接口。
  • 线程安全问题:hashmap是非线程安全的,底层是一个Entry数组,put进来的数据,会计算其hash值,然后放到对应的bucket上去,当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点,对链表而言,新加入的节点会从头结点加入;Hashtable是线程安全的,是个同步容器,其中实现线程安全的方式就是给每个方法都加上Synchronized关键字。
  • 关于contains方法:HashMap没有Hashtable的contains方法,有的是containsValue和containsKey;Hashtable有contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。
  • key和value是否可以为null:HashMap和HashTable都不能包含重复的key,但是value可以重复。HashTable,kv都不允许出现null,但是由于kv都是Object类型的对象,put(null,null)操作编译可以通过,但是在运行的时候会抛出NullPointerException(空指针)异常;而HashMap,null可以作为k,但是这样的k只有一个,而v的话,可以有多个k的v为null值。当get方法返回null值的时候,可能是没有该k,也有可能该键对应值为null,所以不能用get方法查询HashMap存不存在指定的k,而应该用containsKey方法判断。
  • 遍历方式的不同:HashMap使用的是keySet、entrySet、values和entrySet+Iterator;Hashtable使用的是keySet、keys+Enumeration、entrySet+Iterator和elements;
  • hash值不同:HashTable直接使用对象的hashCode;HashMap重新计算hash值。(h = key.hashCode()) ^ (h >>> 16);
  • 内部实现的数组初始化和扩容不同:在不指定容量的情况下,HashTable的默认容量为11,而HashMap为16;Hashtable不要求底层数组的容量一定要为2的整数次幂,所以扩容时,将容量变为原来的2倍加1。而HashMap则要求一定为2的整数次幂,扩容时将容量变为原来的2倍。

2、Java 垃圾回收机制和生命周期

对象是否会被回收的两个经典算法:引用计数法,和可达性分析算法。

引用计数法

简单的来说就是判断对象的引用数量。实现方式:给对象共添加一个引用计数器,每当有引用对他进行引用时,计数器的值就加1,当引用失效,也就是不在执行此对象是,他的计数器的值就减1,若某一个对象的计数器的值为0,那么表示这个对象没有人对他进行引用,也就是意味着是一个失效的垃圾对象,就会被gc进行回收。

但是这种简单的算法在当前的jvm中并没有采用,原因是他并不能解决对象之间循环引用的问题。

假设有A和B两个对象之间互相引用,也就是说A对象中的一个属性是B,B中的一个属性时A,这种情况下由于他们的相互引用,从而是垃圾回收机制无法识别。

img

可达性分析(Reachability Analysis)

从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

1)标记-清除算法

标记-清除(Mark-Sweep)算法,是现代垃圾回收算法的思想基础。

标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。

一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象(好多资料说标记出要回收的对象,其实明白大概意思就可以了)。然后,在清除阶段,清除所有未被标记的对象。

img

  • 缺点:

    • 1、效率问题,标记和清除两个过程的效率都不高。
    • 2、空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2)标记-整理算法

标记整理算法,类似与标记清除算法,不过它标记完对象后,不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。

img

  • 优点:

    • 1、相对标记清除算法,解决了内存碎片问题。
    • 2、没有内存碎片后,对象创建内存分配也更快速了(可以使用TLAB进行分配)。
  • 缺点:

    • 1、效率问题,(同标记清除算法)标记和整理两个过程的效率都不高。

3)复制算法

复制算法,可以解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可(还可使用TLAB进行高效分配内存)。

img

  • 图的上半部分是未回收前的内存区域,图的下半部分是回收后的内存区域。通过图,我们发现不管回收前还是回收后都有一半的空间未被利用。

  • 优点:

    • 1、效率高,没有内存碎片。
  • 缺点:

    • 1、浪费一半的内存空间。
    • 2、复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。

4)分代收集算法

当前商业虚拟机都是采用分代收集算法,它根据对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法。

  • 在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,就选用复制算法。

  • 而老年代中,因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记清理”或者“标记整理”算法来进行回收。

  • 图的左半部分是未回收前的内存区域,右半部分是回收后的内存区域。

  • 对象分配策略:

    • 对象优先在 Eden 区域分配,如果对象过大直接分配到 Old 区域。
    • 长时间存活的对象进入到 Old 区域。
  • 改进自复制算法

    • 现在的商业虚拟机都采用这种收集算法来回收新生代,IBM 公司的专门研究表明,新生代中的对象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor 。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
    • HotSpot 虚拟机默认 Eden 和 2 块 Survivor 的大小比例是 8:1:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10% 的内存会被“浪费”。当然,98% 的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

对象分配规则:

  • 对象优先分配在 Eden 区。

如果 Eden 区无法分配,那么尝试把活着的对象放到 Survivor0 中去(Minor GC)ps:清除 Eden、Survivor 区,就是 Minor GC 。总结来说,分配的顺序是:新生代(Eden => Survivor0 => Survivor1)=> 老年代

    • 如果 Survivor1 可以放入,那么放入 Survivor1 之后清除 Eden 和 Survivor0 ,之后再把 Survivor1 中的对象复制到 Survivor0 中,保持 Survivor1 一直为空。
    • 如果 Survivor1 不可以放入,那么直接把它们放入到老年代中,并清除 Eden 和 Survivor0 ,这个过程也称为分配担保
    • 如果 Survivor0 可以放入,那么放入之后清除 Eden 区。
    • 如果 Survivor0 不可以放入,那么尝试把 Eden 和 Survivor0 的存活对象放到 Survivor1 中。
  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。

    这样做的目的是,避免在 Eden 区和两个 Survivor 区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。

  • 长期存活的对象进入老年代。

    虚拟机为每个对象定义了一个年龄计数器,如果对象经过了 1 次 Minor GC 那么对象会进入 Survivor 区,之后每经过一次 Minor GC 那么对象的年龄加 1 ,知道达到阀值对象进入老年区。

  • 动态判断对象的年龄。

    为了更好的适用不同程序的内存情况,虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代。如果 Survivor 区中相同年龄的所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。

  • 空间分配担保。

    每次进行 Minor GC 时,JVM 会计算 Survivor 区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次 Full GC ,如果小于检查 HandlePromotionFailure 设置,如果 true 则只进行 Monitor GC ,如果 false 则进行 Full GC 。

如下是一张对象创建时,分配内存的图:img

3、怎么解决 Kafka 数据丢失的问题

Broker端:

Kafka 某个 Broker 宕机,然后重新选举 Partition 的 leader,此时其他的 follower 刚好还有些数据没有同步,结果此时 leader 挂了,然后选举某个 follower 成 leader 之后,就少了一些数据。

  • 给 Topic 设置 replication.factor 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。
  • 在 Producer 端设置 acks=all:这个是要求Producer 需要等待 ISR 中的所有 Follower 都确认接收到数据后才算一次发送完成。
  • 但是如果Broker 集群里,ISR中只有leader(其他节点都和zookeeper断开连接,或者是都没追上)的情况。此时,acks=allacks=1 就等价了。所以在Borker端设置 min.insync.replicas 参数:这个值必须大于 1 ,这个是要求一个 leader 至少感知到有至少一个 follower 还保持连接,没掉队,这样才能确保 leader 挂了至少还有一个 follower 。
  • 关闭unclean leader选举,即不允许非ISR中的副本被选举为leader,以避免数据丢失

某种状态下,follower2副本落后leader副本很多,并且也不在leader副本和follower1副本所在的ISR(In-Sync Replicas)集合之中。follower2副本正在努力的追赶leader副本以求迅速同步,并且能够加入到ISR中。但是很不幸的是,此时ISR中的所有副本都突然下线,情形如下图所示:

img

此时follower2副本还在,就会进行新的选举,不过在选举之前首先要判断unclean.leader.election.enable参数的值。如果unclean.leader.election.enable参数的值为false,那么就意味着非ISR中的副本不能够参与选举,此时无法进行新的选举,此时整个分区处于不可用状态。如果unclean.leader.election.enable参数的值为true,那么可以从非ISR集合中选举follower副本称为新的leader。

img

我们进一步考虑unclean.leader.election.enable参数为true的情况,在上面的这种情形中follower2副本就顺其自然的称为了新的leader。随着时间的推进,新的leader副本从客户端收到了新的消息,如上图所示。

此时,原来的leader副本恢复,成为了新的follower副本,准备向新的leader副本同步消息,但是它发现自身的LEO比leader副本的LEO还要大。Kafka中有一个准则,follower副本的LEO是不能够大于leader副本的,所以新的follower副本就需要截断日志至leader副本的LEO处。

img

如上图所示,新的follower副本需要删除消息4和消息5,之后才能与新的leader副本进行同步。之后新的follower副本和新的leader副本组成了新的ISR集合,参考下图。

img

原本客户端已经成功的写入了消息4和消息5,而在发生日志截断之后就意味着这2条消息就丢失了,并且新的follower副本和新的leader副本之间的消息也不一致。也就是说如果unclean.leader.election.enable参数设置为true,就有可能发生数据丢失和数据不一致的情况,Kafka的可靠性就会降低;而如果unclean.leader.election.enable参数设置为false,Kafka的可用性就会降低。具体怎么选择需要读者更具实际的业务逻辑进行权衡,可靠性优先还是可用性优先。

producer端来说:

  • push为同步,producer.type设置为 sync;

    如果是异步会丢失数据,flush是kafka的内部机制,kafka优先在内存中完成数据的交换,然后将数据持久化到磁盘.kafka首先会把数据缓存(缓存到内存中)起来再批量flush.可以通过log.flush.interval.messages和log.flush.interval.ms来配置flush间隔。内存缓冲池过满,内存溢出会让数据写入过快,但是落入磁盘过慢,有可能会造成数据的丢失。

  • 设置 retries=MAX(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了。

Consumer端:

消费者是poll拉取数据进行消费的,唯一导致消费者弄丢数据的情况为,消费到了数据,consumer自动提交offset,让broker以为你已经消费好了数据,但是如果你才刚开始准备处理这个数据就挂了,那么这条数据就丢失了。

high-level版本自动提交 offset

所以说要使用low-level版本关闭自动提交 offset ,在处理完之后自己手动提交 offset ,就可以保证数据不会丢。但是这样的话保证不了消息的幂等性,也就是说,刚处理完还未提交offset,自己就挂了,下一次消费的时候会重复消费数据。所以为了保证幂等性:

  • 写库操作,可以先用主键查一下,如果有这条数据就update。
  • 写入redis,没问题,直接set幂等。
  • 让生产者发送每条数据的时候加一个全局唯一id,消费一条数据先去缓存中查找,缓存可以是写入内存的queue或者是redis,没有这条数据就处理并加入缓存,有这条数据就说明之前消费过,就不处理了。
  • 数据库的唯一键约束,保证数据不会重复插入多条,只会报错但是不会有重复数据。
	 //producer用于压缩数据的压缩类型。默认是无压缩。正确的选项值是none、gzip、snappy。压缩最好用于批量处理,批量处理消息越多,压缩性能越好
     props.put("compression.type", "gzip");
     //增加延迟
     props.put("linger.ms", "50");
     //这意味着leader需要等待所有备份都成功写入日志,这种策略会保证只要有一个备份存活就不会丢失数据。这是最强的保证。,
     props.put("acks", "all");
     //无限重试,直到你意识到出现了问题,设置大于0的值将使客户端重新发送任何数据,一旦这些数据发送失败。注意,这些重试与客户端接收到发送错误时的重试没有什么不同。允许重试将潜在的改变数据的顺序,如果这两个消息记录都是发送到同一个partition,则第一个消息失败第二个发送成功,则第二条消息会比第一条消息出现要早。
     props.put("retries ", MAX_VALUE);
     props.put("reconnect.backoff.ms ", 20000);
     props.put("retry.backoff.ms", 20000);
     //关闭unclean leader选举,即不允许非ISR中的副本被选举为leader,以避免数据丢失
     props.put("unclean.leader.election.enable", false);
     //关闭自动提交offset
     props.put("enable.auto.commit", false);
     限制客户端在单个连接上能够发送的未响应请求的个数。设置此值是1表示kafka broker在响应请求之前client不能再向同一个broker发送请求。注意:设置此参数是为了避免消息乱序
     props.put("max.in.flight.requests.per.connection", 1);

4、zookeeper 是如何保证数据一致性的

Zookeeper 具有如下特性:

  • 顺序一致性(有序性)

从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到 Zookeeper 中去。

有序性是 Zookeeper 中非常重要的一个特性。

    • 所有的更新都是全局有序的,每个更新都有一个唯一的时间戳,这个时间戳称为zxid(Zookeeper Transaction Id)。
    • 而读请求只会相对于更新有序,也就是读请求的返回结果中会带有这个 Zookeeper 最新的 zxid 。
  • 原子性

所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,即整个集群要么都成功应用了某个事务,要么都没有应用。

  • 单一视图

无论客户端连接的是哪个 Zookeeper 服务器,其看到的服务端数据模型都是一致的。

  • 可靠性

一旦服务端成功地应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会一直被保留,除非有另一个事务对其进行了变更。

  • 实时性

Zookeeper 保证在一定的时间段内,客户端最终一定能够从服务端上读取到最新的数据状态。

Zookeeper 对于读写请求有所不同:

  • 客户端的读请求可以被集群中的任意一台机器处理,如果读请求在节点上注册了监听器,这个监听器也是由所连接的 Zookeeper 机器来处理。
  • 对于写请求,这些请求会同时发给其他 Zookeeper 机器并且达成一致后,请求才会返回成功。因此,随着 Zookeeper 的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。

Zookeeper保证了顺序一致性(满足最终一致性)。从整体(read 操作 +write 操作)上来说是 sequential consistency(顺序一致性),写操作实现了 Linearizability(线性一致性:也叫强一致性,或者原子一致性)。

顺序一致性:

从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到 Zookeeper 中去。

顺序一致性是 Zookeeper 中非常重要的一个特性。

    • 所有的更新都是全局有序的,每个更新都有一个唯一的时间戳,这个时间戳称为zxid(Zookeeper Transaction Id)。
    • 而读请求只会相对于更新有序,也就是读请求的返回结果中会带有这个 Zookeeper 最新的 zxid 。

Paxos的基本思路:(深入解读zookeeper一致性原理)

假设有一个社团,其中有团员、议员(决议小组成员)两个角色

团员可以向议员申请提案来修改社团制度

议员坐在一起,拿出自己收到的提案,对每个提案进行投票表决,超过半数通过即可生效

为了秩序,规定每个提案都有编号ID,按顺序自增

每个议员都有一个社团制度笔记本,上面记着所有社团制度,和最近处理的提案编号,初始为0

投票通过的规则:

新提案ID 是否大于 议员本中的ID,是议员举手赞同

如果举手人数大于议员人数的半数,即让新提案生效

例如:

刚开始,每个议员本子上的ID都为0,现在有一个议员拿出一个提案:团费降为100元,这个提案的ID自增为1

每个议员都和自己ID对比,一看 1>0,举手赞同,同时修改自己本中的ID为1

发出提案的议员一看超过半数同意,就宣布:1号提案生效

然后所有议员都修改自己笔记本中的团费为100元

以后任何一个团员咨询任何一个议员:“团费是多少?”,议员可以直接打开笔记本查看,并回答:团费为100元

可能会有极端的情况,就是多个议员一起发出了提案,就是并发的情况

例如

刚开始,每个议员本子上的编号都为0,现在有两个议员(A和B)同时发出了提案,那么根据自增规则,这两个提案的编号都为1,但只会有一个被先处理

假设A的提案在B的上面,议员们先处理A提案并通过了,这时,议员们的本子上的ID已经变为了1,接下来处理B的提案,由于它的ID是1,不大于议员本子上的ID,B提案就被拒绝了,B议员需要重新发起提案

上面就是Paxos的基本思路,对照ZooKeeper,对应关系就是:

团员 -client

议员 -server

议员的笔记本 -server中的数据

提案 -变更数据的请求

提案编号 -zxid(ZooKeeper Transaction Id)

提案生效 -执行变更数据的操作

ZooKeeper中还有一个leader的概念,就是把发起提案的权利收紧了,以前是每个议员都可以发起提案,现在有了leader,大家就不要七嘴八舌了,先把提案都交给leader,由leader一个个发起提案

Paxos算法就是通过投票、全局编号机制,使同一时刻只有一个写操作被批准,同时并发的写操作要去争取选票,只有获得过半数选票的写操作才会被批准,所以永远只会有一个写操作得到批准,其他的写操作竞争失败只好再发起一轮投票

ZooKeeper采用了Zab协议。

Zab协议 zookeeper automatic broadcast 两种模式

  1. 恢复(选主)模式,可用性 - 当服务重启或者在leader崩溃之后,进入恢复模式,当leader被选举出来且大多数server完成了和leader的状态同步后恢复模式结束。
  2. 广播模式(同步)一致性

Zab做了如下几条保证,来达到ZooKeeper要求的一致性。

(a) Zab要保证同一个leader的发起的事务要按顺序被apply,同时还要保证只有先前的leader的所有事务都被apply之后,新选的leader才能在发起事务。这个是为了保证每个Server的数据视图的一致性

(b) 一些已经Skip的消息,需要仍然被Skip。

©如果任何一个server按T、T’的顺序提交了事务,那么所有server都必须按T、T’的顺序提交事务。

为了能够实现,Skip已经被skip的消息。我们在Zxid中引入了epoch。

ZooKeeper 采用了递增的事务 id 来识别,所有的 proposal(提议)都在被提出的时候加上了 zxid 。zxid 实际上是一个 64 位数字。

  • 高 32 位是 epoch 用来标识 Leader 是否发生了改变,如果有新的 Leader 产生出来,epoch会自增。
  • 低 32 位用来递增计数。

当新产生的 peoposal 的时候,会依据数据库的两阶段过程,首先会向其他的 Server 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。

假设ZK集群由三台机器组成,Server1、Server2、Server3。Server1为Leader,他生成了三条Proposal,P1、P2、P3。但是在发送完P1之后,Server1就挂了。

img

Server1挂掉之后,Server3被选举成为Leader,因为在Server3里只有一条Proposal—P1。所以,Server3在P1的基础之上又发出了一条新Proposal—P2',由于Leader发生了变换,epoch要加1,所以epoch由原来的0变成了1,而counter要置0。那么,P2'的Zxid为10。

img

Server2发送完P2'之后,它也挂了。此时Server1已经重启恢复,并再次成为了Leader。那么,Server1将发送还没有被deliver的Proposal—P2和P3。由于Server2中P2'的Zxid为10,而Leader-Server1中P2和P3的Zxid分别为02和03,P2'的epoch位高于P2和P3。所以此时Leader-Server1的P2和P3都会被拒绝,那么我们Zab的第二条保证也就实现了。

img

5、hadoop 和 spark 在处理数据时,处理出现内存溢出的方法有哪些?

Hadoop来说

堆内存溢出:

mapreduce.map.java.opts=-Xmx2048m 表示jvm堆内存

mapreduce.map.memory.mb=2304 (container的内存)

mapreduce.reduce.java.opts=-=-Xmx2048m (默认参数,表示jvm堆内存)

mapreduce.reduce.memory.mb=2304 (container的内存)

mapreduce.{map|reduce}.java.opts能够通过Xmx设置JVM最大的heap的使用,一般设置为0.75倍的memory.mb,因为需要为java code等预留些空间

栈内存溢出:

StackOverflowError,递归深度太大,在程序中减少递归。

MRAppMaster内存不足:

yarn.app.mapreduce.am.command-opts=-Xmx1024m(默认参数,表示jvm堆内存)

yarn.app.mapreduce.am.resource.mb=1536(container的内存)

Spark来说

Map过程产生大量对象内存溢出:

​ rdd.map(x => for(i <- 1 to 10000000) yiled i.toString),每个rdd产生大量对象会造成内存溢出问题,通过减少Task大小,也就是先调用repartion方法增加分区再map。

mappartion和foreachpartion:

​ 这两个方法,虽然对比map方法,每个task只执行function1次,会一次把整个partion的数据拿进内存,性能比较高。写入数据库的时候会减少创建数据库连接的次数。但是如果一个partion内数据太多,会直接oom。可以进行repartion操作。

数据不平衡导致内存溢出

​ repartion操作。

coalesce算子

​ coalesce合并多个小文件,开始有100个文件,coalesce(10)后产生10个文件,但是coalesce不产生shuffle,之后的所有操作都是变成10个task,每个task一次读取10个文件执行,用的是原先的10倍内存,会oom。解决办法就是开始执行100个task,然后处理后的结果经过用repation(10)的shuffle过程变成10个分区。

shuffle内存溢出

​ Executor 端的任务并发度,多个同时运行的 Task 会共享 Executor 端的内存,使得单个 Task 可使用的内存减少。

​ 数据倾斜,有可能造成单个 Block 的数据非常的大

RDD中重复数据可以转换成字符串,公用一个对象。

standalone模式下资源分配不均匀

​ 配置了–total-executor-cores 和 –executor-memory 这两个参数,但是没有配置–executor-cores这个参数的话,就有可能导致,每个Executor的memory是一样的,但是cores的数量不同,那么在cores数量多的Executor中,由于能够同时执行多个Task,就容易导致内存溢出的情况。这种情况的解决方法就是同时配置–executor-cores或者spark.executor.cores参数,确保Executor资源分配均匀。

6、java 实现快速排序

public class QuickSort {
   
	public static void quickSort(int[] arr) {
   
        if (arr == null || arr.length < 2) {
   
            return;
        }
        quicksort(arr, 0, arr.length - 1);
    }
    private static void quicksort(int[] arr, int l, int r) {
   
        if (l < r) {
   
            //数组中随机一个数做划分值
            swap(arr, l + (int)(Math.random() * (r - l +1)), r);
            int[] p = partition(arr, l, r);
            quickSort(arr, l, p[0] - 1);
            quickSort(arr, p[1] + 1, r);
        }
    }
    public static int[] partition(int[] arr, int l, int r) {
   
    	int less = l - 1;
        int more = r;
        while (l < more) {
   
            if (arr[l] < arr[r]) {
   
                swap(arr, ++less, l++);
            } else if (arr[l] > arr[r]) {
   
                swap(arr, --more, l);
            } else {
   
                l++;
            }
        }
        swap(arr, more, r);
        // 等于区域的下标位置
        return new int[]{
   less + 1, more};
    }
    private static void swap(int[] arr, int i, int j) {
   
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

7、设计微信群发红包数据库表结构(包含表名称、字段名称、类型)

drop table if exists wc_groupsend_rp;
create external table wc_groupsend_rp (
     imid string, --设备ID
     wcid string, --微信号
     wcname string, --微信名
     wcgroupName string, --群名称
     rpamount double, --红包金额
     rpid string, --红包标识
     rpcount int, --红包数量
     rptype int, --红包类型 比如1拼手气红包,2为普通红包,3为指定人领取红包
     giverpdt string, --发红包时间
    setuprpdt string, --创建红包时间 点击红包按钮的时间     paydt string, --支付时间
) COMMENT '群发红包表'
PARTITIONED BY (`giverpdt` string)
row format delimited fields terminated by '\t';

create external table wc_groupcash_rp (
    rpid string, --红包标识
     imid string, --设备ID
     wcid string, --微信号
     wcname string, --微信名
    wcgroupName string, --群名称
     cashdt stirng, --红包领取时间 每领取一次更新一条数据 
     cashcount int, --领取人数
     cashamount double, --领取金额
     cashwcid string, --领取人的微信
     cashwcname string, --领取人微信昵称
     cashsum double, --已领取总金额
) COMMENT '红包领取表'
PARTITIONED BY (`rpid` string)
row format delimited fields terminated by '\t'; 

8、如何选型:业务场景、性能要求、维护和扩展性、成本、开源活跃度

9、spark 调优

性能调优之在实际项目中广播大变量.xls

性能调优之在实际项目中调节并行度.xls

性能调优之在实际项目中分配更多资源.xls

性能调优之在实际项目中使用fastutil优化数据格式.xls

性能调优之在实际项目中使用Kryo序列化.xls

性能调优之在实际项目中调节数据本地化等待时长.xls

性能调优之在实际项目中重构RDD架构以及RDD持久化.xls

  • Shuffle 调优

Shuffle调优之原理概述.xls

Shuffle调优之HashShuffleManager与SortShuffleManager.xls

Shuffle调优之合并map端输出文件.xls

Shuffle调优之调节map端内存缓冲与reduce端内存占比.xls

JVM调优之调节executor堆外内存与连接等待时长.xls

JVM调优之原理概述以及降低cache操作的内存占比.xls

1)使用foreachPartitions替代foreach。
原理类似于“使用mapPartitions替代map”,也是一次函数调用处理一个partition的所有数据,而不是一次函数调用处理一条数据。在实践中发现,foreachPartitions类的算子,对性能的提升还是很有帮助的。比如在foreach函数中,将RDD中所有数据写MySQL,那么如果是普通的foreach算子,就会一条数据一条数据地写,每次函数调用可能就会创建一个数据库连接,此时就势必会频繁地创建和销毁数据库连接,性能是非常低下;但是如果用foreachPartitions算子一次性处理一个partition的数据,那么对于每个partition,只要创建一个数据库连接即可,然后执行批量插入操作,此时性能是比较高的。实践中发现,对于1万条左右的数据量写MySQL,性能可以提升30%以上。
2)设置num-executors参数
参数说明:该参数用于设置Spark作业总共要用多少个Executor进程来执行。Driver在向YARN集群管理器申请资源时,YARN集群管理器会尽可能按照你的设置来在集群的各个工作节点上,启动相应数量的Executor进程。这个参数非常之重要,如果不设置的话,默认只会给你启动少量的Executor进程,此时你的Spark作业的运行速度是非常慢的。
参数调优建议:该参数设置的太少,无法充分利用集群资源;设置的太多的话,大部分队列可能无法给予充分的资源。针对数据交换的业务场景,建议该参数设置1-53)设置executor-memory参数
参数说明:该参数用于设置每个Executor进程的内存。Executor内存的大小,很多时候直接决定了Spark作业的性能,而且跟常见的JVM OOM异常也有直接的关联。
参数调优建议:针对数据交换的业务场景,建议本参数设置在512M及以下。
4) executor-cores
参数说明:该参数用于设置每个Executor进程的CPU core数量。这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程,因此每个Executor进程的CPU core数量越多,越能够快速地执行完分配给自己的所有task线程。
参数调优建议:Executor的CPU core数量设置为2~4个较为合适。建议,如果是跟他人共享一个队列,那么num-executors * executor-cores不要超过队列总CPU core的1/3~1/2左右比较合适,避免影响其他人的作业运行。
5) driver-memory
参数说明:该参数用于设置Driver进程的内存。
参数调优建议:Driver的内存通常来说不设置,或者设置512M以下就够了。唯一需要注意的一点是,如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。
6) spark.default.parallelism
参数说明:该参数用于设置每个stage的默认task数量。这个参数极为重要,如果不设置可能会直接影响你的Spark作业性能。
参数调优建议:如果不设置这个参数, Spark自己根据底层HDFS的block数量来设置task的数量,默认是一个HDFS block对应一个task。Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适,此时可以充分地利用Spark集群的资源。针对数据交换的场景,建议此参数设置为1-107) spark.storage.memoryFraction
参数说明:该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor 60%的内存,可以用来保存持久化的RDD数据。根据你选择的不同的持久化策略,如果内存不够时,可能数据就不会持久化,或者数据会写入磁盘。
参数调优建议:如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中,降低了性能。但是如果Spark作业中的shuffle类操作比较多,而持久化操作比较少,那么这个参数的值适当降低一些比较合适。如果发现作业由于频繁的gc导致运行缓慢(通过spark web ui可以观察到作业的gc耗时),意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。针对数据交换的场景,建议降低此参数值到0.2-0.48) spark.shuffle.memoryFraction
参数说明:该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。也就是说,Executor默认只有20%的内存用来进行该操作。shuffle操作在进行聚合时,如果发现使用的内存超出了这个20%的限制,那么多余的数据就会溢写到磁盘文件中去,此时就会极大地降低性能。

参数调优建议:如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。如果发现作业由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。针对数据交换的场景,建议此值设置为0.1或以下。

资源参数参考示例:
./bin/spark-submit \

  --master yarn-cluster \

  --num-executors 1 \

  --executor-memory 512M \

  --executor-cores 2 \

  --driver-memory 512M \

  --conf spark.default.parallelism=2 \

  --conf spark.storage.memoryFraction=0.2 \

  --conf spark.shuffle.memoryFraction=0.1;

10、Flink和spark的通信框架

spark用netty,flink用akak

11、java 代理

java代理分为静态代理和动态代理和Cglib代理,下面进行逐个说明。

静态代理:

接口类AdminService.java接口

package com.lance.proxy.demo.service;

public interface AdminService {
   
    void update();
    Object find();
}

实现类AdminServiceImpl.java

package com.lance.proxy.demo.service;

public class AdminServiceImpl implements AdminService{
   
    public void update() {
   
        System.out.println("修改管理系统数据");
    }

    public Object find() {
   
        System.out.println("查看管理系统数据");
        return new Object();
    }
}

代理类AdminServiceProxy.java

package com.lance.proxy.demo.service;

public class AdminServiceProxy implements AdminService {
   

    private AdminService adminService;

    public AdminServiceProxy(AdminService adminService) {
   
        this.adminService = adminService;
    }

    public void update() {
   
        System.out.println("判断用户是否有权限进行update操作");
        adminService.update();
        System.out.println("记录用户执行update操作的用户信息、更改内容和时间等");
    }

    public Object find() {
   
        System.out.println("判断用户是否有权限进行find操作");
        System.out.println("记录用户执行find操作的用户信息、查看内容和时间等");
        return adminService.find();
    }
}

测试类StaticProxyTest.java

package com.lance.proxy.demo.service;

public class StaticProxyTest {
   
    public static void main(String[] args) {
   
        AdminService adminService = new AdminServiceImpl();
        AdminServiceProxy proxy = new AdminServiceProxy(adminService);
        proxy.update();
        System.out.println("=============================");
        proxy.find();
    }
}

输出:

判断用户是否有权限进行update操作
修改管理系统数据
记录用户执行update操作的用户信息、更改内容和时间等
=============================
判断用户是否有权限进行find操作
记录用户执行find操作的用户信息、查看内容和时间等
查看管理系统数据

总结:
静态代理模式在不改变目标对象的前提下,实现了对目标对象的功能扩展。
不足:静态代理实现了目标对象的所有方法,一旦目标接口增加方法,代理对象和目标对象都要进行相应的修改,增加维护成本。

JDK动态代理

为解决静态代理对象必须实现接口的所有方法的问题,Java给出了动态代理,动态代理具有如下特点:
1.Proxy对象不需要implements接口;
2.Proxy对象的生成利用JDK的Api,在JVM内存中动态的构建Proxy对象。需要使用java.lang.reflect.Proxy类

  /**
     * Returns an instance of a proxy class for the specified interfaces
     * that dispatches method invocations to the specified invocation
     * handler.
 
     * @param   loader the class loader to define the proxy class
     * @param   interfaces the list of interfaces for the proxy class
     *          to implement
     * @param   h the invocation handler to dispatch method invocations to
     * @return  a proxy instance with the specified invocation handler of a
     *          proxy class that is defined by the specified class loader
     *          and that implements the specified interfaces
     */
static Object newProxyInstance(ClassLoader loader, Class<?>[]interfaces,InvocationHandler invocationHandler );

方法参数说明:
a.ClassLoader loader:指定当前target对象使用类加载器,获取加载器的方法是固定的;
b.Class<?>[] interfaces:target对象实现的接口的类型,使用泛型方式确认类型
c.InvocationHandler invocationHandler:事件处理,执行target对象的方法时,会触发事件处理器的方法,会把当前执行target对象的方法作为参数传入。

代码为:

AdminServiceImpl.java和AdminService.java和原来一样

AdminServiceInvocation.java

package com.lance.proxy.demo.service;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class AdminServiceInvocation  implements InvocationHandler {
   

    private Object target;

    public AdminServiceInvocation(Object target) {
   
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   
        System.out.println("判断用户是否有权限进行操作");
       Object obj = method.invoke(target);
        System.out.println("记录用户执行操作的用户信息、更改内容和时间等");
        return obj;
    }
}

AdminServiceDynamicProxy.java

package com.lance.proxy.demo.service;

import java.lang.reflect
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值