前言
本文用于记录小白第一次性能调优,仅供参考,欢迎交流
Part 1
测试场景
receive集群group1(两个receive节点)上单个代理,100个tcp连接,2s-10s之间随机推送报文数据
实际结果
100个连接经过ELB负载均衡后均匀的分配在了两个receive节点上,但是只有单个analyze节点在执行数据策略链
具体原因
- 在receive解析完数据进行推送时,指定了key为proxyID,在本次测试场景中,同一个代理虽然存在100个tcp连接推送数据,但是共享一个代理id,导致所有的推送数据进入同一个分区,最终被analyze节点3所持有的消费者长期消费。
- 单节点的消费者数量为5个,3个节点共计15个消费者,而数据传输topic中的分区数为10,导致部分消费者无法获取到分区,处于空闲状态。
原理解析
- 在Kafka中,生产者在推送数据时,分区数不变的情况下,如果指定了key,相同key的数据一定分配到同一个partition(同一key的消息在单个分区内有序)。
- 在同一个消费者组(consumer group)中,一个分区(partition)在同一时刻只能被该组内的一个消费者(consumer)消费,且消费组内分区与消费者的持有关系保持固定。当有消费者加入或退出消费组时,触发rebalance,持有关系会重新分配。
调整措施
- 调整消费者与分区的数量关系,concurrency * 节点数 <= 分区数。
- 从业务角度出发,推送数据时移除key,设备传输过程中的业务分包或网络分包,在receive节点已经完成组包以及解析,接受时间也已固定,因此具体由哪个节点执行后续的匹配、计算、过滤、转发并无差异。
Part 2
测试场景
- 两个receive集群group1和group2(每个集群包含两个节点)
- 每个集群一个代理,每个代理200个tcp连接以及10个测点
- 单个连接1s-3s之间随机推送报文数据,每个数据报文10个测点全量匹配,共计转发10次
实际结果
- 经过约1个小时左右的压测,analyze会出现OOM的情况,同时消费组中部分消费者掉线
- analyze节点消费速度偏慢
- 其他服务服务器指标偏高,但未触及水位线
具体原因
- 通过查看GC日志发现,GC频繁且实际能释放的空间极少
- 导出heap_dump文件后通过MAT进行解析,发现某个线程池占据了约1.5G的内存(此时服务堆内存设置2G),通过对线程池outgoing引用的分析发现,主要内存占用还是在线程池的任务队列中
- 通过path to GC Roots找到强引用路径,定位阻碍GC存在内存泄漏的的线程池为executeDataStrategyKafka-Executor,引用的起点是Kafka的消费者
- 最终找到实际的罪魁祸首@Async和@KafkaListener的混合使用,同时消费速度跟不上生产速度,单次批量拉取500条消息,封装为任务进入等待队列,导致线程池中积压了大量解析后的数据,最终撑爆了内存
- 服务设定的是批量拉取(单次poll最多拉取500条数据),消费者将数据基于proxyId分组后通过并行流进行处理,单个分组内的消息同步消费,消费过程中涉及到服务调用,数据转发等网络I/O。因此,在只有两个代理的压测场景下,并行流实际只有两个线程在工作,最终单次poll的处理时间超过了kafka默认的单次最大调用时间(5分钟),导致消费者被踢下线
原理解析
- 线程池必须显示设定队列大小,同时要基于业务考量队列大小的合理性,避免因为部分业务产生OOM导致整个服务不可用。
- 并行流(parallelStream)默认使用ForkJoin线程池(线程数为CPU核心数-1),且该线程池被CompletableFuture、Arrays等方法共用,容易导致业务之间相互阻塞。同时并行流更适用于简易的CPU密集计算,而非I/O密集型操作
- 在使用消息队列时,消费者端禁止开启自动提交(例如当前情况,大量消息从kafka转到任务队列中积压,OOM后导致数据全部丢失)
- 若kafka消费者未定时发送心跳(默认3s)或者单次poll的处理时间超标(默认5分钟),均会导致消费者从消费组中移除;Full GC耗时过长会导致消费者心跳发送卡死,同样出现离线
调整措施
- 移除消费者与kafka之间的异步线程池
- 并行流调整为CompletableFuture + 专属线程池
- 关闭kafka消费自动提交,改为手动提交