目录
0.MQ三大作用
- 异步——被调用接口并未执行真正的操作,接口RT(响应时间)低,可以提高接口的吞吐能力
- 削峰——即使有大批量的接口调用请求也不会让系统受影响
- 解耦——生产者不需要调用消费者的接口,不直接和消费者产生关系,只需要把消息放到队列中
1.核心概念
broker
——消息服务器,是一个实际进程(每个broker都相当于一个JVM进程),每个broker都是由zk管理的kafka节点,多个broker就组成了kafka集群,producer产生的消息向这里写,consumer消费的消息从这里取
1.有多个broker时会通过zk选举出一个主broker,称为controller,它负责追踪集群中的其他broker,处理新加入的和失败的broker节点,重新分配分区等等管理协调集群的工作
2.producer想将数据发送到partition中就得知道partition的信息
老版本中producer是从zk中获取partition的信息
新版本是从配置的broker连接中获取,即为了不让zk成为负担
topic
——消息的逻辑分类,一般以业务模块来做区分,由于不同的业务模块有不同的消息类型,且对应的消费者和生产者也不是同一批,为了在同一个系统中对不同类型消息做区分,就有了topic这个概念。
因此,在kafka内部规定,每条消息都只属于一个topic
生产者发送消息时,必须指定topic
消费者消费消息时,也必须指定topic
partition
——是topic中的物理分区。一个topic中可以有多个partition。其作用是可以通过多分区来实现消息的负载均衡,来提高kafka的吞吐量
kafka默认规定消息写入分区是轮询的形式,即如果有
1
2
3
4
5
6
7
8
8条消息,写入A、B 2个分区中,那么A分区上会被写入1,3,5,7 B分区会被写入2,4,6,8·
注意:在同一个Group中,分区和消费者的关系只能是1对1,N对1,而不能是1对N
因为会破坏有序性(即如果分区的消息中包含两个消费者需要的消息,还需要通过维护一个offset来区分两段消息,这样当然可以实现,但是会降低性能)
consumer group
- 每个组内的消费者只能消费同一个topic中不同分区的数据,而无法消费同一个分区中的数据
所以,一定程度上说,这就是一种消费的负载均衡
-
消费者组是逻辑上的消息订阅者
-
各个消费者组的消费是互不影响的,即不同组内的消费者可以消费同一个分区数据
offset
对于生产者来说,就是消费写入的位置,某批消息写入到某个位置,下次有新消息就在这个offset上追加
对于消费者来说,offset记录了消费消息的位置,offset记录到哪里,下次消费就从哪里开始消费
offset是在运行时维护在内存中的,维护在内存中的数据都会有持久化的问题
老版本中,offset是维护在zk中的
新版本中,kafka自身维护了一个topic,offset的信息会保存在这个topic的partition中
kafka升级的内容中,一个重要的思想就是将原本保存在zk上的kafka信息往外迁移
遵循zk是调度工具,而不是存储的思想
replica
消息的副本,为了提高可用性,可以将分区副本放在别的broker上,这个从分区就叫做replica
2.常见问题
1.消息有序性保证
- 生产者->Partition
全局有序:由于生产者向partition中写入消息是分散在不同的partition中的,所以要想保证全局有序,就只能让一个topic中只有1个partition
局部有序:但是这样就浪费了kafka的性能,因此一般情况下,采取的都是局部有序的策略。即通过指定partition的key,让相同的key上的value值是有序的。
kafka会根据key来进行hash计算,根据计算结果来决定该把消息放入哪个partition中,因此,同一个key对应的value会去到同一个partition中。这样就能在有多个partition的同时保证消息的有序性了
- Partition->消费者
kafka中规定了同个消费者组内的消费者不能消费同一分区的数据,通过这一点保证了消费者端的消费消息时的一致性
2.消息丢失问题
1.发送丢失(即可能存在的producer->broker,broker->broker,broker->磁盘)
解决方式:使用带有callback的api
来发送消息,设置相关参数( acks、retries)来保证消息不丢
- acks——消息发送时服务端上的节点同步数,可以设为0,1,-1·
0指不需要同步确认(异步)
1指只需要leader的分区确认(半同步)
-1/all 指需要包括follower在内的所有分区确认(全同步)
一般业务配置为1,如果是安全或者风控需要消息健壮性的需要设置为-1
1强调高吞吐,-1强调消息的可靠性
- retries
重试次数,当acks为0时该项不生效(即你都不知道消息是否成功发出了,那怎么知道是否需要重试呢)
且如果要使用该项配置,需在消费端保证业务幂等性
2.接收丢失
但其实一般来说,还是消费消息的时候丢失更多一些
消费者想要不丢失消息,就不要自动提交,而应该选择手动提交
我们是这么做的
1.从kafka拉取消息时,为每条消息分配一个递增的唯一ID(msgID)
2.将这个唯一ID存入内存队列中(sortSet),同时以唯一ID为key,以消息信息为value,存入Map中
3.在消息消费完成后,ack时,获取消息的唯一ID,从sortSet中删除该ID
4.同时校验当前消息的唯一ID是否<=队列中的首个ID(即最小ID),如果小于,则提交当前OFFSET到kafka
5.这样,如果系统挂了,重启后会从sortSet中的首个元素开始拉取,保证每个消息至少被处理一次
6.剩下的问题就是可能会有少量的消息重复,需要下游去做好幂等
3.重复消费
Q:如何保证消息不被重复消费,即如何实现幂等性?
幂等性:多次请求获取到的结果和一次请求一致
如果后续操作是针对DB,那么在插入之前先查询一下是否存在该id,如果存在,就更新;
如果是针对redis,那么使用setnx的api,可以做到天然的幂等(项目中的场景)
如果再复杂一点的场景,可以在生产者发送数据时加一个全局唯一id,并将其作为key存入redis中,消费时先去redis中查询一下,如果没消费过,就继续处理,如果消费过,就不处理了
总结
消息的重复可以用幂等来解决,但是不管怎么样,消息的不丢失是无法保证的
3.其他
1.AKF
前置知识
AKF是系统可伸缩问题的一套系统方法论,他用一个scala cube立方体定义了系统拓展的三个维度
x轴:水平复制拓展(比如提升应用性能,就直接在多台服务器上部署同一个应用,用Nginx来做负载均衡)
y轴:功能拆分拓展(比如按业务模块来拆分系统,让每个业务模块有一套自己对应的DB)
z轴:用户信息拓展(比如数据库的分库分表,就是拆分用户信息来做拓展)
在kafka中如何体现
x轴:replica。在kafka中,可以横向拓展partition,即通过在另一个节点上搭建partition集群来实现拓展。
但是在kafka中,会选举出主从partition,使用时是用leader partition来做读写,而follower partition只负责同步leader 的消息,因此,这一拓展的作用是提高kafka的可用性,而不能提升kafka性能
这里也可以将主作为读写,从作为只读来实现读写分离(类似MySQL),但是这样就会存在一系列一致性问题,kafka为了避免这种问题,做了简化,只允许客户端从主分片上读写,从分片只是备份
y轴:topic。在kafka中,生产者发送消息时需要指定向哪个topic,发送消费者消费消息时需要指定从哪个topic拉取消息,这个topic就从业务层面上区分了各个消息
z轴:partition。思路是有关的数据放到同一个partition中,无关的数据放到不同的partition中,
kafka提供的操作是生产者发送消息时相同的key的消息会路由到同一个partition中
2.pull模型/kafka为什么选择pull而不是push
首先,kafka在生产者->broker采用的是push模式,在broker->消费者采用的是pull模式
push的缺点:因为push模式有很明显的缺点,即无法适应不同消费速率,消费要求的消费者,因为发送速率是由broker来控制的,
这样就可能会造成消费者来不及消费消息,而拒绝服务或阻塞网络
pull的优点:消费速度和消费数量都可以由consumer来控制
3.广播、单播的概念
这是 Kafka 用来实现一个 Topic 消息的广播(发给所有的 Consumer)和单播(发给某一个 Consumer)的手段。一个 Topic 可以对应多个 Consumer Group。如果需要实现广播,只要每个 Consumer 有一个独立的 Group 就可以了。要实现单播只要所有的 Consumer 在同一个 Group 里。用 Consumer Group 还可以将 Consumer 进行自由的分组而不需要多次发送消息到不同的 Topic。
以kafka来说,由于同组的consumer只能消费不同分区上的消息,而不同组的consumer可以消费同个分区的消息
所以,要实现广播,就把每个consumer都放在不同的组里
相对的,要想实现单播,就把所有consumer放到一个组里
顺序写磁盘效率比随机写内存要高,这是kafka高性能的重要原因之一
4.实战面试题
kafka的数据删除策略
分为删除(delete)和压缩(compact)两种,默认为删除策略。
删除:根据时间来删除segment。即当(不活跃的segment的时间戳 - 当前时间)> 配置的保留时间时,
这个segment就会被删除
压缩:日志不会被完全删除,而是segment中每个key对应的最新一条消息,删除剩余的旧消息
var code = "786401fb-7920-4b6a-8281-77aec500f9fb"