目录

13. 软件架构设计中的常见问题

哪些常见的软件设计问题

1. 消息队列

1.1 消息系统的实现方式

消息系统采用发布/订阅模式,为了区分不同的消息系统,提出以下两个问题对区分很有帮助:

  1. 如果生产者发送消息的速度比消费者能处理的快,会发生什么?
    • 一般有三种选择: 系统丢弃消息;将消息缓存在队列中;激活背压(流量控制,即阻止生产者发送消息)
    • 如果消息被缓存在队列中,那么队列增长时会发生什么非常重要
    • 如果内存无法容纳所有队列,系统是否会崩溃,还是消息会被写入磁盘
    • 如果消息会落盘,又会如何影响消息传递系统的性能
  2. 如果节点崩溃或者暂时离线,是否会有消息丢失?
    • 持久化需要写入磁盘或者结合复制方案,这些都是有成本的
    • 如果能够接受消息丢失,那么同样的硬件上可以获得更高的吞吐量和更低的延迟

消息传递有如下几种方式:

  1. 生产者与消费者之间直接消息传递
    • 这种方式通常要求应用程序意识消息丢失的可能性,它们只支持有限的容错
    • 通常还假定生产者和消费者同时在线。诸如 RPC 和 HTTP 请求,webhooks 都是这种消息传递方式
  2. 消息代理
    • 消息代理本质上是一种针对处理消息而优化的数据库
    • 将数据集中在代理上,可以更容易的适应不断变化的客户端
    • 持久性问题被转移到代理。消息写入代理的结果也通常导致消费以异步方式工作
    • 为了确保消息不会丢失,消息代理使用确认: 客户端必须在处理完消息后显示的告诉代理,以便代理可以将其从队列中移除
    • 但是消息的确认/重传会导致消息的重复和乱序

1.2 pull 和 push 的对比

消息系统的消息拉取有 pull 和 push 两种模式:

  1. pull: 消费者周期性拉取消息
  2. push: 服务端向客户端推送消息

它们之前的取舍可以从以下几个方面去考虑

  1. 消息消费的及时性
  2. 负载控制

对于 push,消息到达服务器后,就会被推送值消费者,实时性好。消费的消费速率由服务端把控,有可能会出现服务端发送过快压垮消费者客户端的情况。

对于 pull 由消费者定期轮询拉取消息,消费的速率由客户端把控,轮询总是存在周期间隔,因而对实时性有一定影响。

CMQ 提供了长轮询的优化方法,用以平衡 Pull/Push 模型各自的缺点。基本方式是:消费者如果尝试拉取失败,不是直接 return,而是把连接挂在那里 wait,服务端如果有新的消息到来,把连接拉起,返回最新消息。

因此 pull/push 的选择,本质上是对实时性和负载控制的一种均衡。

2. 数据库对比

2.1 如何对比两个数据库

3. RabbitMQ 与 Kafka

之所以没有选择 Kafka 是因为如下几个原因:

  1. max.poll.interval.ms 需要预估每批数据的消费时间,否则 Kafka 会将消费者视为失败,重新再均衡

AMQP 类型的消息代理更适合如下场景:

  • 消息处理的代价很高,希望在逐个消息的基础上并行处理
  • 而且消息排序又不那么重要的情况下,并且需要在他们处理后返回并在此读取旧消息时 更可取

相反,基于日志的消息系统更适用于:

  • 在消息吞吐量高,每个消息处理速度快
  • 消息顺序有很重要
  • 需要读取已经确认的旧消息