2021超全大数据面试宝典,吐血总结十万字,大数据面试收藏这一篇就够了

 

本文最新版已发布至公众号【五分钟学大数据】

获取此套面试题最新pdf版,请搜索公众号【五分钟学大数据】,对话框发送 面试宝典

扫码获取最新PDF版:

版本

时间

描述

V1.0

2020-02-18

创建

V1.2

2020-06-17

新增 spark 、flink相关面试题

V1.3

2021-06-18

新增 java、JVM、mysql、JUC等

 

 


 

 

 

 

 

 

本套面试题堪称史上最全,既有面试技巧,面试流程,还有技术总结,面试真题,包含算法,Java,Mysql,大数据框架,大数据项目等(持续更新中最新版请扫描下方二维码关注公众号:五分钟学大数据,回复【面试宝典】获取)。

第一版是按照大数据技术进行划分(另一版,可在公众号【五分钟学大数据】后台发送 面试 获取),第二版是综合版 (此版)。

扫码关注公众号,获取最新PDF版:(对话框发送 面试宝典)

 

目   录

第1章        找工作流程. 1

1.1         学习技能. 1

1.2         编写简历. 1

1.3         投简历. 1

1.4         约面试时间. 1

1.5         面试. 1

1.6         等回复. 1

1.7         拿offer 2

1.8         入职. 2

1.9         准备必备资料,签合同. 2

第2章        面试说明. 2

2.1         笔试. 2

2.2         面试. 3

2.2.1人事面试. 3

2.2.2 ·手写代码. 3

2.2.3技术经理面试. 3

2.2.4 CTO/技术架构师等面试(如果过了技术经理面试). 4

2.3         机试. 4

2.4         面试考察方式. 4

2.4.1知识广度. 4

2.4.2知识深度. 6

2.5         面试过程最关键的是什么?. 7

2.6         面试要点. 7

2.7         面试技巧. 7

2.7.1六个常见问题. 7

2.7.2两个注意事项. 8

2.7.3 自我介绍(控制在4分半以内,不超过5分钟). 8

第3章        手写代码. 9

3.1         冒泡排序. 9

3.2         二分查找. 10

3.3         快排. 12

3.4         归并. 13

3.5         二叉树之Scala实现. 15

3.5.1二叉树概念. 15

3.5.2二叉树的特点. 15

3.5.3二叉树的Scala代码实现. 15

3.6         手写Spark-WordCount 21

第4章        项目架构. 21

4.1         数仓概念. 21

4.2         系统数据流程设计. 22

4.3         框架版本选型. 22

4.4         服务器选型. 22

4.5         集群规模. 23

4.6         人员配置参考. 23

4.6.1整体架构. 23

4.6.2你们部门的职级等级,晋升规则. 23

4.6.3人员配置参考. 23

第5章        项目涉及技术. 24

5.1         Linux&Shell相关总结. 24

5.1.1 Linux常用命令. 24

5.1.2 Shell常用工具. 24

5.2         Hadoop相关总结. 24

5.2.1 Hadoop常用端口号. 24

5.2.2 Hadoop配置文件以及简单的Hadoop集群搭建. 25

5.2.3 HDFS读流程和写流程. 25

5.2.4 MapReduce的Shuffle过程及Hadoop优化(包括:压缩、小文件、集群优化). 26

5.2.5 Yarn的Job提交流程. 28

5.2.6 Yarn的默认调度器、调度器分类、以及他们之间的区别. 29

5.2.7项目经验之LZO压缩. 30

5.2.8 Hadoop参数调优. 31

5.2.9项目经验之基准测试. 32

5.2.10 Hadoop宕机. 33

5.3         Zookeeper相关总结. 33

5.3.1选举机制. 33

5.3.2常用命令. 34

5.4         Flume相关总结. 34

5.4.1 Flume组成,Put事务,Take事务. 34

5.4.2 Flume拦截器. 34

5.4.3 Flume Channel选择器. 35

5.4.4 Flume监控器. 35

5.4.5 Flume采集数据会丢失吗?(防止数据丢失的机制). 36

5.4.6 Flume内存. 36

5.4.7 FileChannel优化. 36

5.4.8 HDFS Sink小文件处理. 37

5.5         Kafka相关总结. 38

5.5.1 Kafka架构. 38

5.5.2 Kafka压测. 38

5.5.3 Kafka的机器数量. 38

5.5.4 Kafka的日志保存时间. 38

5.5.5 Kafka的硬盘大小. 38

5.5.6 Kafka监控. 38

5.5.7 Kakfa分区数. 39

5.5.8副本数设定. 39

5.5.9多少个Topic 39

5.5.10 Kafka丢不丢数据. 39

5.5.11 Kafka的ISR副本同步队列. 39

5.5.12 Kafka中数据量计算. 39

5.5.13 Kafka挂掉. 40

5.5.14 Kafka消息数据积压,Kafka消费能力不足怎么处理?. 40

5.5.15 Kafka的再平衡机制. 40

5.6         Hive相关总结. 47

5.6.1 Hive的架构. 47

5.6.2 Hive和数据库比较. 48

5.6.3内部表和外部表. 48

5.6.4 4个By区别. 48

5.6.5窗口函数. 48

5.6.6 自定义UDF、UDTF 49

5.6.7 Hive优化. 49

5.7         HBase相关总结. 51

5.7.1 HBase存储结构. 51

5.7.2读流程. 51

5.7.3写流程. 52

5.7.4数据flush过程. 52

5.7.5数据合并过程. 52

5.7.6 hbase-default.xml中相关参数. 53

5.7.7 rowkey设计原则. 54

5.7.8 RowKey如何设计. 54

5.8         Sqoop参数. 54

5.8.1 Sqoop导入导出Null存储一致性问题. 54

5.8.2 Sqoop数据导出一致性问题. 54

5.8.3 Sqoop底层运行的任务是什么. 55

5.8.4 Sqoop数据导出的时候一次执行多长时间. 55

5.9         Scala相关总结. 55

5.9.1元组. 55

5.9.2隐式转换. 55

5.9.3函数式编程理解. 56

5.9.4样例类. 56

5.9.5柯里化. 56

5.9.6闭包. 57

5.9.7 Some、None、Option的正确使用. 57

5.10       Spark相关总结. 57

5.10.1 Spark有几种部署方式?请分别简要论述. 57

5.10.2 Spark任务使用什么进行提交,javaEE界面还是脚本. 58

5.10.3 Spark提交作业参数(重点). 58

5.10.4简述Spark的架构与作业提交流程(画图讲解,注明各个部分的作用)(重点). 59

5.10.5如何理解Spark中的血统概念(RDD)(笔试重点). 59

5.10.6简述Spark的宽窄依赖,以及Spark如何划分stage,每个stage又根据什么决定task个数? (笔试重点). 59

5.10.7请列举Spark的transformation算子(不少于8个),并简述功能(重点). 59

5.10.8请列举Spark的action算子(不少于6个),并简述功能(重点). 60

5.10.9请列举会引起Shuffle过程的Spark算子,并简述功能。. 61

5.10.10简述Spark的两种核心Shuffle(HashShuffle与SortShuffle)的工作流程(包括未优化的HashShuffle、优化的HashShuffle、普通的SortShuffle与bypass的SortShuffle)(重点). 61

5.10.11 Spark常用算子reduceByKey与groupByKey的区别,哪一种更具优势?(重点). 62

5.10.12 Repartition和Coalesce关系与区别. 63

5.10.13分别简述Spark中的缓存机制(cache和persist)与checkpoint机制,并指出两者的区别与联系. 63

5.10.14简述Spark中共享变量(广播变量和累加器)的基本原理与用途。(重点). 63

5.10.15当Spark涉及到数据库的操作时,如何减少Spark运行中的数据库连接数?. 64

5.10.16简述SparkSQL中RDD、DataFrame、DataSet三者的区别与联系? (笔试重点). 64

5.10.17 SparkSQL中join操作与left join操作的区别?. 65

5.10.18 SparkStreaming有哪几种方式消费Kafka中的数据,它们之间的区别是什么?(重点). 65

5.10.19简述SparkStreaming窗口函数的原理(重点). 66

5.10.20请手写出wordcount的Spark代码实现(Scala)(手写代码重点). 67

5.10.21如何使用Spark实现topN的获取(描述思路或使用伪代码)(重点). 67

5.10.22京东:调优之前与调优之后性能的详细对比(例如调整map个数,map个数之前多少、之后多少,有什么提升). 67

5.11       Flink相关总结. 68

5.11.1简单介绍一下Flink 68

5.11.2 Flink相比Spark Streaming有什么区别?. 68

5.11.3 Flink中的分区策略有哪几种?. 70

5.11.4 Flink的并行度有了解吗?Flink中设置并行度需要注意什么?. 76

5.11.5 Flink支持哪几种重启策略?分别如何配置?. 76

5.11.6 Flink的分布式缓存有什么作用?如何使用?. 76

5.11.7 Flink中的广播变量,使用广播变量需要注意什么事项?. 77

5.11.8 Flink中对窗口的支持包括哪几种?说说他们的使用场景. 77

5.11.9 Flink 中的 State Backends是什么?有什么作用?分成哪几类?说说他们各自的优缺点?. 78

5.11.10 Flink中的时间种类有哪些?各自介绍一下?. 79

5.11.11 WaterMark是什么?是用来解决什么问题?如何生成水印?水印的原理是什么?. 79

5.11.12 Flink的table和SQL熟悉吗?Table API和SQL中TableEnvironment这个类有什么作用. 79

5.11.13 Flink如何实现SQL解析的呢?. 80

5.11.14 Flink是如何做到批处理与流处理统一的?. 80

5.11.15 Flink中的数据传输模式是怎么样的?. 81

5.11.16 Flink的容错机制. 82

5.11.17 Flink在使用Window时出现数据倾斜,你有什么解决办法?. 82

5.11.18 Flink任务,delay极高,请问你有什么调优策略?. 82

第6章        业务交互数据分析. 83

6.1         电商常识. 83

6.2         电商业务流程. 83

6.3         业务表关键字段. 83

6.3.1订单表(order_info. 83

6.3.2订单详情表(order_detail. 84

6.3.3商品表. 84

6.3.4用户表. 84

6.3.5商品一级分类表. 84

6.3.6商品二级分类表. 85

6.3.7商品三级分类表. 85

6.3.8支付流水表. 85

6.4         MySql中表的分类. 85

6.5         同步策略. 86

6.6         关系型数据库范式理论. 86

6.7         数据模型. 87

6.8         业务数据数仓搭建. 87

6.8.1 ods. 88

6.8.2 dwd. 88

6.8.3 dws. 88

6.9         需求一:GMV成交总额. 89

6.10       需求二:转化率. 89

6.10.1新增用户占日活跃用户比率表. 89

6.10.2用户行为转化率表. 89

6.11       需求三:品牌复购率. 89

6.11.1用户购买商品明细表(宽表). 89

6.11.2品牌复购率表. 89

6.12       项目中有多少张宽表. 90

6.13       拉链表. 90

第7章        项目中遇到过哪些问题. 91

7.1         Hadoop宕机. 91

7.2         Ganglia监控. 92

7.3         Flume小文件. 92

7.4         Kafka挂掉. 92

7.5         Kafka消息数据积压,Kafka消费能力不足怎么处理?. 92

7.6         Kafka数据重复. 92

7.7         Mysql高可用. 92

7.8         自定义UDF和UDTF解析和调试复杂字段. 93

7.9         Sqoop数据导出Parquet 93

7.10       Sqoop数据导出控制. 93

7.11       Sqoop数据导出一致性问题. 93

7.12       SparkStreaming优雅关闭. 94

7.13       Spark OOM、数据倾斜解决. 94

第8章        项目经验. 94

8.1         框架经验. 94

8.1.1 Hadoop 94

8.1.2 Flume 95

8.1.3 Kafka 95

8.1.4 Tez引擎优点(略过)?. 96

8.1.5 Sqoop参数. 96

8.1.6 Azkaban每天执行多少个任务. 96

8.2         业务经验. 97

8.2.1 ODS层采用什么压缩方式和存储格式?. 97

8.2.2 DWD层做了哪些事?. 97

8.2.3 DWS层做了哪些事?. 97

8.2.4分析过哪些指标(一分钟至少说出30个指标). 99

8.2.5分析过最难的两个指标,现场手写. 102

8.2.6数据仓库每天跑多少张表,大概什么时候运行,运行多久?. 103

8.2.7数仓中使用的哪种文件存储格式. 103

8.2.8数仓中用到过哪些Shell脚本及具体功能. 104

8.2.9项目中用过的报表工具. 104

8.2.10测试相关. 104

8.2.11项目实际工作流程. 104

8.2.12项目中实现一个需求大概多长时间. 104

8.2.13项目在3年内迭代次数,每一个项目具体是如何迭代的。. 105

8.2.14项目开发中每天做什么事. 105

第9章        JavaSE(答案精简). 105

9.1         hashMap底层源码,数据结构. 105

9.2         Java自带有哪几种线程池?. 108

9.3         HashMap和HashTable区别. 109

9.4         TreeSet和HashSet区别. 110

9.5         String buffer和String build区别. 110

9.6         Final、Finally、Finalize 110

9.7         ==和Equals区别. 111

第10章     Redis(答案精简). 111

10.1       缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级. 111

10.2       哨兵模式. 118

10.3       数据类型. 119

10.4       持久化. 119

10.5       悲观锁. 120

10.6       乐观锁. 120

10.7       redis是单线程的,为什么那么快. 120

第11章     MySql 121

11.1       MyISAM与InnoDB的区别. 121

11.2       索引. 121

11.3       b-tree和b+tree的区别. 122

11.4       MySQL的事务. 122

11.5       常见面试sql 123

第12章     JVM 128

12.1       JVM内存分哪几个区,每个区的作用是什么? 128

12.2       Java类加载过程? 129

12.3       java中垃圾收集的方法有哪些? 130

12.4       如何判断一个对象是否存活?(或者GC对象的判定方法) 131

12.5       什么是类加载器,类加载器有哪些? 131

12.6       简述Java内存分配与回收策略以及Minor GC和Major GC(full GC)  132

第13章     JUC 132

13.1       Synchronized与Lock的区别. 132

13.2       Runnable和Callable的区别. 133

13.3       什么是分布式锁. 133

13.4       什么是分布式事务. 133

 

 

  1. 找工作流程
  1. 学习技能

1.1.1

  1. 编写简历

简历编写可在公众号【五分钟学大数据】后台发送:简历,获取大数据简历的模板,包含各个行业及各个项目,非常全。可直接扫码:

  1. 投简历

如能内推,尽量走内推路线,可免简历筛选,如果找不到内推路径,可在牛客网上搜内推码,上面有很多公司的员工发内推码。

招聘软件推荐 Boss直聘,这个软件还是很靠谱的,其他的智联招聘,拉勾,前程无忧,猎聘也不错。

  1. 约面试时间
  1. 一般需要多轮面试
  2. 不想去的公司先去面试,积累面试经验,想去的公司约到最后
  3. 合理安排面试时间,给自己尽可能的留一些复习的时间
  1. 面试
  1. 人事面试,了解基本情况--笔试/填表
  2. 一面:技术经理面试(面试官一般是入职之后你的直接上级,此面很重要)
  3. 二面:技术老大面试
  4. 三面:架构师/大Boss面试
  5. .....
  1. 等回复
  1. 不要立马入职
  1. 拿offer
  2. 入职
  3. 准备必备资料,签合同
  1. 面试说明
  1. 笔试
  1. 试题量:一般两页纸,多的有3页纸
  2. 考题类型:选择和简答,简答居多

选择题:基础知识类型的选择题(编程语言,数据库,操作系统,大数据要点知识,网络,计算机组成原理等)

    简答题

    1. 知识点的详细解释类,比如,MapReduce的Shuffle详细过程
    2. 对比之类,Hive内部表和外部表的区别,MapReduce和Spark的异同?

    编程题:算法题(考察逻辑思维,考察解答问题的方式方法等)

    数据库类型的SQL编写题目(MySQL或者Hive)这是必出题,也是分必拿题。否则就全盘覆没了。

SQL:必有(Hive和MySQL)

    场景题

    给你假定一种场景,让你给出解决方案,面试问的最多的也是这种

    遇到了问题(集群节点宕机,任务运行出错,数据丢失等),该如何解决?

    简单脚本题

    写出一个简单的数据处理脚本,或者运维脚本(会的就自己写,不会的就百度搜,或者求救于小伙伴)

  1. 面试
      1. 人事面试

聊基本情况,聊工作经历,聊人生价值观,聊对工作的态度,聊方方面面,就是不聊技术

请事先准备好针对你个人基本情况的面试题

1、比如你原来在上海,为何10月份来北京找工作?

2、比如工作经历中,有4个月是不上班的,怎么解释?

.....

      1. ·手写代码

也就是做题 

当然技术面试过程中,也有可能会出现手写代码的问题

经典手写代码:

  • 快速排序和归并排序,冒泡排序(优先级由高到低)
  • 一个设计模式的实现:比如单例,比如代理,比如装饰器
  • 多线程相关,比如两个线程交替执行
  • java,mapreduce,scala,spark的wordcount
  • 画出你的项目的架构图 或者 数据处理流程架构图
      1. 技术经理面试

基本只聊技术。

基本套路:

  • 先自我介绍
  • 然后介绍项目
  • 从项目入手
  • 先聊业务
  • 再切入到技术点
  • 问如何实现、项目结构、数据处理流程
  • 问遇到过什么难题,怎么解决的,怎么发现的和怎么避免
  • 问相类似的场景,如何做技术选型
  • 问相类似的突发情况,应该怎么决策快速解决问题

    考察广度的同时,也会考察深度

通俗的说,也就是会在不同的方向,不同的领域问各种问题,然后针对你能回答的问题,就深入探讨,以此得知你对这门技术的掌握程度

    所以:

    考察广度,就是看你的技术领域分布

    考察深度,就是看你你对这个技术掌握程度如何

    总体来说:

  • 聊业务
  • 聊技术
  • 聊问题
  • 聊解决方案
  • ....
      1. CTO/技术架构师等面试(如果过了技术经理面试)

    到了这种级别的面试,一般考察技术的就少了。

    更多的是考察发展,眼光更长远的,考察你的职业生涯规划,考察你的价值观,考察你是否符合公司的长期发展战略

    能了解公司更多信息,就切合公司实际去描述

    如果不了解公司的,那就尽量按照通用套路说

  1. 机试

少数场景会有的。但是不要慌。按照自己的本事来。

有些是现场的,这种很少。有些是给出需求之后让你回来之后自己做。

  1. 面试考察方式
      1. 知识广度

编程语言方向

    java是重点(重中之重),其次:scala、python

    java中的重点考察方向:

        集合(优缺点,底层实现,更好的替代方案,如何根据场景选择和使用)

        并发(锁,JMM,各种关键字(volite),技术点,线程池,......)

        面向对象

    数据库方向

    mysql是重点,其次是hbase,redis

    sql语句的编写和优化就不说了,没有不考查的 

    Hadoop体系/Spark体系

    架构原理

    工作机制

    典型的常见流程

    某个功能的详细分析

    问题和运维难点

    集群规模/集群规划

    任务多少

    任务运行总时长

    每天数据量

    总数据量

    多少条记录

    每条记录多大

    每条记录多少个字段

    hive的总表数等等

 

    其他知识:

        ElasticSearch

        Flink

        机器学习

        架构

        优化

        源码

        数据结构

        算法

 

      1. 知识深度

追根究底的问各种你答的上来的东西的底层实现细节,直到你答不上来为止,或者到他满意为止  

关于HashMap的问题:

  • 什么是HashMap,能否自己实现一个?
  • 什么时候使用HashMap,有没有替代品?有没有什么好的hasmap使用经验?
  • HashMap和HashTable的区别?优缺点?如何选择在那种场景使用?有没有更好的map实现类?
  • 你知道HashMap的内部数据结构么?/   put和get操作的原理么?
  • HashMap的初始长度和扩容策略是怎样的?(什么时候触发扩容,扩大到多少,扩容的时候要考虑什么问题?)
  • HashMap初始化传入的容量参数的值就是HashMap实际分配的空间么?
  • HashMap解决hash冲突的策略是什么?
  • HashMap能同步/线程安全么?怎么做?
  • 了解HashMap的条件竞争么?
  • JDK7和JDK8的hashmap一样么?
  • HashMap的key的hash计算规则是怎样的?
  • HashMap的key有什么要求?为什么最好是String或者Integer这种类型呢?为什么不要是自定义对象呢?
  • 了解Hash攻击么?
  • 了解ConcurrentHashMap么?了解他的工作原理么?和HashMap相比较,优势在哪里?
  1. 面试过程最关键的是什么?
  1. 不是你说了什么,而是你怎么说
  2. 大大方方的聊,放松
  1. 面试要点
  1. 乐观开朗,不让要人觉得跟你很难交流,要善于交流,善于倾听

    别人说的合理的给予赞同和钦佩

    别人说的你不赞同的你不要反对,你可以用另外一种方式表现出你的看法和意见,这是讨论和交流,不是针锋相对

  1. 积极向上,所有人都希望融入团队的新人都能给团队增添活力,带给团队乐趣,大家轻松工作,愉快生活
  2. 上进好学,不是说别人一说到不会的,你就说我会去学的,而是要表现出我曾经就是这么学过来的
  3. 有礼貌有情商有智商,不要看起来傻傻的没见过世面一样的,不妄自菲薄,但是也要谦卑,要表现出我有货,但是知道自己不够,自己正在努力
  4. 面试要注意引导。尽量把面试官往你擅长的领域去引导。
  5. 关于回答问题,记住,如果不懂这个问题,可以让面试官再叙述一遍的。如果是真回答不上来,就真诚的回答说不知道,不了解,不太清楚。如果你发现你回答的某些问题的答案,面试官在质疑你,你也不要质疑自己。就一口咬死就是这样的。当然不能是离谱的答案还要坚持。
  6. 总之一切随机应变
  1. 面试技巧
      1. 六个常见问题
  1. 你的优点是什么?

    大胆的说出自己各个方面的优势和特长

不要写消遣类的爱好,比如爬上,唱歌看电影

  1. 你的缺点是什么?

    不要谈自己真实问题;用“缺点”衬托自己的优点

  1. 你的离职原因是什么?
  • 不说前东家坏话,哪怕被伤过
  • 合情合理合法
  • 不要说超过1个以上的原因
  1. 您对薪资的期望是多少?
  • 非终面不深谈薪资
  • 只说区间,不说具体数字
  • 底线是不低于当前薪资
  • 非要具体数字,区间取中间值,或者当前薪资的+20%
  1. 您还有什么想问的问题?
  • 这是体现个人眼界和层次的问题
  • 问题本身不在于面试官想得到什么样的答案,而在于你跟别的应聘者的对比
  • 标准答案:

公司希望我入职后的3-6个月内,给公司解决什么样的问题

公司(或者对这个部门)未来的战略规划是什么样子的?

  1. 您最快多长时间能入职?

    一周左右,如果公司需要,可以适当提前

 

      1. 两个注意事项

 

  1. 职业化的语言
  2. 职业化的形象

 

      1. 自我介绍(控制在4分半以内,不超过5分钟)
  1. 个人基本信息
  2. 工作履历

    时间、公司名称、任职岗位、主要工作内容、工作业绩、离职原因

  1. 深度沟通(也叫压力面试)

    刨根问底下沉式追问(注意是下沉式,而不是发散式的)

    基本技巧:往自己熟悉的方向说

  1. 手写代码
    1. 冒泡排序
/**

 * 冒泡排序 时间复杂度 O(n^2) 空间复杂度O(1)

 */

  public class BubbleSort {

    public static void bubbleSort(int[] data) {

        System.out.println("开始排序");

        int arrayLength = data.length;

        for (int i = 0; i < arrayLength - 1; i++) {

            boolean flag = false;

            for (int j = 0; j < arrayLength - 1 - i; j++) {

                if(data[j] > data[j + 1]){

                    int temp = data[j + 1];

                    data[j + 1] = data[j];

                    data[j] = temp;

                    flag = true;

                }

            }

            System.out.println(java.util.Arrays.toString(data));

            if (!flag)

                break;

        }

    }

    public static void main(String[] args) {

        int[] data = { 9, -16, 21, 23, -30, -49, 21, 30, 30 };

        System.out.println("排序之前:\n" + java.util.Arrays.toString(data));

        bubbleSort(data);

        System.out.println("排序之后:\n" + java.util.Arrays.toString(data));

    }

}
    1. 二分查找

 

实现代码:

/**

 * 二分查找 时间复杂度O(log2n);空间复杂度O(1)

 */

  def binarySearch(arr:Array[Int],left:Int,right:Int,findVal:Int): Int={

  if(left>right){//递归退出条件,找不到,返回-1

    -1

  }

  val midIndex = (left+right)/2

  

  if (findVal < arr(midIndex)){//向左递归查找

    binarySearch(arr,left,midIndex,findVal)

  }else if(findVal > arr(midIndex)){//向右递归查找

    binarySearch(arr,midIndex,right,findVal)

  }else{//查找到,返回下标

    midIndex

  }

}

拓展需求:当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到。

代码实现如下:

/*

{1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到,比如这里的 1000.

//分析

1. 返回的结果是一个可变数组 ArrayBuffer

2. 在找到结果时,向左边扫描,向右边扫描 [条件]

3. 找到结果后,就加入到ArrayBuffer

 */

  def binarySearch2(arr: Array[Int], l: Int, r: Int,

                  findVal: Int): ArrayBuffer[Int] = {

  

  //找不到条件?

  if (l > r) {

    return ArrayBuffer()

  }

  

  val midIndex = (l + r) / 2

  val midVal = arr(midIndex)

  if (midVal > findVal) {

    //向左进行递归查找

    binarySearch2(arr, l, midIndex - 1, findVal)

  } else if (midVal < findVal) { //向右进行递归查找

    binarySearch2(arr, midIndex + 1, r, findVal)

  } else {

    println("midIndex=" + midIndex)

    //定义一个可变数组

    val resArr = ArrayBuffer[Int]()

    //向左边扫描

    var temp = midIndex - 1

    breakable {

      while (true) {

        if (temp < 0 || arr(temp) != findVal) {

          break()

        }

        if (arr(temp) == findVal) {

          resArr.append(temp)

        }

        temp -= 1

      }

    }

    //将中间这个索引加入

    resArr.append(midIndex)

    //向右边扫描

    temp = midIndex + 1

    breakable {

      while (true) {

        if (temp > arr.length - 1 || arr(temp) != findVal) {

          break()

        }

        if (arr(temp) == findVal) {

          resArr.append(temp)

        }

        temp += 1

      }

    }

    return resArr

  }

 

    1. 快排

 

代码实现:

/**

 * 快排 

 * 时间复杂度:平均时间复杂度为O(nlogn)

 * 空间复杂度:O(logn),因为递归栈空间的使用问题

 */

  public class QuickSort {

    public static void main(String[] args) {

        int [] a = {1,6,8,7,3,5,16,4,8,36,13,44};

        QKSourt(a,0,a.length-1);

        for (int i:a) {

            System.out.print(i + " ");

        }

    }

    private static void QKSourt(int[] a, int start, int end) {

        if (a.length < 0){

            return ;

        }

        if (start >= end){

            return ;

        }

        int left = start;

        int right = end;

        int temp = a[left];

        while (left < right){

            while (left < right && a[right] > temp){

                right -- ;

            }

            a[left] = a[right];

            while (left < right && a[left] < temp){

                left ++ ;

            }

            a[right] = a[left];

        }

        a[left] = temp;

        System.out.println(Arrays.toString(a));

        QKSourt(a, start, left -1);

        QKSourt(a,left+1,end);

    }

}
    1. 归并

 

核心思想:不断的将大的数组分成两个小数组,直到不能拆分为止,即形成了单个值。此时使用合并的排序思想对已经有序的数组进行合并,合并为一个大的数据,不断重复此过程,直到最终所有数据合并到一个数组为止。

 

代码实现:

/**

 * 快排 

 * 时间复杂度:O(nlogn)

 * 空间复杂度:O(n)

 */

  def merge(left: List[Int], right: List[Int]): List[Int] = (left, right) match {

  case (Nil, _) => right

  case (_, Nil) => left

  case (x :: xTail, y :: yTail) =>

    if (x <= y) x :: merge(xTail, right)

    else y :: merge(left, yTail)

}
    1. 二叉树之Scala实现
      1. 二叉树概念

 

      1. 二叉树的特点
  1. 树执行查找、删除、插入的时间复杂度都是O(logN)
  2. 遍历二叉树的方法包括前序、中序、后序
  3. 非平衡树指的是根的左右两边的子节点的数量不一致
  4. 在非空二叉树中,第i层的结点总数不超过 , i>=1;
  5. 深度为h的二叉树最多有个结点(h>=1),最少有h个结点;
  6. 对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1;
      1. 二叉树的Scala代码实现

定义节点以及前序、中序、后序遍历

class TreeNode(treeNo:Int){

  val no = treeNo

  var left:TreeNode = null

  var right:TreeNode = null

  

  //后序遍历

  def postOrder():Unit={

    //向左递归输出左子树

    if(this.left != null){

      this.left.postOrder

    }

    //向右递归输出右子树

    if (this.right != null) {

      this.right.postOrder

    }

  

    //输出当前节点值

    printf("节点信息 no=%d \n",no)

  }

  

  //中序遍历

  def infixOrder():Unit={

    //向左递归输出左子树

    if(this.left != null){

      this.left.infixOrder()

    }

  

    //输出当前节点值

    printf("节点信息 no=%d \n",no)

  

    //向右递归输出右子树

    if (this.right != null) {

      this.right.infixOrder()

    }

  }

  

  //前序遍历

  def preOrder():Unit={

    //输出当前节点值

    printf("节点信息 no=%d \n",no)

  

    //向左递归输出左子树

    if(this.left != null){

      this.left.postOrder()

    }

  

    //向右递归输出右子树

    if (this.right != null) {

      this.right.preOrder()

    }

  }

  

  //后序遍历查找

  def postOrderSearch(no:Int): TreeNode = {

    //向左递归输出左子树

    var resNode:TreeNode = null

    if (this.left != null) {

      resNode = this.left.postOrderSearch(no)

    }

    if (resNode != null) {

      return resNode

    }

    if (this.right != null) {

      resNode = this.right.postOrderSearch(no)

    }

    if (resNode != null) {

      return resNode

    }

    println("ttt~~")

    if (this.no == no) {

      return this

    }

    resNode

  }

  

  //中序遍历查找

  def infixOrderSearch(no:Int): TreeNode = {

  

    var resNode : TreeNode = null

    //先向左递归查找

    if (this.left != null) {

      resNode = this.left.infixOrderSearch(no)

    }

    if (resNode != null) {

      return resNode

    }

    println("yyy~~")

    if (no == this.no) {

      return this

    }

    //向右递归查找

    if (this.right != null) {

      resNode = this.right.infixOrderSearch(no)

    }

    return resNode

  

  }

  

  //前序查找

  def preOrderSearch(no:Int): TreeNode = {

    if (no == this.no) {

      return this

    }

    //向左递归查找

    var resNode : TreeNode = null

    if (this.left != null) {

      resNode = this.left.preOrderSearch(no)

    }

    if (resNode != null){

      return  resNode

    }

    //向右边递归查找

    if (this.right != null) {

      resNode = this.right.preOrderSearch(no)

    }

  

    return resNode

  }

  

  //删除节点

  //删除节点规则

  //1如果删除的节点是叶子节点,则删除该节点

  //2如果删除的节点是非叶子节点,则删除该子树

  

  def delNode(no:Int): Unit = {

    //首先比较当前节点的左子节点是否为要删除的节点

    if (this.left != null && this.left.no == no) {

      this.left = null

      return

    }

    //比较当前节点的右子节点是否为要删除的节点

    if (this.right != null && this.right.no == no) {

      this.right = null

      return

    }

    //向左递归删除

    if (this.left != null) {

      this.left.delNode(no)

    }

    //向右递归删除

    if (this.right != null) {

      this.right.delNode(no)

    }

  }

}

 

定义二叉树,前序、中序、后序遍历,前序、中序、后序查找,删除节点

class BinaryTree {

  var root: TreeNode = null

  

  //后序遍历

  def postOrder(): Unit = {

    if (root != null) {

      root.postOrder()

    } else {

      println("当前二叉树为空,不能遍历")

    }

  }

  

  //中序遍历

  def infixOrder(): Unit = {

    if (root != null) {

      root.infixOrder()

    } else {

      println("当前二叉树为空,不能遍历")

    }

  }

  

  //前序遍历

  def preOrder(): Unit = {

    if (root != null) {

      root.preOrder()

    } else {

      println("当前二叉树为空,不能遍历")

    }

  }

  

  //后序遍历查找

  def postOrderSearch(no: Int): TreeNode = {

    if (root != null) {

      root.postOrderSearch(no)

    } else {

      null

    }

  }

  

  //中序遍历查找

  def infixOrderSeacher(no: Int): TreeNode = {

    if (root != null) {

      return root.infixOrderSearch(no)

    } else {

      return null

    }

  }

  

  //前序查找

  def preOrderSearch(no: Int): TreeNode = {

  

    if (root != null) {

      return root.preOrderSearch(no)

    } else {

      //println("当前二叉树为空,不能查找")

      return null

    }

  }

  

  //删除节点

  def delNode(no: Int): Unit = {

    if (root != null) {

      //先处理一下root是不是要删除的

      if (root.no == no) {

        root = null

      } else {

        root.delNode(no)

      }

    }

  

  }

}
    1. 手写Spark-WordCount
val conf: SparkConf =

  new SparkConf().setMaster("local[*]").setAppName("WordCount")

  

  val sc = new SparkContext(conf)

  

  sc.textFile("/input")

  .flatMap(_.split(" "))

  .map((_, 1))

  .reduceByKey(_ + _)

  .saveAsTextFile("/output")

  

  sc.stop()

 

  1. 项目架构
    1. 数仓概念

数据仓库的输入数据源和输出系统分别是什么?

输入系统:埋点产生的用户行为数据、JavaEE后台产生的业务数据。

输出系统:报表系统、用户画像系统、推荐系统

 

    1. 系统数据流程设计

 

    1. 框架版本选型
  1. Apache:运维麻烦,组件间兼容性需要自己调研。(一般大厂使用,技术实力雄厚,有专业的运维人员)
  2. CDH:国内使用最多的版本,但 CM不开源,但其实对中、小公司使用来说没有影响(建议使用)
  3. HDP:开源,可以进行二次开发,但是没有CDH稳定,国内使用较少
    1. 服务器选型

服务器使用物理机还是云主机?

  1. 机器成本考虑:
    1. 物理机:以128G内存,20核物理CPU,40线程,8THDD和2TSSD硬盘,单台报价4W出头,需考虑托管服务器费用。一般物理机寿命5年左右
    2. 云主机,以阿里云为例,差不多相同配置,每年5W
  2. 运维成本考虑:
    1. 物理机:需要有专业的运维人员
    2. 云主机:很多运维工作都由阿里云已经完成,运维相对较轻松
    1. 集群规模

 

 

    1. 人员配置参考
      1. 整体架构

属于研发部,技术总监下面有各个项目组,我们属于数据组,其他还有后端项目组,基础平台等。总监上面就是副总等级别了。其他的还有产品运营部等。

      1. 你们部门的职级等级,晋升规则

职级就分初级,中级,高级。晋升规则不一定,看公司效益和职位空缺。

      1. 人员配置参考

小型公司(3人左右):组长1人,剩余组员无明确分工,并且可能兼顾javaEE和前端。

中小型公司(3~6人左右):组长1人,离线2人左右,实时1人左右(离线一般多于实时),JavaEE 1人(有或者没有人单独负责JavaEE,有时是有组员大数据和JavaEE一起做,或者大数据和前端一起做)。

中型公司(5~10人左右):组长1人,离线3~5人左右(离线处理、数仓),实时2人左右,JavaEE 1人左右(负责对接JavaEE业务),前端1人左右(有或者没有人单独负责前端)。

中大型公司(5~20人左右):组长1人,离线5~10人(离线处理、数仓),实时5人左右,JavaEE2人左右(负责对接JavaEE业务),前端1人(有或者没有人单独负责前端)。(发展比较良好的中大型公司可能大数据部门已经细化拆分,分成多个大数据组,分别负责不同业务)

上面只是参考配置,因为公司之间差异很大,例如ofo大数据部门只有5个人左右,因此根据所选公司规模确定一个合理范围,在面试前必须将这个人员配置考虑清楚,回答时要非常确定。

  1. 项目涉及技术
    1. Linux&Shell相关总结
      1. Linux常用命令

序号

命令

命令解释

1

top

查看内存

2

df -h

查看磁盘存储情况

3

iotop

查看磁盘IO读写(yum install iotop安装)

4

iotop -o

直接查看比较高的磁盘读写程序

5

netstat -tunlp | grep 端口号

查看端口占用情况

6

uptime

查看报告系统运行时长及平均负载

7

ps  aux

查看进程

      1. Shell常用工具

awk、sed、cut、sort

    1. Hadoop相关总结
      1. Hadoop常用端口号

Hadoop 2.x 版本:

  • dfs.namenode.http-address:50070
  • dfs.datanode.http-address:50075
  • SecondaryNameNode辅助名称节点端口号:50090
  • dfs.datanode.address:50010
  • fs.defaultFS:8020 或者9000
  • yarn.resourcemanager.webapp.address:8088
  • 历史服务器web访问端口:19888

Hadoop 3.x版本:(仅列出相对于2.x版本有更改的端口,箭头前为2.x,箭头后为3.x)

NameNode 的端口:

50070 --> 9870

8020 --> 9820,

50470 --> 9871

Secondary NameNode的端口:

50091 --> 9869

50090 --> 9868

DataNode 的端口:

 50020 --> 9867

 50010 --> 9866

 50475 --> 9865

50075 --> 9864

Hadoop KMS 的端口:

16000 --> 9600

      1. Hadoop配置文件以及简单的Hadoop集群搭建
  1. 配置文件:

core-site.xml、hdfs-site.xml、mapred-site.xml、yarn-site.xml

hadoop-env.sh、yarn-env.sh、mapred-env.sh、slaves

  1. 简单的集群搭建过程:

JDK安装

配置SSH免密登录

配置hadoop核心文件: 

格式化namenode

      1. HDFS读流程和写流程

HDFS读数据:

 

HDFS写数据:

 

      1. MapReduce的Shuffle过程及Hadoop优化(包括:压缩、小文件、集群优化)

MapReduce的详细工作流程:

 

 

一、Shuffle机制

  1. Map方法之后Reduce方法之前这段处理过程叫Shuffle
  2. Map方法之后,数据首先进入到分区方法,把数据标记好分区,然后把数据发送到环形缓冲区;环形缓冲区默认大小100m,环形缓冲区达到80%时,进行溢写;溢写前对数据进行排序,排序按照对key的索引进行字典顺序排序,排序的手段快排;溢写产生大量溢写文件,需要对溢写文件进行归并排序;对溢写的文件也可以进行Combiner操作,前提是汇总操作,求平均值不行。最后将文件按照分区存储到磁盘,等待Reduce端拉取。
  3. 每个Reduce拉取Map端对应分区的数据。拉取数据后先存储到内存中,内存不够了,再存储到磁盘。拉取完所有数据后,采用归并排序将内存和磁盘中的数据都进行排序。在进入Reduce方法前,可以对数据进行分组操作。

二、Hadoop优化

  1. HDFS小文件影响
    1. 影响NameNode的寿命,因为文件元数据存储在NameNode的内存中
    2. 影响计算引擎的任务数量,比如每个小的文件都会生成一个Map任务
  2. 数据输入小文件处理:
    1. 合并小文件:对小文件进行归档(Har)、自定义Inputformat将小文件存储成SequenceFile文件。
    2. 采用ConbinFileInputFormat来作为输入,解决输入端大量小文件场景。
    3. 对于大量小文件Job,可以开启JVM重用。
  3. Map阶段
    1. 增大环形缓冲区大小。由100m扩大到200m
    2. 增大环形缓冲区溢写的比例。由80%扩大到90%
    3. 减少对溢写文件的merge次数。
    4. 不影响实际业务的前提下,采用Combiner提前合并,减少 I/O。
  4. Reduce阶段
    1. 合理设置Map和Reduce数:两个都不能设置太少,也不能设置太多。太少,会导致Task等待,延长处理时间;太多,会导致 Map、Reduce任务间竞争资源,造成处理超时等错误。
    2. 设置Map、Reduce共存:调整slowstart.completedmaps参数,使Map运行到一定程度后,Reduce也开始运行,减少Reduce的等待时间。
    3. 规避使用Reduce,因为Reduce在用于连接数据集的时候将会产生大量的网络消耗。
    4. 增加每个Reduce去Map中拿数据的并行数
    5. 集群性能可以的前提下,增大Reduce端存储数据内存的大小。
  5. IO传输
    1. 采用数据压缩的方式,减少网络IO的的时间。安装Snappy和LZOP压缩编码器。
    2. 使用SequenceFile二进制文件
  6. 整体
    1. MapTask默认内存大小为1G,可以增加MapTask内存大小为4-5g
    2. ReduceTask默认内存大小为1G,可以增加ReduceTask内存大小为4-5g
    3. 可以增加MapTask的cpu核数,增加ReduceTask的CPU核数
    4. 增加每个Container的CPU核数和内存大小
    5. 调整每个Map Task和Reduce Task最大重试次数

三、压缩

压缩格式

Hadoop自带?

算法

文件扩展名

支持切分

换成压缩格式后,原来的程序是否需要修改

DEFLATE

是,直接使用

DEFLATE

.deflate

和文本处理一样,不需要修改

Gzip

是,直接使用

DEFLATE

.gz

和文本处理一样,不需要修改

bzip2

是,直接使用

bzip2

.bz2

和文本处理一样,不需要修改

LZO

否,需要安装

LZO

.lzo

需要建索引,还需要指定输入格式

Snappy

否,需要安装

Snappy

.snappy

和文本处理一样,不需要修改

提示:如果面试过程问起,我们一般回答压缩方式为Snappy,特点速度快,缺点无法切分(可以回答在链式MR中,Reduce端输出使用bzip2压缩,以便后续的map任务对数据进行split)

四、切片机制

  1. 简单地按照文件的内容长度进行切片
  2. 切片大小,默认等于Block大小
  3. 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片

提示:切片大小公式:max(0,min(Long_max,blockSize))

      1. Yarn的Job提交流程

 

      1. Yarn的默认调度器、调度器分类、以及他们之间的区别
  1. Hadoop调度器重要分为三类:

FIFO(先进先出调度器)、Capacity Scheduler(容量调度器)和Fair Sceduler(公平调度器)。

Hadoop2.7.2默认的资源调度器是 容量调度器

  1. 区别:

FIFO调度器:先进先出,同一时间队列中只有一个任务在执行。

 

容量调度器:多队列;每个队列内部先进先出,同一时间队列中只有一个任务在执行。队列的并行度为队列的个数。

 

公平调度器:多队列;每个队列内部按照缺额大小分配资源启动任务,同一时间队列中有多个任务执行。队列的并行度大于等于队列的个数。

  1. 一定要强调生产环境中不是使用的FifoScheduler,面试的时侯会发现候选人大概了解这几种调度器的区别,但是问在生产环境用哪种,却说使用的FifoScheduler(企业生产环境一定不会用这个调度的

 

      1. 项目经验之LZO压缩

启用lzo的压缩方式对于小规模集群是很有用处,压缩比率大概能降到原始日志大小的1/3。同时解压缩的速度也比较快。

Hadoop默认不支持LZO压缩,如果需要支持LZO压缩,需要添加jar包,并在hadoop的cores-site.xml文件中添加相关压缩配置。

  1. 环境准备

maven(下载安装,配置环境变量,修改sitting.xml加阿里云镜像)
    gcc-c++
    zlib-devel
    autoconf
    automake
    libtool
    通过yum安装即可

yum -y install gcc-c++ lzo-devel zlib-devel autoconf automake libtool

  1. 下载、安装并编译LZO

wget http://www.oberhumer.com/opensource/lzo/download/lzo-2.10.tar.gz

tar -zxvf lzo-2.10.tar.gz

cd lzo-2.10

./configure -prefix=/usr/local/hadoop/lzo/

make

make install

  1. 编译hadoop-lzo源码
    1. 下载hadoop-lzo的源码

下载地址:https://github.com/twitter/hadoop-lzo/archive/master.zip

    1. 解压之后,修改pom.xml

<hadoop.current.version>2.7.2</hadoop.current.version>

    1. 声明两个临时环境变量

 export C_INCLUDE_PATH=/usr/local/hadoop/lzo/include

 export LIBRARY_PATH=/usr/local/hadoop/lzo/lib

    1. 进入hadoop-lzo-master,执行maven编译命令

mvn package -Dmaven.test.skip=true

    1. 进入target,将hadoop-lzo-0.4.21-SNAPSHOT.jar放到hadoop的classpath下

如${HADOOP_HOME}/share/hadoop/common

    1. 修改core-site.xml增加配置支持LZO压缩

<configuration>

        <property>

            <name>io.compression.codecs</name>

            <value>

            org.apache.hadoop.io.compress.GzipCodec,

            org.apache.hadoop.io.compress.DefaultCodec,

            org.apache.hadoop.io.compress.BZip2Codec,

            org.apache.hadoop.io.compress.SnappyCodec,

            com.hadoop.compression.lzo.LzoCodec,

            com.hadoop.compression.lzo.LzopCodec

            </value>

        </property>

        <property>

            <name>io.compression.codec.lzo.class</name>

            <value>com.hadoop.compression.lzo.LzoCodec</value>

        </property>

</configuration>

 

注意:

LZO本身是不支持分片的,但是我们给LZO压缩的文件加上索引,就支持分片了

 

      1. Hadoop参数调优
  1. 在hdfs-site.xml文件中配置多目录,最好提前配置好,否则更改目录需要重新启动集群
  2. NameNode有一个工作线程池,用来处理不同DataNode的并发心跳以及客户端并发的元数据操作。

dfs.namenode.handler.count=20 * log2(Cluster Size),比如集群规模为10台时,此参数设置为60

  1. 编辑日志存储路径dfs.namenode.edits.dir设置与镜像文件存储路径dfs.namenode.name.dir尽量分开,达到最低写入延迟
  2. 服务器节点上YARN可使用的物理内存总量,默认是8192(MB),注意,如果你的节点内存资源不够8GB,则需要调减小这个值,而YARN不会智能的探测节点的物理内存总量。yarn.nodemanager.resource.memory-mb
  3. 单个任务可申请的最多物理内存量,默认是8192(MB)。yarn.scheduler.maximum-allocation-mb
      1. 项目经验之基准测试

Hadoop 带有一些基准测试程序,可以最少的准备成本轻松运行。基准测试被打包在测试程序JAR文件中,通过无参调用JAR文件可以得到其列表

  1. 查看信息:

 

  1. 写入速度测试

测试写入10个10M文件

 

  1. 测试读文件速度

 

  1. 命令查看

[root@node1 hadoop-2.6.0-cdh5.14.0]# cat TestDFSIO_results.log

----- TestDFSIO ----- : write

           Date & time: Fri Nov 08 10:43:20 CST 2019

       Number of files: 10

Total MBytes processed: 100.0

     Throughput mb/sec: 4.551039912620034

Average IO rate mb/sec: 5.2242536544799805

 IO rate std deviation: 2.1074511594328245

    Test exec time sec: 52.394

 

----- TestDFSIO ----- : read

           Date & time: Fri Nov 08 10:45:28 CST 2019

       Number of files: 10

Total MBytes processed: 100.0

     Throughput mb/sec: 73.85524372230428

Average IO rate mb/sec: 135.5804901123047

 IO rate std deviation: 111.20953898062095

    Test exec time sec: 28.231

 

      1. Hadoop宕机
  1. 如果MR造成系统宕机。此时要控制Yarn同时运行的任务数,和每个任务申请的最大内存。调整参数:yarn.scheduler.maximum-allocation-mb(单个任务可申请的最多物理内存量,默认是8192MB)
  2. 如果写入文件过量造成NameNode宕机。那么调高Kafka的存储大小,控制从Kafka到HDFS的写入速度。高峰期的时候用Kafka进行缓存,高峰期过去数据同步会自动跟上。
    1. Zookeeper相关总结
      1. 选举机制

半数机制

三个核心选举原则:

  1. Zookeeper集群中只有超过半数以上的服务器启动,集群才能正常工作;
  2. 在集群正常工作之前,myid小的服务器给myid大的服务器投票,直到集群正常工作,选出Leader;
  3. 选出Leader之后,之前的服务器状态由Looking改变为Following,以后的服务器都是Follower

比如:

集群半数以上存活,集群可用

假设有五台服务器组成的zookeeper集群,它们的id从1-5,同时它们都是最新启动的

  1. 1启动,选自己
  2. 2启动,选自己(比1大,12选2)
  3. 3启动,选自己(123都选3,超过半数)当选leader
  4. 4启动,已有leader3
  5. 5启动,已有leader3
      1. 常用命令

ls、get、create

    1. Flume相关总结
      1. Flume组成,Put事务,Take事务
  1. flume组成,Put事务,Take事务

Taildir Source:断点续传、多目录。Flume1.6以前需要自己自定义Source记录每次读取文件位置,实现断点续传。

File Channel:数据存储在磁盘,宕机数据可以保存。但是传输速率慢。适合对数据传输可靠性要求高的场景,比如,金融行业。

Memory Channel:数据存储在内存中,宕机数据丢失。传输速率快。适合对数据传输可靠性要求不高的场景,比如,普通的日志数据。

Kafka Channel:减少了Flume的Sink阶段,提高了传输效率。          

Source到Channel是Put事务

Channel到Sink是Take事务

      1. Flume拦截器
  1. 拦截器注意事项

项目中自定义了:ETL拦截器和区分类型拦截器。

采用两个拦截器的优缺点:优点,模块化开发和可移植性;缺点,性能会低一些

  1. 自定义拦截器步骤
    1. 实现 Interceptor
    2. 重写四个方法
    1. initialize 初始化
    2. public Event intercept(Event event) 处理单个Event
    3. public List<Event> intercept(List<Event> events) 处理多个Event,在这个方法中调用Event intercept(Event event)
    4. close 方法
    1. 静态内部类,实现Interceptor.Builder
      1. Flume Channel选择器

 

      1. Flume监控器

Ganglia监控

加州伯克利大学千禧计划的其中一个开源项目.是一个集群汇总监控用的的软件,和Cacti不同,cacti是详细监控集群中每台服务器的运行状态,而Ganglia是将集群中的服务器数据进行汇总然后监控。有时通过cacti或者zabbix(监控软件)看不出来的集群总体负载问题,却能够在Ganglia中体现。被监控的主机(即client)安装ganglia-gmond并启动该进程。服务器端需要安装gmetad和web程序。大致大构图如下:

    参考链接:https://www.cnblogs.com/yinzhengjie/p/9798739.html

 

      1. Flume采集数据会丢失吗?(防止数据丢失的机制)

不会,Channel存储可以存储在File中,数据传输自身有事务。

      1. Flume内存

开发中在flume-env.sh中设置JVM heap为4G或更高,部署在单独的服务器上(4核8线程16G内存)

-Xmx(设定程序运行期间最大可占用的内存大小)与-Xms(设定程序启动时占用内存大小)最好设置一致,减少内存抖动带来的性能影响,如果设置不一致容易导致频繁fullgc。

      1. FileChannel优化

通过配置dataDirs指向多个路径,每个路径对应不同的硬盘,增大Flume吞吐量。

官方说明如下:

checkpointDir和backupCheckpointDir也尽量配置在不同硬盘对应的目录中,保证checkpoint坏掉后,可以快速使用backupCheckpointDir恢复数据

      1. HDFS Sink小文件处理
  1. HDFS存入大量小文件,有什么影响?

元数据层面:每个小文件都有一份元数据,其中包括文件路径,文件名,所有者,所属组,权限,创建时间等,这些信息都保存在Namenode内存中。所以小文件过多,会占用Namenode服务器大量内存,影响Namenode性能和使用寿命

计算层面:默认情况下MR会对每个小文件启用一个Map任务计算,非常影响计算性能。同时也影响磁盘寻址时间。

  1. HDFS小文件处理

官方默认的这三个参数配置写入HDFS后会产生小文件,hdfs.rollInterval、hdfs.rollSize、hdfs.rollCount

基于以上hdfs.rollInterval=3600,hdfs.rollSize=134217728,hdfs.rollCount =0,hdfs.roundValue=10,hdfs.roundUnit= second几个参数综合作用,效果如下:

(1)tmp文件在达到128M时会滚动生成正式文件

(2)tmp文件创建超10分时会滚动生成正式文件

举例:在2018-01-01 05:23的时侯sink接收到数据,那会产生如下tmp文件:

/itcast/20180101/itcast.201801010520.tmp

即使文件内容没有达到128M,也会在05:33时滚动生成正式文件

    1. Kafka相关总结
      1. Kafka架构

 

      1. Kafka压测

Kafka官方自带压力测试脚本(kafka-consumer-perf-test.sh、kafka-producer-perf-test.sh)。Kafka压测时,可以查看到哪个地方出现了瓶颈(CPU,内存,网络IO)。一般都是网络IO达到瓶颈。

      1. Kafka的机器数量

Kafka机器数量=2*(峰值生产速度*副本数/100)+1

      1. Kafka的日志保存时间

7天

      1. Kafka的硬盘大小

每天的数据量*7天

      1. Kafka监控

公司自己开发的监控器;

开源的监控器:KafkaManager、KafkaMonitor

      1. Kakfa分区数

分区数并不是越多越好,一般分区数不要超过集群机器数量。分区数越多占用内存越大(ISR等),一个节点集中的分区也就越多,当它宕机的时候,对系统的影响也就越大。

分区数一般设置为:3-10个

      1. 副本数设定

一般我们设置成2个或3个,很多企业设置为2个。

      1. 多少个Topic

通常情况:多少个日志类型就多少个Topic。也有对日志类型进行合并的。

      1. Kafka丢不丢数据

Ack=0,相当于异步发送,消息发送完毕即offset增加,继续生产。

Ack=1,leader收到leader replica 对一个消息的接受ack才增加offset,然后继续生产。

Ack=-1,leader收到所有replica 对一个消息的接受ack才增加offset,然后继续生产。

      1. Kafka的ISR副本同步队列

ISR(In-Sync Replicas),副本同步队列。ISR中包括Leader和Follower。如果Leader进程挂掉,会在ISR队列中选择一个服务作为新的Leader。有replica.lag.max.messages(延迟条数)和replica.lag.time.max.ms(延迟时间)两个参数决定一台服务是否可以加入ISR副本队列,在0.10版本移除了replica.lag.max.messages参数,防止服务频繁的进去队列。

任意一个维度超过阈值都会把Follower剔除出ISR,存入OSR(Outof-Sync Replicas)列表,新加入的Follower也会先存放在OSR中。

      1. Kafka中数据量计算

每天总数据量100g,每天产生1亿条日志, 10000万/24/60/60=1150条/每秒钟

平均每秒钟:1150条

低谷每秒钟:400条

高峰每秒钟:1150条*(2-20倍)=2300条->23000条

每条日志大小:0.5k-2k

每秒多少数据量:2.3M-20MB

      1. Kafka挂掉
  1. Flume记录
  2. 日志有记录
  3. 短期没事
      1. Kafka消息数据积压,Kafka消费能力不足怎么处理?
  1. 如果是Kafka消费能力不足,则可以考虑增加Topic的分区数,并且同时提升消费组的消费者数量,消费者数=分区数。(两者缺一不可)
  2. 如果是下游的数据处理不及时:提高每批次拉取的数量。批次拉取数据过少(拉取数据/处理时间<生产速度),使处理的数据小于生产的数据,也会造成数据积压。
      1. Kafka的再平衡机制
        1. 什么是再平衡

所谓的再平衡,指的是在kafka consumer所订阅的topic发生变化时发生的一种分区重分配机制。一般有三种情况会触发再平衡:

  1. consumer group中的新增或删除某个consumer,导致其所消费的分区需要分配到组内其他的consumer上;
  2. consumer订阅的topic发生变化,比如订阅的topic采用的是正则表达式的形式,如test-*此时如果有一个新建了一个topic test-user,那么这个topic的所有分区也是会自动分配给当前的consumer的,此时就会发生再平衡;
  3. consumer所订阅的topic发生了新增分区的行为,那么新增的分区就会分配给当前的consumer,此时就会触发再平衡。

Kafka提供的再平衡策略主要有三种:Round Robin,Range和Sticky,默认使用Range。这三种分配策略的主要区别在于:

  1. Round Robin:会采用轮询的方式将当前所有的分区依次分配给所有的consumer;
  2. Range:首先会计算每个consumer可以消费的分区个数,然后按照顺序将指定个数范围的分区分配给各个consumer;
  3. Sticky:这种分区策略是最新版本中新增的一种策略,其主要实现了两个目的:
    1. 将现有的分区尽可能均衡的分配给各个consumer,存在此目的的原因在于Round Robin和Range分配策略实际上都会导致某几个consumer承载过多的分区,从而导致消费压力不均衡;
    2. 如果发生再平衡,那么在重新分配前的基础上会尽力保证当前未宕机的consumer所消费的分区不会被分配给其他的consumer上;

 

本文主要会通过几个示例来对上面讲解的三种分区重分配策略的基本实现原理进行讲解。

 

        1. Round Robin

关于Round Robin重分配策略,其主要采用的是一种轮询的方式分配所有的分区,该策略主要实现的步骤如下。这里我们首先假设有三个topic:t0、t1和t2,这三个topic拥有的分区数分别为1、2和3,那么总共有六个分区,这六个分区分别为:t0-0、t1-0、t1-1、t2-0、t2-1和t2-2。这里假设我们有三个consumer:C0、C1和C2,它们订阅情况为:C0订阅t0,C1订阅t0和t1,C2订阅t0、t1和t2。那么这些分区的分配步骤如下:

  1. 首先将所有的partition和consumer按照字典序进行排序,所谓的字典序,就是按照其名称的字符串顺序,那么上面的六个分区和三个consumer排序之后分别为:

 

  1. 然后依次以按顺序轮询的方式将这六个分区分配给三个consumer,如果当前consumer没有订阅当前分区所在的topic,则轮询的判断下一个consumer:
  1. 尝试将t0-0分配给C0,由于C0订阅了t0,因而可以分配成功;
  2. 尝试将t1-0分配给C1,由于C1订阅了t1,因而可以分配成功;
  3. 尝试将t1-1分配给C2,由于C2订阅了t1,因而可以分配成功;
  4. 尝试将t2-0分配给C0,由于C0没有订阅t2,因而会轮询下一个consumer;
  5. 尝试将t2-0分配给C1,由于C1没有订阅t2,因而会轮询下一个consumer;
  6. 尝试将t2-0分配给C2,由于C2订阅了t2,因而可以分配成功;
  7. 同理由于t2-1和t2-2所在的topic都没有被C0和C1所订阅,因而都不会分配成功,最终都会分配给C2。

按照上述的步骤将所有的分区都分配完毕之后,最终分区的订阅情况如下:

 

从上面的步骤分析可以看出,轮询的策略就是简单的将所有的partition和consumer按照字典序进行排序之后,然后依次将partition分配给各个consumer,如果当前的consumer没有订阅当前的partition,那么就会轮询下一个consumer,直至最终将所有的分区都分配完毕。但是从上面的分配结果可以看出,轮询的方式会导致每个consumer所承载的分区数量不一致,从而导致各个consumer压力不均一。

        1. Range(默认策略)

所谓的Range重分配策略,就是首先会计算各个consumer将会承载的分区数量,然后将指定数量的分区分配给该consumer。这里我们假设有两个consumer:C0和C1,两个topic:t0和t1,这两个topic分别都有三个分区,那么总共的分区有六个:t0-0、t0-1、t0-2、t1-0、t1-1和t1-2。那么Range分配策略将会按照如下步骤进行分区的分配:

  1. 需要注意的是,Range策略是按照topic依次进行分配的,比如我们以t0进行讲解,其首先会获取t0的所有分区:t0-0、t0-1和t0-2,以及所有订阅了该topic的consumer:C0和C1,并且会将这些分区和consumer按照字典序进行排序;
  2. 然后按照平均分配的方式计算每个consumer会得到多少个分区,如果没有除尽,则会将多出来的分区依次计算到前面几个consumer。比如这里是三个分区和两个consumer,那么每个consumer至少会得到1个分区,而3除以2后还余1,那么就会将多余的部分依次算到前面几个consumer,也就是这里的1会分配给第一个consumer,总结来说,那么C0将会从第0个分区开始,分配2个分区,而C1将会从第2个分区开始,分配1个分区;
  3. 同理,按照上面的步骤依次进行后面的topic的分配。

 

最终上面六个分区的分配情况如下:

 

可以看到,如果按照Range分区方式进行分配,其本质上是依次遍历每个topic,然后将这些topic的分区按照其所订阅的consumer数量进行平均的范围分配。这种方式从计算原理上就会导致排序在前面的consumer分配到更多的分区,从而导致各个consumer的压力不均衡。

        1. Sticky

Sticky策略是新版本中新增的策略,顾名思义,这种策略会保证再分配时已经分配过的分区尽量保证其能够继续由当前正在消费的consumer继续消费,当然,前提是每个consumer所分配的分区数量都大致相同,这样能够保证每个consumer消费压力比较均衡。关于这种分配方式的分配策略,我们分两种情况进行讲解,即初始状态的分配和某个consumer宕机时的分配情况。

          1. 初始分配

初始状态分配的特点是,所有的分区都还未分配到任意一个consumer上。这里我们假设有三个consumer:C0、C1和C2,三个topic:t0、t1和t2,这三个topic分别有1、2和3个分区,那么总共的分区为:t0-0、t1-0、t1-1、t2-0、t2-1和t2-2。关于订阅情况,这里C0订阅了t0,C1订阅了t0和1,C2则订阅了t0、t1和t2。这里的分区分配规则如下:

  1. 首先将所有的分区进行排序,排序方式为:首先按照当前分区所分配的consumer数量从低到高进行排序,如果consumer数量相同,则按照分区的字典序进行排序。这里六个分区由于所在的topic的订阅情况各不相同,因而其排序结果如下:

 

  1. 然后将所有的consumer进行排序,其排序方式为:首先按照当前consumer已经分配的分区数量有小到大排序,如果两个consumer分配的分区数量相同,则会按照其名称的字典序进行排序。由于初始时,这三个consumer都没有分配任何分区,因而其排序结果即为其按照字典序进行排序的结果:

 

  1. 然后将各个分区依次遍历分配给各个consumer,首先需要注意的是,这里的遍历并不是C0分配完了再分配给C1,而是每次分配分区的时候都整个的对所有的consumer从头开始遍历分配,如果当前consumer没有订阅当前分区,则会遍历下一个consumer。然后需要注意的是,在整个分配的过程中,各个consumer所分配的分区数是动态变化的,而这种变化是会体现在各个consumer的排序上的,比如初始时C0是排在第一个的,此时如果分配了一个分区给C0,那么C0就会排到最后,因为其拥有的分区数是最多的。上面的六个分区整体的分配流程如下:
    1. 首先将t2-0尝试分配给C0,由于C0没有订阅t2,因而分配不成功,继续轮询下一个consumer;
    2. 然后将t2-0尝试分配给C1,由于C1没有订阅t2,因而分配不成功,继续轮询下一个consumer;
    3. 接着将t2-0尝试分配给C2,由于C2订阅了t2,因而分配成功,此时由于C2分配的分区数发生变化,各个consumer变更后的排序结果为:

 

    1. 接下来的t2-1和t2-2,由于也只有C2订阅了t2,因而其最终还是会分配给C2,最终在t2-0、t2-1和t2-2分配完之后,各个consumer的排序以及其分区分配情况如下:

 

    1. 接着继续分配t1-0,首先尝试将其分配给C0,由于C0没有订阅t1,因而分配不成功,继续轮询下一个consumer;
    2. 然后尝试将t1-0分配给C1,由于C1订阅了t1,因而分配成功,此时各个consumer以及其分配的分区情况如下:

 

    1. 同理,接下来会分配t1-1,虽然C1和C2都订阅了t1,但是由于C1排在C2前面,因而该分区会分配给C1,即:

 

    1. 最后,尝试将t0-0分配给C0,由于C0订阅了t0,因而分配成功,最终的分配结果为:

 

上面的分配过程中,需要始终注意的是,虽然示例中的consumer顺序始终没有变化,但这是由于各个分区分配之后正好每个consumer所分配的分区数量的排序结果与初始状态一致。这里读者也可以比较一下这种分配方式与前面讲解的Round Robin进行对比,可以很明显的发现,Sticky重分配策略分配得更加均匀一些。

          1. 模拟consumer宕机

由于前一个示例中最终的分区分配方式模拟宕机的情形比较简单,因而我们使用另一种订阅策略。这里我们的示例的consumer有三个:C0、C1和C2,topic有四个:t0、t1、t2和t3,每个topic都有两个分区,那么总的分区有:t0-0、t0-1、t1-0、t1-1、t2-0、t2-1、t3-0和t3-1。这里的订阅情况为三个consumer订阅所有的主题,那么如果按照Sticky的分区分配策略,初始状态时,分配情况如下,读者可以按照前一示例讲解的方式进行推算:

 

这里我们假设在消费的过程中,C1发生了宕机,此时就会发生再平衡,而根据Sticky策略,其再分配步骤如下:

  1. 首先会将宕机之后未分配的分区进行排序,排序方式为:首先按照分区所拥有的consumer数量从低到高进行排序,如果consumer数量相同,则按照分区的字典序进行排序。这里需要注意的是,由于只有C1宕机,因而未分配的分区为:t0-1、t2-0和t3-1,排序之后的结果为:

 

  1. 然后将所有的consumer进行排序,排序方式为:首先将consumer按照其所拥有的consumer数量从小到大排序,如果数量相同,则按照consumer名称的字典序进行排序,排序结果如下:

 

  1. 接着依次遍历各个分区,将其分配给各个consumer,需要注意的是,在分配的过程中,consumer所分配的分区数量是在变化的,而这种变化是会反应在consumer的排序上的:
    1. 首先尝试将t0-1分配给C2,由于C2订阅了t0,因而可以分配成功,此时consumer排序和分区分配情况如下,需要注意的是,虽然分配之后,C2和C0的分区数量相同,但是由于按照字典序,C0在C2前面,因而排序情况还是会发生变化:

 

    1. 然后尝试将t2-0分配给C0,由于C0订阅了t2,因而分配可以成功,此时consumer排序和分区分配情况如下:

 

    1. 最后尝试分配t3-1给C2,由于C2订阅了t3,因而分配可以成功,此时consumer排序与分区分配情况如下:

 

在上面的分区分配过程中,我们可以看到,由于分区的不断分配,各个consumer所拥有的分区数量也在不断变化,因而其排序情况也在变化,但是最终可以看到,各个分区是均匀的分配到各个consumer的,并且还保证了当前consumer已经消费的分区是不会分配到其他的consumer上的。

    1. Hive相关总结
      1. Hive的架构

 

      1. Hive和数据库比较

Hive 和数据库除了拥有类似的查询语言,再无类似之处。

  1. 数据存储位置

Hive存储在HDFS。数据库将数据保存在块设备或者本地文件系统中。

  1. 数据更新

Hive中不建议对数据的改写。而数据库中的数据通常是需要经常进行修改的,

  1. 执行延迟

Hive执行延迟较高。数据库的执行延迟较低。当然,这个是有条件的,即数据规模较小,当数据规模大到超过数据库的处理能力的时候,Hive的并行计算显然能体现出优势。

  1. 数据规模

Hive支持很大规模的数据计算;数据库可以支持的数据规模较小。

      1. 内部表和外部表
  1. 内部表:当我们删除一个内部表时,Hive也会删除这个表中数据。内部表不适合和其他工具共享数据。
  2. 外部表:删除该表并不会删除掉原始数据,删除的是表的元数据
      1. 4个By区别
  1. Sort By:分区内有序;
  2. Order By:全局排序,只有一个Reducer;
  3. Distrbute By:类似MR中Partition,进行分区,结合sort by使用。
  4. Cluster By:当Distribute by和Sorts by字段相同时,可以使用Cluster by方式。Cluster by除了具有Distribute by的功能外还兼具Sort by的功能。但是排序只能是升序排序,不能指定排序规则为ASC或者DESC。
      1. 窗口函数

RANK() 排序相同时会重复,总数不会变

DENSE_RANK() 排序相同时会重复,总数会减少

ROW_NUMBER() 会根据顺序计算

  1. OVER():指定分析函数工作的数据窗口大小,这个数据窗口大小可能会随着行的变而变化
  2. CURRENT ROW:当前行
  3. n PRECEDING:往前n行数据
  4. n FOLLOWING:往后n行数据
  5. UNBOUNDED:起点,UNBOUNDED PRECEDING 表示从前面的起点, UNBOUNDED FOLLOWING表示到后面的终点
  6. LAG(col,n):往前第n行数据
  7. LEAD(col,n):往后第n行数据
  8. NTILE(n):把有序分区中的行分发到指定数据的组中,各个组有编号,编号从1开始,对于每一行,NTILE返回此行所属的组的编号。注意:n必须为int类型。
      1. 自定义UDF、UDTF

在项目中是否自定义过UDF、UDTF函数,以及用他们处理了什么问题,及自定义步骤?

  1. 自定义过。
  2. 用UDF函数解析公共字段;用UDTF函数解析事件字段。

自定义UDF:继承UDF,重写evaluate方法

自定义UDTF:继承自GenericUDTF,重写3个方法:initialize(自定义输出的列名和类型),process(将结果返回forward(result)),close

为什么要自定义UDF/UDTF,因为自定义函数,可以自己埋点Log打印日志,出错或者数据异常,方便调试.

      1. Hive优化
  1. MapJoin

如果不指定MapJoin或者不符合MapJoin的条件,那么Hive解析器会将Join操作转换成Common Join,即:在Reduce阶段完成join。容易发生数据倾斜。可以用MapJoin把小表全部加载到内存在map端进行join,避免reducer处理。

  1. 行列过滤

列处理:在SELECT中,只拿需要的列,如果有,尽量使用分区过滤,少用SELECT *。

行处理:在分区剪裁中,当使用外关联时,如果将副表的过滤条件写在Where后面,那么就会先全表关联,之后再过滤。

  1. 采用分桶技术
  2. 采用分区技术
  3. 合理设置Map数
    1. 通常情况下,作业会通过input的目录产生一个或者多个map任务。

主要的决定因素有:input的文件总个数,input的文件大小,集群设置的文件块大小。

    1. 是不是map数越多越好?

答案是否定的。如果一个任务有很多小文件(远远小于块大小128m),则每个小文件也会被当做一个块,用一个map任务来完成,而一个map任务启动和初始化的时间远远大于逻辑处理的时间,就会造成很大的资源浪费。而且,同时可执行的map数是受限的。

    1. 是不是保证每个map处理接近128m的文件块,就高枕无忧了?

答案也是不一定。比如有一个127m的文件,正常会用一个map去完成,但这个文件只有一个或者两个小字段,却有几千万的记录,如果map处理的逻辑比较复杂,用一个map任务去做,肯定也比较耗时。

针对上面的问题2和3,我们需要采取两种方式来解决:即减少map数和增加map数;

  1. 小文件进行合并

在Map执行前合并小文件,减少Map数:CombineHiveInputFormat具有对小文件进行合并的功能(系统默认的格式)。HiveInputFormat没有对小文件合并功能。

  1. 合理设置Reduce数

Reduce个数并不是越多越好

    1. 过多的启动和初始化Reduce也会消耗时间和资源;
    2. 另外,有多少个Reduce,就会有多少个输出文件,如果生成了很多个小文件,那么如果这些小文件作为下一个任务的输入,则也会出现小文件过多的问题;

在设置Reduce个数的时候也需要考虑这两个原则:处理大数据量利用合适的Reduce数;使单个Reduce任务处理数据量大小要合适;

  1. 常用参数

// 输出合并小文件

SET hive.merge.mapfiles = true; -- 默认true,在map-only任务结束时合并小文件

SET hive.merge.mapredfiles = true; -- 默认false,在map-reduce任务结束时合并小文件

SET hive.merge.size.per.task = 268435456; -- 默认256M

SET hive.merge.smallfiles.avgsize = 16777216; -- 当输出文件的平均大小小于该值时,启动一个独立的map-reduce任务进行文件merge

    1. HBase相关总结
      1. HBase存储结构

 

      1. 读流程

 

 

  1. Client先访问zookeeper,从meta表读取region的位置,然后读取meta表中的数据。meta中又存储了用户表的region信息;
  2. 根据namespace、表名和rowkey在meta表中找到对应的region信息;
  3. 找到这个region对应的regionserver;
  4. 查找对应的region;
  5. 先从MemStore找数据,如果没有,再到BlockCache里面读;
  6. BlockCache还没有,再到StoreFile上读(为了读取的效率);
  7. 如果是从StoreFile里面读取的数据,不是直接返回给客户端,而是先写入BlockCache,再返回给客户端。
      1. 写流程

 

图2  HBase写数据流程

  1. Client向HregionServer发送写请求;
  2. HRegionServer将数据写到HLog(write ahead log)。为了数据的持久化和恢复;
  3. HRegionServer将数据写到内存(MemStore);
  4. 反馈Client写成功。
      1. 数据flush过程
  1. 当MemStore数据达到阈值(默认是128M,老版本是64M),将数据刷到硬盘,将内存中的数据删除,同时删除HLog中的历史数据;
  2. 并将数据存储到HDFS中;
  3. 在HLog中做标记点。
      1. 数据合并过程
  1. 当数据块达到3块,Hmaster触发合并操作,Region将数据块加载到本地,进行合并;
  2. 当合并的数据超过256M,进行拆分,将拆分后的Region分配给不同的HregionServer管理;
  3. 当HregionServer宕机后,将HregionServer上的hlog拆分,然后分配给不同的HregionServer加载,修改.META.;
  4. 注意:HLog会同步到HDFS。
      1. hbase-default.xml中相关参数

Flush:

      <!--当memstore的大小超过这个值的时候,会flush到磁盘。128M-->

      <property>

            <name>hbase.hregion.memstore.flush.size</name>

            <value>134217728</value>

      </property>

      <!--单个regionserver的全部memstore的最大值。超过这个值总容量(Max Heap=983.4 M)*0.4,

      一个新的put插入操作会被挂起,强制执行flush操作。 -->

      <property>

            <name>hbase.regionserver.global.memstore.upperLimit</name>

            <value>0.4</value>

      </property>

      <!--当强制执行flush操作的时候,当低于这个值的时候,flush会停止。默认是堆大小的 35% . -->

      <property>

            <name>hbase.regionserver.global.memstore.lowerLimit</name>

            <value>0.35</value>

      </property>

 

Compact:

      <!--当一个HStore含有多于这个值的HStoreFiles(每一个memstore flush产生一个HStoreFile)的时候,会执行一个合并操作,把这HStoreFiles写成一个-->  

      <property>

            <name>hbase.hstore.compactionThreshold</name>

            <value>3</value>

      </property>

      <!--一个Region中的所有HStoreFile的major compactions的时间间隔。默认是1天。-->

      <property>

            <name>hbase.hregion.majorcompaction</name>

            <value>86400000</value>

      </property>

 

Split:

      <!--最大HStoreFile大小。若某个列族的HStoreFile增长达到这个值,这个Hegion会被切割成两个。 默认: 10G.-->

      <property>

            <name>hbase.hregion.max.filesize</name>

            <value>10737418240</value>

      </property>

 

      1. rowkey设计原则
  1. rowkey长度原则(Rowkey的长度被很多开发者建议说设计在10~100个字节,不过建议是越短越好,不要超过16个字节)
  2. rowkey散列原则
  3. rowkey唯一原则
      1. RowKey如何设计
  1. 生成随机数、hash、散列值
  2. 字符串反转
  1. Sqoop参数

/opt/module/sqoop/bin/sqoop import \

--connect \

--username \

--password \

--target-dir \

--delete-target-dir \

--num-mappers \

--fields-terminated-by   \

 

      1. Sqoop导入导出Null存储一致性问题

Hive中的Null在底层是以“\N”来存储,而MySQL中的Null在底层就是Null,为了保证数据两端的一致性。在导出数据时采用--input-null-string和--input-null-non-string两个参数。导入数据时采用--null-string和--null-non-string。

      1. Sqoop数据导出一致性问题

1)场景1:如Sqoop在导出到Mysql时,使用4个Map任务,过程中有2个任务失败,那此时MySQL中存储了另外两个Map任务导入的数据,此时老板正好看到了这个报表数据。而开发工程师发现任务失败后,会调试问题并最终将全部数据正确的导入MySQL,那后面老板再次看报表数据,发现本次看到的数据与之前的不一致,这在生产环境是不允许的。

官网:http://sqoop.apache.org/docs/1.4.6/SqoopUserGuide.html

Since Sqoop breaks down export process into multiple transactions, it is possible that a failed export job may result in partial data being committed to the database. This can further lead to subsequent jobs failing due to insert collisions in some cases, or lead to duplicated data in others. You can overcome this problem by specifying a staging table via the --staging-table option which acts as an auxiliary table that is used to stage exported data. The staged data is finally moved to the destination table in a single transaction.

–staging-table方式

2)场景2:设置map数量为1个(不推荐,面试官想要的答案不只这个)

多个Map任务时,采用–staging-table方式,仍然可以解决数据一致性问题。

      1. Sqoop底层运行的任务是什么

只有Map阶段,没有Reduce阶段的任务。

      1. Sqoop数据导出的时候一次执行多长时间

Sqoop任务5分钟-2个小时的都有。取决于数据量。

  1. Scala相关总结
      1. 元组
  1. 元组的创建

val tuple1 = (1, 2, 3, "heiheihei")

println(tuple1)

  1. 元组数据的访问,注意元组元素的访问有下划线,并且访问下标从1开始,而不是0

val value1 = tuple1._4

println(value1)

  1. 元组的遍历

方式1:

for (elem <- tuple1.productIterator  ) {

   print(elem)

}

println()

方式2:

tuple1.productIterator.foreach(i => println(i))

隐式转换函数是以implicit关键字声明的带有单个参数的函数。这种函数将会自动应用,将值从一种类型转换为另一种类型。

1)Scala中函数的地位:一等公民 
2) Scala中的匿名函数(函数字面量) 
3)Scala中的高阶函数 
4)Scala中的闭包 
5)Scala中的部分应用函数 
6)Scala中的柯里化函数

      1. 样例类

case class Person(name:String,age:Int)

一般使用在 ds=df.as[Person]

      1. 柯里化

函数编程中,接受多个参数的函数都可以转化为接受单个参数的函数,这个转化过程就叫柯里化,柯里化就是证明了函数只需要一个参数而已。其实我们刚的学习过程中,已经涉及到了柯里化操作,所以这也印证了,柯里化就是以函数为主体这种思想发展的必然产生的结果。

1)柯里化的示例

def mul(x: Int, y: Int) = x * y

println(mul(10, 10))

def mulCurry(x: Int) = (y: Int) => x * y

比较两个字符串在忽略大小写的情况下是否相等,注意,这里是两个任务:

  • 全部转大写(或小写)
  • 比较是否相等

针对这两个操作,我们用一个函数去处理的思想,其实无意间也变成了两个函数处理的思想。示例如下:

val a = Array("Hello", "World")

val b = Array("hello", "world")

println(a.corresponds(b)(_.equalsIgnoreCase(_)))

其中corresponds函数的源码如下:

def corresponds[B](that: GenSeq[B])(p: (A,B) => Boolean): Boolean = {

 

val i = this.iterator

 

val j = that.iterator

 

while (i.hasNext && j.hasNex  t)

 

  if (!p(i.next(), j.next()    ))

 

    return fals

!i.hasNext && !j.hasNext

}

尖叫提示:不要设立柯里化存在义这样的命题,柯里化,是面向函数思想的必然产生结果。 

      1. 闭包

一个函数把外部的那些不属于自己的对象也包含(闭合)进来。

案例1:

def minusxy(x: Int) = (y: Int) => x - y

这就是一个闭包:

1) 匿名函数(y: Int) => x -y嵌套在minusxy函数中。

2) 匿名函数(y: Int) => x -y使用了该匿名函数之外的变量x

3) 函数minusxy返回了引用了局部变量的匿名函数

案例2

def minusxy(x: Int) = (y: Int) => x - y

val f1 = minusxy(10)

val f2 = minusxy(10)

println(f1(3) + f2(3))

此处f1,f2这两个函数就叫闭包。

      1. Some、None、Option的正确使用

val map = Map("Tom"-> 23)

map("Jack") // 抛出异常 java.util.NoSuchElementException: key not found: Jack

map.get("Jack") // None

map("Tom") // 23

map.get("Tom") // Some(23)

 

使用模式匹配取出最后结果

val optionAge = map.get("Tom")

val age = optionAge match {

 case Some(x) => optionAge.get

 case None => 0

 

  1.  Spark相关总结
      1. Spark有几种部署方式?请分别简要论述
  1. Local:运行在一台机器上,通常是练手或者测试环境。
  2. Standalone:构建一个基于Master+Slaves的资源调度集群,Spark任务提交给Master运行。是Spark自身的一个调度系统。
  3. Yarn: Spark客户端直接连接Yarn,不需要额外构建Spark集群。有yarn-client和yarn-cluster两种模式,主要区别在于:Driver程序的运行节点。
  4. Mesos:国内大环境比较少用。
      1. Spark任务使用什么进行提交,javaEE界面还是脚本

Shell 脚本。

      1. Spark提交作业参数(重点)

参考答案:

https://blog.csdn.net/gamer_gyt/article/details/79135118

  1. 在提交任务时的几个重要参数

executor-cores —— 每个executor使用的内核数,默认为1,官方建议2-5个,我们企业是4个

num-executors —— 启动executors的数量,默认为2

executor-memory —— executor内存大小,默认1G

driver-cores —— driver使用内核数,默认为1

driver-memory —— driver内存大小,默认512M

  1. 给一个提交任务的样式

spark-submit \

  --master local[5]  \

  --driver-cores 2   \

  --driver-memory 8g \

  --executor-cores 4 \

  --num-executors 10 \

  --executor-memory 8g \

  --class PackageName.ClassName XXXX.jar \

  --name "Spark Job Name" \

  InputPath      \

  OutputPath

      1. 简述Spark的架构与作业提交流程(画图讲解,注明各个部分的作用)(重点

 

      1. 如何理解Spark中的血统概念(RDD)(笔试重点

RDD在Lineage依赖方面分为两种Narrow Dependencies与Wide Dependencies用来解决数据容错时的高效性以及划分任务时候起到重要作用。

      1. 简述Spark的宽窄依赖,以及Spark如何划分stage,每个stage又根据什么决定task个数? (笔试重点

Stage:根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage。

Task:Stage是一个TaskSet,将Stage根据分区数划分成一个个的Task。

      1. 请列举Spark的transformation算子(不少于8个),并简述功能(重点)
  1. map(func):返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成.
  2. mapPartitions(func):类似于map,但独立地在RDD的每一个分片上运行,因此在类型为T的RD上运行时,func的函数类型必须是Iterator[T] => Iterator[U]。假设有N个元素,有M个分区,那么map的函数的将被调用N次,而mapPartitions被调用M次,一个函数一次处理所有分区。
  3. reduceByKey(func,[numTask]):在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用定的reduce函数,将相同key的值聚合到一起,reduce任务的个数可以通过第二个可选的参数来设置。
  4. aggregateByKey (zeroValue:U,[partitioner: Partitioner]) (seqOp: (U, V) => U,combOp: (U, U) => U: 在kv对的RDD中,,按key将value进行分组合并,合并时,将每个value和初始值作为seq函数的参数,进行计算,返回的结果作为一个新的kv对,然后再将结果按照key进行合并,最后将每个分组的value传递给combine函数进行计算(先将前两个value进行计算,将返回结果和下一个value传给combine函数,以此类推),将key与计算结果作为一个新的kv对输出。
  5. combineByKey(createCombiner: V=>C, mergeValue: (C, V) =>C, mergeCombiners: (C, C) =>C):

对相同K,把V合并成一个集合。

    1. createCombiner: combineByKey() 会遍历分区中的所有元素,因此每个元素的键要么还没有遇到过,要么就和之前的某个元素的键相同。如果这是一个新的元素,combineByKey()会使用一个叫作createCombiner()的函数来创建那个键对应的累加器的初始值
    2. mergeValue: 如果这是一个在处理当前分区之前已经遇到的键,它会使用mergeValue()方法将该键的累加器对应的当前值与这个新的值进行合并
    3. mergeCombiners: 由于每个分区都是独立处理的, 因此对于同一个键可以有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器, 就需要使用用户提供的 mergeCombiners() 方法将各个分区的结果进行合并。

根据自身情况选择比较熟悉的算子加以介绍。

      1. 请列举Spark的action算子(不少于6个),并简述功能(重点)
  1. reduce:
  2. collect:
  3. first:
  4. take:
  5. aggregate:
  6. countByKey:
  7. foreach:
  8. saveAsTextFile:
      1. 请列举会引起Shuffle过程的Spark算子,并简述功能。

reduceBykey:

groupByKey:

…ByKey:

      1. 简述Spark的两种核心Shuffle(HashShuffle与SortShuffle)的工作流程(包括未优化的HashShuffle、优化的HashShuffle、普通的SortShuffle与bypass的SortShuffle)(重点

未经优化的HashShuffle:

 

优化后的Shuffle:

 

普通的SortShuffle:

      当 shuffle read task 的 数 量 小 于 等 于 spark.shuffle.sort。

bypassMergeThreshold 参数的值时(默认为 200),就会启用 bypass 机制。

 

      1. Spark常用算子reduceByKey与groupByKey的区别,哪一种更具优势?(重点)

reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v]。

groupByKey:按照key进行分组,直接进行shuffle。

开发指导:reduceByKey比groupByKey,建议使用。但是需要注意是否会影响业务逻辑。

      1. Repartition和Coalesce关系与区别
  1. 关系:

两者都是用来改变RDD的partition数量的,repartition底层调用的就是coalesce方法:coalesce(numPartitions, shuffle = true)

  1. 区别:

repartition一定会发生shuffle,coalesce根据传入的参数来判断是否发生shuffle

一般情况下增大rdd的partition数量使用repartition,减少partition数量时使用coalesce

      1. 分别简述Spark中的缓存机制(cache和persist)与checkpoint机制,并指出两者的区别与联系

都是做RDD持久化的

cache:内存,不会截断血缘关系,使用计算过程中的数据缓存。

checkpoint:磁盘,截断血缘关系,在ck之前必须没有任何任务提交才会生效,ck过程会额外提交一次任务。

      1. 简述Spark中共享变量(广播变量和累加器)的基本原理与用途。(重点

累加器(accumulator)是Spark中提供的一种分布式的变量机制,其原理类似于mapreduce,即分布式的改变,然后聚合这些改变。累加器的一个常见用途是在调试时对作业执行过程中的事件进行计数。而广播变量用来高效分发较大的对象。

共享变量出现的原因:

通常在向 Spark 传递函数时,比如使用 map() 函数或者用 filter() 传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。

Spark的两个共享变量,累加器与广播变量,分别为结果聚合与广播这两种常见的通信模式突破了这一限制。

      1. 当Spark涉及到数据库的操作时,如何减少Spark运行中的数据库连接数?

使用foreachPartition代替foreach,在foreachPartition内获取数据库的连接。

      1. 简述SparkSQL中RDD、DataFrame、DataSet三者的区别与联系? (笔试重点
  1. RDD

优点:

编译时类型安全 

编译时就能检查出类型错误

面向对象的编程风格 

直接通过类名点的方式来操作数据

缺点:

序列化和反序列化的性能开销 

无论是集群间的通信, 还是IO操作都需要对对象的结构和数据进行序列化和反序列化。

GC的性能开销,频繁的创建和销毁对象, 势必会增加GC

  1. DataFrame

DataFrame引入了schema和off-heap

schema : RDD每一行的数据, 结构都是一样的,这个结构就存储在schema中。 Spark通过schema就能够读懂数据, 因此在通信和IO时就只需要序列化和反序列化数据, 而结构的部分就可以省略了。

  1. DataSet

DataSet结合了RDD和DataFrame的优点,并带来的一个新的概念Encoder。

当序列化数据时,Encoder产生字节码与off-heap进行交互,能够达到按需访问数据的效果,而不用反序列化整个对象。Spark还没有提供自定义Encoder的API,但是未来会加入。

三者之间的转换:

 

      1. SparkSQL中join操作与left join操作的区别?

join和sql中的inner join操作很相似,返回结果是前面一个集合和后面一个集合中匹配成功的,过滤掉关联不上的。

leftJoin类似于SQL中的左外关联left outer join,返回结果以第一个RDD为主,关联不上的记录为空。

部分场景下可以使用left semi join替代left join:

因为 left semi join 是 in(keySet) 的关系,遇到右表重复记录,左表会跳过,性能更高,而 left join 则会一直遍历。但是left semi join 中最后 select 的结果中只许出现左表中的列名,因为右表只有 join key 参与关联计算了

      1. SparkStreaming有哪几种方式消费Kafka中的数据,它们之间的区别是什么?重点

一、基于Receiver的方式

这种方式使用Receiver来获取数据。Receiver是使用Kafka的高层次Consumer API来实现的。receiver从Kafka中获取的数据都是存储在Spark Executor的内存中的(如果突然数据暴增,大量batch堆积,很容易出现内存溢出的问题),然后Spark Streaming启动的job会去处理那些数据。

然而,在默认的配置下,这种方式可能会因为底层的失败而丢失数据。如果要启用高可靠机制,让数据零丢失,就必须启用Spark Streaming的预写日志机制(Write Ahead Log,WAL)。该机制会同步地将接收到的Kafka数据写入分布式文件系统(比如HDFS)上的预写日志中。所以,即使底层节点出现了失败,也可以使用预写日志中的数据进行恢复。

 

二、基于Direct的方式

这种新的不基于Receiver的直接方式,是在Spark 1.3中引入的,从而能够确保更加健壮的机制。替代掉使用Receiver来接收数据后,这种方式会周期性地查询Kafka,来获得每个topic+partition的最新的offset,从而定义每个batch的offset的范围。当处理数据的job启动时,就会使用Kafka的简单consumer api来获取Kafka指定offset范围的数据。

 

优点如下: 

简化并行读取:如果要读取多个partition,不需要创建多个输入DStream然后对它们进行union操作。Spark会创建跟Kafka partition一样多的RDD partition,并且会并行从Kafka中读取数据。所以在Kafka partition和RDD partition之间,有一个一对一的映射关系。

高性能:如果要保证零数据丢失,在基于receiver的方式中,需要开启WAL机制。这种方式其实效率低下,因为数据实际上被复制了两份,Kafka自己本身就有高可靠的机制,会对数据复制一份,而这里又会复制一份到WAL中。而基于direct的方式,不依赖Receiver,不需要开启WAL机制,只要Kafka中作了数据的复制,那么就可以通过Kafka的副本进行恢复。

一次且仅一次的事务机制

三、对比:

基于receiver的方式,是使用Kafka的高阶API来在ZooKeeper中保存消费过的offset的。这是消费Kafka数据的传统方式。这种方式配合着WAL机制可以保证数据零丢失的高可靠性,但是却无法保证数据被处理一次且仅一次,可能会处理两次。因为Spark和ZooKeeper之间可能是不同步的。

基于direct的方式,使用kafka的简单api,Spark Streaming自己就负责追踪消费的offset,并保存在checkpoint中。Spark自己一定是同步的,因此可以保证数据是消费一次且仅消费一次。

在实际生产环境中大都用Direct方式

      1. 简述SparkStreaming窗口函数的原理(重点

 窗口函数就是在原来定义的SparkStreaming计算批次大小的基础上再次进行封装,每次计算多个批次的数据,同时还需要传递一个滑动步长的参数,用来设置当次计算任务完成之后下一次从什么地方开始计算。

图中time1就是SparkStreaming计算批次大小,虚线框以及实线大框就是窗口的大小,必须为批次的整数倍。虚线框到大实线框的距离(相隔多少批次),就是滑动步长。

      1. 请手写出wordcount的Spark代码实现(Scala)(手写代码重点)

 

val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("WordCount")

  val sc = new SparkContext(conf)

  sc.textFile("/input")

  .flatMap(_.split(" "))

  .map((_,1))

  .reduceByKey(_+_)

  .saveAsTextFile("/output")

 sc.stop()

 

      1. 如何使用Spark实现topN的获取(描述思路或使用伪代码)(重点)

方法1:

(1)按照key对数据进行聚合(groupByKey)

(2)将value转换为数组,利用scala的sortBy或者sortWith进行排序(mapValues)数据量太大,会OOM。

方法2:

(1)取出所有的key

(2)对key进行迭代,每次取出一个key利用spark的排序算子进行排序

方法3:

(1)自定义分区器,按照key进行分区,使不同的key进到不同的分区

(2)对每个分区运用spark的排序算子进行排序

      1. 京东:调优之前与调优之后性能的详细对比(例如调整map个数,map个数之前多少、之后多少,有什么提升)

这里举个例子。比如我们有几百个文件,会有几百个map出现,读取之后进行join操作,会非常的慢。这个时候我们可以进行coalesce操作,比如240个map,我们合成60个map,也就是窄依赖。这样再shuffle,过程产生的文件数会大大减少。提高join的时间性能。

  1. Flink相关总结
      1. 简单介绍一下Flink

Flink核心是一个流式的数据流执行引擎,其针对数据流的分布式计算提供了数据分布、数据通信以及容错机制等功能。基于流执行引擎,Flink提供了诸多更高抽象层的API以便用户编写分布式任务:DataSet API, 对静态数据进行批处理操作,将静态数据抽象成分布式的数据集,用户可以方便地使用Flink提供的各种操作符对分布式数据集进行处理,支持Java、Scala和Python。DataStream API,对数据流进行流处理操作,将流式的数据抽象成分布式的数据流,用户可以方便地对分布式数据流进行各种操作,支持Java和Scala。Table API,对结构化数据进行查询操作,将结构化数据抽象成关系表,并通过类SQL的DSL对关系表进行各种查询操作,支持Java和Scala。此外,Flink还针对特定的应用领域提供了领域库,例如:Flink ML,Flink的机器学习库,提供了机器学习Pipelines API并实现了多种机器学习算法。Gelly,Flink的图计算库,提供了图计算的相关API及多种图计算算法实现。

      1. Flink相比Spark Streaming有什么区别?

架构模型上:Spark Streaming 的task运行依赖driver 和 executor和worker,当然driver和excutor还依赖于集群管理器Standalone或者yarn等。而Flink运行时主要是JobManager、TaskManage和TaskSlot。另外一个最核心的区别是:Spark Streaming 是微批处理,运行的时候需要指定批处理的时间,每次运行 job 时处理一个批次的数据;Flink 是基于事件驱动的,事件可以理解为消息。事件驱动的应用程序是一种状态应用程序,它会从一个或者多个流中注入事件,通过触发计算更新状态,或外部动作对注入的事件作出反应。

 

 

任务调度上:Spark Streaming的调度分为构建 DGA 图,划分 stage,生成 taskset,调度 task等步骤,而Flink首先会生成 StreamGraph,接着生成 JobGraph,然后将 jobGraph 提交给 Jobmanager 由它完成 jobGraph 到 ExecutionGraph 的转变,最后由 jobManager 调度执行。

 

 

时间机制上:flink 支持三种时间机制事件时间,注入时间,处理时间,同时支持 watermark 机制处理滞后数据。Spark Streaming 只支持处理时间,Structured streaming则支持了事件时间和watermark机制。

容错机制上:二者保证exactly-once的方式不同。spark streaming 通过保存offset和事务的方式;Flink 则使用两阶段提交协议来解决这个问题。

      1. Flink中的分区策略有哪几种?

Flink中默认提供了八大分区策略(也叫分区器)。八大分区策略继承关系图

 

ChannelSelector: 接口,决定将记录写入哪个Channel。有3个方法:

  1. void setup(int numberOfChannels): 初始化输出Channel的数量。
  2. int selectChannel(T record): 根据当前记录以及Channel总数,决定应将记录写入下游哪个Channel。八大分区策略的区别主要在这个方法的实现上。
  3. boolean isBroadcast(): 是否是广播模式。决定了是否将记录写入下游所有Channel。
  4. StreamPartitioner:抽象类,也是所有流分区器的基类。

 

注意:

这里以及下边提到的Channel可简单理解为下游Operator的某个实例。Flink 中改变并行度,默认RebalancePartitioner分区策略。分区策略,可在Flink WebUI上直观看出,如REBALANCE,即使用了RebalancePartitioner分区策略;SHUFFLE,即使用了ShufflePartitioner分区策略。

 

  1. GlobalPartitioner: DataStream => DataStream

GlobalPartitioner,GLOBAL分区。将记录输出到下游Operator的第一个实例。

selectChannel实现

public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {

//对每条记录,只选择下游operator的第一个Channel

return 0;

}

API使用

dataStream

    .setParallelism(2)

    // 采用GLOBAL分区策略重分区

    .global()

    .print()

    .setParallelism(1);

  1. ShufflePartitioner: DataStream => DataStream

ShufflePartitioner,SHUFFLE分区。将记录随机输出到下游Operator的每个实例。

selectChannel实现

private Random random = new Random();

@Override

public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {

    //对每条记录,随机选择下游operator的某个Channel

     return random.nextInt(numberOfChannels);

}

API使用

dataStream

    .setParallelism(2)

    // 采用SHUFFLE分区策略重分区

    .shuffle()

    .print()

    .setParallelism(4);

 

  1. RebalancePartitioner: DataStream => DataStream

RebalancePartitioner,REBALANCE分区。将记录以循环的方式输出到下游Operator的每个实例。

selectChannel实现

public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {

    //第一条记录,输出到下游的第一个Channel;第二条记录,输出到下游的第二个Channel...如此循环

     nextChannelToSendTo = (nextChannelToSendTo + 1) % numberOfChannels;

     return nextChannelToSendTo;

}

API使用

dataStream

        .setParallelism(2)

        // 采用REBALANCE分区策略重分区

        .rebalance()

        .print()

        .setParallelism(4);

 

  1. RescalePartitioner: DataStream => DataStream

RescalePartitioner,RESCALE分区。基于上下游Operator的并行度,将记录以循环的方式输出到下游Operator的每个实例。举例: 上游并行度是2,下游是4,则上游一个并行度以循环的方式将记录输出到下游的两个并行度上;上游另一个并行度以循环的方式将记录输出到下游另两个并行度上。若上游并行度是4,下游并行度是2,则上游两个并行度将记录输出到下游一个并行度上;上游另两个并行度将记录输出到下游另一个并行度上。

selectChannel实现

private int nextChannelToSendTo = -1;

@Override

public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {

     if (++nextChannelToSendTo >= numberOfChannels) {

          nextChannelToSendTo = 0;

     }

     return nextChannelToSendTo;

}

API使用

dataStream

    .setParallelism(2)

    // 采用RESCALE分区策略重分区

    .rescale()

    .print()

    .setParallelism(4);

  1. BroadcastPartitioner: DataStream => DataStream

BroadcastPartitioner,BROADCAST分区。广播分区将上游数据集输出到下游Operator的每个实例中。适合于大数据集Join小数据集的场景。

selectChannel实现

@Override

public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {

    //广播分区不支持选择Channel,因为会输出到下游每个Channel中

     throw new UnsupportedOperationException("Broadcast partitioner does not support select channels.");

}

@Override

public boolean isBroadcast() {

    //启用广播模式,此时Channel选择器会选择下游所有Channel

     return true;

}

API使用

dataStream

    .setParallelism(2)

    // 采用BROADCAST分区策略重分区

    .broadcast()

    .print()

    .setParallelism(4);

  1. ForwardPartitioner

ForwardPartitioner,FORWARD分区。将记录输出到下游本地的operator实例。ForwardPartitioner分区器要求上下游算子并行度一样。上下游Operator同属一个SubTasks。

selectChannel实现

@Override

public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {

     return 0;

}

API使用

dataStream

    .setParallelism(2)

    // 采用FORWARD分区策略重分区

    .forward()

    .print()

    .setParallelism(2);

  1. KeyGroupStreamPartitioner(HASH方式):

KeyGroupStreamPartitioner,HASH分区。将记录按Key的Hash值输出到下游Operator实例。

selectChannel实现

@Override

public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {

     K key;

     try {

          key = keySelector.getKey(record.getInstance().getValue());

     } catch (Exception e) {

          throw new RuntimeException("Could not extract key from " + record.getInstance().getValue(), e);

     }

     return KeyGroupRangeAssignment.assignKeyToParallelOperator(key, maxParallelism, numberOfChannels);

}

// KeyGroupRangeAssignment中的方法

public static int assignKeyToParallelOperator(Object key, int maxParallelism, int parallelism) {

     return computeOperatorIndexForKeyGroup(maxParallelism, parallelism, assignToKeyGroup(key, maxParallelism));

}

// KeyGroupRangeAssignment中的方法

public static int assignToKeyGroup(Object key, int maxParallelism) {

     return computeKeyGroupForKeyHash(key.hashCode(), maxParallelism);

}

// KeyGroupRangeAssignment中的方法

public static int computeKeyGroupForKeyHash(int keyHash, int maxParallelism) {

     return MathUtils.murmurHash(keyHash) % maxParallelism;

}

API使用

dataStream

    .setParallelism(2)

    // 采用HASH分区策略重分区

    .keyBy((KeySelector<Tuple3<String, Integer, String>, String>) value -> value.f0)

    .print()

    .setParallelism(4);

  1. CustomPartitionerWrapper

CustomPartitionerWrapper,CUSTOM分区。通过Partitioner实例的partition方法(自定义的)将记录输出到下游。

selectChannel实现

Partitioner<K> partitioner;

KeySelector<T, K> keySelector;

public CustomPartitionerWrapper(Partitioner<K> partitioner, KeySelector<T, K> keySelector) {

     this.partitioner = partitioner;

     this.keySelector = keySelector;

}

@Override

public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {

     K key;

     try {

          key = keySelector.getKey(record.getInstance().getValue());

     } catch (Exception e) {

          throw new RuntimeException("Could not extract key from " + record.getInstance(), e);

     }

     return partitioner.partition(key, numberOfChannels);

}

自定义分区器将指定的Key分到指定的分区

// 自定义分区器,将不同的Key(用户ID)分到指定的分区

// key: 根据key的值来分区

// numPartitions: 下游算子并行度

static class CustomPartitioner implements Partitioner<String> {

      @Override

      public int partition(String key, int numPartitions) {

          switch (key){

              case "user_1":

                  return 0;

              case "user_2":

                  return 1;

              case "user_3":

                  return 2;

              default:

                  return 3;

          }

      }

  }

使用自定义分区器

dataStream

    .setParallelism(2)

    // 采用CUSTOM分区策略重分区

    .partitionCustom(new CustomPartitioner(),0)

    .print()

    .setParallelism(4);

      1. Flink的并行度有了解吗?Flink中设置并行度需要注意什么?

Flink程序由多个任务(Source、Transformation、Sink)组成。任务被分成多个并行实例来执行,每个并行实例处理任务的输入数据的子集。任务的并行实例的数量称之为并行度。Flink中人物的并行度可以从多个不同层面设置:操作算子层面(Operator Level)、执行环境层面(Execution Environment Level)、客户端层面(Client Level)、系统层面(System Level)。Flink可以设置好几个level的parallelism,其中包括Operator Level、Execution Environment Level、Client Level、System Level在flink-conf.yaml中通过parallelism.default配置项给所有execution environments指定系统级的默认parallelism;在ExecutionEnvironment里头可以通过setParallelism来给operators、data sources、data sinks设置默认的parallelism;如果operators、data sources、data sinks自己有设置parallelism则会覆盖ExecutionEnvironment设置的parallelism。 

      1. Flink支持哪几种重启策略?分别如何配置?

重启策略种类:固定延迟重启策略(Fixed Delay Restart Strategy)故障率重启策略(Failure Rate Restart Strategy)无重启策略(No Restart Strategy)Fallback重启策略(Fallback Restart Strategy)

      1. Flink的分布式缓存有什么作用?如何使用?

Flink提供了一个分布式缓存,类似于hadoop,可以使用户在并行函数中很方便的读取本地文件,并把它放在taskmanager节点中,防止task重复拉取。

此缓存的工作机制如下:程序注册一个文件或者目录(本地或者远程文件系统,例如hdfs或者s3),通过ExecutionEnvironment注册缓存文件并为它起一个名称。

当程序执行,Flink自动将文件或者目录复制到所有taskmanager节点的本地文件系统,仅会执行一次。用户可以通过这个指定的名称查找文件或者目录,然后从taskmanager节点的本地文件系统访问它。

      1. Flink中的广播变量,使用广播变量需要注意什么事项?

在Flink中,同一个算子可能存在若干个不同的并行实例,计算过程可能不在同一个Slot中进行,不同算子之间更是如此,因此不同算子的计算数据之间不能像Java数组之间一样互相访问,而广播变量Broadcast便是解决这种情况的。我们可以把广播变量理解为是一个公共的共享变量,我们可以把一个dataset 数据集广播出去,然后不同的task在节点上都能够获取到,这个数据在每个节点上只会存在一份。

      1. Flink中对窗口的支持包括哪几种?说说他们的使用场景

 

  1. Tumbling Time Window

假如我们需要统计每一分钟中用户购买的商品的总数,需要将用户的行为事件按每一分钟进行切分,这种切分被成为翻滚时间窗口(Tumbling Time Window)。翻滚窗口能将数据流切分成不重叠的窗口,每一个事件只能属于一个窗口。

  1. Sliding Time Window

我们可以每30秒计算一次最近一分钟用户购买的商品总数。这种窗口我们称为滑动时间窗口(Sliding Time Window)。在滑窗中,一个元素可以对应多个窗口。

  1. Tumbling Count Window

当我们想要每100个用户购买行为事件统计购买总数,那么每当窗口中填满100个元素了,就会对窗口进行计算,这种窗口我们称之为翻滚计数窗口(Tumbling Count Window),上图所示窗口大小为3个。

  1. Session Window

在这种用户交互事件流中,我们首先想到的是将事件聚合到会话窗口中(一段用户持续活跃的周期),由非活跃的间隙分隔开。如上图所示,就是需要计算每个用户在活跃期间总共购买的商品数量,如果用户30秒没有活动则视为会话断开(假设raw data stream是单个用户的购买行为流)。一般而言,window 是在无限的流上定义了一个有限的元素集合。这个集合可以是基于时间的,元素个数的,时间和个数结合的,会话间隙的,或者是自定义的。Flink 的 DataStream API 提供了简洁的算子来满足常用的窗口操作,同时提供了通用的窗口机制来允许用户自己定义窗口分配逻辑。

      1. Flink 中的 State Backends是什么?有什么作用?分成哪几类?说说他们各自的优缺点?

Flink流计算中可能有各种方式来保存状态:

  1. 窗口操作
  2. 使用了KV操作的函数
  3. 继承了CheckpointedFunction的函数

当开始做checkpointing的时候,状态会被持久化到checkpoints里来规避数据丢失和状态恢复。选择的状态存储策略不同,会导致状态持久化如何和checkpoints交互。

Flink内部提供了这些状态后端:

  1. MemoryStateBackend
  2. FsStateBackend
  3. RocksDBStateBackend

如果没有其他配置,系统将使用MemoryStateBackend。

      1. Flink中的时间种类有哪些?各自介绍一下?

Flink中的时间与现实世界中的时间是不一致的,在flink中被划分为事件时间,摄入时间,处理时间三种。如果以EventTime为基准来定义时间窗口将形成EventTimeWindow,要求消息本身就应该携带EventTime如果以IngesingtTime为基准来定义时间窗口将形成IngestingTimeWindow,以source的systemTime为准。如果以ProcessingTime基准来定义时间窗口将形成ProcessingTimeWindow,以operator的systemTime为准。

      1. WaterMark是什么?是用来解决什么问题?如何生成水印?水印的原理是什么?

Watermark是Apache Flink为了处理EventTime 窗口计算提出的一种机制,本质上也是一种时间戳。watermark是用于处理乱序事件的,处理乱序事件通常用watermark机制结合window来实现。详细参考:https://www.jianshu.com/p/1c2542f11da0

      1. Flink的table和SQL熟悉吗?Table API和SQL中TableEnvironment这个类有什么作用

TableEnvironment是Table API和SQL集成的核心概念。它负责:

  1. 在内部catalog中注册表
  2. 注册外部catalog
  3. 执行SQL查询
  4. 注册用户定义(标量,表或聚合)函数
  5. 将DataStream或DataSet转换为表
  6. 持有对ExecutionEnvironment或StreamExecutionEnvironment的引用 
      1. Flink如何实现SQL解析的呢?

 

StreamSQL API的执行原理如下:

  1. 用户使用对外提供Stream SQL的语法开发业务应用;
  2. 用calcite对StreamSQL进行语法检验,语法检验通过后,转换成calcite的逻辑树节点;最终形成calcite的逻辑计划;
  3. 采用Flink自定义的优化规则和calcite火山模型、启发式模型共同对逻辑树进行优化,生成最优的Flink物理计划;
  4. 对物理计划采用janino codegen生成代码,生成用低阶API DataStream 描述的流应用,提交到Flink平台执行详细参考:https://cloud.tencent.com/developer/article/1471612

 

      1. Flink是如何做到批处理与流处理统一的?

Flink设计者认为:有限流处理是无限流处理的一种特殊情况,它只不过在某个时间点停止而已。Flink通过一个底层引擎同时支持流处理和批处理。详细参考:https://cloud.tencent.com/developer/article/1501348

      1. Flink中的数据传输模式是怎么样的?

 

大概的原理,上游的task产生数据后,会写在本地的缓存中,然后通知JM自己的数据已经好了,JM通知下游的Task去拉取数据,下游的Task然后去上游的Task拉取数据,形成链条。

但是在何时通知JM?这里有一个设置,比如pipeline还是blocking,pipeline意味着上游哪怕产生一个数据,也会去通知,blocking则需要缓存的插槽存满了才会去通知,默认是pipeline。

虽然生产数据的是Task,但是一个TaskManager中的所有Task共享一个NetworkEnvironment,下游的Task利用ResultPartitionManager主动去上游Task拉数据,底层利用的是Netty和TCP实现网络链路的传输。

那么,一直都在说Flink的背压是一种自然的方式,为什么是自然的了?

从上面的图中下面的链路中可以看到,当下游的process逻辑比较慢,无法及时处理数据时,他自己的local buffer中的消息就不能及时被消费,进而导致netty无法把数据放入local buffer,进而netty也不会去socket上读取新到达的数据,进而在tcp机制中,tcp也不会从上游的socket去读取新的数据,上游的netty也是一样的逻辑,它无法发送数据,也就不能从上游的localbuffer中消费数据,所以上游的localbuffer可能就是满的,上游的operator或者process在处理数据之后进行collect.out的时候申请不能本地缓存,导致上游的process被阻塞。这样,在这个链路上,就实现了背压。

如果还有相应的上游,则会一直反压上去,一直影响到source,导致source也放慢从外部消息源读取消息的速度。一旦瓶颈解除,网络链路畅通,则背压也会自然而然的解除。

      1. Flink的容错机制

Flink基于分布式快照与可部分重发的数据源实现了容错。用户可自定义对整个Job进行快照的时间间隔,当任务失败时,Flink会将整个Job恢复到最近一次快照,并从数据源重发快照之后的数据。

 

详细参考:https://www.jianshu.com/p/1fca8fb61f86

      1. Flink在使用Window时出现数据倾斜,你有什么解决办法?

注意:这里window产生的数据倾斜指的是不同的窗口内积攒的数据量不同,主要是由源头数据的产生速度导致的差异。核心思路:1.重新设计key 2.在窗口计算前做预聚合可以参考这个:https://blog.csdn.net/it_lee_j_h/article/details/88641894

      1. Flink任务,delay极高,请问你有什么调优策略?

首先要确定问题产生的原因,找到最耗时的点,确定性能瓶颈点。比如任务频繁反压,找到反压点。主要通过:资源调优、作业参数调优。资源调优即是对作业中的Operator的并发数(parallelism)、CPU(core)、堆内存(heap_memory)等参数进行调优。作业参数调优包括:并行度的设置,State的设置,checkpoint的设置。

  1. 业务交互数据分析
  1. 电商常识

SKU(库存量单位):一台银色、128G内存的、支持联通网络的iPhoneX

SPU(标准产品单位):iPhoneX

  1. 电商业务流程

 

  1. 业务表关键字段
      1. 订单表(order_info)

标签

含义

id

订单编号

total_amount

订单金额

order_status

订单状态

user_id

用户id

payment_way

支付方式

out_trade_no

支付流水号

create_time

创建时间

operate_time

操作时间

     
      1. 订单详情表(order_detail)

标签

含义

id

订单编号

order_id

订单号

user_id

用户id

sku_id

商品id

sku_name

商品名称

order_price

商品价格

sku_num

商品数量

create_time

创建时间

     
      1. 商品表

标签

含义

id

skuId

spu_id

spuid

price

价格

sku_name

商品名称

sku_desc

商品描述

weight

重量

tm_id

品牌id

category3_id

品类id

create_time

创建时间

     
      1. 用户表

标签

含义

id

用户id

name

姓名

birthday

生日

gender

性别

email

邮箱

user_level

用户等级

create_time

创建时间

     
      1. 商品一级分类表

标签

含义

id

id

name

名称

     
      1. 商品二级分类表

标签

含义

id

id

name

名称

category1_id

一级品类id

     
      1. 商品三级分类表

标签

含义

id

id

name

名称

Category2_id

二级品类id

     
      1. 支付流水表

标签

含义

id

编号

out_trade_no

对外业务编号

order_id

订单编号

user_id

用户编号

alipay_trade_no

支付宝交易流水编号

total_amount

支付金额

subject

交易内容

payment_type

支付类型

payment_time

支付时间

     

订单表跟订单详情表有什么区别?

    订单表的订单状态会变化,订单详情表不会,因为没有订单状态。

    订单表记录user_id,订单id订单编号,订单的总金额order_status,支付方式,订单状态等。

    订单详情表记录user_id,商品sku_id ,具体的商品信息(商品名称sku_name,价格order_price,数量sku_num)

  1. MySql中表的分类

实体表,维度表,事务型事实表,周期性事实表

其实最终可以把事务型事实表,周期性事实表统称实体表,实体表,维度表统称维度表

 

订单表(order_info)(周期型事实表)

订单详情表(order_detail)(事务型事实表)

商品表(实体表)

用户表(实体表)

商品一级分类表(维度表)

商品二级分类表(维度表)

商品三级分类表(维度表)

支付流水表(事务型实体表)

  1. 同步策略

 

实体表,维度表统称维度表,每日全量或者每月(更长时间)全量

事务型事实表:每日增量

周期性事实表:拉链表

  1. 关系型数据库范式理论

1NF:属性不可再分割(例如不能存在5台电脑的属性,坏处:表都没法用)

2NF:不能存在部分函数依赖(例如主键(学号+课名)-->成绩,姓名,但学号--》姓名,所以姓名部分依赖于主键(学号+课名),所以要去除,坏处:数据冗余)

3NF:不能存在传递函数依赖(学号--》宿舍种类--》价钱,坏处:数据冗余和增删异常)

Mysql关系模型:关系模型主要应用与OLTP系统中,为了保证数据的一致性以及避免冗余,所以大部分业务系统的表都是遵循第三范式的。

Hive 维度模型:维度模型主要应用于OLAP系统中,因为关系模型虽然冗余少,但是在大规模数据,跨表分析统计查询过程中,会造成多表关联,这会大大降低执行效率。所以HIVE把相关各种表整理成两种:事实表和维度表两种。所有维度表围绕着事实表进行解释。

  1. 数据模型

雪花模型、星型模型和星座模型

(在维度建模的基础上又分为三种模型:星型模型、雪花模型、星座模型。)

星型模型(一级维度表),雪花(多级维度),星座模型(星型模型+多个事实表)

  1. 业务数据数仓搭建

sqoop

导数据的原理是mapreduce,

import  把数据从关系型数据库 导到 数据仓库,自定义InputFormat,

export  把数据从数据仓库 导到 关系型数据库,自定义OutputFormat,

用sqoop从mysql中将八张表的数据导入数仓的ods原始数据层

全量无条件,增量按照创建时间,增量+变化按照创建时间或操作时间。

 

origin_data

sku_info商品表(每日导全量)

user_info用户表(每日导全量)

base_category1商品一级分类表(每日导全量)

base_category2商品二级分类表(每日导全量)

base_category3商品三级分类表(每日导全量)

order_detail订单详情表(每日导增量)

payment_info支付流水表(每日导增量)

order_info订单表(每日导增量+变化)

      1. ods

(八张表,表名,字段跟mysql完全相同)

从origin_data把数据导入到ods层,表名在原表名前加ods_

      1. dwd

对ODS层数据进行判空过滤。对商品分类表进行维度退化(降维)。其他数据跟ods层一模一样

订单表 dwd_order_info

订单详情表 dwd_order_detail

用户表 dwd_user_info

支付流水表 dwd_payment_info

商品表 dwd_sku_info

其他表字段不变,唯独商品表,通过关联3张分类表,增加了

    category2_id` string COMMENT '2id', 

    `category1_id` string COMMENT '3id', 

    `category3_name` string COMMENT '3', 

    `category2_name` string COMMENT '2', 

    `category1_name` string COMMENT '1', 

小结:

  1. 维度退化要付出什么代价?或者说会造成什么样的需求处理不了?

如果被退化的维度,还有其他业务表使用,退化后处理起来就麻烦些。

还有如果要删除数据,对应的维度可能也会被永久删除。

  1. 想想在实际业务中还有那些维度表可以退化

城市的三级分类(省、市、县)等

      1. dws

从订单表 dwd_order_info 中获取 下单次数 和 下单总金额

从支付流水表 dwd_payment_info 中获取 支付次数 和 支付总金额

从事件日志评论表 dwd_comment_log 中获取评论次数

最终按照user_id聚合,获得明细,跟之前的mid_id聚合不同

  1. 需求一:GMV成交总额

从用户行为宽表中dws_user_action,根据统计日期分组,聚合,直接sum就可以了。

  1. 需求二:转化率
      1. 新增用户占日活跃用户比率表

 

从日活跃数表 ads_uv_count 和 日新增设备数表 ads_new_mid_count 中取即可。

      1. 用户行为转化率表

 

从用户行为宽表dws_user_action中取,下单人数(只要下单次数>0),支付人数(只要支付次数>0)

从日活跃数表 ads_uv_count 中取活跃人数,然后对应的相除就可以了。

  1. 需求三:品牌复购率

需求:以月为单位统计,购买2次以上商品的用户

      1. 用户购买商品明细表(宽表)

 

      1. 品牌复购率表

 

从用户购买商品明细宽表dws_sale_detail_daycount中,根据品牌id--sku_tm_id聚合,计算每个品牌购买的总次数,购买人数a=购买次数>=1,两次及以上购买人数b=购买次数>=2,三次及以上购买人数c=购买次数>=3,

单次复购率=b/a,多次复购率=c/a

  1. 项目中有多少张宽表

宽表要3-5张,用户行为宽表,用户购买商品明细行为宽表,商品宽表,购物车宽表,物流宽表、登录注册、售后等。

  1. 为什么要建宽表

需求目标,把每个用户单日的行为聚合起来组成一张多列宽表,以便之后关联用户维度信息后进行,不同角度的统计分析。

  1. 拉链表

 

订单表拉链表 dwd_order_info_his

    `id` string COMMENT '订单编号',

    `total_amount` decimal(10,2) COMMENT '订单金额',

    `order_status` string COMMENT '订单状态',

    `user_id` string COMMENT '用户id' ,

    `payment_way` string COMMENT '支付方式', 

    `out_trade_no` string COMMENT '支付流水号', 

    `create_time` string COMMENT '创建时间', 

    `operate_time` string COMMENT '操作时间' ,

    `start_date`  string COMMENT '有效开始日期',

    `end_date`  string COMMENT '有效结束日期'

1)创建订单表拉链表,字段跟拉链表一样,只增加了有效开始日期和有效结束日期

初始日期,从订单变化表ods_order_info导入数据,且让有效开始时间=当前日期,有效结束日期=9999-99-99

(从mysql导入数仓的时候就只导了新增的和变化的数据ods_order_info,dwd_order_info跟ods_order_info基本一样,只多了一个id的判空处理)

2)建一张拉链临时表dwd_order_info_his_tmp,字段跟拉链表完全一致

3)新的拉链表中应该有这几部分数据,

(1)增加订单变化表dwd_order_info的全部数据

(2)更新旧的拉链表左关联订单变化表dwd_order_info,关联字段:订单id, where 过滤出end_date只等于9999-99-99的数据,如果旧的拉链表中的end_date不等于9999-99-99,说明已经是终态了,不需要再更新

如果dwd_order_info.id is null , 没关联上,说明数据状态没变,让end_date还等于旧的end_date

如果dwd_order_info.id is not null , 关联上了,说明数据状态变了,让end_date等于当前日期-1

把查询结果插入到拉链临时表中

4)把拉链临时表覆盖到旧的拉链表中

  1. 项目中遇到过哪些问题
    1. Hadoop宕机
  1. 如果MR造成系统宕机。此时要控制Yarn同时运行的任务数,和每个任务申请的最大内存。调整参数:yarn.scheduler.maximum-allocation-mb(单个任务可申请的最多物理内存量,默认是8192MB)
  2. 如果写入文件过量造成NameNode宕机。那么调高Kafka的存储大小,控制从Kafka到HDFS的写入速度。高峰期的时候用Kafka进行缓存,高峰期过去数据同步会自动跟上。
    1. Ganglia监控

Ganglia监控Flume发现尝试提交的次数大于最终成功的次数

  1. 增加Flume内存
  2. 增加Flume台数
    1. Flume小文件

Flume上传文件到HDFS时参数大量小文件?

调整hdfs.rollInterval、hdfs.rollSize、hdfs.rollCount这三个参数的值。

    1. Kafka挂掉
  1. Flume记录
  2. 日志有记录
  3. 短期没事
    1. Kafka消息数据积压,Kafka消费能力不足怎么处理?
  1. 如果是Kafka消费能力不足,则可以考虑增加Topic的分区数,并且同时提升消费组的消费者数量,消费者数=分区数。(两者缺一不可)
  2. 如果是下游的数据处理不及时:提高每批次拉取的数量。批次拉取数据过少(拉取数据/处理时间<生产速度),使处理的数据小于生产的数据,也会造成数据积压。
    1. Kafka数据重复

在下一级消费者中去重。(redis、SparkStreaming)

    1. Mysql高可用

Hive的metadata存储在MySql中(配置MySql的高可用(主从复制和读写分离和故障转移))

    1. 自定义UDF和UDTF解析和调试复杂字段

自定义UDF(extends UDF 实现evaluate方法) 解析公共字段

自定义UDTF(extends Genertic UDTF->实现三个方法init(指定返回值的名称和类型)、process(处理字段一进多出)、close方法) -> 更加灵活以及方便定义bug

    1. Sqoop数据导出Parquet

Ads层数据用Sqoop往MySql中导入数据的时候,如果用了orc(Parquet)不能导入,需转化成text格式

    1. Sqoop数据导出控制

Sqoop中导入导出Null存储一致性问题:   

Hive中的Null在底层是以“\N”来存储,而MySQL中的Null在底层就是Null,为了保证数据两端的一致性。在导出数据时采用--input-null-string和--input-null-non-string两个参数。导入数据时采用--null-string和--null-non-string。

    1. Sqoop数据导出一致性问题

当Sqoop导出数据到MySql时,使用4个map怎么保证数据的一致性

因为在导出数据的过程中map任务可能会失败,可以使用—staging-table  –clear-staging

sqoop export --connect jdbc:mysql://192.168.137.10:3306/user_behavior --username root --password 123456 --table app_cource_study_report --columns watch_video_cnt,complete_video_cnt,dt --fields-terminated-by "\t" --export-dir "/user/hive/warehouse/tmp.db/app_cource_study_analysis_${day}" --staging-table app_cource_study_report_tmp --clear-staging-table --input-null-string '\N'

任务执行成功首先在tmp临时表中,然后将tmp表中的数据复制到目标表中(这个时候可以使用事务,保证事务的一致性)

    1. SparkStreaming优雅关闭

如何优雅的关闭SparkStreaming任务(将写好的代码打包,Spark-Submit)

Kill -9 xxx ?

开启另外一个线程每5秒监听HDFS上一个文件是否存在。如果检测到存在,调用ssc.stop()方法关闭SparkStreaming任务(当你要关闭任务时,可以创建你自定义监控的文件目录)

    1. Spark OOM、数据倾斜解决

   

  1. 项目经验
  1. 框架经验
      1. Hadoop
  1. Hadoop集群基准测试(HDFS的读写性能、MapReduce的计算能力测试)
  2. 一台服务器一般都有很多个硬盘插槽(插了几个插槽)

如果不配置datanode.data.dir多目录,每次插入一块新的硬盘都需要重启服务器

配置了即插即用

  1. Hdfs参数调优

Namenode有一个工作线程池,用来处理与datanode的心跳(报告自身的健康状况和文件恢复请求)和元数据请求    dfs.namenode.handler.count=20 * log2(Cluster Size)

4)编辑日志存储路径dfs.namenode.edits.dir设置与镜像文件存储路径    dfs.namenode.name.dir尽量分开,达到最低写入延迟(提高写入的吞吐量)

5)YARN参数调优yarn-site.xml

(1)服务器节点上YARN可使用的物理内存总量,默认是8192(MB)

(2)单个任务可申请的最多物理内存量,默认是8192(MB)。

6)HDFS和硬盘空闲控制在70%以下。

      1. Flume
  1. Flume内存配置为4G(flume-env.sh修改)
  2. FileChannel优化

通过配置dataDirs指向多个路径,每个路径对应不同的硬盘,增大Flume吞吐量。

checkpointDir和backupCheckpointDir也尽量配置在不同硬盘对应的目录中,保证checkpoint坏掉后,可以快速使用backupCheckpointDir恢复数据

  1. Sink:HDFS Sink小文件处理

这三个参数配置写入HDFS后会产生小文件,hdfs.rollInterval、hdfs.rollSize、hdfs.rollCount

      1. Kafka
  1. Kafka的吞吐量测试(测试生产速度和消费速度)
  2. Kafka内存为6G(不能超过6G)
  3. Kafka数量确定:2 * 峰值生产速度(m/s)* 副本数 / 100  + 1 = ?
  4. Kafka中的数据量计算

每天数据总量100g(1亿条)   10000万/24/60/60 = 1150条/s

平均每秒钟:1150条

低谷每秒:400条

高峰每秒钟:1150 * 10 = 11000 条

每条日志大小: 1K左右

每秒多少数据量:20MB

  1. Kafka消息数据积压,Kafka消费能力不足怎么处理?
    1. 如果是Kafka消费能力不足,则可以考虑增加Topic的分区数,并且同时提升消费组的消费者数量,消费者数=分区数。(两者缺一不可)
    2. 如果是下游的数据处理不及时:提高每批次拉取的数量。批次拉取数据过少(拉取数据/处理时间<生产速度),使处理的数据小于生产的数据,也会造成数据积压。
      1. Tez引擎优点(略过)?

Tez可以将多个有依赖的作业转换为一个作业,这样只需写一次HDFS,且中间节点较少,从而大大提升作业的计算性能。

      1. Sqoop参数
  1. Sqoop导入导出Null存储一致性问题
  2. Sqoop数据导出一致性问题

–staging-table方式 --clear-staging

  1. Sqoop数据导出的时候一次执行多长时间

Sqoop任务5分钟-2个小时的都有。取决于数据量。

      1. Azkaban每天执行多少个任务
  1. 每天集群运行多少job?
  2. 每个任务的资源是如何分配的?
  3. 多个指标(200)*6=1200(1000-2000个job)
  4. 每天集群运行多少个task? 1000*(5-8)=5000多个
  5. 任务挂了怎么办?运行成功或者失败都会发邮件

Zip a.job b.job c.job   job.zip  把压缩的zip包放到azkaban的web界面上提交(指定sechduler)

  1. 业务经验
      1. ODS层采用什么压缩方式和存储格式?

压缩采用Snappy,存储采用orc,压缩比是100g数据压缩完10g左右。

      1. DWD层做了哪些事?
  1. 数据清洗
    1. 空值去除
    2. 过滤核心字段无意义的数据,比如订单表中订单id为null,支付表中支付id为空
    3. 对手机号、身份证号等敏感数据脱敏
    4. 对业务数据传过来的表进行维度退化和降维。
    5. 将用户行为宽表和业务表进行数据一致性处理

select case when a is null then b else a end as JZR,

    ...

from A

  1. 清洗的手段

Sql、mr、rdd、kettle、Python(项目中采用sql进行清除)

  1. 清洗掉多少数据算合理

1万条数据清洗掉1条。

      1. DWS层做了哪些事?
  1. DWS层有3-5张宽表(处理100-200个指标   70%以上的需求)

具体宽表名称:用户行为宽表,用户购买商品明细行为宽表,商品宽表,购物车宽表,物流宽表、登录注册、售后等。

  1. 哪个宽表最宽?大概有多少个字段?

最宽的是用户行为宽表。大概有60-100个字段

  1. 具体用户行为宽表字段名称

评论、打赏、收藏、关注--商品、关注--人、点赞、分享、好价爆料、文章发布、活跃、签到、补签卡、幸运屋、礼品、金币、电商点击、gmv

CREATE TABLE `app_usr_interact`(

  `stat_dt` date COMMENT '互动日期',

  `user_id` string COMMENT '用户id',

  `nickname` string COMMENT '用户昵称',

  `register_date` string COMMENT '注册日期',

  `register_from` string COMMENT '注册来源',

  `remark` string COMMENT '细分渠道',

  `province` string COMMENT '注册省份',

  `pl_cnt` bigint COMMENT '评论次数',

  `ds_cnt` bigint COMMENT '打赏次数',

  `sc_add` bigint COMMENT '添加收藏',

  `sc_cancel` bigint COMMENT '取消收藏',

  `gzg_add` bigint COMMENT '关注商品',

  `gzg_cancel` bigint COMMENT '取消关注商品',

  `gzp_add` bigint COMMENT '关注人',

  `gzp_cancel` bigint COMMENT '取消关注人',

  `buzhi_cnt` bigint COMMENT '点不值次数',

  `zhi_cnt` bigint COMMENT '点值次数',

  `zan_cnt` bigint COMMENT '点赞次数',

  `share_cnts` bigint COMMENT '分享次数',

  `bl_cnt` bigint COMMENT '爆料数',

  `fb_cnt` bigint COMMENT '好价发布数',

  `online_cnt` bigint COMMENT '活跃次数',

  `checkin_cnt` bigint COMMENT '签到次数',

  `fix_checkin` bigint COMMENT '补签次数',

  `house_point` bigint COMMENT '幸运屋金币抽奖次数',

  `house_gold` bigint COMMENT '幸运屋积分抽奖次数',

  `pack_cnt` bigint COMMENT '礼品兑换次数',

  `gold_add` bigint COMMENT '获取金币',

  `gold_cancel` bigint COMMENT '支出金币',

  `surplus_gold` bigint COMMENT '剩余金币',

  `event` bigint COMMENT '电商点击次数',

  `gmv_amount` bigint COMMENT 'gmv',

  `gmv_sales` bigint COMMENT '订单数')

PARTITIONED BY (  `dt` string)

  1. 商品详情  -----  购物车  ----- 订单  ------ 付款的转换比率

       5%          30%        70%

  1. 每天的GMV是多少,哪个商品卖的最好?每天下单量多少?
    1. 100万的日活每天大概有10万人购买,平均每人消费100元,一天的GMV在1000万
    2. 面膜,每天销售5000个
    3. 每天下单量在10万左右
      1. 分析过哪些指标(一分钟至少说出30个指标)
  1. 离线指标

网站流量指标  独立访问数UV  页面访客数PV

流量质量指标类  跳出率  平均页面访问时长 人均页面访问数

  1. 购物车类指标 

加入购物车次数  加入购物车买家次数  加入购物车商品数  购物车支付转化率

  1. 下单类指标 

下单笔数  下单金额  下单买家数  浏览下单转化率

  1. 支付类指标 

支付金额  支付买家数 支付商品数  浏览-支付买家转化率

下单-支付金额转化率  下单-支付买家数转换率

  1. 交易类指标 

交易成功订单数  交易成功金额  交易成功买家数  交易成功商品数

交易失败订单数  交易失败订单金额  交易失败买家数 

交易失败商品数  退款总订单量  退款金额  退款率

  1. 市场营销活动指标  

新增访问人数   新增注册人数  广告投资回报率  UV订单转化率

  1. 风控类指标 

买家评价数  买家上传图片数  买家评价率  买家好评率  买家差评率

物流平均配送时间

  1. 投诉类指标 

发起投诉数  投诉率 撤销投诉(申诉数)

  1. 商品类指标 

产品总数  SKU数  SPU数 

上架商品SKU数  上架商品SPU数  上架商品数 

 

日活跃用户,

月活跃用户,

各区域Top10商品统计,

季度商品品类点击率top10,

用户留存,

月APP的用户增长人数,

广告区域点击数top3,

活跃用户每天在线时长,

投诉人数占比,

沉默用户占比,

用户的新鲜度,

商品上架的sku数,

同种品类的交易额排名,

统计买家的评价率,

用户浏览时长,

统计下单的数量,

统计支付的数量,

统计退货的数量,

用户的(日活、月活、周活),

统计流失人数

 

日活,周活,月活,沉默用户占比,增长人数,活跃用户占比,在线时长统计,歌曲访问数,歌曲访问时长,各地区Top10歌曲统计 ,投诉人数占比,投诉回应时长,留存率,月留存率,转化率,GMV,复购vip率,vip人数,歌榜,挽回率,粉丝榜,打赏次数,打赏金额,发布歌曲榜单,歌曲热度榜单,歌手榜单,用户年龄组,vip年龄组占比,收藏数榜单,评论数

  1. 用户活跃数统计(日活,月活,周活)
  2. 某段时间的新增用户/活跃用户数
  3. 页面单跳转化率统计
  4. 活跃人数占比(占总用户比例)
  5. 在线时长统计(活跃用户每天在线时长)
  6. 统计本月的人均在线时长
  7. 订单产生效率(下单的次数与访问次数比)
  8. 页面访问时长(单个页面访问时长)   
  9. 统计本季度付款订单
  10. 统计某广告的区城点击数top3
  11. 统计本月用户的流失人数
  12. 统计本月流失人数占用户人数的比例
  13. 统计本月APP的用户增长人数
  14. 统计本月的沉默用户
  15. 统计某时段的登录人数
  16. 统计本日用户登录的次数平均值
  17. 统计用户在某类型商品中的浏览深度(页面转跳率)
  18. 统计用户从下单开始到交易成功的平均时长
  19. Top10热门商品的统计
  20. 统计下单的数量
  21. 统计支付的数量
  22. 统计退货的数量
  23. 统计动销率(有销量的商品/在线销售的宝贝)
  24. 统计支付转化率
  25. 统计用户的消费频率
  26. 统计商品上架的SKU数
  27. 统计同种品类的交易额排名
  28. 统计按下单退款排序的top10的商品
  29. 统计本APP的投诉人数占用户人数的比例
  30. 用户收藏商品
      1. 分析过最难的两个指标,现场手写

最近连续3周活跃用户数:

 

 

最近7天连续3天活跃用户数:

 

      1. 数据仓库每天跑多少张表,大概什么时候运行,运行多久?

基本一个项目建一个库,表格个数为初始的原始数据表格加上统计结果表格的总数。(一般70-100张表格)

每天0:30开始运行。

所有离线数据报表控制在8小时之内

大数据实时处理部分控制在5分钟之内。

      1. 数仓中使用的哪种文件存储格式

常用的包括:textFile,rcFile,ORC,Parquet,一般企业里使用ORC或者Parquet,因为是列式存储,且压缩比非常高,所以相比于textFile,查询速度快,占用硬盘空间少

      1. 数仓中用到过哪些Shell脚本及具体功能
  1. 集群启动停止脚本(Hadoop、Flume、Kafka、Zookeeper)
  2. Sqoop和数仓之间的导入导出脚本
  3. 数仓层级之间的数据导入脚本。
      1. 项目中用过的报表工具

Echarts、kibana、supesrset

      1. 测试相关
  1. 公司有多少台测试服务器?

测试服务器一般三台

  1. 测试数据哪来的?

一部分自己写Java程序自己造,一部分从生产环境上取一部分。

  1. 如何保证写的sql正确性

需要造一些特定的测试数据,测试。

离线数据和实时数据分析的结果比较。

  1. 测试环境什么样?

测试环境的配置是生产的一半

  1. 测试之后如何上线?

上线的时候,将脚本打包,提交git。先发邮件抄送经理和总监,运维。通过之后跟运维一起上线。

      1. 项目实际工作流程
  1. 先与产品讨论,看报表的各个数据从哪些埋点中取
  2. 将业务逻辑过程设计好,与产品确定后开始开发
  3. 开发出报表SQL脚本,并且跑几天的历史数据,观察结果
  4. 将报表放入调度任务中,第二天给产品看结果。
  5. 周期性将表结果导出或是导入后台数据库,生成可视化报表
      1. 项目中实现一个需求大概多长时间

刚入职第一个需求大概需要7天左右。

对业务熟悉后,平均一天一个需求。

影响时间的因素:开会讨论需求、表的权限申请、测试等

      1. 项目在3年内迭代次数,每一个项目具体是如何迭代的。

差不多一个月会迭代一次。就产品或我们提出优化需求,然后评估时间。每周我们都会开会做下周计划和本周总结。

有时候也会去预研一些新技术。

      1. 项目开发中每天做什么事

新需求比如埋点或是报表来了之后,需要设计做的方案,设计完成之后跟产品讨论,再开发。

数仓的任何步骤出现问题,需要查看问题,比如日活,月活下降等。

  1. JavaSE(答案精简)
  1. hashMap底层源码,数据结构

hashMap的底层结构在jdk1.7中由数组+链表实现,在jdk1.8中由数组+链表+红黑树实现,以数组+链表的结构为例。

 

 

 

 

 

JDK1.8之前Put方法:

 

 

 

 

 

 

 

 

 

 

 

 

JDK1.8之后Put方法:

 

 

  1. Java自带有哪几种线程池?
  1. newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这种类型的线程池特点是:

工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。

如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。

在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。

  1. newFixedThreadPool

创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

  1. newSingleThreadExecutor

创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

  1. newScheduleThreadPool

创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。延迟3秒执行。

  1. HashMap和HashTable区别
  1. 线程安全性不同

HashMap是线程不安全的,HashTable是线程安全的,其中的方法是Synchronize的,在多线程并发的情况下,可以直接使用HashTabl,但是使用HashMap时必须自己增加同步处理。

  1. 是否提供contains方法

HashMap只有containsValue和containsKey方法;HashTable有contains、containsKey和containsValue三个方法,其中contains和containsValue方法功能相同。

  1. key和value是否允许null值

Hashtable中,key和value都不允许出现null值。HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。

  1. 数组初始化和扩容机制

 HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。

 Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

  1. TreeSet和HashSet区别

HashSet是采用hash表来实现的。其中的元素没有按顺序排列,add()、remove()以及contains()等方法都是复杂度为O(1)的方法。

TreeSet是采用树结构实现(红黑树算法)。元素是按顺序进行排列,但是add()、remove()以及contains()等方法都是复杂度为O(log (n))的方法。它还提供了一些方法来处理排序的set,如first(), last(), headSet(), tailSet()等等。

  1. String buffer和String build区别
  1. StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,
  2. 只是StringBuffer 中的方法大都采用了 synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是线程不安全的。 
  3. 在单线程程序下,StringBuilder效率更快,因为它不需要加锁,不具备多线程安全而StringBuffer则每次都需要判断锁,效率相对更低
  1. Final、Finally、Finalize

final:修饰符(关键字)有三种用法:修饰类、变量和方法。修饰类时,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。修饰变量时,该变量使用中不被改变,必须在声明时给定初值,在引用中只能读取不可修改,即为常量。修饰方法时,也同样只能使用,不能在子类中被重写。

finally:通常放在try…catch的后面构造最终执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。

finalize:Object类中定义的方法,Java中允许使用finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize() 方法可以整理系统资源或者执行其他清理工作。

  1. ==和Equals区别

== : 如果比较的是基本数据类型,那么比较的是变量的值

如果比较的是引用数据类型,那么比较的是地址值(两个对象是否指向同一块内存)

equals:如果没重写equals方法比较的是两个对象的地址值。

如果重写了equals方法后我们往往比较的是对象中的属性的内容

 

equals方法是从Object类中继承的,默认的实现就是使用==

 

  1. Redis(答案精简)
  1. 缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级

缓存雪崩

缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。

缓存正常从Redis中获取,示意图如下:

 

缓存失效瞬间示意图如下:

 

缓存失效时的雪崩效应对底层系统的冲击非常可怕!大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

以下简单介绍两种实现方式的伪代码:

(1)碰到这种情况,一般并发量不是特别多的时候,使用最多的解决方案是加锁排队,伪代码如下:

 

加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!

注意:加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!

(2)还有一个解决办法解决方案是:给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存,实例伪代码如下:

 

解释说明:

1、缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;

2、缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。 这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。

关于缓存崩溃的解决方法,这里提出了三种方案:使用锁或队列、设置过期标志更新缓存、为key设置不同的缓存失效时间,还有一各被称为“二级缓存”的解决方法,有兴趣的读者可以自行研究。

 

缓存穿透

缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。

有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴!

 

把空结果,也给缓存起来,这样下次同样的请求就可以直接返回空了,即可以避免当查询的值为空时引起的缓存穿透。同时也可以单独设置个缓存区域存储空值,对要查询的key进行预先校验,然后再放行给后面的正常缓存处理逻辑。

 

缓存预热

缓存预热这个应该是一个比较常见的概念,相信很多小伙伴都应该可以很容易的理解,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

解决思路:

  1. 直接写个缓存刷新页面,上线时手工操作下;
  2. 数据量不大,可以在项目启动的时候自动进行加载;
  3. 定时刷新缓存;

 

缓存更新

除了缓存服务器自带的缓存失效策略之外(Redis默认的有6种策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:

  1. 定时去清理过期的缓存;
  2. 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

 

缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

  1. 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
  2. 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
  3. 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
  4. 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

 

  1. 哨兵模式

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。

哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

 

这里的哨兵有两个作用

  1. 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  2. 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

用文字描述一下故障切换(failover)的过程。假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。

  1. 数据类型

string

字符串

list

可以重复的集合

set

不可以重复的集合

hash

类似于Map<String,String>

zset(sorted set)

带分数的set

  1. 持久化
  1. RDB持久化:
    1. 在指定的时间间隔内持久化
    2. 服务shutdown会自动持久化
    3. 输入bgsave也会持久化
  2. AOF :  以日志形式记录每个更新操作

Redis重新启动时读取这个文件,重新执行新建、修改数据的命令恢复数据。

保存策略:

推荐(并且也是默认)的措施为每秒持久化一次,这种策略可以兼顾速度和安全性。

缺点:

    1. 比起RDB占用更多的磁盘空间
    2. 恢复备份速度要慢
    3. 每次读写都同步的话,有一定的性能压力
    4. 存在个别Bug,造成恢复不能

选择策略:

官方推荐:

如果对数据不敏感,可以选单独用RDB;不建议单独用AOF,因为可能出现Bug;如果只是做纯内存缓存,可以都不用

  1. 悲观锁

所谓悲观锁:具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

简单来说:执行操作前假设当前的操作肯定(或有很大几率)会被打断(悲观)。基于这个假设,我们在做操作前就会把相关资源锁定,不允许自己执行期间有其他操作干扰。

悲观锁的并发性能差,但是能保证不会发生脏数据的可能性小一点。

  1. 乐观锁

执行操作前假设当前操作不会被打断(乐观)。基于这个假设,我们在做操作前不会锁定资源,万一发生了其他操作的干扰,那么本次操作将被放弃。Redis使用的就是乐观锁。

  1. redis是单线程的,为什么那么快
  1. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
  2. 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的
  3. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
  4. 使用多路I/O复用模型,非阻塞IO
  5. 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
  1. MySql
  1. MyISAM与InnoDB的区别

对比项

MyISAM

InnoDB

外键

不支持

支持

事务

不支持

支持

行表锁

表锁,即使操作一条记录也会锁住整个表,不适合高并发的操作

行锁,操作时只锁某一行,不对其它行有影响,

适合高并发的操作

缓存

只缓存索引,不缓存真实数据

不仅缓存索引还要缓存真实数据,对内存要求较高,而且内存大小对性能有决定性的影响

 

  1. 索引

数据结构:B+Tree

一般来说能够达到range就可以算是优化了

口诀(两个法则加6种索引失效的情况)

全值匹配我最爱,最左前缀要遵守;

带头大哥不能死,中间兄弟不能断;

索引列上少计算,范围之后全失效;

LIKE百分写最右,覆盖索引不写*;

不等空值还有OR,索引影响要注意;

VAR引号不可丢,SQL优化有诀窍。

  1. b-tree和b+tree的区别
  1. B-树的关键字和记录是放在一起的,叶子节点可以看作外部节点,不包含任何信息;B+树的非叶子节点中只有关键字和指向下一个节点的索引,记录只放在叶子节点中。
  2. 在B-树中,越靠近根节点的记录查找时间越快,只要找到关键字即可确定记录的存在;而B+树中每个记录的查找时间基本是一样的,都需要从根节点走到叶子节点,而且在叶子节点中还要再比较关键字。
  1. MySQL的事务

一、事务的基本要素(ACID)

  1. 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位
  2. 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
  3. 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
  4. 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

 

二、事务的并发问题

  1. 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
  2. 不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致
  3. 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

 

三、MySQL事务隔离级别

事务隔离级别                       脏读        不可重复读      幻读

读未提交(read-uncommitted)       是          是              是

不可重复读(read-committed)       否          是              是

可重复读(repeatable-read)        否          否              是

串行化(serializable)             否          否              否

  1. 常见面试sql
  1. 用一条SQL语句查询出每门课都大于80分的学生姓名

name   kecheng   fenshu
    张三    语文    81
    张三    数学    75
    李四    语文    76
    李四    数学     90
    王五    语文    81
    王五    数学    100
    王五    英语    90
    A: select distinct name from table where name not in (select distinct name from table where fenshu<=80)
    B:select name from table group by name having min(fenshu)>80

 

  1. 学生表

自动编号   学号  姓名 课程编号 课程名称 分数
    1     2005001 张三   0001   数学   69
    2     2005002 李四   0001   数学   89
    3     2005001 张三   0001   数学   69
    删除除了自动编号不同, 其他都相同的学生冗余信息

    A: delete tablename where 自动编号 not in(select min(自动编号) from tablename group by学号, 姓名, 课程编号, 课程名称, 分数)

 

  1. 一个叫team的表,里面只有一个字段name,一共有4条纪录,分别是a,b,c,d,对应四个球队,现在四个球队进行比赛,用一条sql语句显示所有可能的比赛组合

select a.name, b.name
    from team a, team b 
    where a.name < b.name

  1. 面试题:怎么把这样一个

year   month amount
    1991   1     1.1
    1991   2     1.2
    1991   3     1.3
    1991   4     1.4
    1992   1     2.1
    1992   2     2.2
    1992   3     2.3
    1992   4     2.4
    查成这样一个结果
    year m1  m2  m3   m4
    1991 1.1 1.2 1.3 1.4
    1992 2.1 2.2 2.3 2.4 

    答案
    select year, 
    (select amount from aaa m where month=1 and m.year=aaa.year) as m1,
    (select amount from aaa m where month=2 and m.year=aaa.year) as m2,
    (select amount from aaa m where month=3 and m.year=aaa.year) as m3,
    (select amount from  aaa m where month=4 and m.year=aaa.year) as m4
    from aaa group by year

  1. 说明:复制表(只复制结构,源表名:a新表名:b)

SQL: select * into b from a where 1<>1 (where1=1,拷贝表结构和数据内容)
    ORACLE:create table b

As

Select * from a where 1=2

 

[<>(不等于)(SQL Server Compact)

比较两个表达式。 当使用此运算符比较非空表达式时,如果左操作数不等于右操作数,则结果为 TRUE。 否则,结果为 FALSE。]

  1. 原表:
    courseid coursename score
    -------------------------------------
    1 java 70
    2 oracle 90
    3 xml 40
    4 jsp 30
    5 servlet 80
    -------------------------------------
    为了便于阅读,查询此表后的结果显式如下(及格分数为60):
    courseid coursename score mark
    ---------------------------------------------------
    1 java 70 pass
    2 oracle 90 pass
    3 xml 40 fail
    4 jsp 30 fail
    5 servlet 80 pass
    ---------------------------------------------------
    写出此查询语句
    select courseid, coursename ,score ,if(score>=60, "pass","fail")  as mark from course
  2. 表名:购物信息

购物人      商品名称     数量

A            甲          2

B            乙          4

C            丙          1

A            丁          2

B            丙          5

……

 

给出所有购入商品为两种或两种以上的购物人记录

 

答:select * from 购物信息 where 购物人 in (select 购物人 from 购物信息 group by 购物人 having count(*) >= 2);

  1. info 表

date          result

2005-05-09    win

2005-05-09    lose

2005-05-09    lose

2005-05-09    lose

2005-05-10    win

2005-05-10    lose

2005-05-10    lose

如果要生成下列结果, 该如何写sql语句?

win           lose

2005-05-09  2   2

2005-05-10  1   2

答案:

(1) select date, sum(case when result = "win" then 1 else 0 end) as "win", sum(case when result = "lose" then 1 else 0 end) as "lose" from info group by date;

(2) select a.date, a.result as win, b.result as lose

  from

  (select date, count(result) as result from info where result = "win" group by date) as a

  join

  (select date, count(result) as result from info where result = "lose" group by date) as b

  on a.date = b.date;

 

  1. JVM
  1. JVM内存分哪几个区,每个区的作用是什么?

 

java虚拟机主要分为以下几个区:

  1. 方法区:
    1. 有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类型的卸载
    2. 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。
    3. 该区域是被线程共享的。
    4. 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。
  2. 虚拟机栈:
    1.  虚拟机栈也就是我们平常所称的栈内存,它为java方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。
    2. 虚拟机栈是线程私有的,它的生命周期与线程相同。
    3. 局部变量表里存储的是基本数据类型、returnAddress类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定
    4. 操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式
    5. 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。
  3. 本地方法栈:

本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务。

  1. 堆:

java堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。

  1. 程序计数器:

内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个java虚拟机规范没有规定任何OOM情况的区域。

 

  1. Java类加载过程?

Java类加载需要经历一下几个过程:

  1. 加载

加载时类加载的第一个过程,在这个阶段,将完成一下三件事情:

    1. 通过一个类的全限定名获取该类的二进制流。
    2. 将该二进制流中的静态存储结构转化为方法去运行时数据结构。 
    3. 在内存中生成该类的Class对象,作为该类的数据访问入口。
  1. 验证

验证的目的是为了确保Class文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成以下四钟验证:

    1. 文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.
    2. 元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。
    3. 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
    4. 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。
    5. 准备

准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

  1. 解析

该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。

  1. 初始化

初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

 

  1. java中垃圾收集的方法有哪些?
  1. 引用计数法   应用于:微软的COM/ActionScrip3/Python等
    1. 如果对象没有被引用,就会被回收,缺点:需要维护一个引用计算器
  2. 复制算法  年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)
    1. 效率高,缺点:需要内存容量大,比较耗内存
    2. 使用在占空间比较小、刷新次数多的新生区
  3. 标记清除  老年代一般是由标记清除或者是标记清除与标记整理的混合实现
    1. 效率比较低,会差生碎片。
  4. 标记压缩  老年代一般是由标记清除或者是标记清除与标记整理的混合实现
    1. 效率低速度慢,需要移动对象,但不会产生碎片。
  5. 标记清除压缩 标记清除-标记压缩的集合,多次GC后才Compact
    1. 使用于占空间大刷新次数少的养老区,是3 4的集合体

 

  1. 如何判断一个对象是否存活?(或者GC对象的判定方法)

判断一个对象是否存活有两种方法:

  1. 引用计数法
  2. 可达性算法(引用链法)
  3. 什么是类加载器,类加载器有哪些?

实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。

主要有一下四种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
  2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  3. 系统类加载器(system class loader)也叫应用类加载器:它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
  4. 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
  1. 简述Java内存分配与回收策略以及Minor GC和Major GC(full GC)

内存分配:

  1. 栈区:栈分为java虚拟机栈和本地方法栈
  2. 堆区:堆被所有线程共享区域,在虚拟机启动时创建,唯一目的存放对象实例。堆区是gc的主要区域,通常情况下分为两个区块年轻代和年老代。更细一点年轻代又分为Eden区,主要放新创建对象,From survivor 和 To survivor 保存gc后幸存下的对象,默认情况下各自占比 8:1:1。
  3. 方法区:被所有线程共享区域,用于存放已被虚拟机加载的类信息,常量,静态变量等数据。被Java虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代(permanment generation)
  4. 程序计数器:当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。线程私有的。

 

回收策略以及Minor GC和Major GC:

  1. 对象优先在堆的Eden区分配。
  2. 大对象直接进入老年代。
  3. 长期存活的对象将直接进入老年代。

当Eden区没有足够的空间进行分配时,虚拟机会执行一次Minor GC.Minor GC通常发生在新生代的Eden区,在这个区的对象生存期短,往往发生GC的频率较高,回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,但是通过配置,可以在Full GC之前进行一次Minor GC这样可以加快老年代的回收速度。

  1. JUC
  1. Synchronized与Lock的区别
  1. Synchronized能实现的功能Lock都可以实现,而且Lock比Synchronized更好用,更灵活。
  2. Synchronized可以自动上锁和解锁;Lock需要手动上锁和解锁
  1. Runnable和Callable的区别
  1. Runnable接口中的方法没有返回值;Callable接口中的方法有返回值
  2. Runnable接口中的方法没有抛出异常;Callable接口中的方法抛出了异常
  3. Runnable接口中的落地方法是call方法;Callable接口中的落地方法是run方法
  1. 什么是分布式锁

当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。分布式锁可以将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存,如 Redis,通过set (key,value,nx,px,timeout)方法添加分布式锁。

  1. 什么是分布式事务

分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。

 

---------------------------------------------------------------------​

 


posted @ 2021-07-16 16:22  五分钟学大数据  阅读(1481)  评论(0编辑  收藏  举报