https://zhuanlan.zhihu.com/p/79579389
https://www.jianshu.com/p/a036405f989c
https://blog.csdn.net/zhaoyachao123/article/details/89527233
作者:扎心了,老铁
https://www.cnblogs.com/qingyunzong/category/1212387.html
本文是本人在之前的实际开发中使用kafka中的总结以及对应的代码,相关参考的优秀的博客文档已经在开头参考中贴出,可以直接移步学习
Kafka是一个分布式,支持分区(partition),多副本(replica),基于zookeeper协调的高性能的分布式消息系统。 这句话基本上是涵盖了kafka全部的特性特征
附一张kakfa的架构图(该架构图片来源于:51CTO博客)
在一套完备的kafka架构中,是存在着多个Producer,多个Broker,多个Consumer,每个Producer可以对应多个Topic,每个Consumer只能对应一个ConsumerGroup。Producer使用push模式将消息发布到broker,Consumer使用pull模式从broker订阅并消费消息
kafka需要依赖于zookeeper进行集群节点的管理,以及在consumer group 发生变化时进行rebalance。
kafka对应zookeeper的数据存储结构如下:
上面的kafka01并不是zookeeper的节点。可以通过zookeeper的get / 进行查看
一个Broker是一个kafka的部署节点,Broker是分布式部署并且相互依赖,每个Broker在启动时,都会到Zookeeper上进行注册,即到/brokers/ids下创建属于自己的节点
如/brokers/ids/[0…N]。
该节点存储着当前注册broker的基本信息:
kafka使用了全局唯一的数字来指代每个Broker服务器,不同的Broker必须使用不同的Broker Id进行注册,其中,Broker创建的节点类型是临时节点,一旦Broker宕机,则对应的临时节点也会被自动删除。
{
"listener_security_protocol_map":{
"PLAINTEXT":"PLAINTEXT"
},
"endpoints":[
"PLAINTEXT://192.168.129.128:9192"
],
"jmx_port":-1, //jmx端口号
"host":"192.168.129.128", //主机ip地址
"timestamp":"1571828014387", //kafka broker初始启动时的时间戳,
"port":9192, //kafka broker的服务端端口号,由server.properties中参数port确定
"version":4 //版本号
}
在kafka中,同一个Topic的消息会被分成多个分区并将其分布在多个Broker上并将其分配在多个Broker上,这些分区信息及与Broker的对应关系也都是由Zookeeper在维护,由专门的节点来记录,如:
/borkers/topics
Kafka中每个Topic都会以/brokers/topics/[topic]的形式被记录,如/brokers/topics/login和/brokers/topics/search等。Broker服务器启动后,会到对应Topic节点(/brokers/topics)上注册自己的Broker ID并写入针对该Topic的分区总数,如/brokers/topics/login/3->2,这个节点表示Broker ID为3的一个Broker服务器,对于"login"这个Topic的消息,提供了2个分区进行消息存储,同样,这个分区节点也是临时节点。
使用**get /brokers/topics/[topic]**查看数据
{
"version":1, //版本编号,目前固定为1
"partitions":{
//partitionId编号
"0":[
0 //同步副本组brokerId列表
],
"1":[
0
],
"2":[
0
],
"3":[
0
]
}
}
使用 get /broker/topics/[topic]/partition/0/state 查看partition状态
{
"controller_epoch":7, //表示kafka集群中的中央控制器选举次数
"leader":0, //表示该partition选举lead的brokerId,
"version":1, //版本编号,固定1
"leader_epoch":6, //该partition lead选举次数
"isr":[
0 //同步副本组brokerId列表
]
}
使用 get /controller_epoll
此值为一个数字,kafka集群中第一个broker第一次启动时为1,以后只要集群中center controller中央控制器所在broker变更或挂掉,就会重新选举新的center controller,每次center controller变更controller_epoch值就会 + 1;
使用 get /controller 查看center controller中央控制器所在kafka broker的信息
{
"version":1, //版本编号默认为1,
"brokerid":0, //kafka集群中broker唯一编号,
"timestamp":"1571828013306" //kafka broker中央控制器变更时的时间戳
}
由于同一个Topic消息会被分区并将其分布在多个Broker上,因此,生产者需要将消息合理地发送到这些分布式的Broker上,那么如何实现生产者的负载均衡,Kafka支持传统的四层负载均衡,也支持Zookeeper方式实现负载均衡。
与生产者类似,Kafka中的消费者同样需要进行负载均衡来实现多个消费者合理地从对应的Broker服务器上接收消息,每个消费者分组包含若干消费者,每条消息都只会发送给分组中的一个消费者,不同的消费者分组消费自己特定的Topic下面的消息,互不干扰。
消费组 (Consumer Group):
consumer group 下有多个Consumer(消费者)
对于每个消费组(consumer group),kafka都会为其分配一个唯一的全局的Group ID,Group 内部的所有消费者共享该 ID,订阅的topic下的每个分区只能分配给某个 group 下的一个consumer(当然该分区还可以被分配给其他group)。
同时,Kafka为每个消费者分配一个Consumer ID,通常采用"Hostname:UUID"形式表示。
在kafka中,规定每个消费分区(partition)只能被同组的一个消费者进行消费, ,因此,需要在 Zookeeper 上记录 消息分区 与 Consumer 之间的关系,每个消费者一旦确定了对一个消息分区的消费权力,需要将其Consumer ID 写入到 Zookeeper 对应消息分区的临时节点上,例如:
/consumers/[group_id]/owners/[topic]/[broker_id-partition_id]
其中,[broker_id-partition_id]就是一个 消息分区 的标识,节点内容就是该 消息分区 上 消费者的Consumer ID。
在消费者对指定消息分区进行消息消费的过程中,需要定时地将分区消息的消费进度Offset记录到Zookeeper上,以便在该消费者进行重启或者其他消费者重新接管该消息分区的消息消费后,能够从之前的进度开始继续进行消息消费。Offset在Zookeeper中由一个专门节点进行记录,其节点路径为
/consumers/[group_id]/offsets/[topic]/[broker_id-partition_id]
节点内容就是Offset的值。
消费者服务器在初始化启动时加入消费者分组的步骤如下
注册到消费者分组。每个消费者服务器启动时,都会到Zookeeper的指定节点下创建一个属于自己的消费者节点,例如/consumers/[group_id]/ids/[consumer_id],完成节点创建后,消费者就会将自己订阅的Topic信息写入该临时节点。
对 消费者分组 中的 消费者 的变化注册监听。每个 消费者 都需要关注所属 消费者分组 中其他消费者服务器的变化情况,即对/consumers/[group_id]/ids节点注册子节点变化的Watcher监听,一旦发现消费者新增或减少,就触发消费者的负载均衡。
对Broker服务器变化注册监听。消费者需要对/broker/ids/[0-N]中的节点进行监听,如果发现Broker服务器列表发生变化,那么就根据具体情况来决定是否需要进行消费者负载均衡。
进行消费者负载均衡。为了让同一个Topic下不同分区的消息尽量均衡地被多个 消费者 消费而进行 消费者 与 消息 分区分配的过程,通常,对于一个消费者分组,如果组内的消费者服务器发生变更或Broker服务器发生变更,会发出消费者负载均衡。
Kafka的ack机制,指的是producer的消息发送确认机制,这直接影响到Kafka集群的吞吐量和消息可靠性。而吞吐量和可靠性就像硬币的两面,两者不可兼得,只能平衡。
request.required.acks有三个只0,1,-1(all),
常见的数据丢失的解决方案:
1,消费端的数据丢失:
2,kafka本身弄丢了数据:
丢失场景:这块比较常见的一个场景,就是kafka某个broker宕机,然后重新选举partiton的leader时。要是此时其他的follower刚好还有些数据没有同步,结果此时leader挂了,然后选举某个follower成leader之后,他不就少了一些数据?这就丢了一些数据。
3,生产端的数据丢失问题,
设置ack为all
问题如下:kafka为了保证数据的一致性使用了ISR机制,isr 的全称是:In-Sync Replicas isr 是一个副本的列表,里面存储的都是能跟leader 数据一致的副本,确定一个副本在isr列表中,有2个判断条件
kafka的指定topic是分为多个partition,每一个partition是分为多个Segment。以创建topic:zhoucg_topic为例:
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 2 --partitions 4 --topic zhoucg_topic
其中,对应的partition的个数为4。
kafka的数据存储是位于kafka目录下的log目录中
每一个partition为一个目录。partition命令的规则是topic的名称加上一个序号,序号从0开始,每一个partition目录下的文件被平均切割成大小相等的数据文件(默认是500m,可以自行调整)
其中,每一个数据文件都被称为一个段(segment file)但每个段消息数量不一定相等,这种特性能够使得老的segment可以被快速清除。默认保留7天的数据。
这里我们测试模拟向topic插入大量数据
Segment File组成:由3大部分组成。分别为index file和data file,此2个文件一一相应,成对出现,后缀”.index”和“.log”分别表示为segment索引文件、数据文件,以.timeindex为后缀的表示对应kafka的具体时间日志
索引文件.index和数据文件.log的对应关系:
上图的左半部分是索引文件,里面存储的是一对一对的key-value,其中key是消息在数据文件(对应的log文件)中的编号,比如“1,3,6,8……”,分别表示在log文件中的第1条消息,第3条消息,第6条消息、第8条消息……,那么为什么在index文件中这些编号不是连续的呢?
这是因为index文件并没有为数据文件中的每条消息建立索引,而是采用了稀疏存储的方式,每隔一定字节的数据建立一条索引。这样避免了索引文件占用过多的空间,从而可以将索引文件保留在内存中。
但缺点是没有建立索引的Message也不能一次定位到其在数据文件的位置,从而需要做一次顺序扫描,但是这次顺序扫描的范围就很小了。
其中以索引文件中元数据3,497为例,其中3代表在右边log数据文件中从上到下第3个消息(在全局partiton表示第368772个消息),
其中497表示该消息的物理偏移地址(位置)为497。
Produer发送消息时,会根据Partition机制选择将其存储到哪一个Partition,如果Partition机制设置合理,所有消息可以均匀分布到不同的Partition里,这样就实现了负载均衡。
在发送一条消息时,可以指定这条消息的key,Producer根据这个key和Partition机制来判断应该将这条消息发送到哪个Partition,Paritition机制可以通过指定Producer的paritition. class这一参数来指定,该class必须实现kafka.producer.Partitioner接口。
例如:
1,首先设置生产者对应的partitioner.class参数
//此处对应的是kafka定义分区的类
config.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.common.kafka.Partitions");
2,然后创建一个Partitions类,该类需要继承自org.apache.kafka.clients.producer.Partitioner
package com.common.kafka;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.Node;
import java.util.List;
import java.util.Map;
/**
* kafka生产者消息设置自定义分区
* @author: zhoucg
* @date: 2019-10-22
*/
public class Partitions implements Partitioner{
/**
*
* @param topic 指定topic数据
* @param key 当前key
* @param keyBytes 当前key的字节数组
* @param value 当前value
* @param valueBytes 当前value的字节数组
* @param cluster 当前集群信息
* @return 返回的值对应当前消息的partition
*/
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
/**
* 可以根据对应的集群环境和对应的value数据进行负载指定message对应的partition数据
*/
List nodes = cluster.nodes();
return 0;
}
@Override
public void close() {
}
@Override
public void configure(Map configs) {
}
}
其中Cluster类的相关数据:
在partition()方法中,我们可以自行设计相应的算法规则,将对应的message对应到指定的partition中
为了更好的做负载均衡,kafka尽量将所有的partition均匀分配到整个集群中,一个典型的部署方式是一个Topic的Partition数量大于broker的数量,同时为了提高kafka的容错能力,也需要将同一个partition的Replica尽量分散到不同的机器,实际上,如果所有的Replica都在同一个Broker上,那一旦该Broker宕机,该Partition的所有Replica都无法工作。
Kafka分配Replica的算法如下:
参照:Kafka学习之路 (三)Kafka的高可用#四、producer发布消息
参考:5 kafka的数据存储结构
kafka和其他消息中间件的优势
1,查看kafka topic列表,使用 --list
bin/kafka-topics.sh --zookeeper 127.0.0.1:2181 --list
2,查看kafka特定topic的详情,使用–topic与–describe参数
bin/kafka-topics.sh --zookeeper 127.0.0.1:2181 --topic lx_test_topic --describe
Topic:lx_test_topic PartitionCount:1 ReplicationFactor:1 Configs:
Topic: lx_test_topic Partition: 0 Leader: 0 Replicas: 0 Isr: 0
列出了lx_test_topic的parition数量、replica因子以及每个partition的leader、replica信息
3,查看指定topic中的实时推送过来的数据
./bin/kafka-console-consumer.sh --bootstrap-server localhost:9192 --topic RT.DPC.STATION.DI
4,创建指定topic
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 2 --partitions 4 --topic test
replication-factor:表示副本个数
partitions:表示分区个数
5,创建生产者
bin/kafka-console-producer.sh --broker-list localhost:9192 --topic test --producer.config config/producer.properties
producer.properties:表示对应生产者的相关参数配置
6,创建消费者
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --new-consumer --from-beginning --consumer.config config/consumer.properties
from-beginning:表示从对应的分区的offset0开始消费数据
consumer.properties:消费者对应的相关参数配置
7,topic分区扩容
./kafka-topics.sh --zookeeper localhost:2181 -alter --partitions 4 --topic zhoucg_topic
8,topic副本修改
1,根据topic的分区的分区情况修改partitions-topic.json信息
{
"partitions":
[
{
"topic": "test1",
"partition": 0,
"replicas": [1,2]
},
{
"topic": "test1",
"partition": 1,
"replicas": [0,3]
},
{
"topic": "test1",
"partition": 2,
"replicas": [4,5]
}
],
"version":1
}
2,执行副本迁移
../bin/kafka-reassign-partitions.sh --zookeeper 127.0.0.1:2181 --reassignment-json-file partitions-topic.json --execute
3,查看迁移情况
../bin/kafka-reassign-partitions.sh --zookeeper 127.0.0.1:2181 --reassignment-json-file partitions-topic.json --verify
Status of partition reassignment:
Reassignment of partition [mx_prd_nginx_access,0] is still in progress
Reassignment of partition [mx_prd_nginx_access,1] completed successfully
Reassignment of partition [mx_prd_nginx_access,2] is still in progress
简单的生产者消费者类
import kafka.consumer.Consumer;
import kafka.consumer.ConsumerConfig;
import kafka.consumer.ConsumerIterator;
import kafka.consumer.KafkaStream;
import kafka.javaapi.consumer.ConsumerConnector;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* Created by zhoucg on 2018-09-25.
*/
public class KafkaUtil {
private static KafkaProducer producer = null;
private static ConsumerConnector consumer = null;
static {
Map config = new HashMap<>();
//kafka服务器地址
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.129.128:9192");
//kafka消息序列化类 即将传入对象序列化为字节数组
config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.ByteArraySerializer");
//kafka消息key序列化类 若传入key的值,则根据该key的值进行hash散列计算出在哪个partition上
config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
//当多条消息发送到同一个partition时,该值控制生产者批量发送消息的大小,批量发送可以减少生产者到服务端的请求数,有助于提高客户端和服务端的性能。
config.put(ProducerConfig.BATCH_SIZE_CONFIG,1024*1024*5);
//往kafka服务器提交消息间隔时间,0则立即提交不等待
config.put(ProducerConfig.LINGER_MS_CONFIG,0);
//kakfa的ack模式 0,1,-1(all)
//config.put(ProducerConfig.ACKS_CONFIG,"all");
//生产者发送失败后,重试的次数
config.put(ProducerConfig.RETRIES_CONFIG,1);
//消费者配置文件
Properties properties = new Properties();
properties.put("zookeeper.connect","192.168.129.128:2181");
properties.put("group.id","123");
properties.put("auto.commit.interval.ms","1000");
ConsumerConfig consumerConfig = new ConsumerConfig(properties);
producer = new KafkaProducer<>(config);
consumer = Consumer.createJavaConsumerConnector(consumerConfig);
}
/**
*启动一个消费程序
* @param topic 要消费的topic名称
* @param handler 自己的处理逻辑的实现
* @param threadCount 消费线程数,该值应小于等于partition个数,多了也没用
*/
public static void startConsumer(String topic, final MqMessageHandler handler, int threadCount) throws Exception{
if(threadCount<1)
throw new Exception("处理消息线程数最少为1");
//设置处理消息线程数,线程数应小于等于partition数量,若线程数大于partition数量,则多余的线程则闲置,不会进行工作
//key:topic名称 value:线程数
Map topicCountMap = new HashMap();
topicCountMap.put(topic, new Integer(threadCount));
Map>> consumerMap = consumer.createMessageStreams(topicCountMap);
//Map>> consumerMap = consumer.createMessageStreams(topicCountMap);
//声明一个线程池,用于消费各个partition
ExecutorService executor= Executors.newFixedThreadPool(threadCount);
//获取对应topic的消息队列
List> streams = consumerMap.get(topic);
//为每一个partition分配一个线程去消费
for (final KafkaStream stream : streams) {
executor.execute(new Runnable() {
@Override
public void run() {
ConsumerIterator it = stream.iterator();
//有信息则消费,无信息将会阻塞
while (it.hasNext()){
T message=null;
try {
//将字节码反序列化成相应的对象
byte[] bytes=it.next().message();
message = (T) SerializationUtils.deserialize(bytes);
} catch (Exception e) {
e.printStackTrace();
return;
}
//调用自己的业务逻辑
try {
handler.handle(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
}
/**
*发送消息,发送的对象必须是可序列化的
*/
public static Future send(String topic, Serializable value) throws Exception{
try {
//将对象序列化称字节码
byte[] bytes=SerializationUtils.serialize(value);
Future future=producer.send(new ProducerRecord(topic,bytes));
RecordMetadata recordMetadata = future.get();
return future;
}catch(Exception e){
throw e;
}
}
//内部抽象类 用于实现自己的处理逻辑
public static abstract class MqMessageHandler{
public abstract void handle(T message);
}
public static void main(String[] args) throws Exception {
//发送一个信息
send("zhoucg_topic",new User("id","userName", "password"));
//为test启动一个消费者,启动后每次有消息则打印对象信息
/*KafkaUtil.startConsumer("test", new MqMessageHandler() {
@Override
public void handle(User user) {
//实现自己的处理逻辑,这里只打印出消息
System.out.println(user.toString());
}
},2);*/
}
static class User implements Serializable{
private static final long serialVersionUID = 8576358642877640767L;
private People people;
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public People getPeople() {
return people;
}
public void setPeople(People people) {
this.people = people;
}
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", userName='" + userName + '\'' +
", password='" + password + '\'' +
'}';
}
private String id;
private String userName;
private String password;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public User(){}
public User(String id,String userName,String password){
this.id = id;
this.userName = userName;
this.password = password;
}
}
static class People {
private String name;
private String age ;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
}
}
kafka生产者创建:
基本配置信息:
//kafka服务器地址
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.129.128:9192");
//kafka消息序列化类 即将传入对象序列化为字节数组
config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.ByteArraySerializer");
//kafka消息key序列化类 若传入key的值,则根据该key的值进行hash散列计算出在哪个partition上
config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
//当多条消息发送到同一个partition时,该值控制生产者批量发送消息的大小,批量发送可以减少生产者到服务端的请求数,有助于提高客户端和服务端的性能。
config.put(ProducerConfig.BATCH_SIZE_CONFIG,1024*1024*5);
//往kafka服务器提交消息间隔时间,0则立即提交不等待
config.put(ProducerConfig.LINGER_MS_CONFIG,0);
//kakfa的ack模式 0,1,-1(all)
//config.put(ProducerConfig.ACKS_CONFIG,"all");
//生产者发送失败后,重试的次数
config.put(ProducerConfig.RETRIES_CONFIG,1);
//创建生产者:
producer = new KafkaProducer<>(config);
kafka消费者创建:
//消费者配置文件
Properties properties = new Properties();
properties.put("zookeeper.connect","192.168.129.128:2181");
properties.put("group.id","123");
properties.put("auto.commit.interval.ms","1000");
ConsumerConfig consumerConfig = new ConsumerConfig(properties);
consumer = Consumer.createJavaConsumerConnector(consumerConfig);
kafka生产者发送消息:
/**
*发送消息,发送的对象必须是可序列化的
*/
public static Future send(String topic, Serializable value) throws Exception{
try {
//将对象序列化称字节码
byte[] bytes=SerializationUtils.serialize(value);
Future future=producer.send(new ProducerRecord(topic,bytes));
RecordMetadata recordMetadata = future.get();
return future;
}catch(Exception e){
throw e;
}
}
其中,Future返回的RecordMetadata对象能够获取对应message的offset,以及写入的topic的partition索引
kafka消费者消费消息:
/**
*启动一个消费程序
* @param topic 要消费的topic名称
* @param handler 自己的处理逻辑的实现
* @param threadCount 消费线程数,该值应小于等于partition个数,多了也没用
*/
public static void startConsumer(String topic, final MqMessageHandler handler, int threadCount) throws Exception{
if(threadCount<1)
throw new Exception("处理消息线程数最少为1");
//设置处理消息线程数,线程数应小于等于partition数量,若线程数大于partition数量,则多余的线程则闲置,不会进行工作
//key:topic名称 value:线程数
Map topicCountMap = new HashMap();
topicCountMap.put(topic, new Integer(threadCount));
Map>> consumerMap = consumer.createMessageStreams(topicCountMap);
//Map>> consumerMap = consumer.createMessageStreams(topicCountMap);
//声明一个线程池,用于消费各个partition
ExecutorService executor= Executors.newFixedThreadPool(threadCount);
//获取对应topic的消息队列
List> streams = consumerMap.get(topic);
//为每一个partition分配一个线程去消费
for (final KafkaStream stream : streams) {
executor.execute(new Runnable() {
@Override
public void run() {
ConsumerIterator it = stream.iterator();
//有信息则消费,无信息将会阻塞
while (it.hasNext()){
T message=null;
try {
//将字节码反序列化成相应的对象
byte[] bytes=it.next().message();
message = (T) SerializationUtils.deserialize(bytes);
} catch (Exception e) {
e.printStackTrace();
return;
}
//调用自己的业务逻辑
try {
handler.handle(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
}