队列与日志对比
日志无处不在:系统的基础
日志结构在许多关键系统中起着基础性的作用:
- 关系数据库:数据库首先写入预写日志(WAL),以确保事务的原子性和持久性。即便数据库崩溃,所有表的数据也可以从 WAL 中重建。
- 文件系统:如 XFS 文件系统,先将文件元数据的更改写入日志,然后再写入文件本身,以保证一致性。
- 分布式系统:Raft、ZAB、Paxos 等状态机复制协议依赖日志,确保所有副本按相同顺序应用相同的命令,进而保持一致性。
队列与日志:它们的区别
虽然队列和日志都以时间顺序存储数据,但它们在消费模型上的差异显著:
特性 | 队列 | 日志 |
---|---|---|
读取方式 | 破坏性读取:读取后删除 | 非破坏性读取:读取后数据仍然保留 |
消费模式 | 多个消费者争抢数据,平行消费 | 每个消费者按顺序独立读取数据 |
顺序保证 | 在并行消费下无法保证严格的顺序 | 强顺序:所有消费者按固定顺序读取 |
重放能力 | 无法重放 | 支持重放,可返回到任意读取位置 |
扩展性 | 增加消费者提高吞吐量 | 通过分区实现并行消费 |
状态管理 | 每条消息都有独立的状态机 | 每个订阅仅维护一个光标 |
队列的优势与局限
破坏性读取
队列在读取后立即删除数据,因而适用于并行消费:
- 多个消费者可从同一队列中读取不同的数据段,分担负载。
- 然而,消费顺序会丢失,难以保证所有数据按时间顺序处理。
消息状态管理
每条消息的状态需要单独维护:
- 读取后进入“待处理”状态。
- 处理完成后进行确认或删除。
- 若处理失败,则可能导致消息重复投递,进一步影响顺序。
日志的优势与局限
非破坏性读取
日志允许消费者读取数据而不删除:
- 消费者维护一个光标,指示读取位置,可随时回退或重放。
- 每个消费者按顺序独立读取,保证强一致性。
并行消费的挑战
日志天然是串行消费的:
- 新增消费者不会立即提高吞吐量。
- 通过分区实现并行消费,每个分区独立维护顺序。
支持多个逻辑消费者
队列的实现
队列只能支持一个逻辑消费者组:
- 每个逻辑消费者都需要独立的队列。
- 为每个消费者组创建多个队列导致写放大问题。
日志的实现
日志支持多个逻辑消费者:
- 每个消费者组只需维护一个独立的光标。
- 一个日志可以支持多个消费者组,无须复制数据,避免写放大。
队列与日志的网络实现
在分布式环境中,网络延迟和故障需要考虑以下问题:
- 至少一次 vs. 至多一次交付
- 队列通过独立的“读取”和“删除”操作实现至少一次交付,但可能导致重复消息。
- 日志通过光标管理实现顺序一致性,即使重发也能保持数据顺序。
- 消息重放
- 队列无法重放已读取的消息。
- 日志允许消费者回退光标,重新读取数据,支持错误恢复和历史重放。
在日志上实现队列:两者融合
现代消息系统常在日志之上实现队列,同时具备两者的优点:
- 日志中的队列元数据
- 消息本身存储在日志中,队列仅存储状态元数据:
- 每条消息的状态由一个映射表记录。
- 每个订阅都有独立的元数据映射,实现虚拟队列。
- 支持重放与顺序保证
- 通过将队列的头部向后移动,可以重放消息。
- 同时,通过对同一消息键的消息进行哈希分区,可以实现部分顺序。
优化后的消息调度策略
- 非阻塞键控调度
- 根据消息键分配不同的消费者,同一键的消息总是由同一消费者处理。
- 适用于会话化的协议,如 Kafka 的共享组。
- 阻塞键控调度
- 当相同键的前一条消息未完成处理时,阻塞后续消息。
- 适用于基于请求的协议,如 Amazon SQS FIFO 队列。
结论:模糊的界限
现代消息系统正在模糊队列与日志的界限:
- Kafka、Pulsar、Google PubSub 等系统在日志之上实现了队列语义,既支持顺序一致性,又能实现并行消费。
- Spring for Apache Kafka 和 Confluent Parallel Consumer 等库进一步增强了日志的队列化使用。
在正确的场景中,日志不仅可以替代队列,甚至在性能、可靠性和灵活性方面表现更优。因此,面对大规模分布式系统和实时数据处理的需求时,我们可能需要更多的考虑使用日志。
本文由作者按照 CC BY 4.0 进行授权