最近总结一波面试问题(包括python,MySQL,大数据等,一个人力量有限),有兴趣查看 github
数据倾斜以为着某一个或者某几个 partition 的数据特别大,导致这几个 partition 上的计算需要耗费相当长的时间。
在 spark 中同一个应用程序划分成多个 stage,这些 stage 之间是串行执行的,而一个 stage 里面的多个 task 是可以并行执行,task 数目由 partition 数目决定,如果一个 partition 的数目特别大,那么导致这个 task 执行时间很长,导致接下来的 stage 无法执行,从而导致整个 job 执行变慢。避免数据倾斜,一般是要选用合适的 key,或者自己定义相关的 partitioner,通过加盐或者哈希值来拆分这些 key,从而将这些数据分散到不同的 partition 去执行。
如下算子会导致 shuffle 操作,是导致数据倾斜可能发生的关键点所在:
groupByKey;reduceByKey;aggregaByKey;join;cogroup;
/mydata/spark-1.6.3-bin-spark-1.6.3-2.11-withhive/bin/spark-submit \
--master yarn \
--deploy-mode client \
--num-executors 1 \
--executor-memory 7G \
--executor-cores 6 \
--conf spark.ui.port=5052 \
--conf spark.yarn.executor.memoryOverhead=1024 \
--conf spark.storage.memoryFraction=0.2 \
--class UserAnalytics \
/mydata/lei/sparkstreamingproject.py
--num-executors 表示启动多少个 executor 来运行该作业
--executor-memory 表示每一个 executor 进程允许使用的内存空间
--executor-cores 在同一个 executor 里,最多允许多少个 task 可同时并发运
--executor.memoryOverhead 用于存储已被加载的类信息、常量、静态变量等数据
--shuffle.memoryFraction 用于 shuffle 阶段缓存拉取到的数据所使用的内存空间
--storage.memoryFraction 用于 Java 堆栈的空间
海量日志中,提取出某日访问百度次数最多的那个 IP?首先是这一天,并且是访问百度的日志中的 IP 取出来,逐个写入到一个大文件中。注意到IP 是 32 位的,最多有个 2^32 个 IP。同样可以采用映射的方法, 比如模 1000,把整个大文件映射为 1000 个小文件,再找出每个小文中出现频率最大的 IP(可以采用 hash_map进行频率统计,然后再找出频率最大 的几个)及相应的频率。然后再在这 1000 个最大的IP 中,找出那个频率最大的 IP,即为所求。
算法思想:分而治之 + Hash
(1). IP 地址最多有 2^32=4G 种取值情况,所以不能完全加载到内存中处理;
(2). 可以考虑采用“分而治之”的思想,按照 IP 地址的 Hash(IP)%1024 值,把海量 IP日志分别存储到 1024 个小文件中。这样,每个小文件最多包含 4MB 个 IP 地址;
(3). 对于每一个小文件,可以构建一个 IP 为 key,出现次数为 value 的 Hash map,同时记录当前出现次数最多的那个 IP 地址;
(4). 可以得到 1024 个小文件中的出现次数最多的 IP,再依据常规的排序算法得到总体上出现次数最多的 IP;
有一个 1G 大小的一个文件,里面每一行是一个词,词的大小不超过 16 字节,内存限制大小是 1M。返回频数最高的 100 个词。
方案:顺序读文件中,对于每个词 x,取 hash(x)%5000,然后按照该值存到 5000 个小文件(记为 x0,x1,…x4999)中。这样每个文件大概是 200k 左右。如果其中的有的文件超过了 1M 大小,还可以按照类似的方法继续往下分,直到分解得到的小文件的大小都不超过 1M。对每个小文件,统计每个文件中出现的词以及相应的频率(可以采用 trie 树/hash_map 等),并取出出现频率最大的 100 个词(可以用含 100 个结 点的最小堆),并把 100 个词及相应的频率存入文件,这样又得到了 5000 个文件。下一步就是把这 5000 个文件进行归并(类似与归并排序)的过程。
有 10 个文件,每个文件 1G,每个文件的每一行存放的都是用户的 query,每个文件的query 都可能重复。要求你按照 query 的频度排序。还是典型的 TOP K 算法,解决方案如下:
方案 1:顺序读取 10 个文件,按照 hash(query)%10 的结果将 query 写入到另外 10 个文件(记为)中。这样新生成的文件每个的大小大约也 1G(假设 hash 函数是随机的)。找一台内存在 2G 左右的机器,依次对用 hash_map(query, query_count)来统计每个query 出现的次数。利用快速/堆/归并排序按照出现次数进行排序。将排序好的 query 和对应的 query_cout 输出到文件中。这样得到了 10 个排好序的文件(记为)。对这 10 个文件进行归并排序(内排序与外排序相结合)。
方案 2:一般 query 的总量是有限的,只是重复的次数比较多而已,可能对于所有的 query,一次性就可以加入到内存了。这样,我们就可以采用 trie 树/hash_map 等直接来统计每个 query出现的次数,然后按出现次数做快速/堆/归并排序就可以了。
方案 3:与方案 1 类似,但在做完 hash,分成多个文件后,可以交给多个文件来处理,采用分布式的架构来处理(比如 MapReduce),最后再进行合并。
问题:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为 1-255 字节。 假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是 1 千万,但如果除去重复后,不超过 3 百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。),请你统计最热门的 10 个查询串,要求使用的内存不能超过 1G。
答案:典型的 Top K 算法。
第一步、先对这批海量数据预处理,在 O(n) 的时间内用 Hash 表完成统计;
第二步、借助堆这个数据结构,找出 Top K,时间复杂度为 N*logK。即,借助堆结构,我们可以在 log 量级的时间内查找和调整/移动。因此,维护一个 K(该题目中是 10)大小的小根堆,然后遍历 300 万的 Query,分别 和根元素进行对比所以,我们最终的时间复杂度是:O(N) + N’*O(logK),(N 为 1000 万,N’为 300 万)。
或者:采用 trie 树,关键字域存该查询串出现的次数,没有出现为 0。最后用 10 个元素的最小推来对出现频率进行排序。
(1)使用 Kryo 进行序列化。
在 spark 中主要有三个地方涉及到序列化:
第一,在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输;第二,将自定义的类型作为 RDD的泛型数据时(JavaRDD,Student 是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现 serializable 借口;第三,使用可序列化的持久化策略时,spark 会将 RDD 中的每个 partition 都序列化成为一个大的字节数组。
对于这三种出现序列化的地方,我们都可以通过 Kryo 序列化类库,来优化序列化和反序列化的性能。Spark 默认采用的是 Java 的序列化机制。但是 Spark同时支持使用 Kryo 序列化库,而且 Kryo 序列化类库的性能比 Java 的序列化类库要高。官方介绍,Kryo 序列化比 Java 序列化性能高出 10 倍。Spark 之所以认没有使用 Kryo 作为序列化类库,是因为 Kryo 要求最好要注册所有需要进行序列化的自定义类型,因此对于开发者来说这种方式比较麻烦。
(2)优化数据结构。
Spark 官方建议,在 spark 编码实现中,特别对于算子函数中的代码,尽量使用字符串替代对象,使用原始类型(如 int、long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低 GC 频率,提升性能。
(3)对多次使用的 RDD 进行持久化并序列化。
原因:Spark 中对于一个 RDD 执行多次算子的默认原理是这样的:每次对一个 RDD 执行一个算子操作时,都会重新从源头出计算一遍,计算出那个 RDD来,然后再对这个 RDD 执行你的算子操作。这种方式的性能是很差的解决办法:因此对于这种情况,建议是对多次使用的 RDD 进行持久化。此时 spark 就会根据你的持久化策略,将 RDD 中的数据保存到内存或者磁盘中。以后每次对这个 RDD 进行算子操作时,都会直接从内存或磁盘中提取持久化的RDD 数据,然后执行算子,而不会从源头出重新计算一遍这个 RDD。
(4)垃圾回收调优。
首先使用更高效的数据结构,比如 array 和 string;
其次是在持久化 rdd 时,使用序列化的持久化级别,而且使用 Kryo 序列化类库;这样每个 partition 就只是一个对象(一个字节数组)然后是监测垃圾回收。
可以通过在 spark-submit 脚本中,增加一个配置即可--conf" spark.executor.extraJavaOptions=-verbose:gc-XX:+PrintGCDetails -XX:+PrintGCTimeStamps" 注意,这里打印出 java 虚拟机的垃圾回收的相关信息,但是输出到了 worker上的日志,而不是 driver 日志上。还可以通过 sparkUI(4040 端口)来观察每个stage 的垃圾回收的情况;
然后,优化 executor 内存比例。对于垃圾回收来说,最重要的是调节 RDD缓存占用的内存空间,与算子执行时创建对象占用的内存空间的比例。默认是60%存放缓存 RDD,40%存放 task 执行期间创建的对象。出现的问题是,task创建的对象过大,一旦发现 40%内存不够用了,就会频繁触发 GC 操作,从而频繁导致 task 工作线程停止,降低 spark 程序的性能。解决措施是调优这个比例,使用 new SparkConf().set("spark.storage.memoryFraction", "0.5") 即可,给年轻代多的空间来存放短时间存活的对象。
最后,如果发现 task 执行期间大量的 Full GC 发生,那么说明年轻代的 Eden区域给的空间不够大,可以执行以下操作来优化垃圾回收行为:给 Eden 区域分配更大的空间,使用-Xmn 即可,通常建议给 Eden 区域预计大小的 4/3;如果使用 hdfs 文件,那么很好估计 Eden 区域大小。如果每个 executor 有 4个 task,然后每个 hdfs 压缩块解压后大小是 3 倍,此外每个 hdfs 块的大小是 64M,那么 Eden 区域的预计代销就是 4*3*64MB,通过-Xmn 参数,将 Eden 区域大小设置为 4*3*64*4/3.
(5)提高并行度
减少批处理所消耗时间的常见方式还有提高并行度。首先可以增加接收器数目,当记录太多导致但台机器来不及读入并分发的话,接收器会成为系统瓶颈,这时需要创建多个输入DStream 来增加接收器数目,然后使用 union 来把数据合并为一个数据源;然后可以将接收到的数据显式的重新分区,如果接收器数目无法在增加,可以通过使用 DStream.repartition来显式重新分区输入流来重新分配收到的数据;最后可以提高聚合计算的并行度,对于像reduceByKey()这样的操作,可以在第二个参数中制定并行度。
(6)广播大数据集
有时会遇到在算子函数中使用外部变量的场景,建议使用 spark 的广播功能来提升性能。默认情况下,算子函数使用外部变量时会将该变量复制多个副本通过网络传输到 task 中,此时每个 task 都有一个变量副本。如果变量本身比较大,那么大量的变量副本在网络中传输的性能开销以及在各个节点的 executor中占用过多的内存导致频繁 GC,都会极大影响性能。所以建议使用 spark 的广播性能,对该变量进行广播。广播的好处在于,会保证每个 executor 的内存中,只驻留一份变量副本,而 executor 中的 task 执行时会共享该 executor 中的那份变量副本。这样的话,可以大大降低变量副本的数量,从而减少网络传输的性能开销,并减少对 executor 内存的占用开销,降低 GC 的频率。
(7)尽量使用高性能的算子
使用 reduceBykey/aggregateBykey 替代 groupByKey。
原因是:如果因为业务需要,一定要使用 shuffle 操作,无法用 map 类算子来代替,那么尽量使用可以 map-side 预聚合的算子。所谓的 map-side 预聚合,说的是在每个节点本地对相同的 key 进行一次聚合操作,类似于 MR 的本地 combiner。map-side 预聚合之后,每个节点本地就只会有一条相同的 key,因为多条相同的 key 都被聚合起来了。其他节点在拉取所有节点上的相同 key 时,就会大大减少需要拉取的数据量,从而也就减少了磁盘 IO 以及网络传输开销。通过来说,在可能的情况下,建议尽量使用 reduceByKey 或者 aggregateByKey 算子来替代 groupBykey 算子。因为 reduceBykey 和 aggregateBykey 算子都会使用用户自定义的函数对每个节本地相同的 key 进行预聚合。但是 groupbykey 算子是不会进行预聚合的,全量的数据会在集群的各个节点之间分发和传输,性能相对来说比较差。
使用 mapPartitions 替代普通 map;
使用 foreachPartitions 替代 foreach;
使用 filter 之后进行 coalesce 操作:通常对一个 RDD 执行 filter 算子过滤掉RDD 中以后比较多的数据后,建议使用 coalesce 算子,手动减少 RDD 的partitioning 数量,将 RDD 中的数据压缩到更少的 partition 中去,只要使用更少的 task 即可处理完所有的 partition,在某些场景下对性能有提升
使用 repartitionAndSortWithinPartitions 替代 repartition 与 sort 类操作:repartitionAndSortWithinPartitions 是 spark 官网推荐的一个算子。官方建议,如果需要在 repartition 重分区之后,还要进行排序,建议直接使用是这个算子。因为该算子可以一边进行重分区的 shuffle 操作,一边进行排序。Shuffle 和 sort 两个操作同时进行,比先 shuffle 再 sort 来说,性能更高。
(8)批次和窗口大小的设置(针对 spark streaming 中的特殊优化)
最常见的问题是 Spark Streaming 可以使用的最小批次间隔是多少。寻找最小批次大小的最佳实践是从一个比较大的批次开始,不断使用更小的批次大小如果 streaming 用户界面中显示的处理时间保持不变,那么就可以进一步减小批次大小。对于窗口操作,计算结果的间隔对于性能也有巨大的影响。