工作了太安逸,没有准备好。还是挺喜欢这家公司。业务上的问题我就不放出来了。技术的问题我就凭自己的记忆写一写,以此来帮助大家和自己更好的面试。面试过程中问了很多Kafka的问题。
自己没有回答出来的,我将会百度贴出来。也欢迎大家指点错误。祝大家和自己找到满意的工作!
分为六个阶段。
阶段1、逻辑切片
:inputSplit进行标准分割,默认片的大小和块的大小一样的。Split size=Block size。每一个切片由一个MapTask处理。
阶段2、对切片的数据按一定规则解析成
对。默认是把每一行解析成键值对。
阶段3、调用map方法。每解析出一个
,调用一次map方法。每次map会输出0或者多个键值对。
阶段4、按照一定规则对阶段三输出的键值对进行分区。默认只有一个分区,分区的数据就是reducer任务运行的数量
。
阶段5、分区中键值对进行排序
。按照键进行排序,对于键相同的值进行排序。
阶段6、对数据进行局部聚合处理,combinner处理。默认是没有的。
其实面试官想问的估计是Mapreduce的分区
1、Mapreduce中将map输出的kv对,按照相同key分组,然后分发给不同的reducetask。
2、默认规则:根据key的hashcode%reducetask数来分发
所以设置分区可以设置reducetask的个数。
主要分为三个部分的消息不丢失。
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
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
就从:“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堆分为新生代和老年代,采用了不同的回收方式。(回收方式即回收算法)
常用算法有:标记-清除算法,标记-压缩算法,复制算法,分代收集算法。
1、标记-清除算法:标记活着或是死亡。(步骤:标记-清除)优点:不需要移动对象的位置,只需要标记。缺点:效率低,递归与全栈对象遍历。
2、标记-压缩算法:标记清除发的改进版。(步骤:标记-整理)优点:不会产生大量的碎片空间。缺点:如果存活的对象过多,整理阶段较多复制操作。
3、复制算法:将内存分成两部分,然后每次只使用一部分,满了复制到剩下的,然后清空,循环。优点:简单,不产生内存碎片。缺点:每次运行,内存只能使用一半。
4、分带收集算法:大多采用这种,将堆分为新生代和老年代
。在新生代中,对象生存较短,每次回收都会有大量对象死去,name这时就采用复制算法。老年代李的对象存活率较高,没有额外空间进行分配担保,所以可以使用标记-整理或者标记-清除。
1、Serial收集器:只使用一个线程去回收,串行收集器。(-XX:+UseSerialGC)
2、并行收集器:
(1)parNew:new代表新生代,所以适用于新生代。(+UseParNewGC)
(2)Parallel:类似ParNew,新生代复制算法,老年代标记-压缩,更加关注吞吐量 。(-XX:+UseParallelGC)
3、CMS收集器:
4、G1收集器:
包含了几种不同的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
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
重平衡:当集群中有新成员加入,或者某些主题增加了分区之后。消费者是怎么进行重新分配分区再进行消费的?这里就涉及到重平衡(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