13. 软件架构设计中的常见问题
目录
哪些常见的软件设计问题
1. 消息队列
1.1 消息系统的实现方式
消息系统采用发布/订阅模式,为了区分不同的消息系统,提出以下两个问题对区分很有帮助:
- 如果生产者发送消息的速度比消费者能处理的快,会发生什么?
- 一般有三种选择: 系统丢弃消息;将消息缓存在队列中;激活背压(流量控制,即阻止生产者发送消息)
- 如果消息被缓存在队列中,那么队列增长时会发生什么非常重要
- 如果内存无法容纳所有队列,系统是否会崩溃,还是消息会被写入磁盘
- 如果消息会落盘,又会如何影响消息传递系统的性能
- 如果节点崩溃或者暂时离线,是否会有消息丢失?
- 持久化需要写入磁盘或者结合复制方案,这些都是有成本的
- 如果能够接受消息丢失,那么同样的硬件上可以获得更高的吞吐量和更低的延迟
消息传递有如下几种方式:
- 生产者与消费者之间直接消息传递
- 这种方式通常要求应用程序意识消息丢失的可能性,它们只支持有限的容错
- 通常还假定生产者和消费者同时在线。诸如 RPC 和 HTTP 请求,webhooks 都是这种消息传递方式
- 消息代理
- 消息代理本质上是一种针对处理消息而优化的数据库
- 将数据集中在代理上,可以更容易的适应不断变化的客户端
- 持久性问题被转移到代理。消息写入代理的结果也通常导致消费以异步方式工作
- 为了确保消息不会丢失,消息代理使用确认: 客户端必须在处理完消息后显示的告诉代理,以便代理可以将其从队列中移除
- 但是消息的确认/重传会导致消息的重复和乱序
1.2 pull 和 push 的对比
消息系统的消息拉取有 pull 和 push 两种模式:
- pull: 消费者周期性拉取消息
- push: 服务端向客户端推送消息
它们之前的取舍可以从以下几个方面去考虑
- 消息消费的及时性
- 负载控制
对于 push,消息到达服务器后,就会被推送值消费者,实时性好。消费的消费速率由服务端把控,有可能会出现服务端发送过快压垮消费者客户端的情况。
对于 pull 由消费者定期轮询拉取消息,消费的速率由客户端把控,轮询总是存在周期间隔,因而对实时性有一定影响。
CMQ 提供了长轮询的优化方法,用以平衡 Pull/Push 模型各自的缺点。基本方式是:消费者如果尝试拉取失败,不是直接 return,而是把连接挂在那里 wait,服务端如果有新的消息到来,把连接拉起,返回最新消息。
因此 pull/push 的选择,本质上是对实时性和负载控制的一种均衡。
2. 数据库对比
2.1 如何对比两个数据库
3. RabbitMQ 与 Kafka
之所以没有选择 Kafka 是因为如下几个原因:
max.poll.interval.ms需要预估每批数据的消费时间,否则 Kafka 会将消费者视为失败,重新再均衡
AMQP 类型的消息代理更适合如下场景:
- 消息处理的代价很高,希望在逐个消息的基础上并行处理
- 而且消息排序又不那么重要的情况下,并且需要在他们处理后返回并在此读取旧消息时 更可取
相反,基于日志的消息系统更适用于:
- 在消息吞吐量高,每个消息处理速度快
- 消息顺序有很重要
- 需要读取已经确认的旧消息