文章目录
- 壹 微服务Nacos
- 1.1 SpringCloud常见组件
- 1.2 Nacos的服务注册表结构
- 1.3 Nacos如何支撑内部数十万服务注册压力
- 1.4 Nacos避免并发读写冲突问
- 1.5 Nacos与Eureka的区别
- 1.6 Sentinel的限流与Gateway的限流的差别
- 1.7 Sentinel的线程隔离与Hystix的线程隔离的差别
- 贰 MQ知识
- 2.1 选择RabbitMQ的reason
- 2.2 RabbitMQ如何确保消息的不丢失
- 2.3 RabbitMQ如何避免消息堆积
- 2.4 RabbitMQ保证消息的有序性
- 2.5 防止MQ消息被重复消费
- 2.6 保证RabbitMQ的高可用
- 2.7 使用MQ可以解决那些问题?
壹 微服务Nacos
1.1 SpringCloud常见组件
SpringCloud包含的组件很多,有很多功能是重复的。其中最常用组件包括:
- 注册中心组件:Eureka、Nacos等
- 负载均衡组件:Ribbon
- 远程调用组件:OpenFeign
- 网关组件:Zuul、Gateway
- 服务保护组件:Hystrix、Sentinel
- 服务配置管理组件:SpringCloudConfig、Nacos
1.2 Nacos的服务注册表结构
-
Nacos采用了数据的分级存储模型,最外层是Namespace,用来隔离环境。然后是Group,用来对服务分组。接下来就是服务(Service)了,一个服务包含多个实例,但是可能处于不同机房,因此Service下有多个集群(Cluster),Cluster下是不同的实例(Instance)。
-
对应到Java代码中,Nacos采用了一个多层的Map来表示。结构为Map<String, Map<String, Service>>,其中最外层Map的key就是namespaceId,值是一个Map。内层Map的key是group拼接serviceName,值是Service对象。Service对象内部又是一个Map,key是集群名称,值是Cluster对象。而Cluster对象内部维护了Instance的集合。
1.3 Nacos如何支撑内部数十万服务注册压力
- Nacos内部接收到注册的请求时,不会立即写数据,而是将服务注册的任务放入一个阻塞队列就立即响应给客户端。然后利用线程池读取阻塞队列中的任务,异步来完成实例更新,从而提高并发写能力。
1.4 Nacos避免并发读写冲突问
- Nacos在更新实例列表时,会采用CopyOnWrite技术,首先将旧的实例列表拷贝一份,然后更新拷贝的实例列表,再用更新后的实例列表来覆盖旧的实例列表。这样在更新的过程中,就不会对读实例列表的请求产生影响,也不会出现脏读问题。
1.5 Nacos与Eureka的区别
Nacos与Eureka有相同点,也有不同之处:
- 接口方式:Nacos与Eureka都对外暴露了Rest风格的API接口,用来实现服务注册、发现等功能
- 实例类型:Nacos的实例有永久和临时实例之分;而Eureka只支持临时实例
- 健康检测:Nacos对临时实例采用心跳模式检测,对永久实例采用主动请求来检测;Eureka只支持心跳模式
- 服务发现:Nacos支持定时拉取和订阅推送两种模式;Eureka只支持定时拉取模式
1.6 Sentinel的限流与Gateway的限流的差别
- 限流算法常见的有三种实现:滑动时间窗口、令牌桶算法、漏桶算法。Gateway则采用了基于Redis实现的令牌桶算法。而Sentinel内部却比较复杂:
- 默认限流模式是基于滑动时间窗口算法
- 排队等待的限流模式则基于漏桶算法
- 而热点参数限流则是基于令牌桶算法
1.7 Sentinel的线程隔离与Hystix的线程隔离的差别
- Hystix默认是基于线程池实现的线程隔离,每一个被隔离的业务都要创建一个独立的线程池,线程过多会带来额外的CPU开销,性能一般,但是隔离性更强。
- Sentinel是基于信号量(计数器)实现的线程隔离,不用创建线程池,性能较好,但是隔离性一般。
贰 MQ知识
2.1 选择RabbitMQ的reason
- 各个消息队列技术的对比:
- kafka是以吞吐量高而闻名,不过其数据稳定性一般,而且无法保证消息有序性。
- 阿里巴巴的RocketMQ基于Kafka的原理,弥补了Kafka的缺点,继承了其高吞吐的优势,其客户端目前以Java为主。
- RabbitMQ基于面向并发的语言Erlang开发,吞吐量不如Kafka,但是对我们公司来讲够用了。而且消息可靠性较好,并且消息延迟极低,集群搭建比较方便。支持多种协议,并且有各种语言的客户端,比较灵活。Spring对RabbitMQ的支持也比较好,使用起来比较方便。
2.2 RabbitMQ如何确保消息的不丢失
RabbitMQ针对消息传递过程中可能发生问题的各个地方,给出了针对性的解决方案:
- 生产者发送消息时可能因为网络问题导致消息没有到达交换机:
- RabbitMQ提供了publisher confirm机制
- 生产者发送消息后,可以编写ConfirmCallback函数
- 消息成功到达交换机后,RabbitMQ会调用ConfirmCallback通知消息的发送者,返回ACK
- 消息如果未到达交换机,RabbitMQ也会调用ConfirmCallback通知消息的发送者,返回NACK
- 消息超时未发送成功也会抛出异常
- RabbitMQ提供了publisher confirm机制
- 消息到达交换机后,如果未能到达队列,也会导致消息丢失:
- RabbitMQ提供了publisher return机制
- 生产者可以定义ReturnCallback函数
- 消息到达交换机,未到达队列,RabbitMQ会调用ReturnCallback通知发送者,告知失败原因
- RabbitMQ提供了publisher return机制
- 消息到达队列后,MQ宕机也可能导致丢失消息:
- RabbitMQ提供了持久化功能,集群的主从备份功能
- 消息持久化,RabbitMQ会将交换机、队列、消息持久化到磁盘,宕机重启可以恢复消息
- 镜像集群,仲裁队列,都可以提供主从备份功能,主节点宕机,从节点会自动切换为主,数据依然在
- RabbitMQ提供了持久化功能,集群的主从备份功能
- 消息投递给消费者后,如果消费者处理不当,也可能导致消息丢失:
- SpringAMQP基于RabbitMQ提供了消费者确认机制、消费者重试机制,消费者失败处理策略:
- 消费者的确认机制:
- 消费者处理消息成功,未出现异常时,Spring返回ACK给RabbitMQ,消息才被移除
- 消费者处理消息失败,抛出异常,宕机,Spring返回NACK或者不返回结果,消息不被异常
- 消费者重试机制:
- 默认情况下,消费者处理失败时,消息会再次回到MQ队列,然后投递给其它消费者。Spring提供的消费者重试机制,则是在处理失败后不返回NACK,而是直接在消费者本地重试。多次重试都失败后,则按照消费者失败处理策略来处理消息。避免了消息频繁入队带来的额外压力。
- 消费者失败策略:
- 当消费者多次本地重试失败时,消息默认会丢弃。
- Spring提供了Republish策略,在多次重试都失败,耗尽重试次数后,将消息重新投递给指定的异常交换机,并且会携带上异常栈信息,帮助定位问题。
- 消费者的确认机制:
- SpringAMQP基于RabbitMQ提供了消费者确认机制、消费者重试机制,消费者失败处理策略:
2.3 RabbitMQ如何避免消息堆积
消息堆积问题产生的原因往往是因为消息发送的速度超过了消费者消息处理的速度。因此解决方案无外乎以下三点:
- 提高消费者处理速度
- 增加更多消费者
- 增加队列消息存储上限
- 提高消费者处理速度
- 消费者处理速度是由业务代码决定的,所以我们能做的事情包括:
- 尽可能优化业务代码,提高业务性能
- 接收到消息后,开启线程池,并发处理多个消息
- 优点:成本低,改改代码即可
- 缺点:开启线程池会带来额外的性能开销,对于高频、低时延的任务不合适。推荐任务执行周期较长的业务。
- 增加更多消费者
- 一个队列绑定多个消费者,共同争抢任务,自然可以提供消息处理的速度。
- 优点:能用钱解决的问题都不是问题。实现简单粗暴
- 缺点:问题是没有钱。成本太高
- 增加队列消息存储上限
- 在RabbitMQ的1.8版本后,加入了新的队列模式:Lazy Queue.这种队列不会将消息保存在内存中,而是在收到消息后直接写入磁盘中,理论上没有存储上限。可以解决消息堆积问题。
- 优点:磁盘存储更安全;存储无上限;避免内存存储带来的Page Out问题,性能更稳定;
- 缺点:磁盘存储受到IO性能的限制,消息时效性不如内存模式,但影响不大。
2.4 RabbitMQ保证消息的有序性
- RabbitMQ是队列存储,天然具备先进先出的特点,只要消息的发送是有序的,那么理论上接收也是有序的。不过当一个队列绑定了多个消费者时,可能出现消息轮询投递给消费者的情况,而消费者的处理顺序就无法保证。因此,要保证消息的有序性,需要做的下面几点:
- 保证消息发送的有序性
- 保证一组有序的消息都发送到同一个队列
- 保证一个队列只包含一个消费者
2.5 防止MQ消息被重复消费
- 消息重复消费的原因多种多样,不可避免。所以只能从消费者端入手,只要能保证消息处理的幂等性就可以确保消息不被重复消费。而幂等性的保证又有很多方案:
- 给每一条消息都添加一个唯一id,在本地记录消息表及消息状态,处理消息时基于数据库表的id唯一性做判断
- 同样是记录消息表,利用消息状态字段实现基于乐观锁的判断,保证幂等
- 基于业务本身的幂等性。比如根据id的删除、查询业务天生幂等;新增、修改等业务可以考虑基于数据库id唯一性、或者乐观锁机制确保幂等。本质与消息表方案类似。
2.6 保证RabbitMQ的高可用
- 要实现RabbitMQ的高可用无外乎下面两点:
- 做好交换机、队列、消息的持久化
- 搭建RabbitMQ的镜像集群,做好主从备份。当然也可以使用仲裁队列代替镜像集群。
2.7 使用MQ可以解决那些问题?
-
RabbitMQ能解决的问题:
-
解耦合:将几个业务关联的微服务调用修改为基于MQ的异步通知,可以解除微服务之间的业务耦合。同时还提高了业务性能。
-
流量削峰:将突发的业务请求放入MQ中,作为缓冲区。后端的业务根据自己的处理能力从MQ中获取消息,逐个处理任务。流量曲线变的平滑很多
-
延迟队列:基于RabbitMQ的死信队列或者DelayExchange插件,可以实现消息发送后,延迟接收的效果。