Apache Kafka生产环境集群资源规划与配置
更多精选文章,可微信搜索 知了小巷,关注公众号并回复 资料 两个字,有大数据学习资料和视频。
Kafka集群资源规划
可以参考confluent版的部署建议:
https://docs.confluent.io/current/kafka/deployment.html
操作系统
Kafka源码既包括Scala也有Java源文件,属于JVM体系的大数据框架。Java是跨平台语言,源码编译后可以运行在不同操作系统对应的JVM上面。尽管如此,Kafka运行在不同操作系统上还是有些差异的,而且不同操作系统的差异会给Kafka集群带来相当大的影响。
实际生产中部署Kafka集群最多的仍然是Linux Server。
一般考虑以下三点因素:
- 操作系统底层提供的I/O模型支持
- 数据在网络中的传输效率
- 社区活跃和支持度
主流的IO模型通常有如下5种:
- 阻塞I/O(Bloking I/O)
同步阻塞:线程发起IO系统调用后会被被阻塞,转到内核空间处理,整个IO处理完毕后返回数据。
优缺点:一般需要给每个IO请求分配一个IO线程,因此系统开销大。
典型应用:阻塞Socket、Java BIO - 非阻塞I/O(Non-Blocking I/O)
同步非阻塞:线程不断轮询读取内核IO设备缓冲区,如果没数据则立即返回EWOULDBLOCK,有则返回数据。
优缺点:CPU消耗多,无效IO多。
典型应用:非阻塞Socket(设置为NONBLOCK) - 同步多路复用I/O(Multiplexing I/O)
线程调用select/poll/epoll传入多个设备fd,然后阻塞或者轮询等待。如果有I/O设备准备好则返回可读条件,用户线程主动调用I/O读写,如果没有则继续阻塞。
优缺点:相比前两种模型,I/O复用可以监听多个I/O设备,所以一个线程内可以处理多个网络请求。
典型应用:select、poll、epoll。Nginx以及Java NIO基于此I/O模型。 - 信号驱动I/O(Signal-Driven I/O)
利用Linux信号机制,用sigaction函数将SIGIO读写信号以及handler回调函数存在内核队列中。当设备I/O缓冲区可写或可读时触发SIGIO中断,返回设备fd并回调handler。 - 异步I/O(Asynchronous I/O)
Linux AIO有两种主流实现:glibc aio 和 libaio/内核aio。
Windows系统提供了一个叫 IOCP 线程模型属于异步I/O。
Kafka与I/O模型的关系
Kafka的KafkaServer、KafkaProducer、KafkaConsumer直接使用的是Kafka自己实现的Selector,底层用到了Java NIO的channels API,当然也包括:
private final java.nio.channels.Selector nioSelector;。
Kafka实现的Selector:
kafka/clients/src/main/java/org/apache/kafka/common/network/Selector.java
// ...
import java.nio.channels.CancelledKeyException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.channels.UnresolvedAddressException;
// A nioSelector interface for doing non-blocking multi-connection network I/O.
public class Selector implements Selectable, AutoCloseable {
// ...
}
Java NIO Selector在Linux上的实现机制是epoll,而在Windows平台上的实现机制是select。因此在这一点上将Kafka部署在Linux Server上是有优势的,能够获得更高效的I/O性能。
网络传输效率
KafkaProducer向Broker发送消息以及KafkaConsumer从Broker消费消息,都是通过网络传输的,但是消息会持久化到磁盘里面,所以Kafka需要在磁盘和网络间进行大量数据传输。当数据在磁盘和网络中进行传输时,尽量避免在操作系统内核态中拷贝数据,从而实现快速地数据传输。Linux平台实现了零拷贝机制, Windows平台JDK8也实现了类似机制,目前JDK版本上面问题不大。
对于零拷贝,首先想到的是不为0或者大于0的拷贝是什么,在哪里发生的,从哪儿拷贝到哪儿?
非零拷贝
应用进程发起read请求,内核接收到read请求之后,会先检查内核空间中是否已经存在进程所需要的数据,如果已经存在,则直接把数据copy给进程的缓冲区;如果没有,内核随即向磁盘控制器发出命令,要求从磁盘读取数据,磁盘控制器把数据直接写入内核read缓冲区,这一步通过DMA完成;接下来就是内核将数据copy到应用进程的缓冲区;如果应用进程发起write请求,同样需要把用户缓冲区里面的数据copy到内核的socket缓冲区里面,然后再通过DMA把数据copy到网卡中,发送出去。
上面的点就在于,每次都需要把内核空间的数据拷贝到用户空间。
4个地方:内核缓冲区、进程即用户缓冲区、磁盘、网络。
当数据从磁盘读出传输到网络时,磁盘->内核缓冲区(read)->用户缓冲区->内核缓冲区(socket)->网络;如果能将数据直接从内核缓冲区(read) 传输到 内核缓冲区(socket)就好了,让数据传送只发生在内核空间;如果内核缓冲区缓存的是数据内存地址和偏移量,连内核空间中的一次cpu copy也省掉了,就变成0拷贝了。
Java中零拷贝:
- MappedByteBuffer 虚拟内存映射
- DirectByteBuffer 继承于MappedByteBuffer
- Channel-to-Channel传输 sendfile
Linux操作系统主要是两种方式:1. mmap+write方式 2. sendfile方式。
Kafka中FileRecords里面writeTo就用到了FileChannel#transferTo方法,底层是sendfile。但是transferTo、transferFrom方法实现里面也用到了MappedByteBuffer。
社区支持方面始终是以Linux系统为主,Windows平台BUG几乎不怎么维护
毫无疑问,生产环境选择Linux Server。
磁盘
磁盘规划对Kafka来说非常重要,一般就是在机械磁盘和SSD固态硬盘之间做选择。
- 机械磁盘: 成本低且容量大,但易损坏
- SSD:性能优势大,不过单价高
由于Kafka Log顺序写读的特性,使用普通机械硬盘就可以了。
Kafka大量使用磁盘,但它使用的方式多是顺序读写操作,一定程度上规避了机械磁盘最大的劣势,即随机读写操作慢的问题。从这一点上来说,使用SSD似乎并没有太大的性能优势,毕竟从性价比上来说,机械磁盘物美价廉,而它因易损坏而造成的可靠性差等缺陷,又由Kafka 在软件层面提供机制(多副本)来保证,因此使用普通机械磁盘是很划算的。
关于磁盘阵列(RAID)的优势:
- 提供冗余的磁盘存储空间
- 提供负载均衡
RAID的两个优势对于任何一个分布式系统都很有吸引力。不过,Kafka实现了多副本机制来提高可靠性;通过对数据进行分区存储(读写),也在软件层面实现了负载均衡。
- 可以不搭建RAID,普通磁盘即可
- 机械磁盘完全胜任生产Kafka环境
磁盘容量
Kafka集群预期到底需要多少存储空间?
Kafka需要将消息保存在底层的磁盘上,这些消息默认会被保存一段时间然后自动被删除。虽然这段时间是可以配置的,但仍然需要结合自身具体业务场景和存储需求来规划Kafka集群的存储容量。
假设现在每天要向Kafka集群发送1亿条消息,为了防止消息丢失,每条消息存储两份,并且默认保存两周时间,两周以后会自动被删除掉。一条消息的大小为1KB,每天1亿条1KB大小的消息,保存两份且留存两周的时间,那么总的空间大小就等于1亿 * 1KB * 2 / 1000 / 1000 = 200GB,预留10%的磁盘空间用于存储其他类型的数据(比如索引数据等),假设最终总的存储容量就是220GB。要保存两周,那么整体容量即为220GB * 14,大约3TB左右。但是我们可以对消息进行压缩,假设压缩比是75%,0.75 * 3 = 2.25TB。
存储这一块,主要考虑以下因素:
- 新增加的消息数量(增量)
- 消息留存时间
- 平均消息大小
- 副本数量,比如2~3
- 是否启用压缩
网络带宽
客观上来说,对于Kafka,网络容易成为瓶颈,因为需要通过网络进行大量的数据传输,无论是生产者向Broker发送消息,还是消费者从Broker消费消息,又或者Follower副本从Leader副本同步消息(一般为内网)。
普通的以太网络,带宽主要有两种:1Gbps的千兆网络和10Gbps的万兆网络,特别是千兆网络应该是一般公司网络的标准配置。
1 Gbps = 1,000 Mbps,注意是小写的b。
大写B代表Byte,小写b代表bit,1 Byte = 8 bit。
带宽是1Gbps,即每秒处理1Gb的数据,假设每台Kafka服务器都是安装在专属的机器上,也就是说每台Kafka机器上没有混布其他服务,真实环境中也不建议这么做。假设Kafka会用到 70%的带宽资源,因为总要为其他应用或进程留一些资源。所以,单台Kafka服务器最多也就能使用大约700Mb的带宽资源,既然是最多,实际上可能只会用到三分之一,700Mb / 3 ≈ 240Mbps。那么,1小时内处理1TB数据需要多少台Kafka机器呢?1小时1TB,即每秒需要处理2336Mb的数据(10241024/36008),2336Mb / 240Mbps ≈ 10,即约等于10台服务器,再加上副本,额外乘以3,则最终大约需要30个Kafka节点。
Kafka集群参数配置
Broker端参数
server.properties
- broker.id:The broker id for this server. If unset, a unique broker id will be generated
- log.dirs:指定了Broker需要使用的若干个文件目录路径,必须手动指定
- log.dir:只能表示单个路径,它是补充log.dirs参数用的。
- zookeeper.connect
- listeners
- advertised.listeners
- auto.create.topics.enable
- unclean.leader.election.enable
- auto.leader.rebalance.enable
- log.retention.{hour|minutes|ms}
- log.retention.bytes
- message.max.bytes
broker.id: To avoid conflicts between zookeeper generated broker id’s and user configured broker id’s, generated broker ids start from reserved.broker.max.id + 1.
Type: int
Default: -1
Valid Values:
Importance: high
Update Mode: read-only
只要设置log.dirs即可,可以不用再去设置log.dir。
# A comma separated list of directories under which to store log files
# 千万不要放在/tmp目录下...
log.dirs=/tmp/kafka-logs
log.dirs: The directories in which the log data is kept. If not set, the value in log.dir is used
Type: string
Default: null
Valid Values:
Importance: high
Update Mode: read-only
log.dir
log.dir: The directory in which the log data is kept (supplemental for log.dirs property)
Type: string
Default: /tmp/kafka-logs
Valid Values:
Importance: high
Update Mode: read-only
生产环境中一定要为log.dirs配置多个路径,具体格式是一个逗号隔开的CSV格式,比如 /xxxx/kafka1,/xxxx/kafka2,/xxxx/kafka3 这样。如果有条件的话最好保证这些目录挂载到不同的物理磁盘上。这样做有两个好处:
- 提升读写性能:比起单块磁盘,多块物理磁盘同时读写数据有更高的吞吐量。
- 能够实现故障转移:即 Failover。Kafka 1.1版本之后,如果有磁盘坏掉了,会自动在其他正常的磁盘上重建副本,而且Broker还能正常工作。这也是Kafka能够舍弃磁盘阵列(RAID)的基础。
与Zookeeper相关的参数
zookeeper.connect
zookeeper.connect: Specifies the ZooKeeper connection string in the form hostname:port where host and port are the host and port of a ZooKeeper server.
# Zookeeper connection string (see zookeeper docs for details).
# This is a comma separated host:port pairs, each corresponding to a zk
# server. e.g. "127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002".
# You can also append an optional chroot string to the urls to specify the
# root directory for all kafka znodes.
# hostname1:port1,hostname2:port2,hostname3:port3
zookeeper.connect=localhost:2181
# Timeout in ms for connecting to zookeeper
zookeeper.connection.timeout.ms=6000
一般配置:
hostname1:port1,hostname2:port2,hostname3:port3
让多个Kafka集群使用同一套ZooKeeper集群:
- kafka1
hostname1:port1,hostname2:port2,hostname3:port3/chroot/kafka1 - kafka2
hostname1:port1,hostname2:port2,hostname3:port3/chroot/kafka2
与Broker连接相关的参数
# The address the socket server listens on. It will get the value returned from
# java.net.InetAddress.getCanonicalHostName() if not configured.
# FORMAT:
# listeners = listener_name://host_name:port
# EXAMPLE:
# listeners = PLAINTEXT://your.host.name:9092
# 内网访问,可以设置为内网IP
#listeners=PLAINTEXT://:9092
# Hostname and port the broker will advertise to producers and consumers. If not set,
# it uses the value for "listeners" if configured. Otherwise, it will use the value
# returned from java.net.InetAddress.getCanonicalHostName().
# 主要用于外网访问,可以设置为外网IP
#advertised.listeners=PLAINTEXT://your.host.name:9092
PLAINTEXT:表示明文传输、SSL表示使用SSL或TLS加密传输等;也可能是自己定义的协议名字,比如CONTROLLER://localhost:9092
# Maps listener names to security protocols, the default is for them to be the same. See the config documentation for more details
#listener.security.protocol.map=PLAINTEXT:PLAINTEXT,SSL:SSL,SASL_PLAINTEXT:SASL_PLAINTEXT,SASL_SSL:SASL_SSL
上面host是IP还是hostname:最好全部使用主机名,即Broker端和Client端应用配置中全部填写主机名。
关于Topic管理的参数
- auto.create.topics.enable:是否允许自动创建Topic。
- unclean.leader.election.enable:是否允许Unclean Leader选举。
- auto.leader.rebalance.enable:是否允许定期进行Leader选举。
生产环境最好不要自动创建topic;而且要对即将用到的topic有专门的申请审核机制,由运维严格把控,决不能允许自行创建任何Topic。
Kafka的Leader选举,并不是所有的副本都能选为Leader,只有数据比较多的才有机会,数据落后比较多的副本没有机会称为Leader。如果数据较多的副本全都挂了怎么办??如果unclean…参数为false,就是坚决不能让那些落后太多的副本竞选Leader。如果unclean参数…是true,那么Kafka允许从那些“跑得慢”的副本中选一个出来当Leader。这样做的后果是数据有可能就丢失了,因为这些副本保存的数据本来就不全,
auto.leader.rebalance.enable如果设置为true,则会允许Kafka定期地对一些Topic分区进行Leader重选举。这里并不是选Leader,而是换一个新的Leader。换一次Leader是要付出代价的,而且请求也要切换到新的Leader,因此建议置为false。
关于数据留存的参数
- log.retention.{hour|minutes|ms}:控制一条消息数据被保存多长时间。从优先级上来说ms设置最高、minutes 次之、hour最低。
- log.retention.bytes:这是指定Broker为消息保存的总磁盘容量大小。
- message.max.bytes:控制Broker能够接收的最大消息大小。
# The minimum age of a log file to be eligible for deletion due to age
# 168表示默认保存7天的数据,自动删除7天前的数据
# 很多公司把Kafka当做存储来使用,那么这个值就要相应地调大
log.retention.hours=168
#log.retention.bytes=1073741824
# 默认1000012
# 生产环境有必要根据实际情况设置一个比较大的值
# The largest record batch size allowed by Kafka.
message.max.bytes
Topic级别的参数
Topic级别参数会覆盖全局Broker参数的值,而每个Topic都能设置自己的参数值,这就是所谓的Topic级别参数。
关于数据留存时间的设置,实际生产中,可以允许不同部门的Topic根据自身业务需要,设置适合自己Topic的留存时间。
retention.ms: This configuration controls the maximum time we will retain a log before we will discard old log segments to free up space if we are using the “delete” retention policy. This represents an SLA on how soon consumers must read their data. If set to -1, no time limit is applied.
Type: long
Default: 604800000
Valid Values: [-1,…]
Server Default Property: log.retention.ms
Importance: medium
- retention.ms:规定了该Topic消息被保存的时长。默认是7天,即该Topic只保存最近 7天的消息。一旦设置了这个值,它会覆盖掉Broker端的全局参数值。
- retention.bytes:规定了要为该Topic预留多大的磁盘空间。和全局参数作用相似,这个值通常在多租户的Kafka集群中会有用武之地。当前默认值是-1,表示可以无限使用磁盘空间。
Topic级别参数的设置:
- 创建Topic时进行设置
- 修改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
使用kafka-configs来修改Topic级别参数:
bin/kafka-configs.sh --zookeeper localhost:2181 --entity-type topics--entity-name transaction --alter --add-config max.message.bytes=10485760
JVM参数
Kafka自2.0.0版本开始,已经正式摒弃对Java 7的支持了,所以至少也是使用Java 8。JVM端设置,堆大小这个参数至关重要。
JVM堆大小设置成6GB,业界比较公认的一个合理值。
Java8 GC手动指定为G1收集器。
设置两个环境变量:
- KAFKA_HEAP_OPTS:指定堆大小。
- KAFKA_JVM_PERFORMANCE_OPTS:指定GC参数。
$ 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
$ bin/kafka-server-start.sh config/server.properties
操作系统参数
OS一般关注:
- 文件描述符限制
- 文件系统类型
- Swappiness
- 提交时间
“Too many open files”
最重要的ulimit -n,每一个Java应用,一个 Java 项目最好都调整下这个值。
$ ulimit -n
256
文件系统类型的选择
根据官网的测试报告,XFS的性能要强于ext4,所以生产环境最好还是使用XFS,也可以试试ZFS。
swap的调优
swap可以设置成一个较小的值而不是0。因为一旦设置成0,当物理内存耗尽时,操作系统会触发OOM killer这个组件,它会随机挑选一个进程然后kill掉,即根本不给用户任何的预警。但如果设置成一个比较小的值,当开始使用swap空间时,我们至少能够观测到Broker性能开始出现急剧下降,从而给我们进一步调优和诊断问题的时间。基于这个考虑,建议将swappniess配置成一个接近0 但不为0的值,比如1。
提交时间或者说是Flush落盘时间
向Kafka发送数据并不是真要等数据被写入磁盘才会认为成功,而是只要数据被写入到操作系统的页缓存(Page Cache)上就可以了,随后操作系统根据LRU算法会定期将页缓存上的“脏”数据落盘到物理磁盘上。这个定期就是由提交时间来确定的,默认是5秒。一般情况下我们会认为这个时间太频繁了,可以适当地增加提交间隔来降低物理磁盘的写操作。当然一般可能会有这样的疑问:如果在页缓存中的数据在写入到磁盘前机器宕机了,那岂不是数据就丢失了。的确,这种情况数据确实就丢失了,但鉴于Kafka在软件层面已经提供了多副本的冗余机制,因此这里稍微拉大提交间隔去换取性能还是一个合理的做法。
Page Cache刷写磁盘策略
- 页高速缓存变得太满,但还需要更多的页,或者脏页的数量已经太多。
- 自从页变成脏页以来已过去太长的时间。
- 用户进程通过调用sync()、fsync()或者fdatasync()系统调动来触发。
前两项都可以通过系统参数配置来调整。
使用sysctl -a | grep dirty命令可以查看默认配置:
# sysctl -a | grep dirty
vm.dirty_background_bytes = 0
vm.dirty_background_ratio = 10
vm.dirty_bytes = 0
vm.dirty_expire_centisecs = 3000
vm.dirty_ratio = 20
# 5秒在这里
# 理论上调小这个参数,可以提高刷磁盘的频率,从而尽快把脏数据刷新到磁盘上。但一定要保证间隔时间内一定可以让数据刷盘完成。
vm.dirty_writeback_centisecs = 500
Page Cache缓存查看工具
git clone --depth 1 https://github.com/brendangregg/perf-tools