之前遇到的一个问题记录一下,当时公司的项目线上运行了几个月遇到了这个问题。
1 出现问题
客户反馈说系统异常,赶紧通知运维同学发现一直获取不到数据。运维同学因为急于恢复,选择了重启服务。这也导致了我们前前后后用了一个月才解决这个问题。
我们通过日志排查问题,发现程序中几个定时任务,其中一个出现了问题,如下:
2019-10-08 21:55:06.021 |-INFO [taskPool-7] cn.qyxing.qxt.service.RadarService [365] -| get livedata task running ……
日志只停留在这一条,从数据源获取数据,一直没有下载成功。
2 查找原因
网上查询了一下,得知:
SpringBoot使用 @scheduled 定时任务,执行的时候是在一个单线程中,如果前一个任务执行时间过长,则有可能会导致其他后续任务被阻塞,直到该任务执行完成。
此时我判断线程被阻塞,但是没有找到原因,同事说是不是有流未关闭,帮他核查了一遍代码,未发现问题。
此时突发奇想:既然是单线程的问题,换成异步的线程池是不是就能解决这个问题。
代码见文章最后 -- 异步任务的线程池和定时任务的异步线程
将代码更新以后,生产环境运行一个星期一直正常,和同事放下心来。
3 问题再现
万万没想到又过了一个星期,问题又出现了,感觉还是这个定时任务的问题,这次提醒运维同学先不要急着重启
jstack命令,导出当前环境的线程状态,结合运行日志,终于定位到问题所在
"taskPool-7" #88 prio=5 os_prio=0 tid=0x00007f60a9dca800 nid=0x9a8 runnable [0x00007f5f5e2db000]
java.lang.Thread.State: RUNNABLE
at java.net.PlainSocketImpl.socketAccept(Native Method)
at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513)
at org.apache.commons.net.ftp.FTPClient._openDataConnection_(FTPClient.java:858)
at org.apache.commons.net.ftp.FTPClient._retrieveFileStream(FTPClient.java:1980)
at org.apache.commons.net.ftp.FTPClient.retrieveFileStream(FTPClient.java:1967)
at cn.qyxing.qxt.util.FtpUtil.downloadFtpFile(FtpUtil.java:204)
at cn.qyxing.qxt.service.RadarService.getLiveData(RadarService.java:151)
at cn.qyxing.qxt.scheduled.QxtScheduledTask.gatherRadarDataTask(QxtScheduledTask.java:62)
at sun.reflect.GeneratedMethodAccessor135.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84)
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:93)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
4 解决问题
线程状态一直是 RUNNABLE,但是就是不往下执行,接着往下看发现是FTPClient的问题。
此时觉得奇怪,因为前三个月运行一直是正常的,查看版本更新记录,发现数据源发生过更换,之前提供的共享文件服务因为安全问题停用了,现在是改用ftp下载才出现的问题。
这一次给 FTPClient 设置了连接超时时间,ConnectTimeout、DataTimeout、DefaultTimeout,更新上线运行,谁知过了三个星期问题再次现。
因为已经知道是FTPClient问题,于是网上查询了一下,发现有不少人遇到这种假死问题。原来是FTP服务器采用主动模式的导致的,于是重新设置FTP服务器的传输模式,并指定端口,果然问题解决。
FTP有两种模式:主动模式(active mode)和被动模式(passive mode)。默认情况下是启动的主动模式。
在主动模式下,客户端会开启一个大于1024的端口(不固定)来接收FTP服务器通过20端口传过来的数据。这个过程FTP服务器主动向客户端发起数据连接请求,但是由于linux防火墙的屏蔽,客户端无法正常接收FTP服务器发送来的数据流,所以就会出现假死的现象
最后总结:FTP主动模式下载文件会假死,导致@scheduled定时任务阻塞
附:异步任务的线程池和定时任务的异步线程
@Configuration
@EnableAsync
@EnableScheduling
public class ExecutorConfig implements SchedulingConfigurer, AsyncConfigurer {
private Logger logger = LoggerFactory.getLogger(ExecutorConfig.class);
@Autowired
private TaskThreadPoolConfig config;
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("taskExecutor-");
//线程池维护线程的最少数量
executor.setCorePoolSize(config.getCorePoolSize());
//线程池维护线程的最大数量
executor.setMaxPoolSize(config.getMaxPoolSize());
//线程池所使用的缓冲队列
executor.setQueueCapacity(config.getQueueCapacity());
//线程池维护线程所允许的空闲时间
executor.setKeepAliveSeconds(config.getKeepAliveSeconds());
//当调度器shutdown被调用时等待当前被调度的任务完成
executor.setWaitForTasksToCompleteOnShutdown(true);
//等待时常
executor.setAwaitTerminationSeconds(config.getAwaitTerminationSeconds());
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncUncaughtExceptionHandler() {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
logger.error("==========================" + ex.getMessage() + "=======================", ex);
logger.error("exception method:" + method.getName());
}
};
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
TaskScheduler taskScheduler = taskScheduler();
taskRegistrar.setTaskScheduler(taskScheduler);
}
/**
* 并行任务使用策略:多线程处理
*
* @return ThreadPoolTaskScheduler 线程池
*/
@Bean(destroyMethod = "shutdown")
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(config.getSchedulerCorePoolSize());
scheduler.setAwaitTerminationSeconds(60 * 6);
scheduler.setThreadNamePrefix("taskPool-");
scheduler.setAwaitTerminationSeconds(60);
scheduler.setWaitForTasksToCompleteOnShutdown(true);
return scheduler;
}
}