AkiraZheng's Time.

分布式Kafka学习

Word count: 3.5kReading time: 12 min
2024/08/05

一、消息队列

消息队列是传递消息的容器(队列),自然遵循先进先出的原则。

消息队列的三大主要作用

  • 解耦无依赖的业务间解耦(不需要给上一个任务返回结果),因此可以处理统一条消息的多个任务
  • 异步:耗时任务提交给消息队列后,立即返回,不阻塞,允许水平扩展多个机器,轮询处理结果(还是需要返回结果的)
  • 削峰填谷:流量控制,特别是在秒杀场景避免瞬间大量请求导致系统崩溃

下面将介绍一下保证消息队列性能的一些设计

1. 消息可靠性:保证消息不丢失

1)三种消息丢失场景

在以下三个环节有可能发生消息丢失:

  • 生产阶段:生产者发送消息时,网络中断等原因导致消息丢失
  • 存储阶段(到达中间件broken):消息队列中间件接收到消息后,存储阶段可能导致消息丢失
  • 消费阶段:消费者消费消息进行处理后,未成功提交偏移量就会导致消息丢失

2)保证消息不丢失的方法

针对以上三种场景,可以采取以下措施:

  • 生产阶段:需要考虑网络中断问题
    • 可以采用客户端重试机制
    • ACK消息确认机制(即生产者发送消息后消息队列返回确认消息,生产者再发送下一条消息)来保证消息at least once发送
      • Kafka生产侧有3种返回acks的配置方式:0(不返回)、1(也是默认的)(只需要leader确认持久化后的ACK,不等待所有副本的同步)、all或-1(需要leader及所有ISR副本都同步后返回ACK)
  • 存储阶段
    • 消息队列中间件可以采用MySQL数据库持久化的方式,将消息持久化到磁盘
    • 同时配合生产阶段,当确认持久化完成后,再返回ACK确认消息
  • 消费阶段
    • 消费者消费消息后需要提交偏移量,以保证消息至少被处理一次
      • 说明:这样就算消费者宕机,重启后也可以再拉取消息消费

三种阶段的消息丢失场景示意图:

2. 消息顺序性:保证消息有序

有序指的是消息的消费顺序发送的顺序一致,即先发送的消息先被消费

Kafka的一个topic主题很多不同的partition存储,在一个Partition里是有序的,所以可以在Partition的路由上实现有序

在实际中,我们可以根据不同场景有序性的需求,来进行路由设计

  • 业务分区:不同用户的操作必须有序
    • 每个细化的子业务有一个单独的key分到某个Partition
    • 扩展性低,当某个业务增长快的话,会加重那个Partition的压力
  • 客户分区同一个用户的操作必须有序
    • user_id%n作为路由规则,分到某个Partition
      • 缺点:扩展新Partition分区后需要全部迁移数据
    • hash(id)%哈希槽(每个Partition分配几个负责的节点),通过循环哈希环减少迁移压力(通过分配一些槽给新Partition,这样只需要将这些槽进行迁移)

比如在交易场景下,某个用户账户余额为0,我们要保证用户先充值1000元再转账2000元的可实现性,这个时候就要求必须按顺序执行这个用户的多条消息,否则在充值1000元还没处理完的情况下,转账2000元就会出现余额不足

  • 解决方法:要保证同一个用户的多条消息必须有序,通过user_id路由同一个用户永远打到一个Partition中

路由方式示意图:

3. 消息幂等性:保证消息不重复

消息幂等的方式一般就是保证对外接口的幂等性,即多次调用接口返回的结果是一样的,在数学上表示为f(f(x))=f(x),比如绝对值函数abs(abs(x))=abs(x)

1)幂等性的场景

首先要让消息不重复同样需要控制三个阶段:

  • 生产阶段由于网络抖动和Web的重试机制导致重复发送,一个相同的请求总有概率重放到Kafka中,所以需要引入幂等式
  • 存储阶段:天然不重复存储
  • 消费阶段消费者消费后,未及时提交偏移量,重启后再次拉取消费

2)CRUD接口的幂等性

后端操作中的接口一般就是CRUD操作,而这4种接口的幂等性如下:

接口类型 幂等性描述
新增插入 必须保证幂等性,因为重复插入会导致数据重复
删除 不需要保证幂等性,因为重复删除的最终结果都是删掉这条数据
更新 将字段确定性的更新,不需要保证幂等性,因为重复更新的最终结果都是一样的;但是如果是非确定性更新(如自增),则需要保证幂等性
查询 不需要保证幂等性,因为事务性操作重复查询的结果都是一样的

综上所述,增(数据库唯一ID)自增性的改操作(乐观锁version)都要保证幂等

3)幂等性的实现

在解决消息重复的问题上,一般有以下几种方式:

  • 唯一ID(最常用,适用于新增插入)服务端采用分布式ID(UUID/雪花算法)生成唯一ID返回给客户端,字段中ID字段为有唯一性约束
    • 为什么要用分布式ID?UUID/雪花算法分库下保证全局唯一,而自增不能保证
    • 为什么是服务端生成?因为客户端生成的话,不同客户端生成的ID可能会重复
    • 服务的生成的唯一ID什么时候能起幂等作用?
      • 数据网络抖动的多次发送主要是由前端去控制的,全局唯一ID的后端接口幂等其实是为了保证当用户觉得该条消息被消费很久没得到返回,会重新根据前面拿过的全局唯一ID再次请求,此时如果服务端发现该ID已经存在则不会再插入
  • 防重表(适用于新增插入)允许ID重复,但是加了一个唯一索引字段unique_key用于隐性保证唯一性
  • 乐观锁(适用于更新)version字段作为乐观锁,每次更新时version+1,这样即使重复更新也不会影响数据
    • step1:客户端查询version
    • step2:客户端发起更新请求,带上version
    • step3:服务端校验version,如果一致则更新,否则返回错误
  • Token(同时适用于增删更)Token一次性的删除后不能再次删除,因此重复删除不会有影响

4)redis和数据库的幂等性

  • redis
    • 唯一ID:每个消息分配一个唯一ID
    • Redis Set:将已消费的消息ID存入Redis Set中,每次消费前先判断是否在Set中
    • 原子操作:SISMEMBER和SADD是原子操作,保证了幂等性
  • 数据库
    • 唯一ID约束:ID字段设置唯一约束
    • INSERT IGNORE:插入时使用INSERT IGNORE,如果ID重复则不插入
    • 事务:使用事务保证原子性
  • Redis和MySQL结合实现幂等:在MySQL唯一ID前面加一层Redis Set先过滤重复的UID

4. 消息的积压处理

1)消息积压的原因

  • 消费者处理能力不足:消费者处理消息的速度不够快,导致消息积压
  • 消费者线程阻塞
  • 消费者宕机

2)消息积压的解决方案

  • 扩容提高消费速率:增加消费者进行水平扩容
    • 由于要保持消息有序性,因此一个Partition只能有一个消费者,所以水平扩容也是有要求的,当消费者与Partition持平时,还要扩容Partition
  • 服务主动降级:紧急情况下可以先取消一些非核心业务或流程,如阿里通过orange开关取消某个非核心业务链路

二、Kafka

1. Kafka简介

Kafka是分布式消息队列,是高吞吐量的分布式发布订阅消息系统,具有持久性(zookeeper提供的主从管理)高性能(10w/s级别)高可用性(健壮的副本Partition)最终一致性(主从一致)等特点。

单机吞吐支持10W/s级别,与RocketMQ相比,其不具备消息回溯能力且支持主题数在百级(RocketMQ支持千级)。

2. Kafka架构

Kafka的架构主要包括生产者消费者broker(单机)topic(消息发布订阅模式)partition(一个topic的Partition可以分布在多台机器上-主从一致)replicaleaderfollower等概念。

除此以外,还有zookeeper用于管理集群broker,它相当于管家,可以选举leader、follow保存副本,保证数据可用性(可以用于broken的注册、发现与选举)

总的架构如下:

Kafka的设计理念可分解为以下主要部分:

1)MQ数据堆积

MQ中数据堆积本质是消费者消费能力差,可以通过增加消费者线程,也就是多消费者模式,同理也可以添加多生产者提高吞吐量

2)多生产者和多消费者竞争MQ

分Topic

将消息队列根据不同主题Topic分为多个MQ减少冲突等待

Topic的Partition分区

单个Topic中还可以再细分成多个Partition分区,每个消费者对应一个Partition分区,从而降低多线程竞争

3)高性能

将多个Partition分布在不同机器上,每个机器称为broker

4)高可用

单个broker如果宕机了,该部分的功能将无法继续进行,因此可以设计leader-follower的集群方式

leader负责读写数据,follower负责复制数据,当leader宕机时可以从follower中选举出新的leader

5)持久化

数据放在内存中有宕机丢失的风险,因此数据还应该具备持久化到磁盘的能力

同时为了防止磁盘溢出,还应该设置过期时间

3. Kafka的优缺点

3.1 优点

  • 高吞吐量:支持10W/s级别的消息堆积吞吐量(RocketMQ一样)
  • 高可用性:通过leader-follower机制保证数据的可用性
  • 持久化:支持数据持久化到磁盘
  • 分布式:支持多broker多partition多topic的分布式架构
  • 水平扩展:支持水平扩展,可以通过增加broker、partition等方式提高吞吐量
  • 支持批量处理(RocketMQ不支持)
  • 支持消息顺序:在一个队列中可靠的先进先出(FIFO)和严格的顺序传递;支持拉(pull)和推(push)两种消息模式;

3.2 缺点

  • 不支持消息回溯:Kafka不支持消息回溯,即消费者消费过的消息无法再次消费,也就不支持重试
  • 使用短轮询,实时性取决于轮询频率

4. Kafka高性能的原因

Kafka高性能的原因有:零拷贝批量操作顺序写、数据压缩、多层次、页缓存

4.1 零拷贝*

零拷贝:优化网络数据从本地到网卡发送的拷贝次数

操作:将数据直接从内核空间的磁盘文件直接拷贝到网卡中,不经过用户态

优点:系统调用从2次变成1次、拷贝由4次变成2次

原来的网卡数据发送流程:

零拷贝的数据发送流程:

4.2 批量操作*

批量操作:一次性拉取多条消息进行消费

方法包括:累积到一定时间ms就操作、累积到一定batchSize数据量就操作、累积到一定缓存大小就操作

优点:节省宽带

缺点:造成延时

4.3 顺序写*

顺序写:指磁盘的顺序写,使磁盘写性能更接近内存性能

操作:Kafka写入数据其实就是添加到每个Partition的末端(也就是磁盘文件)

原因:非顺序写磁盘需要转动寻址、对齐扇区,速度慢

4.4 数据压缩

数据压缩:生产者/broken压缩-消费者解压数据

优点:降低网络需求和存储压力

4.5 多层次

多层次:Kafka利用分治Partition主从思想,将顺序IO存储压力进行切分

Topic=n*Partition=m*broken,Kafka返回路由信息,使其按规则访问对应的Partition,每个Partition分为3个文件作为索引文件实现快速查找

分治思想:一个业务写入一个topic —> 一个topic具有多个切片Partition —> 一个Partition有多个Broken用作数据主从存储

4.6 页缓存

页缓存:消息进入Kafka后线写入内存缓存PageCache后面再由操作系统刷入磁盘(肯定需要刷入磁盘防止电脑断电、重启导致数据丢失)

优点:当查询的数据打中PageCache时可以直接从PageCache中获取数据,提升效率(未完待续:需要了解什么情况下会去命中PageCache)

从PageCache到磁盘同步固化机制:

  • 1)内存空间<=阈值后,PageCache输入磁盘并被释放
  • 2)脏页在内存驻留一定时间后
    • 脏页:当消息被写入PageCache后,由于不是及时更新到磁盘的,所以此时页缓存数据跟磁盘不一致,被称为脏页
    • 主动调用刷脏方法:sync或fsync方法(系统调用)
CATALOG
  1. 一、消息队列
    1. 1. 消息可靠性:保证消息不丢失
    2. 2. 消息顺序性:保证消息有序
    3. 3. 消息幂等性:保证消息不重复
    4. 4. 消息的积压处理
  2. 二、Kafka
    1. 1. Kafka简介
    2. 2. Kafka架构
    3. 3. Kafka的优缺点
      1. 3.1 优点
      2. 3.2 缺点
    4. 4. Kafka高性能的原因
      1. 4.1 零拷贝*
      2. 4.2 批量操作*
      3. 4.3 顺序写*
      4. 4.4 数据压缩
      5. 4.5 多层次
      6. 4.6 页缓存