mq Too many publishes in progress

在使用 org.eclipse.paho.client.mqttv3:1.2.5版本时,如果并发度过高或者跑了一段时间之后就会出现:Too many publishes in progress 的问题。首先看一下为什么会出现这个问题。

直接先上总结:

  • qos=0:在1.2.1版本之前token也会存在在 tokenStore里面,所以在为0时,如果并发过高就会导致释放 actualInFlight不及时的情况以及误删除其他msgId的token的情况,导致后续发送数据失败。由于现在我使用的是1.2.5版本qos=0已经不存入 tokenStore了,每次发送完之后就会删除掉token以及释放id,所以就不会出现Too many publishes in progress的问题和no new message IDs being available 的异常信息
  • qos=1:发送消息时会存入到tokenStore当中,等待服务端的回应,回应了ack之后才会执行释放actualInFlight,由于并发度过高,客户端等待服务端回应时的数据比较慢,释放来不及就会导致出现这种问题,而no new message IDs being available 也是因为并发度过高,获取到重复的id循环执行次数超过了两次就会抛出异常;至于为什么明明没有发送消息了,后续发送数据还是会 Too many publishes in progress 我猜测,tokenStore被误删了,导致后续没有获取到token就不执行释放actualInFlight;或者是等待发送队列里面数据过多,一直处于满的状态;再或者就是mqtt服务端返回ack因为网络的问题,消费方的数据消费很慢,后续就一直再等待。嗯,以上的猜测我觉得很合理。

问题复现

首先我们使用QOS=0 的情况下来用 100个线程同时发送消息,会发现消息都是正常接收回复。让我看看源码 QOS=0的情况下框架都做了什么事
在这里插入图片描述
在这里插入图片描述

QOS=0

org.eclipse.paho.client.mqttv3.MqttAsyncClient#publish() 方法:验证了topic,封装成了 MqttDeliveryToken对象,将发送的消息设置到对象中,然后调用 comms.sendNoWait(pubMsg, token);
在这里插入图片描述
org.eclipse.paho.client.mqttv3.internal.ClientComms#sendNoWait() :首先判断连接状态,以及消息类型;如果连接断开了并且 disconnectedMessageBuffer不为空并且里面的消息数量不为0,就会把发送的消息放入其中
在这里插入图片描述

org.eclipse.paho.client.mqttv3.internal.ClientComms#internalSend(): 当前方法并没有做什么特别的事就事设置了一下token中的client信息

org.eclipse.paho.client.mqttv3.internal.ClientState#send(): 这是重点方法

getNextMessageId():这个方法是mqtt用来生成消息id的,取值范围是1 - 65535 之间,如果取出的id 存在于 inUseMsgIds,就会继续循环下一次自增,循环次数超过两次就会抛出:32001状态码,就是:Internal error, caused by no new message IDs being available 的异常情况,这个情况在qos为1并且并发情况过高下就会出现这个问题。我也不明白为什么框架会限制在 65535之间(还是太菜了,理解困难)

public void send(MqttWireMessage message, MqttToken token) throws MqttException {
		final String methodName = "send";
		//首先判断msgId是否必须要求,默认为true并且是否为0,这个msgId是框架自动生成的id,并不是我们在创建MqttMessage时生成,默认为0
		if (message.isMessageIdRequired() && (message.getMessageId() == 0)) {
				if(message instanceof MqttPublish  && (((MqttPublish) message).getMessage().getQos() != 0)){
				//
						message.setMessageId(getNextMessageId());
				}else if(message instanceof MqttPubAck ||
						message instanceof MqttPubRec ||
						message instanceof MqttPubRel ||
						message instanceof MqttPubComp ||
						message instanceof MqttSubscribe ||
						message instanceof MqttSuback ||
						message instanceof MqttUnsubscribe || 
						message instanceof MqttUnsubAck){
					message.setMessageId(getNextMessageId());
				}
		}
		if (token != null) {
			message.setToken(token);
			try {
				token.internalTok.setMessageID(message.getMessageId());
			} catch (Exception e) {
			}
		}
			
		if (message instanceof MqttPublish) {
			synchronized (queueLock) {
			//重点来了,这里 实际在飞行窗口中的数据是否大于最大飞行窗口的数量(默认为10)
				if (actualInFlight >= this.maxInflight) {
					//@TRACE 613= sending {0} msgs at max inflight window
					log.fine(CLASS_NAME, methodName, "613", new Object[]{ Integer.valueOf(actualInFlight)});
					//这里就会抛出异常情况:Too many publishes in progress
					throw new MqttException(MqttException.REASON_CODE_MAX_INFLIGHT);
				}
				
				MqttMessage innerMessage = ((MqttPublish) message).getMessage();
				//@TRACE 628=pending publish key={0} qos={1} message={2}
				log.fine(CLASS_NAME,methodName,"628", new Object[]{ Integer.valueOf(message.getMessageId()),  Integer.valueOf(innerMessage.getQos()), message});
				//接下来根据qos的等级设置到对应的缓存当中,包括设置token
				switch(innerMessage.getQos()) {
					case 2:
						outboundQoS2.put( Integer.valueOf(message.getMessageId()), message);
						persistence.put(getSendPersistenceKey(message), (MqttPublish) message);
						tokenStore.saveToken(token, message);
						break;
					case 1:
						outboundQoS1.put( Integer.valueOf(message.getMessageId()), message);
						persistence.put(getSendPersistenceKey(message), (MqttPublish) message);
						tokenStore.saveToken(token, message);
						break;
				}
				//添加到待发送的队列当中
				pendingMessages.addElement(message);
				queueLock.notifyAll();
			}
		} else {
			//@TRACE 615=pending send key={0} message {1}
			log.fine(CLASS_NAME,methodName,"615", new Object[]{ Integer.valueOf(message.getMessageId()), message});
			
			if (message instanceof MqttConnect) {
				synchronized (queueLock) {
					// Add the connect action at the head of the pending queue ensuring it jumps
					// ahead of any of other pending actions.
					tokenStore.saveToken(token, message);
					pendingFlows.insertElementAt(message,0);
					queueLock.notifyAll();
				}
			} else {
				if (message instanceof MqttPingReq) {
					this.pingCommand = message;
				}
				else if (message instanceof MqttPubRel) {
					outboundQoS2.put( Integer.valueOf(message.getMessageId()), message);
					persistence.put(getSendConfirmPersistenceKey(message), (MqttPubRel) message);
				}
				else if (message instanceof MqttPubComp)  {
					persistence.remove(getReceivedPersistenceKey(message));
				}
				
				synchronized (queueLock) {
					if ( !(message instanceof MqttAck )) {
						tokenStore.saveToken(token, message);
					}
					pendingFlows.addElement(message);
					queueLock.notifyAll();
				}
			}
		}
	}

org.eclipse.paho.client.mqttv3.internal.CommsSender#run(): 然后就是 CommsSender线程中在循环读取数据。图中指示的 get() 方法

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

QOS=1

接下来看看QOS=1的情况,还是线程为100个同时发送;就会看到一下情况,正在进行过多的发布。但是其中能够正常处理其中的一些消息,跑到后续就会发现 Internal error, caused by no new message IDs being available 的异常信息。接下来看看为什么QOS=1的情况会出现这种情况。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
还是上面的代码步骤,但是qos=1的情况多了一个存储token的部分,在添加进队列当中时会存储一次token信息,目的就是等待服务端返回成功的ack。
在这里插入图片描述
org.eclipse.paho.client.mqttv3.internal.CommsReceiver#run(): 让我们看看 Receiver线程的run方法
在这里插入图片描述
org.eclipse.paho.client.mqttv3.internal.ClientState#notifyReceivedAck() : 当前方法就会根据收到消息的类型进行对应的操作,我们发送消息然后服务端回复的消息类型应该是 MqttPubAck对象
在这里插入图片描述
在这里插入图片描述
根据当前客户端的状态判断处理的逻辑,如果运行的状态为正在运行中,就将消息token放入到完成的队列当中,否则执行下面的逻辑,else中的 handleActionComplete() 会释放掉对应的token以及id,然后调用复写的回调 onSuccess和onFailure方法
在这里插入图片描述
上面的代码我们可以看到 消息被发送到了 completeQueue当中,而在消费者则是 CommsCallback线程中,接下我们看看run方法

org.eclipse.paho.client.mqttv3.internal.CommsCallback#run

在这里插入图片描述
org.eclipse.paho.client.mqttv3.internal.ClientState#notifyComplete:当前方法就会根据对应的消息类型进行操作
在这里插入图片描述
如果消息类型为 MqttPubAck的话就会执行oqs为1的释放,并且执行 decrementInFlight() 方法,将actualInFlight–,然后释放掉 inUseMsgIds 当中正在使用的id
在这里插入图片描述
总结:

  • qos=0:在1.2.1版本之前token也会存在在 tokenStore里面,所以在为0时,如果并发过高就会导致释放 actualInFlight不及时的情况以及误删除其他msgId的token的情况,导致后续发送数据失败。由于现在我使用的是1.2.5版本qos=0已经不存入 tokenStore了,每次发送完之后就会删除掉token以及释放id,所以就不会出现Too many publishes in progress的问题和no new message IDs being available 的异常信息
  • qos=1:发送消息时会存入到tokenStore当中,等待服务端的回应,回应了ack之后才会执行释放actualInFlight,由于并发度过高,客户端等待服务端回应时的数据比较慢,释放来不及就会导致出现这种问题,而no new message IDs being available 也是因为并发度过高,获取到重复的id循环执行次数超过了两次就会抛出异常;至于为什么明明没有发送消息了,后续发送数据还是会 Too many publishes in progress 我猜测,tokenStore被误删了,导致后续没有获取到token就不执行释放actualInFlight;或者是等待发送队列里面数据过多,一直处于满的状态;再或者就是mqtt服务端返回ack因为网络的问题,消费方的数据消费很慢,后续就一直再等待。
解决方案

第一种解决方案:调整 maxInFlight 的大小,因为默认为10,这个并发数量可太小了。很可惜,不管设置的多大,这个飞行窗口在并发很高的情况下依旧会出问题

第二种就是修改源码:我直接在本地项目中复制了同一个包路径,然后复制了一个 ClientState类,修改了其中 msgId生成的策略,以及飞行窗口的判断逻辑。很可惜,他源码居然加密了,修改源码后执行时会报 :signer information does not match signer information of other classes in the same package的错误,好家伙,我放弃了。后来还是选择了方案一,调整飞行窗口的数量,能够满足日常情况就行了(因为我们业务,没那么大的并发)

第三种解决方案:在发送数据时,创建一个阻塞队列,判断飞行窗口中实际数量是否大于最大值,如果大于最大值,那么就存入到等待队列当中等待条件满足后再取出数据进行发送。还可以创建副本客户端,用于并发程度过高的压力分担

第四种解决方案:加服务器,加服务

<think>好的,我现在需要帮助用户解决MQTT出现的“Too many publishes in progress (32202)”错误。首先,我得仔细理解用户的问题和提供的引用内容。用户在使用Paho MQTT客户端时,遇到了这个错误,并且引用了四个相关的资料,其中提到了一些可能的原因,比如并发发布时的阻塞、actualInFlight与maxInflight的判断,以及pendingMessages队列的问题。 首先,我应该明确这个错误的具体原因。根据引用[1],当actualInFlight >= maxInflight时,会抛出这个异常。这说明客户端在同时处理的消息数量超过了设定的最大值。引用[3]提到在并发发送QoS 0消息时,由于Sender线程和发布线程的竞争,可能导致token被提前移除,使得其他消息无法获取token,从而actualInFlight不断增加。而引用[4]指出这可能与Paho客户端内部的实现有关,特别是使用spring-integration-mqtt时的问题。 接下来,我需要整理解决方案。用户需要调整maxInflight参数,但引用[1]也提到这不能根本解决问题。因此,可能需要结合其他方法,如优化发布频率、使用同步发布或调整QoS级别。另外,引用[3]的解决方案提到修改源码,这可能是一个选项,但需要谨慎处理。 然后,我要考虑如何结构化回答。首先解释错误原因,再分点列出解决方案。需要确保每个方案都有对应的引用支持,并在末尾添加相关问题。同时,按照用户的要求,使用中文,行内公式用$...$,独立公式用$$...$$,但这个问题可能不涉及数学公式,所以可能不需要使用。不过,如果有涉及性能分析的地方,可能需要公式,比如计算吞吐量或并发量。 还要注意用户提到的系统级指令,比如引用标识的添加。每个引用的地方需要自然地在段落后加上[^n]。例如,在提到调整maxInflight时引用[1],在修改QoS级别时引用[3]。 还需要检查是否有其他可能的解决方案,比如优化客户端代码结构,使用批量发布,或者检查网络延迟导致消息堆积。此外,是否需要升级客户端版本?引用[4]提到是框架的问题,可能查看是否有更新的版本修复了这个问题。 最后,生成相关问题时要围绕MQTT的错误处理、性能优化、参数配置等方面。例如,如何监控actualInFlight的值,或者如何在高并发下优化MQTT客户端的性能。 总结步骤: 1. 解释错误原因:并发发布超过maxInflight限制,导致actualInFlight过高。 2. 分点解决方案:调整参数、优化发布频率、同步控制、QoS设置、代码修改、框架升级。 3. 每个方案对应引用来源。 4. 生成相关问题,帮助用户进一步解决问题。 需要确保回答清晰,步骤明确,引用正确,并且符合格式要求。</think>### 解决“Too many publishes in progress (32202)”错误的方法 此错误是由于MQTT客户端(如Paho)在并发发布消息时,未完成的消息数量(`actualInFlight`)超过预设的阈值(`maxInflight`)导致的。以下是具体解决方案: --- #### 1. **调整`maxInflight`参数** - **操作**:在客户端配置中增大`maxInflight`的值(默认为10)。例如,在Paho客户端中可通过`MqttConnectOptions.setMaxInflight()`设置[^1][^3]。 - **注意**:此方法仅缓解问题,无法根治高并发场景下的阻塞。 --- #### 2. **优化消息发布频率** - **操作**:使用限流机制(如令牌桶算法)控制单位时间的消息发布量,避免突发流量导致`actualInFlight`激增。 - **代码示例**: ```java // 使用RateLimiter(Guava库) RateLimiter limiter = RateLimiter.create(100); // 每秒100条 if (limiter.tryAcquire()) { mqttClient.publish(topic, message); } ``` --- #### 3. **同步控制并发发布** - **操作**:对发布操作添加同步锁(`synchronized`),强制串行发送,避免并发竞争问题。 - **代码示例**: ```java public synchronized void safePublish(String topic, MqttMessage message) { mqttClient.publish(topic, message); } ``` --- #### 4. **降低QoS级别** - **操作**:将消息的QoS从1/2调整为0(最多一次),避免因消息确认机制导致`actualInFlight`累积。 - **权衡**:QoS=0不保证消息可靠性,需根据场景选择。 --- #### 5. **修改客户端源码(高级)** - **操作**:若使用Paho客户端,可修改`TokenStore`相关逻辑,确保`token`分配与队列处理的原子性[^3]。 - **关键代码段**: ```java // 在Sender线程中同步处理pendingMessages队列 synchronized (pendingMessages) { Message message = pendingMessages.poll(); // 发送消息并更新token } ``` --- #### 6. **升级或更换MQTT框架** - **操作**:检查`spring-integration-mqtt`或Paho客户端的版本,升级到修复了此问题的版本。若问题持续,可尝试其他MQTT实现(如EMQX客户端)[^4]。 --- ### 根本原因分析 错误源于高并发场景下,消息发布线程与Sender线程对`pendingMessages`队列的资源竞争。当Sender线程移除消息的`token`时,其他消息可能因`token`不足而无法发送,最终导致`actualInFlight`超过阈值。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值