Kafka是从领英诞生,2011年初开源,2012年10月23日由Apache Incubator孵化。kafka最初是为了解决linkedin的数据管道问题。Linkedin采用了ActiveMQ来进行数据交换,大约是在2010年前后,那时的ActiveMQ还大大无法满足Linkedin对数据传递系统的要求,经常由于各种缺陷而导致消息阻塞或者服务无法正常访问,为了解决这个问题,Linkedin决定研发自己的消息传递系统,当时linkedin的首席架构师Jay Kreps 便开始组织团队进行消息传递系统的研发。
Kafka 成为主流的消息中间件,与它所“扮演”的三大角色是分不开的。
1.消息系统: Kafka 和传统的消息中间件都具备系统解稿、冗余存储、流量削峰、缓冲、异步通信、扩展性、可恢复性等功能。此外,Kafka 还提供了大多数消息系统没有的功能:消息顺序性保障及回溯消费的功能。
2.存储系统: Kafka 把消息持久化到磁盘,相比于其他基于内存存储的系统而言,降低了数据丢失的风险。也正是得益于Kafka 的消息持久化功能和多副本机制,我们可以把 Kafka 作为长期的数据存储系统来使用,只需要把对应的数据保留策略设置为“永久”或“启用主题的日志压缩功能”即可。
3.流式处理平台: Kafka 不仅为每个流行的流式处理框架提供了可靠的数据来源,还提供了一个完整的流式处理类库,比如窗口、连接、变换和聚合等各类操作。
Kafka有一个称为Kafka Connect的内置框架,用于编写源和汇,这些源和汇要么不断地将数据摄入Kafka,要么不断地把Kafka中的数据摄入外部系统。不同应用程序或数据系统的连接器本身是联合的,并与主代码库分开维护。Confluent在Confluent Hub维护外部托管的连接器列表。
Apache Kafka中的文档
Confluent平台中的文档
Apache Kafka中的Kafka Streams代码示例
Confluent提供的Kafka Streams代码示例
复杂事件处理(CEP):https://github.com/fhussonnois/kafkastreams-cep.
Fluent Kafka Streams测试:https://github.com/bakdata/fluent-kafka-streams-
tests(博客文章:https://medium.com/bakdata/fluent-kafka-streams-tests-
e641785171ec)
Azkarra Streams-一个轻量级的java框架,可以轻松地基于Kafka Streams构建和管理流式微服务。
Storm-流处理框架。
Samza-一个基于YARN的流处理框架。
Storm Spout-使用来自Kafka的消息并以Storm元组的形式发送
Kafka Storm-Kafka 0.8,Storm 0.9,Avro集成
SparkStreaming-Kafka接收器支持Kafka 0.8及以上版本
Flink-Apache Flink与Kafka集成
IBM Streams-一个具有Kafka源和宿的流处理框架,用于消费和生成Kafka消息
Spring Cloud Stream-用于构建事件驱动微服务的框架,Spring Cloud Data Flow-用于Spring Cloud Stream应用程序的云原生编排服务
ApacheApex-流处理框架,带有Kafka的连接器作为源和宿。
Logstash-输入和输出插件,用于丰富事件并可选地存储在Elasticsearch中
Logagent-Kafka输入和Kafka输出插件
...
https://cwiki.apache.org/confluence/display/KAFKA/Ecosystem
消息队列是分布式系统中重要的组件,kafka就可以看做是一种消息队列。
降低模块与模块之间的调用强依赖关系,不直接调用管理系统。如业务中台系统,需要通知下游系统消息数据,如果直接由交给业务中台调用下游系统接口,会出现依赖倒置的情况。
异步通信的消息,不需要等待其他系统的完成响应,提升系统的并发
减少并发流量,对服务器的压力。MQ消费者是通过自身消费能力从消息队列中获取到消息,防止并发流量导致服务器无法承载压力,从而导致服务器宕机的情况。
实时收集网站活动数据(例如注册、登录、下单、充值、支付等),根据业务数据类型将消息发布到不同的Topic,然后利用订阅消息的实时投递,将消息流用于实时处理、实时监控或者加载到Hadoop、MaxCompute等离线数据仓库系统进行离线处理。
许多公司,例如淘宝、天猫等,每天都会产生大量的日志(一般为流式数据,例如搜索引擎PV、查询等)。相较于以日志为中心的系统,例如Scribe和Flume,消息队列Kafka版在具备高性能的同时,可以实现更强的数据持久化以及更短的端到端响应时间。消息队列Kafka版的这种特性决定它适合作为日志收集中心。消息队列Kafka版忽略掉文件的细节,可以将多台主机或应用的日志数据抽象成一个个日志或事件的消息流,异步发送到消息队列Kafka版集群,从而实现非常低的RT。消息队列Kafka版客户端可批量提交消息和压缩消息,对生产者而言几乎感觉不到性能的开支。消费者可以使用Hadoop、MaxCompute等离线仓库存储和Strom、Spark等实时在线分析系统对日志进行统计分析
在很多领域,如股市走向分析、气象数据测控、网站用户行为分析,由于数据产生快、实时性强且量大,很难统一采集这些数据并将其入库存储后再做处理,这便导致传统的数据处理架构不能满足需求。与传统架构不同,消息队列Kafka版以及Storm、Samza、Spark等流计算引擎的出现,就是为了更好地解决这类数据在处理过程中遇到的问题,流计算模型能实现在数据流动的过程中对数据进行实时地捕捉和处理,并根据业务需求进行计算分析,最终把结果保存或者分发给需要的组件。
特性 |
Kafka |
ActiveMQ |
RabbitMQ |
RocketMQ |
单机吞吐量 |
10万级别,这是kafka最大的优点,就是吞吐量高。一般配合大数据类的系统来进行实时数据计算、日志采集等场景 |
万级,吞吐量比RocketMQ和Kafka要低了一个数量级 |
万级,吞吐量比RocketMQ和Kafka要低了一个数量级 |
10万级,RocketMQ也是可以支撑高吞吐的一种MQ |
topic数量对吞吐量的影响 |
topic可以达到几百,几千个的级别,吞吐量会有较小幅度的下降这是RocketMQ的一大优势,在同等机器下,可以支撑大量的topic |
|
|
topic可以达到几百,几千个的级别,吞吐量会有较小幅度的下降这是RocketMQ的一大优势,在同等机器下,可以支撑大量的topic |
时效性 |
延迟在ms级以内 |
ms级 |
微秒级,这是rabbitmq的一大特点,延迟是最低的 |
ms级 |
可用性 |
非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
高,基于主从架构实现高可用性 |
高,基于主从架构实现高可用性 |
非常高,分布式架构 |
消息可靠性 |
经过参数优化配置,消息可以做到0丢失 |
有较低的概率丢失数据 |
|
经过参数优化配置,可以做到0丢失 |
功能支持 |
功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 |
MQ领域的功能极其完备 |
基于erlang开发,所以并发能力很强,性能极其好,延时很低 |
MQ功能较为完善,还是分布式的,扩展性好 |
优劣势总结 |
kafka的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展同时kafka最好是支撑较少的topic数量即可,保证其超高吞吐量而且kafka唯一的一点劣势是有可能消息重复消 |
非常成熟,功能强大,在业内大量的公司以及项目中都有应用偶尔会有较低概率丢失消息而且现在社区以及国内应用都越来越少,官方社区现在对ActiveMQ 5.x维护越来越少,几个月才发布一个版本而且确实主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用 |
erlang语言开发,性能极其好,延时很低;吞吐量到万级,MQ功能比较完备而且开源提供的管理界面非常棒,用起来很好用社区相对比较活的。RabbitMQ吞吐量会低一些这是因为他做的实现机制比较重。erlang开发很难去看懂源码,你公司对这个东西的掌控很弱,基本职能依赖于开源社区的快速维护和修复bug。 |
接口简单易用,而且毕竟在阿里大规模应用过,可以做到大规模吞吐,性能也非常好,分布式扩展也很方便,社区维护还可以,可靠性和可用性是ok的,还可以支撑大规模的topic数量。阿里出品都是java系的,我们可以自己阅读源码 |
https://www.jetbrains.com/zh-cn/idea/
https://www.oracle.com/java/technologies/downloads/
https://www.scala-lang.org/download/
https://gradle.org/install/#manually
http://archive.apache.org/dist/kafka/
bin: 启动脚本;
clients:生产者和消费者代码;
config: 配置文件;
core : kafka server端,scala语言开发,实现了集群管理,分区副本管理,消息存储和 消息获取,网络通信等功能;
docs:kakfa文档
examples:生产者消费者demo 启动脚本;
streams:kafka 流相关代码;
jmh-benchmarks:JMH测试;
log4j-appender:日志处理;
tools:工具包;
admin包:执行管理命令的功能;
api 包: 封装请求和响应DTO对象;
cluster包:集群对象,例如Replica 类代表一个分区副本,Partition类代表一个分区;
common包: 通用jar包;
controller包: 和kafkaController(kc)相关的类,重点模块,一个kafka集群只有一 个leader kc,该kc负责 分区管理,副本管理,并保证集群信息在集群中同 步;
coordinator包:组协调者相关,负责处理消费者组的代码;
log包: 磁盘存储相关,重点模块;
network包: 网络相关,重点模块,使用的是NIO,从这里可学习如何应用java 的NIO类;
consumer包,producer好多废弃类,无需关注;
server包: kafka实例的各种管理类,核心包,也是重点;
tools: 工具类
clients 包:生产者producer 和消费者consumer的代码
common 包:常用方法和工具包
server 包:服务端方法接口
Kafka作为分布式发布-订阅消息系统,我们来看下Kafka的核心架构模型:
名称 |
解释 |
Producer |
生产者,消息的产生者,是消息的入口 |
Broker |
kafka 一个实例,kafka cluster表示集群的意思 |
Topic |
消息的主题,kafka的数据就保存在topic |
Partition |
Topic的分区,每个 topic 可以有多个分区 |
Replication |
每一个分区都有多个副本,副本的作用是做备胎 |
Consumer |
消费者,即消息的消费方,是消息的出口 |
Consumer Group |
我们可以将多个消费组组成一个消费者组 |
在kafka中,每个 Broker就是kafka的一个代理节点,简单的来说就是一个kafka服务节点。一般来说,在一个kafka集群中,每个机器上都只部署一个实例,那么,这个机器就可以看作是一个Broker。就这样,多个Broker就组成了一个Kafka集群。Broker 在zookeeper的协调下完成Kafka集群协作的同时,也承担着消息的存储。
作为一个消息队列组件,Producer 与 Consumer 当然是最为重要的两个组成部分,也就是消息的发送方与接收方。如上图所示,Producer 将数据发送到 Broker 上,由 Broker 进行存储,Consumer 则连接到 Broker 上进行拉取和消费数据。多个 Consumer 可以组成一个消费组来统一进行管理。
Kafka中,消息以Topic为单位进行归类,Producer 将消息发送到特定的Topic上,而Consumer则在启动时需要订阅某个主题并进行消费。
Topic是由若干个分区组成的,每个分区都只能属于单个的主题,事实上,Topic 只是逻辑上的概念,而分区才是Topic借以实现的实体。
每个分区可以看作是一个可追加的日志文件,每个消息都拥有自己在分区中的偏移量offset,而在每个分区中,消息的 offset 就成为了消息的唯一标识,依赖offset,kafka 实现了在单个分区内的消息有序性,可以理解,一个分区中若干条消息的消费是按照消息的offset有序的,而在一个Topic中,消息的消费是无序的。
Topic是一个逻辑概念,因此他可以跨越多个Broker,但每个分区则只能存在于某个Broker上,被一个Consumer Group内的一个Consumer 进行消费,但反之,一个 Consumer 是可以同时消费多个分区的。
每个Broker上可以拥有很多个分区,每当一条消息被发送到broker之前,都会根据分区规则选择存储到哪个具体的分区,最为合理的规则是让消息可以均匀的分配到不同的分区中,这样,多个机器共同承担一个Topic的读写,同一个机器上,多个分区log 文件同时承担他们所对应的Topic的读写,就可以让整个集群的IO性能大幅提升。
如同其他许多存储系统,多副本备份也是Kafka重要的特性,每个leader副本分区都有若干个follower副本分区分布在其他Broker上。
当某个Broker发生故障,Kafka基于“一主多从”的副本同步机制,可以轻松的将某个follower副本分区作为leader分区,实现故障的自动转移和容灾,从而保障整个集群的高可用状态。
Kafka的Producer发送消息采用的是异步发送的方式。在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程,以及一个线程共享变量——RecordAccumulator。 main 线程将消息发送给 RecordAccumulator,Sender 线程不断从 RecordAccumulator 中拉取 消息发送到 Kafka broker。
发送数据先经常拦截器、序列化器、分区器(默认轮询分区)。
batch.size:只有数据积累到 batch.size 之后,sender 才会发送数据。
linger.ms:如果数据迟迟未达到 batch.size,sender 等待 linger.time 之后就会发送数据
kafka采用pull(拉)模式,consumer从broker中拉取数据
pull模式的不足:如果Kafka没有数据,消费者可能会陷入循环中,一直返回空数据
kafka不采用push(推)模式,因为由broker决定消息发送速率,很难适应所有消费者的消费速率
注意两点:
1.每个分区的数据只能由消费者组中一个消费者消费
2.一个消费者可以消费多个分区数据
消费者组,由多个consumer组成。形成一个消费者组的条件,是所有消费者的groupid相同。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费。消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。如果向消费组中添加更多的消费者,超过主题分区数量,则有一部分消费者就会闲置,不会接收任何消息。
磁盘大多数都还是机械结构(SSD不在讨论的范围内),如果将消息以随机写的方式存入磁盘,就需要按柱面、磁头、扇区的方式寻址,缓慢的机械运动(相对内存)会消耗大量时间,导致磁盘的写入速度与内存写入速度差好几个数量级。
为了规避随机写带来的时间消耗,Kafka 采取了顺序写的方式存储数据,每条消息都被append 到 partition 中,属于顺序写磁盘,因此效率非常高。这种设计所带的的缺陷就是Kafka是不会删除数据,所以Kafka采用消费端记录读写位移(offset )的形式来记录对topic的读的记录。对于这个offset的话,Kafka是完全无视,这个状态完全由消费端SDK去维护,SDK将这个值保存在zookper上。当然如果不删除消息,硬盘肯定会被撑满,所以 Kakfa 提供了两种策略来删除数据。一是基于时间,二是基于 partition 文件大小,具体配置可以参看它的配置文档。即使是顺序写,过于频繁的大量小 I/O 操作一样会造成磁盘的瓶颈,所以 Kakfa 在此处的处理是把这些消息集合在一起批量发送,这样减少对磁盘 I/O 的过度操作,而不是一次发送单个消息。
每个分区写入过程没有带offset,这种append-only的写法保证了顺序写入,一定程度降低磁盘负载(避免随机写操带来的频繁磁盘寻道问题)。
零拷贝并不是不需要拷贝,而是减少不必要的拷贝次数。通常是说在IO 读写过程中。一般的文件传输,数据会经历四次复制。
第一次,程序调用read(),将数据从磁盘拷贝到内核模式的ReadBuffer中,
第二次,CPU控制将ReadBuffer中的数据拷贝到用户模式的Application Cache中,
第三次,程序调用write(),将Application Cache下的内容复制到内核模式下的Socket Buffer中,
第四次,Cpu控制将Socket Buffer中的数据拷贝到网卡设备中传输。
相应的用户态和内核态的切换也发生了四次,第一次从用户态切换到内核态,调用Read()。第二次从内核态切换回用户态,将数据拷贝到Application Cache。第三次,从用户态切换到内核态,数据从Application Cache 中拷贝到SocketBuffer。第四次,数据全程拷贝完毕,从内核态又切换会用户态。即数据全程发生了四次复制,四次用户态和内核态的切换。为了优化这个流程,kafka使用了“零拷贝技术”。
“零拷贝技术”只用将磁盘文件的数据复制到页面缓冲区一次,然后将数据从页面的缓存直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面进行缓存),避免了重复复制操作。
如果有10个消费者,传统方式下,数据复制次数为4 * 10 = 40 次,而是用零拷贝技术只需要复制1 + 10 = 11 次,其中一次为从磁盘复制到页面缓存,10 次表示10个消费者各自读取一次页面缓存,由此可以看出kafka的效率是非常高的。
Producer发消息到Broker时候可能丢消息,这个时候如果不想丢消息,需要选择带有callBack的api进行发送,这样如果发送成功了,会回调告诉我们已经发送成功了,如果失败了,收到回调结果后自己在业务上重试就好了。回调方法设置好acks,retries,factor这些参数就好了。
消息发送到Broker后,Broker之间的同步也是有可能丢消息的,就是集群中发消息的Broker发完之后就挂了,这样还没来得及将消息同步给别的Broker,数据自然会丢。
还有一个过程就是Broker刷盘的过程,也可能导致数据会丢,Broker把数据存储到磁盘之前,走的是操作系统缓存,这个异步刷盘的过程也可能导致数据会丢。
实际生产过程中,最常见的还是消费端消费消息的时候,这个时候不能使用autoCommit,要使用手动提交。这里我们的方法是要要给每个消息建立一个映射关系,消费端从kafka拉取数据的时候批量拉取(比如一次30条或者50条),然后为每条拉取的消息分配一个msgId,将msgId存入内存队列sortSet中,使用Map存储msgId与msg(有offset)的映射关系,当消息处理完返回ack时,获取当前处理消息的msgId,然后从sortSet删除该msgId表示该消息已经处理过,接着与sortSet队列头部的第一个Id比较(就是最小的msgId),如果当前msgId<= sortSet第一个Id,则提交当前offset,即便系统挂了,在下次重启时就会从sortSet队首的消息开始拉取,实现至少一次处理,这样做会有少量重复消费的情况,只需要在下游保证幂等就可以了。
对于消费书顺序问题,我们一方面可以利用kafka本身的特性,就是把一定顺序的消息往同一个partition里面放就可以保证消费顺序。
还有就是可以通过数据库冗余字段的方式去处理,就是单独用一个或者多个字段来标记处理状态,消息来的时候只更新对应的状态就可以了,这样会保证消息的最终一致性。
最后我们也可以通过消息补偿机制,用另一个进行消费相同的topic的数据,消息落盘,延迟处理。将消息与DB进行对比,如果发现数据不一致,重新发送消息到主进程处理。
1.提高生产者吞吐量
(1)buffer.memory:发送消息的缓冲区大小,默认值是32m,可以增加到64m
(2)batch.size:默认是16k。如果batch设置太小,会导致频繁网络请求,吞吐量下降;如果batch太大,会导致一条消息需要等待很久才能被发送出去,增加网络延时
(3)linger.ms:这个值默认是0,意思是消息立即被发送。一般设置一个5-100ms。如果linger.ms设置的太小,会导致频繁网络请求,吞吐量下降;如果linger.ms太长,会导致一条消息需要等待很久才能被发送出去,增加网络延时
(4)compression.type:默认是none,不压缩,但是也可以使用lz4压缩,效率还是不错的,压缩之后可以减小数据量,提升吞吐量,但是会加大producer的CPU开销
2.提高消费者吞吐量
(1)如果是kafka消费能力不足,可以考虑增加下topic分区数,并且同时提升消费者组的消费者数量,消费者数=分区数(两者缺一不可)
(2)如果是下游数据处理不及时:提高每批次拉取的数量。拉取数据过少(拉取数据/处理时间<生产速度),使处理的数据小于生产的数据,也会造成数据积压