史上最详细大数据基础知识

# **1___Hive**

## 0.0、hive基本命令

```sql

[1、分区表]

--创建分区

alter table table_name add partition(分区字段='分区值');

alter table table_name add partition(分区字段='分区1'),partition(分区字段='分区值2');

load data local inpath '/root/dept_1.txt' into table deptinfo partition (year='2022');

--本地的root目录下的dept_1.txt数据 上传到表deptinfo中,并创建分区year=2022

--删除分区

alter table table_name drop partition(分区字段='分区值1'),partition(分区字段='分区值2');

--查看分区表的分区情况

show partitions table_name;

--查看具体分区

show partitions table_name partition(分区名)

--查看表结构

desc formatted table_name;

--另外,在分区表的基础上,还可以对分区字段再继续做二级分区。

--例如按年进行了分区,在分区后,还可以添加一个按月的二级分区。

--动态分区

--开启动态分区(默认已开启)

set hive.exec.dynamic.partition; =>true

--将动态分区设置为非严格模式(默认为严格模式)

set hive.exec.dynamic.partition.mode; =>strict

set hive.exec.dynamic.partition.mode=nonstrict;

--查看最大动态分区数

set hive.exec.max.dynamic.partitions; =>1000

set hive.exec.max.dynamic.partitions.pernode; =>100

--动态分区最大设为1000个

set hive.exec.max.dynamic.partitions.pernode=1000;

--动态分区示例:

create external table kb16.user_movie_rating(

userid bigint,

movieid bigint,

rating decimal(2,1),

`timestamp` bigint

)

row format delimited

fields terminated by ','

location '/test/kb16/hive/user_movie_rating'

tblproperties("skip.header.line.count"="1"); --从文件第二 行开始导入数据

--创建分区表

create external table kb16.user_movie_rating_par(

userid bigint,

movieid bigint,

rating decimal(2,1),

`timestamp` bigint

)

partitioned by(dt string) --按时间分区

row format delimited

00000000

location '/test/kb16/hive/user_movie_rating_par';

insert overwrite table kb16.user_movie_rating_par partition(dt)

select userid,movieid,rating,`timestamp`,date_format(from_unixtime(`timestamp`),'yyyy-MM') dt

from user_movie_rating;

--年作为分区

insert overwrite table user_movie_rating_par partition(dt)

select U.*,date_format(from_unixtime(U.`timestamp`),'yyyy') from user_movie_rating U;

--分区分桶表

create external table user_movie_rating_par_bucket(

userid bigint,

movieid bigint,

rating decimal(2,1),

`timestamp` bigint

)

partitioned by (years int)

clustered by (`timestamp`) sorted by (`timestamp` ASC) into 5 buckets

row format delimited fields terminated by ',';

insert overwrite table user_movie_rating_par_bucket partition(years)

select *,pmod(cast(date_format(from_unixtime(`timestamp`),'yyyy') as int),5) years

from user_movie_rating;

```

## 1.0、hive优化

### **1.** **表的优化**

**(1) 小表、大表Join**

将key相对分散,并且数据量小的表放在join的左边,这样可以有效减少内存溢出错误发生的概率;再进一步,可以使用map join让小的维度表(1000条以下的记录条数)先进内存。在map端完成reduce。

**(2) 大表Join大表**

**a. 空key过滤**

有时join超时是因为某些key对应的数据太多,而相同key对应的数据都会发送到相同的reducer上,从而导致内存不够。此时我们应该仔细分析这些异常的key,很多情况下,这些key对应的数据是异常数据,我们需要在SQL语句中进行过滤。

**b. 空key转换**

有时虽然某个key为空对应的数据很多,但是相应的数据不是异常数据,必须要包含在join的结果中,此时我们可以表a中key为空的字段赋一个随机的值,使得数据随机均匀地分不到不同的reducer上。

**(3)MapJoin**

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

```sql

设置自动选择Mapjoin

set hive.auto.convert.join = true; 默认为true

大表小表的阈值设置(默认25M以下认为是小表):

set hive.mapjoin.smalltable.filesize=25000000;

```

**(4)Group By**

Map阶段同一Key数据分发给一个reduce,当一个key数据过大时就倾斜了。并不是所有的聚合操作都需要在Reduce端完成,很多聚合操作都可以先在Map端进行部分聚合,最后在Reduce端得出最终结果。

**(5) 开启Map端聚合**

```sql

// 是否在Map端进行聚合,默认为True

set hive.map.aggr = true

// 在Map端进行聚合操作的条目数目 set hive.groupby.mapaggr.checkinterval = 100000

// 有数据倾斜的时候进行负载均衡(默认是false)

set hive.groupby.skewindata = true

```

**对数据倾斜负载均衡的理解**

会有两个MR Job。第一个MR Job中,Map的输出结果会随机分布到Reduce中,每个Reduce做部分聚合操作,并输出结果,这样处理的结果是相同的Group By Key有可能被分发到不同的Reduce中,从而达到负载均衡的目的;第二个MR Job再根据预处理的数据结果按照Group By Key分布到Reduce中(这个过程可以保证相同的Group By Key被分布到同一个Reduce中),最后完成最终的聚合操作。

**( 6 ) Count(Distinct)去重统计**

由于COUNT DISTINCT操作需要用一个Reduce Task来完成,这一个Reduce需要处理的数据量太大,就会导致整个Job很难完成,一般COUNT DISTINCT使用先GROUP BY再COUNT的方式替换,但是需要注意group by造成的数据倾斜问题。

**( 7 ) 笛卡尔积**

尽量避免笛卡尔积,join的时候不加on条件,或者无效的on条件,Hive只能使用1个reducer来完成笛卡尔积。

​ **( 8 ) 行列过滤**

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

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

### **2.** **合理设置Map数**

**首先理清楚Map数是越多越好吗?**

**逻辑**:如果一个任务有很多小文件(远远小于块大小128m),则每个小文件也会被当作一个块,用一个map任务来完成,而一个map任务启动和初始化的时间远远大于逻辑处理的时间,就会造成很大的资源浪费。

**保证每个map处理接近128m的文件块是不是就可以了?**

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

**复杂文件增加Map数**

**原理**:文件都很大,任务逻辑复杂,map执行非常慢的时候,可以考虑增加Map数,来使得每个map处理的数据量减少,从而提高任务的执行效率。

```sql

computeSliteSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M

```

调整maxSize最大值。让maxSize最大值低于blocksize就可以增加map的个数。

**小文件进行合并,减少map数**

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

```sql

set hive.input.format= org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;

```

Map-Reduce的任务结束时合并小文件的设置

```sql

// 在map-only任务结束时合并小文件,默认true

SET hive.merge.mapfiles = true;

// 在map-reduce任务结束时合并小文件,默认false

SET hive.merge.mapredfiles = true;

// 合并文件的大小,默认256M

SET hive.merge.size.per.task = 268435456;

//当输出文件的平均大小小于该值时,启动一个独立的map-reduce任务进行文件merge

SET hive.merge.smallfiles.avgsize = 16777216;

```

### **3.** **合理设置Reduce数**

同样考虑是不是越多越好?

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

(1)数据量设置

```sql

// 每个Reduce处理的数据量默认是256MB hive.exec.reducers.bytes.per.reducer=256000000

// 每个任务最大的reduce数,默认为1009

hive.exec.reducers.max=1009

// 计算reducer数的公式 N=min(hive.exec.reducers.max,总输入数据量/hive.exec.reducers.bytes.per.reducer)

```

(2)文件配置

```sql

mapreduce.job.reduces = 15

```

### **4.** **并行执行**

通过设置参数hive.exec.parallel值为true,就可以开启并发执行。不过,在共享集群中,需要注意下,如果job中并行阶段增多,那么集群利用率就会增加。建议在数据量大,sql很长的时候使用,数据量小,sql比较的小开启有可能还不如之前快。

```sql

//打开任务并行执行,默认为false

set hive.exec.parallel=true;

//同一个sql允许最大并行度,默认为8。 set hive.exec.parallel.thread.number=16;

```

### **5.** JVM**重用**

JVM来执行map和Reduce任务的。这时JVM的启动过程可能会造成相当大的开销,尤其是执行的job包含有成百上千task任务的情况。JVM重用可以使得JVM实例在同一个job中重新使用N次。

缺点是,开启JVM重用将一直占用使用到的ask插槽,以便进行重用,直到任务完成后才能释放。

```sql

set mapreduce.job.jvm.numtasks=10

```

### **6.** **列式存储**

因为每个字段的数据聚集存储,在查询只需要少数几个字段的时候,能大大减少读取的数据量;每个字段的数据类型一定是相同的,列式存储可以针对性地设计更好的设计压缩算法。

TextFile和SequenceFile的存储格式都是基于行存储的;

ORC和Parquet是基于列式存储的。

### **7.** **压缩(选择快的)**

```sql

// 启用中间数据压缩 set hive.exec.compress.intermediate=true

// 启用最终数据压缩 set mapreduce.map.output.compress=true

// 设置压缩方式 set mapreduce.map.outout.compress.codec= org.apache.hadoop.io.compress.DefaultCodec org.apache.hadoop.io.compress.GzipCodec org.apache.hadoop.io.compress.BZip2Codec org.apache.hadoop.io.compress.Lz4Codec

```

## 2.0、hive数据倾斜

### 1、**Hive数据倾斜表现**

就是单说hive自身的MR引擎:发现所有的map task全部完成,并且99%的reduce task完成,只剩下一个或者少数几个reduce task一直在执行,这种情况下一般都是发生了数据倾斜。说白了就是Hive的数据倾斜本质上是MapReduce的数据倾斜。

### **2、Hive数据倾斜的原因**

在MapReduce编程模型中十分常见,大量相同的key被分配到一个reduce里,造成一个reduce任务累死,其他reduce任务闲死。查看任务进度,发现长时间停留在99%或100%,查看任务监控界面,只有少量的reduce子任务未完成。

1. key分布不均衡。

2. 业务问题或者业务数据本身的问题,某些数据比较集中。

3. 建表的时候考虑不周。

4. 某些SQL语句本身就有数据倾斜,例如:

(1)大表join小表:其中小表key集中,分发到某一个或者几个Reduce上的数据远高于平均值。

(2)大表join大表:空值或无意义值:如果缺失的项很多,在做join时这些空值就会非常集中,拖累进度。

(3)group by: group by的时候维度过小,某值的数量过多,处理某值的reduce非常耗时间。

(4)Count distinct:某特殊值过多,处理此特殊值的reduce耗时。

### 3、Hive**数据倾斜解决**

**【参数调节】** hive.map.aggr = true

​ Map端部分聚合。

​ 有数据倾斜的时候进行负载均衡,当选项设定为true,生成的查询计划会有两个MRJob。第一个MRJob中,Map的输出结果集合会随机分不到Reduce中,每个Reduce做部分聚合操作,并输出结果,这样处理的结果是相同于Group By Key 有可能被分发到不同的Reduce中,从而达到负载均衡的目的;第二个MRJob再根据预处理的数据结果按照Group By Key分 到Reduce中(这个过程可以保证相同的Group By Key被分布到同一个Reduce中),最后完成最终的聚合操作。

**【SQL调整】**

​ 1)如何join:关于驱动表的选取,选用join key分布最均匀的表作为驱动表,做好列裁剪和filter操作,以达到两表做join的时候,数据量相对变小的效果。

​ 2)大小表join的时候:使用map join 让小的维度表先进内存,在map端完成reduce。效率很高。

​ 3)大表join大表的时候:把空值的key变成一个字符串加上随机数,把倾斜的数据分到不同的reduce上,由于null值关联不上,处理后不影响最终的结果。

​ 4)count distinct 大量相同特殊值,将这些值为空的情况单独处理,如果是计算count distinct,可以不用处理,直接过滤,在最后结果中加1即可。如果还有其他的计算,需要进行group by,可以先将那些值为空的记录单独处理,再和其他计算结果进行 union。

​ 5)group by 维度过小的时候:采用sum() group by 的方法来替换count(distinct)完成计算。

​ 6)单独处理倾斜key:一般来讲倾斜的key都很少,我们可以将它们抽样出来,对应的行单独存入临时表中,然后打上随机数前缀,最后再进行聚合。或者是先对key做一层hash,先将数据随机打散让它的并行度变大,再汇集。其实办法一样。

## 3.0、hive 内部表和外部表,分区表

未被 external 修饰的是内部表(managed table),被 external 修饰的为外部表 (external table)

**内部表:**

​ 与数据库中的table在概念上是类似的。

​ 每一个table在hive中都有一个相应的目录存储数据。

​ 所有的table数据都保存在这个目录中。

​ 删除表时,元数据与数据都会被删除。

**外部表:**

​ 指向已经在hdfs中存在的数据,可以创建partition。

​ 它和内部表在元数据的组织上是相同的,而实际数据的存储则有较大的差异。

​ 外部表只有一个过程,加载数据和创建表同时完成,并不会移动到数据库目录中,知识与外部数据建立一个链接。当删除一个外部表时,仅删除连接和元数据。

**分区表:**

​ partition对应于数据库的partition列的密集索引。

​ 在hive中,表的一个partition对应于表下的一个目录。所有的partition的数据都存储在对应的目录中。

**区别:**

1) 内部表数据由 Hive 自身管理,外部表数据由 HDFS 管理;

2) 内部表数据存储的位置是 hive.metastore.warehouse.dir(默认: /user/hive/warehouse),外部表数据的存储位置由自己制定(如果没有 Location, Hive将在HDFS上的/user/hive/warehouse文件夹下以外部表的表名创建一个文件 夹,并将属于这个表的数据存放在这里);

3) 删除内部表会直接删除元数据(metadata)及存储数据;删除外部表仅仅会删除元数据,HDFS 上的文件并不会被删除;

## 4.0、hive的分桶表

在分区数量过于庞大以至于可能导致文件系统崩溃时,我们就需要使用分桶来解决问题

分桶是相对分区进行更细粒度的划分。分桶则是指定分桶表的某一列,让该列数据按照哈希取模的方式随机、均匀地分发到各个桶文件中。因为分桶操作需要根据某一列具体数据来进行哈希取模操作,故指定的分桶列必须基于表中的某一列(字段) 要使用关键字clustered by 指定分区依据的列名,还要指定分为多少桶:

```sql

create table test(id int,name string) cluster by (id) into 5 buckets ....... insert into buck select id ,name from p cluster by (id)

```

## 5.0、hive分区分桶

### **1)分区**

​ 按照数据表的某列或者某些列分为多个分区,分区从形式上可以理解为文件夹,就我之前的业务中,用户行为表记录的是用户每天的行为日志数据,这个每天都会生成大量的日志,如果直接存在一张表上,会导致数据表的内容巨大,在查询时进行全表扫描耗费资源就非常多了,实际情况下,我们是按照日期对数据表进行分区,分区字样就是每天的日期,这样不同的日期的数据存放在不同的分区,在查询时只要指定具体分的分区字段的值就可以直接从该分区查找。分区是以字段的形式在表结构中存在,可以通过describe table 命令查看到字段存在。

#### **静态分区:**

静态分区实在编译期间指定分区名。 支持load 和 insert两种插入方式。 适用于分区数少,分区名可以明确的数据把。

```sql

--创建静态分区表

create table if not exists table_2(id int,name string)

partitioned by(age int)

row format delimited fields terminated by ',' stored as textfile;

--插入数据

insert into table table_2 partition(age=25) select id,name from table_1

--insert overwrite 是覆盖 , insert into 是追加

```

#### **动态分区:**

根据分区字段的实际值,动态进行分区, 是在sql 执行的时候进行分区。 需要先将动态分区设置打开。 只能使用insert的插入方式。 通过普通表选出的字段包含分区字段,分区字段设置在最后,多个分区字段按照分区顺序放置。

```sql

--开启动态分区功能(默认为false)

set hive.exec.dynamic.partition=true

--配置允许所有分区都是动态的(默认为strict)

set hive.exec.dynamic.partition.mode=nonstrict;

--插入数据

insert overwrite table table_2 partition(age) select id,name,age from table_1;

```

#### **静态分区和动态分区的区别:**

​ 静态分区和动态分区的主要区别在于静态分区是手动指定的,而动态分区是通过数据来进行判断。

**静态分区**是在编译期间指定分区名。 支持load 和 insert两种插入方式。 适用于分区数少,分区名可以明确的数据把。

根据分区字段的实际值,**动态进行分区**, 是在sql 执行的时候进行分区。 需要先将动态分区设置打开。 只能使用insert的插入方式。 通过普通表选出的字段包含分区字段,分区字段设置在最后,多个分区字段按照分区顺序放置。

### **2)分桶**

​ 分桶是相对分区进行更细粒度的划分。分桶将整个数据内容安装某列属性值得hash值进行分区,如果安装name属性分为3个桶,就是对name属性值的hash值对3取模,按照取模结果对数据分桶。如取模结果为0的数据记录存放到一个文件,取模为1的数据存放到一个文件,取模为2的数据存放到一个文件。

```sql

--建分桶表语句

create table bucketed_user(id int,name string) clustered by(id) into 4 buckets;

```

​ 对于每一个表或者分区,hive可以进一步组织成桶,也就是说桶是更为细粒度的数据范围划分。hive也是针对某一列进行桶的组织。hive采用对列值哈希,然后除以桶的个数取余的方式决定该条记录存放在哪个桶当中。把表(或者分区)组织成桶有两个理由:

​ 1.获得更高的查询处理效率

​ 桶为表加上了额外的结构,hive在处理有些查询时能利用这个结构。具体而言,连接两个在(包含连接列的)相同列上划分了桶的表,可以使用map端连接(map-side join)高效的实现。比如join操作。对于join操作两个表有一个相同的列,如果对这两个表都进行了桶操作。那么将保存相同列值得桶进行join操作就可以,可以大大减少join的数据量。

​ 2.使取样更高效。

​ 在处理大规模数据集时,在开发和修改查询的阶段,如果能在数据集的一小部分数据上试运行查询,会带来更多方便。

### **3)区别**

\1. 分区是表的部分列的集合,可以为频繁使用的数据建立分区,这样查找分区中的数据时就不需要扫描全表,这对于提高查找效率很有帮助。

\2. 分桶不同于分区对列直接进行拆分,而桶往往使用列的哈希值对数据打散,并分发到各个不同的桶中从而完成数据的分桶过程。

\3. 分区和分桶最大的区别就是分桶随机分割数据库,分区是非随机分割数据库。

## 6.0、hive 有索引吗

Hive 支持索引,但是 Hive 的索引与关系型数据库中的索引并不相同,比如,Hive 不支持主键或者外键。

Hive 索引可以建立在表中的某些列上,以提升一些操作的效率,例如减少MapReduce 任务中需要读取的数据块的数量。

在可以预见到分区数据非常庞大的情况下,索引常常是优于分区的。

虽然 Hive 并不像事物数据库那样针对个别的行来执行查询、更新、删除等操作。 它更多的用在多任务节点的场景下,快速地全表扫描大规模数据。但是在某些场 景下,建立索引还是可以提高 Hive 表指定列的查询速度。(虽然效果差强人意)

**索引适用的场景**:适用于不更新的静态字段。以免总是重建索引数据。每次建立、更新数据后,都 要重建索引以构建索引表。

**Hive 索引的机制如下:**hive 在指定列上建立索引,会产生一张索引表(Hive 的一张物理表),里面的字 段包括,索引列的值、该值对应的 HDFS 文件路径、该值在文件中的偏移量; v0.8 后引入 bitmap 索引处理器,这个处理器适用于排重后,值较少的列(例如, 某字段的取值只可能是几个枚举值) 因为索引是用空间换时间,索引列的取值过多会导致建立 bitmap 索引表过大。

## 7.0、如何对 hive 进行调度

\1. 将 hive 的 sql 定义在脚本当中

\2. 使用 azkaban 或者 oozie 进行任务的调度

\3. 监控任务调度页面

## 8.0、ORC、Parquet 等列式存储的优点

ORC 和 Parquet 都是高性能的存储方式,这两种存储格式总会带来存储和性能上 的提升

***Parquet:***

\1. Parquet 支持嵌套的数据模型,类似于 Protocol Buffers,每一个数据模型 的 schema 包含多个字段,每一个字段有三个属性:重复次数、数据类型 和字段名。 重复次数可以是以下三种:required(只出现 1 次),repeated(出现 0 次或 多次),optional(出现 0 次或 1 次)。每一个字段的数据类型可以分成两种: group(复杂类型)和 primitive(基本类型)。

\2. Parquet 中没有 Map、Array 这样的复杂数据结构,但是可以通过 repeated 和 group 组合来实现的。

\3. 由于 Parquet 支持的数据模型比较松散,可能一条记录中存在比较深的嵌 套关系,如果为每一条记录都维护一个类似的树状结可能会占用较大的存 储空间,因此 Dremel 论文中提出了一种高效的对于嵌套数据格式的压缩 算法:Striping/Assembly 算法。通过 Striping/Assembly 算法,parquet 可以 使用较少的存储空间表示复杂的嵌套格式,并且通常 Repetition level 和Definition level 都是较小的整数值,可以通过 RLE 算法对其进行压缩,进 一步降低存储空间。

\4. Parquet文件是以二进制方式存储的,是不可以直接读取和修改的,Parquet 文件是自解析的,文件 中包括该文件的数据和元数据。

***ORC:***

\1. ORC 文件是自描述的,它的元数据使用 Protocol Buffers 序列化,并且文件 中的数据尽可能的压缩以降低存储空间的消耗。

\2. 和 Parquet 类似,ORC 文件也是以二进制方式存储的,所以是不可以直接读 取,ORC 文件也是自解析的,它包含许多的元数据,这些元数据都是同构 ProtoBuffer 进行序列化的。

\3. ORC 会尽可能合并多个离散的区间尽可能的减少 I/O 次数。

\4. ORC 中使用了更加精确的索引信息,使得在读取数据时可以指定从任意一行 开始读取,更细粒度的统计信息使得读取 ORC 文件跳过整个 row group,ORC 默认会对任何一块数据和索引信息使用 ZLIB 压缩,因此 ORC 文件占用的存 储空间也更小。

\5. 在新版本的 ORC 中也加入了对 Bloom Filter 的支持,它可以进一 步提升谓词下推的效率,在 Hive 1.2.0 版本以后也加入了对此的支 持。

## 9.0、数据建模用的哪些模型

***星型模型***

星形模式(Star Schema)是最常用的维度建模方式。星型模式是以事实表为中心, 所有的维度表直接连接在事实表上,像星星一样。 星形模式的维度建模由一个事实表和一组维表成,且具有以下特点:

a. 维表只和事实表关联,维表之间没有关联;

b. 每个维表主键为单列,且该主键放置在事实表中,作为两边连接的外键;

c. 以事实表为核心,维表围绕核心呈星形分布;

***雪花模型***

雪花模式(Snowflake Schema)是对星形模式的扩展。雪花模式的维度表可以拥有 其他维度表的,虽然这种模型相比星型更规范一些,但是由于这种模型不太容易 理解,维护成本比较高,而且性能方面需要关联多层维表,性能也比星型模型要 低。所以一般不是很常用。

***星座模型***

星座模式是星型模式延伸而来,星型模式是基于一张事实表的,而星座模式是基 于多张事实表的,而且共享维度信息。前面介绍的两种维度建模方法都是多维表 对应单事实表,但在很多时候维度空间内的事实表不止一个,而一个维表也可能 被多个事实表用到。在业务发展后期,绝大部分维度建模都采用的是星座模式。

## 10.0、 为什么要对数据仓库分层?

\1. 用空间换时间,通过大量的预处理来提升应用系统的用户体验(效率),因 此数据仓库会存在大量冗余的数据。

\2. 如果不分层的话,如果源业务系统的业务规则发生变化将会影响整个数据清 洗过程,工作量巨大。

\3. 通过数据分层管理可以简化数据清洗的过程,因为把原来一步的工作分到了 多个步骤去完成,相当于把一个复杂的工作拆成了多个简单的工作,把一个大的 黑盒变成了一个白盒,每一层的处理逻辑都相对简单和容易理解,这样我们比较 容易保证每一个步骤的正确性,当数据发生错误的时候,往往我们只需要局部调 整某个步骤即可。

## 11.0、sort by 和 order by 的区别

*order by* 会对输入做全局排序,因此只有一个 reducer(多个 reducer 无法保证全 局有序)只有一个 reducer,会导致当输入规模较大时,需要较长的计算时间。

*sort by* 不是全局排序,其在数据进入 reducer 前完成排序. 因此,如果用 sort by 进行排序,并且设置 mapred.reduce.tasks>1, 则 sort by 只 保证每个 reducer 的输出有序,不保证全局有序。

## 12.0、**hive 小文件过多怎么解决**

### **一、小文件产生的原因:**

#### 1.直接向表中插入数据:

​ 因为这种方式每次插入时都会产生一个文件,多次插入少量数据的话,就会出现多个小文件,但这种方式一般在工作中基本没人会这么用的。

```sql

insert into table A values (1,'zhangsan',88),(2,'lisi',61);

```

#### 2.通过load方式加载数据:

​ 用 load 方式是既可以导入文件也可以导入文件夹,如果导入一个文件,hive表就有一个文件;如果导入一个文件夹的时候,那么hive表的文件的数量就是这个文件夹下面所有的文件的数量。

```sql

load data local inpath '/export/score.csv' overwrite into table A -- 导入文件

load data local inpath '/export/score' overwrite into table A -- 导入文件夹

```

#### 3.通过查询方式加载数据:

​ 这种方式是工作中常用的,当然也是最容易产生小文件的方式,因为insert 导入数据的时候就会启动 MapReduce 任务,MapReduce中 得reduce 有多少个那么就会输出多少个文件,可以理解为:文件数量=ReduceTask数量**分区数*

​ 当然也有很多简单任务没有reduce,只有map阶段,那么就可以理解为:

文件数量=MapTask数量*分区数

​ 因为 insert 导入时至少会有一个MapTask,所以每执行一次 insert 时hive中至少产生一个文件。

​ 如果有的业务需要每隔很短的时间就需要把数据同步到 hive 中,这样产生的文件就会特别的多。

### **二、小文件过多产生的影响:**

1. 首先对底层存储HDFS来说,HDFS本身就不适合存储大量小文件,因为如果小文件过多就会导致namenode元数据特别大, 占用太多内存,这样就会严重影响HDFS的性能。

2. 其次对 hive 来说,在进行查询时,每个小文件都会当成一个块,启动一个Map任务来完成,而一个Map任务启动和初始化的时间远远大于逻辑处理的时间,就会造成很大的资源浪费。并且同时可执行的Map数量是受限的。

### **三、怎么解决小文件过多:**

#### **1.使用 hive 自带的 concatenate 命令,自动合并小文件。**

**使用方法:**

```sql

#对于非分区表

alter table A concatenate;

#对于分区表

alter table B partition(day=20201224) concatenate;

```

注意:

1、concatenate 命令只支持 RCFile和 ORC 文件类型。

2、使用concatenate命令合并小文件时不能指定合并后的文件数量,但可以多次执行该命令。

3、当多次使用concatenate后文件数量不在变化,这个跟参数 mapreduce.input.fileinputformat.split.minsize=256mb 的设置有关,可设定每个文件的最小size。

#### **2.调整参数,减少Map数量。**

**1)我们可以设置map输入合并小文件的相关参数:**

```sql

#执行Map前进行小文件合并

#CombineHiveInputFormat底层是 Hadoop的 CombineFileInputFormat 方法

#此方法是在mapper中将多个文件合成一个split作为输入

set hive.input.format=org.apache.hadoop.hive.sql.io.CombineHiveInputFormat; -- 默认

#每个Map最大输入大小(这个值决定了合并后文件的数量)

set mapred.max.split.size=256000000; -- 256M

#一个节点上split的至少的大小(这个值决定了多个DataNode上的文件是否需要合并)

set mapred.min.split.size.per.node=100000000; -- 100M

#一个交换机下split的至少的大小(这个值决定了多个交换机上的文件是否需要合并)

set mapred.min.split.size.per.rack=100000000; -- 100M

```

**2)也可以设置map输出和reduce输出进行合并的相关参数:**

```sql

#设置map端输出进行合并,默认为true

set hive.merge.mapfiles = true;

#设置reduce端输出进行合并,默认为false

set hive.merge.mapredfiles = true;

#设置合并文件的大小

set hive.merge.size.per.task = 256*1000*1000; -- 256M

#当输出文件的平均大小小于该值时,启动一个独立的MapReduce任务进行文件merge

set hive.merge.smallfiles.avgsize=16000000; -- 16M

```

**3)启用压缩:**

```sql

# hive的查询结果输出是否进行压缩

set hive.exec.compress.output=true;

# MapReduce Job的结果输出是否使用压缩

set mapreduce.output.fileoutputformat.compress=true;

```

#### 3. 调整参数,减少Reduce的数量。

```sql

#reduce 的个数决定了输出的文件的个数,所以可以调整reduce的个数控制hive表的文件数量,

#hive中的分区函数 distribute by 正好是控制MR中partition分区的,

#然后通过设置reduce的数量,结合分区函数让数据均衡的进入每个reduce即可。

#设置reduce的数量有两种方式,第一种是直接设置reduce个数

set mapreduce.job.reduces=10;

#第二种是设置每个reduce的大小,Hive会根据数据总大小猜测确定一个reduce个数

set hive.exec.reducers.bytes.per.reducer=5120000000; -- 默认是1G,设置为5G

#执行以下语句,将数据均衡的分配到reduce中

set mapreduce.job.reduces=10;

insert overwrite table A partition(dt)

select * from B

distribute by rand();

解释:如设置reduce数量为10,则使用 rand(), 随机生成一个数 x % 10 ,

这样数据就会随机进入 reduce 中,防止出现有的文件过大或过小

```

#### **4. 使用hadoop的archive将小文件归档。**

​ Hadoop Archive简称HAR,是一个高效地将小文件放入HDFS块中的文件 存档工具,它能够将多个小文件打包成一个HAR文件,这样在减少namenode内存使用的同时,仍然允许对文件进行透明的访问。

```sql

#用来控制归档是否可用

set hive.archive.enabled=true;

#通知Hive在创建归档时是否可以设置父目录

set hive.archive.har.parentdir.settable=true;

#控制需要归档文件的大小

set har.partfile.size=1099511627776;

#使用以下命令进行归档

ALTER TABLE A ARCHIVE PARTITION(dt='2020-12-24', hr='12');

#对已归档的分区恢复为原文件

ALTER TABLE A UNARCHIVE PARTITION(dt='2020-12-24', hr='12');

```

注意:

归档的分区可以查看不能 insert overwrite,必须先 unarchive。

## 13.0、hive和数据库比较

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

1) 数据存储位置

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

2) 数据更新

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

3) 执行延迟

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

4) 数据规模

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

## 14.0、**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。

## 15.0、**窗口函数**

Rank() 排序相同时会重复,总数不会变 1 2 2 4

Dense_Rank() 排序相同时会重复,总数会减少 1 2 2 3

Row_Number() 会根据顺序计算 1 2 3 4

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 类型。

## 16.0、自定义 UDF、UDTF、UDAF

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

1)

自定义过。

2) 用 UDF 函数解析公共字段;用 UDTF 函数解析事件字段。

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

自定义 UDTF:继承自 GenericUDTF,重写 3 个方法:initialize(自定义输出的列名和类型),

process(将结果返回 forward(result)),close

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

1. UDF 一进一出,继承了org.apache.hadoop.hive.ql.exec.UDF类,并覆写了 evalute方法。

2. UDAF 聚合函数,多进一出。如 count max min

3. UDTF 一进多出, 如 列转行函数 lateral view explore()

## 17.0、hive是怎么集成hbase的

1)首先我们需要将hbase的客户端 jar包 拷入到hive lib目录下。

2)修改 hive/conf下的 hive-site.xml配置文件。添加这样的属性。

```xml

<property>

<name>hbase.zookeeper.quorum</name>

<value>hadoop</value>

</property>

```

3)启动hive,创建管理表 hbase_table,指定数据存储在hbase表中。这样的配置

```sql

stored by 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'

with serdeproperties

("hbase.columns.mapping"=":key,accuracy:total_score,accuracy:question_count,accuracy:accuracy")

tblproperties("hbase.table.name"="exam:analysis");

```

4)往hive表hbase_table 表中插入数据。

## 18.0、hive查询的时候on和where有什么区别

**左右关联时:**

​ 条件不为主表条件时,放在on和where后面一样。

​ 条件为主表条件时,放在on后面,结果为主表全量,放在where后面为主表条件筛选过后的全量。

​ 举个例子:

```sql

select * from a left join b on a.id=b.id and a.dt='2022-6-14';

-- 这种情况在left join 在on上面写了主表a的条件不会生效的,全表扫描。

select * from a left join b on a.id=b.id and b.dt='2022-6-14';

--这种情况如果是left join在on上写入附表b的条件会生效,但是语义与写到where条件不同。

select * from a inner join b on a.id=b.id and a.dt='2022-6-14';

--如果是inner join 在on上写主表a、附表b的条件都会生效。

select * from a left join b on a.id=b.id where a.dt='2022-6-14';

--一般是这么写,条件尽量别写在on后面,直接写到where后面就可以了。

```

## 19.0、hive里面的left join 是怎么回事,怎么执行的

​ 不考虑where条件下,left join会把左表所有数据查询出来,on及其后面的条件仅仅会影响右表的数据(符合就显示,不符合全部为null)。

​ 在join阶段,where子句的条件都不会被使用,仅在join阶段完成之后,where子句条件才会被使用,它将从匹配阶段产生的数据中检索过滤。

​ 所以左连接关注的是左边的主表数据,不应该把on后面的从表中的条件加到where后,这样会影响原有主表中的数据。

​ where后面:是先连接生成临时查询结果,然后再筛选on后面;先根据条件过滤筛选,再连接生成临时查询结果。

​ 对于条件在on加个and还是用子查询,查询结果是一模一样的,至于如何使用这个需要分情况,用子查询的话会多一个maptask,但是如果利用这个子查询过滤到很多数据的话,用子查询还是比较建议的,因为不会加载太多的数据到内存中,如果过滤数据不多的情况下,建议用on后面加and条件。

## 20.0、拉链表

**什么是拉链表:**

​ 拉链表,它是一种数据模型吧。主要是针对数仓设计中表存储数据的方式而定义的,所谓拉链就是记录历史。记录一个事物从开始,一直到当前状态的所有变化的信息。拉链表可以避免按每一天存储所有记录造成的海量存储问题,也是处理缓慢变化数据的常用方式把。

​ 如果当前信息至今有效,可以在生效结束日期中填入一个极大值。

**拉链表的使用场景:**

​ 1.数据量大,且表中部分字段会更新,比如之前项目中的业主委托事实表,这数据里有业主的委托状态这个字段,可能现在是委托出售,但后面有可能变成委托出租或者停止委托等等。

​ 2.他这个变化的比例和频率不是很大。比如我们需要查看某套房屋在过去一段时间内,委托状态更新过几次的的问题。

​ 3.如果对这边表每天都保留一份全量,那么每次全量中会保存很多不变的信息,对存储是极大的浪费;

​ 这种情况下使用拉链表基本就兼顾了我们的需求了。首先它在空间上做了一个取舍,它每日的增量可能比那种每天都保留一份全量的方法少很多很多数据,拉链表也能够获取最新的数据,也能添加筛选条件获取历史的数据。

## 21.0、使用hive-sql如何查询A表中B表不存在的数据

select distinct A.ID from A where A.ID not in (select ID from B);

select * from A where (select count(1) as num from A where A.ID=B.ID) = 0;

## 22.0、列转行,行转列函数

列转行: explore() 一般搭配 lateral view视图 来使用

行转列:case when

多行转列:collect_list()和collect_set(),它们都是将分组中的某列转为一个数组返回,不同的是collect_list不去重而 collect_set去重。

# 3___Kafka

## 0. kafka基础命令

```shell

#启动服务

kafka-server-start.sh /opt/software/kafka280/config/server.properties

#关闭服务

kafka-server-stop.sh

```

```shell

#查看主题

kafka-topics.sh --bootstrap-server single1:9092 --list

kafka-topics.sh --list --bootstrap-server single1:9092

#创建主题

kafka-topics.sh --bootstrap-server single1:9092 --create --topic TOPIC_NAME --partitions N --replication-factor N #replication备份数量

kafka-topics.sh --bootstrap-server single1:9092 --create --topic kb16_test02 --partitions 1 --replication-factor 1

#查看主题详情

kafka-topics.sh --bootstrap-server single1:9092 --describe --topic kb16_test02

#删除主题

kafka-topics.sh --bootstrap-server single1:9092 --delete --topic TOPIC_NAME

#创建控制台生产者

kafka-console-producer.sh --broker-list single1:9092 --topic kb16_test02 < file.log

#创建控制台消费者

kafka-console-consumer.sh --bootstrap-server single1:9092 --topic kb16_press_t1 --from-beginning

```

## 1. 为什么要用消息队列

\1. 解耦

允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。

\2. 可恢复性

系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。

\3. 缓冲

有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况。

\4. 灵活性与峰值处理能力

在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。

\5. 异步通信

很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。

## 2. 为什么要使用 kafka

1. 缓冲和削峰:上游数据时有突发流量,下游可能扛不住,或者下游没有足够多的机器来保证冗余,kafka 在中间可以起到一个缓冲的作用,把消息暂存在 kafka 中,下游服务就可以按照自己的节奏进行慢慢处理。

2. 解耦和扩展性:项目开始的时候,并不能确定具体需求。消息队列可以作为一个接口层,解耦重要的业务流程。只需要遵守约定,针对数据编程即可获 取扩展能力。

3. 冗余:可以采用一对多的方式,一个生产者发布消息,可以被多个订阅 topic 的服务消费到,供多个毫无关联的业务使用。

4. 健壮性:消息队列可以堆积请求,所以消费端业务即使短时间死掉,也不会影响主要业务的正常进行。

5. 异步通信:很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。

## 3. kafka的组件与作用(架构)

\1. Producer :消息生产者,就是向kafka broker发消息的客户端。

\2. Consumer :消息消费者,向kafka broker取消息的客户端。

\3. Consumer Group (CG):消费者组,由多个consumer组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。

\4. Broker :一台kafka服务器就是一个broker。一个集群由多个broker组成。一个broker可以容纳多个topic。

\5. Topic :可以理解为一个队列,生产者和消费者面向的都是一个topic。

\6. Partition:为了实现扩展性,一个非常大的topic可以分布到多个broker(即服务器)上,一个topic可以分为多个partition,每个partition是一个有序的队列。

\7. Replica:副本,为保证集群中的某个节点发生故障时,该节点上的partition数据不丢失,且kafka仍然能够继续工作,kafka提供了副本机制,一个topic的每个分区都有若干个副本,一个leader和若干个follower。

\8. leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是leader。

\9. follower:每个分区多个副本中的“从”,实时从leader中同步数据,保持和leader数据的同步。leader发生故障时,某个follower会成为新的leader。

## 4. kafka为什么要分区

\1. 方便在集群中扩展,每个Partition可以通过调整以适应它所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了。

\2. 可以提高并发,因为可以以Partition为单位读写。

## 5. Kafka生产者分区策略

\1. 指明 partition 的情况下,直接将指明的值直接作为partiton值。

\2. 没有指明partition值但有key的情况下,将key的hash值与topic的partition数进行取余得到partition值。

\3. 既没有partition值又没有key值的情况下,第一次调用时随机生成一个整数(后面每次调用在这个整数上自增),将这个值与topic可用的partition总数取余得到partition值,也就是常说的round-robin算法。

## 6. kafka的数据可靠性怎么保证

为保证producer发送的数据,能可靠的发送到指定的topic,topic的每个partition收到producer发送的数据后,都需要向producer发送ack(acknowledgement确认收到),如果producer收到ack,就会进行下一轮的发送,否则重新发送数据。所以引出ack机制。

## 7. ack应答机制(可问:造成数据重复和丢失的相关问题)

Kafka为用户提供了三种可靠性级别,用户根据对可靠性和延迟的要求进行权衡,选择以下的配置。

acks参数配置:

0: producer不等待broker的ack,这一操作提供了一个最低的延迟,broker一接收到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据。

1: producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,那么将会丢失数据。

-1(all):producer等待broker的ack,partition的leader和follower全部落盘成功后才返回ack。但是如果在follower同步完成后,broker发送ack之前,leader发生故障,那么会造成数据重复。

## **8. Kafka 消费过的消息如何再消费**

kafka 消费消息的 offset 是定义在 zookeeper 中的, 如果想重复消费 kafka 的消 息,可以在 redis 中自己记录 offset 的 checkpoint 点(n 个),当想重复消费消息时,通过读取 redis 中的 checkpoint 点进行 zookeeper 的 offset 重设,这样就可以 达到重复消费消息的目的了

## 9. kafka 的数据是放在磁盘上还是内存上,为什么速度会快

kafka 使用的是磁盘存储。

速度快是因为:

\1. Kafka本身是分布式集群,同时采用分区技术,并发度高。

\2. 顺序写磁盘

Kafka的producer生产数据,要写入到log文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能到600M/s,而随机写只有100K/s。

\3. 零拷贝技术

零拷贝并不是不需要拷贝,而是减少不必要的拷贝次数。通常是说在IO读写过程中。

**传统IO流程:**

第一次:将磁盘文件,读取到操作系统内核缓冲区。

第二次:将内核缓冲区的数据,copy到application应用程序的buffer。

第三步:将application应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区)

第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输。

传统方式,读取磁盘文件并进行网络发送,经过的四次数据copy是非常繁琐的。实际IO读写,需要进行IO中断,需要CPU响应中断(带来上下文切换),尽管后来引入DMA来接管CPU的中断请求,但四次copy是存在“不必要的拷贝”的。

重新思考传统IO方式,会注意到实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传输回套接字缓冲区之外什么都不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区。

显然,第二次和第三次数据copy 其实在这种场景下没有什么帮助反而带来开销,这也正是零拷贝出现的意义。

所以零拷贝是指读取磁盘文件后,不需要做其他处理,直接用网络发送出去。

## **10. Kafka 数据怎么保障不丢失**

分三个点说,一个是生产者端,一个消费者端,一个 broker 端。

1、**生产者数据的不丢失:**

kafka 的 ack 机制:在 kafka 发送数据的时候,每次发送消息都会有一个确认反馈 机制,确保消息正常的能够被收到,其中状态有 0,1,-1。

**如果是同步模式**:

ack 设置为 0,风险很大,一般不建议设置为 0。即使设置为 1,也会随着 leader 宕机丢失数据。所以如果要严格保证生产端数据不丢失,可设置为-1。

**如果是异步模式**:

也会考虑 ack 的状态,除此之外,异步模式下的有个 buffer,通过 buffer 来进行 控制数据的发送,有两个值来进行控制,时间阈值与消息的数量阈值,如果 buffer 满了数据还没有发送出去,有个选项是配置是否立即清空 buffer。可以设置为-1, 永久阻塞,也就数据不再生产。异步模式下,即使设置为-1。也可能因为程序员 的不科学操作,操作数据丢失,比如 kill -9,但这是特别的例外情况。

​ **注:**

```sql

ack=0:producer 不等待 broker 同步完成的确认,继续发送下一条(批)信息。

ack=1(默认):producer 要等待leader成功收到数据并得到确认,才发送下一条message。

ack=-1:producer 得到 follwer 确认,才发送下一条数据。

```

2、**消费者数据的不丢失:**

通过 offset commit 来保证数据的不丢失,kafka 自己记录了每次消费的 offset 数 值,下次继续消费的时候,会接着上次的 offset 进行消费。

而 offset 的信息在 kafka0.8 版本之前保存在 zookeeper 中,在 0.8 版本之后保存 到 topic 中,即使消费者在运行过程中挂掉了,再次启动的时候会找到 offset 的 值,找到之前消费消息的位置,接着消费,由于 offset 的信息写入的时候并不 是每条消息消费完成后都写入的,所以这种情况有可能会造成重复消费,但是不 会丢失消息。

唯一例外的情况是,我们在程序中给原本做不同功能的两个 consumer 组设置 KafkaSpoutConfig.bulider.setGroupid 的时候设置成了一样的 groupid,这种情况会 导致这两个组共享同一份数据,就会产生组 A 消费 partition1,partition2 中的消 息,组 B 消费 partition3 的消息,这样每个组消费的消息都会丢失,都是不完整 的。 为了保证每个组都独享一份消息数据,groupid 一定不要重复才行。

3、**kafka集群中的broker的数据不丢失:**

每个 broker 中的 partition 我们一般都会设置有 replication(副本)的个数,生产 者写入的时候首先根据分发策略(有 partition 按 partition,有 key 按 key,都没 有轮询)写入到 leader 中,follower(副本)再跟 leader 同步数据,这样有了备 份,也可以保证消息数据的不丢失。

## 11. 采集数据为什么选择 kafka

采集层 主要可以使用 Flume, Kafka 等技术。

Flume: Flume 是管道流方式,提供了很多的默认实现,让用户通过参数部署, 及扩展 API.

Kafka: Kafka 是一个可持久化的分布式的消息队列。 Kafka 是一个非常通用的系 统。你可以有许多生产者和很多的消费者共享多个主题 Topics。

相比之下,Flume 是一个专用工具被设计为旨在往 HDFS,HBase 发送数据。它对 HDFS 有特殊的优化,并且集成了 Hadoop 的安全特性。

所以,Cloudera 建议如果数据被多个系统消费的话,使用 kafka;如果数据被设 计给 Hadoop 使用,使用 Flume。

## 12. kafka 重启是否会导致数据丢失

\1. kafka 是将数据写到磁盘的,一般数据不会丢失。

\2. 但是在重启 kafka 过程中,如果有消费者消费消息,那么 kafka 如果来不及提交 offset, 可能会造成数据的不准确(丢失或者重复消费)。

## 13. kafka 宕机了如何解决

**先考虑业务是否受到影响**

​ kafka 宕机了,首先我们考虑的问题应该是所提供的服务是否因为宕机的机器而 受到影响,如果服务提供没问题,如果实现做好了集群的容灾机制,那么这块就 不用担心了。

**节点排错与恢复**

​ 想要恢复集群的节点,主要的步骤就是通过日志分析来查看节点宕机的原因,从 而解决,重新恢复节点。

## 14. 为什么 Kafka 不支持读写分离

在 Kafka 中,生产者写入消息、消费者读取消息的操作都是与 leader 副本进行 交互的,从 而实现的是一种**主写主读**的生产消费模型。 Kafka 并不支持**主写从读**,因为主写从读有 2 个很明显的缺点:

**数据一致性问题**:

​ 数据从主节点转到从节点必然会有一个延时的时间窗口,这个 时间 窗口会导致主从节点之间的数据不一致。某一时刻,在主节点和从节点中 A 数据的值都为 X, 之后将主节点中 A 的值修改为 Y,那么在这个变更通知到从 节点之前,应用读取从节点中的 A 数据的值并不为最新的 Y,由此便产生了数 据不一致的问题。

**延时问题:**

​ 类似 Redis 这种组件,数据从写入主节点到同步至从节点中的过程 需要经历 网络→主节点内存→网络→从节点内存 这几个阶段,整个过程会耗费 一定的时间。而在 Kafka 中,主从同步会比 Redis 更加耗时,它需要经历 网络 →主节点内存→主节点磁盘→网络→从节 点内存→从节点磁盘 这几个阶段。对 延时敏感的应用而言,主写从读的功能并不太适用。

而 kafka 的**主写主读**的优点就很多了:

​ \1. 可以简化代码的实现逻辑,减少出错的可能;

​ \2. 将负载粒度细化均摊,与主写从读相比,不仅负载效能更好,而且对用户可控;

​ \3. 没有延时的影响;

​ \4. 在副本稳定的情况下,不会出现数据不一致的情况。

## 15. kafka 数据分区和消费者的关系

每个分区只能由同一个消费组内的一个消费者(consumer)来消费,可以由不同的 消费组的消费者来消费,同组的消费者则起到并发的效果。

## **16. kafka 的数据 offset 读取流程**

\1. 连接 ZK 集群,从 ZK 中拿到对应 topic 的 partition 信息和 partition 的 Leader 的相关信息

\2. 连接到对应 Leader 对应的 broker

\3. consumer 将⾃自⼰己保存的 offset 发送给 Leader

\4. Leader 根据 offset 等信息定位到 segment(索引⽂文件和⽇日志⽂文件)

\5. 根据索引⽂文件中的内容,定位到⽇日志⽂文件中该偏移量量对应的开始 位置读取相应⻓长度的数据并返回给 consumer

## **17. kafka 内部如何保证顺序,结合外部组件如何保证消费者的顺序**

kafka 只能保证 partition 内是有序的,但是 partition 间的有序是没办法的。爱奇 艺的搜索架构,是从业务上把需要有序的打到同⼀个 partition。

## **18. Kafka 消息数据积压,Kafka 消费能力不足怎么处理**

\1. 如果是 Kafka 消费能力不足,则可以考虑增加 Topic 的分区数,并且同时 提升消费组的消费者数量,消费者数=分区数。(两者缺一不可)

\2. 如果是下游的数据处理不及时:提高每批次拉取的数量。批次拉取数据过 少(拉取数据/处理时间<生产速度),使处理的数据小于生产的数据,也 会造成数据积压。

## **19. Kafka 单条日志传输大小**

kafka 对于消息体的大小默认为单条最大值是 1M 但是在我们应用场景中, 常常 会出现一条消息大于 1M,如果不对 kafka 进行配置。则会出现生产者无法将消 息推送到 kafka 或消费者无法去消费 kafka 里面的数据, 这时我们就要对 kafka 进

行以下配置:server.properties

```sql

replica.fetch.max.bytes: 1048576 broker 可复制的消息的最大字节数, 默认为 1M message.max.bytes: 1000012 kafka 会接收单个消息 size 的最大限制, 默认为 1M 左右

```

注意:

message.max.bytes **必须小于等于** replica.fetch.max.bytes,否则就会导致 replica 之间数据同步失败。

## 20. kafka参数优化

**Broker参数配置(server.properties)**

```shell

1、日志保留策略配置

# 保留三天,也可以更短 (log.cleaner.delete.retention.ms) log.retention.hours=72

2、Replica相关配置

default.replication.factor:1 默认副本1个

3、网络通信延时

replica.socket.timeout.ms:30000 #当集群之间网络不稳定时,调大该参数 replica.lag.time.max.ms= 600000# 如果网络不好,或者kafka集群压力较大,会出现副本丢失,然后会 频繁复制副本,导致集群压力更大,此时可以调大该参数。

```

**Producer优化(producer.properties)**

```shell

compression.type:none gzip snappy lz4

#默认发送不进行压缩,推荐配置一种适合的压缩算法,可以大幅度的减缓网络压力和Broker的存储压力。

```

**Kafka内存调整(kafka-server-start.sh)**

```shell

默认内存1个G,生产环境尽量不要超过6个G。

export KAFKA_HEAP_OPTS="-Xms4g -Xmx4g"

```

## 21. Kafka适合以下应用场景

\1. 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer。

\2. 消息系统:解耦生产者和消费者、缓存消息等。

\3. 用户活动跟踪:kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后消费者通过订阅这些topic来做实时的监控分析,亦可保存到数据库。

\4. 运营指标:kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告;

\5. 流式处理:比如spark和flink。

# 4___Hbase

## 0. HBase基础命令

```shell

#启动 | 关闭

start-hbase.sh

stop-hbase.sh

#进入hbase

hbase shell

```

```shell

[#DDL#]

#查看存在的所有表

list

#查看命名空间(相当于查看当前存在的库)

list_namespace

#创建命名空间(相当于创建库)

create_namespace 'kb16nb'

#查看指定命名空间下面的存在的表

list_namespace_tables 'kb16nb'

[#表#]

#建表

create 'kb16nb:student','base','bigdata','cloud' #base,bigdata,cloud都是列簇名

#详细查看表的定义

describe 'kb16nb:student'

#查看修改表状态

is_enabled 'kb16nb:student'#查看表是否被启用

is_disabled 'kb16nb:student' #查看表是否被禁用

enable 'kb16nb:student' #启用表

disable 'kb16nb:student'#禁用表

#删除表(表必须已禁用)

drop 'kb16nb:student'

[#DML#]

#删除表所有数据

truncate 'kb16nb:student'

#删除某行某列的最新版本数据

delete 'kb16nb:student','rowkey','columnfamily:colname'

#删除某行的所有版本数据

deleteall 'kb16nb:student','rowkey'

#删除行的最新版本数据

delete 'kb16nb:student','rowkey'

#删除某行某列的所有版本数据

deleteall 'kb16nb:student','rowkey','columnfamily:colname'

#查询全表数据

scan 'kb16nb:student'

#新增数据(可以同时执行)

put 'kb16nb:student','1','base:name','zhangsan'

put 'kb16nb:student','2','base:name','lisa'

put 'kb16nb:student','1','base:age',18

put 'kb16nb:student','2','base:age',19

put 'kb16nb:student','1','base:product','bigdata'

put 'kb16nb:student','2','base:product','cloud'

#指定查询

get 'kb16nb:student','2','base:name'

scan 'kb16nb:student',{COLUMN=>'base'}

scan 'kb16nb:student',{COLUMN=>'base:name'}

scan 'kb16nb:student',{COLUMN=>'base:name',LIMIT=>2} #查询前两条

scan 'kb16nb:student',{COLUMN=>'base',LIMIT=>2,STARTROW=>'2'}

scan 'kb16nb:student',{COLUMN=>'base:name',LIMIT=>2,STARTROW=>'2'} #查询指定行键从2开始的两条

// #STARTROW 包含,STOPROW 不包含

scan 'kb16nb:student',{

COLUMN=>'base:name',

LIMIT=>3,

STARTROW=>'2', (2包含)

STOPROW=>'4'} (4不好含)

```

在hive中建表关联hbase

```sql

create external table kb16.hive_map_hbase_01(

stuid int,

stuname string,

stuage int,

product string

)stored by 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'

with serdeproperties ("hbase.columns.mapping"=":key,base:name,base:age,base:product")

tblproperties("hbase.table.name"="kb16nb:student");

```

## 1.为什么选择HBase

1、海量存储

Hbase适合存储PB级别的海量数据,在PB级别的数,能在几十到几百毫秒内返回数据。这与Hbase的极易扩展性息息相关。正是因为Hbase良好的扩展性,才为海量数据的存储提供了便利。

2、列式存储

这里的列式存储其实说的是列族存储,Hbase是根据列族来存储数据的。HBase中的每个列都由Column Family(列族)和Column Qualifier(列限定符)进行限定,例如info:name,info:age。

3、极易扩展

Hbase的扩展性主要体现在两个方面,一个是基于上层处理能力(RegionServer)的扩展,一个是基于存储的扩展(HDFS)。

通过横向添加RegionSever的机器,进行水平扩展,提升Hbase上层的处理能力,提升Hbsae服务更多Region的能力。

4、稀疏

稀疏主要是针对Hbase列的灵活性,在列族中,你可以指定任意多的列,在列数据为空的情况下,是不会占用存储空间的。

5、 数据多版本

数据多版本:每个单元中的数据可以有多个版本,默认情况下,版本号自动分配,版本号就是单元格插入时的时间戳。

## 2. **HBase与Hive的对比**

**Hive**

(1) 数据仓库

Hive的本质其实就相当于将HDFS中已经存储的文件在Mysql中做了一个双射关系,以方便使用HQL去管理查询。

(2) 用于数据分析、清洗

Hive适用于离线的数据分析和清洗,延迟较高。

(3) 基于HDFS、MapReduce

Hive存储的数据依旧在DataNode上,编写的HQL语句终将是转换为MapReduce代码执行。

**HBase**

(1)数据库

是一种面向列存储的非关系型数据库。

(2) 用于存储结构化和非结构化的数据

适用于单表非关系型数据的存储,不适合做关联查询,类似JOIN等操作。

(3) 基于HDFS

数据持久化存储的体现形式是Hfile,存放于DataNode中,被ResionServer以region的形式进行管理。

(4) 延迟较低,接入在线业务使用

面对大量的企业数据,HBase可以直线单表大量数据的存储,同时提供了高效的数据访问速度。

## 3. Hbase 写流程

(1)Client先访问zookeeper,获取hbase:meta表位于哪个Region Server。

(2)访问对应的Region Server,获取hbase:meta表,根据读请求的namespace:table/rowkey,查询出目标数据位于哪个Region Server中的哪个Region中。并将该table的region信息以及metA表的位置信息缓存在客户端的meta cache,方便下次访问。

(3)与目标Region Server进行通讯。

(4)将数据顺序写入(追加)到WAL。

(5)将数据写入对应的MemStore,数据会在MemStore进行排序。

(6)向客户端发送ack。

(7)等达到MemStore的刷写时机后,将数据刷写到HFile。

## 4. Hbase读流程

(1)Client先访问zookeeper,获取hbase:meta表位于哪个Region Server。

(2)访问对应的Region Server,获取hbase:meta表,根据读请求的namespace:table/rowkey,查询出目标数据位于哪个Region Server中的哪个Region中。并将该table的region信息以及meta表的位置信息缓存在客户端的meta cache,方便下次访问。

(3)与目标Region Server进行通讯。

(4)分别在MemStore和Store File(HFile)中查询目标数据,并将查到的所有数据进行合并。此处所有数据是指同一条数据的不同版本(time stamp)或者不同的类型(Put/Delete)。

(5)将查询到的新的数据块(Block,HFile数据存储单元,默认大小为64KB)缓存到Block Cache。

(6)将合并后的最终结果返回给客户端。

## 5. HDFS 和 HBase 各自使用场景

首先一点需要明白:Hbase 是基于 HDFS 来存储的。

HDFS:

\1. 一次性写入,多次读取。

\2. 保证数据的一致性。

\3. 主要是可以部署在许多廉价机器中,通过多副本提高可靠性,提供了容错 和恢复机制。

HBase:

\1. 瞬间写入量很大,数据库不好支撑或需要很高成本支撑的场景。

\2. 数据需要长久保存,且量会持久增长到比较大的场景。

\3. HBase 不适用与有 join,多级索引,表关系复杂的数据模型。

\4. 大数据量(100s TB 级数据)且有快速随机访问的需求。如:淘宝的交易 历史记录。数据量巨大无容置疑,面向普通用户的请求必然要即时响应。

\5. 业务场景简单,不需要关系数据库中很多特性(例如交叉列、交叉表,事

务,连接等等)。

## 6. Hbase 的存储结构

Hbase 中的每张表都通过行键(rowkey)按照一定的范围被分割成多个子表 (HRegion),默认一个 HRegion 超过 256M 就要被分割成两个,由HRegionServer 管理,管理哪些 HRegion 由 Hmaster 分配。 HRegion 存取一个子表时,会创 建一个 HRegion 对象,然后对表的每个列族(Column Family)创建一个 store 实 例, 每个 store 都会有 0 个或多个 StoreFile 与之对应,每个 StoreFile 都会 对应一个 HFile,HFile 就是实际的存储文件,一个 HRegion 还拥有一个 MemStore 实例。

## 7. 热点现象(数据倾斜)怎么产生的,以及解决方法有哪些

**热点现象**:

某个小的时段内,对HBase的读写请求集中到极少数的Region上,导致这些region 所在的 RegionServer 处理请求量骤增,负载量明显偏大,而其他的 RgionServer 明显空闲。

**热点现象出现的原因**:

HBase 中的行是按照 rowkey 的字典顺序排序的,这种设计优化了 scan 操作,可 以将相关的行以及会被一起读取的行存取在临近位置,便于 scan。然而糟糕的 rowkey 设计是热点的源头。 热点发生在大量的 client 直接访问集群的一个或极少数个节点(访问可能是读, 写或者其他操作)。大量访问会使热点 region 所在的单个机器超出自身承受能力, 引起性能下降甚至 region 不可用,这也会影响同一个 RegionServer 上的其他 region,由于主机无法服务其他 region 的请求。

**热点现象解决办法**:

为了避免写热点,设计 rowkey 使得不同行在同一个 region,但是在更多数据情 况下,数据应该被写入集群的多个 region,而不是一个。常见的方法有以下这些:

​ **加盐**:在 rowkey 的前面增加随机数,使得它和之前的 rowkey 的开头不同。分配 的前缀种类数量应该和你想使用数据分散到不同的 region 的数量一致。加盐之后 的 rowkey 就会根据随机生成的前缀分散到各个 region 上,以避免热点。

​ **哈希**:哈希可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈 希可以让客户端重构完整的 rowkey,可以使用 get 操作准确获取某一个行数据

​ **反转**:第三种防止热点的方法时反转固定长度或者数字格式的 rowkey。这样可 以使得 rowkey 中经常改变的部分(最没有意义的部分)放在前面。这样可以有 效的随机 rowkey,但是牺牲了 rowkey 的有序性。反转 rowkey 的例子以手机号 为 rowkey,可以将手机号反转后的字符串作为 rowkey,这样的就避免了以手机 号那样比较固定开头导致热点问题

​ **时间戳反转**:一个常见的数据处理问题是快速获取数据的最近版本,使用反转的 时间戳作为 rowkey 的一部分对这个问题十分有用,可以用 Long.Max_Value - timestamp 追加到 key 的末尾,例如[key][reverse_timestamp],[key]的最新值可以 通过 scan [key]获得[key]的第一条记录,因为 HBase 中 rowkey 是有序的,第一条 记录是最后录入的数据。

​ \1. 比如需要保存一个用户的操作记录,按照操作时间倒序排序,在设计 rowkey 的时候,可以这样设计[userId 反转] [Long.Max_Value - timestamp],在查询用户的所有操作记录数据的时候, 直接指定反转后的 userId,startRow 是[userId 反 转][000000000000],stopRow 是[userId 反转][Long.Max_Value - timestamp]

​ \2. 如果需要查询某段时间的操作记录,startRow 是[user 反 转][Long.Max_Value - 起始时间],stopRow 是[userId 反转][Long.Max_Value 结束时间]

​ **HBase** **建表预分区**:创建 HBase 表时,就预先根据可能的 RowKey 划分出多个 region 而不是 默认的一个,从而可以将后续的读写操作负载均衡到不同的 region 上,避免热点现象。

## 8. HBase 的 rowkey 设计原则

(1)**长度原则**:

100 字节以内,8 的倍数最好,可能的情况下越短越好。因为 HFile 是按照 keyvalue 存储的,过长的 rowkey 会影响存储效率;其次,过长的 rowkey 在 memstore 中较大,影响缓冲效果,降低检索效率。最后,操作系统大多为 64 位,8 的倍数,充分利用操作系统的最佳性能。

(2)**散列原则**:

如果Rowkey是按时间戳的方式递增,不要将时间放在二进制码的前面,建议将Rowkey的高位作为散列字段,由程序循环生成,低位放时间字段,这样将提高数据均衡分布在每个Regionserver实现负载均衡的几率。如果没有散列字段,首字段直接是时间信息将产生所有新数据都在一个 RegionServer上堆积的热点现象,这样在做数据检索的时候负载将会集中在个别RegionServer,降低查询效率。

(3)**唯一原则**:

分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能 会被访问 的数据放到一块。

## 9. HBase 的列簇设计

**原则**:

在合理范围内能尽量少的减少列簇就尽量减少列簇,因为列簇是共享 region 的,每个列簇数据相差太大导致查询效率低下。

**最优**:

将所有相关性很强的 key-value 都放在同一个列簇下,这样既能做到查询 效率最高,也能保持尽可能少的访问不同的磁盘文件。以用户信息为例,可以将 必须的基本信息存放在一个列族,而一些附加的额外信息可以放在另一列族。

## 10. HBase 中 compact 用途是什么,什么时候触发,分为哪两种,有什么区别

在 hbase 中每当有 memstore 数据 flush 到磁盘之后,就形成一个 storefile, 当 storeFile 的数量达到一定程度后,就需要将 storefile 文件来进行 compaction 操作。

**Compact** **的作用**:

\1. 合并文件

\2. 清除过期,多余版本的数据

\3. 提高读写数据的效率

**HBase** **中实现了两种** **compaction** **的方式:****minor and major**。

这两种 compaction 方式的区别是:

\1. Minor 操作只用来做部分文件的合并操作以及包括 minVersion=0 并且 设置 ttl 的过期版本清理,不做任何删除数据、多版本数据的清理工作。

\2. Major 操作是对 Region 下的 HStore 下的所有 StoreFile 执行合并操 作,最终的结果是整理合并出一个文件。

## 11. HBase优化

**高可用**

在HBase中Hmaster负责监控RegionServer的生命周期,均衡RegionServer的负载,如果Hmaster挂掉了,那么整个HBase集群将陷入不健康的状态,并且此时的工作状态并不会维持太久。所以,HBase支持对Hmaster的高可用

**内存优化**

HBase操作过程中需要大量的内存开销,毕竟Table是可以缓存在内存中的,一般会分配整个可用内存的70%给HBase的Java堆。但是不建议分配非常大的堆内存,因为GC过程持续太久会导致RegionServer处于长期不可用状态,一般16~48G内存就可以了,如果因为框架占用内存过高导致系统内存不足,框架一样会被系统服务拖死。

**配置优化**

(1)开启HDFS追加同步,可以优秀的配合HBase的数据同步和持久化。默认值为true。

```shell

dfs.support.append

```

(2)HBase一般都会同一时间操作大量的文件,根据集群的数量和规模以及数据动作,设置为4096或者更高。默认值:4096。

```

fs.datanode.max.transfer.threads

```

(3)优化延迟高的数据操作的等待时间

如果对于某一次数据操作来讲,延迟非常高,socket需要等待更长的时间,建议把该值设置为更大的值(默认60000毫秒),以确保socket不会被timeout掉。

```

dfs.image.transfer.timeout

```

(4)优化数据的写入效率

开启这两个数据可以大大提高文件的写入效率,减少写入时间。第一个属性值修改为true,第二个属性值修改为:org.apache.bigdata.io.compress.GzipCodec或者其他压缩方式。

```

mapreduce.map.output.compress mapreduce.map.output.compress.codec

```

(5)优化HStore文件大小

默认值10GB,如果需要运行HBase的MR任务,可以减小此值,因为一个region对应一个map任务,如果单个region过大,会导致map任务执行时间过长。该值的意思就是,如果HFile的大小达到这个数值,则这个region会被切分为两个Hfile。

```

hbase.hregion.max.filesize

```

(6)优化HBase客户端缓存

用于指定HBase客户端缓存,增大该值可以减少RPC调用次数,但是会消耗更多内存,反之则反之。一般我们需要设定一定的缓存大小,以达到减少RPC次数的目的。

```

hbase.client.write.buffer

```

( 7 ) 指定scan.next扫描HBase所获取的行数

用于指定scan.next方法获取的默认行数,值越大,消耗内存越大。

```

hbase.client.scanner.caching

```

(8)flush、compact、split机制

当MemStore达到阈值,将Memstore中的数据Flush进Storefile;compact机制则是把flush出来的小文件合并成大的Storefile文件。split则是当Region达到阈值,会把过大的Region一分为二。

```

128M就是Memstore的默认阈值

hbase.hregion.memstore.flush.size:134217728

```

当MemStore使用内存总量达到HBase.regionserver.global.memstore.upperLimit指定值时,将会有多个MemStores flush到文件中,MemStore flush 顺序是按照大小降序执行的,直到刷新到MemStore使用内存略小于lowerLimit

```

hbase.regionserver.global.memstore.upperLimit:0.4 hbase.regionserver.global.memstore.lowerLimit:0.38

```

## 12. Phoenix二级索引

在Hbase中,按字典顺序排序的rowkey是一级索引。不通过rowkey来查询数据时需要过滤器来扫描整张表。通过二级索引,这样的场景也可以轻松定位到数据。

二级索引的原理通常是在写入时针对某个字段和rowkey进行绑定,查询时先根据这个字段查询到rowkey,然后根据rowkey查询数据,二级索引也可以理解为查询数据时多次使用索引的情况。

**索引**

**全局索引**

全局索引适用于多读少写的场景,在写操作上会给性能带来极大的开销,因为所有的更新和写操作

(DELETE,UPSERT VALUES和UPSERT SELECT)都会引起索引的更新,在读数据时,Phoenix将通过索引表来达到快速查询的目的。

**本地索引**

本地索引适用于写多读少的场景,当使用本地索引的时候即使查询的所有字段都不在索引字段中时也会

用到索引进行查询,Phoneix在查询时会自动选择是否使用本地索引。

**覆盖索引**

只需要通过索引就能返回所要查询的数据,所以索引的列必须包含所需查询的列。

**函数索引**

索引不局限于列,可以合适任意的表达式来创建索引,当在查询时用到了这些表达式时就直接返回表达

式结果

**索引优化**

(1)根据主表的更新来确定更新索引表的线程数

```

index.builder.threads.max:(默认值:10)

```

(2)builder线程池中线程的存活时间

```

index.builder.threads.keepalivetime:(默认值:60)

```

(3)更新索引表时所能使用的线程数(即同时能更新多少张索引表),其数量最好与索引表的数量一致

```

index.write.threads.max:(默认值:10)

```

(4) 更新索引表的线程所能存活的时间

```

index.write.threads.keepalivetime(默认值:60)

```

(5) 每张索引表所能使用的线程(即在一张索引表中同时可以有多少线程对其进行写入更新),增加此值可以提高更新索引的并发量

```

hbase.htable.threads.max(默认值:2147483647)

```

(6) 索引表上更新索引的线程的存活时间

```

hbase.htable.threads.keepalivetime(默认值:60)

```

(7) 允许缓存的索引表的数量

增加此值,可以在更新索引表时不用每次都去重复的创建htable,由于是缓存在内存中,所以其值越大,其需要的内存越多

```

index.tablefactoy.cache.size(默认值:10)

```

## 13.**数据 flush 过程**

1)

当 MemStore 数据达到阈值(默认是 128M,老版本是 64M),将数据刷到硬盘,将内存中的 数据删除,同时删除 HLog 中的历史数据;

2) 并将数据存储到 HDFS 中;

3) 在 HLog 中做标记点。

## 14.**数据合并过程**

1)

当数据块达到 3 块,Hmaster 触发合并操作,Region 将数据块加载到本地,进行合并;

2)

当合并的数据超过 256M,进行拆分,将拆分后的 Region 分配给不同的 HregionServer 管理;

3)

当 HregionServer 宕 机 后 , 将 HregionServer 上 的 hlog 拆 分 , 然 后 分 配 给 不 同 的 HregionServer 加载,修改.META.;

4) 注意:HLog 会同步到 HDFS。

# 5___Zookeeper

## 0. Zookeeper基础命令

```shell

#启动服务 | 服务状态 | 停止服务

zkServer.sh start | status | stop

```

## 1. Zookeeper介绍

Zookeeper从设计模式角度来理解,是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生了变化,Zookeeper就负责通知已经在Zookeeper上注册的那些观察者做出相应的反应。

## 2. Zookeeper特点

\1. 集群中只要有半数以上节点存活,Zookeeper集群就能正常提供服务。所以这就是选举机制的奇数原则(Zookeeper适合安装奇数台服务)。

\2. 一个领导者Leaders和多个跟随者Follower组成的集群。

## 3. Zookeeper的选举机制

**新集群选举**

假设有五台服务器组成的Zookeeper集群,从Service1到Service5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。假设这些服务器依序启动,来看看会发生什么。

\1. Service1启动,发起一次选举。服务器1投自己一票。此时服务器1票数一票,不够半数以上(3票),选举无法完成,服务器1状态保持为LOOKING;

\2. Service2启动,再发起一次选举。Service1和Service2分别投自己一票并交换选票信息:此时Service1发现Service2的ID比自己目前投票推举的(Service1)大,更改选票为推举Service2。此时Service1票数0票,Service2票数2票,没有半数以上结果,选举无法完成,Service1,Service2状态保持LOOKING。

\3. Service3启动,发起一次选举。此时Service1和Service2都会更改选票为Service3。此次投票结果:Service1为0票,Service2为0票,Service3为3票。此时Service3的票数已经超过半数,Service3当选Leader。Service1与Service2更改状态为FOLLOWING,Service3更改状态为LEADING。

\4. Service4启动,发起一次选举。此时Service1,Service2,Service3已经不是LOOKING状态,不会更改选票信息。交换选票信息结果:Service3为3票,Service4为1票。此时Service4服从多数,更改选票信息为Service3,并更改状态为FOLLOWING。

\5. Service5启动,同理第4步一样Service5当FOLLOWING。

**非全新集群选举**

对于运行正常的zookeeper集群,中途有机器down掉,需要重新选举时,选举过程就需要加入数据ID、服务器ID、和逻辑时钟。

\1. 逻辑时钟:这个值从0开始,每次选举必须一致。小的选举结果被忽略,重新投票(除去选举次数不完整的服务器)。

\2. 数据id:数据新的version大,数据每次更新都会更新version。数据id大的胜出(选出数据最新的服务器)。

\3. 服务器id:即myid。数据id相同的情况下,服务器id大的胜出(数据相同的情况下,选择服务器id最大,即权重最大的服务器)。

# 6___Flink

## 1、Flink 核心特点

**批流一体**

所有的数据都天然带有时间的概念,必然发生在某一个时间点。把事件按照时间顺序排列起来,就形成了一个事件流,也叫作数据流。**无界数据**是持续产生的数据,所以必须持续地处理无界数据流。**有界数**据,就是在一个确定的时间范围内的数据流,有开始有结束,一旦确定了就不会再改变。

**可靠的容错能力**

集群级容错

​ 集群管理器集成(Hadoop YARN、Mesos或Kubernetes)

​ 高可用性设置(HA模式基于ApacheZooKeeper)

应用级容错( Checkpoint)

​ 一致性(其本身支持Exactly-Once 语义)

​ 轻量级(检查点的执行异步和增量检查点)

高吞吐、低延迟

## **2、 Flink 中的 Time 有哪几种**

在 flink 中被划分为事件时间,提取时间,处理时间三种。

\1. 如果以 EventTime 为基准来定义时间窗口那将形成 EventTimeWindow,要 求消息本身就应该携带 EventTime。

\2. 如果以 IngesingtTime 为基准来定义时间窗口那将形成 IngestingTimeWindow,以 source 的 systemTime 为准。

\3. 如果以 ProcessingTime 基准来定义时间窗口那将形ProcessingTimeWindow,以 operator 的 systemTime 为准。

## **3、 对于迟到数据是怎么处理的**

Flink 中 WaterMark 和 Window 机制解决了流式数据的乱序问题,对于因为延 迟而顺序有误的数据,可以根据 eventTime 进行业务处理,对于延迟的数据 Flink 也有自己的解决办法,主要的办法是给定一个允许延迟的时间,在该时间范围内 仍可以接受处理延迟数据 设置允许延迟的时间是通过 allowedLateness(lateness: Time)设置 保存延迟数据则是通过 sideOutputLateData(outputTag: OutputTag[T])保存获取延迟数据是通过 DataStream.getSideOutput(tag: OutputTag[X])获取。

Flink 中极其重要的 Time 与 Window 详细解析:

https://mp.weixin.qq.com/s?__biz=Mzg2MzU2MDYzOA==&mid=2247483905&idx=1&sn=11434f3788a8a78418d21bacddcedbf7&chksm=ce77f4d0f9007dc6c3701f4f0306185cf5bfbec44b6f7944d570ccd0df07dd6f12902769ef31&token=1679639512&lang=zh_CN

## **4、 Flink 的运行必须依赖 Hadoop 组件吗**

Flink 可以完全独立于 Hadoop,在不依赖 Hadoop 组件下运行。但是做为大数据 的基础设施,Hadoop 体系是任何大数据框架都绕不过去的。Flink 可以集成众多 Hadooop 组件,例如 Yarn、Hbase、HDFS 等等。例如,Flink 可以和 Yarn 集成做 资源调度,也可以读写 HDFS,或者利用 HDFS 做检查点。

## **5、Flink 集群有哪些角色,各自有什么作用**

有以下三个角色:

**JobManager** **处理器:**

也称之为 Master,用于协调分布式执行,它们用来调度 task,协调检查点,协调 失败时恢复等。Flink 运行时至少存在一个 master 处理器,如果配置高可用模式则会存在多个 master 处理器,它们其中有一个是 leader,而其他的都是 standby。

**TaskManager** **处理器:**

也称之为 Worker,用于执行一个 dataflow 的 task(或者特殊的 subtask)、数据缓

冲和 data stream 的交换,Flink 运行时至少会存在一个 worker 处理器。

**Clint** **客户端:**

Client 是 Flink 程序提交的客户端,当用户提交一个 Flink 程序时,会首先创建一

个 Client,该 Client 首先会对用户提交的 Flink 程序进行预处理,并提交到 Flink

集群中处理,所以 Client 需要从用户提交的 Flink 程序配置中获取 JobManager 的

地址,并建立到 JobManager 的连接,将 Flink Job 提交给 JobManager。

## **6、Flink 资源管理中 Task Slot 的概念**

在 Flink 中每个 TaskManager 是一个 JVM 的进程, 可以在不同的线程中执行一个 或多个子任务。 为了控制一个 worker 能接收多少个 task。worker 通过 task slot(任务槽)来进行 控制(一个 worker 至少有一个 task slot)。

## **7、Flink 的重启策略了解吗**

Flink 支持不同的重启策略,这些重启策略控制着 job 失败后如何重启:

**固定延迟重启策略**

固定延迟重启策略会尝试一个给定的次数来重启 Job,如果超过了最大的重启次 数,Job 最终将失败。在连续的两次重启尝试之间,重启策略会等待一个固定的 时间。

**失败率重启策略**

失败率重启策略在 Job 失败后会重启,但是超过失败率后,Job 会最终被认定失 败。在两个连续的重启尝试之间,重启策略会等待一个固定的时间。

**无重启策略**

Job 直接失败,不会尝试进行重启。

## **8、Flink 是如何保证 Exactly-once 语义的**

Flink 通过实现**两阶段提交**和状态保存来实现端到端的一致性语义。分为以下几

个步骤:

开始事务(beginTransaction)创建一个临时文件夹,来写把数据写入到这个文件 夹里面

预提交(preCommit)将内存中缓存的数据写入文件并关闭

正式提交(commit)将之前写完的临时文件放入目标目录下。这代表着最终的 数据会有一些延迟

丢弃(abort)丢弃临时文件 若失败发生在预提交成功后,正式提交前。可以根据状态来提交预提交的数据, 也可删除预提交的数据。

## **9、 Flink 是如何处理反压的**

Flink 内部是基于 producer-consumer 模型来进行消息传递的,Flink 的反压设计 也是基于这个模型。Flink 使用了高效有界的分布式阻塞队列,就像 Java 通用 的阻塞队列(BlockingQueue)一样。下游消费者消费变慢,上游就会受到阻塞。

## **10.、Flink 中的状态存储**

Flink 在做计算的过程中经常需要存储中间状态,来避免数据丢失和状态恢复。 选择的状态存储策略不同,会影响状态持久化如何和 checkpoint 交互。Flink 提 供了三种状态存储方式:MemoryStateBackend 、 FsStateBackend RocksDBStateBackend。

## **11、Flink 是如何支持批流一体的**

**Flink** **的开发者认为批处理是流处理的一种特殊情况。** 批处理是有限的流处理。Flink **使用一个引擎支持了** **DataSet API** **和** **DataStream** **API**。

## **12、Flink 的内存管理是如何做的**

Flink 并不是将大量对象存在堆上,而是将对象都序列化到一个预分配的内存块 上。此外,Flink 大量的使用了堆外内存。如果需要处理的数据超出了内存限制, 则会将部分数据存储到硬盘上。Flink 为了直接操作二进制数据实现了自己的序 列化框架。

## 13、简单介绍一下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 及多种图计算算法实现。

## 14、**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 则使用两阶段提交协议来解决这个问题。

## 15、**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。

## 16、**Flink 的分布式缓存有什么作用?如何使用**

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

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

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

## 17、**Flink 中的广播变量,使用广播变量需要注意什么事项**

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

## 18、**Flink 中对窗口的支持包括哪几种?说说他们的使用场景**

![image-20220530102023447](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220530102023447.png)

1) Tumbling Time Window

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

2) Sliding Time Window

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

3) Tumbling Count Window

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

4) Session Window

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

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

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

1) 窗口操作

2) 使用了 KV 操作的函数

3) 继承了 CheckpointedFunction 的函数

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

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

1) MemoryStateBackend

2) FsStateBackend

3) RocksDBStateBackend

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

## 20、Flink 中的时间种类有哪些?各自介绍一下?

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

## 21、**WaterMark 是什么?是用来解决什么问题?如何生成水** **印?水印的原理是什么?**

Watermark 是 Apache Flink 为了处理 EventTime 窗口计算提出的一种机制,本质上也是一种 时间戳。watermark 是用于处理乱序事件的,处理乱序事件通常用 watermark 机制结合 window 来实现。

## 22、**Flink 的 table 和 SQL 熟 悉 吗 ? Table API 和 SQL 中** **TableEnvironment 这个类有什么作用**

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

A) 在内部 catalog 中注册表

B) 注册外部 catalog

C) 执行 SQL 查询

D) 注册用户定义(标量,表或聚合)函数

E) 将 DataStream 或 DataSet 转换为表

F) 持有对 ExecutionEnvironment 或 StreamExecutionEnvironment 的引用

## 23、**Flink 如何实现 SQL 解析的呢?**

StreamSQL API 的执行原理如下:

1、用户使用对外提供 Stream SQL 的语法开发业务应用;

2、用 calcite 对 StreamSQL 进行语法检验,语法检验通过后,转换成 calcite 的逻辑树节 点;最终形成 calcite 的逻辑计划;

3、采用 Flink 自定义的优化规则和 calcite 火山模型、启发式模型共同对逻辑树进行优化, 生成最优的 Flink 物理计划;

4、对物理计划采用 janino codegen 生成代码,生成用低阶 API DataStream 描述的流应用, 提交到 Flink 平台执行。

## 24、**Flink 是如何做到批处理与流处理统一的?**

Flink 设计者认为:有限流处理是无限流处理的一种特殊情况,它只不过在某个时间点停止 而 已 。 Flink 通 过 一 个 底 层 引 擎 同 时 支 持 流 处 理 和 批 处 理 。

## 25、**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 也放慢从外部 消息源读取消息的速度。一旦瓶颈解除,网络链路畅通,则背压也会自然而然的解除。

## 26、**Flink 的容错机制**

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

# 7___Flume

## 0、Flume脚本案例

```shell

# fulme到kafka数据迁移

users.sources=usersSource

users.channels=usersChannel

users.sinks=usersSink

users.sources.usersSource.type=spooldir

users.sources.usersSource.spoolDir=/opt/kb16tmp/flumelogfile/users

users.sources.usersSource.deserializer=LINE

users.sources.usersSource.deserializer.maxLineLength=320000

users.sources.usersSource.includePattern=users_[0-9]{4}-[0-9]{2}-[0-9]{2}.csv

users.sources.usersSource.interceptors=head_filter

users.sources.usersSource.interceptors.head_filter.type=regex_filter

users.sources.usersSource.interceptors.head_filter.regex=^user_id*

users.sources.usersSource.interceptors.head_filter.excludeEvents=true

users.channels.usersChannel.type=file

users.channels.usersChannel.checkpointDir=/opt/kb16tmp/checkpoint/users

users.channels.usersChannel.dataDirs=/opt/kb16tmp/datadir/users

users.sinks.usersSink.type=org.apache.flume.sink.kafka.KafkaSink

users.sinks.usersSink.batchSize=640

users.sinks.usersSink.brokerList=192.168.245.168:9092

users.sinks.usersSink.topic=users

users.sources.usersSource.channels=usersChannel

users.sinks.usersSink.channel=usersChannel

```

```shell

# flume同时到kafka和hdfs迁移数据

train.sources=trainSource

train.channels=fileChannel memoryChannel

train.sinks=kafkaSink hdfsSink

train.sources.trainSource.type=spooldir

train.sources.trainSource.spoolDir=/opt/kb16tmp/flumelogfile/train

train.sources.trainSource.deserializer=LINE

train.sources.trainSource.deserializer.maxLineLength=320000

train.sources.trainSource.includePattern=train_[0-9]{4}-[0-9]{2}-[0-9]{2}.csv

train.sources.trainSource.interceptors=head_filter

train.sources.trainSource.interceptors.head_filter.type=regex_filter

train.sources.trainSource.interceptors.head_filter.regex=^user*

train.sources.trainSource.interceptors.head_filter.excludeEvents=true

train.channels.fileChannel.type=file

train.channels.fileChannel.checkpointDir=/opt/kb16tmp/checkpoint/train

train.channels.fileChannel.dataDirs=/opt/kb16tmp/datadir/train

train.channels.memoryChannel.type=memory

train.channels.memoryChannel.capacity=64000

train.channels.memoryChannel.transactionCapacity=16000

train.sinks.kafkaSink.type=org.apache.flume.sink.kafka.KafkaSink

train.sinks.kafkaSink.batchSize=640

train.sinks.kafkaSink.brokerList=192.168.245.168:9092

train.sinks.kafkaSink.topic=train

train.sinks.hdfsSink.type=hdfs

train.sinks.hdfsSink.hdfs.fileType=DataStream

train.sinks.hdfsSink.hdfs.filePrefix=train

train.sinks.hdfsSink.hdfs.fileSuffix=.csv

train.sinks.hdfsSink.hdfs.path=hdfs://192.168.245.168:9000/kb16file/train/%Y-%m-%d

train.sinks.hdfsSink.hdfs.useLocalTimeStamp=true

train.sinks.hdfsSink.hdfs.batchSize=640

train.sinks.hdfsSink.hdfs.rollCount=0

train.sinks.hdfsSink.hdfs.rollSize=64000000

train.sinks.hdfsSink.hdfs.rollInterval=30

train.sinks.hdfsSink.hdfs.minBlockReplicas=1

train.sources.trainSource.channels=fileChannel memoryChannel

train.sinks.kafkaSink.channel=fileChannel

train.sinks.hdfsSink.channel=memoryChannel

```

## 1、Flume组成,Put事务,Take事务

Flume组成,Put事务,Take事务

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

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

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

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

​Source到Channel是Put事务

​Channel到Sink是Take事务

## 2 、Flume拦截器

(1)拦截器注意事项

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

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

(2)自定义拦截器步骤

​ a)实现 Interceptor

​ b)重写四个方法

​ initialize 初始化

​ public Event intercept(Event event) 处理单个Event

​ public List<Event> intercept(List<Event> events) 处理多个Event,在这个方法中调用Event intercept(Event event)

​ close 方法

​ c)静态内部类,实现Interceptor.Builder

## **3** 、Flume采集数据会丢失吗(防止数据丢失的机制)

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

## **4**、 Flume内存

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

-Xmx与-Xms最好设置一致,减少内存抖动带来的性能影响,如果设置不一致容易导致频繁fullgc。

## **5** 、FileChannel优化

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

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

## 6、HDFSSink小文件处理

(1)HDFS存入大量小文件,有什么影响?

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

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

(2)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秒时会滚动生成正式文件

## 7、flume可以设置文件:

rollSize

默认值:1024,当临时文件达到该大小(单位:bytes)时,滚动成目标文件。

如果设置成0,则表示不根据临时文件大小来滚动文件。

rollCount

默认值:10,当events数据达到该数量时候,将临时文件滚动成目标文件,

如果设置成0,则表示不根据events数据来滚动文件。

round

默认值:false,是否启用时间上的"舍弃",类似于四舍五入,

如果启用,则会影响除了%t的其他所有时间表达式。

roundValue

默认值:1,时间上进行"舍弃"的值。

roundUnit

默认值:seconds,时间上进行"舍弃"的单位,包含:second,minute,hour

当设置了round、roundValue、roundUnit参数时,需要在sink指定的HDFS路径上指定按照时间生成的目录的格式,如有需求,每采集1小时就在HDFS目录上生成一个目录,里面存放这1小时内采集到的数据。

# 8___Hadoop

## 一、HDFS

### **1、 请说下 HDFS 读写流程**

#### HDFS 写流程 (上传)

1)client 客户端发送上传请求,通过 RPC 与 namenode 建立通信,namenode 检 查该用户是否有上传权限,以及上传的文件是否在 hdfs 对应的目录下重名,如 果这两者有任意一个不满足,则直接报错,如果两者都满足,则返回给客户端一 个可以上传的信息

2)client 根据文件的大小进行切分,默认 128M 一块,切分完成之后给 namenode 发送请求第一个 block 块上传到哪些服务器上

3)namenode 收到请求之后,根据网络拓扑和机架感知以及副本机制进行文件 分配,返回可用的 DataNode 的地址

注:Hadoop 在设计时考虑到数据的安全与高效, 数据文件默认在 HDFS 上存放三份, 存储策略为本地一份,同机架内其它某一节点上一份, 不同机架的某一节点上一份

4)客户端收到地址之后与服务器地址列表中的一个节点如 A 进行通信,本质上 就是 RPC 调用,建立 pipeline,A 收到请求后会继续调用 B,B 在调用 C,将整个 pipeline 建立完成,逐级返回 client

5)client 开始向 A 上发送第一个 block(先从磁盘读取数据然后放到本地内存缓 存),以 packet(数据包,64kb)为单位,A 收到一个 packet 就会发送给 B,然 后 B 发送给 C,A 每传完一个 packet 就会放入一个应答队列等待应答

6)数据被分割成一个个的 packet 数据包在 pipeline 上依次传输,在 pipeline 反 向传输中,逐个发送 ack(命令正确应答),最终由 pipeline 中第一个 DataNode 节点 A 将 pipelineack 发送给 Client

7)当一个 block 传输完成之后, Client 再次请求 NameNode 上传第二个 block ,namenode 重新选择三台 DataNode 给 client

#### HDFS读流程 (下载)

1)client 向 namenode 发送 RPC 请求。请求文件 block 的位置

2)namenode 收到请求之后会检查用户权限以及是否有这个文件,如果都符合, 则会视情况返回部分或全部的 block 列表,对于每个 block,NameNode 都会返 回含有该 block 副本的 DataNode 地址; 这些返回的 DN 地址,会按照集群拓 扑结构得出 DataNode 与客户端的距离,然后进行排序,排序两个规则:网络拓 扑结构中距离 Client 近的排靠前;心跳机制中超时汇报的 DN 状态为 STALE, 这样的排靠后

3)Client 选取排序靠前的 DataNode 来读取 block,如果客户端本身就是 DataNode,那么将从本地直接获取数据(短路读取特性)

4)底层上本质是建立 Socket Stream(FSDataInputStream),重复的调用父类 DataInputStream 的 read 方法,直到这个块上的数据读取完毕

5)当读完列表的 block 后,若文件读取还没有结束,客户端会继续向 NameNode 获取下一批的 block 列表

6)读取完一个 block 都会进行 checksum 验证,如果读取 DataNode 时出现错 误,客户端会通知 NameNode,然后再从下一个拥有该 block 副本的 DataNode 继续读

7)read 方法是并行的读取 block 信息,不是一块一块的读取;NameNode 只 是返回 Client 请求包含块的 DataNode 地址,并不是返回请求块的数据

8) 最终读取来所有的 block 会合并成一个完整的最终文件

### **2、简单说明HDFS中NameNode,DataNode的作用**

#### 1)NameNode

就是Master,它是一个管理者。也叫HDFS的元数据节点。集群中只能有一个Active的NameNode对外提供服务。

​ (1) 管理HDFS的名称空间(文件目录树);HDFS很方便的一点就是对于用户来说很友好,用户不考虑细节的话,看到的目录结构和我们使用Window和Linux文件系统很像。

​ (2) 管理数据块(Block)映射信息及副本信息;一个文件对应的块的名字以及块被存储在哪里,以及每一个文件备份多少都是由NameNode来管理的。

​ (3) 处理客户端的读写请求。

#### 2)DataNode

就是worker。实际存储数据块的节点,NameNode下达命令,DataNode执行实际的操作。

​ (1) 存储实际的数据块。

​ (2) 执行数据块的读写操作。

### **3、Secondary NameNode 的作用,NameNode的启动过程**

Secondary NameNode 有两个作用。一个是镜像备份,另一个是日志与镜像的定期合并,即合并 NameNode 的 edit logs 到 fsimage 文件中。

第一阶段:NameNode启动

​ 1)第一次启动NameNode格式化后,创建fsimage和edits文件。如果不是第一次启动,直接加载编辑日志和镜像文件到内存。

​ 2)客户端对元数据进行增删改的请求。

​ 3)NameNode记录操作日志,更新滚动日志。

​ 4)NameNode在内存中对数据进行增删改查。

第二阶段:Secondary NameNode工作

​ 1)Secondary NameNode 询问NameNode是否需要checkpoint。直接带回NameNode是否检查结果。

​ 2)Secondary NameNode 请求执行 checkpoint

​ 3)NameNode 滚动正在写的edits日志。

​ 4)将滚动前的编辑日志和镜像文件拷贝到Secondary NameNode。

​ 5)Secondary NameNode 加载编辑日志和镜像文件到内存,并合并。

​ 6)生成新的镜像文件 fsimage.chkpoint。

​ 7)拷贝 fsimage.chkpoint 到 NameNode。

​ 8)NameNode将 fsimage.chkpoint 重新命名成fsimage 。

### **4、集群的安全模式**

#### 1)进入安全模失的情况

集群启动时必定会进入安全模式:

​ NameNode启动时,首先将映像文件(fsimage)载入内存,并执行编辑日志(edits)中的各项操作。一旦在内存中成功建立文件系统元数据的映像,则创建一个新的fsimage文件和一个空的编辑日志。此时,NameNode开始监听DataNode请求。但是此刻,NameNode运行在安全模式,即NameNode的文件系统对于客户端来说是只读的。

​ 系统中的数据块的位置并不是由NameNode维护的,而是以块列表的形式存储在DataNode中。在系统的正常操作期间,NameNode会在内存中保留所有块位置的映射信息。在安全模式下,各个DataNode会向NameNode发送最新的快列表信息,NameNode了解到足够多的块位置信息之后,即可高效运行文件系统。

​ 如果满足“最小副本条件”,NameNode会在30秒钟之后就退出安全模式,所谓的最小副本条件就是值在整个文件系统的99.9%的块满足最小副本级别(默认值:dfs.replication.min=1)。在启动一个刚刚格式化的HDFS集群时,因为系统中还没有任何块,所以NameNode不会进入安全模式。

#### 2)异常情况下导致的安全模式

**原因**:block确实有缺失,当namenode发现集群中的block丢失数量达到一个阈值时,namenode就进入安全模式状态,不再接收客户端的数据更新请求。

**解决方法:**

​ 1、调低阈值:

​ hdfs-site.xml中:

​ <name>dfs.namenode.safemode.threshold-pct</name>

​ <value>0.999f</value>

​ 2、强制离开:

​ hdfs dfsadmin -safemode leave

​ 3、重新格式化集群。

​ 4、修复损坏的块文件。

**基本语法:**

​ 集群处于安全模式,不能执行重要操作(写操作)。集群启动完成后,自动退出安全模式。

​ 1) bin/hdfs dfsadmin -safemode get (功能:查看安全模式状态)

​ 2) bin/hdfs dfsadmin -safemode enter (功能:进入安全模式状态)

​ 3) bin/hdfs dfsadmin -safemode leave (功能:离开安全模式状态)

​ 4) bin/hdfs dfsadmin -safemode wait (功能:等待安全模式状态)

### **5、为什么HDFS不适合存小文件**

​ HDFS天生就是为了存储大文件而生的,一个块的元数据大小大概在150字节左右,存储一个小文件就要占用NameNode 150字节的内存,如果存储大量的小文件很快就将NameNode 内存耗尽,而整个集群存储的数据量很小,失去了HDFS的意义,同时也会影响NameNode的寻址时间,导致寻址时间过长。

​ 可以将数据合并上传,或者将文件append形式追加在HDFS文件末尾。

### **6、HDFS支持的存储格式和压缩算法**

#### 1.存储格式

##### 1)SequenceFile

以二进制键值对的形式存储数据,支持三种记录存储方式。

​ **无压缩:io效率较差,相比压缩,不压缩的情况下没有什么优势。

​ **记录级压缩:对每条记录都压缩,这种压缩效率比较一般。

​ **块级压缩:这里的块不同于HDFS中的块的概念,这种方式会将达到指定块大小的二进制数据压缩为一个块。

##### 2)Avro

将数据定义和数据一起存储在一条消息中,其中数据定义以JSON格式存储,数据以二进制格式存储。Avro标记用于将大型数据集分割成适合MapReduce处理的子集。

##### 3)RCFile

以列格式保存每个行组数据。它不是存储第一行然后是第二行,而是存储所有行上的第一列,然后是所有行的第二列..

##### 4)Parquet

是Hadoop的一种列存储格式,提供了高效的编码和压缩方案。

#### 2、压缩算法

##### 1)Gzip压缩

​ **优点**:压缩率比较高,而且压缩和解压速度也比较快;Hadoop本身支持,在应用中处理gzip格式的文件就和直接处理文本一样;大部分linux系统都自带gzip命令,使用方便。

​ **缺点**:不支持split。

##### 2)Bzip2压缩

​ **优点**:支持split;具有很高的压缩率,比gzip压缩率还高;Hadoop本身支持;在linux系统下自带bzip2命令,使用方便。

​ **缺点**:压缩,解压速度慢,不支持native。

##### 3)Lzo压缩

​ **优点:**压缩和解压速度比较快,合理的压缩率;支持split,是Hadoop中最流行的压缩格式;可以在linux系统下安装lzop命令,使用方便。

​ **缺点**:压缩率比gzip要低一点;Hadoop本身不支持,需要安装;在应用中对 Lzo格式的文件需要做一些特殊处理(为了支持split需要建索引,还需要指定 inputformat为Lzo格式)

##### 4)Snappy压缩

​ **优点**:告诉压缩速度和合理的压缩率。

​ **缺点**:不支持 split ;压缩率比 gzip 要低;Hadoop 本身不支持,需要安装。

### **7、说一下HDFS的可靠性策略**

#### 1.文件完整性

1)在文件建立时,每个数据块都产生校验和,校验和会保存在 .meta 文件内。

2)客户端获取数据时可以检查校验和是否相同,从而发现数据块是否损坏。

3)如果正在读取的数据块损坏,则可以继续读取其他副本。NameNode标记该块已经损坏,然后复制block达到预期设置的文件备份数。

4)DataNode在其文件创建后三周验证其checksum。

#### 2.网络或者机器失效时

1)副本冗余。

2)机架感知策略(副本放置策略)。

3)心跳机制策略。

#### 3.NameNode挂掉

1)主备切换(高可用)。

2)镜像文件和操作日志磁盘存储。

3)镜像文件和操作日志可以存储多份,多磁盘存储。

#### 4.其他保障可靠性机制

1)快照。保存了系统某一时刻的影像,可以还原到该时候。

2)回收站机制。

3)安全模式。

### **8、HDFS的优缺点**

#### 1)HDFS优点

**高容错性:**数据自动保存多个副本,副本丢失后,会自动恢复。

**适合批处理**:移动计算而非数据、数据位置暴露给计算框架。

**适合大数据处理:**GB、TB、甚至PB级数据、百万规模以上的文件数量,1000以上的节点规模。

**流式文件访问:**一次性写入,多次读取;保证数据一致性。

**可构建在廉价机器上:**通过多副本提供可靠性,提供了容错和恢复机制。

#### 2)HDFS缺点

**不适合低延迟数据访问:**比如毫秒级、低延迟与高吞吐率。

**不适合小文件存取:**占用NameNode大量内存,寻道时间超过读取时间。

**不适合并发写入、文件随机修改:**一个文件只能有一个写入者,仅支持append。

### **9、HDFS 在读取文件的时候,如果其中一个块突然损坏了怎么办**

客户端读取完 DataNode 上的块之后会进行 checksum 验证,也就是把客户端读 取到本地的块与 HDFS 上的原始块进行校验,如果发现校验结果不一致,客户端 会通知 NameNode,然后再从下一个拥有该 block 副本的 DataNode 继续读

### **10、HDFS 在上传文件的时候,如果其中一个 DataNode 突然挂掉了怎么办**

客户端上传文件时与DataNode建立pipeline管道,管道正向是客户端向DataNode 发送的数据包,管道反向是 DataNode 向客户端发送 ack 确认,也就是正确接收到数据包之后发送一个已确认接收到的应答,当 DataNode 突然挂掉了,客户端 接收不到这个 DataNode 发送的 ack 确认 ,客户端会通知 NameNode,NameNode 检查该块的副本与规定的不符, NameNode 会通知 DataNode 去复制副本,并将挂掉的 DataNode 作下线处理,不 再让它参与文件上传与下载。

### **11、 NameNode 在启动的时候会做哪些操作**

NameNode 数据存储在内存和本地磁盘,本地磁盘数据存储在 fsimage 镜像文件 和 edits 编辑日志文件

**首次启动 NameNode**

1、格式化文件系统,为了生成 fsimage 镜像文件

2、启动 NameNode

(1)读取 fsimage 文件,将文件内容加载进内存

(2)等待 DataNade 注册与发送 block report

3、启动 DataNode

(1)向 NameNode 注册

(2)发送 block report

(3)检查 fsimage 中记录的块的数量和 block report 中的块的总数是否相同

4、对文件系统进行操作(创建目录,上传文件,删除文件等)

(1)此时内存中已经有文件系统改变的信息,但是磁盘中没有文件系统改变的信息,此时 会将这些改变信息写入 edits 文件中,edits 文件中存储的是文件系统元数据改变的信息。

**第二次启动 NameNode**

1、读取 fsimage 和 edits 文件

2、将 fsimage 和 edits 文件合并成新的 fsimage 文件

3、创建新的 edits 文件,内容为空

4、启动 DataNode

### **12、Secondary NameNode 不能恢复 NameNode 的全部数据,那如何保证 NameNode 数据存储安全**

这个问题就要说 NameNode 的高可用了,即 **NameNode HA**

一个 NameNode 有单点故障的问题,那就配置双 NameNode,配置有两个关键点, 一是必须要保证这两个 NN 的元数据信息必须要同步的,二是一个 NN 挂掉之后 另一个要立马补上。

\1. 元数据信息同步在 HA 方案中采用的是“共享存储”。每次写文件时,需要将日志同 步写入共享存储,这个步骤成功才能认定写文件成功。然后备份节点定期从共享存储同 步日志,以便进行主备切换。

\2. 监控 NameNode 状态采用 zookeeper,两个 NameNode 节点的状态存放在 ZK 中,另外两个 NameNode 节点 分别有一个进程监控程序,实施读取 ZK 中有 NameNode 的状态,来判断当前的 NameNode 是不是已 经 down 机。如果 standby 的 NameNode 节点的 ZKFC 发现主节点已经挂掉,那么就会强制给原 本的 active NameNode 节点发送强制关闭请求,之后将备用的 NameNode 设置为 active。

​ **如果面试官再问 HA 中的 共享存储 是怎么实现的知道吗?**

可以进行解释下:NameNode 共享存储方案有很多,比如 Linux HA, VMware FT, QJM 等,目 前社区已经把由 Clouderea 公司实现的基于 QJM(Quorum Journal Manager)的方案合并 到 HDFS 的 trunk 之中并且作为**默认的共享存储**实现 基于 QJM 的共享存储系统**主要用于保存 EditLog,并不保存 FSImage 文件**。FSImage 文件 还是在 NameNode 的本地磁盘上。QJM 共享存储的基本思想来自于 Paxos 算法,采用多个 称为 JournalNode 的节点组成的 JournalNode 集群来存储 EditLog。每个 JournalNode 保存同样的 EditLog 副本。每次 NameNode 写 EditLog 的时候,除了向本地磁盘写入 EditLog 之外,也会并行地向 JournalNode 集群之中的每一个 JournalNode 发送写请求, 只要大多数 (majority) 的 JournalNode 节点返回成功就认为向 JournalNode 集群写入 EditLog 成功。如果有 2N+1 台 JournalNode,那么根据大多数的原则,最多可以容忍有 N 台 JournalNode 节点挂掉

### **13、在 NameNode HA 中,会出现脑裂问题吗?怎么解决脑裂**

​ 假设 NameNode1 当前为 Active 状态,NameNode2 当前为 Standby 状态。如果某一时 刻 NameNode1 对应的 ZKFailoverController 进程发生了“假死”现象,那么 Zookeeper 服务端会认为 NameNode1 挂掉了,根据前面的主备切换逻辑,NameNode2 会替代 NameNode1 进入 Active 状态。但是此时 NameNode1 可能仍然处于 Active 状态正常运行,这样 NameNode1 和 NameNode2 都处于 Active 状态,都可以对外提供服务。这种 情况称为脑裂。

​ 脑裂对于 NameNode 这类对数据一致性要求非常高的系统来说是灾难性的,数 据会发生错乱且无法恢复。Zookeeper 社区对这种问题的解决方法叫做 fencing, 中文翻译为隔离,也就是想办法把旧的 Active NameNode 隔离起来,使它不能 正常对外提供服务。

在进行 fencing 的时候,会执行以下的操作:

1) 首先尝试调用这个旧 Active NameNode 的 HAServiceProtocol RPC 接口的 transitionToStandby 方法,看能不能把它转换为 Standby 状态。

2) 如果 transitionToStandby 方法调用失败,那么就执行 Hadoop 配置文件之中 预定义的隔离措施,Hadoop 目前主要提供两种隔离措施,通常会选择 sshfence:

(1) sshfence:通过 SSH 登录到目标机器上,执行命令 fuser 将对应的进程杀死

(2) shellfence:执行一个用户自定义的 shell 脚本来将对应的进程隔离

### **14、小文件过多会有什么危害,如何避免**

Hadoop 上大量 HDFS 元数据信息存储在 NameNode 内存中,因此过多的小文件必 定会压垮 NameNode 的内存 。

每个元数据对象约占 150byte,所以如果有 1 千万个小文件,每个文件占用一个 block,则 NameNode 大约需要 2G 空间。如果存储 1 亿个文件,则 NameNode 需要 20G 空间 。

显而易见的解决这个问题的方法就是合并小文件,可以选择在客户端上传时执行 一定的策略先合并,或者是使用 Hadoop 的 CombineFileInputFormat<K,V\>实现小 文件的合并 。

### **15、请说下 HDFS 的组织架构**

1)Client:客户端

(1)切分文件。文件上传 HDFS 的时候,Client 将文件切分成一个一个的 Block, 然后进行存储

(2)与 NameNode 交互,获取文件的位置信息

(3)与 DataNode 交互,读取或者写入数据

(4)Client 提供一些命令来管理 HDFS,比如启动关闭 HDFS、访问 HDFS 目录及 内容等

2)NameNode:名称节点,也称主节点,存储数据的元数据信息,不存储具体 的数据

(1)管理 HDFS 的名称空间

(2)管理数据块(Block)映射信息

(3)配置副本策略

(4)处理客户端读写请求

3)DataNode:数据节点,也称从节点。NameNode 下达命令,DataNode 执行实 际的操作

(1)存储实际的数据块

(2)执行数据块的读/写操作

4)Secondary NameNode:并非 NameNode 的热备。当 NameNode 挂掉的时候, 它并不能马上替换 NameNode 并提供服务

(1)辅助 NameNode,分担其工作量

(2)定期合并 Fsimage 和 Edits,并推送给 NameNode

(3)在紧急情况下,可辅助恢复 NameNode

## 二、MapReduce

### **1、MapReduce的工作流程**

![image-20220531090618072](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220531090618072.png)

**MapReduce可以分成Map和Reduce两部分理解。**

**1.Map:**映射过程,把一组数据按照某种Map函数映射成新的数据。我们将这句话拆分提炼出重要信息,也就是说,map主要是:映射、变换、过滤的过程。一条数据进入map会被处理成多条数据,也就是1进N出。

**2.Reduce:**归纳过程,把若干组映射结果进行汇总并输出。我们同样将重要信息提炼,得到reduce主要是:分解、缩小、归纳的过程。一组数据进入reduce会被归纳为一组数据(或者多组数据),也就是一组进N出。

**MR的整体执行流程:**

1、在MapReduce程序读取文件的输入目录上存放相应的文件。

2、客户端程序在submit()方法执行前,获取待处理的数据信息,然后根据集群中的参数的配置形成一个任务分配规划。

3、客户端提交切片信息给Yarn,Yarn中的ResourceManager启动MapReduceAPPmaster。

4、MapReduceAPPmaster启动后根据本次job的描述信息,计算出需要的maptask实例对象,然后向集群申请机器启动相应数量的maptask进程。

5、Maptask利用客户端指定的inputformat来读取数据,形成输出的kv键值对。

6、Maptask将输入kv键值对传递给客户定义的map()方法,做逻辑运算。

7、Map()方法运算完毕后将kv键值对收集到maptask缓存。

8、shuffle阶段

​ 1)maptask收集我们的map()方法输出的kv键值对,放到环形缓存区中。

​ 2)maptask中的kv键值对按照key分区**排序**,并不断溢写到本地磁盘文件,可能会溢出多个文件。

​ 3)多个文件会被合并成大的溢出文件。

​ 4)在溢写过程中,及合并过程中,都会不停的进行分区和针对key的**排序**操作。

​ 5)ReduceTask根据自己的分区号,去各个maptask机器上获取相应的结果分区数据。

​ 6)ReduceTask会取到同一个分区的来自不同的maptask的结果文件,ReduceTask会将这些文件再进行归并**排序**。

​ 7)合并成大文件后,shuffle的过程也就结束,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一个的键值对group,调用用户自定义的reduce()方法)。

9、MapReduceAPPmaster监控到所有的maptask进程任务完成后,会根据客户指定的参数启动相应数量的ReduceTask进程,并告知ReduceTask进程要处理的数据分区。

10、ReduceTask进程启动后,根据MapReduceAPPmaster告知的待处理数据所在的位置,从若干台maptask运行所在的机器上获取若干个maptask输出结果文件,并在本地进行重新归并**排序**,然后按照相同key的键值对为一个组,调用客户定义的reduce()方法进行逻辑运算。

11、ReduceTask运算完毕后,调用客户指定的outputformat将结果数据输出到外部。

### **2、Hadoop Shuffle 原理**

​ 1)map方法之后reduce方法之前这段处理过程叫Shuffle。

​ 2)map方法之后,数据首先进入到分区方法,把数据标记好分区,然后把数据发送到环形缓冲区;环形缓冲区默认大小100M,环形缓冲区达到80%时,进行溢写;溢写前对数据进行排序,排序按照对key的索引进行字典顺序排序,排序的手段是快排;溢写产生大量溢写文件,需要对溢写文件进行归并排序;对溢写的文件也进行Combiner操作,前提是汇总操作。求平均值不行,最后将文件按照分区存储到磁盘,等待Reduce端拉取。

​ 3)每个reducer拉取Map端对应分区的数据。拉取数据后先存储到内存中,内存不够了,再存储到磁盘。拉取完所有数据后,采用归并排序将内存和磁盘中的数据都进行排序。在进入Reduce方法前,可以对数据进行分组操作。

**相关细节:**

1.maptask执行,收集maptask的输出数据,将数据写入环形缓冲区中,记录起始偏移量。

2.环形缓冲区默认大小为100M,当数据达到80M时,记录终止偏移量。

3.将数据进行分区(默认分组根据key的hash值%reduce 数量进行分区),分区内进行快速排序。

4.分区、排序结束后,将数据刷写到磁盘(这个过程中,maptask输出的数据写入剩余20%环形缓冲区,同样需要记录起始偏移量)。

5.maptask结束后将形成的多个小文件做归并排序合并成一个大文件。

6.当有一个maptask执行完成后,reducetask启动。

7.reducetask到运行完成maptask的机器上拉取属于自己分区的数据。

8.reducetask将拉取过来的数据分组,每组数据调用一次reduce()方法。

9.执行reduce逻辑,将结果输出到文件。

### **3、MR 中 Map Task 的工作机制**

![image-20220531090826974](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220531090826974.png)

**简单概述**:

inputFile 通过 split 被切割为多个 split 文件,通过 Record 按行读取内容给 map(自己写的处理逻辑的方法) ,数据被 map 处理完之后交给 OutputCollect 收集器,对其结果 key 进行分区(默 认使用的 hashPartitioner),然后写入 buffer,每个 map task 都有一个内存缓冲 区(环形缓冲区),存放着 map 的输出结果,当缓冲区快满的时候需要将缓冲 区的数据以一个临时文件的方式溢写到磁盘,当整个 map task 结束后再对磁盘中这个 maptask 产生的所有临时文件做合并,生成最终的正式输出文件,然后等待 reduce task 的拉取

**详细步骤**:

1) 读取数据组件 InputFormat (默认 TextInputFormat) 会通过 getSplits 方法对输入目录中的文件进行逻辑切片规划得到 block, 有多少个 block 就对应启动多少个 MapTask.

2) 将输入文件切分为 block 之后, 由 RecordReader 对象 (默认是 LineRecordReader) 进行读取, 以 \n 作为分隔符, 读取一行数据, 返回 <key, value\>, Key 表示每行首字符偏移值, Value 表示这一行文本内容

3) 读取 block 返回 <key,value\>, 进入用户自己继承的 Mapper 类中,执行用户重写的 map 函数, RecordReader 读取一行这里调用一次

4) Mapper 逻辑结束之后, 将 Mapper 的每条结果通过 context.write 进行collect 数据收集. 在 collect 中, 会先对其进行分区处理,默认使用 HashPartitioner

5) 接下来,会将数据写入内存,内存中这片区域叫做环形缓冲区(默认 **100M),** 缓冲区的作用是 批量收集 **Mapper** 结果, 减少磁盘 IO 的影响. 我们的 Key/Value 对以及 **Partition** **的结果都会被写入缓冲区.当然,写入之前,Key 与 Value 值都会被序列化成字节数组**

6) 当环形缓冲区的数据达到溢写比列(默认 0.8),也就是 80M 时,溢写线程启动,需要对这 **80MB** **空间内的** **Key** **做排序** **(Sort)**. 排序是 MapReduce 模型默认的 行为, 这里的排序也是对序列化的字节做的排序

7) 合并溢写文件, 每次溢写会在磁盘上生成一个临时文件 (写之前判断是否有 Combiner), 如果 Mapper 的输出结果真的很大, 有多次这样的溢写发生, 磁盘 上相应的就会有多个临时文件存在. 当整个数据处理结束之后开始对磁盘中的 临时文件进行 Merge 合并, 因为最终的文件只有一个, 写入磁盘, 并且为这个 文件提供了一个索引文件, 以记录每个 reduce 对应数据的偏移量

### **4、MR 中 Reduce Task 的工作机制**

![image-20220531090854157](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220531090854157.png)

**简单描述**:

Reduce 大致分为 copy、sort、reduce 三个阶段,重点在前两个阶段。copy 阶 段包含一个 eventFetcher 来获取已完成的 map 列表,由 Fetcher 线程去 copy 数据,在此过程中会启动两个 merge 线程,分别为 inMemoryMerger 和 onDiskMerger,分别将内存中的数据 merge 到磁盘和将磁盘中的数据进行 merge。待数据 copy 完成之后,copy 阶段就完成了,开始进行 sort 阶段,sort 阶段主要是执行 finalMerge 操作,纯粹的 sort 阶段,完成之后就是 reduce 阶 段,调用用户定义的 reduce 函数进行处理

**详细步骤**:

1) **Copy** **阶段**:简单地拉取数据。Reduce 进程启动一些数据 copy 线程(Fetcher), 通过 HTTP 方式请求 maptask 获取属于自己的文件(map task 的分区会标识每个 map task 属于哪个 reduce task ,默认 reduce task 的标识从 0 开始)。

2) **Merge** **阶段**:这里的 merge 如 map 端的 merge 动作,只是数组中存放的是不 同 map 端 copy 来的数值。Copy 过来的数据会先放入内存缓冲区中,这里的缓冲 区大小要比 map 端的更为灵活。merge 有三种形式:内存到内存;内存到磁盘; 磁盘到磁盘。默认情况下第一种形式不启用。当内存中的数据量到达一定阈值, 就启动内存到磁盘的 merge。与 map 端类似,这也是溢写的过程,这个过程中 如果你设置有 Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件。 第二种 merge 方式一直在运行,直到没有 map 端的数据时才结束,然后启动第 三种磁盘到磁盘的 merge 方式生成最终的文件。

3) **合并排序**:把分散的数据合并成一个大的数据后,还会再对合并后的数据排 序。

4) **对排序后的键值对调用** **reduce** **方法**,键相等的键值对调用一次 reduce 方法, 每次调用会产生零个或者多个键值对,最后把这些输出的键值对写入到 HDFS 文 件中。

### **5、请说下 MR 中 shuffle 阶段**

shuffle 阶段分为四个步骤:依次为:分区,排序,规约,分组,其中前三个步骤 在 map 阶段完成,最后一个步骤在 reduce 阶段完成 shuffle 是 Mapreduce 的核心,它分布在 Mapreduce 的 map 阶段和 reduce 阶段。一般把从 Map 产生输出开始到 Reduce 取得数据作为输入之前的过程称 作 shuffle。

\1. **Collect** **阶段**:将 MapTask 的结果输出到默认大小为 100M 的环形缓冲区,保存的 是 key/value,Partition 分区信息等。

\2. **Spill** **阶段**:当内存中的数据量达到一定的阀值的时候,就会将数据写入本地磁盘, 在将数据写入磁盘之前需要对数据进行一次排序的操作,如果配置了 combiner,还会将有 相同分区号和 key 的数据进行排序。

\3. **Merge** **阶段**:把所有溢出的临时文件进行一次合并操作,以确保一个 MapTask 最终 只产生一个中间数据文件

4.**Copy 阶段**:ReduceTask 启动 Fetcher 线程到已经完成 MapTask 的节点上复制一份 属于自己的数据,这些数据默认会保存在内存的缓冲区中,当内存的缓冲区达到一定的阀值 的时候,就会将数据写到磁盘之上

\4. **Merge** **阶段**:在 ReduceTask 远程复制数据的同时,会在后台开启两个线程对内存到 本地的数据文件进行合并操作

\5. **Sort** **阶段**:在对数据进行合并的同时,会进行排序操作,由于 MapTask 阶段已经对 数据进行了局部的排序,ReduceTask 只需保证 Copy 的数据的最终整体有效性即可。

Shuffle 中的缓冲区大小会影响到 mapreduce 程序的执行效率,原则上说,缓冲区越大, 磁盘 io 的次数越少,执行速度就越快 缓冲区的大小可以通过参数调整, 参数:mapreduce.task.io.sort.mb 默认 100M

### **6、shuffle 阶段的数据压缩机制了解吗**

在 shuffle 阶段,可以看到数据通过大量的拷贝,从 map 阶段输出的数据,都要 通过网络拷贝,发送到 reduce 阶段,这一过程中,涉及到大量的网络 IO,如果 数据能够进行压缩,那么数据的发送量就会少得多。

hadoop 当中支持的压缩算法:

gzip、bzip2、LZO、LZ4、**Snappy**,这几种压缩算法综合压缩和解压缩的速率,谷歌的 Snappy 是最优的,一般都选择 Snappy 压缩。

### **7、在写 MR 时,什么情况下可以使用规约**

规约(combiner)是不能够影响任务的运行结果的,局部汇总,适用于求和类, 不适用于求平均值,如果 reduce 的输入参数类型和输出参数的类型是一样的, 则规约的类可以使用 reduce 类,只需要在驱动类中指明规约的类即可。

### **8、combine函数的作用**

​ combine分为map端和reduce端,作用是把相同一个key的键值对合并在一起,可以自定义的。combine函数把一个map函数产生的键值对合并成一个新的键值对,将新的键值对作为输入到reduce函数中。这个合并的目的就是为了减少网络传输。

### **9、map join 和 reduce join区别及优化**

#### 1)Map-side Join (Broadcast join)

**思想:**小表复制到各个节点上,并加载到内存中;大表分片,与小表完成连接操作。

两份数据中,如果有一份数据比较小,小数据全部加载到内存,按关键字建立索引。大数据文件作为map的输入,对map()函数每一对输入,都能够方便的和已加载到内存的小数据进行连接。把连接结果按key输出,经过shuffle阶段,reduce端得到的就是已经按key分组的,并且连接好了的数据。

这种方法,要使用Hadoop中的DistributedCache把小数据分布到各个计算节点,每个map节点都要把小数据加载到内存,按关键字建立索引。

​ ----- Join操作在map task中完成,因此无需启动reduce task。

​ ----- 适合一个大表,一个小表的连接操作。

**这种方法有明显的局限性:**

- 有一份数据比较小,在map端,能够把它加载在内存,并进行join操作。

#### 2)Reduce-side Join (Shuffle join)

**思想:**map端按照连接字段进行hash,reduce端完成连接操作。

在map阶段,把关键字作为key输出,并在value中标记出数据是来自data1还是data2。因为在shuffle阶段已经自然按key分组,reduce阶段,判断每一个value是来自**data1**还是data2,在内部分成两组,做集合的成绩。

​ ----- Join操作在reduce task中完成。

​ ----- 适合两个大表的连接操作。

**这种方法有2个问题:**

- map阶段没有对数据瘦身,shuffle的网络传输和排序性能很低。

- reduce端对2个集合做乘积计算,很耗内存,容易导致OOM。

#### 3)优化方案

**使用内存服务器,扩大节点的内存空间**

针对map join,可以报一份数据放到专门的内存服务器,在map()方法中,对每一个的输入对,根据key到内存服务器中取出数据,进行连接。

**使用BloomFilter过滤空连接的数据**

对其中一份数据在内存中建立BloomFilter,另外一份数据在连接之前,用BloomFilter判断它的key是否存在,如果不存在,那这个记录是空连接,可以忽略。

### **10、hadoop优化,也是MR优化**

#### **1)数据输入**:

(1)合并小文件,在执行MR任务前将小文件进行合并,大量的小文件会产生大量的map任务,增大map任务装载次数,而任务的装载比较耗时间,从而会导致MR运行较慢。

(2)采用ConbinFileInputFormat来作为输入,解决输入端大量小文件场景。

#### **2)Map阶段**

(1)减少溢写次数,通过调整 io.sort.mb 及 sort.spill.percent 参数值,增大触发溢写的内存上限,减少溢写次数,从而减少磁盘IO;

(2)减少合并次数,通过调整 io.sort.factor 参数,增大 merge 的文件数目,减少 merge 的次数,从而缩短 MR 处理时间;

(3)在map之后,不影响业务逻辑的前提下,先进行combine处理,减少IO。

#### **3)Reduce阶段**

(1)合理设置Map和Reduce数:两个都不能设置太少,也不能设置太多。太少,会导致Task等待时间太长,延长处理时间;太多,会导致 Map、Reduce任务间竞争资源,造成处理超时等错误。

(2)设置Map、Reduce共存:调整slow start.completedmaps参数,使Map运行到一定程度后,Reduce也开始运行,从而减少Reduce的等待时间。

(3)规避使用Reduce,因为Reduce在用于连接数据集的时候将会产生大量的网络消耗。

(4)合理设置reduce端的buffer,可以通过设置参数来配置,使得buffer中的一部分数据可以直接输送到reduce,从而减少IO开销;MapReduce.reduce.input.buffer.percent 的默认为0.0, 当值大于0时,会保留在指定比例的内存读buffer中的数据直接拿给reduce使用。

#### **4)IO传输**

(1)采用数据压缩的方式,减少网络IO的的时间。安装Snappy和LZOP压缩编码器。

(2)使用SequenceFile二进制文件

#### 5)其他的

(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最大重试次数

### **11、Hadoop中有哪些进程,各自的作用**

**1)NameNode:** 管理文件系统的元数据存储,记录文件中各个数据块的位置信息,负责执行有关文件系统的命名空间的操作,如打开、关闭、重命名文件和目录等,一个HDFS集群只有一个Active的NameNode,可以有其他的 从元数据节点。

**2)SecondaryNameNode:** 合并NameNode的edit logs 到 fsimage 文件中辅助 NameNode将内存中的元数据信息持久化。

**3)NodeManager:**是Yarn中每个节点上的代理,它管理Hadoop集群中单个计算节点包括与ResourceManager保持通信,监督Container的生命周期管理,监控每个Container的资源使用(内存、cpu等)情况,追踪节点健康状况,管理日志和不同应用程序用到的附属服务。

**4)DataNode:**数据存储节点,保存和检索block(文件块)负责提供来自文件系统客户端的读写请求,执行块的创建、删除等操作。

**5)ResourceManager:**在Yarn中,ResourceManager负责集群中所有资源的统一管理和分配,他接受来自各个节点(NodeManager)的资源汇报信息,并把这些信息按照一定的策略分配给各个应用程序(实际上是ApplicationManager)ResourceManager与每个节点的NodeManager和每个应用的ApplicationManager一起工作。

### **12、Hadoop参数调优**

**1)**在hdfs-site.xml文件中配置多目录,最好提前配置好,否则更改目录需要重新启动集群

**2)**NameNode有一个工作线程池,用来处理不同DataNode的并发心跳以及客户端并发的元数据操作。

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

**3)**编辑日志存储路径dfs.namenode.edits.dir设置与镜像文件存储路径dfs.namenode.name.dir尽量分开,达到最低写入延迟

**4)**服务器节点上YARN可使用的物理内存总量,默认是8192(MB),注意,如果你的节点内存资源不够8GB,则需要调减小这个值,而YARN不会智能的探测节点的物理内存总量。yarn.nodemanager.resource.memory-mb

**5)**单个任务可申请的最多物理内存量,默认是8192(MB)。yarn.scheduler.maximum-allocation-mb

### **13、列举MapReduce中可干预的组件**

1)combine: 相当于在map端(每个maptask生成的文件)做了一次reduce。

2)partition:分区,默认根据key的hash值 除以reduce的数量,自定义分区时继承Partitioner类,重写getPartition分区方法。自定义分区可以有效的解决数据倾斜的问题。

3)group:分组,继承WritableComparator类,重写compare方法,自定义分组(就是定义reduce输入的数据分组规则)。

4)sort:排序,继承WritableComparable类,重写compare To方法,根据自定义的排序方法,将reduce的输出结果进行排序。

5)分片:可调整客户端的blockSize, minSize, maxSize。

### **14、分片和分块的区别**

1)分片是逻辑概念,分片是有冗余。

2)分块是物理概念,是将数据拆分,无冗余。

## 三、Yarn

### **1、yarn 集群的架构和工作原理知道多少**

YARN 的基本设计思想是将 MapReduce V1 中的 JobTracker 拆分为两个独立的服 务:ResourceManager 和 ApplicationMaster。ResourceManager 负责整个系统的资源管理和分配,ApplicationMaster 负责单个应用程序的的管理。

**1) ResourceManager:**

RM 是一个全局的资源管理器,负责整个系统的资源管理和分配,它主要由两个 部分组成:调度器(Scheduler)和应用程序管理器(Application Manager)。 调度器根据容量、队列等限制条件,将系统中的资源分配给正在运行的应用程序, 在保证容量、公平性和服务等级的前提下,优化集群资源利用率,让所有的资源 都被充分利用应用程序管理器负责管理整个系统中的所有的应用程序,包括应用 程序的提交、与调度器协商资源以启动 ApplicationMaster、监控 ApplicationMaster 运行状态并在失败时重启它。

**2) ApplicationMaster:**

用户提交的一个应用程序会对应于一个 ApplicationMaster,它的主要功能有:

a.与 RM 调度器协商以获得资源,资源以 Container 表示。

b.将得到的任务进一步分配给内部的任务。

c.与 NM 通信以启动/停止任务。

d.监控所有的内部任务状态,并在任务运行失败的时候重新为任务申请资源以重

启任务。

**3) nodeManager:**

NodeManager 是每个节点上的资源和任务管理器,一方面,它会定期地向 RM 汇 报本节点上的资源使用情况和各个 Container 的运行状态;另一方面,他接收并 处理来自 AM 的 Container 启动和停止请求。

**4) container:**

Container 是 YARN 中的资源抽象,封装了各种资源。一个应用程序会分配一个 Container,这个应用程序只能使用这个 Container 中描述的资源。 不同于 MapReduceV1 中槽位 slot 的资源封装,Container 是一个动态资源的划分 单位,更能充分利用资源。

### **2、yarn 的任务提交流程是怎样的**

#### 1.作业提交

1)client调用job.waitForCompletion 方法,向整个集群提交MapReduce作业。

2)client向ResourceManager申请一个作业ID。

3)ResourceManager给Client返回该job资源的提交路径(HDFS路径)和作业ID,每一个作业都有唯一的ID。

4)Client发送jar包、切片信息和配置文件到指定的资源提交路径。

5)Client提交完资源后,向ResourceManager申请运行MapReduceApplicationMaster(针对该job的ApplicationMaster)

#### 2.作业初始化

6)当ResourceManager收到Client的请求后,将该job添加到容量调度器(ResourceScheduler)中。

7)某一个空闲的NodeManager领取到该job。

8)该NodeManager创建Container,并产生MrApplicationMaster。

9)下载Client提交的资源到本地,根据分片信息生成MapTask和ReduceTask。

#### 3.任务分配

10)MrApplicationMaster 向 ResourceManager申请运行多个MapTask任务资源。

11)ResourceManager将运行MapTask任务分配给空闲的多个NodeManager,NodeManager分别领取任务并创建容器(Container)。

#### 4.任务运行

12)MrApplicationMaster向两个接收到任务的NodeManager发送程序启动脚本,每个接收到任务的NodeManager启动MapTask,MapTask对数据进行处理,并分区排序。

13)MrApplicationMaster等待所有的MapTask运行完毕后,向ResourceManager申请容器(Container),运行ReduceTask。

14)程序运行完毕后,MrApplicationMaster会向ResourceManager申请注销自己。

15)进度和状态更新。Yarn中的任务将其进度和状态(包括counter)返回给应用管理器,客户端每秒(通过mapreduce.client.progressmonitor.pollinterval设置)向应用管理器请求进度更新,展示给用户。可以使用Yarn WebUI查看任务执行状态。

#### 5.作业完成

除了向应用管理器请求作业进度外,客户端每5分钟都会通过调用waitForCompletion()来检查作业是否完成。时间间隔可以通过mapreduce.client.completion.pollinterval来设置。作业完成之后,应用管理器和container会清理工作状态。作业的信息会呗作业历史服务器存储以备之后用户核查。

### **3、 yarn 的资源调度三种模型了解吗**

在 Yarn 中有三种调度器可以选择:FIFO Scheduler ,Capacity Scheduler,Fair Scheduler

apache 版本的 hadoop 默认使用的是 capacity scheduler 调度方式。CDH 版本的默 认使用的是 fair scheduler 调度方式

**FIFO Scheduler**(先来先服务):

FIFO Scheduler 把应用按提交的顺序排成一个队列,这是一个先进先出队列,在 进行资源分配的时候,先给队列中最头上的应用进行分配资源,待最头上的应用 需求满足后再给下一个分配,以此类推。

FIFO Scheduler 是最简单也是最容易理解的调度器,也不需要任何配置,但它并 不适用于共享集群。大的应用可能会占用所有集群资源,这就导致其它应用被阻 塞,比如有个大任务在执行,占用了全部的资源,再提交一个小任务,则此小任 务会一直被阻塞。

**Capacity Scheduler**(能力调度器):

对于 Capacity 调度器,有一个专门的队列用来运行小任务,但是为小任务专门设 置一个队列会预先占用一定的集群资源,这就导致大任务的执行时间会落后于使 用 FIFO 调度器时的时间。

**Fair Scheduler**(公平调度器):

在 Fair 调度器中,我们不需要预先占用一定的系统资源,Fair 调度器会为所有运 行的 job 动态的调整系统资源。

比如:当第一个大 job 提交时,只有这一个 job 在运行,此时它获得了所有集群 资源;当第二个小任务提交后,Fair 调度器会分配一半资源给这个小任务,让这 两个任务公平的共享集群资源。

需要注意的是,在 Fair 调度器中,从第二个任务提交到获得资源会有一定的延迟, 因为它需要等待第一个任务释放占用的 Container。小任务执行完成之后也会释 放自己占用的资源,大任务又获得了全部的系统资源。最终的效果就是 Fair 调度 器即得到了高的资源利用率又能保证小任务及时完成。

# 9___Sqoop

## 1、Sqoop脚本案例

```shell

# hdfs -> mysql

sqoop export \

# jdbc

--connect jdbc:mysql://single1:3306/test \

--username root \

--password root \

--table score_kb16 \

--columns stu_name,stu_gender,java_score,mysql_score \

# mapreduce

--export-dir /test/kb16/hive/kb16_scores/kb16_scores.txt \

--fields-terminated-by ','

```

```shell

# mysql -> hdfs

sqoop import \

--connect jdbc:mysql://single1:3306/test \

--username root \

--password root \

--table order_info \

--columns order_id,order_user_id,order_dt,order_money,order_status \

--where "order_dt between '2019-01-05' and '2019-01-10'" \

-m 1 \

--delete-target-dir \

--target-dir /test/kb16/hive/order_info \

--fields-terminated-by ','

```

```shell

#分区表按分区导入日数据

create external table kb16.sqoop_order_info_par_cluster(

id bigint,

order_id bigint,

order_user_id bigint,

order_dt string,

order_money string,

order_status int

)

partitioned by (ym string)

clustered by (id) sorted by (order_dt) into 4 buckets

row format delimited

fields terminated by ','

stored as textfile;

# 手动添加分区

alter table kb16.sqoop_order_info_par_cluster

add partition (ym='2019-01');

# 删除分区

alter table kb16.sqoop_order_info_par_cluster

drop partition (ym='2019-01');

# 查看分区

show partitions kb16.sqoop_order_info_par_cluster partition(ym='2019-03');

sqoop import \

--connect jdbc:mysql://single1:3306/test \

--username root \

--password root \

--table order_info \

--where "date_format(order_dt,'%Y-%m')='2019-02'" \

-m 1 \

--fields-terminated-by ',' \

--delete-target-dir \

--target-dir /hive312/warehouse/kb16.db/sqoop_order_info_par_cluster/ym=2019-02

```

## 2、Sqoop导入导出Null存储一致性问题

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

## 3、Sqoop数据导出一致性问题

1)场景1:

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

-staging-table 方式

```shell

sqoop export \

--connect jdbc:mysql://192.168.245.168:3306/user_behavior \

--username root \

--password root \

--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'

```

## 4、Sqoop底层运行的任务是什么

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

## 5、Sqoop数据导出的时候依次执行多长时间

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

# 10___Redis

## 1、Redis 持久化机制

​ Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。

实现:单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。

RDB是Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。( 快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。)

AOF:Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。

当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。

## 2、缓存雪崩

**缓存雪崩我们可以简单的理解为**:由于原有缓存失效,新缓存未到期间

(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。

**解决办法:**

大多数系统设计者考虑用加锁( 最多的解决方案)或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开。

## 3、缓存穿透

​ **缓存穿透是指**用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。

**解决办法:**

​ 最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。

5TB的硬盘上放满了数据,请写一个算法将这些数据进行排重。如果这些数据是一些32bit大小的数据该如何解决?如果是64bit的呢?

对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。

Bitmap: 典型的就是哈希表

缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。

**布隆过滤器(推荐)**

就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。

它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。

Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。

Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。

受提醒补充:缓存穿透与缓存击穿的区别

缓存击穿:是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据。

解决方案;在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。

增:给一个我公司处理的案例:背景双机拿token,token在存一份到redis,保证系统在token过期时都只有一个线程去获取token;线上环境有两台机器,故使用分布式锁实现。

## 4、缓存预热

​ **缓存预热**就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

**解决思路:**

1、直接写个缓存刷新页面,上线时手工操作下;

2、数据量不大,可以在项目启动的时候自动进行加载;

3、定时刷新缓存;

## 5、缓存更新

​ 除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:

(1)定时去清理过期的缓存;

(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

## 6、缓存降级

​ 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

**以参考日志级别设置预案:**

(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;

(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;

(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;

(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

## **7、热点数据和冷数据是什么**

热点数据,缓存才有价值

对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存

对于上面两个例子,寿星列表、导航信息都存在一个特点,就是信息修改频率不高,读取通常非常高的场景。

对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。

**数据更新前至少读取两次,**缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。

那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。

## 8、单线程的redis为什么这么快

(一)纯内存操作

(二)单线程操作,避免了频繁的上下文切换

(三)采用了非阻塞I/O多路复用机制

## 9、redis的数据类型,以及每种数据类型的使用场景

回答:一共五种

**(一)String**

最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存。

**(二)hash**

这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。

**(三)list**

使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。本人还用一个场景,很合适—取行情信息。就也是个生产者和消费者的场景。LIST可以很好的完成排队,先进先出的原则。

**(四)set**

因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。

另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。

**(五)sorted set**

sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。

## 10、Redis 内部结构

​ dict 本质上是为了解决算法中的查找问题(Searching)是一个用于维护key和value映射关系的数据结构,与很多语言中的Map或dictionary类似。 本质上是为了解决算法中的查找问题(Searching)

1、sds sds就等同于char * 它可以存储任意二进制数据,不能像C语言字符串那样以字符’\0’来标识字符串的结 束,因此它必然有个长度字段。

2、skiplist (跳跃表) 跳表是一种实现起来很简单,单层多指针的链表,它查找效率很高,堪比优化过的二叉平衡树,且比平衡树的实现

3、quicklist

4、ziplist 压缩表 ziplist是一个编码后的列表,是由一系列特殊编码的连续内存块组成的顺序型数据结构,

## 11、Redis 为什么是单线程的

​ 官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)Redis利用队列技术将并发访问变为串行访问

**1)绝大部分请求是纯粹的内存操作(非常快速)**

**2)采用单线程,避免了不必要的上下文切换和竞争条件**

**3)非阻塞IO优点:**

​ 1.速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查 找和操作的时间复杂度都是O(1)

2. 支持丰富数据类型,支持string,list,set,sorted set,hash

3. 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行

4. 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除如何解决redis的并发竞争key问题

同时有多个子系统去set一个key。这个时候要注意什么呢? 不推荐使用redis的事务机制。因为我们的生产环境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到多个key操作的时候,这多个key不一定都存储在同一个redis-server上。因此,redis的事务机制,十分鸡肋。

(1)如果对这个key操作,不要求顺序: 准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可

(2)如果对这个key操作,要求顺序: 分布式锁+时间戳。 假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。

(3) 利用队列,将set方法变成串行访问也可以redis遇到高并发,如果保证读写key的一致性

对redis的操作都是具有原子性的,是线程安全的操作,你不用考虑并发问题,redis内部已经帮你处理好并发的问题了。

## 12、Redis 集群方案应该怎么做?都有哪些方案?

1.twemproxy,大概概念是,它类似于一个代理方式, 使用时在本需要连接 redis 的地方改为连接 twemproxy, 它会以一个代理的身份接收请求并使用一致性 hash 算法,将请求转接到具体 redis,将结果再返回 twemproxy。

缺点: twemproxy 自身单端口实例的压力,使用一致性 hash 后,对 redis 节点数量改变时候的计算值的改变,数据无法自动移动到新的节点。

2.codis,目前用的最多的集群方案,基本和 twemproxy 一致的效果,但它支持在 节点数量改变情况下,旧节点数据可恢复到新 hash 节点

3.redis cluster3.0 自带的集群,特点在于他的分布式算法不是一致性 hash,而是 hash 槽的概念,以及自身支持节点设置从节点。

## 13、有没有尝试进行多机redis 的部署?如何保证数据一致的?

主从复制,读写分离

一类是主数据库(master)一类是从数据库(slave),主数据库可以进行读写操作,当发生写操作的时候自动将数据同步到从数据库,而从数据库一般是只读的,并接收主数据库同步过来的数据,一个主数据库可以有多个从数据库,而一个从数据库只能有一个主数据库。

## 14、对于大量的请求怎么样处理

redis是一个单线程程序,也就说同一时刻它只能处理一个客户端请求;

redis是通过IO多路复用(select,epoll, kqueue,依据不同的平台,采取不同的实现)来处理多个客户端请求的

## 15、Redis 常见性能问题和解决方案

(1) Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件

(2) 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次

(3) 为了主从复制的速度和连接的稳定性, Master 和 Slave 最好在同一个局域网内

(4) 尽量避免在压力很大的主库上增加从库

(5) 主从复制不要用图状结构,用单向链表结构更为稳定,即: Master <- Slave1 <- Slave2 <-Slave3…

## 16、为什么Redis的操作是原子性的,怎么保证原子性的

对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。

Redis的操作之所以是原子性的,是因为Redis是单线程的。

Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。

多个命令在并发中也是原子性的吗?

不一定, 将get和set改成单命令操作,incr 。使用Redis的事务,或者使用Redis+Lua==的方式实现.

## 17、Redis事务

Redis事务功能是通过Mulit、Exec、Discard和Watch 四个原语实现的

Redis会将一个事务中的所有命令序列化,然后按顺序执行。

1.redis 不支持回滚“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。

2.如果在一个事务中的命令出现错误,那么所有的命令都不会执行;

3.如果在一个事务中出现运行错误,那么正确的命令会被执行。

注:redis的discard只是结束本次事务,正确命令造成的影响仍然存在.

1)MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。

2)EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。

3)通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。

4)WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。

## 18、Redis实现分布式锁

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。

将 key 的值设为 value ,当且仅当 key 不存在。 若给定的 key 已经存在,则 SETNX 不做任何动作。

```shell

127.0.0.1:6379> setnx lock-key value1

<integer> 1

127.0.0.1:6379> setnx lock-key value2

<integer> 0

127.0.0.1:6379> get lock-key

"value1"

```

**解锁:**使用 del key 命令就能释放锁

解决死锁:

1)通过Redis中expire()给锁设定最大持有时间,如果超过,则Redis来帮我们释放锁。

2) 使用 setnx key “当前系统时间+锁持有的时间”和getset key “当前系统时间+锁持有的时间”组合的命令就可以实现。

# 11___Mysql

## 一、索引

### **1、什么是索引**

索引是一种[数据结构](https://so.csdn.net/so/search?q=数据结构&spm=1001.2101.3001.7020),可以帮助我们快速的进行数据的查找。

### **2、索引是个什么样的数据结构呢**

索引的数据结构和具体存储引擎的实现有关,在 MySQL 中使用较多的索引有 **Hash 索引,B+ 树索引**等,而我们经常使用的 InnoDB 存储引擎的默认索引实现为:B+ 树索引。

### **3、为什么使用索引**

- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。

- 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。

- 帮助服务器避免排序和临时表。

- 将随机IO变为顺序IO。

- 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。

### **4、Innodb为什么要用自增id作为主键**

​ 如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。如果使用非自增主键(如果身份证号或学号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置, 频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE(optimize table)来重建表并优化填充页面。

### **5、[Hash]索引和 B+ 树索引有什么区别或者说优劣呢**

首先要知道 Hash 索引和 B+ 树索引的底层实现原理:

hash 索引底层就是 hash 表,进行查找时,调用一次 hash 函数就可以获取到相应的键值,之后进行回表查询获得实际数据。B+ 树底层实现是多路平衡查找树。对于每一次的查询都是从根节点出发,查找到叶子节点方可以获得所查键值,然后根据查询判断是否需要回表查询数据。

那么可以看出他们有以下的不同:

hash 索引进行等值查询更快(一般情况下),但是却无法进行范围查询。

因为在 hash 索引中经过 hash 函数建立索引之后,索引的顺序与原顺序无法保持一致,不能支持范围查询。而 B+ 树的的所有节点皆遵循(左节点小于父节点,右节点大于父节点,多叉树也类似),天然支持范围。

hash 索引不支持使用索引进行排序,原理同上。

hash 索引不支持模糊查询以及多列索引的最左前缀匹配。原理也是因为 hash 函数的不可预测。

hash索引任何时候都避免不了回表查询数据,而B+树在符合某些条件(聚簇索引,覆盖索引等)的时候可以只通过索引完成查询

hash 索引虽然在等值查询上较快,但是不稳定。性能不可预测,当某个键值存在大量重复的时候,发生 hash 碰撞,此时效率可能极差。而 B+ 树的查询效率比较稳定,对于所有的查询都是从根节点到叶子节点,且树的高度较低。

因此,在大多数情况下,直接选择 B+ 树索引可以获得稳定且较好的查询速度。而不需要使用 hash 索引。

### **6、什么是 聚簇索引**

聚簇索引就是按照每张表的 主键 构造一棵B+树,同时叶子节点中存放的就是整张表的行记录数据。

在 InnoDB 中,只有主键索引是聚簇索引,如果没有主键,则挑选一个唯一键建立聚簇索引。如果没有唯一键,则MySQL自动为InnoDB表生成一个隐含字段来建立聚簇索引,这个字段长度为6个字节,类型为长整形。

当查询使用聚簇索引时,在对应的叶子节点,可以获取到整行数据,因此不用再次进行回表查询。

### **7、说一说索引的底层实现**

**Hash索引**

基于哈希表实现,只有精确匹配索引所有列的查询才有效,对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),并且Hash索引将所有的哈希码存储在索引中,同时在索引表中保存指向每个数据行的指针。

**B-Tree索引(MySQL使用B+Tree)**

B-Tree能加快数据的访问速度,因为存储引擎不再需要进行全表扫描来获取数据,数据分布在各个节点之中。

B+Tree索引

是B-Tree的改进版本,同时也是数据库索引所采用的存储结构。数据都在叶子节点上,并且增加了顺序访问指针,每个叶子节点都指向相邻的叶子节点的地址。相比B-Tree来说,进行范围查找时只需要查找两个节点,进行遍历即可。而B-Tree需要获取所有节点,相比之下B+Tree效率更高。

B+tree性质:

1、n棵子tree的节点包含n个关键字,不用来保存数据而是保存数据的索引。

2、所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。

3、所有的非终端结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。

4、B+ 树中,数据对象的插入和删除仅在叶节点上进行。

5、B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点。

### **8、索引有哪些优缺点**

**索引的优点**

可以大大加快数据的检索速度,这也是创建索引的最主要的原因。

通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

**索引的缺点**

1、时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;

2、空间方面:索引需要占物理空间。

### **9、聚簇索引和非聚簇索引的区别**

- **聚簇索引的叶子节点存放的是主键值和数据行,支持覆盖索引**

- **非聚簇索引的叶子节点存放的是主键值或数据记录的地址(InnoDB辅助索引的data域存储相应记录主键的值,MyISAM辅助索引的data域保存数据记录的地址)**

### **10、MyISAM和InnoDB实现B+树索引方式的区别是什么**

**MyISAM**,B+Tree叶节点的data域存放的是数据记录的地址,在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的key存在,则取出其data域的值,然后以data域的值为地址读取相应的数据记录,这被称为“非聚簇索引”

**InnoDB**,其数据文件本身就是索引文件,相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的节点data域保存了完整的数据记录,这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引,这被称为“聚簇索引”或者聚集索引,而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。

在根据主键索引搜索时,直接找到key所在的节点即可取出数据;根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。

总结:InnoDB 主键索引使用的是聚簇索引,MyISAM 不管是主键索引,还是二级索引使用的都是非聚簇索引。

### **11、MySQL中有几种索引类型,可以简单说说吗**

FullText :即为全文索引,目前只有MyISAM引擎支持。其可以在CREATE TABLE ,ALTER TABLE ,CREATE INDEX 使用,不过目前只有 Char、Varchar ,Text 列上可以创建全文索引。

HASH :由于HASH的唯一(几乎100%的唯一)及类似键值对的形式,很适合作为索引。HASH索引可以一次定位,不需要像树形索引那样逐层查找,因此具有极高的效率。但是,这种高效是有条件的,即只在“=”和“in”条件下高效,对于范围查询、排序及组合索引仍然效率不高。

Btree :Btree索引就是一种将索引值按一定的算法,存入一个树形的数据结构中(二叉树),每次查询都是从树的入口root开始,依次遍历node,获取leaf。这是MySQL里默认和最常用的索引类型。

Rtree :Rtree在MySQL很少使用,仅支持geometry数据类型,支持该类型的存储引擎只有MyISAM、BDb、InnoDb、NDb、Archive几种。相对于Btree,Rtree的优势在于范围查找。

### **12、覆盖索引是什么**

如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称 之为“覆盖索引”。

我们知道在InnoDB存储引 擎中,如果不是主键索引,叶子节点存储的是主键值。最终还是要“回表”,也就是要通过主键再查找一次,这样就 会比较慢。覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!

### **13、非聚簇索引一定会回表查询吗**

不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。

举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行select age from employee where age < 20的查询时,在索引的叶子节点上,已经包含了age信息,不会再次进行回表查询。

### **14、联合索引是什么?为什么需要注意联合索引中的顺序**

MySQL 可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。

**具体原因为:**

MySQL 使用索引时需要索引有序,假设现在建立了"name,age,school"的联合索引,那么索引的排序为:先按照name排序,如果 name 相同,则按照 age 排序,如果 age 的值也相等,则按照 school 进行排序。

当进行查询时,此时索引仅仅按照 name 严格有序,因此必须首先使用 name 字段进行等值查询,之后对于匹配到的列而言,其按照 age 字段严格有序,此时可以使用 age 字段用做索引查找,以此类推。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。

### **15、创建的索引有没有被使用到?或者说怎么才可以知道这条语句运行很慢的原因**

MySQL 提供了 explain 命令来查看语句的执行计划,MySQL 在执行某个语句之前,会将该语句过一遍查询优化器,之后会拿到对语句的分析,也就是执行计划,其中包含了许多信息。可以通过其中和索引有关的信息来分析是否命中了索引,例如possilbe_key,key,key_len等字段,分别说明了此语句可能会使用的索引,实际使用的索引以及使用的索引长度。

“执行计划”中需要知道的几个“关键字”

id :编号

select_type :查询类型

table :表

type :类型

possible_keys :预测用到的索引

key :实际使用的索引

key_len :实际使用索引的长度

ref :表之间的引用

rows :通过索引查询到的数据量

Extra :额外的信息

### **16、那么在哪些情况下会发生针对该列创建了索引但是在查询的时候并没有使用呢**

使用不等于查询

列参与了数学运算或者函数

在字符串 like 时左边是通配符。类似于’%aaa’。

当 mysql 分析全表扫描比使用索引快的时候不使用索引。

当使用联合索引,前面一个条件为范围查询,后面的即使符合最左前缀原则,也无法使用索引。

以上情况,MySQL无法使用索引。

### **17、为什么Mysql用B+树做索引而不用B-树或红黑树、二叉树**

主要原因:B+树只要遍历叶子节点就可以实现整棵树的遍历,而且在数据库中基于范围的查询是非常频繁的,而B树只能中序遍历所有节点,效率太低。

**B-tree:**

​ B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B(B-)树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了。

​ 由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引。

**Hash:**

虽然可以快速定位,但是没有顺序,IO复杂度高;

基于Hash表实现,只有Memory存储引擎显式支持哈希索引 ;

适合等值查询,如=、in()、<=>,不支持范围查询 ;

因为不是按照索引值顺序存储的,就不能像B+Tree索引一样利用索引完成排序 ;

Hash索引在查询等值时非常快 ;

因为Hash索引始终索引的所有列的全部内容,所以不支持部分索引列的匹配查找 ;

如果有大量重复键值得情况下,哈希索引的效率会很低,因为存在哈希碰撞问题 。

**二叉树:**

树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且IO代价高。

**红黑树:**

树的高度随着数据量增加而增加,IO代价高。

### **18、MySQL索引种类**

普通索引、唯一索引(主键索引、唯一索引)、联合索引、全文索引、空间索引

### **19、索引在什么情况下遵循最左前缀的规则**

在建立了联合索引的前提条件下,数据库会一直从左向右的顺序依次查找,直到遇到了范围查询(>,<,between,like等)

### **20、mysql在什么情况下索引会失效**

1)计算、函数、类型转换会导致索引失效。

2)范围条件右边的列索引失效。

3)不等于(!= 或者 <>)会导致索引失效。

4)is null 可以使用索引,is not null无法使用索引。

5)like以通配符%开头,索引失效。

6)or 前后只要存在非索引的列,都会导致索引失效。

7)数据库和表的字符集统一使用utf8mb4,因为这样兼容性更好,统一字符集可以避免由于字符集转换产生乱码。不同的字符集进行比较前需要进行转换会造成索引失效。

## 二、事务

### **1、什么是事务**

事务是一系列的数据库操作,他们要符合 ACID 特性,事务是数据库应用的基本单位。MySQL 事务主要用于处理操作量大,复杂度高的数据。

### **2、ACID是什么?可以详细说一下吗**

A=Atomicity:原子性,就是要么全部成功,要么全部失败。不可能只执行一部分操作。

C=Consistency:一致性,系统(数据库)总是从一个一致性的状态转移到另一个一致性的状态,不会存在中间状态。

I=Isolation:隔离性,通常来说:一个事务在完全提交之前,对其他事务是不可见的.注意前面的通常来说加了红色,意味着有例外情况。

D=Durability:持久性,一旦事务提交,那么就永远是这样子了,哪怕系统崩溃也不会影响到这个事务的结果。

### **3、MySQL中为什么要有事务回滚机制**

而在 MySQL 中,恢复机制是通过回滚日志(undo log)实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后在对数据库中的对应行进行写入。当事务已经被提交之后,就无法再次回滚了。

**回滚日志作用:**

1.能够在发生错误或者用户执行 ROLLBACK 时提供回滚相关的信息。

2.在整个系统发生崩溃、数据库进程直接被杀死后,当用户再次启动数据库进程时,还能够立刻通过查询回滚日志将之前未完成的事务进行回滚,这也就需要回滚日志必须先于数据持久化到磁盘上,是我们需要先写日志后写数据库的主要原因。

### **4、数据库并发事务会带来哪些问题**

数据库并发事务会带来 脏读、幻读、丢弃更改、不可重复读 这四个常见问题,其中:

**脏读:**A 事务读取到了 B 事务未提交的内容,但是之后B事务满足一致性等特性而做了回滚操作,那么读取事务得到的结果就是脏数据了。

**幻读:**A 事务读取了一个范围的内容,而同时 B 事务在此期间插入(删除)了一条数据。造成"幻觉"。

**丢弃修改:**两个写事务T1 T2同时对A=0进行递增操作,结果T2覆盖T1,导致最终结果是1 而不是2,事务被覆盖

**不可重复读:**当设置T2事务只能读取 T1 事务已经提交的部分,T2 读取一个数据,然后T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。

### **5、怎么解决这些并发带来的问题呢?MySQL 的事务隔离级别了解吗**

**MySQL 的四种隔离级别如下:**

1、未提交读(READ UNCOMMITTED):事务中发生了修改,即使没有提交,其他事务也是可见的,比如对于一个数A原来50修改为100,但是我还没有提交修改,另一个事务看到这个修改,而这个时候原事务发生了回滚,这时候A还是50,但是另一个事务看到的A是100.可能会导致脏读、幻读或不可重复读。

2、已提交读(READ COMMITTED):对于一个事务从开始直到提交之前,所做的任何修改是其他事务不可见的,举例就是对于一个数A原来是50,然后提交修改成100,这个时候另一个事务在A提交修改之前,读取的A是50,刚读取完,A就被修改成100,这个时候另一个事务再进行读取发现A就突然变成100了;可以阻止脏读,但是幻读或不可重复读仍有可能发生。

3、可重复读(REPEATABLE READ):就是对一个记录读取多次的记录是相同的,比如对于一个数A读取的话一直是A,前后两次读取的A是一致的;可以阻止脏读和不可重复读,但幻读仍有可能发生。

4、可串行化(SERIALIZABLE):在并发情况下,和串行化的读取的结果是一致的,没有什么不同,比如不会发生脏读和幻读;该级别可以防止脏读、不可重复读以及幻读。

### **6、Innodb使用的是哪种隔离级别呢**

MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重复读)

原因: 与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读)事务隔离级别 下使用的是 Next-Key Lock 锁算法 ,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以 说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要 求,即达到了 SQL标准的SERIALIZABLE(可串行化)隔离级别。

InnoDB 存储引擎在分布式事务 的情况下一般会用到SERIALIZABLE(可串行化)隔离级别。

### **7、不可重复读和幻读区别是什么?可以举个例子吗?**

不可重复读的重点是修改,幻读的重点在于新增或者删除。

例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 1000的操作还没完成,事务2中的B先生就修改了A的工资为2000,导致A再读自己的工资时工资变为 2000;这就是不可重复读。

例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记 录就变为了5条,这样就导致了幻读。

## 三、锁

### **1、对 MySQL 的锁了解吗?**

当数据库有并发事务的时候,可能会产生数据的不一致,这时候需要一些机制来保证访问的次序,锁机制就是这样的一个机制.

就像酒店的房间,如果大家随意进出,就会出现多人抢夺同一个房间的情况,而在房间上装上锁,申请到钥匙的人才可以入住并且将房间锁起来,其他人只有等他使用完毕才可以再次使用.

### **2、MySQL 锁的分类**

Mysql中锁的分类按照不同类型的划分可以分成不同的锁:

按照 **锁的粒度** 划分可以分成:

- 行锁

- 表锁

- 页锁

按照 **使用的方式** 划分可以分为:

- 共享锁

- 排它锁

按照 **思想** 的划分:

- 乐观锁

- 悲观锁

### **3、行级锁、表级锁、页级锁的描述与特点**

**行级锁:**

​ **描述:**行级锁是mysql中锁定粒度最细的一种锁。表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁和排他锁

​ **特点:**开销大,加锁慢,会出现死锁。发生锁冲突的概率最低,并发度也最高。

**表级锁:**

​ **描述:**表级锁是mysql中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分mysql引擎支持。最常使用的MyISAM与InnoDB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)

​ **特点:** 开销小,加锁快,不会出现死锁。发生锁冲突的概率最高,并发度也最低。

**页级锁:**

​ **描述:**页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。BDB 支持页级锁。

​ **特点:**开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

### **4、共享锁 、 排他锁的描述**

**共享锁:**

**描述:**

​ 共享锁又称读锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。

如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。

**用法:**

​ SELECT … LOCK IN SHARE MODE;

​ 在查询语句后面增加LOCK IN SHARE MODE,MySQL 就会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据。

**排他锁:**

**描述:**

排他锁又称写锁、独占锁,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。

**用法:**

SELECT … FOR UPDATE;

在查询语句后面增加FOR UPDATE,MySQL 就会对查询结果中的每行都加排他锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请排他锁,否则会被阻塞。

```

用上面的例子来说就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的. 一种是真正的入住一晚,在这期间,无论是想入住的还是想看房的都不可以.

```

### **5、悲观锁与乐观锁**

- **乐观锁(Optimistic Lock)**:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。 乐观锁不能解决脏读的问题。

```

乐观锁, 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

```

​ **悲观锁(Pessimistic Lock)**:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。

```

悲观锁,顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

```

### **6、数据库悲观锁和乐观锁的原理和应用场景分别有什么**

悲观锁,先获取锁,再进行业务操作,一般就是利用类似 SELECT … FOR UPDATE 这样的语句,对数据加锁,避免其他事务意外修改数据。当数据库执行SELECT … FOR UPDATE时会获取被select中的数据行的行锁,select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。

乐观锁,先进行业务操作,只在最后实际更新数据时进行检查数据是否被更新过。Java 并发包中的 AtomicFieldUpdater 类似,也是利用 CAS 机制,并不会对数据加锁,而是通过对比数据的时间戳或者版本号,来实现乐观锁需要的版本判断。

### **7、MySQL常用存储引擎的锁机制**

- **MyISAM和MEMORY采用表级锁(table-level locking)**

- **BDB采用页面锁(page-level locking)或表级锁,默认为页面锁**

- **InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁**

### **8、InnoDB 存储引擎有几种锁算法**

- **Record Lock** — 单个行记录上的锁;

- **Gap Lock** — 间隙锁,锁定一个范围,不包括记录本身;

- **Next-Key Lock** — 锁定一个范围,包括记录本身。

### **9、什么是死锁**

是指二个或者二个以上的进程在执行时候,因为争夺资源造成相互等待的现象,进程一直处于等待中,无法得到释放,这种状态就叫做死锁。

### **10、死锁出现的案列**

批量入库,存在则更新,不存在则插入,`insert into tab(xx,xx) on duplicate key update xx=‘xx’`。

### **11、如何处理死锁**

1. 通过`innodblockwait_timeout`来设置超时时间,一直等待直到超时

2. 发起死锁检测,发现死锁之后,主动回滚死锁中的事务,不需要其他事务继续。

### **12、如何避免死锁**

1、为了在单个innodb表上执行多个并发写入操作时避免死锁,可以在事务开始时,通过为预期要修改行,使用select …for update语句来获取必要的锁,即使这些行的更改语句是在之后才执行的。

2、在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时在申请排他锁。因为这时候当用户在申请排他锁时,其他事务可能又已经获得了相同记录的共享锁。

3、如果事务需要修改或锁定多个表,则应在每个事务中以相同的顺序使用加锁语句。在应用中,如果不同的程序会并发获取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。

4、通过 select …lock in share mode获取行的读锁后,如果当前事务在需要对该记录进行更新操作,则很有可能造成死锁。

5、改变事务隔离级别。

### **13、Innodb默认是如何对待死锁的**

innodb默认是使用设置死锁时间来让死锁超时的策略,默认`innodblockwait_timeout`设置的时长是50s

### **14、如何开启死锁检测**

设置`innodbdeadlockdetect`设置为on可以主动检测死锁,在innodb中这个值默认就是on开启的状态

### **15、什么是全局锁?它的应用场景有哪些?**

全局锁就是对整个数据库实例加锁,它的典型使用场景就是做全库逻辑备份,这个命令可以使用整个库处于只读状态,使用该命令之后,数据更新语句,数据定义语句,更新类事务的提交语句等操作都会被阻塞。

### **16、使用全局锁会导致的问题**

- 如果在主库备份,在备份期间不能更新,业务停止,所以更新业务会处于等待状态

- 如果在从库备份,在备份期间不能执行主库同步的binlog,导致主从延迟

### **17、优化锁方面你有什么建议**

1、尽量使用较低的隔离级别。

2、精心设计索引, 并尽量使用索引访问数据, 使加锁更精确, 从而减少锁冲突的机会。

3、选择合理的事务大小,小事务发生锁冲突的几率也更小。

4、给记录集显示加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁。

5、不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会。

6、尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响。

7、不要申请超过实际需要的锁级别。

8、除非必须,查询时不要显示加锁。 MySQL 的 MVCC 可以实现事务中的查询不用加锁,优化事务性能;MVCC 只在 COMMITTED READ(读提交)和 REPEATABLE READ(可重复读)两种隔离级别下工作。

9、对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能。

## **四、存储引擎**

### **1、MySQL 支持哪些存储引擎**

MySQL 支持多种存储引擎,比如InnoDB,MyISAM,Memory,Archive等等。在大多数的情况下,直接选择使用 `InnoDB` 引擎都是最合适的,InnoDB 也是 MySQL 的默认存储引擎。

### **2、InnoDB 和 MyISAM 有什么区别**

**InnoDB**

1、是 MySQL 默认的事务型存储引擎,只有在需要它不支持的特性时,才考虑使用其它存储引擎。

2、实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ)。在可重复读隔离级别下,通过多版本并发控制(MVCC)+ 间隙锁(Next-Key Locking)防止幻影读。

3、主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。

4、内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。

5、支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。

**MyISAM**

1、设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。

2、提供了大量的特性,包括压缩表、空间数据索引等。

3、不支持事务。

4、不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。

**总结:**

InnoDB 支持事物,而 MyISAM 不支持事物。

InnoDB 支持行级锁,表锁,而 MyISAM 支持表级锁。

InnoDB 支持 MVCC,而 MyISAM 不支持。

InnoDB 支持外键,而 MyISAM 不支持。

InnoDB5.7之前不支持全文索引,而 MyISAM 支持。

nnoDB必须有主键,没有指定会默认生成一个隐藏列作为主键,而MyISAM可以没有。

### **3、你了解MySQL的内部构造吗?一般可以分为哪两个部分?**

可以分为**服务层和存储引擎层**两部分,其中:

```

服务层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。

```

```

存储引擎层负责数据的存储和提取。 其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。现在最常用的存储引擎是InnoDB,它从MySQL5.5.5版本开始成为了默认的存储引擎。

```

### **4、说一下MySQL是如何执行一条SQL的?具体步骤有哪些?**

**Server层按顺序执行sql的步骤为:**

```

1.客户端请求->

2.连接器(验证用户身份,给予权限) ->

3.查询缓存(存在缓存则直接返回,不存在则执行后续操作)->

4.分析器(对SQL进行词法分析和语法分析操作) ->

5.优化器(主要对执行的sql优化选择最优的执行方案方法) ->

6.执行器(执行时会先看用户是否有执行权限,有才去使用这个引擎提供的接口)->

7.去引擎层获取数据返回(如果开启查询缓存则会缓存查询结果)

```

**简单概括:**

- **连接器**:管理连接、权限验证;

- **查询缓存**:命中缓存则直接返回结果;

- **分析器**:对SQL进行词法分析、语法分析;(判断查询的SQL字段是否存在也是在这步)

- **优化器**:执行计划生成、选择索引;

- **执行器**:操作引擎、返回结果;

- **存储引擎**:存储数据、提供读写接口。

### **5、SQL 的执行顺序?**

```sql

SELECT DISTINCT

< select_list >

FROM

< left_table > < join_type >

JOIN < right_table > ON < join_condition >

WHERE

< where_condition >

GROUP BY

< group_by_list >

HAVING

< having_condition >

ORDER BY

< order_by_condition >

LIMIT < limit_number >

```

它的执行顺序是:

![image-20220530160909206](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220530160909206.png)

**FROM 连接**

首先,对 SELECT 语句执行查询时,对FROM 关键字两边的表执行连接,会形成笛卡尔积,这时候会产生一个虚表VT1(virtual table)

**ON 过滤**

然后对 FROM 连接的结果进行 ON 筛选,创建 VT2,把符合记录的条件存在 VT2 中。

**JOIN 连接**

第三步,如果是 OUTER JOIN(left join、right join) ,那么这一步就将添加外部行,如果是 left join 就把 ON 过滤条件的左表添加进来,如果是 right join ,就把右表添加进来,从而生成新的虚拟表 VT3。

**WHERE 过滤**

第四步,是执行 WHERE 过滤器,对上一步生产的虚拟表引用 WHERE 筛选,生成虚拟表 VT4。

**GROUP BY**

根据 group by 字句中的列,会对 VT4 中的记录进行分组操作,产生虚拟机表 VT5。果应用了group by,那么后面的所有步骤都只能得到的 VT5 的列或者是聚合函数(count、sum、avg等)。

**HAVING**

紧跟着 GROUP BY 字句后面的是 HAVING,使用 HAVING 过滤,会把符合条件的放在 VT6

**SELECT**

第七步才会执行 SELECT 语句,将 VT6 中的结果按照 SELECT 进行刷选,生成 VT7

**DISTINCT**

在第八步中,会对 TV7 生成的记录进行去重操作,生成 VT8。事实上如果应用了 group by 子句那么 distinct 是多余的,原因同样在于,分组的时候是将列中唯一的值分成一组,同时只为每一组返回一行记录,那么所以的记录都将是不相同的。

**ORDER BY**

应用 order by 子句。按照 order_by_condition 排序 VT8,此时返回的一个游标,而不是虚拟表。sql 是基于集合的理论的,集合不会预先对他的行排序,它只是成员的逻辑集合,成员的顺序是无关紧要的。

### **6、简述触发器、函数、视图、存储过程**

**触发器:**使用触发器可以定制用户对表进行【增、删、改】操作时前后的行为,触发器无法由用户直接调用,而知由于对表的【增/删/改】操作被动引发的。

**函数:**是MySQL数据库提供的内部函数(当然也可以自定义函数)。这些内部函数可以帮助用户更加方便-的处理表中的数据。

**视图:**视图是虚拟表或逻辑表,它被定义为具有连接的SQL SELECT查询语句。

**存储过程:**存储过程是存储在数据库目录中的一坨的声明性SQL语句,数据库中的一个重要对象,有效提高了程序的性能。

### **7、听说过视图吗?那游标呢?**

- 视图是一种**虚拟的表**,通常是有一个表或者多个表的行或列的子集,具有和物理表相同的功能 游标是对查询出来的结果集作为一个单元来有效的处理。

- 一般不使用游标,但是需要逐条处理数据的时候,游标显得十分重要。

### **8、视图的作用是什么?可以更改吗?**

视图是虚拟的表,与包含数据的表不一样,视图只包含使用时动态检索数据的查询;不包含任何列或数据。使用视图可以简化复杂的 sql 操作,隐藏具体的细节,保护数据;视图创建后,可以使用与表相同的方式利用它们。

视图不能被索引,也不能有关联的触发器或默认值,如果视图本身内有order by 则对视图再次order by将被覆盖。

对于某些视图比如未使用联结子查询分组聚集函数Distinct Union等,是可以对其更新的,对视图的更新将对基表进行更新;但是视图主要用于简化检索,保护数据,并不用于更新,而且大部分视图都不可以更新。

## 五、表结构

### **1、为什么要尽量设定一个主键?**

主键是数据库确保数据行在整张表唯一性的保障,即使业务上本张表没有主键,也建议添加一个自增长的 ID 列作为主键.设定了主键之后,在后续的删改查的时候可能更加快速以及确保操作数据范围安全。

### **2、主键使用自增 ID 还是 UUID?**

**推荐使用自增ID,不要使用 UUID。**

因为在 InnoDB存储引擎中,主键索引是作为聚簇索引存在的,也就是说,主键索引的B+树叶子节点上存储了主键索引以及全部的数据(按照顺序),如果主键索引是自增ID,那么只需要不断向后排列即可,如果是UUID,由于到来的ID与原来的大小不确定,会造成非常多的数据插入,数据移动,然后导致产生很多的内存碎片,进而造成插入性能的下降.

总之,在数据量大一些的情况下,用自增主键性能会好一些。

### **3、字段为什么要求定义为not null?**

null 值会占用更多的字节,且会在程序中造成很多与预期不符的情况。

### **4、如果要存储用户的密码散列,应该使用什么字段进行存储?**

密码散列,用户身份证号等固定长度的字符串应该使用 `char` 而不是 `varchar` 来存储,这样可以节省空间且提高检索效率。

### **5、说一说Drop、Delete与Truncate的共同点和区别?**

- Drop直接删掉表;

- Truncate删除表中数据,再插入时自增长id又从1开始 ;

- Delete删除表中数据,可以加where字句。

### **6、数据库中的主键、超键、候选键、外键是什么?**

**超键:**在关系中能唯一标识元组的属性集称为关系模式的超键

**候选键:**不含有多余属性的超键称为候选键。也就是在候选键中,若再删除属性,就不是键了!

**主键:**用户选作元组标识的一个候选键程序主键

**外键:**如果关系模式R中属性K是其它模式的主键,那么k在模式R中称为外键。

## 六、三范式

- **第一范式: 每个列都不可以再拆分。**

- **第二范式: 非主键列完全依赖于主键,而不能是依赖于主键的一部分。**

- **第三范式: 非主键列只依赖于主键,不依赖于其他非主键。**

**在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能,事实上我们经常会为了性能而妥协数据库的设计。**

## 七、left join、right join以及inner join的区别

**left join:**左关联,主表在左边,右边为从表。如果左侧的主表中没有关联字段,会用null 填满

**right join:**右关联 主表在右边和letf join相反

**inner join:** 内关联只会显示主表和从表相关联的字段,不会出现null

## 八、count(1)、count(*)与count(列名)的执行区别

**执行效果上 :**

**count(*):**包括了所有的列,相当于行数,在统计结果的时候, 不会忽略列值为NULL

**count(1):**包括了忽略所有列,用1代表代码行,在统计结果的时候, 不会忽略列值为NULL

**count(列名):**只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数, 即某个字段值为NULL时,不统计。

**执行效率上:**

列名为主键,count(列名)会比count(1)快

列名不为主键,count(1)会比count(列名)快

如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*)

如果有主键,则select count(主键)的执行效率是最优的。

如果表只有一个字段,则select count(*)最优。

## 九、什么是sql注入

SQL注入攻击指的是通过构建特殊的输入作为参数传入Web应用程序,而这些输入大都是SQL语法里的一些组合,通过执行[SQL语句](https://so.csdn.net/so/search?q=SQL语句&spm=1001.2101.3001.7020)进而执行攻击者所要的操作,其主要原因是程序没有细致地过滤用户输入的数据,致使非法数据侵入系统。

## 十、简述数据库的读写分离

读写分离为了确保数据库产品的稳数据定性,很多数据库拥有双机热备功能。也就是,第一台数据库服务器,是对外提供增删改业务的生产服务器;第二台数据库服务器,主要进行读的操作。

## 十一、sql中null与空值的区别

**1.占用空间区别:**空值(’’)的长度是0,是不占用空间的;而的NULL长度是NULL,是占用空间的

**2.插入/查询方式区别:**NULL值查询使用is null/is not null查询,而空值(’’)可以使用=或者!=、<、>等算术运算符。

**3.COUNT 和 IFNULL函数:**使用 COUNT(字段) 统计会过滤掉 NULL 值,但是不会过滤掉空值。

**4.索引字段说明:**在有NULL值的字段上使用常用的索引,如普通索引、复合索引、全文索引等不会使索引失效。在官网查看在空间索引的情况下,说明了 索引列必须为NOT NULL。

## 十二、优化

### **1、表结构优化**

尽量使用数字型字段

```

若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

```

尽可能的使用 varchar 代替 char

```

变长字段存储空间小,可以节省存储空间。

```

当索引列大量重复数据时,可以把索引删除掉

```

比如有一列是性别,几乎只有男、女、未知,这样的索引是无效的。

```

### **2、查询优化**

1. 应尽量避免在 where 子句中使用!=或<>操作符

2. 应尽量避免在 where 子句中使用 or 来连接条件

3. 任何查询也不要出现select *

4. 避免在 where 子句中对字段进行 null 值判断

### **3、索引优化**

1. 对作为查询条件和 order by的字段建立索引

2. 避免建立过多的索引,多使用组合索引

### **4、慢查询优化**

1. 分析语句,是否加载了不必要的字段/数据

2. 分析 SQL 执行句话,是否命中索引等

3. 如果 SQL 很复杂,优化 SQL 结构

4. 如果表数据量太大,考虑分表

# 12___Spark

## 0、spark运行原理

1、构建Spark Application运行环境

2、SparkContext向资源管理器注册

3、SparkContext向资源管理器申请运行Executor

4、资源管理器分配Executor

5、资源管理器启动Executor

6、Executor发送心跳至资源管理器

7、SparkContext构建成DAG图

8、将DAG图分解成Stage(TaskSet)

9、把Stage(TaskSet)发送给TaskScheduler

10、Executor向SparkContext申请Task

11、TaskScheduler将Task发放给Executor运行

12、同时SparkContext将应用程序代码发放给Executor

13、Task在Executor上运行,运行完毕释放所有资源

1)、Application:Spark应用程序

指的是用户编写的Spark应用程序,包含了Driver功能代码和分布在集群中多个节点上运行的Executor代码。

Spark应用程序,由一个或多个作业JOB组成,如下图所示。

![image-20220531173613629](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220531173613629.png)

2)、Driver:驱动程序

Spark中的Driver即运行上述Application的Main()函数并且创建SparkContext,其中创建SparkContext的目的是为了准备Spark应用程序的运行环境。在Spark中由SparkContext负责和ClusterManager通信,进行资源的申请、任务的分配和监控等;当Executor部分运行完毕后,Driver负责将SparkContext关闭。通常SparkContext代表Driver,如下图所示。

![image-20220531173659557](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220531173659557.png)

3)、Cluster Manager:资源管理器

指的是在集群上获取资源的外部服务,常用的有:Standalone,Spark原生的资源管理器,由Master负责资源的分配;Haddop Yarn,由Yarn中的ResearchManager负责资源的分配;Messos,由Messos中的Messos Master负责资源管理。

4)、Executor:执行器

Application运行在Worker节点上的一个进程,该进程负责运行Task,并且负责将数据存在[内存](https://so.csdn.net/so/search?q=内存&spm=1001.2101.3001.7020)或者磁盘上,每个Application都有各自独立的一批Executor,如下图所示。

![image-20220531173733269](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220531173733269.png)

5)、Worker:计算节点

集群中任何可以运行Application代码的节点,类似于Yarn中的NodeManager节点。在Standalone模式中指的就是通过Slave文件配置的Worker节点,在Spark on Yarn模式中指的就是NodeManager节点,在Spark on Messos模式中指的就是Messos Slave节点,如下图所示。

![image-20220531173804897](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220531173804897.png)

6)、DAGScheduler:有向无环图调度器

基于DAG划分Stage 并以TaskSet的形势提交Stage给TaskScheduler;负责将作业拆分成不同阶段的具有依赖关系的多批任务;最重要的任务之一就是:计算作业和任务的依赖关系,制定调度逻辑。在SparkContext初始化的过程中被实例化,一个SparkContext对应创建一个DAGScheduler。

![image-20220531173834099](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220531173834099.png)

7)、TaskScheduler:任务调度器

将Taskset提交给worker(集群)运行并回报结果;负责每个具体任务的实际物理调度。如图所示。

![image-20220531173902216](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220531173902216.png)

8)、Job:作业

由一个或多个调度阶段所组成的一次计算作业;包含多个Task组成的并行计算,往往由**Spark Action**催生,一个JOB包含多个RDD及作用于相应RDD上的各种Operation。如图所示。

![image-20220531173927115](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220531173927115.png)

9)、Stage:调度阶段

一个任务集对应的调度阶段;每个Job会被拆分很多组Task,每组任务被称为Stage,也可称TaskSet,一个作业分为多个阶段;Stage分成两种类型ShuffleMapStage、ResultStage。如图所示。

![image-20220531173951581](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220531173951581.png)

10)、TaskSet:任务集

由一组关联的,但相互之间没有Shuffle依赖关系的任务所组成的任务集。如图所示。

![image-20220531174032466](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220531174032466.png)

提示:

1)一个Stage创建一个TaskSet;

2)为Stage的每个Rdd分区创建一个Task,多个Task封装成TaskSet

11)、Task:任务

被送到某个Executor上的工作任务;单个分区数据集上的最小处理流程单元(单个stage内部根据操作数据的分区数划分成多个task)。如图所示。

![image-20220531174108248](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220531174108248.png)

![image-20220531174125915](C:\Users\HM\AppData\Roaming\Typora\typora-user-images\image-20220531174125915.png)

## 一、Spark Core

### 1、Spark有几种部署模式,每种模式特点

#### 1.1、本地模式

spark不一定非要跑在hadoop集群,可以在本地,起多个线程的方式来指定。将spark应用以多线程的方式直接运行在本地,一般都是为了方便调试,本地模式分三类:

​ 1)local: 只启动一个executor

​ 2)local[k]: 启动k个executor

​ 3)local[*]: 启动跟cpu数目相同的executor

#### 1.2、standalone模式

分布式部署集群,自带完整的服务,资源管理和任务监控是spark自己监控,这个模式也是其他模式的基础。

#### 1.3、Spark on yarn 模式

分布式部署集群,资源和任务监控交给yarn管理,Spark客户端直接链接yarn,不需要额外构建spark集群。有spark-client和yarn-cluster两种模式,主要区别在于:Driver程序的运行节点。

​ 1)cluster适合生产,driver运行在集群子节点,具有容错功能。

​ 2)client适合调试,driver运行在客户端。

### **2、Driver的功能是什么**

一个spark作业运行时包括一个driver进程,即主进程,有main函数,有sparkcontext实例,程序的入口

功能: 负责向集群RM申请资源,向master注册信息,负责job的调度,解析,生成stage并调度Task到executor上,包括DAGscheduler,Taskscheduler。

### **3、Hadoop和Spark都是并行计算,那么他们异同点是什么**

​ 两者都是用mapreduce模型来进行并行计算的,hadoop的一个作业称为job,job里面分为map task和reduce task,每个task都是在自己的进程中运行的,当task结束时,进程也会结束。

​ spark用户提交的任务称为application,一个application对应一个sparkcontext,application中存在多个job,每触发一次action操作就会产生一个job,这些job可以并行或者串行执行,每个job中有多个stage,stage是shuffle过程中DAGSchaduler通过RDD之间的依赖关系划分job而来的,每个stage里面有多个task,组成taskset有TaskSchaduler分发到各个executor中执行,executor的生命周期是和application一样的,即使没有job运行也是存在的,所以task可以快速启动读取内存进行计算,spark的迭代计算都是在内存中进行的,API中提供了大量的RDD操作如join,groupby等,而且通过DAG图可以实现良好的容错。

​ Hadoop的job只有map和reduce操作,表达能力比较欠缺而且在mapreduce过程中会重复的读写HDFS,造成大量的IO操作,多个job需要自己管理关系。

### **4、简单描述Spark中的概念RDD,他有哪些特性**

​ RDD叫做弹性分布式数据集,是Spark中最基本的数据抽象,它代表一个不可变、可分区、里面的元素可并行计算的集合。

​ RDD的五大特性:

​ 1)A list of partitions 一个分区列表,RDD中的数据都存在一个分区列表里面

​ 2)A function for computing each split 作用在每一个分区中的函数

​ 3)A list of dependencies on other RDDs 一个RDD依赖于其他多个RDD,这个点很重要,RDD的容错机制就是依据这个特性而来的。

​ 4)Optionally,a Partitioner for key-value RDDs 可选的,针对于kv类型的RDD才具有这个特征,作用是决定了数据的来源以及数据处理后的去向。

​ 5)Optionally,a list of preferred locations to compute each split on 可选项,数据本地性,数据位置最优。

### **5、简述宽依赖和窄依赖**

#### 5.1、窄依赖

​ 指父RDD的每一个分区最多被一个子RDD的分区所用,表现为一个父RDD的分区对应于一个子RDD的分区,或两个父RDD的分区对应于一个子RDD的分区,

map/filter和union属于第一类,对输入进行协同划分的join属于第二类

#### 5.2、宽依赖

指子RDD的分区依赖于父RDD的所有分区,这是因为shuffle类操作

#### 5.3、算子的宽窄依赖

对RDD进行map、filter、union、和Transformation一般是窄依赖。

宽依赖一般是对RDD进行groupbykey,reducebykey等操作,就是对RDD中的partition中的数据进行重分区(shuffle)。

join操作既可能是宽依赖也可能是窄依赖,当要对RDD进行join操作时,如果RDD进行过重分区则为窄依赖,否则为宽依赖。

### **6、Spark如何防止内存溢出**

#### 6.1、 driver端的内存溢出

可以增大driver的内存参数:spark.driver.memory(default 1g)

​ 这个参数用来设置Driver的内存。在Spark程序中,SparkContext,DAGScheduler都是运行在Driver端的,对应rdd的Stage切分也是在Driver端运行,如果用户自己写的程序有过多的步骤,切分出过多的Stage,这部分信息消耗的是Driver的内存,这个时候就需要调大Driver的内存。

#### 6.2、map过程产生大量对象导致内存溢出

​ 这种溢出原因是在单个map中产生了大量的对象导致的,可以通过减少每个Task的大小,以便达到每个Task即使产生大量的对象Executor的内存也装得下,具体点可以在产生大量对象的map操作之前调用repartition方法,分区成更小的块传入map。

#### 6.3 、数据不平衡导致内存溢出

​ 数据不平衡除了有可能导致内存溢出外,也有可能导致性能的问题,解决方法类似,就是调用repartition重新分区。

#### **6.4 、shuffle后内存溢出**

​ shuffle内存溢出的情况可以说都是shuffle后,单个文件过大导致的。在Spark中,join,reduceByKey这一类的过程,都会有shuffle的过程,在shuffle的使用,需要传入一个partitioner,大部分Spark中的shuffle操作,默认的partitioner都是HashPartitioner,默认值是父RDD中最大的分区数,这个参数通过spark.default.parallelism控制(在spark-sql中用spark.sql.shuffle.partitions),spark.default.parallelism参数只对HashPartitioner有效,所以如果是别的Partitioner或者自己实现的Partitioner就不能使用spark.default.parallelism这个参数来控制shuffle的并发量了。如果是别的partitioner导致的shuffle内存溢出,就需要从partitioner的代码增加partitions的数量。

#### **6.5 、standalone模式下资源分配不均导致内存溢出**

​ 在standalone的模式下如果配置了total-executor-cores和-executor-memory这两个参数,但是没有配置executor-cores这个参数的话,就有可能导致,每个Executor的memory是一样的,但是cores的数量不同,那么在cores数量多的Executor中,由于能够同时执行多个Task,就容易导致内存溢出的情况。

解决方法:就是同时配置executor-cores或者spark.executor.cores参数,确保executor资源分配均匀

#### 6.6 、使用rdd.persist(StorageLevel.MEMORY_AND_DISK_SER) 代替rdd.cache()

​ rdd.cache()和rdd.persist(Storage.MEMORY_ONLY)是等价的,在内存不足的时候rdd.cache()的数据会丢失,再次使用的时候会重算,而rdd.persist(StorageLevel.MEMORY_AND_DISK_SER)在内存不足的时候会存储在磁盘,避免重复,只是消耗点IO时间。

### **7、stage,task和job的区别和划分方式**

**job:**一个由多个任务组成的并行计算,当执行一个RDD的action算子时,会生成一个job。

**stage:**每个job被拆分成更小的stage的taskset,stage彼此之间是相互依赖的,各个stage会按照执行顺序依次执行。

**Task:**一个将要被发送到Executor中的工作单元,是stage的一个任务执行单元,一般来说,一个rdd有多少个partition,就会有多少个task,因为每个task只处理一个partition上的数据。

### **8、Spark提交作业参数**

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

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

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

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

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

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

### **9、Spark中reduceByKey和groupByKey和combineBykey区别和用法**

**groupByKey:**

1. 用于对每个key进行操作,将结果生成一个sequence。

2. groupByKey本身不能自定义函数。

3. 会将所有键值对进行移动,不会进行局部merge。

4. 会导致集群节点之间的开销很大,导致传输延时。

**reduceByKey**:

​ 1.用于对每个key对应的多个value进行merge操作。

​ 2.该算子能在本地先进性merge操作。

​ 3.merge操作可以通过函数进行自定义。

**combineBykey**:

​ reducebykey底层就用了combineBykey。

在大的数据集上,reduceByKey(func)的效果比groupByKey()的效果更好一些。因为reduceByKey()会在shuffle之前对数据进行合并,传输速度优于groupByKey。

### **10、foreach和map的区别**

**共同点:**用于遍历集合对象,并且对每一个对象执行指定的方法

**不同点:**

​foreach无返回值,map返回集合对象,前者用于遍历集合,后者用于映射转换集合到另一个集合

​前者的处理逻辑是串行,后者是并行。

​前者是action算子,后者是Transformation算子。

### **11、map和mapPartitions的区别**

**相同:**都属于Transformation算子

**不同:**

​ 1.本质上:

​map对rdd中的每一个元素进行操作。

​mapPartitions对rdd中的每个分区的迭代器进行操作。

​ 2.RDD中的每个分区数据量不大的情况下:

​ map操作性能比较低。比如一个partition中有1万条数据,那么在分析每个分区时,function要执行和计算1万次。

​ mapPartitions性能较高。使用mapPartitions操作之后,一个task仅仅会执行一次function,function一次接收所有的partition数据。只要执行一次就可以了。性能比较高。

​ 3.RDD中的每个分区数据量很大的情况下:(比如一个partition有100万数据)

​ map会慢慢执行完。

​ 但mapPartitions一次传入一个function后,可能一下子内存不够用,造成OOM(内存溢出)。

### **12、foreach和foreachPartition的区别**

**相同:** foreach和foreachPartition都属于行动(Action)算子

**区别:**

1. foreach每次处理RDD中的一条数据。

2. foreachPartition每次处理RDD中每个分区的迭代器中的数据。

### **13、sortByKey这个算子是全局排序么**

是全局排序。

排序的内幕:

1.在sortByKey之前将数据使用partitioner根据数据范围来分。

2.使得p1分区所有的数据小于p2,p2分区所有的数据小区p3,依次类推。(p1-pn是分区表示)

3.然后,使用sortByKey算子针对每一个Partition进行排序,这样全局的数据就被排序了。

### **14、Spark中coalesce与repartition的区别**

​ 通常认知coalesce不产生shuffle会比repartition产生shuffle效率高,但这也是要根据具体问题具体分析的。

​ repartition和coalesce两个都是对RDD的分区进行重新划分,repartition只是coalesce接口中shuffle为true的简易实现。

假设RDD有N个分区,需要重新划分成M个分区,有以下几种情况

**1.N小于M**

​ 一般情况下,N个分区有数据分布不均匀的状况,利用hashPartitioner函数将数据重新分区为M个,这时需要将shuffle设置为true。

**2.N大于M且和M相差不多**

​ 假如N是1000,M是100,那么就可以将N个分区中的若干个分区合并成一个新的分区,最终合并为M个分区,这时可以将shuffle设置为false,在shuffle为false的情况下,如果M>N时,coalesce为无效的,不进行shuffle过程,父RDD和子RDD之间是窄依赖关系。

**3.N大于M且和M相差悬殊**

​ 这时如果将shuffle设置为false,父子RDD是窄依赖关系,他们同处在一个stage中,就可能造成spark程序的并行度不够,从而影响性能,如果在M为1的时候,为了时coalesce之前的操作有更好的并行度,可以将shuffle设置为true。

总之,如果shuffle为false时,如果传入的参数大于现有的分区数目,RDD的分区数不变,就是说不经过shuffle,是无法将RDD的分区数变多的。

### **15、Spark血统的概念**

​ 主要spark它处理分布式运算环境下的数据容错性(节点实效/数据丢失)。为了保证RDD中数据的鲁棒性,RDD数据集通过所谓的血统关系(Lineage)记住了它是如何从其它RDD中演变过来的。相比其它系统的细颗粒度的内存数据更新级别的备份或者LOG机制,RDD的Lineage记录的是粗颗粒度的特定数据转换(Transformation)操作(filter, map, join etc.)行为。当这个RDD的部分分区数据丢失时,它可以通过Lineage获取足够的信息来重新运算和恢复丢失的数据分区。这种粗颗粒的数据模型,限制了Spark的运用场合,但同时相比细颗粒度的数据模型,也带来了性能的提升。

​ RDD在血统关系依赖方面分为两种:窄依赖和宽依赖,用来解决数据容错时的高效性。

​ 窄依赖:是指父RDD的每一个分区最多被一个子RDD的分区使用,表现为一个父RDD的分区对应于一个子RDD的分区或多个父RDD的分区对应于一个子RDD的分区,也就是说一个父RDD的一个分区不可能对应一个子RDD的多个分区。

​ 宽依赖:是指子RDD的分区依赖于父RDD的多个分区或所有分区,也就是说存在一个父RDD的一个分区对应一个子RDD的多个分区。

​ 对于宽依赖,这种计算的输入和输出在不同的节点上,lineage方法对于输入节点完好,而输出节点宕机时,通过重新计算,这种情况下,这种方法容错是有效的,否则无效,因为无法重试,需要向上其祖先追溯看是否可以重试(这就是Lineage,血统的意思)。窄依赖对于数据的重算开销要远小于宽依赖的数据重算开销。

​ 在RDD计算中,通过checkpoint进行容错,做checkpoint有两种方式,一是checkpointdata,二是logging the updates。我们可以控制采用那种方式来实现容错,默认是后者,通过记录跟踪所有生成RDD的转换(transformations),也就是记录每个RDD的linger(血统)来重新计算生成丢失的分区数据。

### **16、SparkRDD的持久化机制**

​ Spark速度非常快的原因之一,就是在不同操作中在内存中持久化(或缓存)一个数据集。当持久化一个RDD后,每一个节点都将把计算的分片结果保存在内存或磁盘中,并在对此数据集(或者衍生出的数据集)进行的其他动作(action)中重用。这使得后续的动作变得更加迅速(通常快10倍)。RDD相关的持久化和缓存,是Spark最重要的特征之一。可以说,缓存是Spark构建迭代式算法和快速交互式查询的关键。

其中cache是将中间结果缓存到内存中,而checkpoint是将运算结果缓存到指定的文件目录,一般为HDFS。

**1.cache()和persist()**

RDD 可以使用 persist() 方法或 cache() 方法进行持久化。数据将会在第一次 action 操作时进行计算,并缓存在节点的内存中。Spark 的缓存具有容错机制,如果一个缓存的 RDD 的某个分区丢失了,Spark 将按照原来的计算过程,自动重新计算并进行缓存。

在 shuffle 操作中(例如 reduceByKey),即便是用户没有调用 persist 方法,Spark 也会自动缓存部分中间数据。这么做的目的是,在 shuffle 的过程中某个节点运行失败时,不需要重新计算所有的输入数据。如果用户想多次使用某个 RDD,推荐在该 RDD 上调用 persist 方法。

cache()和persist()区别在于:cache是persist的一种简化方式,。cache底层就是调用persist的无参版本。同时调用persist(MEMORY_ONLY),将数据持久化到内存中,用unpersist()去除内存中的缓存

**2.checkpoint**

​**2.1原理:**

​当finalRDD执行action类算子job任务的时候,Spark会从finalRDD从后往前回溯查看哪些RDD使用了checkPoint算子,

​将使用了checkPoint的算子标记

​Spark会自动的启动一个job来重新计算标记了的RDD,并将计算的结果存入HDFS,然后中断RDD的依赖关系

​**2.2优点:**

​1.持久化在HDFS上,hdfs默认的三副本备份使得持久化的备份更加的安全

​2.切断RDD的依赖关系,当业务场景复杂的时候,RDD依赖关系非常长的时候,靠后的RDD数据丢失的时候,会经历较长时间的数据重新计算过程,采用 checkPoint会转为依赖checkPointRDD,可以避免长的linger重新计算

​3.建议checkPoint之前进行cache操作,这样会直接将内存中的结果进行checkPoint,不需要重新启动job计算

​**2.3场景:**

​当业务场景非常复杂的时候,RDD的linger依赖会非常长,一旦血统后面的RDD数据丢失的时候,Spark会根据血统依赖重新计算丢失的RDD,这样会造成计算的时间过长,这时候spark提供了一个人checkPoint功能解决这类问题

​**2.4使用:**

​为当前RDD设置检查点,并存储结果到checkPoint目录中,该目录是用sparkContext.setCheckPoint设置,对RDD的checkPoint操作并不会马上执行,必须执行Action操作才能触发。

### **17、Spark提交任务的整个流程说一下**

**1.Standalone-Client方式提交任务**

​ 1)Client模式下提交任务,在客户端启动Driver进程。

​ 2)Driver会向Master申请启动Application启动的资源。

​ 3)资源申请成功,Driver端将Task发送到Worker端执行。

​ 4)Worker将Task执行结果返回到Driver端。

**2.Standalone-Cluster方式提交任务**

​ 1)Standalone-Cluster模式提交APP后,会向Master请求启动Driver。

​ 2)Master接收请求后,随机在集群中一台节点启动Driver进程。

​ 3)Driver启动后为当前的应用程序申请资源。

​ 4)worker将执行情况和执行结果返回给Driver端。

**3.Yarn-Client方式提交任务**

​ 1)客户端提交一个Application,在客户端启动一个Driver进程。

​ 2)应用程序启动后会向ResourceManager发送请求,启动ApplicationMaster的资源。

​ 3)ResourceManager收到请求,随机选择一台NodeManager启动ApplicationMaster,这里的NodeManager相当于Standalone中的worker节点。

​ 4)ApplicationMaster启动后,会向ResourceManager请求一批container资源,用于启动Executor。

​ 5)ResourceManager会找到一批NodeManager返回给ApplicationMaster,用于启动Executor。

**4.Yarn-Cluster方式提交任务**

​ 1)客户机提交Application应用程序,发送请求到ResourceManager,请求启动ApplicationMaster。

​ 2)ResourceManager收到请求后随机在一台NodeManager上启动ApplicationMaster(相当于Driver端)。

​ 3)ApplicationMaster启动,ApplicationMaster发送请求到ResourceManager,请求一批container用于启动Executor。

​ 4)ResourceManager返回一批NodeManager节点给ApplicationMaster。

​ 5)ApplicationMaster链接到NodeManager,发送请求到NodeManager启动Executor。

​ 6)Executor反向注册到ApplicationMaster所有的节点的Driver。Driver发送task到Executor。

### **18、Spark Join的优先经验**

​ spark作为分布式的计算框架,最为影响其执行效率的地方就是频繁的网络传输。所以一般的,在不存在数据倾斜的情况下,想要提高Spark job的执行效率,就尽量减少job的shuffle过程(减少job的stage),或者减少shuffle带来的影响。

1. 尽量减少参与 join 的RDD 的数据量。

2. 尽量避免参与 join 的RDD 都具有重复的key。

3. 尽量避免或者减少 shuffle 过程。

4. 条件允许的情况下,使用 map-join 完成 join。

### **19、Spark的shuffle有几种方式**

**shuffle方式共三种,分别是:**

1. HashShuffle

2. SortShuffle (默认)

3. TungstenShuffle

**HushShuffleManager特点:**

1.数据不进行排序,速度较快。

2.直接写入缓冲区,缓冲区写满后溢写为文件。

3.本ShuffleMapStage的每一个task会生成与下一个ShuffleMapStage并行度相同的文件数量。

4.海量文件操作句柄和临时缓存信息,占用内存容易内存溢出。

**SortShuffleManager(默认)特点:**

1.会对数据进行排序。

2.在写入缓存之前,如果是reduceByKey之类的算子,则会先写入到一个Map内存数据结构中,而如果是join之类的算子,则先写入到Array内存数据结构中。在每条数据写入前先判断是否当到达一定阈值,到达则写入到缓冲区。

3.复用一个core的Task会写到同一个文件里,并生成一个索引文件。其中记录了下一个ShuffleMapStage中每一个task所要拉取数据的start offset和end offset。

### **20、哪些算子操作涉及到shuffle**

distinct 、 groupByKey 、 reduceByKey 、 aggregateByKey 、 join 、

cogroup 、 repartition

### **21、简述MapReduce的shuffle和Spark的shuffle过程**

**MapReduce:**

​ 首先MapReduce的shuffle,主要基于磁盘计算,如果数据量过大的话,那么磁盘io就会产生过大,那么此时性能会很低,计算起来速度很慢,并且MapReduce的shuffle计算默认是需要进行分组排序,那么此数据量很大,那么进行分组排序的时候,每个数据都要分到相同的分区,并且还要排序,资源大大小号,毫无效率可说。

**Spark:**

​ Spark计算主要基于内存,当内存写满,才会写到磁盘,这样速度很快,并且Sparkshuffle的操作可以不进行排序操作,这里可以设置,利用hashshuffle,和consolidation机制,而且shuffle计算可以迭代计算,通过这种设置,可以大大提高性能,并且缩短计算时间。

### 22、Spark两种共享变量:累加器和广播变量

Spark两种共享变量:广播变量(broadcast variable)与累加器(accumulator)

累加器用来对信息进行聚合,而广播变量用来高效分发较大的对象。

#### **1)、广播变量**

**广播变量的定义:**

广播变量可以让程序高效地向所有工作节点发送一个较大的只读值,以供一个或多个spark操作使用,在机器学习中非常有用。广播变量是类型为spark.broadcast.Broadcast[T]的一个对象,其中存放着类型为T的值。它由运行SparkContext的驱动程序创建后发送给会参与计算的节点,非驱动程序所在节点(即工作节点)访问改变量的方法是调用该变量的value方法,这个值只会被发送到各节点一次,作为只读值处理。

**广播变量的使用场景:**

如果我们要在分布式计算里面分发大对象,例如字典、集合、黑白名单等,这个都会由Driver端进行分发,一般来讲,如果这个变量不是广播变量,那么每个task就会分发一份,这在task数目十分多的情况下Driver的带宽会成为系统的瓶颈,而且会大量消耗task服务器上的资源,如果将这个变量声明为广播变量,那么这时每个executor拥有一份,这个executor启动的task会共享这个变量,节省了通信的成本和服务器的资源。

**广播变量的使用:**

```java

val conf = new SparkConf().setAppName("WordCount").setMaster("local")

val sc = new SparkContext(conf)

val pairRdd = sc.parallelize(List((1,1), (5,10), (5,9), (2,4), (3,5), (3,6),(4,7), (4,8),(2,3), (1,2)),4)

//将字段收集到Driver端,并广播发送到Executor

val allIpAndProvince: Array[(Int, Int)] = pairRdd.reduceByKey(_+_).collect()

//广播发送 返回引用

val broadCast: Broadcast[Array[(Int, Int)]] = sc.broadcast(allIpAndProvince)

val value24: RDD[(Int, Int)] = pairRdd.map(x => {

//使用广播变量 进行优化

val value: Array[(Int, Int)] = broadCast.value

val map: Map[Int, Int] = value.toMap

(x._1, x._2 + map.get(x._1).get)

})

```

**注意:**

​ 1、不能将RDD作为广播变量广播出去,RDD是不存储数据的。可以将RDD的结果广播出去。

​ 2、广播变量只能在Driver端定义,不能在Executor端定义。

#### **2)、累加器**

**累加器的定义:**

spark累加器就是定义在驱动程序的一个变量,但是在集群中的每一个任务都会有一份新的副本。在各个任务中更新副本的值都不会对驱动程序中的值产生影响,只有到最后所有的任务都计算完成后才会合并每一个任务的副本到驱动程序。

**累加器的使用场景:**

异常监控,调试,记录符合某特性的数据的数目等。如果一个变量不被声明为一个累加器,那么它将在被改变时不会再driver端进行全局汇总,即在分布式运行时每个task运行的只是原始变量的一个副本,并不能改变原始变量的值,但是当这个变量被声明为累加器后,该变量就会有分布式计数的功能。

**累加器的使用:**

```java

val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("a", 3), ("b", 4)))

// 声明累加器

val sumAcc: LongAccumulator = sc.longAccumulator("sumAcc")

rdd.foreach {

case (word, count) => {

// 使用累加器

sumAcc.add(count)

}

}

// 累加器的toString方法

//println(sumAcc)

//取出累加器中的值

println(sumAcc.value)

```

**注意:**

1、累加器在Driver端定义赋初始值,累加器只能在Driver端读取最后的值,在Excutor端更新。

2、累加器不是一个调优的操作,因为如果不这样做,结果是错的。

### **23、数据倾斜原因及解决方案**

#### **一、数据倾斜发生的原理**

​ 在进行shuffle的时候,必须将各个数据节点上相同的key拉取到某个节点的一个task上来处理,比如按照key进行聚合或join等操作。此时如果某个key对应的数据量特别大的话,就会发生数据倾斜。比如大部分key对应10条数据,但是个别key却对应了100万条数据,那么大部分task可能就只会分配到10条数据,然后1秒钟就运行完了;但是个别task可能分配到了100万数据,要运行一两个小时。因此,整个Spark作业的运行进度是由运行时间最长的那个task决定的。因此出现数据倾斜的时候,Spark作业看起来会运行得非常缓慢,甚至可能因为某个task处理的数据量过大导致内存溢出。

#### **二、如何定位导致数据倾斜的代码**

​ 数据倾斜只会发生在shuffle过程中。这里给大家罗列一些常用的并且可能会触发shuffle操作的算子:distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition等。出现数据倾斜时,可能就是你的代码中使用了这些算子中的某一个所导致的。

##### 1) 某个task执行特别慢的情况

首先要看的,就是数据倾斜发生在第几个stage中。

​ 如果是用yarn-client模式提交,那么本地是直接可以看到log的,可以在log中找到当前运行到了第几个stage;如果是用yarn-cluster模式提交,则可以通过Spark Web UI来查看当前运行到了第几个stage。此外,无论是使用yarn-client模式还是yarn-cluster模式,我们都可以在Spark Web UI上深入看一下当前这个stage各个task分配的数据量,从而进一步确定是不是task分配的数据不均匀导致了数据倾斜。

​ 知道数据倾斜发生在哪一个stage之后,接着我们就需要根据stage划分原理,推算出来发生倾斜的那个stage对应代码中的哪一部分,这部分代码中肯定会有一个shuffle类算子。有一个相对简单实用的推算方法:只要看到Spark代码中出现了一个shuffle类算子或者是Spark SQL的SQL语句中出现了会导致shuffle的语句(比如group by语句),那么就可以判定,以那个地方为界限划分出了前后两个stage。比如我们在Spark Web UI或者本地log中发现,stage1的某几个task执行得特别慢,判定stage1出现了数据倾斜,那么就可以回到代码中定位出stage1主要包括了reduceByKey这个shuffle类算子,此时基本就可以确定是由reduceByKey算子导致的数据倾斜问题。比如某个单词出现了100万次,其他单词才出现10次,那么stage1的某个task就要处理100万数据,整个stage的速度就会被这个task拖慢。

##### 2) 某个task莫名其妙内存溢出的情况

​ 这种情况下去定位出问题的代码就比较容易了。我们建议直接看yarn-client模式下本地log的异常栈,或者是通过YARN查看yarn-cluster模式下的log中的异常栈。一般来说,通过异常栈信息就可以定位到你的代码中哪一行发生了内存溢出。然后在那行代码附近找找,一般也会有shuffle类算子,此时很可能就是这个算子导致了数据倾斜。

​ 但是大家要注意的是,不能单纯靠偶然的内存溢出就判定发生了数据倾斜。因为自己编写的代码的bug,以及偶然出现的数据异常,也可能会导致内存溢出。因此还是要按照上面所讲的方法,通过Spark Web UI查看报错的那个stage的各个task的运行时间以及分配的数据量,才能确定是否是由于数据倾斜才导致了这次内存溢出。

##### 3) 查看导致数据倾斜的key的数据分布情况

​ 知道了数据倾斜发生在哪里之后,通常需要分析一下那个执行了shuffle操作并且导致了数据倾斜的RDD/Hive表,查看一下其中key的分布情况。这主要是为之后选择哪一种技术方案提供依据。针对不同的key分布与不同的shuffle算子组合起来的各种情况,可能需要选择不同的技术方案来解决。

此时根据你执行操作的情况不同,可以有很多种查看key分布的方式:

1. 如果是Spark SQL中的group by、join语句导致的数据倾斜,那么就查询一下SQL中使用的表的key分布情况。

2. 如果是对Spark RDD执行shuffle算子导致的数据倾斜,那么可以在Spark作业中加入查看key分布的代码,比如RDD.countByKey()。然后对统计出来的各个key出现的次数,collect/take到客户端打印一下,就可以看到key的分布情况。

#### **三、数据倾斜的解决方案**

##### 1)解决方案一:使用Hive ETL预处理数据

**方案适用场景:**导致数据倾斜的是Hive表。如果该Hive表中的数据本身很不均匀(比如某个key对应了100万数据,其他key才对应了10条数据),而且业务场景需要频繁使用Spark对Hive表执行某个分析操作,那么比较适合使用这种技术方案。

**方案实现思路:**此时可以评估一下,是否可以通过Hive来进行数据预处理(即通过Hive ETL预先对数据按照key进行聚合,或者是预先和其他表进行join),然后在Spark作业中针对的数据源就不是原来的Hive表了,而是预处理后的Hive表。此时由于数据已经预先进行过聚合或join操作了,那么在Spark作业中也就不需要使用原先的shuffle类算子执行这类操作了。

**方案实现原理:**这种方案从根源上解决了数据倾斜,因为彻底避免了在Spark中执行shuffle类算子,那么肯定就不会有数据倾斜的问题了。但是这里也要提醒一下大家,这种方式属于治标不治本。因为毕竟数据本身就存在分布不均匀的问题,所以Hive ETL中进行group by或者join等shuffle操作时,还是会出现数据倾斜,导致Hive ETL的速度很慢。我们只是把数据倾斜的发生提前到了Hive ETL中,避免Spark程序发生数据倾斜而已。

**方案优点:**实现起来简单便捷,效果还非常好,完全规避掉了数据倾斜,Spark作业的性能会大幅度提升。

**方案缺点:**治标不治本,Hive ETL中还是会发生数据倾斜。

**方案实践经验:**在一些Java系统与Spark结合使用的项目中,会出现Java代码频繁调用Spark作业的场景,而且对Spark作业的执行性能要求很高,就比较适合使用这种方案。将数据倾斜提前到上游的Hive ETL,每天仅执行一次,只有那一次是比较慢的,而之后每次Java调用Spark作业时,执行速度都会很快,能够提供更好的用户体验。

**项目实践经验:**在美团·点评的交互式用户行为分析系统中使用了这种方案,该系统主要是允许用户通过Java Web系统提交数据分析统计任务,后端通过Java提交Spark作业进行数据分析统计。要求Spark作业速度必须要快,尽量在10分钟以内,否则速度太慢,用户体验会很差。所以我们将有些Spark作业的shuffle操作提前到了Hive ETL中,从而让Spark直接使用预处理的Hive中间表,尽可能地减少Spark的shuffle操作,大幅度提升了性能,将部分作业的性能提升了6倍以上。

##### 2)解决方案二:过滤少数导致倾斜的key

**方案适用场景:**如果发现导致倾斜的key就少数几个,而且对计算本身的影响并不大的话,那么很适合使用这种方案。比如99%的key就对应10条数据,但是只有一个key对应了100万数据,从而导致了数据倾斜。

**方案实现思路:**如果我们判断那少数几个数据量特别多的key,对作业的执行和计算结果不是特别重要的话,那么干脆就直接过滤掉那少数几个key。比如,在Spark SQL中可以使用where条件过滤掉这些key或者在Spark Core中对RDD执行filter算子过滤掉这些key。如果需要每次作业执行时,动态判定哪些key的数据量最多然后再进行过滤,那么可以使用sample算子对RDD进行采样,然后计算出每个key的数量,取数据量最多的key过滤掉即可。

**方案实现原理:**将导致数据倾斜的key给过滤掉之后,这些key就不会参与计算了,自然不可能产生数据倾斜。

**方案优点:**实现简单,而且效果也很好,可以完全规避掉数据倾斜。

**方案缺点:**适用场景不多,大多数情况下,导致倾斜的key还是很多的,并不是只有少数几个。

**方案实践经验:**在项目中我们也采用过这种方案解决数据倾斜。有一次发现某一天Spark作业在运行的时候突然OOM了,追查之后发现,是Hive表中的某一个key在那天数据异常,导致数据量暴增。因此就采取每次执行前先进行采样,计算出样本中数据量最大的几个key之后,直接在程序中将那些key给过滤掉。

##### 3)解决方案三:提高shuffle操作的并行度

**方案适用场景:**如果我们必须要对数据倾斜迎难而上,那么建议优先使用这种方案,因为这是处理数据倾斜最简单的一种方案。

**方案实现思路:**在对RDD执行shuffle算子时,给shuffle算子传入一个参数,比如reduceByKey(1000),该参数就设置了这个shuffle算子执行时shuffle read task的数量。对于Spark SQL中的shuffle类语句,比如group by、join等,需要设置一个参数,即spark.sql.shuffle.partitions,该参数代表了shuffle read task的并行度,该值默认是200,对于很多场景来说都有点过小。

**方案实现原理:**增加shuffle read task的数量,可以让原本分配给一个task的多个key分配给多个task,从而让每个task处理比原来更少的数据。举例来说,如果原本有5个key,每个key对应10条数据,这5个key都是分配给一个task的,那么这个task就要处理50条数据。而增加了shuffle read task以后,每个task就分配到一个key,即每个task就处理10条数据,那么自然每个task的执行时间都会变短了。具体原理如下图所示。

**方案优点:**实现起来比较简单,可以有效缓解和减轻数据倾斜的影响。

**方案缺点:**只是缓解了数据倾斜而已,没有彻底根除问题,根据实践经验来看,其效果有限。

**方案实践经验:**该方案通常无法彻底解决数据倾斜,因为如果出现一些极端情况,比如某个key对应的数据量有100万,那么无论你的task数量增加到多少,这个对应着100万数据的key肯定还是会分配到一个task中去处理,因此注定还是会发生数据倾斜的。所以这种方案只能说是在发现数据倾斜时尝试使用的第一种手段,尝试去用嘴简单的方法缓解数据倾斜而已,或者是和其他方案结合起来使用。

##### 4)解决方案四:两阶段聚合(局部聚合+全局聚合)

**方案适用场景:**对RDD执行reduceByKey等聚合类shuffle算子或者在Spark SQL中使用group by语句进行分组聚合时,比较适用这种方案。

**方案实现思路:**这个方案的核心实现思路就是进行两阶段聚合。第一次是局部聚合,先给每个key都打上一个随机数,比如10以内的随机数,此时原先一样的key就变成不一样的了,比如(hello, 1) (hello, 1) (hello, 1) (hello, 1),就会变成(1_hello, 1) (1_hello, 1) (2_hello, 1) (2_hello, 1)。接着对打上随机数后的数据,执行reduceByKey等聚合操作,进行局部聚合,那么局部聚合结果,就会变成了(1_hello, 2) (2_hello, 2)。然后将各个key的前缀给去掉,就会变成(hello,2)(hello,2),再次进行全局聚合操作,就可以得到最终结果了,比如(hello, 4)。

**方案实现原理:**将原本相同的key通过附加随机前缀的方式,变成多个不同的key,就可以让原本被一个task处理的数据分散到多个task上去做局部聚合,进而解决单个task处理数据量过多的问题。接着去除掉随机前缀,再次进行全局聚合,就可以得到最终的结果。具体原理见下图。

**方案优点:**对于聚合类的shuffle操作导致的数据倾斜,效果是非常不错的。通常都可以解决掉数据倾斜,或者至少是大幅度缓解数据倾斜,将Spark作业的性能提升数倍以上。

**方案缺点:**仅仅适用于聚合类的shuffle操作,适用范围相对较窄。如果是join类的shuffle操作,还得用其他的解决方案。

```java

// 第一步,给RDD中的每个key都打上一个随机前缀。

JavaPairRDD<String, Long> randomPrefixRdd = rdd.mapToPair(

new PairFunction<Tuple2<Long,Long>, String, Long>() {

private static final long serialVersionUID = 1L;

@Override

public Tuple2<String, Long> call(Tuple2<Long, Long> tuple)

throws Exception {

Random random = new Random();

int prefix = random.nextInt(10);

return new Tuple2<String, Long>(prefix + "_" + tuple._1, tuple._2);

}

});

// 第二步,对打上随机前缀的key进行局部聚合。

JavaPairRDD<String, Long> localAggrRdd = randomPrefixRdd.reduceByKey(

new Function2<Long, Long, Long>() {

private static final long serialVersionUID = 1L;

@Override

public Long call(Long v1, Long v2) throws Exception {

return v1 + v2;

}

});

// 第三步,去除RDD中每个key的随机前缀。

JavaPairRDD<Long, Long> removedRandomPrefixRdd = localAggrRdd.mapToPair(

new PairFunction<Tuple2<String,Long>, Long, Long>() {

private static final long serialVersionUID = 1L;

@Override

public Tuple2<Long, Long> call(Tuple2<String, Long> tuple)

throws Exception {

long originalKey = Long.valueOf(tuple._1.split("_")[1]);

return new Tuple2<Long, Long>(originalKey, tuple._2);

}

});

// 第四步,对去除了随机前缀的RDD进行全局聚合。

JavaPairRDD<Long, Long> globalAggrRdd = removedRandomPrefixRdd.reduceByKey(

new Function2<Long, Long, Long>() {

private static final long serialVersionUID = 1L;

@Override

public Long call(Long v1, Long v2) throws Exception {

return v1 + v2;

}

});

```

##### 5)解决方案五:将reduce join转为map join

**方案适用场景:**在对RDD使用join类操作,或者是在Spark SQL中使用join语句时,而且join操作中的一个RDD或表的数据量比较小(比如几百M或者一两G),比较适用此方案。

**方案实现思路:**不使用join算子进行连接操作,而使用Broadcast变量与map类算子实现join操作,进而完全规避掉shuffle类的操作,彻底避免数据倾斜的发生和出现。将较小RDD中的数据直接通过collect算子拉取到Driver端的内存中来,然后对其创建一个Broadcast变量;接着对另外一个RDD执行map类算子,在算子函数内,从Broadcast变量中获取较小RDD的全量数据,与当前RDD的每一条数据按照连接key进行比对,如果连接key相同的话,那么就将两个RDD的数据用你需要的方式连接起来。

**方案实现原理:**普通的join是会走shuffle过程的,而一旦shuffle,就相当于会将相同key的数据拉取到一个shuffle read task中再进行join,此时就是reduce join。但是如果一个RDD是比较小的,则可以采用广播小RDD全量数据+map算子来实现与join同样的效果,也就是map join,此时就不会发生shuffle操作,也就不会发生数据倾斜。具体原理如下图所示。

**方案优点:**对join操作导致的数据倾斜,效果非常好,因为根本就不会发生shuffle,也就根本不会发生数据倾斜。

**方案缺点:**适用场景较少,因为这个方案只适用于一个大表和一个小表的情况。毕竟我们需要将小表进行广播,此时会比较消耗内存资源,driver和每个Executor内存中都会驻留一份小RDD的全量数据。如果我们广播出去的RDD数据比较大,比如10G以上,那么就可能发生内存溢出了。因此并不适合两个都是大表的情况。

```java

// 首先将数据量比较小的RDD的数据,collect到Driver中来。

List<Tuple2<Long, Row>> rdd1Data = rdd1.collect()

// 然后使用Spark的广播功能,将小RDD的数据转换成广播变量,这样每个Executor就只有一份RDD的数据。

// 可以尽可能节省内存空间,并且减少网络传输性能开销。

final Broadcast<List<Tuple2<Long, Row>>> rdd1DataBroadcast = sc.broadcast(rdd1Data);

// 对另外一个RDD执行map类操作,而不再是join类操作。

JavaPairRDD<String, Tuple2<String, Row>> joinedRdd = rdd2.mapToPair(

new PairFunction<Tuple2<Long,String>, String, Tuple2<String, Row>>() {

private static final long serialVersionUID = 1L;

@Override

public Tuple2<String, Tuple2<String, Row>> call(Tuple2<Long, String> tuple)

throws Exception {

// 在算子函数中,通过广播变量,获取到本地Executor中的rdd1数据。

List<Tuple2<Long, Row>> rdd1Data = rdd1DataBroadcast.value();

// 可以将rdd1的数据转换为一个Map,便于后面进行join操作。

Map<Long, Row> rdd1DataMap = new HashMap<Long, Row>();

for(Tuple2<Long, Row> data : rdd1Data) {

rdd1DataMap.put(data._1, data._2);

}

// 获取当前RDD数据的key以及value。

String key = tuple._1;

String value = tuple._2;

// 从rdd1数据Map中,根据key获取到可以join到的数据。

Row rdd1Value = rdd1DataMap.get(key);

return new Tuple2<String, String>(key, new Tuple2<String, Row>(value, rdd1Value));

}

});

// 这里得提示一下。

// 上面的做法,仅仅适用于rdd1中的key没有重复,全部是唯一的场景。

// 如果rdd1中有多个相同的key,那么就得用flatMap类的操作,在进行join的时候不能用map,而是得遍历rdd1所有数据进行join。

// rdd2中每条数据都可能会返回多条join后的数据。

```

##### 6)解决方案六:采样倾斜key并分拆join操作

**方案适用场景:**两个RDD/Hive表进行join的时候,如果数据量都比较大,无法采用“解决方案五”,那么此时可以看一下两个RDD/Hive表中的key分布情况。如果出现数据倾斜,是因为其中某一个RDD/Hive表中的少数几个key的数据量过大,而另一个RDD/Hive表中的所有key都分布比较均匀,那么采用这个解决方案是比较合适的。

**方案实现思路:** * 对包含少数几个数据量过大的key的那个RDD,通过sample算子采样出一份样本来,然后统计一下每个key的数量,计算出来数据量最大的是哪几个key。 * 然后将这几个key对应的数据从原来的RDD中拆分出来,形成一个单独的RDD,并给每个key都打上n以内的随机数作为前缀,而不会导致倾斜的大部分key形成另外一个RDD。 * 接着将需要join的另一个RDD,也过滤出来那几个倾斜key对应的数据并形成一个单独的RDD,将每条数据膨胀成n条数据,这n条数据都按顺序附加一个0~n的前缀,不会导致倾斜的大部分key也形成另外一个RDD。 * 再将附加了随机前缀的独立RDD与另一个膨胀n倍的独立RDD进行join,此时就可以将原先相同的key打散成n份,分散到多个task中去进行join了。 * 而另外两个普通的RDD就照常join即可。 * 最后将两次join的结果使用union算子合并起来即可,就是最终的join结果。

**方案实现原理:**对于join导致的数据倾斜,如果只是某几个key导致了倾斜,可以将少数几个key分拆成独立RDD,并附加随机前缀打散成n份去进行join,此时这几个key对应的数据就不会集中在少数几个task上,而是分散到多个task进行join了。具体原理见下图。

**方案优点:**对于join导致的数据倾斜,如果只是某几个key导致了倾斜,采用该方式可以用最有效的方式打散key进行join。而且只需要针对少数倾斜key对应的数据进行扩容n倍,不需要对全量数据进行扩容。避免了占用过多内存。

**方案缺点:**如果导致倾斜的key特别多的话,比如成千上万个key都导致数据倾斜,那么这种方式也不适合。

```java

// 首先从包含了少数几个导致数据倾斜key的rdd1中,采样10%的样本数据。

JavaPairRDD<Long, String> sampledRDD = rdd1.sample(false, 0.1);

// 对样本数据RDD统计出每个key的出现次数,并按出现次数降序排序。

// 对降序排序后的数据,取出top 1或者top 100的数据,也就是key最多的前n个数据。

// 具体取出多少个数据量最多的key,由大家自己决定,我们这里就取1个作为示范。

JavaPairRDD<Long, Long> mappedSampledRDD = sampledRDD.mapToPair(

new PairFunction<Tuple2<Long,String>, Long, Long>() {

private static final long serialVersionUID = 1L;

@Override

public Tuple2<Long, Long> call(Tuple2<Long, String> tuple)

throws Exception {

return new Tuple2<Long, Long>(tuple._1, 1L);

}

});

JavaPairRDD<Long, Long> countedSampledRDD = mappedSampledRDD.reduceByKey(

new Function2<Long, Long, Long>() {

private static final long serialVersionUID = 1L;

@Override

public Long call(Long v1, Long v2) throws Exception {

return v1 + v2;

}

});

JavaPairRDD<Long, Long> reversedSampledRDD = countedSampledRDD.mapToPair(

new PairFunction<Tuple2<Long,Long>, Long, Long>() {

private static final long serialVersionUID = 1L;

@Override

public Tuple2<Long, Long> call(Tuple2<Long, Long> tuple)

throws Exception {

return new Tuple2<Long, Long>(tuple._2, tuple._1);

}

});

final Long skewedUserid = reversedSampledRDD.sortByKey(false).take(1).get(0)._2;

// 从rdd1中分拆出导致数据倾斜的key,形成独立的RDD。

JavaPairRDD<Long, String> skewedRDD = rdd1.filter(

new Function<Tuple2<Long,String>, Boolean>() {

private static final long serialVersionUID = 1L;

@Override

public Boolean call(Tuple2<Long, String> tuple) throws Exception {

return tuple._1.equals(skewedUserid);

}

});

// 从rdd1中分拆出不导致数据倾斜的普通key,形成独立的RDD。

JavaPairRDD<Long, String> commonRDD = rdd1.filter(

new Function<Tuple2<Long,String>, Boolean>() {

private static final long serialVersionUID = 1L;

@Override

public Boolean call(Tuple2<Long, String> tuple) throws Exception {

return !tuple._1.equals(skewedUserid);

}

});

// rdd2,就是那个所有key的分布相对较为均匀的rdd。

// 这里将rdd2中,前面获取到的key对应的数据,过滤出来,分拆成单独的rdd,并对rdd中的数据使用flatMap算子都扩容100倍。

// 对扩容的每条数据,都打上0~100的前缀。

JavaPairRDD<String, Row> skewedRdd2 = rdd2.filter(

new Function<Tuple2<Long,Row>, Boolean>() {

private static final long serialVersionUID = 1L;

@Override

public Boolean call(Tuple2<Long, Row> tuple) throws Exception {

return tuple._1.equals(skewedUserid);

}

}).flatMapToPair(new PairFlatMapFunction<Tuple2<Long,Row>, String, Row>() {

private static final long serialVersionUID = 1L;

@Override

public Iterable<Tuple2<String, Row>> call(

Tuple2<Long, Row> tuple) throws Exception {

Random random = new Random();

List<Tuple2<String, Row>> list = new ArrayList<Tuple2<String, Row>>();

for(int i = 0; i < 100; i++) {

list.add(new Tuple2<String, Row>(i + "_" + tuple._1, tuple._2));

}

return list;

}

});

// 将rdd1中分拆出来的导致倾斜的key的独立rdd,每条数据都打上100以内的随机前缀。

// 然后将这个rdd1中分拆出来的独立rdd,与上面rdd2中分拆出来的独立rdd,进行join。

JavaPairRDD<Long, Tuple2<String, Row>> joinedRDD1 = skewedRDD.mapToPair(

new PairFunction<Tuple2<Long,String>, String, String>() {

private static final long serialVersionUID = 1L;

@Override

public Tuple2<String, String> call(Tuple2<Long, String> tuple)

throws Exception {

Random random = new Random();

int prefix = random.nextInt(100);

return new Tuple2<String, String>(prefix + "_" + tuple._1, tuple._2);

}

})

.join(skewedUserid2infoRDD)

.mapToPair(new PairFunction<Tuple2<String,Tuple2<String,Row>>, Long, Tuple2<String, Row>>() {

private static final long serialVersionUID = 1L;

@Override

public Tuple2<Long, Tuple2<String, Row>> call(

Tuple2<String, Tuple2<String, Row>> tuple)

throws Exception {

long key = Long.valueOf(tuple._1.split("_")[1]);

return new Tuple2<Long, Tuple2<String, Row>>(key, tuple._2);

}

});

// 将rdd1中分拆出来的包含普通key的独立rdd,直接与rdd2进行join。

JavaPairRDD<Long, Tuple2<String, Row>> joinedRDD2 = commonRDD.join(rdd2);

// 将倾斜key join后的结果与普通key join后的结果,uinon起来。

// 就是最终的join结果。

JavaPairRDD<Long, Tuple2<String, Row>> joinedRDD = joinedRDD1.union(joinedRDD2);

```

##### 7)解决方案七:使用随机前缀和扩容RDD进行join

**方案适用场景:**如果在进行join操作时,RDD中有大量的key导致数据倾斜,那么进行分拆key也没什么意义,此时就只能使用最后一种方案来解决问题了。

**方案实现思路:** * 该方案的实现思路基本和“解决方案六”类似,首先查看RDD/Hive表中的数据分布情况,找到那个造成数据倾斜的RDD/Hive表,比如有多个key都对应了超过1万条数据。 * 然后将该RDD的每条数据都打上一个n以内的随机前缀。 * 同时对另外一个正常的RDD进行扩容,将每条数据都扩容成n条数据,扩容出来的每条数据都依次打上一个0~n的前缀。 * 最后将两个处理后的RDD进行join即可。

**方案实现原理:**将原先一样的key通过附加随机前缀变成不一样的key,然后就可以将这些处理后的“不同key”分散到多个task中去处理,而不是让一个task处理大量的相同key。该方案与“解决方案六”的不同之处就在于,上一种方案是尽量只对少数倾斜key对应的数据进行特殊处理,由于处理过程需要扩容RDD,因此上一种方案扩容RDD后对内存的占用并不大;而这一种方案是针对有大量倾斜key的情况,没法将部分key拆分出来进行单独处理,因此只能对整个RDD进行数据扩容,对内存资源要求很高。

**方案优点:**对join类型的数据倾斜基本都可以处理,而且效果也相对比较显著,性能提升效果非常不错。

**方案缺点:**该方案更多的是缓解数据倾斜,而不是彻底避免数据倾斜。而且需要对整个RDD进行扩容,对内存资源要求很高。

**方案实践经验:**曾经开发一个数据需求的时候,发现一个join导致了数据倾斜。优化之前,作业的执行时间大约是60分钟左右;使用该方案优化之后,执行时间缩短到10分钟左右,性能提升了6倍。

```java

// 首先将其中一个key分布相对较为均匀的RDD膨胀100倍。

JavaPairRDD<String, Row> expandedRDD = rdd1.flatMapToPair(

new PairFlatMapFunction<Tuple2<Long,Row>, String, Row>() {

private static final long serialVersionUID = 1L;

@Override

public Iterable<Tuple2<String, Row>> call(Tuple2<Long, Row> tuple)

throws Exception {

List<Tuple2<String, Row>> list = new ArrayList<Tuple2<String, Row>>();

for(int i = 0; i < 100; i++) {

list.add(new Tuple2<String, Row>(0 + "_" + tuple._1, tuple._2));

}

return list;

}

});

// 其次,将另一个有数据倾斜key的RDD,每条数据都打上100以内的随机前缀。

JavaPairRDD<String, String> mappedRDD = rdd2.mapToPair(

new PairFunction<Tuple2<Long,String>, String, String>() {

private static final long serialVersionUID = 1L;

@Override

public Tuple2<String, String> call(Tuple2<Long, String> tuple)

throws Exception {

Random random = new Random();

int prefix = random.nextInt(100);

return new Tuple2<String, String>(prefix + "_" + tuple._1, tuple._2);

}

});

// 将两个处理后的RDD进行join即可。

JavaPairRDD<String, Tuple2<String, Row>> joinedRDD = mappedRDD.join(expandedRDD);

```

### **24、请列举Spark的transformation算子**

**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 合并成一个集合。

a. createCombiner: combineByKey() 会遍历分区中的所有元素,因此每个元素的键要么还没有遇到过,要么就和之前的某个元素的键相同。如果这是一个新的元素,combineByKey()会使用一个叫作 createCombiner()的函数来创建那个键对应的累加器的初始值

b. mergeValue: 如果这是一个在处理当前分区之前已经遇到的键,它会使用 mergeValue()方法将该键的累加器对应的当前值与这个新的值进行合并

c. mergeCombiners: 由于每个分区都是独立处理的, 因此对于同一个键可以有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器, 就需要使用用户提供的mergeCombiners() 方法将各个分区的结果进行合并。

### **25、请列举Spark的action算子,并简述功能**

1. reduce:

2. collect:

3. first:

4. take:

5. aggregate:

6. countByKey:

7. foreach:

8. saveAsTextFile

### **26、Spark调优**

#### **原则一:避免创建重复的RDD**

​对于同一份数据,只应该创建一个RDD,不能创建多个RDD来代表同一份数据。

#### **原则二:尽可能复用同一个RDD**

​除了要避免在开发过程中对一份完全相同的数据创建多个RDD之外,在对不同的数据执行算子操作时还要尽可能地复用一个RDD。比如说,有一个RDD的数据格式是key-value类型的,另一个是单value类型的,这两个RDD的value数据是完全一样的。那么此时我们可以只使用key-value类型的那个RDD,因为其中已经包含了另一个的数据。对于类似这种多个RDD的数据有重叠或者包含的情况,我们应该尽量复用一个RDD,这样可以尽可能地减少RDD的数量,从而尽可能减少算子执行的次数。

#### **原则三:对多次使用的RDD进行持久化**

​Spark中对于一个RDD执行多次算子的默认原理是这样的:每次你对一个RDD执行一个算子操作时,都会重新从源头处计算一遍,计算出那个RDD来,然后再对这个RDD执行你的算子操作。这种方式的性能是很差的。因此对于这种情况,我们的建议是:对多次使用的RDD进行持久化。此时Spark就会根据你的持久化策略,将RDD中的数据保存到内存或者磁盘中。以后每次对这个RDD进行算子操作时,都会直接从内存或磁盘中提取持久化的RDD数据,然后执行算子,而不会从源头处重新计算一遍这个RDD,再执行算子操作。

​如何选择一种最合适的持久化策略

- 默认情况下,性能最高的当然是MEMORY_ONLY,但前提是你的内存必须足够足够大,可以绰绰有余地存放下整个RDD的所有数据。因为不进行序列化与反序列化操作,就避免了这部分的性能开销;对这个RDD的后续算子操作,都是基于纯内存中的数据的操作,不需要从磁盘文件中读取数据,性能也很高;而且不需要复制一份数据副本,并远程传送到其他节点上。但是这里必须要注意的是,在实际的生产环境中,恐怕能够直接用这种策略的场景还是有限的,如果RDD中数据比较多时(比如几十亿),直接用这种持久化级别,会导致JVM的OOM内存溢出异常。

- 如果使用MEMORY_ONLY级别时发生了内存溢出,那么建议尝试使用MEMORY_ONLY_SER级别。该级别会将RDD数据序列化后再保存在内存中,此时每个partition仅仅是一个字节数组而已,大大减少了对象数量,并降低了内存占用。这种级别比MEMORY_ONLY多出来的性能开销,主要就是序列化与反序列化的开销。但是后续算子可以基于纯内存进行操作,因此性能总体还是比较高的。此外,可能发生的问题同上,如果RDD中的数据量过多的话,还是可能会导致OOM内存溢出的异常。

- 如果纯内存的级别都无法使用,那么建议使用MEMORY_AND_DISK_SER策略,而不是MEMORY_AND_DISK策略。因为既然到了这一步,就说明RDD的数据量很大,内存无法完全放下。序列化后的数据比较少,可以节省内存和磁盘的空间开销。同时该策略会优先尽量尝试将数据缓存在内存中,内存缓存不下才会写入磁盘。

- 通常不建议使用DISK_ONLY和后缀为_2的级别:因为完全基于磁盘文件进行数据的读写,会导致性能急剧降低,有时还不如重新计算一次所有RDD。后缀为_2的级别,必须将所有数据都复制一份副本,并发送到其他节点上,数据复制以及网络传输会导致较大的性能开销,除非是要求作业的高可用性,否则不建议使用。

#### **原则四:尽量避免使用shuffle类算子**

​如果有可能的话,要尽量避免使用shuffle类算子。因为Spark作业运行过程中,最消耗性能的地方就是shuffle过程。shuffle过程,简单来说,就是将分布在集群中多个节点上的同一个key,拉取到同一个节点上,进行聚合或join等操作。比如reduceByKey、join等算子,都会触发shuffle操作。

shuffle过程中,各个节点上的相同key都会先写入本地磁盘文件中,然后其他节点需要通过网络传输拉取各个节点上的磁盘文件中的相同key。而且相同key都拉取到同一个节点进行聚合操作时,还有可能会因为一个节点上处理的key过多,导致内存不够存放,进而溢写到磁盘文件中。因此在shuffle过程中,可能会发生大量的磁盘文件读写的IO操作,以及数据的网络传输操作。磁盘IO和网络数据传输也是shuffle性能较差的主要原因。

因此在我们的开发过程中,能避免则尽可能避免使用reduceByKey、join、distinct、repartition等会进行shuffle的算子,尽量使用map类的非shuffle算子。这样的话,没有shuffle操作或者仅有较少shuffle操作的Spark作业,可以大大减少性能开销。

#### **原则五:使用map-side预聚合的shuffle操作**

​如果因为业务需要,一定要使用shuffle操作,无法用map类的算子来替代,那么尽量使用可以map-side预聚合的算子。

所谓的map-side预聚合,说的是在每个节点本地对相同的key进行一次聚合操作,类似于MapReduce中的本地combiner。map-side预聚合之后,每个节点本地就只会有一条相同的key,因为多条相同的key都被聚合起来了。其他节点在拉取所有节点上的相同key时,就会大大减少需要拉取的数据数量,从而也就减少了磁盘IO以及网络传输开销。通常来说,在可能的情况下,建议使用reduceByKey或者aggregateByKey算子来替代掉groupByKey算子。因为reduceByKey和aggregateByKey算子都会使用用户自定义的函数对每个节点本地的相同key进行预聚合。而groupByKey算子是不会进行预聚合的,全量的数据会在集群的各个节点之间分发和传输,性能相对来说比较差。

#### **原则六:使用高性能的算子**

**1、使用reduceByKey/aggregateByKey替代groupByKey**

**2、使用mapPartitions替代普通map**

​mapPartitions类的算子,一次函数调用会处理一个partition所有的数据,而不是一次函数调用处理一条,性能相对来说会高一些。但是有的时候,使用mapPartitions会出现OOM(内存溢出)的问题。因为单次函数调用就要处理掉一个partition所有的数据,如果内存不够,垃圾回收时是无法回收掉太多对象的,很可能出现OOM异常。所以使用这类操作时要慎重!

**3、使用foreachPartitions替代foreach**

​原理类似于“使用mapPartitions替代map”,也是一次函数调用处理一个partition的所有数据,而不是一次函数调用处理一条数据。在实践中发现,foreachPartitions类的算子,对性能的提升还是很有帮助的。比如在foreach函数中,将RDD中所有数据写**MySQL**,那么如果是普通的foreach算子,就会一条数据一条数据地写,每次函数调用可能就会创建一个数据库连接,此时就势必会频繁地创建和销毁数据库连接,性能是非常低下;但是如果用foreachPartitions算子一次性处理一个partition的数据,那么对于每个partition,只要创建一个数据库连接即可,然后执行批量插入操作,此时性能是比较高的。实践中发现,对于1万条左右的数据量写MySQL,性能可以提升30%以上。

**4、使用filter之后进行coalesce操作**

​通常对一个RDD执行filter算子过滤掉RDD中较多数据后(比如30%以上的数据),建议使用coalesce算子,手动减少RDD的partition数量,将RDD中的数据压缩到更少的partition中去。因为filter之后,RDD的每个partition中都会有很多数据被过滤掉,此时如果照常进行后续的计算,其实每个task处理的partition中的数据量并不是很多,有一点资源浪费,而且此时处理的task越多,可能速度反而越慢。因此用coalesce减少partition数量,将RDD中的数据压缩到更少的partition之后,只要使用更少的task即可处理完所有的partition。在某些场景下,对于性能的提升会有一定的帮助。

**5、使用repartitionAndSortWithinPartitions替代repartition与sort类操作**

​ repartitionAndSortWithinPartitions是Spark官网推荐的一个算子,官方建议,如果需要在repartition重分区之后,还要进行排序,建议直接使用repartitionAndSortWithinPartitions算子。因为该算子可以一边进行重分区的shuffle操作,一边进行排序。shuffle与sort两个操作同时进行,比先shuffle再sort来说,性能可能是要高的。

#### **原则七:广播大变量**

​有时在开发过程中,会遇到需要在算子函数中使用外部变量的场景(尤其是大变量,比如100M以上的大集合),那么此时就应该使用Spark的广播(Broadcast)功能来提升性能。

​ 在算子函数中使用到外部变量时,默认情况下,Spark会将该变量复制多个副本,通过网络传输到task中,此时每个task都有一个变量副本。如果变量本身比较大的话(比如100M,甚至1G),那么大量的变量副本在网络中传输的性能开销,以及在各个节点的Executor中占用过多内存导致的频繁GC,都会极大地影响性能。

​ 因此对于上述情况,如果使用的外部变量比较大,建议使用Spark的广播功能,对该变量进行广播。广播后的变量,会保证每个Executor的内存中,只驻留一份变量副本,而Executor中的task执行时共享该Executor中的那份变量副本。这样的话,可以大大减少变量副本的数量,从而减少网络传输的性能开销,并减少对Executor内存的占用开销,降低GC的频率。

#### **原则八:使用Kryo优化序列化性能**

​在Spark中,主要有三个地方涉及到了序列化: * 在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输(见“原则七:广播大变量”中的讲解)。 * 将自定义的类型作为RDD的泛型类型时(比如JavaRDD,Student是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现Serializable接口。 * 使用可序列化的持久化策略时(比如MEMORY_ONLY_SER),Spark会将RDD中的每个partition都序列化成一个大的字节数组。

对于这三种出现序列化的地方,我们都可以通过使用Kryo序列化类库,来优化序列化和反序列化的性能。Spark默认使用的是Java的序列化机制,也就是ObjectOutputStream/ObjectInputStream API来进行序列化和反序列化。但是Spark同时支持使用Kryo序列化库,Kryo序列化类库的性能比Java序列化类库的性能要高很多。官方介绍,Kryo序列化机制比Java序列化机制,性能高10倍左右。Spark之所以默认没有使用Kryo作为序列化类库,是因为Kryo要求最好要注册所有需要进行序列化的自定义类型,因此对于开发者来说,这种方式比较麻烦。

#### **原则九:优化数据结构**

### 27、**Spark资源参数调优**

​ 所谓的Spark资源参数调优,其实主要就是对Spark运行过程中各个使用资源的地方,通过调节各种参数,来优化资源使用的效率,从而提升Spark作业的执行性能。以下参数就是Spark中主要的资源参数,每个参数都对应着作业运行原理中的某个部分,我们同时也给出了一个调优的参考值。

#### num-executors

- 参数说明:该参数用于设置Spark作业总共要用多少个Executor进程来执行。Driver在向YARN集群管理器申请资源时,YARN集群管理器会尽可能按照你的设置来在集群的各个工作节点上,启动相应数量的Executor进程。这个参数非常之重要,如果不设置的话,默认只会给你启动少量的Executor进程,此时你的Spark作业的运行速度是非常慢的。

- 参数调优建议:每个Spark作业的运行一般设置50~100个左右的Executor进程比较合适,设置太少或太多的Executor进程都不好。设置的太少,无法充分利用集群资源;设置的太多的话,大部分队列可能无法给予充分的资源。

#### executor-memory

- 参数说明:该参数用于设置每个Executor进程的内存。Executor内存的大小,很多时候直接决定了Spark作业的性能,而且跟常见的JVM OOM异常,也有直接的关联。

- 参数调优建议:每个Executor进程的内存设置4G~8G较为合适。但是这只是一个参考值,具体的设置还是得根据不同部门的资源队列来定。可以看看自己团队的资源队列的最大内存限制是多少,num-executors乘以executor-memory,是不能超过队列的最大内存量的。此外,如果你是跟团队里其他人共享这个资源队列,那么申请的内存量最好不要超过资源队列最大总内存的1/3~1/2,避免你自己的Spark作业占用了队列所有的资源,导致别的同学的作业无法运行。

#### executor-cores

- 参数说明:该参数用于设置每个Executor进程的CPU core数量。这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程,因此每个Executor进程的CPU core数量越多,越能够快速地执行完分配给自己的所有task线程。

- 参数调优建议:Executor的CPU core数量设置为2~4个较为合适。同样得根据不同部门的资源队列来定,可以看看自己的资源队列的最大CPU core限制是多少,再依据设置的Executor数量,来决定每个Executor进程可以分配到几个CPU core。同样建议,如果是跟他人共享这个队列,那么num-executors * executor-cores不要超过队列总CPU core的1/3~1/2左右比较合适,也是避免影响其他同学的作业运行。

#### driver-memory

- 参数说明:该参数用于设置Driver进程的内存。

- 参数调优建议:Driver的内存通常来说不设置,或者设置1G左右应该就够了。唯一需要注意的一点是,如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。

#### spark.default.parallelism

- 参数说明:该参数用于设置每个stage的默认task数量。这个参数极为重要,如果不设置可能会直接影响你的Spark作业性能。

- 参数调优建议:Spark作业的默认task数量为500~1000个较为合适。很多同学常犯的一个错误就是不去设置这个参数,那么此时就会导致Spark自己根据底层HDFS的block数量来设置task的数量,默认是一个HDFS block对应一个task。通常来说,Spark默认设置的数量是偏少的(比如就几十个task),如果task数量偏少的话,就会导致你前面设置好的Executor的参数都前功尽弃。试想一下,无论你的Executor进程有多少个,内存和CPU有多大,但是task只有1个或者10个,那么90%的Executor进程可能根本就没有task执行,也就是白白浪费了资源!因此Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适,比如Executor的总CPU core数量为300个,那么设置1000个task是可以的,此时可以充分地利用Spark集群的资源。

#### spark.storage.memoryFraction

- 参数说明:该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor 60%的内存,可以用来保存持久化的RDD数据。根据你选择的不同的持久化策略,如果内存不够时,可能数据就不会持久化,或者数据会写入磁盘。

- 参数调优建议:如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中,降低了性能。但是如果Spark作业中的shuffle类操作比较多,而持久化操作比较少,那么这个参数的值适当降低一些比较合适。此外,如果发现作业由于频繁的gc导致运行缓慢(通过spark web ui可以观察到作业的gc耗时),意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

#### spark.shuffle.memoryFraction

- 参数说明:该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。也就是说,Executor默认只有20%的内存用来进行该操作。shuffle操作在进行聚合时,如果发现使用的内存超出了这个20%的限制,那么多余的数据就会溢写到磁盘文件中去,此时就会极大地降低性能。

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

资源参数的调优,没有一个固定的值,需要根据自己的实际情况(包括Spark作业中的shuffle操作数量、RDD持久化操作数量以及spark web ui中显示的作业gc情况),同时参考本篇文章中给出的原理以及调优建议,合理地设置上述参数。

### **28、Shuffle相关参数调优**

#### spark.shuffle.file.buffer

- 默认值:32k

- 参数说明:该参数用于设置shuffle write task的BufferedOutputStream的buffer缓冲大小。将数据写到磁盘文件之前,会先写入buffer缓冲中,待缓冲写满之后,才会溢写到磁盘。

- 调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如64k),从而减少shuffle write过程中溢写磁盘文件的次数,也就可以减少磁盘IO次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。

#### spark.reducer.maxSizeInFlight

- 默认值:48m

- 参数说明:该参数用于设置shuffle read task的buffer缓冲大小,而这个buffer缓冲决定了每次能够拉取多少数据。

- 调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如96m),从而减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。

#### spark.shuffle.io.maxRetries

- 默认值:3

- 参数说明:shuffle read task从shuffle write task所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的。该参数就代表了可以重试的最大次数。如果在指定次数之内拉取还是没有成功,就可能会导致作业执行失败。

- 调优建议:对于那些包含了特别耗时的shuffle操作的作业,建议增加重试最大次数(比如60次),以避免由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败。在实践中发现,对于针对超大数据量(数十亿~上百亿)的shuffle过程,调节该参数可以大幅度提升稳定性。

#### spark.shuffle.io.retryWait

- 默认值:5s

- 参数说明:具体解释同上,该参数代表了每次重试拉取数据的等待间隔,默认是5s。

- 调优建议:建议加大间隔时长(比如60s),以增加shuffle操作的稳定性。

#### spark.shuffle.memoryFraction

- 默认值:0.2

- 参数说明:该参数代表了Executor内存中,分配给shuffle read task进行聚合操作的内存比例,默认是20%。

- 调优建议:在资源参数调优中讲解过这个参数。如果内存充足,而且很少使用持久化操作,建议调高这个比例,给shuffle read的聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘。在实践中发现,合理调节该参数可以将性能提升10%左右。

#### spark.shuffle.manager

- 默认值:sort

- 参数说明:该参数用于设置ShuffleManager的类型。Spark 1.5以后,有三个可选项:hash、sort和tungsten-sort。HashShuffleManager是Spark 1.2以前的默认选项,但是Spark 1.2以及之后的版本默认都是SortShuffleManager了。tungsten-sort与sort类似,但是使用了tungsten计划中的堆外内存管理机制,内存使用效率更高。

- 调优建议:由于SortShuffleManager默认会对数据进行排序,因此如果你的业务逻辑中需要该排序机制的话,则使用默认的SortShuffleManager就可以;而如果你的业务逻辑不需要对数据进行排序,那么建议参考后面的几个参数调优,通过bypass机制或优化的HashShuffleManager来避免排序操作,同时提供较好的磁盘读写性能。这里要注意的是,tungsten-sort要慎用,因为之前发现了一些相应的bug。

#### spark.shuffle.sort.bypassMergeThreshold

- 默认值:200

- 参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。

- 调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量。那么此时就会自动启用bypass机制,map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。

#### spark.shuffle.consolidateFiles

- 默认值:false

- 参数说明:如果使用HashShuffleManager,该参数有效。如果设置为true,那么就会开启consolidate机制,会大幅度合并shuffle write的输出文件,对于shuffle read task数量特别多的情况下,这种方法可以极大地减少磁盘IO开销,提升性能。

- 调优建议:如果的确不需要SortShuffleManager的排序机制,那么除了使用bypass机制,还可以尝试将spark.shffle.manager参数手动指定为hash,使用HashShuffleManager,同时开启consolidate机制。在实践中尝试过,发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。

### **29、Spark读取数据生成RDD分区默认是多少**

​ textFile算子的参数minPartitions的默认值为defaultMinPartitions,该方法的实现代码为math.min(defaultParallelism, 2),其中defaultParallelism与CPU的核数有关系,也就是说默认分区数量是取CPU的核数和2的最小值。

### **30、100个分区,想要聚合成两个分片,用哪个算子**

​ coalesce算子,主要就是用于在filter操作之后,针对每个partition的数据量各不相同的情况,来压缩partition的数量。减少partition的数量,而且让每个partition的数据量都尽量均匀紧凑,从而方便后面的task进行计算操作,在某种程度上,能够一定程度的提升性能。

### **31、Spark的通信机制**

Spark的消息通信主要分成三个部分,整体框架;启动消息通信;运行时消息通信;

**1、概述**

​ Spark(旧版本)的远程进程通信(RPC)是通过Akka类库来实现的,Akka使用Scala语言开发,基于Actor并发模型实现,Akka具有高可靠、高性能、可扩展等特点。

**2、具体通信流程**

​ 1)首先启动Master进程,然后启动所有的Worker进程。

​ 2)Worker启动后,在preStart方法中与Master建立连接,向Master发送注册信息,将Worker的信息通过case class封装起来发送给Master。

​ 3)Master接收到Worker的注册消息后将其通过集合保存起来,然后向Worker反馈注册成功的消息。

​ 4)Worker会定期向Master发送心跳包,领受新的计算任务。

​ 5)Master会定期清理超时的Worker。

**3、通信框架**

​ Spark2.2 使用 Netty作为master与worker的通信框架,Spark2.0 之前使用的akka框架。

​ 1)Spark启动消息通信

​ worker向master发送注册消息,master处理完毕后返回注册成功或者是失败的消息,如果成功,worker向master定时发送心跳。

​ 2)Spark运行时消息通信

​ 应用程序SparkContext向master发送注册信息,并由master为该应用分配Executor,executor启动之后会向SparkContext发送注册成功消息,然后SparkContext的rdd 触发Action之后会形成一个DAG,通过DAGScheduler进行划分Stage并将其转化成TaskSet,然后TaskScheduler向Executor发送执行消息,Executor接收到信息之后启动并且运行,最后是由Driver处理结果并回收资源。

### **32、如何使 用 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 的排序算子进行排序

## 二、Spark SQL

### **1、请写出创建Dataset的几种方式**

1)由DataFrame转化成为Dataset。

2)通过 SparkSession.createDataset() 直接创建。

3)通过toDS 方法隐式转换。

### **2、Dataframe相对rdd有哪些不同**

​ **RDD特点:**

​ 1)RDD是一个懒执行的不可变的可以支持Lambda表达式的并行数据集合。

​ 2)RDD的最大好处就是简单,API的人性化程度很高。

​ 3)RDD的劣势是性能限制,它是一个JVM驻内存对象,这也就决定了存在GC的限制和数据增加时java序列化成本的升高。

​ **DataFrame特点:**

​ 与RDD类似,DataFrame也是一个分布式数据容器。然而DataFrame更像传统数据库的二维表格,除了数据以外,还记录数据的结构信息,即schema。同时与Hive类似,DataFrame也支持嵌套数据类型(struct、array、map)。从API易用性的角度上看,DataFrame API提供的是一套高层的关系操作,比函数式的RDD API要更加友好,门槛更低。Spark DataFrame 很好的继承了传统单机数据分析的开发体验。

### **3、SparkSql如何处理结构化数据和非结构化数据**

**结构化数据**: Json转化为 DataFrame、通过注册表操作 sql 的方式。

**非结构化数据**: 通过反射推断方式、构建一个Schema。

### **4、rdd、dataframe和dataset区别**

​ 其实dataset就是dataframe的升级版,相当于dataframe是dataset的子集,主要区别在于,在Spark2.0 以后的dataset添加的编码器,在 dataframe中他不是面向对象的编程思想,而在dataset中编程面向对象编程,同时dataset相当于dataframe和rdd的整合版,操作更加灵活。

**1)RDD**

​ **优点:**

​ 1.编译时类型安全。

​ 2.编译时就能检查出类型错误。

​ 3.面向对象的编程风格。

​ 4.直接通过类名点的方式类操作数据。

​ **缺点:**

​ 1.序列化和反序列化的性能开销大。

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

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

​ **2)DataFrame**

​ DataFrame引入了schema和off-heap。

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

​ **3)DataSet**

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

​ 当序列化数据时,Encoder产生字节码与off-heap进行交互,能够达到按需访问数据的效果,而不用反序列化整个对象。

### **5、Spark SQL 的原理**

#### 1、Spark SQL运行流程

​ **1)对读入的sql语句进行解析**

​ 分辨出sql语句中哪些是关键词(如:select,from,where...)、哪些是表达式、哪些是Projection、哪些是Data Source等。

​ 判断sql语句是否规范。

​ **2)将sql语句和数据库的数据字典进行绑定**

​ 数据字典:列、表、视图等等。

​ 若相关的Projection、DataSource等都是存在的话,就表示该sql语句时可以执行的。

​ **3)数据库选择最优的执行计划**

​ 数据库会提供几个执行计划,这些计划都会运行统计数据。

​ 数据库会从上述各种执行计划中选择一个最优计划。

​ **4)执行计划**

​ 按照Operation(操作) > DataSource(数据源) > Result的次序来执行。

​ 在执行过程中有时候甚至不需要读取物理表就可以返回结果,比如重新运行刚运行的sql语句,可直接从数据库的缓冲池中返回结果。

#### 2、Spark SQL 原理

​ Catalyst 是 Spark SQL 执行优化器的代号,所有 Spark SQL 语句最终都能通过它来解析、优化,最终生成可以执行的java字节码。

​ Catalyst 最主要的数据结构是树,所有sql语句都会用树结构来存储,树中的每个节点有一个类(class),以及 0 或多个子节点。Scala中定义的新的节点类型都是 TreeNode 这个类的子类。

​ Catalyst 另外一个重要的概念是规则。基本上,所有优化都是基于规则的。可以用规则对树进行操作,树中的节点是只读的,所以树也是只读的。规则中定义的函数可能实现从一棵树转换成一颗新树。

​ **整个 Catalyst 的执行过程可以分为以下4个阶段:**

​ 1)分析阶段,分析逻辑树,解决引用。

​ 2)逻辑优化阶段。

​ 3)物理计划阶段,Catalyst会生成多个计划,并基于成本进行对比。

​ 4)代码生成阶段。

### **6、Spark SQL 中缓存方式几种**

Spark SQL 中的缓存方式:

​ 方式一:可以通过SQLConext实例.cacheTable("表明")缓存一张临时表。

​ 方式二:可以通过DataFrame实例.cache()缓存一张虚拟表

registerTempTable不是action类型算子,不发生缓存。

### **7、SparkSQL中join操作与left join操作的区别**

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

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

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

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

## **三、SparkStreaming**

### **1、SparkStreaming有哪几种方式消费Kafka中的数据,他们之间的区别是什么**

#### **1.基于Receiver的方式**

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

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

#### **2.基于Direct的方式**

​ 这种不基于Receiver的直接方式,是在Spark1.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的副本进行恢复。

​ 一次且仅一次的事务机制。

#### **3.区别**

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

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

​ 在实际生产环境中大多用Direct方式。

### **2、简述SparkStreaming窗口函数的原理**

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

### **3、简单描述SparkStreaming的容错原理**

​ SparkStreaming的一个特点就是高容错。

​ 首先Spark RDD 就有容错机制,每一个RDD都是不可变的分布式可重算的数据集,其记录这确定性的操作血统,所以只要输入数据时可容错的,那么任意一个RDD的分区出错或不可用,都是可以利用原始输入数据通过转换操作而重新计算出来的。

​ 预写日志通常被用于数据库和文件系统中,保证数据操作的持久性。预写日志通常是先将操作写入到一个持久可靠的日志文件中,然后才对数据施加该操作,当加入施加操作中出现了异常,可以通过读取日志文件并重新施加该操作。

​ 另外接收数据的正确性只在数据被预写到日志以后接收器才会确认,已经缓存但还没保存的数据可以在Driver重新启动之后由数据源再发送一次,这两个机制确保了零数据丢失,所有数据或者从日志中恢复,或者由数据源重发。

# 13__算法:线性回归

**线性回归是什么:**线性回归主要用来解决回归问题,也就是预测连续值的问题,而能满足这样要求的数学模型被称为“回归模型”。最简单的线性回归模型就是我们都知道的函数 y=ax+b,这种线性函数就是描述了两个变量之间的关系,函数图像是一条连续的直线。这里的a指的就是线性回归模型的权值参数,b指的就是线性回归模型的“偏差值”,而解决线性回归问题的关键就在于求出权值参数和偏差值。。 当然还有另外一种回归模型,也就是非线性模型,它值因变量与自变量之间的关系不能表示为线性对应关系也就是说的不是一条直线,比如我们知道的对数函数、指数函数、二次函数等。

**这边举个简单的例子,比如说通过线性回归实现房价预测的流程**

**第一步:数据采集。**

​ 任何模型的训练都离不开数据,因此收集数据构建数据集时必不可少的环节。比如现在要预测一套房子的售价,那么我们必须先要收集周围房屋的售价,这样才能确保你预测的价格不会过高或者过低。

比如说模拟有这样一个数据表:

样本序号 房屋面积 房间数量 距离市中心距离 是否学区房 房屋售价

​ 1 108 3 25 是 48万

当然刚说的样本数量远远是不止这些的,如果想要更加准确的预测就要手机更多的数据,至少保证上百条样本把,表格中最后一栏字段是 “房屋售价”,这是“有监督学习”的典型特点,被称为“标签”也就是我们所说的“参考答案”。表格中的房屋面积,房间数量,距离市中心距离,以及是否为学区房,这些都是影响最终预测结果的相关因素,我们称这些为“特征”,也叫“属性”。

当然我们可能会想到影响房屋售价的肯定是不止这些因素的,这边只是简单举个例子,因为采集数据是一个很繁琐的过程,因此一般情况下,我们选择与预测结果密切相关的重要“特征”。

**第二步:构建线性回归模型**

​ 有了数据以后,下一步要做的就是构建线性回归模型,这也是最重要的一步,这个过程会涉及到一些数学知识,具体怎么构建的这个我还在研究中。

**第三部:构建完模型后我们就需要对其进行训练**

​ 训练的过程就是将表格中的数据以矩阵的形式输入到模型中通常会抽取百分之80左右的数据用来做训练数据集,模型则通过数学统计方法计算房屋价格与各个特征之间的关联关系,也就是“权值参数”。训练完成之后,就用剩下来的百分之20左右的数据作为测试数据集来验证我们的算法模型。最后就可以对房屋价格进行预测了。首先将数据按照“特征值”依次填好,并输入到模型中,最后模型会输出一个合理的预测结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值