后台开发那些常用技术再次小结(五):消息队列

一、消息队列简介

  消息队列本质上是一个缓冲并分发请求的组件,消息队列工作的上下文中假定消息为单向流动,拥有着“发射后不用管”的特性。消息由消息生产者产生、消息队列缓冲、消息消费者消费信息执行操作,生产者和消费者之间彼此独立的工作,之间只会通过消息格式和消息队列产生耦合,这样就实现了生产者和消费者业务的解耦和异步化操作:之后生产者和消费者可以独立部署、独立伸缩,并且生产者向队列提交完消息之后就可以立即响应客户的请求。

1.1 消息队列中的角色

  消息生产者
  消息生产者也叫作消息发布者,其属于客户端代码的一部分,其只需要负责创建一条合法消息,并发送到消息队列中即可。应用通常有多个生产者,所有发步的消息被排队并异步处理。
  消息队列
  消息队列本身起着向消费者递交消息的缓冲作用,消息队列可以是一个具有权限控制、路由、持久化、失败恢复等多种职责的独立组件,所以通常也被作为一个独立应用程序提供服务,称作消息代理或者面向消息的中间件。
  消息消费者
  消息消费者主要从消息队列中接收并处理消息,是实际处理异步请求的组件。消息消费者不应当知道生产者的情况,其应该只依赖于消息队列中的合法消息做出行动。
  消息消费者通常具有周期模式和守护模式进行工作:周期模式 | 拉模式,消费者定期连接到消息队列检查队列状况,如果有消息才消费,这种模型在诸如PHP、Perl等动态语言中比较常见,可以用在队列消息较少或者网络不稳定的场景;守护模式 | 推模式,消息消费者和消息代理通常用一个持久连接进行无限循环方式运行,消费者阻塞在取消息的操作上,这种模式在持久化应用容器的语言中更常见,比如Java、Nodejs;还有结合两种,采用推拉结合的形式进行工作的。
  从消费者向消息队列取消息的不同订阅方法,也派生出了消息的不同路由方法:
  a. 直接工作队列模型
  该模式下队列由生产者和消费者所周知,多个生产者可以在任意时间点向队列的一端发送消息,而另一端可以有一个或者多个消费者竞争消费消息,但每条消息只会被路由到一个消费者。这种情况下消费者是无状态的,因此失效节点的替换、处理性能的可伸缩性将极为的简单。
  b. 发布订阅模型
  该模式实际是设计模式中观察者模式的变体,此模式下消息可能被发送到不止一个消费者。生产者产生的消息不再是发送给一个队列,而是发送到一个主题;消费者使用的时候必须连接到消息代理,并声明自己所感兴趣的主题;消息到达主题后,会被克隆给每个订阅了该主题的消费者,消费者接受一份消息的拷贝并复制到自己的私有队列中。在实现上,如果在发消息的时间点没有消费者订阅到该主题,通常这个消息会被丢弃掉。
  这种模式的一个实例,就是电商在购物操作被确认后,会将购物事件发送给消息队列的某个主题,那么后续对这个主题感兴趣的消费者就可以收到消息后进行相关事务处理,比如通知供货商、消费者风控规则、消费者积分奖励等。
  c. 定制路由规则
  主要是各种消息路由规则的定制,消费者可以选择灵活的方式决定消息如何路由到自己的队列中。比如日志消息可以根据日志错误等级进行特定方式的路由。

二、消息队列的好处

  实现异步处理
  使用消息队列可以推迟耗时的操作而不必阻塞客户端,消息添加到队列后就可以返回客户端让客户端继续执行。
  异步处理的另外好处,就是保护系统的核心业务和关键特性,比如下单、产品检索、处理支付,如果将非核心业务同核心业务耦合起来,就会引入新的失效点影响核心业务的可用性,这些非核心业务请求都可以放到消息队列中进行异步处理。同时,对于那些消耗计算资源的操作、低价值的操作类型也都可以隔离到消息队列中处理。
  更好的伸缩性
  生产者和消费者可以独立进行伸缩操作,理想情况下只需要简单添加机器联系到消息代理就可以完成伸缩。
  平衡流量峰值
  即便业务流量持续增长,系统仍然可以持续高频接收请求,虽然消息产生的速率比消费速率快,但是可以持续高速的将消息送入队列中。虽然消息生产速率比消费速率会快,会导致整个队列不断增长,消息从发出到处理的延时也会不断变长,但是当峰值过后生产速率比消费速率低,整个系统就会慢慢趋于正常水平。
  失败隔离和自我修复
  消息队列容许从关键路径上删除一些非核心功能,那么通过生产者、消费者的角色隔离就可以提高系统的健壮性和容错性,消费者系统错误就可以同生产者系统隔离。不仅在消费者遇到问题的时候体现出这样的价值,这也意味着消费者可以在任意时刻停止消息的处理,那么对于消费者端软件的维护、升级都是大有裨益的,只要消息被保存好就可以了。
  解耦
  通过消息队列可以让生产者和消费者从不互相直接交互,甚至感知不到对方的存在,他们的行为只依赖于事先约定的消息格式。消费者和生产者可以独立开发、独立维护,甚至可以由不同的团队完成,从而在架构上实现了业务的高度解耦。

三、消息队列的挑战

消息队列在上述描述的好处之外,带来的挑战和代价主要有:
  消息无序
  在大多数情况下为了实现业务的伸缩性,消息消费者都是并行执行而且没有同步机制的,虽然这样可以最大化消费性能,但是某些情况下会出现消息无序的状况,消息处理的顺序取决于每个消费者处理能力的大小以及本身处理消息所需要的计算量。比如创建账户和发送邮件两个消息,这两个消息是有顺序依赖的,但是可能会被并行执行;某些情况下消息处理失败,消息队列基础架构会让该消息重回队列,然后被发送给其他消费者,那么就有可能出现消息的倒序执行、消息的多次执行问题。 针对上面的问题,可能的解决方式有:
  a. 如果只有一个消费者(单线程),每次只消费一条消息,那么可以保证消息严格按照入队的顺序进行有序消费,但是这种方式缺乏伸缩性。
  b. 由应用程序保证消息顺序,整个消息系统仍然是不受消息顺序影响的。比如业务只发送创建账户的消息,而等消费者创建完账户后,该消费者负责创建发送Email的消息。
  c. 某些消息代理可以支持部分消息顺序保证,这类组件主要是进行消息组机制,当消息发送的时候带有一个分组ID的标签,该分组ID可以由应用程序定义,消息代理能够保证同一组的所有消息能够按照发布的顺序被消费。其实内部是进行路由映射的策略,将同一个组ID的消息都发送给同一个消费者,这样就能够保证被顺序消费,但是不利于负载均衡。
  消息重新入队列
  某些失败的场景下可能导致消费的信息被重新入队列。如果这种情况下需要系统健壮,那么就需要保证消费者的操作具有幂等性,但这种幂等性的实现根据业务类型可易可难,通常是在应用程序中增加一个操作跟踪和持久层来解决。
  还有需要注意的是,幂等操作和顺序依赖往往是相互制约的,比如两条消息:一条将价格设置为55美元,一条将价格设置为60美元,那么这样的消息具有幂等性,但是最终结果严格依赖执行顺序;如果改为价格增加5美元,虽然对顺序无依赖,但是操作不具有幂等性。

参考