一、什么是Kafka
Kafka是由LinkedIn开发的一个分布式的消息系统,Kafka的Producer、Broker和Consumer之间采用的是一套自行设计的基于TCP层的协议,使用Scala编写,它以可水平扩展和高吞吐率而被广泛使用。目前越来越多的开源分布式处理系统如Storm,Spark,Flink都支持与Kafka集成。现在我们的数据实时处理平台也使用到了kafka。现在它已被多家不同类型的公司作为多种类型的数据管道和消息系统使用。
- 为什么Kafka可以水平扩展?
原因是由于Kafka系统本身就是无状态的消息传递系统,系统中没有存储任何元数据或者是状态数据,因此它非常容易的被横向扩展。其实在RabbitMQ中也是可以做到水平扩展的,也就是所谓的RabbitMQ分片,但其原理不一样,RabbitMQ属于分片,意味着消息是分散后存储在不同的片中。而Kafka为了提高吞吐量,因此它的集群中所有节点的消息存储量是保持一致的。也就意味着需要做到消息数据的强一致性或者最终一致性才行。
- 水平扩展的目的和好处是什么?
水平扩展的目的是为了解决高可用、高吞吐量的;因为水平扩展之后Kafka的节点变得更多,那么当其中一些节点宕机之后,水平扩展冗余的节点可以及时的响应系统做到高效可用。 当水平节点更多之后,那么带来的好处就是消费者在消费的时候可以不从一个节点获取所需要的消息,可以从来自不同的节点上去获取消息,所以水平的扩展带来的好处就是能提高消费者的高吞吐率;但是同时也带来一个问题?就是如何做到消息数据在各个节点的一致性?
- 那么Kafka是通过什么做到水平扩展集群的呢?
Kafka是依赖于zookeeper分布式中间件进行协调和管理Kafka的集群节点的,那么zookeeper中将存储Kafka的各项元数据信息。并且zk会以心跳的方式去监听各个Kafka节点的存活状态等。Kafka属于一个消息存储和读取执行的中间件,而zookeeper才是整个集群的核心大脑,控制着整个集群的分布和管理。
- 那么zookeeper在Kafka集群中扮演着什么样的角色呢?
目前来看Kafka必须依赖于Zookeeper进行启动和部署,那么zk在中间就扮演着集群管理者的角色,kafka是属于执行者的角色。Zk会选举一个kafka节点作为主节点,负责消息的写入,写入成功之后会由zk进行数据同步到其它节点上去,实现数据的最终一致性。
问题补充
一、高吞吐量原理
- 顺序写入
因为硬盘是机械结构,每次读写都会寻址->写入,其中寻址是一个“机械动作”,它是最耗时的。所以硬盘最“讨厌”随机I/O,最喜欢顺序I/O。为了提高读写硬盘的速度,Kafka就是使用顺序I/O。
每一个Partition其实都是一个文件,Kafka 为每个主题维护了分布式的分区(Partition)日志文件,每个 Partition 在 Kafka 存储层面是 Append Log。
任何发布到此 Partition 的消息都会被追加到 Log 文件的尾部,在分区中的每条消息都会按照时间顺序分配到一个单调递增的顺序编号,Offset 是一个 Long 型的数字。
这种方法有一个缺陷——没有办法删除数据,所以Kafka是不会删除数据的,它会把所有的数据都保留下来,每个消费者(Consumer)对每个Topic都有一个offset用来表示读取到了第几条数据。如果不删除硬盘肯定会被撑满,所以Kakfa提供了两种策略来删除数据。一是基于时间,二是基于partition文件大小。(配置文件中设置)
- 内存映射文件(Memory Mapped Files(简称mmap))
即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以Kafka的数据并不是实时的写入硬盘,它充分利用了现代操作系统分页存储来利用内存提高I/O效率。
工作原理是直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上。
通过mmap,进程像读写硬盘一样读写内存(当然是虚拟内存),也不必关心内存的大小有虚拟内存为我们兜底。
使用这种方式可以获取很大的I/O提升,省去了用户空间到内核空间复制的开销(调用文件的read会把数据先放到内核空间的内存中,然后再复制到用户空间的内存中。)写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。Kafka提供了一个参数——producer.type来控制是不是主动flush,如果Kafka写入到mmap之后就立即flush然后再返回Producer叫同步(sync);写入mmap之后立即返回Producer不调用flush叫异步(async)。
mmap其实是Linux中的一个函数就是用来实现内存映射的,基于Java NIO,它给我提供了一个mappedbytebuffer类可以用来实现内存映射所以才会有如此神速。
3.数据读取
Kafka把所有的消息都存放在一个一个的文件中,当消费者需要数据的时候Kafka直接把“文件”发送给消费者。这就是秘诀所在,比如:10W的消息组合在一起是10MB的数据量,然后Kafka用类似于发文件的方式直接扔出去了,如果消费者和生产者之间的网络非常好(只要网络稍微正常一点10MB根本不是事。。。家里上网都是100Mbps的带宽了),10MB可能只需要1s。所以答案是——10W的TPS,Kafka每秒钟处理了10W条消息。
可能你说:不可能把整个文件发出去吧?里面还有一些不需要的消息呢?是的,Zero Copy对应的是sendfile这个函数(以Linux为例),这个函数接受
out_fd作为输出(一般及时socket的句柄)
in_fd作为输入文件句柄
off_t表示in_fd的偏移(从哪里开始读取)
size_t表示读取多少个
Kafka是用mmap作为文件读写方式的,它就是一个文件句柄,所以直接把它传给sendfile;偏移也好解决,用户会自己保持这个offset,每次请求都会发送这个offset。(放在zookeeper中的);数据量更容易解决了,如果消费者想要更快,就全部扔给消费者。如果这样做一般情况下消费者肯定直接就被压死了;所以Kafka提供了的两种方式——Push,我全部扔给你了,你死了不管我的事情;Pull,好吧你告诉我你需要多少个,我给你多少个。
4.总结
Kafka速度的秘诀在于,把所有的消息都变成一个的文件。通过mmap提高I/O速度,写入数据的时候它是末尾添加所以速度最优;读取数据的时候配合sendfile直接暴力输出。阿里的RocketMQ也是这种模式,只不过是用Java写的。
单纯的去测试MQ的速度没有任何意义,Kafka这种“暴力”、“流氓”、“无耻”的做法已经脱了MQ的底裤,更像是一个暴力的“数据传送器”。所以对于一个MQ的评价只以速度论英雄,世界上没人能干的过Kafka,我们设计选型的时候不能听信网上的流言蜚语——“Kafka最快,大家都在用,所以我们的MQ用Kafka没错”。在这种思想的作用下,你可能根本不会关心“失败者”;而实际上可能这些“失败者”是更适合你业务的MQ。
二、数据丢失的情况
原因1:强行kill线程,导致消费后的数据,offset没有提交。
原因2:设置offset为自动提交,关闭kafka时,如果在close之前,调用 consumer.unsubscribe() 则有可能部分offset没提交,下次重启会重复消费。
原因3(重复消费最常见的原因):消费后的数据,当offset还没有提交时,partition就断开连接。比如,通常会遇到消费的数据,处理很耗时,导致超过了Kafka的session timeout时间(0.10.x版本默认是30秒),那么就会re-blance重平衡,此时有一定几率offset没提交,会导致重平衡后重复消费。
处理方法(参考):
1、每次消费时更新每个topic+partition位置的offset在内存中使用Map<key, value>,key=topic+'-'+partition,value=offset
当调用关闭consumer线程时,把上面Map的offset数据记录到 文件中*(分布式集群可能要记录到redis中)。
下一次启动consumer,需要读取上一次的offset信息,方法是 以当前的topic+partition为key,从上次的Map中去寻找offset。
然后使用consumer.seek()方法指定到上次的offset位置。
2、为了确保consumer消费的数据一定是接着上一次consumer消费的数据,
consumer消费时,记录第一次取出的数据,将其offset和上次consumer最后消费的offset进行对比,如果相同则继续消费。如果不同,则停止消费,检查原因。
三、历史消息消费(offset控制)
这里控制方式有两种,
一种是使用kafka命令,设置offset偏移量 ,这样会在整个topic生效,如果多个消费者可能会产生重复消费。(使用前提相关所有消费服务必须停止。)
./kafka-consumer-groups.sh --bootstrap-server 192.168.1.248:9092 --group zx_test –describe
GROUP TOPIC PID OFFSET LOGSIZE LAG
消费者组 topic名字 partition id 当前已消费的条数 总条数 未消费的条数
如果想控制当前offset,需要注意的是这里面的消息可能消费过后,超过配置文件(server.properties)里面的属性-> log.retention.hours = 168 ,这个属性代表消息保留时间为多少小时。默认为168小时,也就是一周时间。所以你只能控制到近期保留的消息偏移量 (我这里举例设置从80偏移量开始)-> 可以执行如下命令:
bin/kafka-consumer-groups.sh --bootstrap-server 192.168.1.248:9092 --group zx_test --topic zx_test --execute --reset-offsets --to-offset 80
第二种在在代码中指定消费的偏移量,可是指定从开始读取、从队尾读取、指定偏移量读取。
从头开始消费消息seekToBeginning()
配置信息
Properties props = new Properties();
props.put("bootstrap.servers","132.232.14.247:9092");
props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
props.put("group.id","group1");
新建一个KafkaConsumer
KafkaConsumer<String,byte[]> consumer = new KafkaConsumer<String, byte[]>(props);
通过TopicPartition指定要消费的partition
TopicPartition seekToEndPartition = new TopicPartition("mySecondTopic",3);
对consumer指定partitionassign
consumer.assign(Arrays.asList(seekToEndPartition));
调用consumer.seekToBeginning指定从头开始消费
consumer.seekToBeginning(Arrays.asList(seekToEndPartition));
//不改变当前offset,指定从这个topic和partition的开始位置获取。
consumer.seekToBeginning(Arrays.asList(new TopicPartition(topicName, 0)));
开始消费
public class MySeekToBeginningConsumer {
public static void main(String[] args) {
Properties props = new Properties();
props.put("bootstrap.servers","132.232.14.247:9092");
props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
props.put("group.id","group1");
KafkaConsumer<String,byte[]> consumer = new KafkaConsumer<String, byte[]>(props);
try {
TopicPartition seekToEndPartition = new TopicPartition("mySecondTopic",3);
consumer.assign(Arrays.asList(seekToEndPartition));
consumer.seekToBeginning(Arrays.asList(seekToEndPartition));
ConsumerRecords<String, byte[]> records = consumer.poll(5000);
for(ConsumerRecord<String,byte[]> record:records){
System.out.println("s consumption message:partition="+record.partition()+",offset="+record.offset()+",key="+record.key()+",value="+record.value());
}
}catch (java.lang.Exception e){
e.printStackTrace();
}finally {
consumer.close();
}
}
}
从尾开始消费消息seekToEnd()
调用consumer.seekToEnd从末尾开始进行消费
consumer.seekToEnd(Arrays.asList(seekToEndPartition ));
public class MySeekToEndConsumer {
public static void main(String[] args) {
Properties props=new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "group1");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String,byte[]> consumer=new KafkaConsumer<String,byte[]>(props);
try {
TopicPartition seekToEndPartition = new TopicPartition("mySecondTopic", 1);
consumer.assign(Arrays.asList(seekToEndPartition));
consumer.seekToEnd(Arrays.asList(seekToEndPartition ));
ConsumerRecords<String, byte[]> records=consumer.poll(1000);
for(ConsumerRecord<String, byte[]> record : records){
System.out.println("MySeekToEndConsumer consumer message:partition="+record.partition()+",offset="+record.offset()+",key="+record.key()+",value="+record.value());
}
} catch (Exception e) {
e.printStackTrace();
}finally{
consumer.close();
}
}
}
消费指定offset消息seek()
consumer.seek
//指定seekPartition和offset
consumer.seek(seekPartition,10);
public class MySeekConsumer {
public static void main(String[] args) {
Properties props = new Properties();
props.put("bootstrap.servers", "132.232.14.247:9092");
props.put("group.id", "group1");
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<String, String>(props);
try {
TopicPartition seekPartition = new TopicPartition("mySecondTopic", 1);
consumer.assign(Arrays.asList(seekPartition));
consumer.seek(seekPartition,10);
ConsumerRecords<String, String> records = consumer.poll(5000);
for (ConsumerRecord record:records){
System.out.println("s consumption message:partition="+record.partition()+",offset="+record.offset()+",key="+record.key()+",value="+record.value());
}
}catch (Exception e){
e.printStackTrace();
}finally {
consumer.close();
}
}
}
四、主从复制原理
Kafka中主题的每个Partition有一个预写式日志文件,每个Partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到Partition中,Partition中的每个消息都有一个连续的序列号叫做offset, 确定它在分区日志中唯一的位置。
为了提高消息的可靠性,Kafka每个topic的partition有N个副本(replicas),其中N(大于等于1)是topic的复制因子(replica fator)的个数。Kafka通过多副本机制实现故障自动转移,当Kafka集群中一个broker失效情况下仍然保证服务可用。在Kafka中发生复制时确保partition的日志能有序地写到其他节点上,N个replicas中,其中一个replica为leader,其他都为follower, leader处理partition的所有读写请求,(leader读写请求:kafka是由follower周期性或者尝试去pull(拉)过来(其实这个过程与consumer消费过程非常相似),写是都往leader上写,读也只在leader上读,flower只是数据的一个备份,保证leader被挂掉后顶上来,并不往外提供服务),follower会被动定期地去复制leader上的数据。
Leader产生机制
ISR (In-Sync Replicas),这个是指副本同步队列。这个队列里面存放的是所有要同步的副本(replicas)。每个Partition都会有一个ISR,而且是由leader动态维护,即每个partition都有一个唯一的leader。follower从leader同步数据有一些延迟(包括延迟时间replica.lag.time.max.ms和延迟条数replica.lag.max.messages两个维度。
如果性能超过阈值都会把follower剔除出ISR,存入OSR(Outof-Sync Replicas)列表并加入一个新的follower到队列中。新加入的follower也会先存放在OSR中。
leader写入数据后并不会commit,只有ISR列表中的所有folower同步之后才会commit,把滞后的follower移除ISR主要是避免写消息延迟。设置ISR主要是为了broker宕掉之后,重新选举partition的leader从ISR列表中选择。
五、推还是拉?
Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers将消息推送到consumer,也就是pull还push。在这方面,Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息。
一些消息系统比如Scribe和Apache Flume采用了push模式,将消息推送到下游的consumer。这样做有好处也有坏处:由broker决定消息推送的速率,对于不同消费速率的consumer就不太好处理了。消息系统都致力于让consumer以最大的速率最快速的消费消息,但不幸的是,push模式下,当broker推送的速率远大于consumer消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式。
Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。Push模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免consumer崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull模式下,consumer就可以根据自己的消费能力去决定这些策略。
Pull有个缺点是,如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,直到新消息到t达。为了避免这点,Kafka有个参数可以让consumer阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发送)。
六、集成springboot
生产者
pom.xml
spring.kafka.producer.bootstrap-servers=192.168.1.248:9092
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
#topic
kafka.app.topic.foo=test20180430
application.properties
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
Java
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Value("${kafka.app.topic.foo}")
private String topic;
public void send(String message){
LOG.info("topic="+topic+",message="+message);
ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send(topic, message);
future.addCallback(success -> LOG.info("KafkaMessageProducer 发送消息成功!"),
fail -> LOG.error("KafkaMessageProducer 发送消息失败!"));
}
消费者
application.properties
#kafka configuration
#指定消息被消费之后自动提交偏移量,以便下次继续消费
spring.kafka.consumer.enable-auto-commit=true
#指定消息组
spring.kafka.consumer.group-id=guan
#指定kafka服务器地址
spring.kafka.consumer.bootstrap-servers=192.168.1.248:9092
#指定从最近地方开始消费(earliest)
spring.kafka.consumer.auto-offset-reset=latest
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
#topic
kafka.app.topic.foo=test20180430
java
@KafkaListener(topics={"${kafka.app.topic.foo}"})
public void receive(@Payload String message, @Headers MessageHeaders headers){
LOG.info("KafkaMessageConsumer 接收到消息:"+message);
headers.keySet().forEach(key->LOG.info("{}: {}",key,headers.get(key)));
}
二、为什么使用Kafka
上面我们提到kafka是一个分布式的消息系统。那为什么要在我们的数据处理平台中使用这样的一个消息系统呢?消息系统能给我们带来什么样的好处呢?
(1) 解耦(接口解耦)
在项目启动之初来预测将来项目会碰到什么需求,是极其困难的。消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口。这允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
(2) 冗余(持久化数据)
有些情况下,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。
(3) 扩展性
因为采用消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。不需要改变代码、不需要调节参数。扩展就像调大电力按钮一样简单。
(4) 灵活性 & 峰值处理能力
在访问量剧增的情况下,使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。
(5) 顺序保证
在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。Kafka保证一个Partition内的消息的有序性。
(6) 缓冲
在任何重要的系统中,都会有需要不同的处理时间的元素。例如,加载一张图片比应用过滤器花费更少的时间。消息队列通过一个缓冲层来帮助任务最高效率的执行——写入队列的处理会尽可能的快速。该缓冲有助于控制和优化数据流经过系统的速度。
三、Kafka可以做什么
kafka是一个分布式消息队列。具有高性能、持久化、多副本备份、横向扩展能力。
消息队列已经逐渐成为企业IT系统内部通信的核心手段。它具有低耦合、可靠投递、广播、流量控制、最终一致性等一系列功能,成为异步RPC的主要手段之一。当今市面上有很多主流的消息中间件,如老牌的ActiveMQ、RabbitMQ,炙手可热的Kafka,阿里巴巴自主开发RocketMQ等。
1、异步通信
有些业务不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
2、解耦
降低工程间的强依赖程度,针对异构系统进行适配。在项目启动之初来预测将来项目会碰到什么需求,是极其困难的。通过消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口,当应用发生变化时,可以独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
3、冗余
有些情况下,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的”插入-获取-删除”范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。
4、扩展性
因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。不需要改变代码、不需要调节参数。便于分布式扩容。
5、过载保护
在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量无法提取预知;如果以为了能处理这类瞬间峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。
6、可恢复性
系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
7、顺序保证
在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。
8、缓冲
在任何重要的系统中,都会有需要不同的处理时间的元素。消息队列通过一个缓冲层来帮助任务最高效率的执行,该缓冲有助于控制和优化数据流经过系统的速度。以调节系统响应时间。
9、数据流处理
分布式系统产生的海量数据流,如:业务日志、监控数据、用户行为等,针对这些数据流进行实时或批量采集汇总,然后进行大数据分析是当前互联网的必备技术,通过消息队列完成此类数据收集是最好的选择。
主流MQ对比分析
1.RocketMQ
阿里系下开源的一款分布式、队列模型的消息中间件,原名Metaq,3.0版本名称改为RocketMQ,是阿里参照kafka设计思想使用java实现的一套mq。同时将阿里系内部多款mq产品(Notify、metaq)进行整合,只维护核心功能,去除了所有其他运行时依赖,保证核心功能最简化,在此基础上配合阿里上述其他开源产品实现不同场景下mq的架构,目前主要多用于订单交易系统。
具有以下特点:
1)能够保证严格的消息顺序
2)提供针对消息的过滤功能
3)提供丰富的消息拉取模式
4)高效的订阅者水平扩展能力
5)实时的消息订阅机制
6)亿级消息堆积能力
2.RabbitMQ
使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP,STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。同时实现了Broker架构,核心思想是生产者不会将消息直接发送给队列,消息在发送给客户端时先在中心队列排队。对路由(Routing),负载均衡(Load balance)、数据持久化都有很好的支持。多用于进行企业级的ESB整合。
3.ActiveMQ
Apache下的一个子项目。使用Java完全支持JMS1.1和J2EE 1.4规范的 JMS Provider实现,少量代码就可以高效地实现高级应用场景。可插拔的传输协议支持,比如:in-VM, TCP, SSL, NIO, UDP, multicast, JGroups and JXTA transports。RabbitMQ、ZeroMQ、ActiveMQ均支持常用的多种语言客户端 C++、Java、.Net,、Python、 Php、 Ruby等。
4.Kafka
Apache下的一个子项目,使用scala实现的一个高性能分布式Publish/Subscribe消息队列系统,具有以下特性:
快速持久化:通过磁盘顺序读写与零拷贝机制,可以在O(1)的系统开销下进行消息持久化;
高吞吐:在一台普通的服务器上既可以达到10W/s的吞吐速率;
高堆积:支持topic下消费者较长时间离线,消息堆积量大;
完全的分布式系统:Broker、Producer、Consumer都原生自动支持分布式,依赖zookeeper自动实现复杂均衡;
支持Hadoop数据并行加载:对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。
5.ZeroMQ
称最快的消息队列系统,专门为高吞吐量/低延迟的场景开发,在金融界的应用中经常使用,偏重于实时数据通信场景。ZMQ能够实现RabbitMQ不擅长的高级/复杂的队列,但是开发人员需要自己组合多种技术框架,开发成本高。因此ZeroMQ具有一个独特的非中间件的模式,更像一个socket library,你不需要安装和运行一个消息服务器或中间件,因为你的应用程序本身就是使用ZeroMQ API完成逻辑服务的角色。但是ZeroMQ仅提供非持久性的队列,如果down机,数据将会丢失。如:Twitter的Storm中使用ZeroMQ作为数据流的传输。
ZeroMQ套接字是与传输层无关的:ZeroMQ套接字对所有传输层协议定义了统一的API接口。默认支持 进程内(inproc) ,进程间(IPC) ,多播,TCP协议,在不同的协议之间切换只要简单的改变连接字符串的前缀。可以在任何时候以最小的代价从进程间的本地通信切换到分布式下的TCP通信。ZeroMQ在背后处理连接建立,断开和重连逻辑。
特性:
无锁的队列模型:对于跨线程间的交互(用户端和session)之间的数据交换通道pipe,采用无锁的队列算法CAS;在pipe的两端注册有异步事件,在读或者写消息到pipe的时,会自动触发读写事件。
批量处理的算法:对于批量的消息,进行了适应性的优化,可以批量的接收和发送消息。
多核下的线程绑定,无须CPU切换:区别于传统的多线程并发模式,信号量或者临界区,zeroMQ充分利用多核的优势,每个核绑定运行一个工作者线程,避免多线程之间的CPU切换开销。
6.redis
使用C语言开发的一个Key-Value的NoSQL数据库,开发维护很活跃,虽然它是一个Key-Value数据库存储系统,但它本身支持MQ功能,所以完全可以当做一个轻量级的队列服务来使用。对于RabbitMQ和Redis的入队和出队操作,各执行100万次,每10万次记录一次执行时间。测试数据分为128Bytes、512Bytes、1K和10K四个不同大小的数据。实验表明:入队时,当数据比较小时Redis的性能要高于RabbitMQ,而如果数据大小超过了10K,Redis则慢的无法忍受;出队时,无论数据大小,Redis都表现出非常好的性能,而RabbitMQ的出队性能则远低于Redis。
四、Kafka基础概念
1. 点对点模型(PTP)
点对点模型用于消息生产者和消息消费者之间点对点的通信。消息生产者将消息发送到由某个名字标识的特定消费者。这个名字实际上对应于消费服务中的一个队列(Queue),在消息传递给消费者之前它被存储在这个队列中。队列消息可以放在内存中也可以是持久的,以保证在消息服务出现故障时仍然能够传递消息。 点对点模型特性:
- 每个消息只有一个消费者
- 发送者和接受者没有时间依赖
- 接受者确认消息接受和处理成功
2. 发布—订阅模型(Pub/Sub)
发布者/订阅者模型支持向一个特定的消息主题生产消息。0或多个订阅者可能对接收来自特定消息主题的消息感兴趣。在这种模型下,发布者和订阅者彼此不知道对方。这种模式好比是匿名公告板。这种模式被概括为:多个消费者可以获得消息。在发布者和订阅者之间存在时间依赖性。发布者需要建立一个订阅(subscription),以便能够让消费者订阅。订阅者必须保持持续的活动状态以接收消息,除非订阅者建立了持久的订阅。在这种情况下,在订阅者未连接时发布的消息将在订阅者重新连接时重新发布。 其实消息中间件,像MySQL其实也可以作为消息中间件,只要你把消息中间件原理搞清楚,你会发现目前所有的存储,包括NoSQL,只要支持顺序性东西的,就可以作为一个消息中间件。就看你怎么去利用它了。就像redis里面那个队列list,就可以作为一个消息队列。 发布—订阅模型特性:
1)每个消息可以有多个订阅者
2)客户端只有订阅后才能接收到消息
3)持久订阅和非持久订阅
(1) 发布者和订阅者有时间依赖
接收者和发布者只有建立订阅关系才能收到消息。
(2) 持久订阅
订阅关系建立后,消息就不会消失,不管订阅者是否在线。
(3) 非持久订阅
订阅者为了接收消息,必须一直在线
当只有一个订阅者时约等于点对点模式。
3. 消息投递可靠性
一个消息如何算投递成功,Kafka提供了三种模式:
- 第一种是啥都不管,发送出去就当作成功,这种情况当然不能保证消息成功投递到broker;
- 第二种是Master-Slave模型,只有当Master和所有Slave都接收到消息时,才算投递成功,这种模型提供了最高的投递可靠性,但是损伤了性能;
- 第三种模型,即只要Master确认收到消息就算投递成功;实际使用时,根据应用特性选择,绝大多数情况下都会中和可靠性和性能选择第三种模型
消息在broker上的可靠性,因为消息会持久化到磁盘上,所以如果正常stop一个broker,其上的数据不会丢失;但是如果不正常stop,可能会使存在页面缓存来不及写入磁盘的消息丢失,这可以通过配置flush页面缓存的周期、阈值缓解,但是同样会频繁的写磁盘会影响性能,又是一个选择题,根据实际情况配置。
消息消费的可靠性,Kafka提供的是“At least once”模型,因为消息的读取进度由offset提供,offset可以由消费者自己维护也可以维护在zookeeper里,但是当消息消费后consumer挂掉,offset没有即时写回,就有可能发生重复读的情况,这种情况同样可以通过调整commit offset周期、阈值缓解,甚至消费者自己把消费和commit offset做成一个事务解决,但是如果你的应用不在乎重复消费,那就干脆不要解决,以换取最大的性能。
五、Kafka通常用于什么场景
- 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、Hbase、Solr等。
- 消息系统:解耦和生产者和消费者、缓存消息等。
- 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘。
- 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。
- 流式处理:比如spark streaming和storm
- 事件源
六、Kafka架构介绍
1. 采用异步处理模式
消息发送者可以发送一个消息而无须等待响应。消息发送者将消息发送到一条虚拟的通道(主题或者队列)上,消息接收者则订阅或者监听该通道。一条消息可能最终转发给一个或多个消息接收者,这些接收者都无需对消息发送者做出同步回应。整个过程是异步的。
比如用户信息注册。注册完成后过段时间发送邮件或者短信。
2. 应用程序和应用程序调用关系为松耦合关系
发送者和接收者不必要了解对方、只需要确认消息
发送者和接收者不必同时在线
比如在线交易系统为了保证数据的最终一致,在支付系统处理完成后会把支付结果放到信息中间件里通知订单系统修改订单支付状态。两个系统通过消息中间件解耦。
部署架构
七、Kafka部署方式
Zookeeper安装部署
1.下载zookeeper
下载地址:https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/
本例已zookeeper-3.4.13为例
2.下载kafka
下载地址:https://mirrors.tuna.tsinghua.edu.cn/apache/kafka/2.3.0/
本例已kafka_2.12-2.3.0为例
3.安装zookeeper
1)将文件上传到服务器/opt/kafka目录下
2)解压:tar zxvf zookeeper-3.4.13.tar.gz
3)进入Zookeeper的config目录下
cd /opt/kafka/zookeeper/conf
4)拷贝zoo_sample.cfg文件重命名为zoo.cfg,然后修改dataDir属性
# 数据的存放目录 dataDir=/home/data/zkdata # 端口,默认就是2181 clientPort=2181 |
5)配置环境变量
vim /etc/profile ,添加:
# Zookeeper Environment Variable export ZOOKEEPER_HOME=/opt/kafka/zookeeper export PATH=$PATH:$ZOOKEEPER_HOME/bin |
然后 source /etc/profile
6)Zookeeper 启动停止命令
$ zkServer.sh start $ zkServer.sh stop |
看到started表示启动成功
7)验证
可以输入zk的状态命令查看,命令如下:
bin/zkServer.sh status
如果是大节点部署会出现一个leader和若干follower。
因为是单节点部署所以显示standalone表示独立部署。
Kafka下载解压
将文件上传到服务器/opt/kafka目录下
解压:tar zxvf kafka_2.12-2.3.0.tgz
单节点单Broker部署
进入kafka的config目录下,有一个server.properties,添加如下配置
# broker的全局唯一编号,不能重复 broker.id=0 # 监听 listeners=PLAINTEXT://192.168.1.248:9092 advertised.listeners=PLAINTEXT://192.168.1.248:9092 # 日志目录 log.dirs=/home/log/kafka-logs # 配置zookeeper的连接(如果不是本机,需要该为ip或主机名) zookeeper.connect=192.168.1.248:2181 |
启动kafka,进入到kafka的bin目录下
./kafka-server-start.sh ../config/server.properties 后台启动 ./kafka-server-start.sh config/server.properties & |
单节点多broker部署
配置参考官方给的参考文档,一台机器上配置3个kafak
(1) copy 之前设置的server.properties配置文件
cp server.properties server1.properties
cp server.properties server2.properties
server1.properties修改如下参数
broker.id =1
listeners=PLAINTEXT:// 192.168.1.248:9093
log.dir=/home/hadoop/app/tmp/kafka-logs1
server-2.properties修改如下参数
broker.id =2
listeners=PLAINTEXT://192.168.1.248:9094
log.dir=/home/hadoop/app/tmp/kafka-logs2
启动指令如下:
kafka-server-start.sh -daemon ../config/server1.properties &
kafka-server-start.sh -daemon ../config/server2.properties &
使用jps查看,将会看到3个kafka进程,使用jps -m查看进程使用的config文档。
kafka使用:
1.创建topic信息
kafka-topics.sh --create --zookeeper 192.168.1.248:2181 --replicaton-factor(副本数) 3 --partitons 1 --topic "my-replicated-topic"d
查看topic使用kafka-topics.sh --list --zookeeper 192.168.1.248:2181
查看详细信息:kafka-topics.sh --describe --zookeeper 192.168.1.248:2181 ,此时的leader:主的broker ,replicas:代表副本对应的broker id 号, Isr:代表活着的broker
2.发送消息
kafka-producer.sh --broker-list localhost:9093,localhost:9094,localhost:9095 --topic my-replicated-topic
然后启动一个消费者进行消费:
kafka-console-consumer.sh --zookeeper localhost:2181 ---topic my-replicated-topic
集群部署
部署方式和部署单节点多broker类似,只是需要三台以上的计算机或是虚拟机。没台机器分别部署zookeeper和kafka 并单独启动。
注意:每个kafka节点server.properties配置文件中都要配置三个zookeeper节点的IP:端口
安装遇到的问题:
1), centos预装了openJDK导致启动成功但是消息不能发出去,程序也不报错,就一直卡住。
解决办法:卸载openJDK,安装oracle JDK8
- rpm -qa|grep java(看看结果)
- rpm -e –nodeps XXX(.noarch可以不用删除)
- 安装jdk
一些操作命令:
1,开启zookeeper集群 startzk.sh
2,开启kafka集群 start-kafka.sh
2,开启kafka可视化界面 kafka-manager : start-kafka-manager.sh
3, 生产者操作:
kafka-console-producer.sh --broker-list node1:9092 --topic my-kafka-topic //my-kafka-topic时topic的名字
4,消费者操作:
kafka-console-consumer.sh --bootstrap-server node1:9092 --topic my-kafka-topic
# 通过以上命令,可以看到消费者可以接收生产者发送的消息
# 如果需要从头开始接收数据,需要添加--from-beginning参数
kafka-console-consumer.sh --bootstrap-server node01:9092 --from-beginning --topic my-kafka-topic
5,a.创建topic
kafka-topics.sh --create --zookeeper node1:2181 --replication-factor 1 --partitions 1 --topic my-kafka-topic
6.查看topic列表
kafka-topics.sh --list --zookeeper node01:2181
7.如果需要查看topic的详细信息,需要使用describe命令
kafka-topics.sh --describe --zookeeper node1:2181 --topic test-topic
8.#若不指定topic,则查看所有topic的信息
kafka-topics.sh --describe --zookeeper node1:2181
9.删除topic
kafka-topics.sh --delete --zookeeper node1:2181 --topic my-kafka-topic
server.properties中所有配置参数说明列表
参数
说明(解释)
broker.id =0
每一个broker在集群中的唯一表示,要求是正数。当该服务器的IP地址发生改变时,broker.id没有变化,则不会影响consumers的消息情况
log.dirs=/data/kafka-logs
kafka数据的存放地址,多个地址的话用逗号分割/data/kafka-logs-1,/data/kafka-logs-2
port =9092
broker server服务端口
message.max.bytes =6525000
表示消息体的最大大小,单位是字节
num.network.threads =4
broker处理消息的最大线程数,一般情况下不需要去修改
num.io.threads =8
broker处理磁盘IO的线程数,数值应该大于你的硬盘数
background.threads =4
一些后台任务处理的线程数,例如过期消息文件的删除等,一般情况下不需要去做修改
queued.max.requests =500
等待IO线程处理的请求队列最大数,若是等待IO的请求超过这个数值,那么会停止接受外部消息,应该是一种自我保护机制。
host.name
broker的主机地址,若是设置了,那么会绑定到这个地址上,若是没有,会绑定到所有的接口上,并将其中之一发送到ZK,一般不设置
socket.send.buffer.bytes=100*1024
socket的发送缓冲区,socket的调优参数SO_SNDBUFF
socket.receive.buffer.bytes =100*1024
socket的接受缓冲区,socket的调优参数SO_RCVBUFF
socket.request.max.bytes =100*1024*1024
socket请求的最大数值,防止serverOOM,message.max.bytes必然要小于socket.request.max.bytes,会被topic创建时的指定参数覆盖
log.segment.bytes =1024*1024*1024
topic的分区是以一堆segment文件存储的,这个控制每个segment的大小,会被topic创建时的指定参数覆盖
log.roll.hours =24*7
这个参数会在日志segment没有达到log.segment.bytes设置的大小,也会强制新建一个segment会被 topic创建时的指定参数覆盖
log.cleanup.policy = delete
日志清理策略选择有:delete和compact主要针对过期数据的处理,或是日志文件达到限制的额度,会被 topic创建时的指定参数覆盖
log.retention.minutes=3days
数据存储的最大时间超过这个时间会根据log.cleanup.policy设置的策略处理数据,也就是消费端能够多久去消费数据
log.retention.bytes和log.retention.minutes任意一个达到要求,都会执行删除,会被topic创建时的指定参数覆盖
log.retention.bytes=-1
topic每个分区的最大文件大小,一个topic的大小限制 =分区数*log.retention.bytes。-1没有大小限log.retention.bytes和log.retention.minutes任意一个达到要求,都会执行删除,会被topic创建时的指定参数覆盖
log.retention.check.interval.ms=5minutes
文件大小检查的周期时间,是否处罚 log.cleanup.policy中设置的策略
log.cleaner.enable=false
是否开启日志压缩
log.cleaner.threads = 2
日志压缩运行的线程数
log.cleaner.io.max.bytes.per.second=None
日志压缩时候处理的最大大小
log.cleaner.dedupe.buffer.size=500*1024*1024
日志压缩去重时候的缓存空间,在空间允许的情况下,越大越好
log.cleaner.io.buffer.size=512*1024
日志清理时候用到的IO块大小一般不需要修改
log.cleaner.io.buffer.load.factor =0.9
日志清理中hash表的扩大因子一般不需要修改
log.cleaner.backoff.ms =15000
检查是否处罚日志清理的间隔
log.cleaner.min.cleanable.ratio=0.5
日志清理的频率控制,越大意味着更高效的清理,同时会存在一些空间上的浪费,会被topic创建时的指定参数覆盖
log.cleaner.delete.retention.ms =1day
对于压缩的日志保留的最长时间,也是客户端消费消息的最长时间,同log.retention.minutes的区别在于一个控制未压缩数据,一个控制压缩后的数据。会被topic创建时的指定参数覆盖
log.index.size.max.bytes =10*1024*1024
对于segment日志的索引文件大小限制,会被topic创建时的指定参数覆盖
log.index.interval.bytes =4096
当执行一个fetch操作后,需要一定的空间来扫描最近的offset大小,设置越大,代表扫描速度越快,但是也更好内存,一般情况下不需要搭理这个参数
log.flush.interval.messages=None
log文件”sync”到磁盘之前累积的消息条数,因为磁盘IO操作是一个慢操作,但又是一个”数据可靠性"的必要手段,所以此参数的设置,需要在"数据可靠性"与"性能"之间做必要的权衡.如果此值过大,将会导致每次"fsync"的时间较长(IO阻塞),如果此值过小,将会导致"fsync"的次数较多,这也意味着整体的client请求有一定的延迟.物理server故障,将会导致没有fsync的消息丢失.
log.flush.scheduler.interval.ms =3000
检查是否需要固化到硬盘的时间间隔
log.flush.interval.ms = None
仅仅通过interval来控制消息的磁盘写入时机,是不足的.此参数用于控制"fsync"的时间间隔,如果消息量始终没有达到阀值,但是离上一次磁盘同步的时间间隔达到阀值,也将触发.
log.delete.delay.ms =60000
文件在索引中清除后保留的时间一般不需要去修改
log.flush.offset.checkpoint.interval.ms =60000
控制上次固化硬盘的时间点,以便于数据恢复一般不需要去修改
auto.create.topics.enable =true
是否允许自动创建topic,若是false,就需要通过命令创建topic
default.replication.factor =1
是否允许自动创建topic,若是false,就需要通过命令创建topic
num.partitions =1
每个topic的分区个数,若是在topic创建时候没有指定的话会被topic创建时的指定参数覆盖
以下是kafka中Leader,replicas配置参数
controller.socket.timeout.ms =30000
partition leader与replicas之间通讯时,socket的超时时间
controller.message.queue.size=10
partition leader与replicas数据同步时,消息的队列尺寸
replica.lag.time.max.ms =10000
replicas响应partition leader的最长等待时间,若是超过这个时间,就将replicas列入ISR(in-sync replicas),并认为它是死的,不会再加入管理中
replica.lag.max.messages =4000
如果follower落后与leader太多,将会认为此follower[或者说partition relicas]已经失效
##通常,在follower与leader通讯时,因为网络延迟或者链接断开,总会导致replicas中消息同步滞后
##如果消息之后太多,leader将认为此follower网络延迟较大或者消息吞吐能力有限,将会把此replicas迁移
##到其他follower中.
##在broker数量较少,或者网络不足的环境中,建议提高此值.
replica.socket.timeout.ms=30*1000
follower与leader之间的socket超时时间
replica.socket.receive.buffer.bytes=64*1024
leader复制时候的socket缓存大小
replica.fetch.max.bytes =1024*1024
replicas每次获取数据的最大大小
replica.fetch.wait.max.ms =500
replicas同leader之间通信的最大等待时间,失败了会重试
replica.fetch.min.bytes =1
fetch的最小数据尺寸,如果leader中尚未同步的数据不足此值,将会阻塞,直到满足条件
num.replica.fetchers=1
leader进行复制的线程数,增大这个数值会增加follower的IO
replica.high.watermark.checkpoint.interval.ms =5000
每个replica检查是否将最高水位进行固化的频率
controlled.shutdown.enable =false
是否允许控制器关闭broker ,若是设置为true,会关闭所有在这个broker上的leader,并转移到其他broker
controlled.shutdown.max.retries =3
控制器关闭的尝试次数
controlled.shutdown.retry.backoff.ms =5000
每次关闭尝试的时间间隔
leader.imbalance.per.broker.percentage =10
leader的不平衡比例,若是超过这个数值,会对分区进行重新的平衡
leader.imbalance.check.interval.seconds =300
检查leader是否不平衡的时间间隔
offset.metadata.max.bytes
客户端保留offset信息的最大空间大小
kafka中zookeeper参数配置
zookeeper.connect = localhost:2181
zookeeper集群的地址,可以是多个,多个之间用逗号分割hostname1:port1,hostname2:port2,hostname3:port3
zookeeper.session.timeout.ms=6000
ZooKeeper的最大超时时间,就是心跳的间隔,
若是没有反映,那么认为已经死了,不易过大
zookeeper.connection.timeout.ms =6000
ZooKeeper的连接超时时间
zookeeper.sync.time.ms =2000
ZooKeeper集群中leader和follo
八、Kafka中间件的组成
broker
1.中间的kafka cluster,存储消息是由多个server组成的集群。
topic
kafka给消息提供的分类方式。broker用来存储不同topic的消息数据。
kafka将所有消息组织成多个topic的形式存储,而每个topic又可以拆分成多个partition,每个partition又由一个一个消息组成。每个消息都被标识了一个递增序列号代表其进来的先后顺序,并按顺序存储在partition中。
这样,消息就以一个个id的方式,组织起来。
producer选择一个topic,生产消息,消息会通过分配策略append到某个partition末尾。
consumer选择一个topic,通过id指定从哪个位置开始消费消息。消费完成之后保留id,下次可以从这个位置开始继续消费,也可以从其他任意位置开始消费。
上面的id在kafka中称为offset,这种组织和处理策略提供了如下好处:
消费者可以根据需求,灵活指定offset消费。
保证了消息不变性,为并发消费提供了线程安全的保证。每个consumer都保留自己的offset,互相之间不干扰,不存在线程安全问题。
消息访问的并行高效性。每个topic中的消息被组织成多个partition,partition均匀分配到集群server中。生产、消费消息的时候,会被路由到指定partition,减少竞争,增加了程序的并行能力。
增加消息系统的可伸缩性。每个topic中保留的消息可能非常庞大,通过partition将消息切分成多个子消息,并通过负责均衡策略将partition分配到不同server。这样当机器负载满的时候,通过扩容可以将消息重新均匀分配。
保证消息可靠性。消息消费完成之后不会删除,可以通过重置offset重新消费,保证了消息不会丢失。
灵活的持久化策略。可以通过指定时间段(如最近一天)来保存消息,节省broker存储空间。
备份高可用性。消息以partition为单位分配到多个server,并以partition为单位进行备份。备份策略为:1个leader和N个followers,leader接受读写请求,followers被动复制leader。leader和followers会在集群中打散,保证partition高可用。
producer
往broker中某个topic里面生产数据。
producer生产消息需要如下参数:
topic:往哪个topic生产消息。
partition:往哪个partition生产消息。
key:根据该key将消息分区到不同partition。
message:消息。
consumer
从broker中某个topic获取数据。
传统消息系统有两种模式:
队列
发布订阅
kafka通过consumer group将两种模式统一处理:每个consumer将自己标记consumer group名称,之后系统会将consumer group按名称分组,将消息复制并分发给所有分组,每个分组只有一个consumer能消费这条消息。如下图:
于是推理出两个极端情况:
当所有consumer的consumer group相同时,系统变成队列模式
当每个consumer的consumer group都不相同时,系统变成发布订阅
九、Kafka案例总结
说明:案例中均为Java代码
1、单节点案例
生产者
public static String topic = "zx_test";//定义主题
public static void main(String[] args) throws InterruptedException { Properties p = new Properties(); p.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.1.248:9092");//kafka地址,多个地址用逗号分割 p.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); p.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(p);
try { while (true) { String msg = "Hello," + new Random().nextInt(100); ProducerRecord<String, String> record = new ProducerRecord<String, String>(topic,msg, msg); kafkaProducer.send(record); System.out.println("消息发送成功:" + msg); Thread.sleep(500); } }catch(Exception e){ e.printStackTrace(); } finally { kafkaProducer.close(); }
} |
消费者
public static void main(String[] args) { Properties p = new Properties(); p.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.1.248:9092"); p.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); p.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); p.put(ConsumerConfig.GROUP_ID_CONFIG, "zx_test");
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(p); kafkaConsumer.subscribe(Collections.singletonList(Producer.topic));// 订阅消息
while (true) { ConsumerRecords<String, String> records = kafkaConsumer. for (ConsumerRecord<String, String> record : records) { System.out.println(String.format("topic:%s,offset:%d,消息:%s", // record.topic(), record.offset(), record.value())); } } } |
2、分布式部署案例
生产者
public static void main(String[] args) throws InterruptedException { Properties props = new Properties(); //kafka servers props.put("bootstrap.servers", "192.168.1.248:9092,192.168.1.248:9093,192.168.1.248:9094"); props.put("acks", "all"); props.put("retries", 0); props.put("batch.size", 16384); props.put("linger.ms", 1); //topic 分组 props.put("client.id", "DemoProducer"); 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 = 10; i < 20; i++){ producer.send(new ProducerRecord<String, String>("my-topic", Integer.toString(i), Integer.toString(i))); System.out.println("消息发送。"+i); } producer.close(); } |
消费者
public static void main(String[] args) { Properties props = new Properties(); //kafka servers props.put("bootstrap.servers", "192.168.1.248:9092,192.168.1.248:9093,192.168.1.248:9094"); //group props.put("group.id", "DemoConsumer"); 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); //订阅的topic consumer.subscribe(Arrays.asList("my-topic")); while (true) { //超时时间 ms ConsumerRecords<String, String> records = consumer. for (ConsumerRecord<String, String> record : records) System.out.printf("测试 offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value()); } } |