基础
Kafka是基于Scala开发的多分区、多副本基于ZooKeeper协调
的分布式消息引擎系统。和传统的消息队列/消息中间件不同,如下图,kafka一般当作集群用,支持如下功能:
- 消息系统:类似传统消息队列(中间件)的功能,常用于系统解耦和异步处理,相比前辈它提供了消息顺序性和回溯消费等功能。
- 存储系统:Kafka默认将消息顺序保存到磁盘上,其提供了多副本机制,非常适合做长期数据存储系统。
- 流处理系统:Kafka 0.10版本开始提供流处理功能,适合实时处理场景。
如下图,kafka体系结构包含如下三部分:
- 生产者(Producer),向集群写入消息
- 消费者(Consumer),消费集群消息,处理业务逻辑
- 服务代理节点(broker),一般一台服务器部署一个或多个broker
kafka中消息按照主题(Topic)
和分区(Partition)
来划分,一般每种类型或每个业务逻辑消息划分到一个Topic,生产者按照Topic写入,消费者按照Topic订阅消费。
Topic是逻辑上的概念,对应具体存储,一般一个Topic分为多个Partition存储,这样可以支持很好的伸缩而且提高并发写入。为了保证消息不丢失,Kafka引入常见的副本机制(Replication),每个分区有多个副本(Replica)
。
副本之间是一主多从的关系,leader分区
负责对外读写,follower分区
负责异步同步follower消息,follower和leader消息存在一定延时,但最终会一致。当leader分区损坏时,会自动从follower分区选举新的leader分区保证高可用。
结合前面的broker来说,每个broker包含多个Partition,一个Topic具体的分区和副本是尽量均布在不同的broker上的,示意如下。
具体的分区磁盘存储类似如下,每个消息写入添加在原有日志文件(Log)
末尾,产生一个对应的offset,称为消息偏移(Offset)
。
实际消费消息时,消费者只需要保存当前消费的位置即可,称为消费者偏移(Offset)
,由于消息固化在磁盘上,不同的消费者可分别重复消费,每个消费者维持自己的offset,如下:
具体每个Log日志的结构如下,实际中follower副本和leader副本存在滞后,与leader副本存在一定程度的滞后(包括leader副本)组成ISR(In-Sync Replicas)
。与leader副本滞后太冤的称为OSR(Out-of-Sync Replicas)
,全量副本(Assigned Replicas)
=ISR+OSR,只有ISR中副本在leader失效时有资格选为leader。
LEO(Log End Offset)
为当前分区消息写入的最后偏移,HW(High Watermark)
俗称高水位,消费者只能消费HW偏移之前的消息,ISR集合中最小的LEO即为HW。
具体到Topic的消费时,因为每个Topic非为多个分区Partition,因此消费时可以指定一个消费组(Consumer Group)
,消费组包含多个消费者实例(Consumer Instance)
,每个消费者对应一个分区或多个Partition的消费,这样可以实现Topic的并行处理,如下。当一个消费者死掉的时候,会自动重平横,选用其他消费者来消费Topic数据,也就是常说的重平横(Reblance)
。
安装
环境准备
虚拟机环境配置参考
Java环境配置参考
Zookeeper环境配置参考
kafka包下载位置:https://www.apache.org/dyn/closer.cgi?path=/kafka/2.2.0/kafka_2.11-2.2.0.tgz
自动选择最近镜像,进入后下载对应的bin.tar.gz后缀文件,上传虚拟机~/app目录,tar -zxvf xxx 解压,当前使用版本为kafka_2.11-2.2.0.tgz
参数配置
暂时只需要配置如下参数即可运行集群,优化参数后续专门来讲,在config/server.properties中修改如下参数:
broker的编号,多个broker设置不同的编号,这里我们依次设置id为0、1、2。
如果没有设置,那么 Kafka会自动生成一个,和broker.id.geηeration. enable 和 reserved.broker.max.id有关。
这个参数关联如下两个参数:
* listeners
broker监听客户端连接的地址列表,配置格式为 protocoll : //hostnamel:portl, protocol2://hostname2:port2 ,多个地址则中间以逗号隔开。其中 protocol 代表协议类型, Kafka 当前支持的协议类型有PLAINTEXT、SSL、SASL_SSL 等,如果未开启安全认证,则使用简单的 PLAINTEXT 即可。hostname代表主机名, port此代表服务端口。如果不指定主机名,则表示绑定默认网卡localhost,这样无法对外提供服务,所以主机名最好不要为 空; 如果主机名是 0.0.0.0, 则表示绑定所有的网卡。
* advertised.listeners
和listeners功能类似,默认为空,主要用于IaaS (Infrastructure as a Service)环境,比如公 有云上的机器通常配备有多块网卡,即包含私网网卡和公网网卡,对于这种情况而言,可以设置 advertised.listeners 参数绑定公网IP供外部客户端使用,而配置 listeners 参数来绑定私网IP地址供 broker 间通信使用。
这里默认为空,绑定网卡localhost,无法对外服务,因此我们可以修改
listeners= PLAINTEXT://wenzhou001:9092
更简单的方法是,我们可以直接指定host.name=wenzhou001来对外提供服务
- log.dirs/log.dir
log.dir用来配置单个根目录,而log.dirs 用来配置多个根目录(以逗号分隔),同时配置时log.dirs为准。配置多个目录能够提高并发写入性和实现磁盘故障转移。
提前创建好logs目录,配置如下
log.dirs=/Users/wenzhou/Software/kafka_2.11-2.2.0/logs
- zookeeper.connect
broker 要连接的ZooKeeper集群的服务地址,多个地址使用逗号分割,最佳的实践方式是再加一个 chroot路径,这样既可以明确指明该chroot路径下的节点是为 Kafka 所用的,也可以实现多个 Kafka 集群复用一套 ZooKeeper 集群,切记 chroot 只需要写一次,而且是加到最后的,比如
zookeeper.connect=localhost1:2181, localhost2:2181,localhost3:2181/kafka
这里为了简单,直接使用根路径,配置如下
zookeeper.connect=localhost1:2181, localhost2:2181,localhost3:2181
简单使用
开启kafka前,先开启Zookeeper集群,然后运行如下命令,开启Kafka集群
nohup bin/kafka-server-start.sh config/server.properties &
jps此时可以看到进程名为kafka的进程在运行,停止运行操作类似
命令行操作
kafka提供了一些命令脚本,可以快速常见操作Topic,和可以做常见的压测。如下,常见一个名为副本数为3/分区数为4/名为demo的Topic。
bin/kafka-topics.sh --create --topic demo --replication-factor 3 --partitions 4 --bootstrap-server wenzhou001:9092,wenznhou002:9092
如下命令,查看常见的Topic详情
bin/kafka-topics.sh --describe --topic demo --bootstrap-server wenzhou001:9092,wenzhou002:9092
详情如下,可以看到新建的Topic,有四个分区,分区0/3 leader副本在broker.id=0上,分区1/2 leader副本分别在broker.id=2/1上。由于这里副本和broker数量一致,因此每个broker上都存在对应的副本,因此Isr包含3个broker上分区,第一个为leader分区。
Topic:demo PartitionCount:4 ReplicationFactor:3 Configs:segment.bytes=1073741824
Topic: demo Partition: 0 Leader: 0 Replicas: 0,2,1 Isr: 0,2,1
Topic: demo Partition: 1 Leader: 2 Replicas: 2,1,0 Isr: 2,1,0
Topic: demo Partition: 2 Leader: 1 Replicas: 1,0,2 Isr: 1,0,2
Topic: demo Partition: 3 Leader: 0 Replicas: 0,1,2 Isr: 0,1,2
在一台机器上运行如下命令,打开一个生产者命令行
kafka_2.11-2.2.0]$ bin/kafka-console-producer.sh --topic demo --broker-list wenzhou001:9092,wenzhou002:9092
输入如下
>test1
>test2
>test3
再起一个shell,运行如下命令,监控并输出最新生产的消息,但是已经生产的不会再输出
bin/kafka-console-consumer.sh --topic demo --bootstrap-server wenzhou001:9092,wenzhou002:9092
要想从头开始输出,可添加beginning参数,如下
bin/kafka-console-consumer.sh --topic demo --from-beginning --bootstrap-server wenzhou001:9092,wenzhou002:9092
此时再在生产者输入新的内容,两个shell消费都会输出最新内容。
生成者和消费者编写
这里使用本地集群演示代码生产和消费Topic,如下
生产者
producer的原理是为每个Partition分区维持一个本地缓冲buffer,send的数据先保存在本地buffer
,另有一个后台IO线程监测到达一定数量后再批量发送
,相关参数包括如下
- batch.size
本地缓冲buffer的大小,超出这个消息数,会被批量提交
- buffer.memory
本地所有分区缓冲buffer的内存总上限,超过内存占用,后续写入会阻塞,阻塞超过max.block.ms时间会抛出异常
按时间分片,指定时间内聚合消息批量发送
这三个参数是相互影响的,以最快到达瓶颈的参数来触发限制。
因为发送是异步的,因此producer本身还支持重试和应答控制。acks=all要求所有副本写入完成确认后才算发送成功,这样最慢但是一致性最强。retries强调消息发送失败时自动重试
最大次数。
producer是线程安全
的,推荐多个线程公用一个producer对象,因为他们都是依赖后台线程发送的,没必要使用多个。
key.serializer和value.serializer指定序列化方式,这里默认使用java序列化。
0.11版本引入了kafka 幂等生产者和生产者事务,这个后续kafka特性一节再详解。
public static void Generate()
{
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all");
props.put("retries", 0);
props.put("batch.size", 16384);
props.put("linger.ms", 1);
props.put("buffer.memory", 33554432);
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<>(props);
for (int i = 0; i < 10; i++)
{
try {
producer.send(new ProducerRecord<String, String>("test", Integer.toString(i), "Hello World-"+i));
}catch (Exception e){
e.printStackTrace();
}
}
producer.close();
}
消费者
kafka 消费端API从0.10开始有较大改变,如果是使用之前版本API请参考官方对应版本文档。新版API只需要指定bootstrap.servers即可,不再需要指定Cluster的Zookeeper地址,这里不需要指定全部broker机器,kafka会自动根据指定的bootstrap.servers机器获取全部broker列表
,这里最后指定多台,保证一定的冗余性,防止部分机器宕机的情况影响。
原理部分已经说明,Kafka消费端以消费组为单位消费Topic数据,消费组自动实现平衡,broker增减或Consumer增减,均会自动重平衡,group.id指定消费组ID,group.id相同的当作一个消费组。
实际使用,Consumer订阅指定的Topic,每次调用poll获取指定数量的消息数,每条消息中的offset指下一个要处理的消息位移。指定offset的消息处理完后,需要提交committed position,有两种方法,自动提交和手动提交
,enable.auto.commit指定开启自动提交,auto.commit.interval.ms指明自动提交的间隔。
自动提交
只要poll调用成功,就默认消息处理成功了,此时会自动提交commit position,至于后续消息如何处理存储和是否成功,都不再关心,这样处理简单,但是没法完全保证消息业务
成功处理。
手动提交
允许自己控制何时确认提交committed position,保证消息业务完成后再提交, commitSync和commitAsync支持提交本次poll偏移或指定offset position。手动提交允许更加精细的控制,常用于配合Outside Kafka Offset存储实现精确一次性语义控制,和自定义分区绑定特性后续kafka特性一节再详解。
Consumer同样支持失败自动重试
,超过Consumer和broker之间的心跳时间session.timeout.ms或Consumer最大处理时间(两次poll调用时间)max.poll.interval.ms,均认为Consumer不可用,会自动重平衡其他Consumer来处理当前数据。另外可用max.poll.records指定每次poll最大消息数,合理的调整此参数可减少max.poll.interval.ms参数。
Consumer是非线程安全
的,可每个线程/进程使用一个Consumer对象或者使用线程同步技术,多个线程共享Consumer对象。
public static void ReadRecords()
{
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("test"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
}
原创,转载请注明来自