Kafka简介

本文介绍了Kafka的架构设计,包括Producer、Broker和Consumer的角色与功能,详细解析了Kafka如何通过高效的存储机制、复制机制及零拷贝技术实现高性能消息传递。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

架构

Producer发送消息到某个指定的Topic,Broker负责存储消息,Consumer通过订阅的方式消费指定Topic的消息。
这里写图片描述

实现细节

Kafka 依靠下列 4 点达到了高吞吐量、低延时的设计目标的。
1、大量使用操作系统页缓存,内存操作速度快且命中率高。
2、Kafka 不直接参与物理 1/0 操作,而是交由最擅长此事的操作系统来完成。
3、采用追加写入方式,摒弃了缓慢的磁盘随机读/写操作(甚至比内存随机读写速度快)。
4、使用以 sendfile 为代表的零拷贝技术加强网络间的数据传输效率。

Kafka 实现持久化的设计也有新颖之处。普通的系统在实现持久化时可能会先尽量使用内存,当内存资源耗尽时,再一次性地把数据“刷盘”;而 Kafka 则反其道而行之, 所有数据都会立即被写入文件系统的持久化日志中,之后 Kafka 服务器才会返回结果给客户端通知它们消息已被成功写入。这样做既实时保存了数据,又减少了 Kafka 程序对于内存的消耗,从而将节省出的内存留给页缓存使用,更进一步地提升了整体性能 。

Partition

这里写图片描述
物理上把Topic分成一个或多个Partition,每个Partition在物理上对应一个文件夹,该文件夹下存储这个Partition的所有消息和索引文件。在发送一条消息时,可以指定这条消息的key,Producer根据这个key和Partition机制来判断应该将这条消息发送到哪个Parition。同一Topic的一条消息只能被同一个Consumer Group内的一个Consumer消费,但多个Consumer Group可同时消费这一消息。上面两点跟rocket MQ有点相似。

Kafka集群会保留所有的消息,无论其被消费与否。并提供两种策略删除旧数据。一是基于时间,二是基于Partition文件大小。

顺序写且充分利用Page Cache。

Replica

同一个Partition可能会有多个Replica,而这时需要在这些Replication之间选出一个Leader,Producer和Consumer只与这个Leader交互,其它Replica作为Follower从Leader中复制数据。

Producer在发布消息到某个Partition时,先通过ZooKeeper找到该Partition的Leader。Producer只将该消息发送到该Partition的Leader。Leader会将该消息写入其本地Log。每个Follower都从Leader pull数据。Follower在收到该消息并写入其Log后,向Leader发送ACK。一旦Leader收到了ISR(该potitions的Replica集群)中的所有Replica的ACK,该消息就被认为已经commit了,Leader将增加HW并且向Producer发送ACK。Consumer读消息也是从Leader读取,只有被commit过的消息(offset低于HW的消息)才会暴露给Consumer。为了提高性能,每个Follower在接收到数据后就立马向Leader发送ACK,而非等到数据写入Log中。因此,对于已经commit的消息,Kafka只能保证它被存于多个Replica的内存中,而不能保证它们被持久化到磁盘中。

一般一个Topic的Partition数量大于Broker的数量。同时为了提高Kafka的容错能力,也需要将同一个Partition的Replica尽量分散到不同的机器。

分配算法

  • 将所有Broker(假设共n个Broker)和待分配的Partition排序
  • 将第i个Partition分配到第(i mod n)个Broker上
  • 将第i个Partition的第j个Replica分配到第((i + j) mode n)个Broker上

Leader Election

所有Follower都在ZooKeeper上设置一个Watch,一旦Leader宕机,其对应的ephemeral znode会自动删除,此时所有Follower都尝试创建该节点,而创建成功者(ZooKeeper保证只有一个能创建成功)即是新的Leader,其它Replica即为Follower

遇到的问题

  • (split-brain)ZooKeeper能保证所有Watch按顺序触发,但并不能保证同一时刻所有Replica“看”到的状态是一样的,这就可能造成不同Replica的响应不一致
  • (herd effect)如果宕机的那个Broker上的Partition比较多,会造成多个Watch被触发,造成集群内大量的调整
  • ZooKeeper负载过重 每个Replica都要为此在ZooKeeper上注册一个Watch,当集群规模增加到几千个Partition时ZooKeeper负载会过重(解决方法:Broker中选出一个controller,所有Partition的Leader选举都由controller决定)。

Consumer Rebalance

分配算法

  • 将目标Topic下的所有Partirtion排序
  • 对某Consumer Group下所有Consumer排序,第i个Consumer记为Ci
  • N=size(PT)/size(Consumer),向上取整
  • 解除Ci对原来分配的Partition的消费权(i从0开始)
  • 将第i∗N到(i+1)∗N−1个Partition分配给Ci

Offset管理

Consumer将从某个Partition读取的最后一条消息的offset存于ZooKeeper中

零拷贝

sendfile直接将磁盘文件发送到网络设备,减少内存复制次数和内核态到用户态的切换

可以通过调整/proc/sys/vm/dirty_background_ratio和/proc/sys/vm/dirty_ratio来调优性能。

  • 脏页率超过第一个指标会启动pdflush开始Flush Dirty PageCache。
  • 脏页率超过第二个指标会阻塞所有的写操作来进行Flush。
  • 根据不同的业务需求可以适当的降低dirty_background_ratio和提高dirty_ratio

Producer MessageSet

利用批量发送,压缩等技术,减少网络开销、提示利用率

Kafka事务

  • 最多一次: 消息不会被重复发送,最多被传输一次,但也有可能一次不传输;
  • 最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输;
  • 精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是大家所期望的,但是很难做到
    MQTT协议的QOS
    https://blog.youkuaiyun.com/zhangdongnihao/article/details/108242517
    https://www.cnblogs.com/saryli/p/9782421.html
    http://www.blogjava.net/yongboy/archive/2014/02/15/409893.html

consumer可以先读取消息,然后将offset写入日志文件中,然后再处理消息。这存在一种可能就是在存储offset后还没处理消息就crash了,新的consumer继续从这个offset处理那么就会有些消息永远不会被处理,这就是上面说的“最多一次”。

consumer可以先读取消息,处理消息,最后记录offset,当然如果在记录offset之前就crash了,新的consumer会重复的消费一些消息,这就是上面说的“最少一次”;

“精确一次”可以通过将提交分为两个阶段来解决:保存了offset后提交一次,消息处理成功之后再提交一次。但是还有个更简单的做法:将消息的offset和消息被处理后的结果保存在一起 ??太难了

主流MQ对比

这里写图片描述

demo

<dependency>
  <groupId>org.apache.kafka</groupId>
  <artifactId>kafka_2.9.2</artifactId>
  <version>0.8.2.2</version>
</dependency>

<dependency>
  <groupId>org.apache.kafka</groupId>
  <artifactId>kafka-clients</artifactId>
  <version>0.10.0.0</version>
</dependency>
public class KafkaProducerTest {  
      
    private static final Logger LOG = LoggerFactory.getLogger(KafkaProducerTest.class);  
      
    private static Properties properties = null;  
      
    //  kafka连续配置 项
    static {  
    	properties = new Properties();  
       	properties.put("bootstrap.servers", "centos.master:9092,centos.slave1:9092,centos.slave2:9092");
        properties.put("producer.type", "sync");  
        properties.put("request.required.acks", "1");  
        properties.put("serializer.class", "kafka.serializer.DefaultEncoder");  
        properties.put("partitioner.class", "kafka.producer.DefaultPartitioner");  
        properties.put("key.serializer", "org.apache.kafka.common.serialization.ByteArraySerializer");  
        properties.put("value.serializer", "org.apache.kafka.common.serialization.ByteArraySerializer");  
    }

	public void produce() {  
        KafkaProducer<byte[], byte[]> kafkaProducer = new KafkaProducer<byte[],byte[]>(properties);  
        ProducerRecord<byte[],byte[]> kafkaRecord = new ProducerRecord<byte[],byte[]>(  
                "test", "kkk".getBytes(), "vvv".getBytes());  
        kafkaProducer.send(kafkaRecord, new Callback() {  
            public void onCompletion(RecordMetadata metadata, Exception e) {  
                if(null != e) {  
                    LOG.info("the offset of the send record is {}", metadata.offset());  
                    LOG.error(e.getMessage(), e);  
                }  
                LOG.info("complete!");  
            }  
        });  
        kafkaProducer.close();  
    }  
  
    public static void main(String[] args) {  
        KafkaProducerTest kafkaProducerTest = new KafkaProducerTest();  
        for (int i = 0; i < 10; i++) {  
            kafkaProducerTest.produce();  
        }  
    }  
}  
public class ConsumerSample {
   public static void main(String[] args) {
    	Properties props = new Properties();
    	props.put("zk.connect", "localhost:2181");
    	props.put("zk.connectiontimeout.ms", "1000000");
    	props.put("key.serializer", "org.apache.kafka.common.serialization.ByteArraySerializer");  
     	props.put("value.serializer", "org.apache.kafka.common.serialization.ByteArraySerializer");  
    	props.put("groupid", "test_group");

    	ConsumerConfig consumerConfig = new ConsumerConfig(props);
    	ConsumerConnector consumerConnector = 
               Consumer.createJavaConsumerConnector(consumerConfig);

    	HashMap<String, Integer> map = new HashMap<String, Integer>();
	    map.put("test-topic", 4);
	    Map<String, List<KafkaStream<Message>>> topicMessageStreams = 
	   	consumerConnector.createMessageStreams(map);
	    List<KafkaStream<Message>> streams = topicMessageStreams.get("test-topic");

	    ExecutorService executor = Executors.newFixedThreadPool(4); 

	    for (final KafkaStream<Message> stream : streams) {
	          executor.submit(new Runnable() {
	            public void run() {
	                for (MessageAndMetadata msgAndMetadata : stream) {
		                System.out.println("topic: " + msgAndMetadata.topic());
		                Message message = (Message) msgAndMetadata.message();
		                ByteBuffer buffer = message.payload();
		                buffer.get(bytes);
		                String tmp = new String(bytes);
		                System.out.println("message content: " + tmp);
	                }
	         }
	     });
      }
  }
}

参数调优

# Broker
log.dirs Kafka数据存放的目录。可以指定多个目录,中间用逗号分隔,当新partition被创建的时会被存放到 当前存放partition最少的目录.
num.io.threads 服务器用来执行读写请求的IO线程数,此参数的数量至少要等于服务器上磁盘的数量.
queued.max.requests I/O线程可以处理请求的队列大小,若实际请求数超过此大小,网络线程将停止接收新的请求,建议500-1000.
num.partitions 默认partition数量,如果topic在创建时没有指定partition数量,默认使用此值,建议修改为consumer 数量的1-3倍.
log.segment.bytes Segment文件的大小,超过此值将会自动新建一个segment,此值可以被topic级别的参数覆盖, 建议1G ~ 5G.
default.replication.factor 默认副本数量,建议改为2.
num.replica.fetchers Leader处理replica fetch消息的线程数量, 建议设置大点2-4.
offsets.topic.num.partitions offset提交主题分区的数量,建议设置为100 ~ 200


# producer
request.required.acks :用来控制一个produce请求怎样才能算完成, 主要是来表示写入数据的持久化的,有三个值(0, 1, -1), 持久化的程度依次增高.
producer.type : 同步异步模式。async表示异步,sync表示同步。如果设置成异步模式,可以允许生产者以batch的形式push数据,这样会极大的提高broker性能,推荐设置为异步.
partitioner.class : Partition类,默认对key进行hash, 即 kafka.producer.DefaultPartitioner.
compression.codec :指定producer消息的压缩格式,可选参数为: “none”, “gzip” and “snappy”.
queue.buffering.max.ms :启用异步模式时,producer缓存消息的时间。比如我们设置成1000时,它会缓存1秒的数据再一次发送出去,这样可以极大的增加broker吞吐量,但也会造成时效性的降低.
queue.buffering.max.messages:采用异步模式时producer buffer 队列里最大缓存的消息数量,如果超过这个数值,producer就会阻塞或者丢掉消息.
batch.num.messages:采用异步模式时,一个batch缓存的消息数量。达到这个数量值时producer才会发送消息.


# consumer
fetch.message.max.bytes:查询topic-partition时允许的最大消息大小,consumer会为每个partition缓存此大小的消息到内存,因此,这个参数可以控制consumer的内存使用量。这个值应该至少比server允许的最大消息大小大,以免producer发送的消息大于consumer允许的消息.
num.consumer.fetchers:拉数据的线程数量,为了保序,建议一个,用默认值.
auto.commit.enable:如果此值设置为true,consumer会周期性的把当前消费的offset值保存到zookeeper,当consumer失败重启之后将会使用此值作为新开始消费的值.
auto.commit.interval.ms: Consumer提交offset值到zookeeper的周期.

参考资料

http://kafka.apache.org/
https://blog.youkuaiyun.com/b2222505/article/details/78408146
https://www.ibm.com/developerworks/cn/java/j-zerocopy/
https://yq.aliyun.com/articles/62834?utm_campaign=wenzhang&utm_medium=article&utm_source=QQ-qun&utm_content=m_7363
https://blog.youkuaiyun.com/haoyifen/article/details/54692503
http://luojinping.com/2017/11/12/解决-Kafka-Consumer-卡顿的问题/
https://stackoverflow.com/questions/37363119/kafka-producer-org-apache-kafka-common-serialization-stringserializer-could-no

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值