Apach Kafka 是一款分布式流处理框架,用于实时构建流处理应用。它有一个核心 的功能广为人知,即 作为企业级的消息引擎被广泛使用
kafka设计
Kafka 将消息以 topic 为单位进行归纳 将向 Kafka topic 发布消息的程序成为 producers. 将预订 topics 并消费消息的程序成为 consumer. Kafka 以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个 broker. producers 通过网络将消息发送到 Kafka 集群,集群向消费者提供消息
为什么使用MQ?MQ的优点?
主要是:解耦、异步、削峰。
1、解耦:A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃…A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产 生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。如果使用 MQ,A 系统产生一条 数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统 压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超 时等情况。 就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但 是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦。
2、异步:A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求。如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应 给用户,总时长是 3 + 5 = 8ms。
削峰:减少高峰时期对服务器压力。
但是引入MQ以后要注意一下三个问题
3、系统可用性降低
本来系统运行好好的,现在你非要加入个消息队列进去,那消息队列挂了,你的系统不是呵呵了。
1、系统可用性会降低
系统复杂度提高 加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保 证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。
2、一致性问题
A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那 里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。 所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额 外的技术方案和架构来规避掉,做好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复 杂了 10 倍。但是关键时刻,用,还是得用的。
什么是消费组?
1、定义:即消费者组是 Kafka 提供的可扩展且具有容错性的消费者机制。 2、原理:在 Kafka 中,消费者组是一个由多个消费者实例 构成的组。多个实例共同订阅若干个主题, 实现共同消费。同一个组下的每个实例都配置有 相同的组 ID,被分配不同的订阅分区。当某个实例挂 掉的时候,其他实例会自动地承担起 它负责消费的分区。
在 Kafka 中,ZooKeeper 的作用是什么?
Kafka 使用 ZooKeeper 存放集群元数据、成员管理、Controller 选举,以及其他一些管理类任 务。之后,等 KIP-500 提案完成后,Kafka 将完全不再依赖 于 ZooKeeper。
“存放元数据”是指主题 分区的所有数据 都保存在 ZooKeeper 中,且以它保存的数据为权威,其他“人”都要与它 保持对齐。
“成员管理”是指 Broker 节点的注册、注销以及属性变更,等 等。“Controller 选举”是指选举集群 Controller,而其他管 理类任务包括但不限于主题 删除、参数配置等。
解释下 Kafka 中位移(offset)的作用?
在 Kafka 中,每个 主题分区下的每条消息都被赋予了一个唯一的 ID 数值,用于标识它在分区中的位 置。这个 ID 数值,就被称为位移,或者叫偏移量。一旦消息被写入到分区日志,它的位移值将不能 被 修改。
__consumer_offsets 是做什么用的?
它的主要作用是负责注册消费者以及保存位移值。可能你对保存位移值的功能很熟悉, 但其实该主题也 是保存消费者元数据的地方。千万记得把这一点也回答上。另外,这里 的消费者泛指消费者组和独立消 费者,而不仅仅是消费者组。 Kafka 的 GroupCoordinator 组件提供对该主题完整的管理功能,包括该主题的创建、 写入、读取和 Leader 维护等。
Kafka 为什么不支持读写分离?
场景不适用。读写分离适用于那种读负载很大,而写操作相对不频繁的场景,可 Kafka 不属于这 样的场景。
同步机制。Kafka 采用 PULL 方式实现 Follower 的同步,因此,Follower 与 Leader 存 在不一致 性窗口。如果允许读 Follower 副本,就势必要处理消息滞后(Lagging)的问题。
Kafka 能手动删除消息吗?
其实,Kafka 不需要用户手动删除消息。它本身提供了留存策略,能够自动删除过期消息。
对于设置了 Key 且参数 cleanup.policy=compact 的主题而言,我们可以构造一条 的 消息发送给 Broker,依靠 Log Cleaner 组件提供的功能删除掉该 Key 的消息。 对于普通主题而言,我们可以使用 kafka-delete-records 命令,或编写程序调用 Admin.deleteRecords 方法来删除消息。这两种方法殊途同归,底层都是调用 Admin 的 deleteRecords 方法,通过将分区 Log Start Offset 值抬高的方式间接删除消息。
LEO、LSO、AR、ISR、HW 都表示什么含义?
LEO:Log End Offset。日志末端位移值或末端偏移量,表示日志下一条待插入消息的 位移值。举 个例子,如果日志有 10 条消息,位移值从 0 开始,那么,第 10 条消息的位 移值就是 9。此时, LEO = 10。
LSO:Log Stable Offset。这是 Kafka 事务的概念。如果你没有使用到事务,那么这个 值不存在(其 实也不是不存在,只是设置成一个无意义的值)。该值控制了事务型消费 者能够看到的消息范围。 它经常与 Log Start Offset,即日志起始位移值相混淆,因为 有些人将后者缩写成 LSO,这是不对 的。在 Kafka 中,LSO 就是指代 Log Stable Offset。 AR:Assigned Replicas。AR 是主题被创建后,分区创建时被分配的副本集合,副本个 数由副本因 子决定。
AR:Assigned Replicas。AR 是主题被创建后,分区创建时被分配的副本集合,副本个 数由副本因 子决定。
ISR:In-Sync Replicas。Kafka 中特别重要的概念,指代的是 AR 中那些与 Leader 保 持同步的副本 集合。在 AR 中的副本可能不在 ISR 中,但 Leader 副本天然就包含在 ISR 中。关于 ISR,还有一 个常见的面试题目是如何判断副本是否应该属于 ISR。目前的判断 依据是:Follower 副本的 LEO 落后 Leader LEO 的时间,是否超过了 Broker 端参数 replica.lag.time.max.ms 值。如果超过 了,副本就会被从 ISR 中移除。
HW:高水位值(High watermark)。这是控制消费者可读取消息范围的重要字段。一 个普通消费者 只能“看到”Leader 副本上介于 Log Start Offset 和 HW(不含)之间的 所有消息。水位以上的消息是 对消费者不可见的。关于 HW,问法有很多,我能想到的 最高级的问法,就是让你完整地梳理下 Follower 副本拉取 Leader 副本、执行同步机制 的详细步骤。
在 Kafka 中,每个分区(Partition)都有一个 Leader。Leader 是分区的数据管理者,负责处理所有的读写请求。Kafka 集群中的生产者(Producer)和消费者(Consumer)都通过与 Leader 节点进行交互来读写数据。每个分区只能有一个 Leader。
Leader 的责任不仅限于提供数据访问接口,它还负责确保数据的一致性。当生产者向 Kafka 写入数据时,数据会被写入到 Leader 节点上,而消费者从 Leader 节点读取数据。由于 Leader 是分区的主节点,所有的请求都必须通过它进行。
与 Leader 对应的是 Follower 节点。每个分区的 Follower 是 Leader 的备份副本,它从 Leader 节点获取数据并进行同步。Follower 节点不会直接处理写请求,它们只负责从 Leader 读取数据并保持数据一致性。Follower 还可以用于提供读请求的负载均衡,尤其是在集群的读请求压力较大的时候。
如果 Leader 节点发生故障,Kafka 会从已同步的 Follower 中选举出一个新的 Leader,保证系统的高可用性和数据不丢失。这意味着,Follower 节点在故障恢复中起到了至关重要的作用。
在 Kafka 中,所有分区的数据都有多个副本,这些副本分布在集群中的不同 Broker 节点上。这些副本称为 Replica。每个分区有一个 Leader 副本和多个 Follower 副本,所有副本都会保持数据的同步。副本的数量可以在 Kafka 配置中进行设置,通常为 3 个副本,以保证数据的持久性和容错能力。
Replica 的存在确保了数据的冗余备份,即使某些节点发生故障,数据依然能够从其他副本中恢复。Kafka 会定期同步 Leader 和 Follower 之间的数据,确保副本之间的数据一致性。
分区 Leader 选举策略有几种?
分区的 Leader 副本选举对用户是完全透明的,它是由 Controller 独立完成的。你需要回答的是,在哪 些场景下,需要执行分区 Leader 选举。每一种场景对应于一种选举策略。当前,Kafka 有 4 种分区 Leader 选举策略。
OfflinePartition Leader 选举:每当有分区上线时,就需要执行 Leader 选举。所谓的分区上线, 可能是创建了新分区,也可能是之前的下线分区重新上线。这是最常见的分区 Leader 选举场景。 ReassignPartition Leader 选举:当你手动运行 kafka-reassign-partitions 命令,或者是调用 Admin 的 alterPartitionReassignments 方法执行分区副本重分配时,可能触发此类选举。假设原 来的 AR 是[1,2,3],Leader 是 1,当执行副本重分配后,副本集 合 AR 被设置成[4,5,6],显 然,Leader 必须要变更,此时会发生 Reassign Partition Leader 选举。
PreferredReplicaPartition Leader 选举:当你手动运行 kafka-preferred-replica- election 命 令,或自动触发了 Preferred Leader 选举时,该类策略被激活。所谓的 Preferred Leader,指的 是 AR 中的第一个副本。比如 AR 是[3,2,1],那么, Preferred Leader 就是 3。 ControlledShutdownPartition Leader 选举:当 Broker 正常关闭时,该 Broker 上 的所有 Leader 副本都会下线,因此,需要为受影响的分区执行相应的 Leader 选举。
Kafka 消息是采用 Pull 模式,还是 Push 模式?
Kafka 最初考虑的问题是,customer 应该从 brokes 拉取消息还是 brokers 将消息推送到 consumer,也就是 pull 还 push。在这方面,Kafka 遵循了一种大部分消息系统共同的传统 的设计:producer 将消息推送到 broker,consumer 从 broker 拉取消息 一些消息系统比如 Scribe 和 Apache Flume 采用了 push 模式,将消息推送到下游的 consumer。 这样做有好处也有坏处:由 broker 决定消息推送的速率,对于不同消费速率的consumer 就不太好处理了。消息系统都致力于让 consumer 以最大的速率最快速的消费消 息,但不幸的是,push 模式下,当 broker 推送的速率远大于 consumer 消费的速率时, consumer 恐怕就要崩溃了。 最终, Kafka 还是选取了传统的 pull 模式。
Pull 模式的另外一个好处是 consumer 可以自主决定是否批量的从 broker 拉取数据。Push 模式必须在不知道下游 consumer 消费能力和消费策略的情况下决定是立即推送每条消息还 是缓存之后批量推送。如果为了避免 consumer 崩溃而采用较低的推送速率,将可能导致一 次只推送较少的消息而造成浪费。Pull 模式下,consumer 就可以根据自己的消费能力去决 定这些策略 Pull 有个缺点是,如果 broker 没有可供消费的消息,将导致 consumer 不断在循环中轮询, 直到新消息到 t 达。为了避免这点,Kafka 有个参数可以让 consumer 阻塞知道新消息到达 (当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量Pull)
Kafka 存储在硬盘上的消息格式是什么?
消息由一个固定长度的头部和可变长度的字节数组组成。头部包含了一个版本号和 CRC32 校验码。

kafka 的 ack 机制
acks=0:此模式下,生产者在发送消息后不会等待任何来自Broker的确认响应。这意味着一旦消息被发送出去,即使Broker没有成功写入磁盘,生产者也会继续处理其他任务。这种模式适用于对延迟要求极高且可以容忍一定数据丢失的场景,但如果Broker发生故障,可能会导致消息丢失。
acks=1:在此模式下,生产者需要等待Leader副本成功将消息写入本地日志文件后才返回确认。这种模式提供了一定的可靠性保证,因为至少有一个副本已经保存了消息。如果Leader副本在follower副本同步之前崩溃,消息可能会丢失。这种模式适用于大多数应用场景,能够在可接受的延迟范围内提供较好的消息可靠性。
acks=all或acks=-1:在此模式下,生产者需要等待所有在ISR(In-Sync Replicas)中的副本都成功写入消息后才返回确认。这种模式提供了最高的消息可靠性保证,因为只有当所有副本都成功写入消息时,生产者才认为消息已经成功发送。这种模式适用于对消息可靠性要求极高的场景,但相应的延迟也会增加。这确保了消息的可靠性,但会导致更长的延迟。
Kafka 的消费者如何消费数据
消费者每次消费数据的时候,消费者都会记录消费的物理偏移量(offset)的位置 等到下次消费时,他会接着上次位置继续消费.
partition 的数据如何保存到硬盘
topic 中的多个 partition 以文件夹的形式保存到 broker,每个分区序号从 0 递增, 且消息有序 Partition 文件下有多个 segment(xxx.index,xxx.log) segment 文件里的 大小和配置文件大小一致可以根据要求修改 默认为 1g 如果大小大于 1g 时,会滚动一个新的 segment 并且以上一个 segment 最后一条消息的偏移量命名.
Kafka 新建的分区会在哪个目录下创建
在启动 Kafka 集群之前,我们需要配置好 log.dirs 参数,其值是 Kafka 数据的存放目录, 这个参数可以配置多个目录,目录之间使用逗号分隔,通常这些目录是分布在不同的磁盘 上用于提高读写性能。 当然我们也可以配置 log.dir 参数,含义一样。只需要设置其中一个即可。 如果 log.dirs 参数只配置了一个目录,那么分配到各个 Broker 上的分区肯定只能在这个 目录下创建文件夹用于存放数据。
但是如果 log.dirs 参数配置了多个目录,那么 Kafka 会在哪个文件夹中创建分区目录呢?
答案是:Kafka 会在含有分区目录最少的文件夹中创建新的分区目录,分区目录名为 Topic 名+分区 ID。注意,是分区文件夹总数最少的目录,而不是磁盘使用量最少的目录!也就 是说,如果你给 log.dirs 参数新增了一个新的磁盘,新的分区目录肯定是先在这个新的磁 盘上创建直到这个新的磁盘目录拥有的分区目录不是最少为止。
Kafka 创建 Topic 时如何将分区放置到不同的 Broker中
副本因子不能大于 Broker 的个数;
第一个分区(编号为 0)的第一个副本放置位置是随机从 brokerList 选择的;
其他分区的第一个副本放置位置相对于第 0 个分区依次往后移。也就是如果我们有 5 个 Broker,5 个分区,假设第一个分区放在第四个 Broker 上,那么第二个分区将会放在第五 个 Broker 上;第三个分区将会放在第一个 Broker 上;第四个分区将会放在第二个 Broker 上,依次类推;
剩余的副本相对于第一个副本放置位置其实是由 nextReplicaShift 决定的,而这个数也是 随机产生的.剩余的副本相对于第一个副本放置位置其实是由 nextReplicaShift 决定的,而这个数也是 随机产生的
Kafka的数据有序
一个消费者组里它的内部是有序的,消费者组与消费者组之间是无序的。
Kafka数据传输的事务定义有哪三种?
数据传输的事务定义通常有以下三种级别:
(1)最多一次: 消息不会被重复发送,最多被传输一次,但也有可能一次不传输
(2)最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输.
(3)精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被一次而 且仅仅被传输一次,这是大家所期望的。
Kafka 判断一个节点是否还活着有那两个条件?
(1)节点必须可以维护和 ZooKeeper 的连接,Zookeeper 通过心跳机制检查每个节点的连接。(2)如果节点是个 follower,他必须能及时的同步 leader 的写操作,延时不能太久。
监控 Kafka 的框架都有哪些?
1. Kafka Manager:应该算是最有名的专属 Kafka 监控框架了,是独立的监控系统。
2. Kafka Monitor:LinkedIn 开源的免费框架,支持对集群进行系统测试,并实时监控测 试结果。 3. CruiseControl:也是 LinkedIn 公司开源的监控框架,用于实时监测资源使用率,以及 提供常用运 维操作等。无 UI 界面,只提供 REST API。
4. JMX 监控:由于 Kafka 提供的监控指标都是基于 JMX 的,因此,市面上任何能够集成 JMX 的框架 都可以使用,比如 Zabbix 和 Prometheus。
5. 已有大数据平台自己的监控体系:像 Cloudera 提供的 CDH 这类大数据平台,天然就提 供 Kafka 监 控方案。
6. JMXTool:社区提供的命令行工具,能够实时监控 JMX 指标。答上这一条,属于绝对 的加分项,因 为知道的人很少,而且会给人一种你对 Kafka 工具非常熟悉的感觉。如果 你暂时不了解它的用 法,可以在命令行以无参数方式执行一下kafka-run-class.sh kafka.tools.JmxTool,学习下它的用 法。
通过Kafka的AdminClient的监控消费堆积情况.

如何设置 Kafka 能接收的最大消息的大小?
如 果 Producer 都不能向 Broker 端发送数据很大的消息,又何来消费一说呢? 因此,你需要同时设置 Broker 端参数和 Consumer 端参数。
Broker 端参数:message.max.bytes、max.message.bytes(主题级别)和 replica.fetch.max.bytes。 Consumer 端参数:fetch.message.max.bytes。 Broker 端的最后一个参数比较容易遗漏。我们必须调整 Follower 副本能够接收的最大消 息的大小,否 则,副本同步就会失败。
Kafka单个消息的大小,默认值为1MB。如果生产者尝试发送的消息超过这个大小,不仅消息不会被接收,还会收到 broker 返回的错误消息
Kafka分区分配策略
在Kafka内部存在两种默认的分区分配策略:Range和RoundRobin。
Range是默认策略。Range是对每个Topic而言的(即一个Topic一个Topic分),首先对同一个Topic里面的分区按序号进行排序,并对消费者按照字母顺序进行排序。然后用Partition分区的个数除以消费者总线程数来决定每个消费者线程消费几个分区的数据。如果除不尽,那前几个消费者线程会多消费一个分区。

自定义分区分配策略
public class BusinessKeyPartitioner implements Partitioner {
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
// 假设我们的消息是订单消息
OrderMessage order = (OrderMessage) value;
// 使用用户ID作为分区依据,保证同一用户的订单进入同一分区
String userId = order.getUserId();
int partitionCount = cluster.partitionsForTopic(topic).size();
return Math.abs(userId.hashCode() % partitionCount);
}
}
我们基于用户ID来选择分区,这样可以保证同一个用户的所有订单都进入同一个分区,从而保证了订单处理的顺序性。
默认分区选择器分配策略
public class DefaultPartitioner implements Partitioner {
private final StickyPartitionCache stickyPartitionCache = new StickyPartitionCache();
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
// 如果没有指定key,使用轮询粘性分区
if (keyBytes == null) {
return stickyPartitionCache.partition(topic, cluster);
}
// 使用key的哈希值选择分区
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return Utils.toPositive(Utils.murmur2(keyBytes)) % partitions.size();
}
}
-
key为null的处理:
-
以前的版本会随机选择分区
-
现在改用了"粘性分区"策略,这样可以减少不必要的分区切换,提高批处理效率
-
-
key不为null的处理:
-
使用murmur2哈希算法,这个算法的特点是计算速度快,散列效果好
-
通过取模确保分区号在合法范围内
-
粘性分区实现策略
public class StickyPartitionCache {
private final ConcurrentMap<String, Integer> cache = new ConcurrentHashMap<>();
public int partition(String topic, Cluster cluster) {
Integer part = cache.get(topic);
if (part == null || !isValidPartition(cluster, topic, part)) {
part = nextPartition(topic, cluster);
cache.put(topic, part);
}
return part;
}
private int nextPartition(String topic, Cluster cluster) {
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
int nextValue = ThreadLocalRandom.current().nextInt(partitions.size());
return nextValue;
}
}
每次都随机选择分区,而是会尽量保持使用同一个分区,直到这个分区不可用。这样做的好处是可以提高批处理的效率,因为消息更可能被批量发送到同一个分区。
Kafka集群机器数目计算
设用压力测试测出机器写入速度是20M/s一台,峰值的业务数据的速度是100M/s,副本数为6,预估需要部署Kafka机器数量为()
Kafka机器数量=2*(峰值生产速度*副本数/100)+1
所以2*(100*6/100)+1)= 13台,故需要13台机器。
Controller 给Broker 发送的请求不包括
Controller 会向Broker发送三类请求,分别是LeaderAndIsrRequest、StopReplicaRequest和UpdateMetadataRequest。
A:LeaderAndIsrRequest:告诉 Broker 相关主题各个分区的Leader副本位于哪台Broker 上、ISR中的副本都在哪些Broker上。
B:StopReplicaRequest:告知指定Broker停止它上面的副本对象,该请求甚至还能删除副本底层的日志数据。
C:UpdateMetadataRequest:该请求会更新Broker上的元数据缓存。
Kafka的零拷贝如何实现?
传统IO
在开始谈零拷贝之前,首先要对传统的IO方式有一个概念。
基于传统的IO方式,底层实际上通过调用read()和write()来实现。
通过read()把数据从硬盘读取到内核缓冲区,再复制到用户缓冲区;然后再通过write()写入到socket缓冲区,最后写入网卡设备。
整个过程发生了4次用户态和内核态的上下文切换和4次拷贝,具体流程如下:
-
用户进程通过
read()方法向操作系统发起调用,此时上下文从用户态转向内核态 -
DMA控制器把数据从硬盘中拷贝到读缓冲区
-
CPU把读缓冲区数据拷贝到应用缓冲区,上下文从内核态转为用户态,
read()返回 -
用户进程通过
write()方法发起调用,上下文从用户态转为内核态 -
CPU将应用缓冲区中数据拷贝到socket缓冲区
-
DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,
write()返回

mmap+write
mmap+write简单来说就是使用mmap替换了read+write中的read操作,减少了一次CPU的拷贝。
mmap主要实现方式是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次CPU拷贝。

整个过程发生了4次用户态和内核态的上下文切换和3次拷贝,具体流程如下:
-
用户进程通过
mmap()方法向操作系统发起调用,上下文从用户态转向内核态 -
DMA控制器把数据从硬盘中拷贝到读缓冲区
-
上下文从内核态转为用户态,mmap调用返回
-
用户进程通过
write()方法发起调用,上下文从用户态转为内核态 -
CPU将读缓冲区中数据拷贝到socket缓冲区
-
DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,
write()返回
mmap的方式节省了一次CPU拷贝,同时由于用户进程中的内存是虚拟的,只是映射到内核的读缓冲区,所以可以节省一半的内存空间,比较适合大文件的传输。
sendfile
相比mmap来说,sendfile同样减少了一次CPU拷贝,而且还减少了2次上下文切换。
sendfile是Linux2.1内核版本后引入的一个系统调用函数,通过使用sendfile数据可以直接在内核空间进行传输,因此避免了用户空间和内核空间的拷贝,同时由于使用sendfile替代了read+write从而节省了一次系统调用,也就是2次上下文切换。

整个过程发生了2次用户态和内核态的上下文切换和3次拷贝,具体流程如下:
-
用户进程通过
sendfile()方法向操作系统发起调用,上下文从用户态转向内核态 -
DMA控制器把数据从硬盘中拷贝到读缓冲区
-
CPU将读缓冲区中数据拷贝到socket缓冲区
-
DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,
sendfile调用返回
sendfile方法IO数据对用户空间完全不可见,所以只能适用于完全不需要用户空间处理的情况,比如静态文件服务器。
sendfile+DMA Scatter/Gather
Linux2.4内核版本之后对sendfile做了进一步优化,通过引入新的硬件支持,这个方式叫做DMA Scatter/Gather 分散/收集功能。
它将读缓冲区中的数据描述信息--内存地址和偏移量记录到socket缓冲区,由 DMA 根据这些将数据从读缓冲区拷贝到网卡,相比之前版本减少了一次CPU拷贝的过程。

整个过程发生了2次用户态和内核态的上下文切换和2次拷贝,其中更重要的是完全没有CPU拷贝,具体流程如下:
-
用户进程通过
sendfile()方法向操作系统发起调用,上下文从用户态转向内核态 -
DMA控制器利用scatter把数据从硬盘中拷贝到读缓冲区离散存储
-
CPU把读缓冲区中的文件描述符和数据长度发送到socket缓冲区
-
DMA控制器根据文件描述符和数据长度,使用scatter/gather把数据从内核缓冲区拷贝到网卡
-
sendfile()调用返回,上下文从内核态切换回用户态
DMA gather和sendfile一样数据对用户空间不可见,而且需要硬件支持,同时输入文件描述符只能是文件,但是过程中完全没有CPU拷贝过程,极大提升了性能。
由于CPU和IO速度的差异问题,产生了DMA技术,通过DMA搬运来减少CPU的等待时间。
传统的IOread+write方式会产生2次DMA拷贝+2次CPU拷贝,同时有4次上下文切换。
而通过mmap+write方式则产生2次DMA拷贝+1次CPU拷贝,4次上下文切换,通过内存映射减少了一次CPU拷贝,可以减少内存使用,适合大文件的传输。
sendfile方式是新增的一个系统调用函数,产生2次DMA拷贝+1次CPU拷贝,但是只有2次上下文切换。因为只有一次调用,减少了上下文的切换,但是用户空间对IO数据不可见,适用于静态文件服务器。
sendfile+DMA gather方式产生2次DMA拷贝,没有CPU拷贝,而且也只有2次上下文切换。虽然极大地提升了性能,但是需要依赖新的硬件设备支持。
Kafka的消息存储文件清理机制
1、基于时间的删除策略
public class TimeBasedRetentionPolicy {
private final long retentionMs; // 保留时间
private final Timer cleanupTimer; // 清理定时器
public void cleanupExpiredSegments() {
for (LogSegment segment : segments) {
// 检查段的年龄
long segmentAge = System.currentTimeMillis() - segment.lastModified();
// 如果超过保留时间且不是活跃段,则删除
if (segmentAge > retentionMs && !segment.isActive()) {
deleteSegment(segment);
}
}
}
private void deleteSegment(LogSegment segment) {
try {
// 1. 关闭文件句柄
segment.close();
// 2. 删除索引文件
segment.deleteAssociatedIndexes();
// 3. 删除数据文件
segment.deleteFile();
} catch (IOException e) {
handleDeletionError(segment, e);
}
}
}
2、基于大小的删除策略
public class SizeBasedRetentionPolicy {
private final long maxBytes; // 总大小限制
public void enforceRetentionSize() {
long currentSize = calculateCurrentSize();
while (currentSize > maxBytes && !segments.isEmpty()) {
LogSegment oldestSegment = segments.first(); // 获取最老的段
if (!oldestSegment.isActive()) {
currentSize -= oldestSegment.size();
deleteSegment(oldestSegment);
} else {
break; // 不能删除活跃段
}
}
}
private long calculateCurrentSize() {
return segments.stream()
.mapToLong(LogSegment::size)
.sum();
}
}
Log Compaction的特点:
-
基于Key的压缩:
-
相同Key只保留最新的值
-
可以节省大量存储空间
-
特别适合配置类的数据
-
-
压缩过程:
-
会在后台异步运行
-
分为多个清理线程
-
有专门的清理线程池
-
-
使用场景:
-
配置中心
-
数据变更记录
-
状态存储
-
来看看具体实现:
public class LogCompaction {
private final Map<String, Long> offsetMap = new ConcurrentHashMap<>();
public class CleanerConfig {
private final double cleanRatio; // 清理比例
private final int bufferSize; // 清理缓冲区大小
private final int threads; // 清理线程数
public CleanerConfig() {
this.cleanRatio = 0.5; // 默认清理比例
this.bufferSize = 1024 * 1024; // 1MB缓冲区
this.threads = 1; // 单线程清理
}
}
// 执行日志压缩
public void compact(LogSegment segment) {
// 1. 构建偏移量索引
buildOffsetMap(segment);
// 2. 执行清理
cleanSegment(segment);
}
private void buildOffsetMap(LogSegment segment) {
for (Record record : segment.records()) {
// 为每个key记录最新偏移量
offsetMap.put(record.key(), record.offset());
}
}
private void cleanSegment(LogSegment segment) {
// 创建临时段文件
LogSegment cleanedSegment = createTemporarySegment();
for (Record record : segment.records()) {
// 只保留每个key的最新值
if (offsetMap.get(record.key()) == record.offset()) {
cleanedSegment.append(record);
}
}
// 替换原始段文件
replaceSegmentFile(segment, cleanedSegment);
}
}
Kafka的消息0丢失的最佳时间配置如下:
生产端:不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。
记住,一定要使用带有回调通知的 send 方法。
生产端:设置 acks = all 设置最高可靠的、最为严格的发送确认机制。acks 设置成 all,则表明所有副本 Broker 都要接收到消息,该消息才算是“已提交”。这是最高等级的“已提交”定义。
生产端:设置 retries 为一个较大的值如10,设置严格的消息重试机制,包括增加重试次数。当出现网络的瞬时抖动时,消息发送可能会失败,retries 较大,能够自动重试消息发送,避免消息丢失。
Broker 端设置 unclean.leader.election.enable = false。它控制的是哪些 Broker 有资格竞选分区的 Leader。如果一个 Broker 落后原先的 Leader 太多,那么它一旦成为新的 Leader,必然会造成消息的丢失。故一般都要将该参数设置成 false,即不允许这种情况的发生。
Broker 端设置 replication.factor >= 3。这里想表述的是,最好将消息多保存几份,毕竟目前防止消息丢失的主要机制就是冗余。
Broker 端设置 min.insync.replicas > 1。控制的是消息至少要被写入到多少个副本才算是“已提交”。设置成大于 1 可以提升消息持久性。在实际环境中千万不要使用默认值 1。
Broker 端设置 replication.factor > min.insync.replicas。如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作了。我们不仅要改善消息的持久性,防止数据丢失,还要在不降低可用性的基础上完成。推荐设置成 replication.factor = min.insync.replicas + 1。
Consumer 端 确保消息消费完成再提交。Consumer 端有个参数 enable.auto.commit 设置成 false,并采用将两个 API 方法commitSync() 和 commitAsync() 组合使用进行手动提交位移的方式。这对于单 Consumer 多线程处理的场景而言是至关重要的。
业务维度的的0丢失架构, 采用 本地消息表+定时扫描 架构方案,实现业务维度的 0丢失,100%可靠性。

831

被折叠的 条评论
为什么被折叠?



