- 生产环境中的kafka集群应该怎么做?
下面从操作系统、磁盘、磁盘容量、带宽等方面讨论:
操作系统:不同的操作系统给kafka带来的影响是非常大的。目前常见的操作系统有三类:Windows、linux、MacOS。部署在linux应该是最常见的,因为linux比其他两个更适合kafka,主要体现在这三个方面:IO模型的使用、数据网络传输效率、社区支持度。
1. 主流的IO模型通常有5种类型:阻塞式IO、非阻塞式IO、IO多路复用、信号驱动IO、异步IO。每种IO模型都有各自的经典应用场景,Java中Socket对象的阻塞模式和非阻塞模式就对应于前两种,linux中系统调用select函数属于IO多路复用、epoll系统调用属于第三种和第四种之间,第五种在linux中很少有支持,通常认为后面的会比前面的更高级。IO模型与kafka关系是什么?实际上kafka客户端底层使用Java的selector,selector在linux上的实现机制是epoll,在Windows上是select,所以部署在linux能够获取更高效的IO性能。
2. 网络传输效率:kafka生产和消费的消息都是通过网络传输,消息保存在磁盘。所以kafka在磁盘和网络间进行大量数据传输,linux中有Zero Copy技术,就是当数据在磁盘和网络进行传输时避免昂贵的内核态数据拷贝从而实现快速递数据传输。windows平台必须要在Java 8的60更新版本才能有。
3. 社区的支持度:社区对windows平台发现的BUG不做任何承诺,一般不会去修复。
磁盘:机械硬盘,成本低容量大,易损坏;固态硬盘,性能高但价格也高。这里还是推荐使用机械硬盘。kafka使用磁盘不假,但使用方式大多是顺序读写操作,一定程度上规避了机械磁盘最大的劣势,即随机读写操作慢。虽然机械盘易损坏而造成的可靠性差,但kafka在软件层面提供机制来保证,所以固态盘在性能上的优势显得不是太明显了,选用机械盘还是比较划算的。
磁盘选择还有一个问题到底使不使用磁盘阵列(RAID)。RAID两个主要优势在于:提供冗余的磁盘存储空间;提供负载均衡。以上对于分布式系统很有吸引力,但对于kafka而言,一方面kafka实现了自己的冗余机制来提供高可靠性;另一方面通过分区的概念,kafka也能在软件层面实现负载均衡。不过目前依然有大尝使用RAID,但是kafka在存储方面提供了越来越便捷的高可靠性方案,因此在线上环境使用RAID变得不那么重要。
综合考量,追求性价比的公司可以不搭建RAID,使用机械盘完全能够胜任kafka线上环境。
磁盘容量:kafka集群到底需要多大磁盘空间?虽然kafka可以配置消息可以保存多长时间,但还是应该结合自身业务和存储需要来规划kafka集群的存储容量。举个例子:
假设有个业务每天需要向卡夫卡集群发送1亿条消息,每条消息保存两份,另外消息默认保存两周,每条消息平均大小1KB,计算一下这个业务预留多少磁盘空间?1亿 * 1KB * 2 / 1024 / 1024 = 191GB。一般kafka集群除了消息数据还有其他类型数据,比如索引数据等,还需要预留10%的磁盘空间,那么总的存储了容量就是 191 + 191 * 0.1 = 210GB,预留两周,大约2.8TB。kafka支持数据压缩,假设压缩比是0.75,最后需要规划的存储空间就是0.75 * 2.8 = 2.1TB。
总之在规划磁盘容量需要考虑以下几个因素:新增消息数;消息留存时间;平均消息大小;备份数;是否启用压缩。
带宽:对于kafka通过网络大量传输数据的框架而言,带宽特别容易成为瓶颈。事实上,带宽资源不足导致kafka出现性能问题的比例很大,如果跨机房,情况可能更糟。与其说是带宽资源规划 ,其实是所需的kafka服务器的数量。假设公司机房环境是千兆网络,即1Gbps,有个业务,1小时内处理1TB数据,那么这时候需要多少台kafka服务器?带宽1Gbps,即每秒处理1Gb数据,假设每台kafka机器没有部署其他服务,通常只能假设kafka会用70%的带宽资源,因为总要为其它应用或进程留一些资源。根据实际使用经验,超过70%的阈值就有网络丢包的可能性,也就是说单台kafka服务器也就能使用700M变得带宽资源。
这时它能使用的最大带宽资源,不能让kafka服务器常规性使用这么多资源,故通常要再额外预留2/3的资源,即单台使用带宽700Mb/3=240Mbps。这里的2/3还是比较保守,可以根据实际情况来定。那么1小时内处理1TB数据,即每秒要处理2336Mb数据,除以240,约等于10台服务器,如果额外复制两份,总的还有乘以3,即30台。
- 最最重要的集群参数配置
- Broker端参数
Broker端提供200个参数,其中绝大部分参数不用你亲自过问。
log.dirs 和 log.dir,只需要第一个参数即可。线上生产者中要为log.dirs配置多个路径,如/home/kafka1,/home/kafka2这样。有条件的话,最好保证这些目录挂载到不同磁盘。这样可以提升读写性能,毕竟多磁盘吞吐量更高;能够实现故障转移:即Failover。在kafka1.1版本以前,只要kafka Broker使用的任何一块磁盘挂掉,整个broker进程都关闭。但是自1.1开始,这种情况被修正,坏掉的磁盘数据会自动转移到其他磁盘,而且broker还能正常工作,Broker自动在好的路径上重建副本,然后从leader同步,kafka支持的工具能够将某个路径上的数据copy到其他路径。这个改进正是我们舍弃RAID方案的基础:没有这种Failover的话,我们只能依靠RAID来提供保障。
与Zookeeper相关的设置
Zookeeper是一个分布式协调框架,负责协调管理和保存kafka集群的所有元数据信息,比如都有哪些Broker在运行,创建了哪些Topic,每个Topic都有多少分区以及这些分区的Leader副本在哪些机器上。
zookeeper.connect: zk1:2181,zk2:2181,zk3:2181。如果让多个kafka集群使用同一套zookeeper集群,chroot派上用场,那么两套kafka集群可以这样指定:zk1:2181,zk2:2181,zk3:2181/kafka1和zk1:2181,zk2:2181,zk3:2181/kafka2。切忌chroot只能写一次,而且是加到最后。如果这样写就不对:zk1:2181/kafka1,zk2:2181/kafka2,zk3:2181/kafka3.
与Broker连接相关,即客户端程序货其他Broker如何与该Broker通信的设置。
listeners:监听器,告诉外部连接要通过什么协议访问指定主机和端口开放的kafka服务。
advertised.listeners:Broker用于对外发布的,针对通过外网访问的clients设置的。
host.name/port:过期参数,无用。
监听器,是若干个逗号分隔的三元组,每个三元组格式为<协议名称,主机名,端口号>。协议名称可能是标准的名字,如PLAINTEXT表示明文传输,SSL表示SSL或TLS加密传输,也可能使自己定义的协议名称,如CONTROLLER: //localhost:9092。如果你自己定义了协议名称,还要指定listener.security.protocol.map参数告诉这个协议底层使用哪种安全协议,比如指定listener.security.protocol.map=CONTROLLER:PLAINTEXT。还有经常有人问到底用IP地址还是主机名,这里最好全用主机名,即Broker端和Client端应用配置中全部填写主机名。Broker源码中使用的也是主机名,如果使用IP,有可能造成无法连接的情况。
关于Topic管理:
auto.create.topics.enable:是否允许自动创建Topic
auto.leader.election.enable:是否允许Unclean Leader选举
auto.leader.rebalance.enable:是否允许定期进行Leader选举
第一个参数,最好设置成false,好的运维应该严格禁止自行创建Topic。
第二个参数,关闭Unclean Leader选举,每个分区都有多个副本来提供高可用,只有一个副本对外提供服务,那么这些副本都有资格竞争Leader吗?只有那些保存数据比较多的副本有资格做这事。但是,假设那些保存比较多数据的副本挂了呢,此时还要不要进行Leader选举,这时这个参数派上用场。如果false,就坚持之前原则,坚决不让落后太多的副本竞争Leader,这样做的后果就是这个分区不能用了,因为没有Leader了。如果是true,那么允许落后太多的副本中选出一个当Leader,这样做的后果就是数据就丢失了,因为副本保存的数据本来就不全。另外这里特意提一下,最好把这个参数显示设置成false,因为社区最这个参数的默认值来来回回改了好几版。所有副本落后Leader太多,然后导致unclean replica,常见的可能是Leader自己出现问题。
第三个参数,设置为true,允许kafka定期对一些topic分区进行Leader重选举,更换Leader,但是要满足一定条件才会发生。换Leader代价非常高,本质上也没有任何性能收益,建议还是设置成false。
数据留存方面
log.retention.{hour|minute|ms}:控制一条消息数据被保存多久,优先级上,ms最高,hour最低。如log.retention.hour=168表示默认保存7天数据。
log.retention.bytes:指定Broker为消息保存的总磁盘容量大小,默认是-1,表示想保存多少数据都可以,这个参数发挥作用的场景是在云上构建多租户kafka集群:设想要做一个云上kafka服务,每个租户只能使用100GB磁盘空间,为避免恶意租户使用过多磁盘空间,设置这个值就至关重要。
message.max.bytes:控制broker能够接收到的最大消息大小。默认值1000012,不到1MB。线上环境中还是要设置一个较大的值比较保险,即使设置大一些也不会耗费磁盘空间。
2. Topic级别参数
如果同时设置了全局Broker参数和Topic级别参数,Topic参数会覆盖全局broker级别参数的值,每个topic都能设置自己的参数值。上面讲了为消息数据留存时间参数,事假生产环境中,为所有topic数据保存相当长时间,是没有必要的。应该根据具体情况为不同topic设置自己的留存时间。
从保存消息的维度:
retention.ms:规定该topic消息被保存时长,默认7天,一旦设置这个值,会覆盖掉broker端的全局参数值。
retention.bytes:规定了要为该Topic预留多大磁盘空间。当前默认值-1,无限使用,在多租户的kafka集群会有用武之地。
从能够能处理的消息大小维度:
max.message.bytes:Broker能够接受该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
bin/kafka-config.sh --zookeeper 1 localhost:2181 --entity-type topics --entity-name transaction --alter --add-config max-message.bytes=10485760
社区未来很有可能统一使用kafka-configs脚本来调整Topic级别参数。
3. JVM参数
kafka服务器端代码用Scala语言编写,编译成Class文件在JVM上运行,所以JVM参数也是很重要的。
kafka自版本2.0.0开始,正是摒弃对Java7的支持,所以至少要是用Java8吧。
堆大小设置,有个通用建议,将JVM堆大小设置成6GB,默认是1GB。
垃圾回收器,GC设置。JAVA7环境:若CPU资源充裕,建议使用CMS收集器。启用方法是-XX:+UseCurrentMarkSweepGC 。否则使用吞吐量收集器,开启方法是-XX:+UseParallelGC。Java8环境: 需要显示指定G1收集器。使用Java9环境:用默认的G1收集器就好。在没有任何调优得情况下,G1表现得要比CMS出色,主要体现在更少的Full GC,需要调整的参数更少。
那么如何为kafka设置这些参数,只需设置两个环境变量即可:
KAFKA_HEAP_OPTS:指定堆大小;
KAFKA_JVM_PERFORMANCE_OPTS:指定GC参数。
export KAFKA_HEAP_OPTS=--Xms6g --Xms6g
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
4. 操作系统参数
文件描述符限制;文件系统类型;Swappiness;提交时间
首先是ulimit -n。文件描述符系统资源并不像我恩想象的那样昂贵,不用担心调大此值有什么不利。通常情况将它设置成超大值是合理的做法,比如ulimit -n 1000000,如果不设置,经常会看到"Too many open files"错误。
其次是文件系统类型选择,比如ext3、ext4、XFS。根据官网测试报告,XFS性能要强于ext4,所以生产环境最好使用XFS。最近有个kafka使用ZFS的数据报告,貌似性能更强劲,可以试试。https://www.confluent.io/kafka-summit-sf18/kafka-on-zfs。
swap调优。网上很多文章提到设置为0,将swap完全禁掉以防止kafka进城使用swap空间,这里建议不要设置为0,可以设置为一个较小的值。因为一旦设置为0,物理内存耗尽,操作系统就触发OOM killer这个组件,随机挑选一个进程kill掉,根本不给用户任何预警。如果设置成一个较小值,至少能观测到Broker性能开始出现急剧下降,给你进一步调优和诊断问题时间。可以将swappniess配置成一个接近0的值,比如1。
提交时间或者说是Flush落盘时间。向kafka发送数据并不是真要等数据被写入磁盘才会认为成功,而是只要数据被写入到操作系统的页缓存(Page Cache)上就可以,随后操作系统根据LRU算法会定期将页缓存上脏数据落盘到物理磁盘。这个定期就是由提交时间来决定,默认5秒。一般情况我们会认为这个时间太频繁,可以适当增加提交间隔来降低磁盘写操作。当然,如果页缓存中的数据在写入磁盘前机器就宕机,那岂不是数据就丢失了。的确,这种情况数据确实就丢失了,但是kafka软件层面已经提供多副本的冗余机制,稍微拉大提交间隔去换取性能还是合理的。
标注:这个系列文章是本人在极客时间专栏---kafka核心技术与实战中的学习笔记
https://time.geekbang.org/column/article/101171