这里只考虑允许并发消费与顺序消费的业务场景。
首先我们明确一点因为顺序消费会对messageQueue加锁,相同情况下,性能必然远小于并发消费;
先说结论:假设消息消费速度恒定0.5秒。
消费者线程总数 :threadNumtopic
总队列数:msgQueue
并发消费的TPS等同于:
tps = threadNum/0.5s
顺序消费的速度等同于:
tps = min(msgQueue,threadNum)/0.5s ;
ps:因为加锁引起的性能损耗更能达到理论性能。
ps: 如果消费线程较少,并且pullThresholdForQueue较大,将容易导致部分队列饥饿;
接下来分析topic 为什么在消息多时挤压尤为严重。 首先佐证一下consumer 的pullThresholdForQueue这个参数是否存在:每次重启或者新增client,topic的tps 总是瞬间飙升,但是很快就下降了,就是因为这个参数将大量的message缓存至客户端,从而引起Broker错误计算tps。(实际上broker计算tps是非常简陋的,不是严格按照offset计算)接下来看一下线上真实的consumer tps变化:
2022-06-19 08:59:00 INFO - Stats In One Minute, SUM:148 TPS:2.47 AVGPT:1.01
2022-06-19 09:00:00 INFO - Stats In One Minute, SUM:826 TPS:13.77 AVGPT:1.01
2022-06-19 09:00:00 INFO - Stats In One Hour, SUM:6930 TPS:1.93 AVGPT:1.01
2022-06-19 09:01:00 INFO - Stats In One Minute, SUM:703 TPS:11.72 AVGPT:1.00
2022-06-19 09:02:00 INFO - Stats In One Minute, SUM:501 TPS:8.35 AVGPT:1.00
2022-06-19 09:03:00 INFO - Stats In One Minute, SUM:956 TPS:15.93 AVGPT:1.00
2022-06-19 09:04:00 INFO - Stats In One Minute, SUM:498 TPS:8.30 AVGPT:1.00
2022-06-19 09:05:00 INFO - Stats In One Minute, SUM:1663 TPS:27.72 AVGPT:1.01
2022-06-19 09:06:00 INFO - Stats In One Minute, SUM:2126 TPS:35.43 AVGPT:1.01
2022-06-19 09:07:00 INFO - Stats In One Minute, SUM:1912 TPS:31.87 AVGPT:1.01
2022-06-19 09:08:00 INFO - Stats In One Minute, SUM:1180 TPS:19.67 AVGPT:1.01
2022-06-19 09:09:00 INFO - Stats In One Minute, SUM:1633 TPS:27.22 AVGPT:1.01
2022-06-19 09:10:00 INFO - Stats In One Minute, SUM:1230 TPS:20.50 AVGPT:1.01
2022-06-19 09:11:00 INFO - Stats In One Minute, SUM:1848 TPS:30.80 AVGPT:1.00
2022-06-19 09:12:00 INFO - Stats In One Minute, SUM:1147 TPS:19.12 AVGPT:1.02
2022-06-19 09:13:00 INFO - Stats In One Minute, SUM:1665 TPS:27.75 AVGPT:1.04
2022-06-19 09:14:00 INFO - Stats In One Minute, SUM:1017 TPS:16.95 AVGPT:1.01
2022-06-19 09:15:00 INFO - Stats In One Minute, SUM:1478 TPS:24.63 AVGPT:1.01
2022-06-19 09:16:00 INFO - Stats In One Minute, SUM:1274 TPS:21.23 AVGPT:1.02
2022-06-19 09:17:00 INFO - Stats In One Minute, SUM:1371 TPS:22.85 AVGPT:1.04
2022-06-19 09:18:00 INFO - Stats In One Minute, SUM:1256 TPS:20.93 AVGPT:1.01
2022-06-19 09:19:00 INFO - Stats In One Minute, SUM:1189 TPS:19.82 AVGPT:1.01
2022-06-19 09:20:00 INFO - Stats In One Minute, SUM:1216 TPS:20.27 AVGPT:1.02
2022-06-19 09:21:00 INFO - Stats In One Minute, SUM:2558 TPS:42.63 AVGPT:1.01
2022-06-19 09:22:00 INFO - Stats In One Minute, SUM:2222 TPS:37.03 AVGPT:1.02
2022-06-19 09:23:00 INFO - Stats In One Minute, SUM:1616 TPS:26.93 AVGPT:1.01
2022-06-19 09:24:00 INFO - Stats In One Minute, SUM:1200 TPS:20.00 AVGPT:1.01
2022-06-19 09:25:00 INFO - Stats In One Minute, SUM:1087 TPS:18.12 AVGPT:1.06
2022-06-19 09:26:00 INFO - Stats In One Minute, SUM:564 TPS:9.40 AVGPT:1.01
2022-06-19 09:27:00 INFO - Stats In One Minute, SUM:236 TPS:3.93 AVGPT:1.02
2022-06-19 09:28:00 INFO - Stats In One Minute, SUM:1160 TPS:19.33 AVGPT:1.04
2022-06-19 09:29:00 INFO - Stats In One Minute, SUM:793 TPS:13.22 AVGPT:1.05
2022-06-19 09:30:00 INFO - Stats In One Minute, SUM:230 TPS:3.83 AVGPT:1.02
2022-06-19 09:31:00 INFO - Stats In One Minute, SUM:727 TPS:12.12 AVGPT:1.02
而在我测试顺序消费能力时如果
threadNum<messageQueue,
发现1000条消息消费顺序变化为
0-100左右无序排列,之后无限接近于101-996, 102-997,103-998,104-999
明显看出有部分队列发生饥饿,没有资源消费。
但是当我将pullThresholdForQueue设置为1,将无法复现这个情况。
关于饥饿的研究:
整个ConsumeRequest与pullRequest相当于一个生产者、消费者模型;他们共同持有的ProcessQueue对象(对象中维护一个读写锁、一个treeMap(消息对象数组),一个消费reentrantLock)。但是在DefaultMQpullConsumer的任务队列中
每个pullRequest只会拉pullbatchSize条消息,即32条,但是每个pullRequest在到达最大值前不间断启动。
那么假设最坏的情况
即第一个任务在工作过程中,之后的每一个队列都已经将message缓存完毕,那么最终线程池task队列最大数量等同于队列数量;
简化为一个thread多个queue的的模型,如果第一个ConsumerRequest只有一个messageExt对象;那么假设需要0.5秒消费完毕,而在这0.5s内,下一个pullRequest拉取了32个messageExt对象。即消费完第二个ConsumerRequest16秒后以Mq的发送速度完全可以让之后的每个ConsumerRequest缓存到达满值
此时存在四个ConsumerRequest,每个ConsumerRequest有pullThresholdForQueue条messageExt对象。
这样每pullThresholdForQueue条消息会按照顺序被消费者线程分别消费;如下图所示:
pullBatch一直是32,每个consumerRequest的数组长度最开始也的确是32,但是阻塞消费者线程时,每个consumeRequest的treemap将持有250条消息,并且只有四个task;
下一个节点来看consumerThread:
如果consumerThread小于线程池的任务数量,之后的task必然需要等待前面的任务执行完毕,hz13的一个jvm 消费线程只有5个,任务队列至少有16个任务。那么后续11个任务只能饥饿等待。
显而易见,从这64000条消息并不是 64000/64*0.5=500秒执行完毕;而是:
64000/20*0.5=1600秒=26.6分钟
才能执行完毕,而对于最后的ConsumerRequest乃至64000之后的消息都是二十多分钟显然有较大的影响。
PS:这里要分清楚两种对象,最后一个任务队列TreeMap里的头节点CR任务和尾结点CR任务;其实在这种场景下消息量超过64000后消费时间是恒定的,但是头节点和尾节点显然不是同一个时间进入队列的;即头节点的这个任务应该很早就被消费、但是因为线程资源问题、他只能等到最后、整体来看消费时间是不平滑的、是不公平的消费机制。
而我们显示在业务繁忙时刚好延迟半小时左右,理论符合计算值。
从这儿就可以解释之前的各种奇怪现象
解决方案:
必然要保证threadNum>=messageQueue,但是不能一刀切;
①:对延迟要求高的topic 增加threadNum
②:减小对延时要求不高的topic的messageQueue;(有个考虑是环境内所有的topic如果是都是64个队列,这对broker的cpu、内存资源消耗较大)
③:利用并发消费,并发消费task完全不依赖于队列数量,基本整体有序消费,不存在上述问题