记一次生产环境java服务mqtt连接线程数过多的处理过程

项目介绍:

 本项目是负责发放机设备发放商品的平台。发放机设备是厂商控制,发放机平台是我们公司负责开发和维护。发放机设备和平台是通过mtqq协议通信的。

mqtt开发客户端使用的是org.eclipse.paho.client.mqttv3-1.2.0.jar。

mqtt服务器使用的是ActiveMQ。

事件:

  某日,客户反馈说项目不能正常访问了。我就要去linux服务器上查看日志,发现linux ssh连接后不能执行任何命令,报错-bash:fork:retry:no child processes 说明服务器进程的总线程数已经达到最大值了。等了一段时间后系统才自动恢复了。

如下图:

解决过程: 

1、我先执行tail -500f catalina.out查看tomcat的日志

日志中发现mqtt的监听断了,系统一直进行重连操作。

同时发现错误:Caused by: java.lang.OutOfMemoryError: unable to create new native thread表示没有内存创建新线程了

还发现Cannot allocate memory字样,也表示服务器没有内存了。

2、执行free -h命令,发现服务器内存使用率15.9GB/16GB(接近100%)

此时可以断定是某个程序创建了大量线程,占用了过多的内存。(服务器上有多个java服务)

3、执行top命令,之后按shift+m可以按照内存占用从大到小排序

可以发现第一个Java进程占用内存最多,记录它的pid 17045并返回

4、执行ps huHp  17045  | wc -l命令(或top -H -p 17045命令查看一下这个Java进程的总线程数

可以看到有2000多条线程(未截图),所以肯定是这个java进程惹的祸

5、执行jstack 17045 >thread_stack.txt命令dump线程信息到txt文件中并查看

可以发现有太多的mqtt的连接线程处于wait等待状态 

此时可以断定和mqtt的连接有关系,下面需要研究代码

6、查看项目中mqtt的连接代码


@RestController
@RequestMapping({"/weixin"})
public class SubscribeMqttController extends BaseController{
    //省略其他注入……
    
    @Autowired
    private RedisServiceImpl service;
    
    int qos = 1;
    
    private MqttClient client;

    //项目启动时执行连接mqtt操作
    @PostConstruct
    public void init(){
        registSubscribe();
    }
    //定时任务,每1分钟检查一次连接,若连接断开则重连
    @Scheduled(fixedDelay = 1000*60)
	public void clientManager() {
		if (!client.isConnected()) {
			registSubscribe();
		}
	}
    
    public void registSubscribe(){
        try {
            // host为主机名,test为clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,MemoryPersistence设置clientid的保存形式,默认为以内存保存
        	client= new MqttClient(HOST, clientid, new MemoryPersistence());
            // MQTT的连接设置
            MqttConnectOptions options = new MqttConnectOptions();
            // 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接
            options.setCleanSession(true);
            // 设置连接的用户名
            options.setUserName(userName);
            // 设置连接的密码
            options.setPassword(passWord.toCharArray());
            // 设置超时时间 单位为秒
            options.setConnectionTimeout(10);
            // 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
            options.setKeepAliveInterval(20);
            // 设置回调函数
            client.setCallback(new MqttCallback() {
                //连接丢失时的操作
                public void connectionLost(Throwable cause) {
                    //省略代码……
                }
                //收到mqtt消息时的操作
                public void messageArrived(String topic, MqttMessage message) throws Exception {
                    //省略代码……
                }

                public void deliveryComplete(IMqttDeliveryToken token) {
                    System.out.println("deliveryComplete---------"+ token.isComplete());
                }

            });
            client.connect(options);
        	subscribe();
            System.out.println("已开启发放机客户端消息监听");
        } catch (Exception e) {
        	try {
                client.disconnect();//断开连接
			} catch (MqttException e1) {
				e1.printStackTrace();
			}
            e.printStackTrace();
        }
    }
    /**
	 * 监听订阅
	 * @throws Exception
	 */
    public void subscribe() throws Exception{
        //省略代码……
    }

}

可以发现,servlet启动时会执行registSubscribe()方法,连接mqtt服务器。

若连接时抛出异常,或其他原因抛出异常,则执行client.disconnect()断开mqtt连接。

定时器每1分钟检查一下mqtt的连接状态,如果没有连接,则重连。

7、查看创建mqtt连接的MqttClient的源码

发现创建mqttclient时,会创建一个 ScheduledThreadPoolExecutor线程池,来保存连接的状态。

第6步的项目代码中,每次重连都会执行一次mqttclient的创建操作(创建新的连接池来保存连接),之前的连接池也没有销毁,里面的线程一直处于等待状态,最终陷入恶性循环---线程数一直增长,一直到服务器的最大线程数(最终服务器提示:unable to create new native thread)为止。

8、本地测试

在本地也测试了一下,发现client.connect(options);报错后,走trycatch后client.disconnect();也报错了,然后线程池的数量一直再增加。 client.disconnect()方法只是断开连接,并没有清除线程池的操作,所以之前的连接池中的线程还在。

9、使用client.close()方法

 找了找有close方法,里面有清除线程池的操作。。

改成close方法后,本地测试了一下线程数不在增加。

10、部署生产环境后测试

部署生产之后,执行free -h命令,发现服务器内存使用率8GB/16GB(一下子少了8个G),问题解决

 

另一种方式:

还有一种方法是把MqttClient的创建操作放到外面,每次重连不需要重新创建MqttClient,有一个就够了。

其实这种方式最好,省去创建MqttClient的操作,还能提升系统性能

  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
MQTT是一种基于发布/订阅模式的轻量级通信协议,常用于物联网中设备与服务器之间的通信。Java实现MQTT连接需要使用Eclipse Paho MQTT客户端库。 以下是一个简单的Java程序示例,用于连接MQTT代理并发布消息: ```java import org.eclipse.paho.client.mqttv3.*; import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; public class MQTTClient { public static void main(String[] args) { String broker = "tcp://localhost:1883"; String clientId = "JavaClient"; MemoryPersistence persistence = new MemoryPersistence(); try { MqttClient mqttClient = new MqttClient(broker, clientId, persistence); MqttConnectOptions connOpts = new MqttConnectOptions(); connOpts.setCleanSession(true); System.out.println("Connecting to broker: " + broker); mqttClient.connect(connOpts); System.out.println("Connected"); String topic = "test/topic"; String content = "Hello, World!"; int qos = 2; System.out.println("Publishing message: " + content); MqttMessage message = new MqttMessage(content.getBytes()); message.setQos(qos); mqttClient.publish(topic, message); System.out.println("Message published"); mqttClient.disconnect(); System.out.println("Disconnected"); } catch (MqttException me) { System.out.println("reason: " + me.getReasonCode()); System.out.println("msg: " + me.getMessage()); System.out.println("loc: " + me.getLocalizedMessage()); System.out.println("cause: " + me.getCause()); System.out.println("excep: " + me); } } } ``` 在这个例子中,我们使用`MqttClient`类来创建一个MQTT客户端。`MqttConnectOptions`类用于设置连接选项,例如设置清除会话标志(`setCleanSession(true)`)。接着,我们使用`mqttClient.connect(connOpts)`方法连接MQTT代理,并使用`mqttClient.publish(topic, message)`方法发布一条消息。最后,我们使用`mqttClient.disconnect()`方法断开连接。 请确保已经添加Eclipse Paho MQTT客户端库到您的Java项目中,否则编译代码时将会出现错误。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值