记一次生产中的服务端报阿里云iot连接中断的故障问题
我们做的业务是一套软硬件结合的通信光缆智能化巡检方案。无人机设备在空中自主巡航作业时,会将飞控中采集的心跳信息实时推送到IOT平台,然后服务端与IOT平台建立连接,订阅无人机推送的心跳数据,并转发到客户端(APP、WEB巡检平台)在地图界面动态呈现。以此实现通过软件平台监控无人机作业的目的。
当然,巡检平台还会展示一些无人机巡检作业途中采集的影像数据,这里就不详细介绍了。
某一天,运营团队反馈在APP和WEB平台的地图界面上,很长一段时间看不到正在执行作业的无人机图标。通过查阅线上后台日志发现,打印了如下报错信息:
这个故障,导致无人机巡检超出可视范围或地面站监控范围,除了机身上额外安装的GPS装置,基本无法监控无人机在空中的位置和状态,十分危险。虽然无人机正常情况下会按照飞前下发的航线自主飞行。我们一个架次会在空中巡航180KM左右,大致花费时间1小时40分钟左右。
在排查故障的过程中,联系到现场,无人机已经结束作业,正常降落。
由于服务端对接阿里云IOT平台,代码是基于官网提供的Java SDK接入示例代码编写的。根据日志,定位到程序逻辑进入了JMS连接监听器的 连接中断 方法中了。除此之外,再看不到任何有效的报错信息。
基于之前的经验,现场作业过程中,有时也会有连接中断的问题,但是一般都是出在设备端。我们在无人机机舱中装载了自主研发的网联通讯模组,提供4G网络,实现无人机与IOT平台的网络连接,并将飞控中的实时心跳数据通过4G网络推送到IOT平台。但是,由于空中作业,网络信号有时会很不稳定,导致短时间的断联。网络信号恢复后,也会及时重连上,并补传心跳消息。
这种由于设备端网络抖动引起的断联,会导致IOT平台和服务端都接收不到心跳消息。
而目前的故障是:无人机正常将心跳消息推送到IOT平台上了,但是服务端未订阅到心跳消息。也就是说,下游IOT与服务端的连接断开了。而我们的服务端项目是部署在稳定的机房服务器中的,网络也是通畅的,与IOT平台的网络连接应该是十分稳定的。所以跟研发同事一起分析了很久,没找到原因所在。
实在摸不出问题原因,只能提工单,问技术人员了。不过,对于我这种小菜鸡,详细地描述出自己的问题,让阿里工程师理解,还真不容易。通过咨询,确认导致进入连接中断的原因,百分之八九十是 AMQP消费客户端的连接数超过了IOT平台设置的上限64。技术人员说,如果连接数超过64了,就会自动中断连接。
之后,我们进入了IOT实例,查看对应的消费组中在线客户端数,确实是到上限了:
于是赶紧去看代码,看初始化消费组,创建AMQP消费端的逻辑。核心代码片段如下:
// 指定单个进程启动的连接数
// 单个连接消费速率有限,请参考使用限制,最大64个连接
// 连接数和消费速率及rebalance相关,建议每500QPS增加一个连接
private static int connectionCount = 1;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
...
for (int i = 0; i < connectionCount; i++) {
long timeStamp = System.currentTimeMillis();
//签名方法:支持hmacmd5,hmacsha1和hmacsha256
String signMethod = "hmacsha1";
//控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。
//建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。
String clientId = WebSocketIP;
//UserName组装
String userName = clientId + "-" + i + "|authMode=aksign"
+ ",signMethod=" + signMethod
+ ",timestamp=" + timeStamp
+ ",authId=" + accessKey
+ ",iotInstanceId=" + iotInstanceId
+ ",consumerGroupId=" + consumerGroupId
+ "|";
//password组装
String signContent = "authId=" + accessKey + "×tamp=" + timeStamp;
String password = doSign(signContent, accessSecret, signMethod);
//按照qpid-jms的规范,组装连接URL。
String connectionUrl = "failover:(amqps://" + uid + ".iot-amqp." + regionId + ".aliyuncs.com:5671?amqp.idleTimeout=80000)"
+ "?failover.reconnectDelay=30";
Hashtable<String, String> hashtable = new Hashtable<>();
hashtable.put("connectionfactory.SBCF", connectionUrl);
hashtable.put("queue.QUEUE", "default");
hashtable.put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.qpid.jms.jndi.JmsInitialContextFactory");
Context context = new InitialContext(hashtable);
ConnectionFactory cf = (ConnectionFactory) context.lookup("SBCF");
Destination queue = (Destination) context.lookup("QUEUE");
// Create Connection
Connection connection = cf.createConnection(userName, password);
connections.add(connection);
((JmsConnection) connection).addConnectionListener(myJmsConnectionListener);
// Create Session
// Session.CLIENT_ACKNOWLEDGE: 收到消息后,需要手动调用message.acknowledge()
// Session.AUTO_ACKNOWLEDGE: SDK自动ACK(推荐)
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
connection.start();
// 创建接收消息的连接
MessageConsumer consumer = session.createConsumer(queue);
consumer.setMessageListener(messageListener);
}
...
}
根据代码逻辑分析,初始化消费组实例后,只会初始化一个AMQP客户端,但是IOT管理平台上却看到了60多个AMQP客户端,确实很奇怪。之后分析到这段初始化的逻辑是在 onApplicationEvent() 方法中执行的,这个东西有点眼熟,百度了一下,opApplicationEvent()方法是一般在Spring容器启动后触发,而且经常会有执行两次的情况。一般在业务开发中,创建一个自定义的监听器,实现了Spring的事件监听器ApplicationListener< ContextRefreshedEvent >,重写onApplicationEvent()方法,在里面编写一些Spring容器初始化完毕后执行的操作。
当然,在我们的业务场景中期望是只在容器启动完毕后,执行一次 “初始化消费组实例后,初始化一个AMQP客户端” 的操作。但是,很不幸,它执行了很多次。之后继续查阅线上的后台日志,在报错信息的上下仔细分析,最终是发现了问题所在。
服务端创建的这个无人机心跳监听器的类,会连接IOT,并监听当前服务器上的多个网联设备,也就是说,如果同时有多架无人机执行作业,那么监听器会通过线程池处理多个设备的心跳接收,以及后续业务。其中有一处业务逻辑是:当无人机执行完任务,降落后,会将缓存中任务相关的心跳数据一次性写入到DB中。而这里需要获取dao层的bean,同事采用new ClassPathXmlApplicationContext()的方式获取上下文来加载bean。
这种方式获取bean会带来一个问题:会重新加载配置文件,而applicationContext.xml中又加载了application.properties配置文件,当前监听器类中正好通过@Value注解读取了application.properties配置文件中的某几项属性。这样会导致线程每执行一次加载上下文的代码,都会触发onApplicationEvent()的执行,进而导致消费组中新增一个AMQP客户端连接。现场作业频次高的话,一天可能有3架无人机同时作业,每架无人机平均一天作业3个架次。一天就差不多会增加9个客户端连接,差不多作业一个星期就会到达64的上限,我丢!!!。通过运营人员对前期故障出现频次的统计,基本吻合,最后对代码进行了调整。重新上线后,再也没出现之前的故障了。
依据上述分析,导致生产服务后台打印iot连接中断的原因就是,代码中有一处bean注入的方式有问题,导致重复执行多次iot消费组初始化操作,进而导致amqp客户端连接数超过平台设置的上限64,触发平台自动中断连接的规则。
代码中两处地方做了调整:
// 1. onApplicationEvent()中执行初始化消费组和客户端连接的代码加了一层判断
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (null == event.getApplicationContext().getParent()) {
// 初始化消费组和客户端连接的逻辑
}
}
// 2.获取dao层bean的位置做了调整,直接在写入DB逻辑的外层通过@AutoWired注入
@Autowired
private TaskMapper taskMapper;
这个故障的排查时间算是比较长的,故障原因是代码本身的问题,而且是一个比较基础的框架上的问题,最后还好及时修复了故障。
也总结了一些分析问题,解决问题的思路:
这次排错的过程中,也体会到一点:日常工作中,除了对付工作,也要巩固基础,深究原理,要持续学习。
对于onApplicationEvent()和new ClassPathXmlApplicationContext()的问题,提供两篇博文,供大家参考: