本章概括
异步方案那里可以暂时一笔带过,后续写实战的时候会详细深入的
入手源码那里,根据自己对未来的发展规划选择性翻阅吧。
消息积压问题
什么是消息积压
消息积压是使用MQ消息队列系统中,最常见的一种性能问题。如下图所示,当生产端的生产效率大于消费端的消费效率就会造成消息处理不完的情况,也就叫 “消息积压”。
消息积压应对方案(一)
第一种方案主要是优化生产端,消费端的处理能力。(消息队列你就别优化了,那么牛逼的团队设计出那么好的产品不会出问题的)
所以 我们只需要处理如何与消息队列配合,达到一个最佳的性能就够了。
对于生产端来说,并不会影响性能。因为一般生产端都是先执行自己的业务程序之后,再生产消息到MQ的。如果说,你的代码发送消息的性能上不去,你需要优先检查一下,是不是发消息之前的业务逻辑耗时太多导致的。
我们之前在讲生产端发送消息的过程中,假设这一次交互的平均耗时1ms,我们把这1ms的时间分解开,主要包括下列几项
- 发送前的准备数据,序列化数据,构造请求等耗时。也就是生产端在发送网络请求之间的耗时
- 发送消息和返回响应在网络传输中的耗时
- Broker处理消息的耗时
如果是单线程发送,每次只发送 1 条消息,那么每秒只能发送 1000ms / 1ms * 1 条 /ms = 1000 条 消息,这种情况下并不能发挥出消息队列的全部实力。
无论是增加每次发送消息的批量大小,还是增加并发,都能成倍地提升发送性能。至于到底是选择批量发送还是增加并发,主要取决于生产端程序的业务性质。
复习链接 生产端发送消息过程
对于消费端来说,大部分的问题应该都是出在这里的。主要的调优点也是在这个位置。
如果消费端的性能延时只是暂时的,那问题不大,只要消费端的性能恢复之后,超过生产端的性能,那积压的消息是可以逐渐被消化掉的。
要是消费速度一直比生产速度慢,时间长了,整个系统就会出现问题,要么,消息队列的存储被填满无法提供服务,要么消息丢失,这对于整个系统来说都是严重故障。所以,我们在设计系统的时候,一定要保证消费端的消费性能要高于生产端的发送性能,这样的系统才能健康的持续运行。
除了优化业务性能,也可以对消费端进行水平扩容,增加消费端的并发数从而达到总体的消费性能。在扩容 Consumer 的实例数量的同时, 必须同步扩容主题中的分区(也叫队列)数量,确保 Consumer 的实例数和分区数量是相等的。 如下图所示。
如果 Consumer 的实例数量超过分区数量,这样的扩容实际上是没有效果的。原因 我们之前讲过,因为对于消费者来说,在每个分区上实际上只能支持单线程消费
消息积压应对方案(二)
第二种方案应对的业务场景是,系统正常运转时,不会出现消息积压问题。但是某一时刻,突然就开始积压并且持续上涨。
这种问题还是比较纠结的,扩容的话,一般情况下是不需要的,不扩容的话某一刻又挡不住。。。。
遇到这样的场景无需就是两种原因,要不生产变快了,要不消费变慢了。我们可以通过监控程序观察一下数据情况再定位某种原因。
最后实在没办法可以将系统进行降级,通过关闭一些不重要的业务,减少生产方发送的数据量,最低限度的让系统的热卖业务还能正常运转。
重复消费也会拖慢整个系统的消费速度。
关于重复消费可以参考 重复消费解决方案
如何入手学习源码
最核心的一点就是查看 官方文档
官方文档是所有技术中 最权威,最齐全的一个资料聚集地
有些翻译中文的网站,可能会做到更新不及时,所以还是建议直接看英文文档,借助翻译即可。也可以锻炼英文水平
首先要 掌握这个技术的整体结构,有哪些功能特性,涉及到的关键技术、实现原理和生态系统 等等。掌握了这些,对它的整体有了一定了解,然后再去看它的源代码,就会非常通畅了。比如我之前做的MySQL技术分享如下图。
Redis的技术分享如下图
RocketMQ的技术分享还没画,可以暂时参考一下官方
我觉得学习这个东西最好的老师是兴趣。所以一定要带着问题去读源码。比如
- RocketMQ的消息是怎么写到文件中的? (肯定不会像我们业务程序那样直接写的)
- RocketMQ的重发机制是怎么实现的?
- RocketMQ的确认机制是怎么实现的?
- RocketMQ的分布式的实现源码是怎么样的?
- RocketMQ的消息不丢失是否和MySQL那样有binlog,redo log,是否和Redis那样有RDB,AOF呢?
上述问题反正是我当下最想知道的,但是不要操之过急,学懂整体后再深入。
一定不要直接从main里看,程序跟书是不一样的,书是由人学懂之后,整理的一个先后学习顺序。而程序是一个网状结构,是一个功能一个功能的实现
这上述也是我接下来学习源码的方法
源码分析的话,我的想法是等我学完RocketMQ的基础原理,我会从第一篇文章涉及到的知识点,进入源码阅读分析整理
MySQL,Redis就算了,打算发力MQ
异步方案提升系统性能
这里先简单聊一下思想,后续会用代码案例实现。
异步执行
简单的说,异步思想就是,当我们要执行一项比较耗时的操作时,不去等待操作结束,而是给这个操作一个命令:“当操作完成后,接下来去执行什么。”
使用异步编程模型,虽然并不能加快程序本身的速度,但可以减少或者避免线程等待,只用 很少的线程就可以达到超高的吞吐能力。
同时我们也需要注意到异步模型的问题:相比于同步实现,异步实现的复杂度要大很多,代 码的可读性和可维护性都会显著的下降。虽然使用一些异步编程框架会在一定程度上简化异 步开发,但是并不能解决异步模型高复杂度的问题。
异步性能虽好,但一定不要滥用,只有类似在像消息队列这种业务逻辑简单并且需要超高吞 吐量的场景下,或者必须长时间等待资源的地方,才考虑使用异步模型。如果系统的业务逻 辑比较复杂,在性能足够满足业务需求的情况下,采用符合人类自然的思路且易于开发和维 护的同步模型是更加明智的选择。
异步网络
传统的同步网络 IO,一般采用的都是一个线程对应一个 Channel 接收数据,很难支持高并 发和高吞吐量。这个时候,我们需要使用异步的网络 IO 框架来解决问题。
Netty 和 NIO 是两种异步网络框架。
Netty 自动地解决了线程控制、缓存管理、连接管理这些问题,用户只需要实现对应的 Handler 来处理收到的数据即可。
NIO 是更加底层的 API,它提供了 Selector 机制,用单个线程同时管理多个连接,解决了多路 复用这个异步网络通信的核心问题。
以上是 IO、BIO、NIO的发展,流程图
缓存策略
缓存策略的出现,主要解决的就是如何减少与磁盘IO交互,提升系统性能。对于持久化来说,肯定还是要存磁盘的。所以我们必须保证最大的几率命中缓存,同时也要减少与磁盘IO的交互。
一般来说 SSD 每秒钟可以读写几千次,如果说我们的程序在处理业务请求的时候直接来读写磁盘,假设处理每次请求需要读写 3~5 次,即使每次请求的数据量不大,你的程序最多每秒也就 能处理 1000 次左右的请求。 而内存的随机读写速度是磁盘的 10 万倍!所以,使用内存作为缓存来加速应用程序的访问速度,是几乎所有高性能系统都会采用的方法。
只读与读写缓存的抉择
使用缓存,首先你就会面临选择读缓存还是读写缓存的问题。他们唯一的区别就是,在更新数据的时候,是否经过缓存
读写缓存: 它是一种牺牲数据一致性换取性能的设计,天然是不可靠的。
为什么说他是不可靠的呢? 可以从上图中看出,写入数据到PageCache时,并不是同步写入的,而是异步,如果在这段期间,还没开始异步写入时,服务器直接宕机了,就丢失了数据。
如果说我们人工操作,执行一次sync。这样也就失去了缓存的意义
缓存的意义不就是减少与磁盘IO的交互嘛
写缓存的实现是非常复杂的。应用程序不停地更新 PageCache 中的数据,操作系统 需要记录哪些数据有变化,同时还要在另外一个线程中,把缓存中变化的数据更新到磁盘文 件中。在提供并发读写的同时来异步更新数据,这个过程中要保证数据的一致性,并且有非常好的性能,实现这些真不是一件容易的事儿。
所以一般 推荐使用只读缓存。
如何保持缓存数据新鲜
上面提到了推荐使用只读缓存,对于只读缓存来说,缓存中的数据来源只有一个途径,就是从磁盘上来。当数据需要更新的时候,磁盘中的数据和缓存中的副本都需要进行更新。我们知道在分布式系统中,除非是使用事务或者一些分布式一致性算法来保证数据一致性,否则,由于节点宕机、网络传输故 障等情况的存在,我们是无法保证缓存中的数据和磁盘中的数据是完全一致的。
我们要做的就是尽可能最大的保证数据同步。最省心,代价最大的就是采用分布式事务来解决了。
另一种简单的方式就是采用Redis那种过期key的方式实现,数据过期以后即使它还存在缓存中,我们也认为它不再有效,需要从 磁盘上再次加载这条数据,这样就变相地实现了数据更新。
还是根据业务吧。比如微信头像修改后的实时显示,邮件的延迟几秒钟接收都可以采用过期key的方式
如果是那种交易类系统,对数据一致性比较敏感的可以采用牺牲性能来决定一致性。
缓存的置换策略
在使用缓存的过程中,除了要考虑数据一致性的问题,你还需要关注的另一个重要的问题是,在内存有限的情况下,要优先缓存哪些数据,让缓存的命中率最高。
当应用程序要访问某些数据的时候,如果这些数据在缓存中,那直接访问缓存中的数据就可 以了,这次访问的速度是很快的,这种情况我们称为一次缓存命中;如果这些数据不在缓存 中,那只能去磁盘中访问数据,就会比较慢。这种情况我们称为“缓存穿透”。显然,缓存 的命中率越高,应用程序的总体性能就越好。
那用什么样的策略来选择缓存的数据,能使得缓存的命中率尽量高一些呢?
如果你的系统是那种可以预测未来访问哪些数据的系统,比如说,有的系统它会定期做数据同步,每次同步的数据范围都是一样的,像这样的系统,缓存策略很简单,就是你要访问什么数据,就缓存什么数据,甚至可以做到百分之百的命中。
但是,大部分系统,它并没有办法准确地预测未来会有哪些数据会被访问到,所以只能使用一些策略来尽可能地提高缓存命中率。
一般来说,我们都会在数据首次被访问的时候,顺便把这条数据放到缓存中。随着访问的数据越来越多,总有把缓存占满的时刻,这个时候就需要把缓存中的一些数据删除掉,以便存放新的数据,这个过程称为缓存置换。
到这里,问题就变成了:当缓存满了的时候,删除哪些数据,才能会使缓存的命中率更高一 些,也就是采用什么置换策略的问题。
命中率最高的置换策略,一定是根据你的业务逻辑,定制化的策略。比如,你如果知道某些数据已经删除了,永远不会再被访问到,那优先置换这些数据肯定是没问题的。再比如,你的系统是一个有会话的系统,你知道现在哪些用户是在线的,哪些用户已经离线,那优先置换那些已经离线用户的数据,尽量保留在线用户的数据也是一个非常好的策略。
另外一个选择,就是使用通用的置换算法。一个最经典也是最实用的算法就是 LRU 算法, 也叫最近最少使用算法。这个算法它的思想是,最近刚刚被访问的数据,它在将来被访问的 可能性也很大,而很久都没被访问过的数据,未来再被访问的几率也不大。
基于这个思想,LRU 的算法原理非常简单,它总是把最长时间未被访问的数据置换出去。 你别看这个 LRU 算法这么简单,它的效果是非常非常好的。
可以参考Redis的的LRU文章,实现原理都是差不多的 LRU算法的实现
充电分享
少年有梦,不应止于心动,更应付出行动,用自己的关,照亮自己的路,努力追上那个曾经被赋予厚望的自己 —— 致自己,致各位追梦的你们
结尾
有些不懂的地方或者不对的地方,麻烦各位指出,一定修改优化!