一、消息队列优缺点:
优点:解耦、异步、削峰
缺点:
- 系统可用性降低
- 系统复杂性提高
- 一致性问题
二、常见问题:
1、消息重复问题
消息端重复发送情况:
生产者(消息发送端)发送消息成功进入消息中间件,由于各种原因发送端没有收到消息中间件返回的“成功”结果,而且发送端还存在重试机制;
消息中间件重复:
消息被消费端成功消费后,消息中间件没有收到消息的处理结果;
重复消费解决方式
- MVCC:多版本并发控制,乐观锁的一种实现;生产者发送消息进行数据更新时带上数据的版本号,消费端去验证数据的版本号,版本号不一致则操作无法成功;
- 去重表:数据库表上创建唯一索引;
- 消费端保证操作的幂等性
2、消息的可靠性问题
消费端丢失数据:
消费端消费到消息,然后消费端自动向Kafka提交了Offset,但还没来及处理,消费端由于各种原因出错了;
关闭Kafka自动提交Offset,处理完消息之后,手动提交Offset,可保证数据不会丢失,但此方法会存在重复消费问题,比如刚处理完消息还没来得及提交Offset,消费者挂了;
Kafka丢失数据:
Follower 还没有来得及同步数据,Leader 挂了,然后选举某个 Follower 成 Leader 之后,这就会丢了一些数据。
解决方法:
-
给 Topic 设置 replication.factor 参数:这个值必须大于 1,要求每个 Partition 必须有至少 2 个副本。
-
在 Kafka 服务端设置 min.insync.replicas 参数:这个值必须大于 1,这个是要求一个 Leader 至少感知到有至少一个 Follower 还跟自己保持联系,没掉队,这样才能确保 Leader 挂了还有一个 Follower 吧。
-
在 Producer 端设置 acks=all:要求每条数据,必须是写入所有 Replica 之后,才能认为是写成功了
-
在 Producer 端设置 retries=MAX:一旦写入失败,就无限重试。
生产者丢失数据:
设置 acks=all,基本就不会丢失数据
3、消息持续积压问题
-
修复 Consumer 的问题,确保其恢复消费速度,然后将现有 Consumer 都停掉。
-
新建一个 Topic,Partition 是原来的 10 倍,临时建立好原先 10 倍或者 20 倍的 queue 数量。
然后写一个临时的分发数据的 Consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
-
接着临时征用 10 倍的机器来部署 Consumer,每一批 Consumer 消费一个临时 queue 的数据。
-
这种做法相当于是临时将 queue 资源和 Consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。
-
等快速消费完积压数据之后,再恢复原先部署架构,重新用原先的 Consumer 机器来消费消息。
4、Kafka是如何实现高性能的?
利用 Partition 实现并行处理
Kafka 中每个 Topic 都包含一个或多个 Partition,不同 Partition 可位于不同节点。
同时 Partition 在物理上对应一个本地文件夹,每个 Partition 包含一个或多个 Segment,每个 Segment 包含一个数据文件和一个与之对应的索引文件。
在逻辑上,可以把一个 Partition 当作一个非常长的数组,可通过这个“数组”的索引(Offset)去访问其数据。
一方面,由于不同 Partition 可位于不同机器,因此可以充分利用集群优势,实现机器间的并行处理。
另一方面,由于 Partition 在物理上对应一个文件夹,即使多个 Partition 位于同一个节点,也可通过配置让同一节点上的不同 Partition 置于不同的 disk drive 上,从而实现磁盘间的并行处理,充分发挥多磁盘的优势。
利用多磁盘的具体方法是,将不同磁盘 mount 到不同目录,然后在 server.properties 中,将 log.dirs 设置为多目录(用逗号分隔)。
Kafka 会自动将所有 Partition 尽可能均匀分配到不同目录也即不同目录(也即不同 disk)上。
Partition 是最小并发粒度,Partition 个数决定了可能的最大并行度。
ISR 实现可用性与数据一致性的动态平衡
常用数据复制及一致性方案有如下几种:
Master-Slave:
-
RDBMS 的读写分离即为典型的 Master-Slave 方案。
-
同步复制可保证强一致性但会影响可用性。
-
异步复制可提供高可用性但会降低一致性。
WNR:
-
主要用于去中心化的分布式系统中。
-
N 代表总副本数,W 代表每次写操作要保证的最少写成功的副本数,R 代表每次读至少要读取的副本数。
-
当 W+R>N 时,可保证每次读取的数据至少有一个副本拥有最新的数据。
-
多个写操作的顺序难以保证,可能导致多副本间的写操作顺序不一致。Dynamo 通过向量时钟保证最终一致性。
Paxos 及其变种:
-
Google 的 Chubby,Zookeeper 的原子广播协议(Zab),RAFT 等。
基于 ISR 的数据复制方案:Kafka 的数据复制是以 Partition 为单位的。而多个备份间的数据复制,通过 Follower 向 Leader 拉取数据完成。
从这一点来讲,Kafka 的数据复制方案接近于上文所讲的 Master-Slave 方案。
不同的是,Kafka 既不是完全的同步复制,也不是完全的异步复制,而是基于 ISR 的动态复制方案。
ISR,也即 In-Sync Replica。每个 Partition 的 Leader 都会维护这样一个列表,该列表中,包含了所有与之同步的 Replica(包含 Leader 自己)。
每次数据写入时,只有 ISR 中的所有 Replica 都复制完,Leader 才会将其置为 Commit,它才能被 Consumer 所消费。
这种方案,与同步复制非常接近。但不同的是,这个 ISR 是由 Leader 动态维护的。
如果 Follower 不能紧“跟上”Leader,它将被 Leader 从 ISR 中移除,待它又重新“跟上”Leader 后,会被 Leader 再次加到 ISR 中。每次改变 ISR 后,Leader 都会将最新的 ISR 持久化到 Zookeeper 中。
由于 Leader 可移除不能及时与之同步的 Follower,故与同步复制相比可避免最慢的 Follower 拖慢整体速度,也即 ISR 提高了系统可用性。
ISR 中的所有 Follower 都包含了所有 Commit 过的消息,而只有 Commit 过的消息才会被 Consumer 消费。
故从 Consumer 的角度而言,ISR 中的所有 Replica 都始终处于同步状态,从而与异步复制方案相比提高了数据一致性。
ISR 可动态调整,极限情况下,可以只包含 Leader,极大提高了可容忍的宕机的 Follower 的数量。
与 Majority Quorum 方案相比,容忍相同个数的节点失败,所要求的总节点数少了近一半。
高效使用磁盘特性和操作系统特性
将写磁盘的过程变为顺序写
Kafka的整个设计中,Partition相当于一个非常长的数组,而Broker接收到的所有消息顺序写入这个大数组中。同时Consumer通过Offset顺序消费这些数据,并且不删除已经消费的数据,从而避免了随机写磁盘的过程。
由于磁盘有限,不可能保存所有数据,实际上作为消息系统Kafka也没必要保存所有数据,需要删除旧的数据。而这个删除过程,并非通过使用“读-写”模式去修改文件,而是将Partition分为多个Segment,每个Segment对应一个物理文件,通过删除整个文件的方式去删除Partition内的数据。这种方式清除旧数据的方式,也避免了对文件的随机写操作。
在存储机制上,使用了Log Structured Merge Trees(LSM) 。
注:Log Structured Merge Trees(LSM),谷歌 “BigTable” 的论文,中提出,LSM是当前被用在许多产品的文件结构策略:HBase, Cassandra, LevelDB, SQLite,Kafka。LSM被设计来提供比传统的B+树或者ISAM更好的写操作吞吐量,通过消去随机的本地更新操作来达到这个目标。这个问题的本质还是磁盘随机操作慢,顺序读写快。这二种操作存在巨大的差距,无论是磁盘还是SSD,而且快至少三个数量级。
充分利用Page Cache
使用Page Cache的好处如下
- I/O Scheduler会将连续的小块写组装成大块的物理写从而提高性能
- I/O Scheduler会尝试将一些写操作重新按顺序排好,从而减少磁盘头的移动时间
- 充分利用所有空闲内存(非JVM内存)。如果使用应用层Cache(即JVM堆内存),会增加GC负担
- 读操作可直接在Page Cache内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过Page Cache)交换数据
- 如果进程重启,JVM内的Cache会失效,但Page Cache仍然可用
Broker收到数据后,写磁盘时只是将数据写入Page Cache,并不保证数据一定完全写入磁盘。从这一点看,可能会造成机器宕机时,Page Cache内的数据未写入磁盘从而造成数据丢失。但是这种丢失只发生在机器断电等造成操作系统不工作的场景,而这种场景完全可以由Kafka层面的Replication机制去解决。如果为了保证这种情况下数据不丢失而强制将Page Cache中的数据Flush到磁盘,反而会降低性能。也正因如此,Kafka虽然提供了flush.messages和flush.ms两个参数将Page Cache中的数据强制Flush到磁盘,但是Kafka并不建议使用。
如果数据消费速度与生产速度相当,甚至不需要通过物理磁盘交换数据,而是直接通过Page Cache交换数据。同时,Follower从Leader Fetch数据时,也可通过Page Cache完成。
注:Page Cache,又称pcache,其中文名称为页高速缓冲存储器,简称页高缓。page cache的大小为一页,通常为4K。在linux读写文件时,它用于缓存文件的逻辑内容,从而加快对磁盘上映像和数据的访问。 是Linux操作系统的一个特色。
支持多Disk Drive
Broker的log.dirs配置项,允许配置多个文件夹。如果机器上有多个Disk Drive,可将不同的Disk挂载到不同的目录,然后将这些目录都配置到log.dirs里。Kafka会尽可能将不同的Partition分配到不同的目录,也即不同的Disk上,从而充分利用了多Disk的优势。
零拷贝
Kafka中存在大量的网络数据持久化到磁盘(Producer到Broker)和磁盘文件通过网络发送(Broker到Consumer)的过程。这一过程的性能直接影响Kafka的整体吞吐量。
传统模式下的四次拷贝与四次上下文切换
以将磁盘文件通过网络发送为例。传统模式下,一般使用如下伪代码所示的方法先将文件数据读入内存,然后通过Socket将内存中的数据发送出去。
buffer = File.readSocket.send(buffer)
这一过程实际上发生了四次数据拷贝。首先通过系统调用将文件数据读入到内核态Buffer(DMA拷贝),然后应用程序将内存态Buffer数据读入到用户态Buffer(CPU拷贝),接着用户程序通过Socket发送数据时将用户态Buffer数据拷贝到内核态Buffer(CPU拷贝),最后通过DMA拷贝将数据拷贝到NIC Buffer。同时,还伴随着四次上下文切换。
而Linux 2.4+内核通过sendfile系统调用,提供了零拷贝。数据通过DMA拷贝到内核态Buffer后,直接通过DMA拷贝到NIC Buffer,无需CPU拷贝。这也是零拷贝这一说法的来源。除了减少数据拷贝外,因为整个读文件-网络发送由一个sendfile调用完成,整个过程只有两次上下文切换,因此大大提高了性能。
从具体实现来看,Kafka的数据传输通过Java NIO的FileChannel的transferTo和transferFrom方法实现零拷贝。
注: transferTo和transferFrom并不保证一定能使用零拷贝。实际上是否能使用零拷贝与操作系统相关,如果操作系统提供sendfile这样的零拷贝系统调用,则这两个方法会通过这样的系统调用充分利用零拷贝的优势,否则并不能通过这两个方法本身实现零拷贝。
减少网络开销批处理
批处理是一种常用的用于提高I/O性能的方式。对Kafka而言,批处理既减少了网络传输的Overhead,又提高了写磁盘的效率。
Kafka 的send方法并非立即将消息发送出去,而是通过batch.size和linger.ms控制实际发送频率,从而实现批量发送。
由于每次网络传输,除了传输消息本身以外,还要传输非常多的网络协议本身的一些内容(称为Overhead),所以将多条消息合并到一起传输,可有效减少网络传输的Overhead,进而提高了传输效率。
数据压缩降低网络负载
Kafka从0.7开始,即支持将数据压缩后再传输给Broker。除了可以将每条消息单独压缩然后传输外,Kafka还支持在批量发送时,将整个Batch的消息一起压缩后传输。数据压缩的一个基本原理是,重复数据越多压缩效果越好。因此将整个Batch的数据一起压缩能更大幅度减小数据量,从而更大程度提高网络传输效率。
Broker接收消息后,并不直接解压缩,而是直接将消息以压缩后的形式持久化到磁盘。Consumer Fetch到数据后再解压缩。因此Kafka的压缩不仅减少了Producer到Broker的网络传输负载,同时也降低了Broker磁盘操作的负载,也降低了Consumer与Broker间的网络传输量,从而极大得提高了传输效率,提高了吞吐量。
高效的序列化方式
Kafka消息的Key和Payload(或者说Value)的类型可自定义,只需同时提供相应的序列化器和反序列化器即可。因此用户可以通过使用快速且紧凑的序列化-反序列化方式(如Avro,Protocal Buffer)来减少实际网络传输和磁盘存储的数据规模,从而提高吞吐率。这里要注意,如果使用的序列化方法太慢,即使压缩比非常高,最终的效率也不一定高。