kafka性能技术分析

1.通过磁盘顺序读写,效率高,appendLog,对比raid-5 7200rpm的磁盘

sequence io 600M/s

random io 100kb/s

kafka写操作时,依赖底层文件系统的pagecache功能,pagecache会将尽量多的将空闲内存,当做磁盘缓存,写操作先写到pageCache,并将该page标记为dirty;发生读操作时,会先从pageCache中查找,当发生缺页时,才会去磁盘调整;当有其他应用申请内存时,回收pageCache的代价是很低的

,使用pageCache的缓存功能,会减少我们队JVM的内存依赖,JVM为我们提供了GC功能,依赖JVM内存在kafka高吞吐,大数据的场景下有很多问题。如果heap管理缓存,那么JVM的gc会频繁扫描heap空间,带来的开销很大,如果heap过大,full gc带来的成本也很高;所有的In-Process Cache在OS中都有一份同样的PageCache。所以通过只在PageCache中做缓存至少可以提高一倍的缓存空间。如果Kafka重启,所有的In-Process Cache都会失效,而OS管理的PageCache依然可以继续使用。

2.sendFile

传统网络IO模型

① 操作系统将数据从磁盘拷贝到内核区的pagecache

② 用户程序将内核区的pagecache拷贝到用户区缓存

③ 用户程序将用户区的缓存拷贝到socket缓存中

④ 操作系统将socket缓存中的数据拷贝到网卡的buffer上,发送

问题:四次system call 两次context切换,同一份数据在缓存中拷贝多次,效率很低,拷贝动作完全可以在内核中进行,将2 3 步去掉,sendfile就是完成这一功能

kafka设计初衷是数据的拷贝完全通过pageCache来进行,尽量减少磁盘读写,如果kafka生产消费配合的好,那么数据完全走内存,这对集群的吞吐量提升是很大的

当集群只有写操作时,此时的集群只有写,没有读操作。10M/s左右的Send的流量是Partition之间进行Replicate而产生的。从recv和writ的速率比较可以看出,写盘是使用Asynchronous+Batch的方式,底层OS可能还会进行磁盘写顺序优化。而在有Read Request进来的时候分为两种情况,第一种是内存中完成数据交换。

② 已经从pageCache刷出的数据,这时候的数据可以看出大部分走的是磁盘io

TIPS

① kafka官方不建议使用log.flush.interval.messages和log.flush.interval.ms来强制刷盘,因为高可靠应该通过replica副本来保证,强制刷盘对系统性能影响很大(生产是100000 和60000)

② 可以通过调整/proc/sys/vm/dirty_background_ratio和/proc/sys/vm/dirty_ratio来调优性能。

a. 脏页率超过第一个指标会启动pdflush开始Flush Dirty PageCache。

b. 脏页率超过第二个指标会阻塞所有的写操作来进行Flush。

c. 根据不同的业务需求可以适当的降低dirty_background_ratio和提高dirty_ratio

(生产是10 和 20)

2 partition

Partition是Kafka可以很好的横向扩展和提供高并发处理以及实现Replication的基础。

扩展性方面。首先,Kafka允许Partition在集群内的Broker之间任意移动,以此来均衡可能存在的数据倾斜问题。其次,Partition支持自定义的分区算法,例如可以将同一个Key的所有消息都路由到同一个Partition上去。 同时Leader也可以在In-Sync的Replica中迁移。由于针对某一个Partition的所有读写请求都是只由Leader来处理,所以Kafka会尽量把Leader均匀的分散到集群的各个节点上,以免造成网络流量过于集中。

并发方面。任意Partition在某一个时刻只能被一个Consumer Group内的一个Consumer消费(反过来一个Consumer则可以同时消费多个Partition),Kafka非常简洁的Offset机制最小化了Broker和Consumer之间的交互,这使Kafka并不会像同类其他消息队列一样,随着下游Consumer数目的增加而成比例的降低性能。此外,如果多个Consumer恰巧都是消费时间序上很相近的数据,可以达到很高的PageCache命中率,因而Kafka可以非常高效的支持高并发读操作,实践中基本可以达到单机网卡上限。

不过,Partition的数量并不是越多越好,Partition的数量越多,平均到每一个Broker上的数量也就越多。考虑到Broker宕机(Network Failure, Full GC)的情况下,需要由Controller来为所有宕机的Broker上的所有Partition重新选举Leader,假设每个Partition的选举消耗10ms,如果Broker上有500个Partition,那么在进行选举的5s的时间里,对上述Partition的读写操作都会触发LeaderNotAvailableException。

再进一步,如果挂掉的Broker是整个集群的Controller,那么首先要进行的是重新任命一个Broker作为Controller。新任命的Controller要从Zookeeper上获取所有Partition的Meta信息,获取每个信息大概3-5ms,那么如果有10000个Partition这个时间就会达到30s-50s。而且不要忘记这只是重新启动一个Controller花费的时间,在这基础上还要再加上前面说的选举Leader的时间 -_-!!!!!!

此外,在Broker端,对Producer和Consumer都使用了Buffer机制。其中Buffer的大小是统一配置的,数量则与Partition个数相同。如果Partition个数过多,会导致Producer和Consumer的Buffer内存占用过大。

tips

1. Partition的数量尽量提前预分配,虽然可以在后期动态增加Partition,但是会冒着可能破坏Message Key和Partition之间对应关系的风险。

2. Replica的数量不要过多,如果条件允许尽量把Replica集合内的Partition分别调整到不同的Rack。

3. 尽一切努力保证每次停Broker时都可以Clean Shutdown,否则问题就不仅仅是恢复服务所需时间长,还可能出现数据损坏或其他很诡异的问题。

3

Producer

Kafka的研发团队表示在0.8版本里用Java重写了整个Producer,据说性能有了很大提升。我还没有亲自对比试用过,这里就不做数据对比了。本文结尾的扩展阅读里提到了一套我认为比较好的对照组,有兴趣的同学可以尝试一下。

其实在Producer端的优化大部分消息系统采取的方式都比较单一,无非也就化零为整、同步变异步这么几种。

Kafka系统默认支持MessageSet,把多条Message自动地打成一个Group后发送出去,均摊后拉低了每次通信的RTT。而且在组织MessageSet的同时,还可以把数据重新排序,从爆发流式的随机写入优化成较为平稳的线性写入。

此外,还要着重介绍的一点是,Producer支持End-to-End的压缩。数据在本地压缩后放到网络上传输,在Broker一般不解压(除非指定要Deep-Iteration),直至消息被Consume之后在客户端解压。

当然用户也可以选择自己在应用层上做压缩和解压的工作(毕竟Kafka目前支持的压缩算法有限,只有GZIP和Snappy),不过这样做反而会意外的降低效率!!!! Kafka的End-to-End压缩与MessageSet配合在一起工作效果最佳,上面的做法直接割裂了两者间联系。至于道理其实很简单,压缩算法中一条基本的原理“重复的数据量越多,压缩比越高”。无关于消息体的内容,无关于消息体的数量,大多数情况下输入数据量大一些会取得更好的压缩比。

不过Kafka采用MessageSet也导致在可用性上一定程度的妥协。每次发送数据时,Producer都是send()之后就认为已经发送出去了,但其实大多数情况下消息还在内存的MessageSet当中,尚未发送到网络,这时候如果Producer挂掉,那就会出现丢数据的情况。

为了解决这个问题,Kafka在0.8版本的设计借鉴了网络当中的ack机制。如果对性能要求较高,又能在一定程度上允许Message的丢失,那就可以设置request.required.acks=0 来关闭ack,以全速发送。如果需要对发送的消息进行确认,就需要设置request.required.acks为1或-1,那么1和-1又有什么区别呢?这里又要提到前面聊的有关Replica数量问题。如果配置为1,表示消息只需要被Leader接收并确认即可,其他的Replica可以进行异步拉取无需立即进行确认,在保证可靠性的同时又不会把效率拉得很低。如果设置为-1,表示消息要Commit到该Partition的ISR集合中的所有Replica后,才可以返回ack,消息的发送会更安全,而整个过程的延迟会随着Replica的数量正比增长,这里就需要根据不同的需求做相应的优化。

tips

1. Producer的线程不要配置过多,尤其是在Mirror或者Migration中使用的时候,会加剧目标集群Partition消息乱序的情况(如果你的应用场景对消息顺序很敏感的话)。

2. 0.8版本的request.required.acks默认是0(同0.7)。

4

Consumer

Consumer端的设计大体上还算是比较常规的。

• 通过Consumer Group,可以支持生产者消费者和队列访问两种模式。

• Consumer API分为High level和Low level两种。前一种重度依赖Zookeeper,所以性能差一些且不自由,但是超省心。第二种不依赖Zookeeper服务,无论从自由度和性能上都有更好的表现,但是所有的异常(Leader迁移、Offset越界、Broker宕机等)和Offset的维护都需要自行处理。

• 大家可以关注下不日发布的0.9 Release。这帮货又用Java重写了一套Consumer。把两套API合并在一起,同时去掉了对Zookeeper的依赖。据说性能有大幅度提升哦~~

tips

强烈推荐使用Low level API,虽然繁琐一些,但是目前只有这个API可以对Error数据进行自定义处理,尤其是处理Broker异常或由于Unclean Shutdown导致的Corrupted Data时,否则无法Skip只能等着“坏消息”在Broker上被Rotate掉,在此期间该Replica将会一直处于不可用状态。

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏腾讯云服务器团队的专栏

腾讯云批量计算介绍

最初,公有云的工作负载以服务型负载为主,各大厂商也进行了诸多针对性优化。随着云计算的快速发展,越来越多的、不同行业的用户开始使用公有云,批处理型负载显著增加。针...

7452
来自专栏racaljk

Erlang调度器细节探析

Erlang的很多基础特性使得它成为一个软实时的平台。其中包括垃圾回收机制,详细内容可以参见我的上一篇文章Erlang Garbage Collection D...

583
来自专栏阿杜的世界

RocketMQ学习-Broker-1

前面学习了name server的主要代码,这篇文章开始学习broker的源码。broker是RocketMQ的核心模块,这篇文章我们先从整体看下代码结构、Ro...

612
来自专栏ios 技术积累

ios 多线程你需要了解的

进程是指在系统中正在进行的一个应用程序;每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。

623
来自专栏desperate633

Java多线程之死锁(Deadlock)及死锁避免(Deadlock Prevention)线程死锁(Thread Deadlock)更复杂的死锁情况数据库死锁死锁避免(Deadlock Preven

死锁就是当两个或者多个线程阻塞了 ,正在等到所需要的锁,但这些锁被其他也在等待的线程锁持有。死锁常常发生在多个线程在同一个时刻,需要同一些锁,但是他们获取锁的顺...

471
来自专栏钟绍威的专栏

锁的实现原理解锁的实现加锁的实现

 锁在多线程中是必不可少的,他给多线程提供了同步的功能,让多线程可以互斥的执行同步块,并具有可见性。  本文将从happens-before关系出发,结合Ree...

1887
来自专栏大数据架构

Kafka设计解析(六)- Kafka高性能架构之道

1236
来自专栏Java技术栈

干货 | Tomcat 连接数与线程池详解

在使用tomcat时,经常会遇到连接数、线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连接器(Connector)。

854
来自专栏码匠的流水账

聊聊kafka的group coordinator

本文主要来讲一个kafka的group coordinator。在kafka0.9.0版本的时候,开始启用了新的consumer config,这个新的cons...

591
来自专栏kl的专栏

spring batch精选,一文吃透spring batch

批处理是企业级业务系统不可或缺的一部分,spring batch是一个轻量级的综合性批处理框架,可用于开发企业信息系统中那些至关重要的数据批量处理业务.Spri...

4408

扫码关注云+社区