一、kafka 集群评估
搞定10亿请求,需要5台物理机,11 (SAS) * 7T ,需要64G的内存(128G更好),需要16个cpu core (32个更好)
1.1 内存评估
我们发现kafka读写数据的流程都是基于os cache,换句话说假设咱们的os cashe无限大
那么整个kafka是不是相当于就是基于内存去操作,如果是基于内存去操作,性能肯定很好。
内存是有限的。
- 尽可能多的内存资源要给os cache
- Kafka的代码用 核心的代码用的是scala写的,客户端的代码java写的。
都是基于jvm。所以我们还要给一部分的内存给jvm。
Kafka的设计, 没有把很多数据结构都放在j v m 里面。所以我们的这个j v m 不需耍太大的内存。
根据经验,给个10G就可以了。
NameNode: I
jvm里面还放了元数据(几十G) , JVM一定要给得很大。比如给个100G。
假设我们这个10亿请求的这个项目 ,一共会有100个topic。
100 topic * 5 partition * 2 = 1000 partition
一个partition其实就是物理机上面的一个目录,这个目录下面会有很多个.log的文件。
.log就是存储数据文件.默认情况下一个.log文件的大小是1G。
我们如果要保证1000个partition的最新的.log文件的数据如果都在内存里面,这个时候
性能就是最好。1000 * 1G = 1000G内存.
我们只需要把当前最新的这个log保证里面的25%的最新的数据在内存里面。
250M * 1000 = 0.25 G* 1000 =250G的内存。
250内存/ 5 = 50G内存
50G+10G = 60G 内存
64G的内存,另外的4G,操作系统本生是不是也需要内存。
其实Kafka的jvm也可以不用给到10G这么多。
评估出来64G是可以的。
当然如果能给到128G的内存的服务器,那就最好。
我刚刚评估的时候用的都是一个topic是5个partition,但是如果是数据量比较大的topic,可能会有10个partition。
总结.
搞定10亿请求,需要5台物理机,11 (SAS) * 7T ,需要64G的内存(128G更好)
1.2 cpu 评估
依据就是看我们的服务里面有多少线程去跑。
线程就是依托cpu去运行的
如果我们的线程比较多,但是cpu core比较少的话.我们的机器负载就会很高,性能就不好
1)评估一下,kafka的一台服务器,启动以后会有多少线程
1.Acceptor线程 1
2.processor 线程 3 6-9个线程
3.处理请求线程 8个 32个线程
4.定时清理的线程,拉取数据的线程,定时检查ISR列表的机制等等。
所以大概一个kafka服务运行起来以后,会有100多个线程
cpu core = 4,一般来说,几十个线程就肯定把cpu打满了。
cpu core = 8,应该很轻松的能支持几十个线程。
如果我们的线程是100多个、或者差不多200多个;8个cpu core是搞不定了
所以我们这儿建议:cpu core 16个,32个更好
结论:
kafka集群,最低要16个cpu core,如果能到32个cpu core 那就更好。
2cpu * 8 = 16 cpu core
4cpu * 8 = 32 cpu core
总结.
搞定10亿请求,需要5台物理机,11 (SAS) * 7T ,需要64G的内存(128G更好),需要16个cpu core (32个更好)
1.3 网卡评估
一般要么是千兆的网卡 (IG/S),还有的就是万兆的网长(10G/S)
高峰期的时候每秒会有5.5万的请求涌入,5.5/5 =大约是每台服务器会有1万个请求涌入。
我们之前说的,
10000 * 50kb = 488M 也就是每条服务器,每秒要接受488M的数据。数据还要有副木,副本之间的同步
也是走的网络的请求。488 * 2 = 976m/s
说明一下:
很多公司的数据,一个请求里面是没有50kb这么大的,我们公司是因为主机在生产端封装了数据
然后把多条数据合并在一起了,所以我们的一个请求才会有这么大。
说明一下:
一般情况下,网卡的带宽是达不到极限的,如果是千兆的网卡,我们能用的一般就是700M左右。
但是如果最好的情况,我们还是使用万兆的网卡。
如果使用的是万兆的,那就是很轻松。
二、异常处理
1)LeaderNotAvailableException:
这个就是如果某台机器挂了,此时leader副本不可.用,会导致你写入失败,、
要等待其他follower副本切换为leader副本之后,才能继续写入,此时可以重试发送即可
如果说你平时重启kafka的broker进程,肯定会导致leader切换,一定会导致你写入报错,
是LeaderNotAvailableException
2) NotControllerException: 这个也是同理,如果说Controller所在Broker挂了,
那么此时会有问题,需耍等待Controller重新选举,此时也是一样就是重试即可
3) NetworkException: 网络异常 timeout
a.配置.retries参数,他会自动重试的
b.但是如果重试几次之后还是不行,
就会提供Exception给我们来处理了,我们获取到异常以后,再对这个消息进行单独处理,
我们会有备用的链路。
三、如何提升吞吐量
参数一:
buffer.memory:设置发送消息的缓冲区,默认值是33554432,就是32MB
参数二:
compression, type:默认是none,不压缩,但是也可以使用Iz4JK缩,效率还是不错的,
压缩之后可以减小数据量,提升吞吐量:,但是会加大producer端的cpu开销
参数三:
batch.size:设置.batch的大小,如果batch太小,会导致频繁网络请求,吞吐量下降;
如果batch太大,会导致一条消息需要等待很久才能被发送出去,而且会让内存缓冲区有很大压力,
过多数据缓冲在内存里, 默认值是: 16384 , 就是 16 k b , 也就是一个batch 满了 16 kb 就发送出去,
一般在实际生产环境,这个batch的值可以增大一些来提升乔吐量:
如果我们消息比较少
配合使用的参数linger.ms,这个值默认是0,意思就是消息必须立即被发送,但是这是不对的,
一般设置一个100亳秒之类的,这样的话就是说,这个消息被发送出去后进入一个batch,
如果100毫秒内,这个batch满了 16kb,自然就会发送出去
四、消息重复乱序处理
1、消息重复
有的时候一些leader切换之类的问题,需要进行重试,设置retries即可,
但是消息重试会导致,重复发送的问题,比如说网络抖动一下导致他以为没成功,就重试了,
其实人家都成功了.
两次重试的间隔默认是100毫秒,用“retry .backoff.ms”来进行设置
基本上在开发过程中,靠重试机制堪本就可以搞定95%的异常问题
2.消息乱序
消息重试是可能导致消息的乱序的,因为可能排在你后面的消息都发送出去了。
所以可以使用" m a x . i n . f l i g h t . r e q u e s t s . p e r . c o n n e c t i o n " 参数设置为1 ,
这样可以保证producer同一时间只能发送一条消息。
五、参数设置:
producer端
request.required.acks = 0;
只要请求已发送出去,就算是发送完了,不关心有没写成功。
性能很好,如果是对一些日志进行分析,可以承受丢数据的情况,用这个参数性能会很好
request.required.acks = 1;
发送一条消息,当leader partition写入成功以后,才算写入成功。
不过这种方式会有丢数据的可能。
request.required.acks = -1;
需要ISR列表里面所有副本都写完以后才算写入成功
ISR: 1 个副本。1 leader partition 1 follower partition
kafka服务端
min.insync.replicas: 1 如果我们不设置的话,默认这个值是1
一个leader partition会维护一个ISR列表 ,这个值就是限制ISR列表里面至少得有几个副本,
比如这个值是2,那么当ISR列表里只有一个副本的时候,往这个分区插入数据的时候就会报错
数据不丢失方案:
1.分区副本 >= 2 2 3
2.ack = -1
3.min.insync.replicas >= 2
heartbeat.interval.ms:
consumer心跳时间间隔,必须得与coordinator保持心跳才能知道consumer是否故障了
然后如果故障之后,就会通过心跳下发rebalance的指令给其他的comsumer通知他们进行rebalance操作
session.timeout.ms:
kafka长时间感知不到一个consumer就认为他故障了,默认是:10秒
max.poll.interval.ms:
如果住两次poll操作之间超过了这个时间,那么就认为这个comsumer处理能力太弱了,会被剔除消费组,分区分配给别人去消费,
需结合自己的业务处理的性能来设置
fetch.max.bytes:
获取一条消息最大的字节数,一般建议设置大一些,默认为 1m
producer发送的数据,一条消息最大多大 1m -> 10m
Broker 存储数据,一条消息最大能接受多大 1m -> 10m
Consumer
max.poll.records: 一次poll返回消息的最大条数,默认是500条
connection.max.idle.ms: consumer跟broker的socket连接如果空闲超过一定时间,
就会自动回收连接,但是下次消费就要重新建立socket连接,这个建议设置为 -1,不要去回收
enable.auto.commit
开启自动提交偏移量
auto.commit.interval.ms:
每隔多久提交一次偏移量,默认值5000 亳秒
auto.offset.reset:
1.earliest: 当各分区下有已提交的offset时,从提交的offset开始消费,无提交offset时,从头开始消费
2.latest: 当各分区下有已提交的offset时,从提交的offset开始消费,无提交offset时,消费新产生的该分区下的数据 ?
3.none: topic各分区都存在已提交的offset时,从offset后开始消费,只要有一个分区不存在已提交的offset,则抛出异常
六、消费者是如何实现rebalance的?
1、什么是coondinator
每个consumer group 都会选择一个broker作为自己的coondinator
他是负责监控这个消费组里的各个消费者的心跳,以及判断是否宕机,然后开启rebalance的
2.如何选择coondinator机器
首先对groupid进行hash(数字) ,接着对 __consumer_offets的分区数量取模,默认为50,
(__consumer_offets 的分区数可以通过offset.topic.num.partitions来设置,)
找到分区以后,这个分区所在的broker机器就是coondinator机器。
如:
consumer group:
group.id: my_conumser -> my consumer hash -> 求出来就是—个数字
数字/50 =>获取到 0-49的这样的一个数。比如数就是8,
然后就在找到 __consumer_offets的partition 8 的leader partition在
哪台服务器上面。那台就是coondinator服务器。|
__consumer_offets 默认有50个分区
3、rebalance流程
- 每个consumer都发送JoinGroup请求到Coordinator
- 然后 Coordinator 从一个consumer group 中选择一个 consumer 作为 leader
(谁先注册上来,谁就是leader consumer) - Coordinator 把consumer group情况发送给这个leader
- 接着这个leader会负责制定消费方案
- 通过SyncGroup发给Coordinator
- 接着Coordinator 就把消费方案下发给各个consumer,他们会从指定的分区的
leader broker开始进行socket连接以及消费消息
4、三种rebalance策略
range> round-robin> sticky
比如我们消费的一个主题有12个分区:
p0,pl,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11
- range 策略
range策略就是按照partiton的序号范围
partition 0~3 给consumer1
partition 4~7 给一个 consumer2
partition 8~11给一个 consumer3
默认就是这个策略;
2、round-robin 策略
就是轮询分配
consumer1:0,3,6,9
consumer2:1,4,7,10
consumer3:2,5,8,11
但是前面的这两个方案有个问题:
12 -» 2每个消费者会消费6个分区
假设consumer1 挂了,p0 - 5 分配给consumer2, p6-11分配给consumer3?
这样的话,原本在consumer2上的的p6,p7分区就被分配到consumer3上
3.sticky 策略
最新的一个stick策略,就是说尽可能的保证在rebalance的时候,让原本属于这个consumer的分区还是属于他们
然后把多余的分区再均匀分配过去, 这样尽可能维持原来的分区分配的策略.
consumer1:0-3
consumer2:4-7
consumer3:8-11
假设comsumer3挂了
consumer1:0-3 +8,9
consumer2:4-7 +10,11
七、kafka延迟调度机制
第一类延时的任务:
比如说producer的acks=-1,必须等待leader 和follower都写完了才能返回响应。
有一个超时时间,默认是30秒(request.timeout.ms)。
所以需要在写入一条数据到leader磁盘后就必须有一个延时任务,
到期时间是30s延时任务放到(DelayedOperatitonPurgatory(延时管理器))中,在30s之前如果所有follower都写入副本到本地磁盘了
那么这个任务就会被自动触发苏醒,就可以返回响应结果给客户端了,
否则的话,这个延时任务自己指定了最多是30s到期,如果到了超时时间都没等到,就直接超时返回异常。
第一类延时的任务:
follower往leader拉取消息时,如果发现是空的,此时会创建一个延时拉取任务,
延时时间到了之后(比如到了100ms),就给follower返回一个空的数据,然后follower再次发送请求读取消息,
但是如果延时的过程中(还没到100ms),leader写入了消息,这个任务就会自动苏醒,自动执行拉取任务
为什么要设计时间轮?
kafka内部有很多延时任务没有基于JDK Timer来实现,那个插入和删除任务的时间复杂度是O(nlogn)
而是基于了自己写的时间轮来实现的,时间复杂度是O(l),依靠时间轮机制,延时任务插入和删除
时间轮是什么
tickMs: 时间轮间隔
wheelSize:时间轮大小
interval:tickMs * wheelSize,一个时间轮的总的时间跨度
currentTime:当前时间的指针
a: 因为时间轮是一个数组,所以要获取里面数据的时候,靠的是index,时间复杂度是O(l)
b: 数组某个位置上对应的任务,用的是双向链表存储的,往双向链表里面插入和删除任务,时间复杂度也是O(l)
举例:插入一个8ms以后要执行的任务
3、多层级的时间轮
比如:比如要插入一个110ms以后运行的任务
第一层级时间轮: 1ms * 20
第二层级时间轮: 20ms * 20
第三层级时间轮: 400ms * 20