概述
Kafka是Apache旗下,由LinkedIn公司开发,Scala语言编写的消息队列。Kafka是一种分布式的,基于发布/订阅的消息系统,能够高效并实时的吞吐数据,以及通过分布式集群及数据复制冗余机制(副本冗余机制)实现数据的安全。
特点
1 高吞吐量
Kafka 每秒可以生产约 25 万消息(50 MB),每秒处理 55 万消息(110 MB)
2 持久化数据存储
可进行持久化操作。连续读写保证效率。将消息持久化到磁盘,因此可用于批量消费,例如 ETL,以及实时应用程序。通过将数据持久化到硬盘以及 replication 防止数据丢失。
ETL,是英文 Extract-Transform-Load 的缩写,用来描述将数据从来源端经过抽取(extract)、交互转换(transform)、加载(load)至目的端的过程。ETL一词较常用在数据仓库,但其对象并不限于数据仓库。
ETL是构建数据仓库的重要一环,用户从数据源抽取出所需的数据,经过数据清洗,最终按照预先定义好的数据仓库模型,将数据加载到数据仓库中去。
3 分布式系统易于拓展
所有的 producer、broker 和consumer 都会有多个,均为分布式的。无需停机即可扩展机器。
4 客户端状态维护
消息被处理的状态是在 consumer 端维护,而不是由 server 端维护。减轻服务器端的压力,为客户端会话管理提供了更好的灵活性。客户端主要维护offset,可以实现更灵活的信息。
适用场景
消息队列、网站活性跟踪、监控数据、日志收集
配置说明
伪分布式
-
修改server.properties文件
配置Kafka存储位置,要配置为非/tmp目录
log.dirs=非/tmp目录 -
修改zookeeper.properties文件
Kafka内置了一个zookeeper,在伪分布式模式下,可以使用这个内置zk作为集群协调工具,但是这个内置的zookeeper只有一个节点,不能用在生产环境下。
如果使用这个内置的zk,需要在zookeeper.properties文件中修改zk存储路径,改为非/tmp目录
dataDir=非/tmp目录。
完全分布式
-
修改server.properties文件
- 配置当前broker的编号,要求唯一
broker.id=0 #当前server编号 - 配置当前broker使用的端口
port=9092 #使用的端口 - 配置Kafka存储位置,要配置为非/tmp目录
log.dirs=非/tmp目录 - 配置zookeeper集群的地址
zookeeper.connect=hadoop01:2181,hadoop02:2181,hadoop03:2181
- 配置当前broker的编号,要求唯一
-
复制信息:将配置完成的kafka复制到其它broker中,并修正相关配置
scp -r kafka_2.11-1.0.0 root@hadoopxx:/home/software/
启动kafka
启动zookeeper(伪分布式):
bin/zookeeper-server-start.sh config/zookeeper.properties &
启动kafka:
bin/kafka-server-start.sh config/server.properties
操作说明
命令行操作
创建主题
bin/kafka-topics.sh --create --zookeeper hadoop01:2181,hadoop02:2181,hadoop03:2181 --partitions 1 --replication-factor 1 --topic test
查看所有主题
bin/kafka-topics.sh --list --zookeeper hadoop01:2181
查看主题信息
bin/kafka-topics.sh --describe --zookeeper hadoop01:2181 --topic test
启动生产者
bin/kafka-console-producer.sh --broker-list hadoop01:9092 --topic test
启动消费者
bin/kafka-console-consumer.sh --zookeeper hadoop01:2181 --topic test
API操作
操作说明
1 生产者写入数据的步骤
-
序列化ProducerRecord
-
根据分区规则决定数据去往哪一个分区
分区规则
-
如果ProducerRecord中指定了Partition,则直接将数据发往指定的分区。
ProducerRecord<String, String> record = new ProducerRecord<String, String>()是否有分区
-
如果没有指定Partition但指定了Key,则kafka使用自己实现的hash方法对key进行散列,通过hash分桶算法将数据发往对应分区,保证相同的key被映射到相同的partition中。
-
如果没有指定Partition也没有指定Key,则kafka使用默认的partitioner,使用RoundRobin算法将消息均衡地分布在各个partition上。
-
开发者也可以通过实现Partitioner接口,自己开发Partitioner,决定数据如何分发。
-
-
消息被添加到相应的batch中,独立的线程将这些batch发送到Broker上
-
broker收到消息会返回一个响应。如果消息成功写入Kafka,则返回RecordMetaData对象,该对象包含了
Topic信息、Patition信息、消息在Partition中的Offset信息;若失败,返回一个错误。
具体代码
创建Topics
@Test
public void creatTopics(){
ZkUtils zkUtils = ZkUtils.apply("aa:2181,bb:2181,bb:2181", 30000,
30000,JaasUtils.isZkSecurityEnabled());
//检查topic是否存在
boolean isExist = AdminUtils.topicExists(zkUtils, "student");
System.out.println("topic student exists? "+isExist);
//创建topic
//AdminUtils.createTopic(zkUtils, "student", 1, 1, new Properties(), RackAwareMode.Enforced$.MODULE$);
zkUtils.close();
}
创建生产者
@Test
public void producer(){
//1.创建生产者
Properties prop = new Properties();
/* bootstrap.servers->指定broker的地址清单
*key.serializer value.serializer
*必须是一个实现org.apache.kafka.common.serialization.Serializer接口的类
*将其序列化成字节数组*/
prop.put("bootstrap.servers", "student01:9092,student02:9092,student03:9092");
//key
prop.put("key.serializer", StringSerializer.class);
//value
prop.put("value.serializer", StringSerializer.class);
Producer<String, String> producer = new KafkaProducer<>(prop);
//2.创建消息对象 分区名,value名 还能加key,partition,timestamp
ProducerRecord<String, String> record = new ProducerRecord<String, String>("student", "hello banana");
1 异步非阻塞模式
//3.发送数据
try {
Future<RecordMetadata> future = producer.send(record);
RecordMetadata rmd = future.get();
//4.写入成功,获取写入结果信息
System.out.println("--topic:["+rmd.topic()+"]--partition:["+rmd.partition()+"]--offset:["+rmd.offset()+"]--timestamp:["+rmd.timestamp()+"]--");
} catch (InterruptedException | ExecutionException e) {
System.out.println("失败");
e.printStackTrace();
}
2 通过Callback回调处理结果
//3.发送消息到kafka
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata rmd, Exception e) {
if(e == null){//--写入成功没有异常
System.out.println("--topic:["+rmd.topic()+"]--partition:["+rmd.partition()+"]--offset:["+rmd.offset()+"]--timestamp:["+rmd.timestamp()+"]--");
}else{//--写入失败有异常
System.err.println("写入数据失败");
e.printStackTrace();
}
}
});
生产者其他参数
a. acks
指定必须要有多少个partition副本收到消息,生产者才会认为消息的写入是成功的。
acks=0,生产者不需要等待服务器的响应,以网络能支持的最大速度发送消息,吞吐量高,但是如果broker没有收到消息,生产者是不知道的
acks=1,leader partition收到消息,生产者就会收到一个来自服务器的成功响应
acks=all,所有的partition都收到消息,生产者才会收到一个服务器的成功响应
b. buffer.memory
设置生产者内缓存区域的大小,生产者用它缓冲要发送到服务器的消息。
c. compression.type
默认情况下,消息发送时不会被压缩,该参数可以设置成snappy、gzip或lz4对发送给broker的消息进行压缩
d. retries
生产者从服务器收到临时性错误时,生产者重发消息的次数
e. batch.size
发送到同一个partition的消息会被先存储在batch中,该参数指定一个batch可以使用的内存大小,单位是byte。不一定需要等到batch被填满才能发送
f. linger.ms
生产者在发送消息前等待linger.ms,从而等待更多的消息加入到batch中。如果batch被填满或者linger.ms达到上限,就把batch中的消息发送出去
g. max.in.flight.requests.per.connection
生产者在收到服务器响应之前可以发送的消息个数
自定义Avro序列化器
1 编写bean
Class Custom {
private int customID;
private String customerName;
public Custom(int customID, String customerName) {
super();
this.customID = customID;
this.customerName = customerName;
}
public int getCustomID() {
return customID;
}
public String getCustomerName() {
return customerName;
}
}
2 编写Schema
{
"namespace": "customerManagement.avro",
"type": "record",
"name": "Customer",
"fields":[
{
"name": "id", "type": "string"
},
{
"name": "name", "type": "string"
},
]
}
3 发送到kafka
Properties props = new Properties();
props.put("bootstrap", "loacalhost:9092");
props.put("key.serializer", "io.confluent.kafka.serializers.KafkaAvroSerializer");
props.put("value.serializer", "io.confluent.kafka.serializers.KafkaAvroSerializer");
props.put("schema.registry.url", schemaUrl);//schema.registry.url指向序列化的存储位置
String topic = "CustomerContacts";
Producer<String, Customer> produer = new KafkaProducer<String, Customer>(props);
创建消费者
@Test
public void consumer(){
Properties props = new Properties();
props.put("bootstrap.servers", "aa:9092");
//group.id 指定当前消费者所属的消费者组
props.put("group.id", "consumer-tutorial");
//key.deserializer value.deserializer 后面跟用来反序列化的类
//必须是一个实现org.apache.kafka.common.serialization.Deserializer接口的类
props.put("key.deserializer", StringDeserializer.class.getName());
props.put("value.deserializer", StringDeserializer.class.getName());
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
//指定监听的主题
consumer.subscribe(Arrays.asList("enbook", "student"));
try {
while (true) {
//消费时间长度为Long.MAX_VALUE毫秒的数据
ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE);
for (ConsumerRecord<String, String> record : records){
System.out.println("消费:"+record.offset() + ":" + record.value());
}
} catch (Exception e) {
} finally {
consumer.close();
}
}
}
消费者的其他相关参数
a. fetch.min.bytes
指定消费者从broker获取消息的最小字节数,即等到有足够的数据时才把它返回给消费者
b. fetch.max.wait.ms
等待broker返回数据的最大时间,默认是500ms。fetch.min.bytes和fetch.max.wait.ms哪个条件先得到满足,就按照哪种方式返回数据
c. max.partition.fetch.bytes
指定broker从每个partition中返回给消费者的最大字节数,默认1MB
d. session.timeout.ms
指定消费者被认定死亡之前可以与服务器断开连接的时间,默认是3s
e. auto.offset.reset
消费者在读取一个没有偏移量或者偏移量无效的情况下(因为消费者长时间失效,包含偏移量的记录已经过时并被删除)该作何处理。默认是latest(消费者从最新的记录开始读取数据)。另一个值是earliest(消费者从起始位置读取partition的记录)
f. enable.auto.commit
指定消费者是否自动提交偏移量,默认为true
g. partition.assignment.strategy
指定partition如何分配给消费者,默认是Range。
Range:把Topic的若干个连续的partition分配给消费者。
RoundRobin:把Topic的所有partition逐个分配给消费者
h. max.poll.records
单次调用poll方法能够返回的消息数量
偏移量的提交 —commitAsync VS commitSync()
同步提交:提交的方法将会有阻塞,直到提交完成,才放开阻塞,效率低,可靠性高
异步提交:提交的方法不会有阻塞,无论是否提交完成,都继续向下执行,效率高,可靠性低
混合提交:通常情况下使用异步提交,保证效率,出问题情况下使用同步提交,保证可靠提交
//同步提交
try {
while(true){
ConsumerRecords<String, String> rs = consumer.poll(100);
for(ConsumerRecord<String, String> r : rs){
System.out.println("--topic:["+r.topic()+"]"+ "--value:["+r.value()+"]--");
}
//--手动提交offset 同步提交,此方法将会阻塞,直到提交完成,如果提交失败,抛出异常
consumer.commitSync();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//4.关闭消费者
if(consumer!=null)consumer.close();
}
//异步提交
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
if(e != null){//--提交抛出了异常
System.err.println("提交偏移量出错!");
e.printStackTrace();
}
}
//混合提交
try{
while(true){
ConsumerRecords<String, String> rs = consumer.poll(100);
for(ConsumerRecord<String, String> r : rs){
System.out.println("--topic:["+r.topic()+"]"+ "--value:["+r.value()+"]--");
}
//--手动提交offset 混合提交提交,通常用异步提交
consumer.commitAsync();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//--出问题了,提交offset,此处为了保证能够提交,使用同步提交
consumer.commitSync();
//4.关闭消费者
if(consumer!=null)
consumer.close();
}