Kafka复习计划 - Kafka基础知识以及集群参方案和参数
前言
本次复习主要是看的胡夕老师的Kafka
核心技术与实战,讲得很好,主要是查缺补漏。
Kafka
是一种消息引擎系统。负责的事情也很简单:
- 系统A将消息发送给
Kafka
。 - 系统B从
Kafka
中读取系统A发送的消息。
Kafa
对于传输的消息编码格式的选择:纯二进制的字节序列。
Kafka
中传输消息的模型有两种:
- 点对点模型(消息队列模型):系统A发送的消息只能被系统B接收。
- 发布 / 订阅模型:拥有主题
Topic
概念,可以有多个发布者Publisher
向相同的主题发送消息。可以同时被多个订阅者Subscriber
接收。
为什么要使用Kafka
?(或者A系统为什么不能直接把消息发送给B系统?)回答:削峰填谷
所谓的“削峰填谷”就是指缓冲上下游瞬时突发流量,使其更平滑。对于那种发送能力很强的上游系统,如果没有消息引擎的保护,“脆弱”的下游系统可能会直接被压垮导致全链路服务“雪崩”。
Kafka
和其他MQ
的区别在哪?比如RabbitMQ
和Kafka
如何选择?
RabbitMQ
属于比较传统的消息队列系统,支持标准的消息队列协议:AMQP、STOMP、MQTT
等。- 因此倘若具体的应用需要支持这些协议,那么选择
RabbitMQ
。 RabbitMQ
还支持比较复杂的消费路由,而Kafka
不支持。
备注:消息传输协议和Http
这一类网络通信协议是不同的!一般两个进程之间进行数据流的交互有三种方式:
- 通过数据库的读写:进程1写数据到数据库,进程2从数据库中读数据。
- 通过服务调用:
RPC
调用或者Rest
调用。HTTP
协议通常就作为rest
方式的底层通讯协议。 - 通过消息传递的方式:进程1发送消息给中间件,例如
Kafka
。进程2从中间件中读取消息。而消息传输协议就运用与此。
那竟然RPC
调用和发MQ
,其本质都是不同进程之间数据流的一个交互,那么MQ
和RPC
的区别又是什么?
MQ
有自己的buffer
,能够对抗过载(overloaded
)。MQ
支持重试retry
。MQ
允许发布/订阅功能。
一. Kafka 相关术语
Kafka
属于分布式的消息引擎系统。主要提供一套完备的消息发布和订阅解决方案。
首先来说下Kafka
的几个最最基础的概念:
- 消息(
Record
):Kafka
处理的主要对象。 - 主题(
Topic
):承载消息的逻辑容器,一般用来区分具体的业务。 - 生产者(
Producer
):向主题发布信息的应用程序。可以向一个或者多个主题发送消息。 - 消费者(
Consumer
):从主题订阅信息的应用程序。可以同时订阅多个主题的消息。 - 消费者组(
Consumer Group
):为了提高吞吐量,将多个消费者实例共同组成一个组,消费同一个主题。但是主题中的每个分区只会被组内的一个消费者实例消费。
其中,生产者和消费者这样的应用程序,我们统一称之为客户端Clients
。那么随之对应的,也有服务端这么一个概念。
1.1 实现高可用的手段
Kafka
的服务器端由称之为Broker
的进程构成。Broker
有什么作用呢?
Broker
负责接收和处理客户端发送过来的请求。- 负责对消息进行持久化。
高可用手段一:一般Kafka
将不同的Broker
分散运行在不同的机器上。这样哪怕某台机器宕机,那么其他的机器的Broker
也能够对外提供服务。
高可用手段二:备份机制(Replication
)。
备份机制的本质就是对数据进行拷贝。Kafka
中,对这些数据拷贝称之为副本(replica
),副本分为两种:
- 领导者副本:
Leader Replica
,负责对外提供服务,与客户端进行交互。 - 追随者副本:
Follower Replica
,只负责更新领导者副本中的数据。
生产者总是向领导者副本写消息,而消费者也总是从领导者副本中读取消息。追随者副本,注意:不提供对外服务,不提供对外服务,不提供对外服务,重要的事情说三遍。只负责与领导者副本保持同步。
紧接着,为了避免单台Broker
上无法容纳更多的数据。Kafka
对副本进行分割,也就是所谓的分区(Partitioning
)。
Kafka
中将每个主题划分为多个分区。每个分区包含一组有序的消息日志。生产者生产的每条消息只会被发送到一个分区中。
1.2 Kafka的三层消息架构
有了主题、副本、分区概念之后,Kafka
可以分为三层的消息架构:
- 主题层:每个主题分为
M
个分区,每个分区又可以分成N
个副本。 - 分区层:每个分区的
N
个副本中,包含1个领导者副本,负责对外提供服务。N-1
个追随者副本,负责提供冗余数据。 - 消息层:分区中包含若干条消息,每条消息的位移从0开始并依次递增。
1.3 Kafka如何持久化数据
Kafka
使用消息日志来保存数据:一种在磁盘上只能通过追加写(Append-only
) 的方式来记录消息的物理文件。
追加写的好处:
- 将缓慢的随机
IO
操作,改为性能较好地顺序IO
写操作。
同时,Kafka
通过日志段(Log Segment
)机制定期的删除消息来回收磁盘空间。
Kafka
底层,一个日志会进一步细分为多个日志段。- 消息在追加写的时候会记录到当前最新的日志段中。
- 当写满一个日志段后,
Kafka
就会自动分配一个新的日志段,并将老的日志段封存。 - 通过定时任务来检查老的日志段是否能够被删除,达到磁盘空间回收的目的。
其他的概念:
- 重平衡(
Rebalance
):消费者组内某个消费者实例挂掉后,将分区的所有权从该消费者转移到另一个消费者。 - 消费者位移(
Consumer Offset
):每个消费者在消费消息的过程中必然需要有个字段记录它当前消费到了分区的哪个位置上。也就是所谓的消费者位移。
1.4 常见问题
为什么Kafka
不支持主从分离?
- 我们知道
Redis
和Mysql
都支持主从的一个读写分离。主从分离只是一种架构设计,往往适用于那种读多写少的应用场景。 而Kafka
的主要应用场景是消息引擎,用于频繁地生产/消费消息。不属于典型的读多写少场景,因此读写分离方案在这个场景下就不太合适。 Kafka
的副本机制方面,使用的是异步消息的拉取。存在leader
和follower
之间的不一致性。 若采用读写分离。必然需要去处理数据的不一致性问题。例如:如何实现read-your-writes
、如何保证单调读,如何处理消息因果序颠倒的问题等。- 此外,
Kafka
的一个数据性质和Mysql
不一样,Kafka
中的数据存在着消费的概念,可以看做流数据,当然并不是说消费了这个数据就不在了,对应的Kafka
中有着消费者位移的概念,可以看做消费的进度在哪了。而数据库就不存在这样的概念。 Kafka
中的分区可以分散到各个Broker
上,也能做到负载均衡的效果。也没必要通过主从读写分离的方式来负载均衡。
read-your-writes
是什么?
- 当生产者成功向
Kafka
中写入消息的时候。马上使用消费者去读取刚刚生产的消息。 - 那么此刻我希望能读到我刚刚写的最新的数据。
- 这里再说一下关于主从问题,倘若
Kafka
允许从副本提供对外服务,那么由于主从之间数据更新的异步性,无法保证客户端从追随者副本中读取到的信息是最新的。也就无法保证read-your-writes
。
单调读的概念?
- 对于一个消费者而言,在多次消费的时候,不会存在某一个消息时而存在时而不存在的情况。
二. Kafka集群部署方案
首先从操作系统的角度来看:目前比较常见的操作系统有三种:Linux
、Windows
和 macOS
。
2.1 操作系统的选择
考虑操作系统和Kafka
之间的适配性,Linux
是更好的选择,主要从以下三点来说:
IO
模型的使用(复习Kafka
的时候,特地去复习了下IO
模型:IO知识复习)。- 数据网络传输效率。
- 社区支持度。
首先对于IO
模型的使用:
Kafka
的客户端底层使用了Java
的selector
。selector
在Linux
上的实现机制就是epoll
,而在Windows
平台上的实现机制是select
。- 因此在这一点上,
Kafka
部署在Linux
上是有优势的,能够获得更高效的IO
性能。epoll
优势更大。
其次对于网络传输效率方面的考虑:若Kafka
在Linux
上部署,能够享受到零拷贝技术带来的快速数据传输特性。
关于零拷贝的复习:Linux - 零拷贝技术
最后再是社区的支持度:一般人都会选择Linux
来搭建。 社区目前对 Windows
平台上发现的 Kafka Bug
不做任何承诺。
2.2 磁盘的选择
我们都知道,固态硬盘比普通的磁盘要快,这里至于为什么快,我感觉也没必要去深入了解,知道就好。而Kafka集群所需要的存储空间还是比较大的,需要考虑这么几个因素:
- 新增的消息数。
- 消息留存时间。
- 平均消息的大小。
- 副本的数量。
- 消息是否启用压缩功能。
而固态硬盘的优势大,但是成本要高,机械硬盘的成本低,但是容易损坏。这里给个参考。
有条件的情况下,无脑选固态硬盘。
若条件比较苛刻,选择机械硬盘的问题不大。理由如下:
Kafka
虽然大量使用磁盘空间,但是使用的方式大部分是顺序读写IO
。因此一定程度上规避了机械磁盘的劣势(随机IO
读写慢)。Kafka
有自己的冗余机制(副本)来提高可靠性,一定程度上弥补了硬盘容易损坏的缺点。
2.3 重要集群参数
2.3.1 Broker端参数
针对日志存储的相关参数:
log.dirs
:没有默认值(因此必须手动指定),用于指定Broker
需要使用的若干个文件目录路径,每个路径之间用逗号
分割。 最好将目录挂载到不同的物理磁盘上,这样就有两个好处:
- 提升读写性能:多块物理磁盘同时读写比单块磁盘要有更高的吞吐量。
- 实现故障转移。
log.dir
:表示单个路径,用于补充上一个参数用的。
Kafka
的运行一般还依赖于zookeeper
,zookeeper
用于协调管理并保存Kafka
集群的所有元数据信息,例如集群中有哪些Broker
、Topic
、分区等信息。
针对zookeeper
的相关参数:
zookeeper.connect
:用于指定zookeeper
的客户端地址,zk1:2181,zk2:2181
,同样可以配置多个zookeeper
(集群)
注意,若有两套Kafka
集群共用一套zookeeper
集群,可以使用chroot
别名来配置,格式如下:
# kafka1 集群
zk1:2181,zk2:2181,zk3:2181/kafka1
# kafka2 集群
zk1:2181,zk2:2181,zk3:2181/kafka2
只需要在zookeeper
集群的最后添加 / [kafka集群名]
即可。
针对Broker
之间连接和通信的参数:
listeners
:用于告诉外部连接者需要通过什么协议访问指定的主机名和端口开放的Kafka
服务。advertised.listeners
:对外发布的监听器,即面向外网。如果客户端仅仅在内网中使用,那么就不需要配置该参数。
每个监听器对于的格式为若干个逗号
分割的三元组:<协议名称,主机名,端口号>
,例如:
CONTROLLER: //localhost:9092
针对Topic
的相关参数:
auto.create.topics.enable
:是否允许自动创建Topic
。最好设置为false
,避免因为操作失误创建错误名称的主题。unclean.leader.election.enable
:是否允许Unclean Leader
选举。auto.leader.rebalance.enable
:是否允许定期进行Leader
选举(在满足一定的条件下)。建议将其改成false
,因为将其设置为true
,会导致一个正常的Leader
副本被其他的副本替换。本质上的性能收益几乎没有,反而替换一次的Leader
代价却很高。因此生产环境中建议设置为false
。
关于Unclean Leader
选举:
背景:Kafka
中,每个分区都有多个副本来提供高可用和备份,但是只有一个副本能作为Leader
。因此副本之间需要选举出一个Leader
来,但并非所有副本都具有资格参与竞选,只有保存数据比较多的副本才有资格。
那么当拥有竞选资格的副本都挂了的前提下,此时还需不需要进行Leader
选举? 该参数用于这样的情形:
unclean.leader.election.enable = false
:即使发生上述情况,依旧不会竞选新Leader
。后果:该分区不可用。unclean.leader.election.enable = true
:若发生上述情况,此时Kafka
允许从落后的副本中竞选出新副本。后果:可能有数据丢失的情况(落后副本本身保存的数据就不全)
针对数据留存的相关参数:
log.retention.{hour|minutes|ms}
:控制一条消息数据能够被保存多长的时间,优先级:ms > minutes > hour
。log.retention.bytes
:指定Broker
为消息保存的总磁盘容量大小。默认值为-1
,意思是条件允许的情况下,可以在该Broker
上保存任意量的数据。一般来说使用场景在于:限制某个用户使用的磁盘空间。message.max.bytes
:控制Broker
能够接收的最大消息大小。默认1000012 bytes
,还不到1MB
,实际生产上的消息大小往往可能会超过1MB
。
注意:上述提及的所有参数,都不要使用默认值的参数,最好要自己手动配置。
注意:上述提及的所有参数,都不要使用默认值的参数,最好要自己手动配置。
注意:上述提及的所有参数,都不要使用默认值的参数,最好要自己手动配置。
2.3.2 Topic级别参数
前提:Topic
级别参数的优先级 > Broker
端参数(全局)
重要的三个参数(和上面全局的配置非常相似):
retention.ms
:规定了该Topic
消息被保存的时长,默认7天,可覆盖全局值。retention.bytes
:规定了需要为该Topic
预留多大的磁盘空间。max.message.bytes
:能够正常接收该Topic
的最大消息大小。
在创建Topic
的时候,可以通过以下命令来执行:
bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic transaction \
--partitions 1 --replication-factor 1 --config retention.ms=15552000000 \
--config max.message.bytes=5242880
在修改Topic
的时候,可以通过以下命令来执行:
bin/kafka-configs.sh --zookeeper localhost:2181 --entity-type topics \
--entity-name transaction --alter --add-config max.message.bytes=10485760
2.3.3 JVM参数
参考配置如下:
# 先设置对于的环境变量
export KAFKA_HEAP_OPTS=--Xms6g --Xmx6g
export KAFKA_JVM_PERFORMANCE_OPTS= -server -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35 -XX:+ExplicitGCInvokesConcurrent -Djava.awt.headless=true
# 启动Kafka
bin/kafka-server-start.sh config/server.properties
2.3.4 操作系统参数
对Kafka
中影响相对较大的OS
参数有四个:
- 文件描述符限制。
- 文件系统类型:指的是诸如
ext3、ext4
或者XFS
这样的日志文件系统,生产上推荐使用XFS
。 Swappiness
:将其设置为一个较小的值,倘若设置为0,当物理内存耗尽时,操作系统会触发OOM killer
这个组件,它会随机挑选一个进程然后将其停止,不会留下任何的预警。- 提交时间。
重点说下文件描述符限制。文件描述符限制实际上调大此值并不会造成什么严重的影响。当我们遇到这样的报错信息:Too many open files
,就说明咱们的文件描述符限制太小了,需要将其调大。例如:
ulimit -n 1000000
然后说下提交时间(Flush
刷新落盘时间)。
首先kafka
中,何时认为数据已经写入成功?只要数据被写入到操作系统中的页缓存(Page Cache
)上就可以了,即内核缓冲区中的一部分。而并不是说等数据被写入磁盘才认定为成功。
写入到页缓存中的数据,操作系统会根据LRU
算法定期地将其写入到物理磁盘上,而这个定期就是由提交时间来确定的,默认是5秒,一般情况下,我们可以适当的正常这个时间。
哪怕页缓存中的数据在写入到磁盘之前宕机了,发生了数据的丢失,但是鉴于Kafka
的多副本机制,对于数据丢失的情况有所改善,因此这里可以稍微增加点提交时间,提升性能也是可以的。