消息队列深入学习(1) kafka
本章将以卡夫卡为核心深入学习消息队列的方方面面
必要性
分布式
这是宏观上消息队列非常大的优势, 其所带来的好处可以细分如下 :
解耦 :
消息队列专注于生产消费模型, 其作为中间层可以很好地将这两方面解耦, 像我们在代码中使用queue + 条件变量的组合其实本质就是为了实现该功能.
独立 / 持久化 :
消息队列一般运行在一个独立的服务器上, 这样就算消费者崩溃, 消息队列也可以持续作为中间层接收生产者的消息. 并且持久化到本地, 也可以同时供多个消费者读取同一批消息.
异步 :
生产者不必阻塞等待消费者完成消费动作, 并且不同的消费者可以通过分区同时消费消息.
负载均衡 :
和nginx的理念差不多, 多个消费者可以由消息队列进行负载均衡, 提升读取效率.
削峰填谷
这是功能上最核心也最明显的优势, 本质就是可以当一个独立的buffer, 由于消息产生的速率在不同时间段都是不同的, 在消息高峰, 当消费者处理速度跟不上时, 就可以预存在这里, 当消息低谷时, 消费者处理速度跟上时, 就可以慢慢从消息队列这里把多的取出来消费了.
Kafka
topic(消息分类机制)
这是一个逻辑层面上的机制, 你可以认为一个topic就对应一个队列, 生产者可以通过指定topic发送消息, 消费者指定topic取消息.
为什么要设计topic?
本质原因就是只搞一个队列不够用, 并且对于队列的读写在底层肯定是要加锁的, 如果只对一个队列进行大量读写, 并行度肯定高不了, 这样分多个topic的优势就显而易见了.
partition(物理分区)
分区, 每个partition可以被认为是kafka中最小的物理单元.
topic只是逻辑上的队列, 每个topic中都会有很多个partition, 每个partiton内部存储不同的消息且有序, partition之间消息并非有序, 全看发来消息时负载均衡后存在哪里.
一个topic下的逻辑结构类似如下 :
1 | |
为什么要分多个partition?
因为这是为了配合负载均衡的机制, 当多个消费者读取消息时, 可以同时读取不同的分区, 以提高并发量.
一个partition的物理结构类似如下 :
1 | |
看到partiton也许会有很多疑问, 我们一个一个解释 :
log :
实际数据存储单元, 从前往后会存储多个log文件. 需要分成多个文件的原因如下 :
- 配合索引, 存在不同文件中可以利用索引快速查找.
- 配合持久化机制, 在删除过期消息时可以从前往后根据过期时间一个一个删除.
index :
kafka使用一种叫”稀疏索引”的方式进行查找目标消息, 这里不深入.
为什么需要查找?
本质是kafka有持久化机制, 并非消息被一个消费者读取后就要删除, kafka中的所有消息在过期或专门删除之前都会长期保存. 这是为了让一个消息可以供多个消费者一起读取. (这很好理解, 一个消息出现, 可能具体的处理者要读, 可能数据分析服务也要读)
每个消费者内部都有自己当前的读取进度, 因此需要查找到自己要读的下一个消息.
消费者组
你可以认为处理相同事务的消费者应当放到一个消费者组中, 这样kafka在内部负载均衡后会将目标topic中的每个partition分配均匀的消费者, 例子如下 :
1 | |
每个消费者都会维护自己读取分区的进度.
一个partition在同一个消费者组内只能被一个消费者读取.
不同消费者组之间的读取进度相互独立, 互不影响. 例子如下 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16Topic: user-actions (3个Partition)
// 组A:实时数据分析
Group: analytics-group
├── Consumer-A1 → P0
├── Consumer-A2 → P1
└── Consumer-A3 → P2
// 组B:数据归档存储
Group: archive-group
├── Consumer-B1 → P0
├── Consumer-B2 → P1
├── Consumer-B3 → P2
└── Consumer-B4 → 无
// 组C:监控告警
Group: monitoring-group
├── Consumer-C1 → P0, p1
└── Consumer-C2 → P2当一个消费者组中有消费者下线, 会触发重平衡机制, 重新分配每个消费者要读的partition.
broker(高扩展性 / 高可用)
一个Broker代表Kafka集群中的一台服务器, 你可以认为topic/partition属于整个系统层面的概念, 而broker是针对物理层面上给出的一个逻辑概念, 用来维护这个系统在多个服务器上的协调工作.
broker的逻辑结构类似如下 :
1 | |
物理结构类似如下 :
1 | |
高扩展性 :
如果所有partition都在一个服务器上运行, 那么单机压力会比较高, 所以可能需要多开几个服务器, 每个作为一个broker, 每个broker负责几个的partition, 就可以分摊压力实现高扩展性.
高可用 :
和redis差不多, 就是保证一个broker挂了之后topic内部的partition还在, 本质就是做主从复制(replicas).
一个topic中对于broker的管理类似如下 :
1
2
3
4
5// 一个3副本的Partition示例
Topic: important-data, Partition 0:
├── Leader副本: Broker 1 (处理所有读写请求)
├── Follower副本: Broker 2 (从Leader同步数据)
└── Follower副本: Broker 3 (从Leader同步数据)Controller :
这是一个特殊的Broker, 整个系统有且仅有1个, 其主要任务就是管理和决策, 其目的可以认为是将每个broker和topic/partition联系起来, 其职责不限于 :
- 监视每个partition的leader, 当leader崩溃时决定由哪个follower接任.
- 如果有新topic的partition加入, 分配给合适的broker.
- 如果有新broker加入, 重新分配每个broker负责的分区.
- 等等
没必要深入理解, 知道有就行.
持久化保留策略
这在上面已经间接提过一些了, 主要就是两点 :
- 过期策略 : 在partition中创建新log时可以设置过期时间, 过期后系统内部会自动删去.
- 大小策略 : 可以对partition的大小设置限制, 当超过总大小后会从前往后依次删除.
ZK保活机制
kafka中有很多的组件, 这些组件大多都是维护在不同的服务器上, 所以为了保证整个系统互通有无, 需要zookeeper作为一个中枢站, 持续保存所有节点的存活状态, 其他节点会监控(心跳)相关节点的加入与删除来触发某些机制.
和zk相关的机制如下 :
- Broker的注册与发现, 当broker数量变动会触发分区leader的重新选举.
- Controller崩溃后重新选举出新Controller.
- 消费者组重平衡, 当消费者组中数目变化时也会之间触发重平衡.
- 等等
不过使用zk是早期版本的行为, 毕竟kafka要使用的只是zk的简单功能, 来保活自己的系统. 现在已经完全移除了对zk的依赖, 被称为KRaft模式, 该模式用raft协议发送心跳和内置一些选举机制来替代zk的功能.