Kafka消息消费之的性能提升实践
技术背景
从标题中,可以看到提升的难点在于消息要进行保序,使用单线程自然 是可以保序的,比如单条消息处理足够快,比如1ms处理一条,那即使单线程,其处理性能也可以达到1000TPS,也许已经满足你的业务要求,但一旦单条消息处理稍慢,则大量消息的处理则会将该性能弱点成倍扩大,成为瓶颈。简单做个数学题:
报文数量 | 单条处理耗时 | 总计耗时 |
---|---|---|
10K | 1ms | 10S |
10K | 60ms | 10min |
实际困难
项目中采用了BGP-LS协议进行协议拓扑的收集并使用了Kafka作为消息中间件,进行拓扑数据的转发,由于协议收集是一个逐渐收敛过程,同一条链路或节点会上报多次,且属性存在差异,这样处理的顺序则直接影响数据的准确性。
报文 | 单线程串行处理 | 多线程并发处理 |
---|---|---|
A=10,B=10 | 增加A和B属性 | 并发不可控 |
A=20,B=10 | 修改A属性 | 并发不可控 |
A=30,B=9,C=10 | 修改A和B属性增加C属性 | 并发不可控 |
最终数据正确 | 无法确认最终入库的数据 |
解决办法一(分主题)
将Kafka上报的报文体拆分主题,不同主题间的处理可以并发,如协议拓扑可以分为节点和链路两个主题进行分别处理,当然前提是业务逻辑针对节点和链路的处理不能有时序依赖耦合。
解决办法二(保序折中处理)
既然性能瓶颈的产生是由于需要保序导致,那是否可以不保序,或部分消息的处理不保序。通过分析,对于链路保证同一条链路处理有序以及正反向链路处理有序和同一个节点处理有序即可。这样不同链路和节点的处理则可以进行并发处理。原理流程示意图如下:
图中所示的hash algorithm则为分配消息策略,节点采用的分配规则为:
HashCode(节点RouterId)& Integer.MAX_VALUE& (thread_count - 1)
链路采用的分配规则为:
(链路源地址+链路目的地址)& (thread_count - 1)
ps.其中thread_count取值为2的N次方(N>0)
消息的内存队列可以使用BlockingQueue实现,可以参考代码:
https://blog.csdn.net/sunquan291/article/details/103218725
解决办法三(批量处理)
思路依然是如何可以不保序,基于业务特点,如果批量处理一批消息,保证批量消息里无相同的唯一主键消息,那在这个前提下,可以进行并发处理。所以需要进行批量消息的预整理(消息去重),整理的目前是使消息唯一化。
消息整理
以项目中链路消息为例,只有增加链路和删除链路两个类型,消息序列为:
1、增加linkA
2、增加linkB
3、增加linkC
4、增加linkD
5、增加linkB
6、增加linkA
7、删除linkC
8、增加linkB
去重的同时还要将删除某条链路前增加消息过滤掉,最终得到的待处理批量消息集合为:
6、增加linkA
4、增加linkD
8、增加linkB
7、删除linkC
这样8条消息最后整理成4条,这里要注意一下7的删除不能遗漏,而正因为有了7的删除,3的增加则可以忽略。具体的消息整理代码与业务特定有关。
批量+超时提交代码示例
由于需要批量处理消息,则需要规定多少消息为一批,但为了避免极端情况下,消息达不到规定数据而无法处理的囧境,增加了超时机制,在超过固定时间,即使消息数据不够,也进行提交处理。
接口定义:
package com.zte.sdn.oscp.topology.standard.queue;
/**
* @Author 10184538
* @Date 2019/11/26 9:35
**/
public interface BatchQueue<T> {
/**
* 向队列中添加数据
*
* @param t
* @return
*/
boolean add(T t);
/**
* 查询当前进行批量提交时的数据数量
*
* @return
*/
long getCommitSize();
/**
* 查询超时提交的时间
*
* @return
*/
long getTimeOut();
/**
* 执行立即提交处理
*/
void commit();
/**
* 查询当前队列中数据数量
*
* @return
*/
long getQueueSize();
}
实现逻辑:
package com.zte.sdn.oscp.topology.standard.queue;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @Author 10184538
* @Date 2019/11/23 18:49
**/
public class BatchQueueImpl<T> implements BatchQueue<T> {
private static final Logger logger = LoggerFactory.getLogger(BatchQueueImpl.class);
private int[] batchCommit = {
1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
10, 10, 10, 10