消息队列以及Kafka

为何使用消息队列

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

  2. 顺序保证
    在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处
    理。Kafka保证一个Partition内的消息的有序性。

  3. 缓冲、流量削锋
    流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛
    应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。
    可以控制活动的人数,缓解短时间内高流量压垮应用

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

  5. 日志处理
    日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。

Kafka 体系架构

在这里插入图片描述
一个典型的Kafka体系架构包括若干Producer(可以是服务器日志,业务数据,页面前端产生的page view等等),若干broker(Kafka支持水平扩展,一般broker数量越多,集群吞吐率越高),若干Consumer (Group),以及一个Zookeeper集群。Kafka 通过Zookeeper管理集群配置,选举leader,以及在consumer group发生变化时进行rebalance。Producer使用push(推)模式将消息发布到broker,Consumer使用pull(拉)模式从broker订阅并消费消息。

  • Broker:Kafka 集群包含一个或多个服务器,这种服务器被称为 broker。
  • Producer:负责发布消息到 Kafka broker。
  • Consumer:消息消费者,向 Kafka broker 读取消息的客户端。
  • Consumer Group :每个 Consumer 属于一个特定的 Consumer Group,每个Group指定一个Topic。
  • Topic :标识一个消息的类别。
  • Partition(leader、follower) :每个 Topic 包含一个或多个 Partition。
  • Replication : partition的副本文件。
  • Segment :每个partition被分成多个Segment文件。
  • Offset : partition中的每个消息都有一个连续的序列号,用于partition唯一标识一条消息。
  • Leader:partition的副本中,只有一个会被选举成leader作为读写用。

Kafka 工作原理

  • 一个topic可以认为一类消息,每个topic将被分成多个partition,每个partition在存储层面是append log文件。任何发布到此partition的消息都会被追加到log文件的尾部,每条消息在文件中的位置称为offset(偏移量),offset为一个long型的数字,它唯一标记一条消息。每条消息都被append到partition中,是顺序写磁盘,因此效率非常高(经验证,顺序写磁盘效率比随机写内存还要高,这是Kafka高吞吐率的一个很重要的保证)。
  • Topic只是一个逻辑的概念。每个Topic都包含一个或多个Partition,不同Partition可位于不同节点。同时Partition在物理上对应一个本地文件夹,每个Partition包含一个或多个Segment,每个Segment包含一个数据文件和一个与之对应的索引文件。在逻辑上,可以把一个Partition当作一个非常长的数组,可通过这个“数组”的索引(offset)去访问其数据。

复制原理和同步方式

  • 为了提高消息的可靠性,Kafka每个topic的partition有N个副本(replicas),其中N(大于等于1)是topic的复制因子(replica fator)的个数。Kafka通过多副本机制实现故障自动转移,当Kafka集群中一个broker失效情况下仍然保证服务可用。
    在Kafka中发生复制时确保partition的日志能有序地写到其他节点上,N个replicas中,其中一个replica为leader,其他都为
    follower, leader处理partition的所有读写请求,与此同时,follower会被动定期地去复制leader上的数据。
  • Kafka提供了数据复制算法保证,如果leader发生故障或挂掉,一个新leader被选举并被接受客户端的消息成功写入。Kafka确保从同步副本列表中选举一个副本为leader,或者说follower追赶leader数据。leader负责维护和跟踪ISR(In-Sync Replicas的缩写,表示副本同步队列,具体可参考下节)中所有follower滞后的状态。
    当producer发送一条消息到broker后,leader 写入消息并复制到所有follower。消息提交之后才被成功复制到所有的同步副本。消息复制延迟受最慢的follower限制,重要的是快速检测慢副本,如果follower“落后”太多或者失效,leader将会把它从ISR中删除。

Kafka消息传输保障

三种传输保障
At most once 消息可能会丢,但绝不会重复传输
At least one 消息绝不会丢,但可能会重复传输
Exactly once 每条消息肯定会被传输一次且仅传输一次。

  • 当producer向broker发送消息时,Kafka实现的是 at least once
    当producer向broker发送消息时,一旦这条消息被commit,由于副本机制(replication)的存在,它就不会丢失。但是如果producer发送数据给broker后,遇到的网络问题而造成通信中断,那producer就无法
    判断该条消息是否已经提交(commit)。虽然Kafka无法确定网络故障期间发生了什么,但是producer可以retry多次,确保消息已经正确传输到broker中

  • 当consumer向broker获取消息,有几种可能:

    1. Consumer从broker中读取消息后,可以选择commit,该操作会在Zookeeper中存下该consumer在该partition下读取的消息的offset。该consumer下一次再读该partition时会从下一条开始读取。如未commit,下一次读取的开始位置会跟上一次commit之后的开始位置相同。当然也可以将consumer设置为autocommit,即consumer一旦读取到数据立即自动commit。
    2. 如果只讨论这一读取消息的过程,那Kafka是确保了exactly once, 但是如果由于前面producer与broker之间的某种原因导致消息的重复,那么这里就是at least once。
    3. 考虑这样一种情况,当consumer读完消息之后先commit再处理消息,在这种模式下,如果consumer在commit后还没来得及处理消息就crash了,下次重新开始工作后就无法读到刚刚已提交而未处理的消息,这就对应于at most once了。
    4. 读完消息先处理再commit。这种模式下,如果处理完了消息在commit之前consumer crash了,下次重新开始工作时还会处理
      刚刚未commit的消息,实际上该消息已经被处理过了,这就对应于at least once。
要做到exactly once就需要引入 消息去重机制 。

Kafka在producer端和consumer端都会出现消息的重复,这就需要去重处理。

不只是Kafka, 类似RabbitMQ以及RocketMQ这类商业级中间件也只保障at least once, 且也无法从自身去进行消息去重。
所以我们建议业务方根据自身的业务特点进行去重,比如业务消息本身具备幂等性,或者借助Redis等其他产品进行去重处理。

Kafka 搭建

1. 单机模式
  1. 下载Kafka:wget http://kafka.apache.org/downloads.html
  2. 解压:tar -zxvf kafka-XXX.tar.gz
  3. 修改配置 config/server.properties
    listeners=PLAINTEXT://IP:9092
    zookeeper.connect= IP:2181
  • 启动Zookeeper:
    bin/zookeeper-server-start.sh config/zookeeper.properties
    nohup ./bin/zookeeper-server-start.sh config/zookeeper.properties > logs/zookeeper.log 2>&1 &

  • 启动Kafka:
    bin/kafka-server-start.sh config/server.properties
    nohup bin/kafka-server-start.sh config/server.properties > logs/kafka-server.log 2>&1 &

  • 测试:
    1)新建一个话题Topic
    bin/kafka-topics.sh --create --zookeeper IP:2181 --replication-factor 1 --partitions 1 --topic test
    2)启动producer并发布消息:
    bin/kafka-console-producer.sh --broker-list IP:9092 --topic test
    3)启动consumer并消费消息:
    bin/kafka-console-consumer.sh --zookeeper IP:2181 --topic test --from-beginning

2. Kafka搭建环境-集群模式

搭建Zookeeper集群环境 (三台机器做相同的事情)

  1. 下载Zookeeper:wget http://zookeeper.apache.org/releases.html
  2. 解压:tar -zxvf zookeeper-XXX.tar.gz
  3. 分别进入到conf文件夹,将conf/zoo_sample.cfg拷贝一份命名为zoo.cfg
  4. 修改zoo.cfg,在文件末尾追加:
    server.1=[your cluster host 1]:3888:4888
    server.2=[your cluster host 2]:3888:4888
    server.3=[your cluster host 3]:3888:4888
    dataDir=/data/kafka/zookeeper
    创建ServerID标识:
    节点server1:echo “1” > /data/kafka/zookeeper/myid
    节点server2:echo “2” > /data/kafka/zookeeper/myid
    节点server3:echo “3” > /data/kafka/zookeeper/myid
    注:这里设置的myid取值需要和zookeeper.properties中“server.id”保持一致。
  • 搭建Kafka集群(三台机器做相同的事情)
    1)下载&解压(略过)
    2)修改Kafka配置文件,config/server.properties
    broker.id(集群内必须唯一)
    listeners=PLAINTEXT://IP:9092
    zookeeper.connect=192.168.88.31:2181,192.168.88.32:2181,192.168.88.33:2181
  • 启动Zookeeper集群环境,进入zookeeper安装目录下 (三台机器做相同的事情)
    nohup ./bin/zkServer.sh start > zookeeper.log 2>&1 &
  • 启动Kafka集群(三台机器做相同的事情)
    nohup bin/kafka-server-start.sh config/server.properties > logs/kafka-server.log 2>&1 &
  • 测试
    按照单机版本测试,创建Topic,启动生产者、消费者。

代码示例

生产者

import java.util.Properties;
import java.util.Random;
import java.util.concurrent.ExecutionException;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;

public class Producer {
	private final KafkaProducer<String, String> producer;
    private final String topic;
    
    public Producer(String topic) {
		// TODO Auto-generated constructor stub
    	Properties props = new Properties();
        props.put("bootstrap.servers", "172.x.x.x:9092,172.x.x.x:9092,172.x.x.x:9092");
        props.put("client.id", "DemoProducer");
        props.put("batch.size", 16384); //16M
        props.put("linger.ms", 1000);
        props.put("buffer.memory", 33554432); //32M
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("partitioner.class", "com.neu.homework.SimplePartitioner");
        
        producer = new KafkaProducer<>(props);
        this.topic = topic;
    }
    public void produce() throws InterruptedException, ExecutionException{
    	Random rnd = new Random();
    	int events = 200;
    	for (Integer nEvents = 0; nEvents < events; nEvents++) {
	    	String k = "Key" + nEvents;   	
	    	String val = Integer.toString(rnd.nextInt(20));
	        producer.send(new ProducerRecord<>(topic, k, val));
			System.out.println("Sent message: (" + k + ", " + val + ")");
	        
    	}
    	Thread.sleep(10000);
    }
    
    public static void main(String[] args) throws InterruptedException {
    	Producer producer = new Producer(Constants.TOPIC);
    	try {
			producer.produce();
		} catch (ExecutionException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
 

消费者

import java.util.Collections;
import java.util.Properties;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;

public class Consumer extends Thread{
	private final KafkaConsumer<String, String> consumer;
    private final String topic;
    private final Integer id;

    public Consumer(String topic,Integer id) {
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "172.x.x.x:9092,172.x.x.x:9092,172.x.x.x:9092");
        props.put("zookeeper.connect", "172.x.x.x:2181,172.x.x.x:2181,172.x.x.x:2181");
        props.put(ConsumerConfig.GROUP_ID_CONFIG, Constants.GROUP);
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
        props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");//latest,earliest
        props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "30000");
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        
        consumer = new KafkaConsumer<>(props);
        this.topic = topic;
        this.id = id;
    }
    
    public void run(){
    	try {
			consumer.subscribe(Collections.singletonList(this.topic));
			while(true){
				ConsumerRecords<String, String> records = consumer.poll(100);
				for (ConsumerRecord<String, String> record : records) {
				    System.out.println("Consumer" + id.toString() + " Received message: (" + record.key() + ", " + record.value() + ") at partition "+record.partition()+" offset " + record.offset());
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
    }
    public static void main(String[] args) {
    	Consumer Consumer1 = new Consumer(Constants.TOPIC,1);
    	Consumer Consumer2 = new Consumer(Constants.TOPIC,2);
    	Consumer1.start();
    	Consumer2.start();
	}
}

自定义分区

import java.util.Map;

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;

public class SimplePartitioner implements Partitioner {

    public SimplePartitioner() {
    }

    @Override
    public void configure(Map<String, ?> configs) {
    }

	// 自定义分区函数
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
    	int partition = 0;
		if ((Integer.parseInt(value.toString()) % 2) == 1) {
			partition = 1;
		}else {
            partition = 0;
		}
		return partition;
    }

    @Override
    public void close() {
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值