消息引擎系统
需要考虑的两个重要因素:
- 消息设计
- 传输协议设计
消息设计
一天消息要有能够完整清晰表达业务的能力。
同时,为了更好地表达语义以及最大限度地提高重用性,消息通常都采用结构化的方式进行设计。比如SOAP协议中的消息就采用了XML格式,而Web Service也支持JSON格式的消息。
传输协议设计
消息传输协议指定了消息在不同系统之间传输的方式。目前主流的协议包括AMQP、Web Service+SOAP以及微软的MSMQ等。从广义的角度来说,这类协议可能包括任何能够在不同系统间传输消息或是执行语义操作的协议或框架。比如现在主流的RPC及序列化框架,包括Google的Protocol Buffers(简称Google PB,值得注意的是,虽然Google并没有开源PB的RPC框架部分,但其依然是一款非常优秀的序列化框架)、阿里系的Dubbo等。
kafka自己设计了一套二进制消息传输协议
消息引擎范型
消息引擎范型是一个基于网络的架构范型,描述了消息引擎系统的两个不同的子部分是如何互连且交互的
常见的消息引擎范型:
- 消息队列模型
- 发布/订阅模型
消息队列模型
是基于队列提供消息传输服务的,多用于进程间通信(IPC)以及线程间通信。
该模型定义了消息队列(queue)、发送者(sender)和接收者(receiver),提供了一种点对点(point-to-point,p2p)的消息传递方式,即发送者发送每条消息到队列的指定位置,接收者从指定位置获取消息。一旦消息被消费(consumed),就会从队列中移除该消息。每条消息由一个发送者生产出来,且只被一个消费者(consumer)处理——发送者和消费者之间是一对一的关系
发布/订阅模型
有主题(topic)的概念。一个topic可以理解为逻辑语义相近的消息的容器。
这种模型也定义了类似于生产者/消费者这样的角色,即发布者(publisher)和订阅者(subscriber)。发布者将消息生产出来发送到指定的topic中,所有订阅了该topic的订阅者都可以接收到该topic下的所有消息。
kafka引入消息组的概念来同时支持两种模型
消息服务
简称JMS,它只是一套API规范,提供了很多接口用于实现分布式系统间的消息传递。JMS同时支持上面两种消息引擎模型。实际上,当前很多主流的消息引擎系统都完全支持JMS规范,比如ActiveMQ、RabbitMQ(通过RabbitMQ JMS Client)、IBM WebSphere MQ和Kafka等。
Kafka并没有完全遵照JMS规范
kafka概要设计
为了解决超大集数据的实时传输
在设计支出就需要考虑:
- 吞吐量/延时
- 消息持久化
- 负载均衡和故障转移
- 伸缩性
吞吐量/延时
对于Kafka而言,它的吞吐量就是每秒能够处理的消息数或者每秒能够处理的字节数。
对于Kafka而言,延时可以表示客户端发起请求与服务器处理请求并发送响应给客户端之间的这一段时间。显而易见,延时间隔越短越好。
在实际使用场景中,这两个指标通常是一对矛盾体,即调优其中一个指标通常会使另一个指标变差。当然它们之间的关系也不是等比例地此消彼长的关系,比如牺牲20%的延时就能换来20%的吞吐量的提升。其实关于这一点,很多读者可能会感到困惑:为什么需要同时研究这两个指标呢?如果确定了其中的一个指标,另一个指标也应该是确定的才对啊?好吧,按照这种假设,若Kafka处理一条消息需要花费2毫秒,那么计算得到的吞吐量不会超过500条消息/秒(1000/2=500)。
但是若我们采用批处理(batching)的思想,还是使用上面的例子,情况就可以大不一样了。这一次我们不是一条一条地发送消息,而是一小批一小批(micro-batch)地发送。假设在发送前我们首先会等待一段时间(假设是8毫秒),那么此时消息发送的延时变成了10毫秒(2+8),即延时增加了4倍,但假设在这8毫秒中我们总共累积了1000条消息,那么系统整体的吞吐量就变成了100000条/秒(1000/0.01=100000),吞吐量提升了近200倍!各位读者,看到micro-batch的威力了吧?这也就是目前诸如Storm Trident和Spark Streaming等消息处理平台所采用的处理语义思路。
kafka如何做到高吞吐,低延时?
一、Kafka的写入操作是很快的,这主要得益于它对磁盘的使用方法的不同。虽然Kafka会持久化所有数据到磁盘,但本质上每次写入操作其实都只是把数据写入到操作系统的页缓存(page cache)中,然后由操作系统自行决定什么时候把页缓存中的数据写回磁盘上。这样的设计有3个主要优势。
- 操作系统页缓存是在内存中分配的,所以消息写入非常快
- Kafka不必直接与底层的文件系统打交道。所有烦琐的I/O操作都交由操作系统来处理。
- Kafka写入操作采用追加写入(append)的方式,避免了磁盘随机写操作。
对于普通的物理磁盘(非固态硬盘)而言,我们总是认为磁盘的读/写操作是很慢的。事实上普通SAS磁盘随机读/写的吞吐量的确是很慢的,但是磁盘的顺序读/写操作其实是非常快的,它的速度甚至可以匹敌内存的随机I/O速度,鉴于这一事实,Kafka在设计时采用了追加写入消息的方式,即只能在日志文件末尾追加写入新的消息,且不允许修改已写入的消息,因此它属于典型的磁盘顺序访问型操作,所以Kafka消息发送的吞吐量是很高的。在实际使用过程中可以很轻松地做到每秒写入几万甚至几十万条消息。
二、之前提到了Kafka是把消息写入操作系统的页缓存中的。那么同样地,Kafka在读取消息时会首先尝试从OS的页缓存中读取,如果命中便把消息经页缓存直接发送到网络的Socket上。
这就是利用Linux平台的sendfile系统调用做到的,而这种技术就是大名鼎鼎的零拷贝(Zero Copy)技术。
Linux提供的sendfile系统调用实现了这种零拷贝技术,而Kafka的消息消费机制使用的就是sendfile——严格来说是通过Java的FileChannel.transferTo方法实现的
三、除了零拷贝技术,Kafka由于大量使用页缓存,故读取消息时大部分消息很有可能依然保存在页缓存中,因此可以直接命中缓存,不用“穿透”到底层的物理磁盘上获取消息,从而极大地提升了消息读取的吞吐量。事实上,如果我们监控一个经过良好调优的Kafka生产集群便可以发现,即使是那些有负载的Kafka服务器,其磁盘的读操作也很少,这是因为大部分的消息读取操作会直接命中页缓存。
总结一下,Kafka就是依靠下列4点达到了高吞吐量、低延时的设计目标的。
- 大量使用操作系统页缓存,内存操作速度快且命中率高。
- Kafka不直接参与物理I/O操作,而是交由最擅长此事的操作系统来完成。
- 采用追加写入方式,摒弃了缓慢的磁盘随机读/写操作。
- 使用以sendfile为代表的零拷贝技术加强网络间的数据传输效率。
消息持久化
Kafka是要持久化消息的,而且要把消息持久化到磁盘上。这样做的好处如下。
- 解耦消息发送与消息消费
kafka最核心得功能是提供了生产者-消费者模式的完成解决方案。通过将消息持久化使得生产者方不再需要直接和消费者方耦合,它只是简单地把消息生产出来并交由kafka服务器保存即可,因此提升了整体的吞吐量
- 实现灵活的消息处理
很多kafka的下游子系统(接收kafka消息的系统)都有这样的需求-对于已经处理过的消息可能在未来的某个时间点重新处理一次,几消息重演(message replay)。消息持久化便可以很方便的实现这样的需求。
kafka实现持久化的设计新颖,普通系统在实现持久化时可能会先尽量使用内存,当内存资源耗尽时,再一次性地把数据“刷盘”;而kafka则反其道而行之,所有数据都会立即被写入文件系统的持久化日志中,之后kafka服务器才会返回结果给客户端通知他们消息已被成功写入。这样做既实时的保存了数据,又减少了kafka程序对于内存的消耗,从而将节省出的内存留给也缓存使用,更进一步提升了整体性能。
负载均衡和故障转移
作为一个功能完备的分布式系统,kafka如果只提供了最基本的消息引擎功能肯定不足以帮助它脱颖而出。
一套完整的消息引擎解决方案中必然要提供负载均衡和故障转移功能
负载均衡?
让系统的负载根据一定的规则均衡地分配在所有参与工作的服务骑上,从而最大限度的提升系统整体的运行效率。
具体到kafka,默认情况下kafka的每台服务器都有均等的机会为kafka的客户提供服务,可以把负载分散到所有集群中的机器上,避免出现“耗尽某台服务器”的情况发生
kafka实现负载均衡实际上是通过智能化的分区领导者选举(partition leader election)来实现的。
故障转移?
当服务器意外终止时,整个集群可以快速的检测到该失效,并立即将服务器上的应用或服务自动转移到其他服务器上。故障专业通常是以“心跳”或“会话”的机制来实现的,即只要主服务器与备服务器之间的心跳无法维持或主服务器注册到服务中心的超时过期了,那么就认为主服务器已经无法正常运行,集群会自动启动某个备份服务器来替代主服务器的工作。
kafka支持故障转移的方式就是使用会话机制,每台kafka服务器启动后会以会话的形式把自己注册到Zookeeper服务器上。一旦该服务器运转出现问题,与Zookeeper的会话便不能维持从而超时失效,此时kafka集群会选举另一台服务器来完全代替这台服务器继续提供服务
伸缩性
有了消息的持久化,kafka实现了高可靠性,有了负载均衡和使用文件系统的独特设计,kafka实现了高吞吐量;有了故障转移,kafka实现了高可用性。
伸缩性?
表示向分布式系统中增加额外的计算资源(如CPU、内存、存储或带宽)时吞吐量提升的能力。
kafka每台服务器上的状态统一交由Zookeeper保管。扩展kafka集群也只需要一步:启动新的kafka服务器即可。
在kafka服务器上并不是所有状态都不保存。它只是保存了很轻量级的内部状态,因此在整个集群间维护状态已执行的代价很低。
kafka基本概念与术语
kafka的标准定位是分布式流失处理平台。
kafka核心架构:
- 生产者发送消息给kafka服务器
- 消费者从kafka服务器读取消息
- kafka服务器依托Zookeeper集群进行服务的协调管理。
kafka服务器有一个官方名字:breker
消息
kafka对消息格式的设计与保存有很多创新之处
kafka中的消息格式有很多字段组成,其中的很多字段都是用于管理消息的元数据字段,对于用户来说是完全透明的。kafka消息格式共经历过3次变迁,分别为V0、V1和V2版本。目前大部分用户使用的还是V1版本的消息格式。V1版本消息的完整格式如下图
消息由消息头部、key和value组成。消息头部包括消息的CRC码、消息版本号、属性、时间戳、键长度和消息体长度等信息
- key:对消息做partition时使用,即决定消息被保存在某个topic下的哪个partition
- value:消息体,保存实际的消息数据
- timestamp:消息发送时间戳,用于流式处理及其他依赖时间的处理语义。如果不指定则去当前时间
- 属性:kafka为该字段分配了一个字节,目前只是用了最低的3位用于保存消息的压缩类型,其余5位尚未使用。当前只支持4中压缩类型:0(无压缩)、1(GZIP)、2(Snappy)、3(LZ4)
kafka使用紧凑的二进制字节数组来保存上面这些字段,也就是说没有任何多余的比特位浪费。试想如果我们使用java对象来保存上面的消息格式,假设我们简化定义Message Java类为:
上面的java类实现方式是非常朴素的,基本上就是有一个CRC32校验码、一个版本号、两个压缩相关的字段以及消息键值和消息体组成的。在java内存模型中,对象保存的开销其实相当大,对于小对象而言,通常要话费2倍的空间来保存数据(甚至更糟)。另外,随着堆上数据量越来越大,GC的性能会下降很多,从而整理上拖慢了系统的吞吐量。
尽管JMM会对上面的java类进行优化-重排各个字段在内存的布局以减少内存使用量,但该message类的一个实例仍然需要消耗40个左右的字节,而其中有7字节只是为了补齐只用。更糟的是,运行java的操作系统通
常都默认开起了也缓存机制,也就是说堆上保存的对象很有可能在也缓存中还保留一份,这造成了极大的资源浪费。
因此kafka在消息设计的时候特意避开了繁重的java堆上的内存分配,直接使用紧凑二进制字节数组ByreBuffer而不是独立的对象,因此我们至少能够访问多一倍的可用内存。按照kafka官网的说法,在一台32GB内存的机器上,kafka几乎能用到28~30GB的物理内存,同时还不必担心GC的糟糕性能。如果使用buteBuffer来保存同样的消息,主需要24字节,比起纯java堆得实现少了40%的空间占用。这种设计的好处还包括加入了扩展的可能性。
同时,大量使用页缓存而非堆内存还有一个好处——当出现kafka broker进程崩溃时,堆内存上的数据也一并小时,但也缓存的数据依然存在,下次kafka重启后,可以继续提供服务,不需要再单独“热”缓存了。
topic和partition
每个kafka topic都由若干个partition组成
kafka的partition没有太多的业务含义,它的引入单纯的为了提升系统的吞吐量,因此在创建kafka的时候可以根据集群实际配置设置其具体的partition数,实现整体性能的最大化
offset
kafka中的一条消息其实就是一个
leader和follower
Kafka的replica分为两个角色:领导者(leader)和追随者(follower)。如今这种角色设定几乎完全取代了过去的主备的提法(Master-Slave)。和传统主备系统(比如MySQL)不同的是,在这类leader-follower系统中通常只有leader对外提供服务,follower只是被动地追随leader的状态,保持与leader的同步。follower存在的唯一价值就是充当leader的候补:一旦leader挂掉立即就会有一个追随者被选举成为新的leader接替它的工作。
Kafka保证同一个partition的多个replica一定不会分配在同一台broker上。毕竟如果同一个broker上有同一个partition的多个replica,那么将无法实现备份冗余的效果。
ISR
ISR的全称是in-sync replica,翻译过来就是与leader replica保持同步的replica集合。
Kafka为partition动态维护一个replica集合。该集合中的所有replica保存的消息日志都与leader replica保持同步状态。只有这个集合中的replica才能被选举为leader,也只有该集合中所有replica都接收到了同一条消息,Kafka才会将该消息置于“已提交”状态,即认为这条消息发送成功。回到刚才的问题,Kafka承诺只要这个集合中至少存在一个replica,那些“已提交”状态的消息就不会丢失——记住这句话的两个关键点:①ISR中至少存在一个活着的replica;②“已提交”消息。有些Kafka用户经常抱怨:我向Kafka发送消息失败,然后造成数据丢失。其实这是混淆了Kafka的消息交付承诺(message delivery semantic):Kafka对于没有提交成功的消息不做任何交付保证,它只保证在ISR存活的情况下“已提交”的消息不会丢失。
正常情况下,partition的所有replica(含leader replica)都应该与leader replica保持同步,即所有replica都在ISR中。因为各种各样的原因,一小部分replica开始落后于leader replica的进度。当滞后到一定程度时,Kafka会将这些replica“踢”出ISR。相反地,当这些replica重新“追上”了leader的进度时,那么Kafka会将它们加回到ISR中。这一切都是自动维护的,不需要用户进行人工干预,因而在保证了消息交付语
义的同时还简化了用户的操作成本。
kafma使用场景
Kafka以消息引擎闻名,因此它特别适合处理生产环境中的那些流式数据。
消息传输
Kafka非常适合替代传统的消息总线(message bus)或消息代理(message broker)。传统的这类系统擅长于解耦生产者和消费者以及批量处理消息,而这些特点Kafka都具备。除此之外,Kafka还具有更好的吞吐量特性,其内置的分区机制和副本机制既实现了高性能的消息传输,同时还达到了高可靠性和高容错性。因此Kafka特别适合用于实现一个超大量级消息处理应用。
网站行为日志追踪
Kafka最早就是用于重建用户行为数据追踪系统的。很多网站上的用户操作都会以消息的形式发送到Kafka的某个对应的topic上。这些点击流蕴含了巨大的商业价值,事实上,目前就有很多创业公司使用机器学习或其他实时处理框架来帮助收集并分析用户的点击流数据。鉴于这种点击流数据量是很大的,Kafka超强的吞吐量特性此时就有了用武之地。
审计数据收集
很多企业和组织都需要对关键的操作和运维进行监控和审计。这就需要从各个运维应用程序处实时汇总操作步骤信息进行集中式管理。在这种使用场景下,你会发现Kafka是非常适合的解决方案,它可以便捷地对多路消息进行实时收集,同时由于其持久化的特性,使得后续离线审计成为可能。
日志收集
这可能是Kafka最常见的使用方式了——日志收集汇总解决方案。每个企业都会产生大量的服务日志,这些日志分散在不同的机器上。我们可以使用Kafka对它们进行全量收集,并集中送往下游的分布式存储中(比如HDFS等)。比起其他主流的日志抽取框架(比如Apache Flume),Kafka有更好的性能,而且提供了完备的可靠性解决方案,同时还保持了低延时的特点。
Event Sourcing
Event Sourcing实际上是领域驱动设计(Domain-Driven Design,DDD)的名词,它使用事件序列来表示状态变更,这种思想和Kafka的设计特性不谋而合。还记得吧,Kafka也是用不可变更的消息序列来抽象化表示业务消息的,因此Kafka特别适合作为这种应用的后端存储。
流式处理
自0.10.0.0版本开始,Kafka社区推出了一个全新的流式处理组件Kafka Streams。这标志着Kafka正式进入流式处理框架俱乐部。相比老牌流式处理框架Apache Storm、Apache Samza,或是最近风头正劲的Spark Streaming,抑或是Apache Flink,Kafka Streams的竞争力如何?让我们拭目以待。
下一篇 kafka发展历史