大数据面试总结-ysjt

1、前言

工作了太安逸,没有准备好。还是挺喜欢这家公司。业务上的问题我就不放出来了。技术的问题我就凭自己的记忆写一写,以此来帮助大家和自己更好的面试。面试过程中问了很多Kafka的问题。
自己没有回答出来的,我将会百度贴出来。也欢迎大家指点错误。祝大家和自己找到满意的工作!

2、问题

2.1 hadoop的MR的过程

分为六个阶段。
阶段1逻辑切片:inputSplit进行标准分割,默认片的大小和块的大小一样的。Split size=Block size。每一个切片由一个MapTask处理。
阶段2、对切片的数据按一定规则解析成<Key,value>对。默认是把每一行解析成键值对。
阶段3调用map方法。每解析出一个<k,v>,调用一次map方法。每次map会输出0或者多个键值对。
阶段4、按照一定规则对阶段三输出的键值对进行分区。默认只有一个分区,分区的数据就是reducer任务运行的数量
阶段5分区中键值对进行排序。按照键进行排序,对于键相同的值进行排序。
阶段6、对数据进行局部聚合处理,combinner处理。默认是没有的。

2.2 hadoop的MR的分区 – Partitioner

其实面试官想问的估计是Mapreduce的分区
1、Mapreduce中将map输出的kv对,按照相同key分组,然后分发给不同的reducetask。
2、默认规则:根据key的hashcode%reducetask数来分发
所以设置分区可以设置reducetask的个数。

2.3 kafka的消息不丢失

主要分为三个部分的消息不丢失。
1、生产者消息不丢失。默认是异步发送消息,我们可以通过重试retries的次数以及重试时间间隔,默认三次。
2、broker的消息不丢失。副本设置为broker的一半+1,写入follower超过一半才算成功,关闭落后的leader清理数据,以及ISR的个数
3、消费者的消息不丢失,主要是关闭自动提交offset
参数的设置:

消费者:	enable.auto.commit=false
生产者:	acks = -1
生产者:	retries = 3 (根据情况设置)
broker:	replication.factor >=3 (副本多少个)
broker:	min.insync.replicas > 1 (写入副本数量才算成功)
broker:	replication.factor > min.insync.replicas
broker:	unclean.leader.election.enable = false

2.3 KafkaConsumer如何保证一次消费

1、数据有状态:可以根据数据信息进行确认数据是否重复消费,这时候可以使用手动提交的最少一次消费语义实现,即使消费的数据有重复,可以通过状态进行数据去重,以达到幂等的效果
2、存储数据容器具备幂等性:在数据存入的容器具备天然的幂等(比如ElasticSearch的put操作具备幂等性,相同的数据多次执行Put操作和一次执行Put操作的结果是一致的),这样的场景也可以使用手动提交的最少一次消费语义实现,由存储数据端来进行数据去重
3、数据无状态,并且存储容器不具备幂等这里简单说明一下实现思路

1)利用consumer api的seek方法可以指定offset进行消费,在启动消费者时查询数据库中记录的offset信息,如果是第一次启动,那么数据库中将没有offset信息,需要进行消费的元数据插入,然后从offset=0开始消费。
2)关系型数据库具备事务的特性,当数据入库时,同时也将offset信息更新,借用关系型数据库事务的特性保证数据入库和修改offset记录这两个操作是在同一个事务中进行。
3)使用ConsumerRebalanceListener来完成在分配分区时和Relalance时作出相应的处理逻辑。

记录kafka信息表设计:

create table kafka_info(
	topic_group_partition varchar(32) primary key, //主题+组名+分区号 这里冗余设计方便通过这个主键进行更新提升效率 
	topic_group varchar(30), //主题和组名
	partition_num tinyint,//分区号
	offsets bigint default 0 //offset信息
);

代码实现:

package com.huawei.kafka.consumer;
import com.alibaba.fastjson.JSON;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.TopicPartition;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Duration;
import java.util.*;

/**
 * @description:精确一次消费实现
 */
public class AccurateConsumer {

    private static final Properties props = new Properties();

    private static final String GROUP_ID = "Test";

    static {
        props.put("bootstrap.servers", "192.168.142.139:9092");
        props.put("group.id", GROUP_ID);
        props.put("enable.auto.commit", false);//注意这里设置为手动提交方式
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
    }

    final KafkaConsumer<String, String> consumer;

    //用于记录每次消费时每个partition的最新offset
    private Map<TopicPartition, Long> partitionOffsetMap;

    //用于缓存接受消息,然后进行批量入库
    private List<Message> list;

    private volatile boolean isRunning = true;

    private final String topicName;

    private final String topicNameAndGroupId;

    public AccurateConsumer(String topicName) {
        this.topicName = topicName;
        topicNameAndGroupId = topicName + "_" + GROUP_ID;
        consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList(topicName), new HandleRebalance());
        list = new ArrayList<>(100);
        partitionOffsetMap = new HashMap<>();
    }

    //这里使用异步提交和同步提交的组合方式
    public void receiveMsg() {
        try {
            while (isRunning) {
                ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
                if (!consumerRecords.isEmpty()) {
                    for (TopicPartition topicPartition : consumerRecords.partitions()) {
                        List<ConsumerRecord<String, String>> records = consumerRecords.records(topicPartition);
                        for (ConsumerRecord<String, String> record : records) {
                            //使用fastjson将记录中的值转换为Message对象,并添加到list中
                            list.addAll(JSON.parseArray(record.value(), Message.class));
                        }
                        //将partition对应的offset信息添加到map中,入库时将offset-partition信息一起进行入库
                        partitionOffsetMap.put(topicPartition, records.get(records.size() - 1)
                                .offset() + 1);//记住这里一定要加1,因为下次消费的位置就是从+1的位置开始
                    }
                }
                //如果list中存在有数据,则进行入库操作
                if (list.size() > 0) {
                    boolean isSuccess = insertIntoDB(list, partitionOffsetMap);
                    if (isSuccess) {
                        //将缓存数据清空,并将offset信息清空
                        list.clear();
                        partitionOffsetMap.clear();
                    }
                }
            }
        } catch (Exception e) {
            //处理异常
        } finally {
            //offset信息由我们自己保存,提交offset其实没有什么必要
            //consumer.commitSync();
            close();
        }

    }

    private boolean insertIntoDB(List<Message> list, Map<TopicPartition, Long> partitionOffsetMap) {
        Connection connection = getConnection();//获取数据库连接 自行实现
        boolean flag = false;
        try {
            //设置手动提交,让插入数据和更新offset信息在一个事务中完成
            connection.setAutoCommit(false);
            insertMessage(list);//将数据进行入库 自行实现
            updateOffset(partitionOffsetMap);//更新offset信息 自行实现
            connection.commit();
            flag = true;
        } catch (SQLException e) {
            try {
                //出现异常则回滚事务
                connection.rollback();
            } catch (SQLException e1) {
                //处理异常
            }
        }
        return flag;
    }

    //获取数据库连接 自行实现
    private Connection getConnection() {
        return null;
    }

    public void close() {
        isRunning = false;
        if (consumer != null) {
            consumer.close();
        }
    }

    private class HandleRebalance implements ConsumerRebalanceListener {

        @Override
        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
            //发生Rebalance时,只需要将list中数据和记录offset信息清空即可
            //这里为什么要清除数据,应为在Rebalance的时候有可能还有一批缓存数据在内存中没有进行入库,
            //并且offset信息也没有更新,如果不清除,那么下一次还会重新poll一次这些数据,将会导致数据重复
            list.clear();
            partitionOffsetMap.clear();
        }

        @Override
        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
            //获取对应Topic的分区数
            List<PartitionInfo> partitionInfos = consumer.partitionsFor(topicName);
            Map<TopicPartition, Long> partitionOffsetMapFromDB = getPartitionOffsetMapFromDB
                    (partitionInfos.size());

            //在分配分区时指定消费位置
            for (TopicPartition partition : partitions) {
                //如果在数据库中有对应partition的信息则使用,否则将默认从offset=0开始消费
                if (partitionOffsetMapFromDB.get(partition) != null) {
                    consumer.seek(partition, partitionOffsetMapFromDB.get(partition));
                } else {
                    consumer.seek(partition, 0L);
                }
            }
        }
    }
    /**
     * 从数据库中查询分区和offset信息
     * @param size 分区数量
     * @return 分区号和offset信息
     */
    private Map<TopicPartition, Long> getPartitionOffsetMapFromDB(int size) {
        Map<TopicPartition, Long> partitionOffsetMapFromDB = new HashMap<>();
        //从数据库中查询出对应信息
        Connection connection = getConnection();//获取数据库连接 自行实现
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        String querySql = "SELECT partition_num,offsets from kafka_info WHERE topic_group = ?";
        try {
            preparedStatement = connection.prepareStatement(querySql);
            preparedStatement.setString(1, topicNameAndGroupId);
            resultSet = preparedStatement.executeQuery();
            while (resultSet.next()) {
                partitionOffsetMapFromDB.put(new TopicPartition(topicName, resultSet.getInt(1)),
                        resultSet.getLong(2));
            }
            //判断数据库是否存在所有的分区的信息,如果没有,则需要进行初始化
            if (partitionOffsetMapFromDB.size() < size) {
                connection.setAutoCommit(false);
                StringBuilder sqlBuilder = new StringBuilder();
                //partition分区号是从0开始,如果有10个分区,那么分区号就是0-9
                /*这里拼接插入数据 格式 INSERT INTO kafka_info(topic_group_partition,topic_group,partition_num) VALUES
                (topicNameAndGroupId_0,topicNameAndGroupId,0),(topicNameAndGroupId_1, topicNameAndGroupId,1)....*/
                for (int i = 0; i < size; i++) {
                    sqlBuilder.append("(").append
                            (topicNameAndGroupId).append("_").append(i).append(",").append
                            (topicNameAndGroupId).append(",").append(i).append("),");
                }
                //将最后一个逗号去掉加上分号结束
                sqlBuilder.deleteCharAt(sqlBuilder.length() - 1).append(";");
                preparedStatement = connection.prepareStatement("INSERT INTO kafa_info" +
                        "(topic_group_partition,topic_group,partition_num) VALUES " + sqlBuilder.toString());
                preparedStatement.execute();
                connection.commit();
            }
        } catch (SQLException e) {
            //处理异常 回滚事务 这里应该结束程序 排查错误
            try {
                connection.rollback();
            } catch (SQLException e1) {
                //打印日志 排查错误信息
            }

        } finally {
            try {
                if (resultSet != null) {
                    resultSet.close();
                }
                if (preparedStatement != null) {
                    preparedStatement.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (SQLException e) {
                //处理异常 打印日志即可 关闭资源失败
            }
        }
        return partitionOffsetMapFromDB;
    }
}

数据对象

package com.huawei.kafka.consumer;

/**
 * @description: Message对象 这里模拟数据
 */
public class Message {

    private String id;
    private String name;
    private Syring desc;
    private Date time;
     
    //get set toString 方法省略
}

这种实现方式对于以下故障场景测试通过,虽然不能说所有故障场景均可以保证精确一次消费,但目前基本覆盖大部分故障场景

  • 一个消费者组中的某个消费者频繁加入组或离开组

  • 直接kill消费应用程序

  • 故障kafka集群中某个节点

  • 故障客户端网络,使其不能连接kafka server端

  • 直接重启应用程序所在虚拟机
    这里主要使用自己管理offset的方式来确保数据和offset信息是同时变化的,通过数据库事务的特性来保证一致性和原子性。
    该答案转载于:https://blog.csdn.net/qq_18581221/article/details/89766073

2.4 介绍一下Java GC机制

2.4.1、理解GC机制

就从:“GC的区域在哪里”,“GC的对象是什么”,“GC的时机是什么”,“GC做了哪些事”几方面来分析。
1、需要GC的内存区域:堆,方法区。
2、什么时候触发GC
程序调用System.gc时可以触发;
系统自身来决定GC触发的时机

GC又分为 minor GC 和 Full GC (也称为 Major GC )
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
  a.调用System.gc时,系统建议执行Full GC,但是不必然执行
  b.老年代空间不足
  c.方法去空间不足
  d.通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  e.由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

3、GC做了什么事:主要做了清理对象,整理内存的工作。Java堆分为新生代和老年代,采用了不同的回收方式。(回收方式即回收算法)

2.4.2、GC常用算法

常用算法有:标记-清除算法,标记-压缩算法,复制算法,分代收集算法。
1、标记-清除算法:标记活着或是死亡。(步骤:标记-清除)优点:不需要移动对象的位置,只需要标记。缺点:效率低,递归与全栈对象遍历。
2、标记-压缩算法:标记清除发的改进版。(步骤:标记-整理)优点:不会产生大量的碎片空间。缺点:如果存活的对象过多,整理阶段较多复制操作。
3、复制算法:将内存分成两部分,然后每次只使用一部分,满了复制到剩下的,然后清空,循环。优点:简单,不产生内存碎片。缺点:每次运行,内存只能使用一半。
4、分带收集算法:大多采用这种,将堆分为新生代和老年代。在新生代中,对象生存较短,每次回收都会有大量对象死去,name这时就采用复制算法。老年代李的对象存活率较高,没有额外空间进行分配担保,所以可以使用标记-整理或者标记-清除。

2.4.3、垃圾收集器

1、Serial收集器:只使用一个线程去回收,串行收集器。(-XX:+UseSerialGC)
2、并行收集器
(1)parNew:new代表新生代,所以适用于新生代。(+UseParNewGC)
(2)Parallel:类似ParNew,新生代复制算法,老年代标记-压缩,更加关注吞吐量 。(-XX:+UseParallelGC)
3、CMS收集器
4、G1收集器

2.5 谈一谈HashMap

包含了几种不同的Map:HashMap, LinkedHashMap, WeakHashMap, IdentityHashMap。它们都有同样的基本接口Map,实现了Serializable等接口,他是一个Entry数组,Entry是HashMap的一个内部类。
初始化容量默认为16,负载因子默认为0.75,超过16*0.75=12时会扩容为当前容量的两倍。
但是行为、效率、排序策略、保存对象的生命周期和判定“键”等价的策略等各不相同。
HashMap使用了特殊的值,称为“散列码”(hash code),来取代对键的缓慢搜索。“散列码”是“相对唯一”用以代表对象的int值,它是通过将该对象的某些信息进行转换而生成的。所有Java对象都能产生散列码,因为hashCode()是定义在基类Object中的方法。
  HashMap就是使用对象的hashCode()进行快速查询的。此方法能够显著提高性能。
  
HashMap : Map基于散列表的实现。插入和查询“键值对”的开销是固定的。可以通过构造器设置容量capacity和负载因子load factor,以调整容器的性能。
LinkedHashMap : 类似于HashMap,但是迭代遍历它时,取得“键值对”的顺序是其插入次序,或者是最近最少使用(LRU)的次序。只比HashMap慢一点。而在迭代访问时发而更快,因为它使用链表维护内部次序。
WeakHashMap : 弱键(weak key)Map,Map中使用的对象也被允许释放: 这是为解决特殊问题设计的。如果没有map之外的引用指向某个“键”,则此“键”可以被垃圾收集器回收。
IdentifyHashMap : 使用==代替equals()对“键”作比较的hash map。专为解决特殊问题而设计。
该答案转载于:https://www.cnblogs.com/misterzxy/p/3436870.html

2.6 kylin建立cube的过程分析每个步骤的作用

  1. Cube信息:填写cube基本信息。点击Next进入下一步。你可以使用字母、数字和“_”来为你的cube命名
  2. 维度Dimension:(1)建立事实表(2)可以join关联表,Hierarchy从有分级结构的查找表获取维度。,Derived衍生维度查找表获取维度。
  3. 度量Measure:可以新增度量包含五种不同类型的度量(SUM、MAX、MIN、COUNT、COUNT_DISTINCT)
  4. 过滤器Filter:可选步骤,可以使用sql格式添加过滤条件。
  5. 更新设置RefreshSetting:增量构建cube而设计的,选择分区类型、分区列和开始日期。
  6. 高级设置Advanced Setting:会展示你之前设置,聚合组包括的维度列(includes);强制维度(Mandatory Dimensions);层级维度(Hierarchy Dimensions);关联维度(joint Dimensions)。也还有聚合组(Aggregation group)。Rowkeys
  7. 概览 & 保存:你可以概览你的cube并返回之前的步骤进行修改。点击Save按钮完成cube创建

2.7 零拷贝如何提升kafka性能

Kafka使用零拷贝(Zero-Copy)技术来提供它的性能,所谓的零拷贝是指将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序之手,减少了内核和用户模式之间的上下文切换,零拷贝技术通过DMA技术实现。

直接存储器存取方式(Direct Memory Access, DMA)
DMA控制方式是以存储器为中心,在主存和I/O设备之间建立一条直接通路,在DMA控制器的控制下进行设备和主存之间的数据交换。
这种方式只在传输开始和传输结束时才需要CPU的干预。它非常适用于高速设备与主存之间的成批数据传输。

零拷贝技术通过DMA技术将文件内容复制到内核模式下的Read Buffer中。不过没有数据被复制到Socket Buffer,只有包含数据的位置和长度的信息的文件描述符被加到Socket Buffer中。DMA引擎直接将数据从内核模式中传递到网卡设备。这里上下文切换变成了2次,也只经历了2次复制过程就从磁盘中传送出去了。

答案转载于:https://www.jianshu.com/p/254cdf6733f5

2.8 Kafka重平衡

重平衡:当集群中有新成员加入,或者某些主题增加了分区之后。消费者是怎么进行重新分配分区再进行消费的?这里就涉及到重平衡(Rebalance)技术。就是保证分区(RocketMQ 是队列)公平分配且只能被一个消费者订阅(同一个消费组)。

基本流程:就是 Coordinator 感知到 消费者组的变化,
然后在心跳的过程中发送重平衡信号通知各个消费者离组,
然后消费者重新以 JoinGroup 方式加入 Coordinator,并选出Consumer Leader。
当所有消费者加入 Coordinator,
Consumer Leader会根据 Coordinator给予的分区信息给出分区方案。
Coordinator 将该方案以 SyncGroup 的方式将该方案执行下去,通知各个消费者,
这样就完成了一轮 重平衡了。

参数影响重分区:
session.timeout.ms : Coordinator心跳监听,能更早发现消费者失败时间。合理设置,有时会导致频繁重分区。
max.poll.interval.ms: 消费者处理消息的最大逻辑时间。
heartbeat.interval.ms :该参数值必须小于 session.timeout.ms。
流程:
消费组的协调管理已经依赖于 Broker 端某个节点,该节点即是该消费组的 Coordinator, 并且每个消费组有且只有一个 Coordinator,它负责消费组内所有的事务协调,其中包括分区分配,重平衡触发,消费者离开与剔除等等,整个消费组都会被 Coordinator 管控着,在每个过程中,消费组都有一个状态,Kafka 为消费组定义了 5 个状态。
如下:
1.Empty:消费组没有一个活跃的消费者;2.PreparingRebalance:消费组准备进行重平衡,此时的消费组可能已经接受了部分消费者加入组请求;3.AwaitingSync:全部消费者都已经加入组并且正在进行重平衡,各个消费者等待 Broker 分配分区方案;4.Stable:分区方案已经全部发送给消费者,消费者已经在正常消费;5.Dead:该消费组被 Coordinator 彻底废弃。

可以看出,重平衡发生在 PreparingRebalance 和 AwaitingSync 状态机中,重平衡主要包括以下两个步骤:

1.加入组(JoinGroup):当消费者心跳包响应 REBALANCE_IN_PROGRESS 时,说明消费组正在重平衡,此时消费者会停止消费,并且发送请求加入消费组;2.同步更新分配方案:当 Coordinator 收到所有组内成员的加入组请求后,会选出一个consumer Leader,然后让consumer Leader进行分配,分配完后会将分配方案放入SyncGroup请求中发送会Coordinator,Coordinator根据分配方案发送给每个消费者。
答案参考:https://blog.csdn.net/zchdjb/article/details/101730229

2.9

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值