在过去的几年里,软件架构领域发生了巨大的变化。人们不再认为所有的系统都应该共享一个数据库。微服务、事件驱动架构和CQRS(命令查询的责任分离 Command Query Responsibility Segregation)是构建当代业务应用程序的主要工具。除此以外,物联网、移动设备和可穿戴设备的普及,进一步对系统的近实时能力提出了挑战。
首先让我们对“快”这个词达成共识,这个词是多方面的、复杂的、高度模糊的。一种解释是把”延迟、吞吐量和抖动“作为对“快”的衡量指标。还有,比如工业应用领域,行业本身设置了对于“快”的规范和期望。所以,“快”在很大程度上取决于你的参照体系是什么。
Apache Kafka以牺牲延迟和抖动为代价优化了吞吐量,但并没有牺牲,比如持久性、严格的记录有序性和至少一次的分发语义。当有人说“Kafka速度很快”,并假设他们至少有一定的能力时,你可以认为他们指的是Kafka在短时间内分发大量记录的能力。
Kafka诞生于LinkedIn,当时LinkedIn需要高效地传递大量信息,相当于每小时传输数TB的数据量。在当时,消息传播的延迟被认为是可以接受的。毕竟,LinkedIn不是一家从事高频交易的金融机构,也不是一个在确定期限内运行的工业控制系统。Kafka可用于近实时系统。
注意:“实时”并不意味着“快”,它的意思是“可预测的”。具体来说,实时意味着完成一个动作具有时间限制,也就是最后期限。如果一个系统不能满足这个要求,它就不能被归类为”实时系统“。能够容忍一定范围内延迟的系统被称为“近实时”系统。从吞吐量的角度来说,实时系统通常比近实时或非实时系统要慢。
Kafka在速度上有两个重要的方面,需要单独讨论。第一个是与客户端与服务端之间的低效率实现有关。第二个源自于流处理的并行性。
服务端优化
日志的存储
Kafka利用分段、追加日志的方式,在很大程度上将读写限制为顺序I/O(sequential I/O),这在大多数的存储介质上都很快。人们普遍错误地认为硬盘很慢。然而,存储介质的性能,很大程度上依赖于数据被访问的模式。同样在一块普通的7200 RPM SATA硬盘上,随机I/O(random I/O)与顺序I/O相比,随机I/O的性能要比顺序I/O慢3到4个数量级。此外,现代的操作系统提供了预先读和延迟写的技术,这些技术可以以块为单位,预先读取大量数据,并将较小的逻辑写操作合并成较大的物理写操作。因此,顺序I/O和随机I/O之间的性能差异在闪存和其他固态非易失性介质中仍然很明显,不过它们在旋转存储,比如固态硬盘中的性能差异就没有那么明显。
记录的批处理
顺序I/O在大多数存储介质上都非常快,可以与网络I/O的最高性能相媲美。在实践中,这意味着一个设计良好的日志持久化层能跟上网络的读写速度。事实上,Kafka的性能瓶颈通常并不在硬盘上,而是网络。因此,除了操作系统提供的批处理外,Kafka的客户端和服务端会在一个批处理中积累多个记录——包括读写记录,然后在通过网络发送出去。记录的批处理可以缓解网络往返的开销,使用更大的数据包,提高带宽的效率。
批量压缩
当启用压缩时,对批处理的影响特别明显,因为随着数据大小的增加,压缩通常会变得更有效。特别是在使用基于文本的格式时,比如JSON,压缩的效果会非常明显,压缩比通常在5x到7x之间。此外,记录的批处理主要作为一个客户端操作,负载在传递的过程中,不仅对网络带宽有积极影响,而且对服务端的磁盘I/O利用率也有积极影响。
便宜的消费者
不同于传统的消息队列模型,当消息被消费时会删除消息(会导致随机I/O),Kafka不会在消息被消费后删除它们——相反,它会独立地跟踪每个消费者组的偏移量。可以参考Kafka的内部主题__consumer_offsets了解更多。同样,由于只是追加操作,所以速度很快。消息的大小在后台被进一步减少(使用Kafka的压缩特性),只保留任何给定消费者组的最后已知偏移量。
将此模型与传统的消息模型进行对比,后者通常提供几种不同的消息分发拓扑。一种是消息队列——用于点对点消息传递的持久化传输,没有点对多点功能。另一种是发布订阅主题允许点对多点消息通信,但这样做的代价是持久性。在传统消息队列模型中实现持久化的点对多点消息通信模型需要为每个有状态的使用者维护专用消息队列。这将放大读写的消耗。消息生产者被迫将消息写入多个消息队列中。另外一种选择是使用扇出中继,扇出中继可以消费来自一个队列中的记录,并将记录写入其他多个队列中,但这只会将延迟放大点。并且,一些消费者正在服务端上生成负载——读和写I/O的混合,既有顺序的,也有随机的。
Kafka中的消费者是“便宜的”,只要他们不改变日志文件(只有生产者或Kafka的内部进程被允许这样做)。这意味着大量消费者可以并发地从同一主题读取数据,而不会使集群崩溃。添加一个消费者仍然有一些成本,但主要是顺序读取夹杂很少的顺序写入。因此,在一个多样化的消费者系统中,看到一个主题被共享是相当正常的。
未刷新的缓冲写操作
Kafka性能的另一个基本原因是,一个值得进一步研究的原因:Kafka在确认写操作之前并没有调用fsync。ACK的唯一要求是记录已经写入I/O缓冲区。这是一个鲜为人知的事实,但却是一个至关重要的事实。实际上,这就是Kafka的执行方式,就好像它是一个内存队列一样——Kafka实际上是一个由磁盘支持的内存队列(受缓冲区/页面缓存大小的限制)。
但是,这种形式的写入是不安全的,因为副本的出错可能导致数据丢失,即使记录似乎已经被ACK。换句话说,与关系型数据库不同,仅写入缓冲区并不意味着持久性。保证Kafka持久性的是运行几个同步的副本。即使其中一个出错了,其他的(假设不止一个)将继续运行——假设出错的原因不会导致其他的副本也出错。因此,无fsync的非阻塞I/O方法和冗余的同步副本组合为Kafka提供了高吞吐、持久性和可用性。
领取专属 10元无门槛券
私享最新 技术干货