高并发高性能系统
1.从用户使用体验的角度来看,200ms 是第一个分界点:接口的响应时间在 200ms 之内,
用户是感觉不到延迟的,就像是瞬时发生的一样。而 1s 是另外一个分界点:接口的响应时
间在 1s 之内时,虽然用户可以感受到一些延迟,但却是可以接受的,超过 1s 之后用户就
会有明显等待的感觉,等待时间越长,用户的使用体验就越差。所以,健康系统的 99 分位
值的响应时间通常需要控制在 200ms 之内,而不超过 1s 的请求占比要在 99.99% 以上。
2.超时时间短了,会造成大量的超时错误,对用户体验产生影响;超时时间长了,又起不到作
用。我建议你通过收集系统之间的调用日志,统计比如说 99% 的响应时间是怎样的,然后
依据这个时间来指定超时时间。如果没有调用的日志,那么你只能按照经验值来指定超时时
间。不过,无论你使用哪种方式,超时时间都不是一成不变的,需要在后面的系统维护过程
中不断地修改。
Kafka的缺点:
- 消息顺序性:Kafka只能保证分区内的消息顺序性,不能保证跨分区消息的顺序性。
- 延迟会比较高:Kafka为了实现高吞吐量,采用了批量发送的方式,会导致延迟相对较高。
- 重复消费:由于Kafka的消费模型为pull形式,会存在消费失败后重复消费的问题。
RocketMQ的缺点:
- 水平扩展能力弱: RocketMQ集群扩展能力较弱,有着较大瓶颈。
- 社区及文档不如Kafka活跃。
- 吞吐量和累积能力低于Kafka。
Kafka vs RocketMQ
Zookeeper vs Raft
消息队列的幂等性
- 生产端:给每一个生产者一个唯一的 ID,并且为生产的每一条消息赋予一个唯一 ID,消息队列的服务端会存储 < 生产者 ID,最后一条消息 ID> 的映射。当某一个生产者产生新的消息时,消息队列服务端会比对消息 ID 是否与存储的最后一条 ID 一致,如果一致,就认为是重复的消息,服务端会自动丢弃。
- 消费端: 幂等性的保证会稍微复杂一些,你可以从通用层和业务层两个层面来考虑。
- 通用层面,你可以在消息被生产的时候,使用发号器给它生成一个全局唯一的消息 ID,消息被处理之后,把这个 ID 存储在数据库中,在处理下一条消息之前,先从数据库里面查询这个全局 ID 是否被消费过,如果被消费过就放弃消费。 但是消息在处理之后,还没有来得及写入数据库,消费者宕机了重启之后发现数据库中并没有这条消息,还是会重复执行两次消费逻辑,这时你就需要引入事务机制,保证消息处理和写入数据库必须同时成功或者同时失败,但是这样消息处理的成本就更高了,所以,如果对于消息重复没有特别严格的要求,可以直接使用这种通用的方案,而不考虑引入事务。
- 业务层面:这里有很多种处理方式,其中有一种是增加乐观锁的方式。比如,你的消息处理程序需要给一个人的账号加钱,那么你可以通过乐观锁的方式来解决。 你给每个人的账号数据中增加一个版本号的字段,在生产消息时先查询这个账户的版本号,并且将版本号连同消息一起发送给消息队列。消费端在拿到消息和版本号后,在执行更新账户金额 SQL 的时候带上版本号,类似于执行:
update user set amount = amount + 20, version=version+1 where userId=1 and version=3
在更新数据时给数据加了乐观锁,这样在消费第一条消息时,version 值为 1,SQL 可以执行成功,并且同时把 version 值改为了 2;在执行第二条相同的消息时,由于version 值不再是 1,所以这条 SQL 不能执行成功,也就保证了消息的幂等性。
消息的丢失可以通过生产端的重试、消息队列配置集群模式,以及消费端合理处理消费进度三个方式来解决。为了解决消息的丢失通常会造成性能上的问题以及消息的重复问题。通过保证消息处理的幂等性可以解决消息的重复问题。
监控消息的延迟有两种方式:
使用消息队列提供的工具,通过监控消息的堆积来完成;
通过生成监控消息的方式来监控消息的延迟情况。
工具:Kafka 最新版本3.6.0
在 Kafka0.9 之前的版本中,消费进度是存储在 ZooKeeper 中的,消费者在消费消息的时候,先要从 ZooKeeper 中获取最新的消费进度,再从这个进度的基础上消费后面的消息。
在 Kafka0.9 版本之后,消费进度被迁入到 Kakfa 的一个专门的 topic叫“__consumer_offsets”里面。所以,如果你了解 kafka 的原理,你可以依据不同的版本,从不同的位置,获取到这个消费进度的信息。
Kafka 提供了工具叫做“kafka-consumer-groups.sh”(它在 Kafka 安装包的 bin 目录下)
Kafka 通过 JMX 暴露了消息堆积的数据,我在本地启动了一个 console consumer,然后使用 jconsole 连接这个 consumer,你就可以看到这个 consumer 的堆积数据了。
如果消息队列使用的是 Kafka 就无法通过增加消费者数量的方式,来提升消息处理能力。因为在 Kafka 中,一个 Topic(话题)可以配置多个 Partition(分区),数据会被平均或者按照生产者指定的方式,写入到多个分区中,那么在消费的时候,Kafka 约定一个分区只能被一个消费者消费,为什么要这么设计呢?在我看来,如果有多个 consumer(消费者)可以消费一个分区的数据,那么在操作这个消费进度的时候就需要加锁,可能会对性能有一定的影响。所以说,话题的分区数量决定了消费的并行度,增加多余的消费者也是没有用处的,那么你可以通过增加分区来提高消费者对特定话题的处理能力,而不是特定分区的处理能力。
如何在不增加分区的前提下提升消费能力呢?
既然不能增加 consumer,那么你可以在一个 consumer 中提升处理消息的并行度,所以可以考虑使用多线程的方式来增加处理能力:你可以预先创建一个或者多个线程池,在接收到消息之后,把消息丢到线程池中来异步地处理,这样,原本串行的消费消息的流程就变成了并行的消费,可以提高消息消费的吞吐量,在并行处理的前提下,我们就可以在一次和消息队列的交互中多拉取几条数据,然后分配给多个线程来处理。