Spring Task 卡死研究
故障现象
第一次
3月3日中午收到告警
项目描述:海外应用商店接口[发送时间:2018-03-03 13:08:49]
告警IP:hotapps.in.meizu.com
监控内容
HTTPConnectionPool(host='hotapps.in.meizu.com', port=80): Max retries exceeded with url: /c/oversea/hotapp/switchKey?keyName=gms (Caused by ConnectTimeoutError(, 'Connection to hotapps.in.meizu.com timed out. (connect timeout=30)'))
监控平台会定时调用业务接口,根据返回结果来判断业务接口是否正常。这个告警信息显示连接超时,有以下几种可能
nginx 服务挂了
nginx 服务没有挂,但非常的慢,导致不能及时和对端完成创建连接的 3 次握手
在连接过程中,网络异常:例如网络抖动,网络断开
由于之后一直没有收到告警,判断是网络抖动,没有太关注,直到晚上查看 zabbix,如下图
可以看到 13 点左右 redis 里的 key 数量迅速降低,判断是预热 redis 缓存的 task 没有正常执行,由于我们预热 redis 的 task 主要运行在 138 上,怀疑 138 挂了;但是没有接到更多的告警信息又不支持这个判断,此时是周末,无法远程到 138 上查看日志,为了保险,大概在 21 点左右,远程重启了 138,可以看到 redis 的 key 数量此后逐渐上升
之后查看 138 上的 jetty 日志,如下
task 启动和结束都会打印日志,但是可以看到 13 点 45 分以后就没有日志了(机房时区为 UTC,和北京时间相差 8 小时),直到晚上 21点26分 重启以后才能正常输出日志
第二次
时间是3月6日中午,查看 zabbix 发现异常,如下图
一般来说, redis 的 key 数量变动曲线应该是平滑的,这样直线式下降,基本意味着预热 redis 的 task 没有正常执行,查看 jetty 日志验证一下
从日志可以看出来,12 点 30 以后就没有 task 能启动了
分析
这 2 次 task 卡死前最后一条日志,都是
preheat WebYeaAds start...
WebYeaAds task 通过 http 请求从 webeye 拉取广告列表,更新数据库里的广告信息并刷新 redis 缓存。从日志来看,该 task 正常启动了,但无法结束,结合第一次卡死时有网络抖动的情况,感觉和网络相关,判断是请求 webeye 数据时,http 请求没有设置超时造成的:即程序一直在等待 webeye 返回广告信息。但是为什么这个 task 无法结束会造成整个 task 体系卡死呢?
百度了一下 "spring task 卡死"
,发现这种情况并不罕见,而且基本都认为是 http 请求未设置超时时间造成的——实际上这是表面原因,并不是本质原因,后面会提到
验证
可以简单做个试验来证实一下,http 请求是否能卡死 task
<!-- 创建 2 个 task: test1,test2 -->
<task:scheduled-tasks>
<task:scheduled ref="taskJobService" method="test1" cron="0 0/1 * * * ?"/>
<task:scheduled ref="taskJobService" method="test2" cron="0 0/2 * * * ?"/>
</task:scheduled-tasks>
task 代码如下,需要注意的是 HttpClientUtil.httpGet() 方法是没有设置 http 请求超时的
public void test1() {
logger.info("test1...done");
}
public void test2() {
logger.info("test2...");
HttpClientUtil.httpGet("http://browser.in.meizu.com:8080/api/test.debug?name=mike", null);
logger.info("test2...done");
}
test.debug 接口代码如下
@Controller
@RequestMapping("/api/")
public class TestController {
@ResponseBody
@RequestMapping("test.debug")
public String test(@RequestParam(required = true) String name) {
return name;
}
}
立即返回
启动程序后,可以看到日志持续输出
[11:24:00] [INFO ] - test1...done
[11:24:00] [INFO ] - test2...
[11:24:00] [INFO ] - test2...done
[11:25:00] [INFO ] - test1...done
[11:26:00] [INFO ] - test1...done
[11:26:00] [INFO ] - test2...
[11:26:00] [INFO ] - test2...done
[11:27:00] [INFO ] - test1...done
[11:28:00] [INFO ] - test2...
[11:28:00] [INFO ] - test2...done
[11:28:00] [INFO ] - test1...done
休眠 3 分钟 v1
如果让 test.debug 休眠 3 分钟,task 会怎么进行呢
@Controller
@RequestMapping("/api/")
public class TestController {
@ResponseBody
@RequestMapping("test.debug")
public String test(@RequestParam(required = true) String name) {
try {
Thread.sleep(180*1000);
} catch (InterruptedException e) {
}
return name;
}
}
日志输出
[15:15:00] [INFO ] - test1...done
[15:16:00] [INFO ] - test1...done
[15:16:00] [INFO ] - test2...
[15:19:00] [INFO ] - test2...done
[15:19:00] [INFO ] - test1...done
[15:20:00] [INFO ] - test2...
[15:23:00] [INFO ] - test2...done
[15:23:00] [INFO ] - test1...done
这个日志很有意思,值得分析一下
test1 仅在15,19,23分时执行了,按配置应该是每分钟执行
test2 仅在16,20分执行了,按配置应该是每2分钟执行
初步的结论是:task 的执行是顺序的,一个 task 不执行完,其他 task 到时间了也不能执行
那么,是不是因为其中有个 task 是 http 请求才会导致这种结果呢?再试一下
休眠 3 分钟 v2
这次直接让 test2 休眠 3 分钟,代码如下
public void test2() {
logger.info("test2...");
try {
Thread.sleep(180*1000);
} catch (InterruptedException e) {
}
logger.info("test2...done");
}
日志输出
[15:30:00] [INFO ] - test2...
[15:33:00] [INFO ] - test2...done
[15:33:00] [INFO ] - test1...done
[15:34:00] [INFO ] - test2...
[15:37:00] [INFO ] - test2...done
[15:37:00] [INFO ] - test1...done
[15:38:00] [INFO ] - test2...
这很明显了,task 们是串行的,和 task 是在执行什么任务无关。如果多个 task 要在同一时刻执行,究竟谁先执行,谁后执行,那恐怕要看天意了......
单线程验证
理论上来说,task 应该是多线程并行的,但 spring task 框架的表现却是单线程的,接下来在日志里打印出线程信息看下,为了测试方便,就不做休眠了。日志输出如下
[15:48:00] [INFO ] [pool-31-thread-1]- test2...
[15:48:00] [INFO ] [pool-31-thread-1]- test2...done
[15:48:00] [INFO ] [pool-31-thread-1]- test1...done
[15:49:00] [INFO ] [pool-31-thread-1]- test1...done
[15:50:00] [INFO ] [pool-31-thread-1]- test1...done
[15:50:00] [INFO ] [pool-31-thread-1]- test2...
[15:50:00] [INFO ] [pool-31-thread-1]- test2...done
[15:51:00] [INFO ] [pool-31-thread-1]- test1...done
还真是单线程~!
多线程配置
为毛 spring task 框架使用单线程呢?明明配置了线程池啊
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:task="http://www.springframework.org/schema/task" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-4.0.xsd">
<task:scheduler id="scheduler" pool-size="5" />
<task:annotation-driven executor="executor"
scheduler="scheduler" />
</beans>
看下 官方文档 怎么说
文档说得很清楚,scheduled-tasks 节点的 scheduler 属性如果未指定,则使用一个单线程的执行器,所以我们修改一下配置文件,如下
<!-- 创建 2 个 task: test1,test2 -->
<task:scheduled-tasks scheduler="scheduler">
<task:scheduled ref="taskJobService" method="test1" cron="0 0/1 * * * ?"/>
<task:scheduled ref="taskJobService" method="test2" cron="0 0/2 * * * ?"/>
</task:scheduled-tasks>
多线程验证
采用休眠 3 分钟的 v1 版本,输出如下
[16:47:00] [INFO ] [scheduler-5]- test1...done
[16:48:00] [INFO ] [scheduler-5]- test1...done
[16:48:00] [INFO ] [scheduler-3]- test2...
[16:49:00] [INFO ] [scheduler-5]- test1...done
[16:50:00] [INFO ] [scheduler-5]- test1...done
[16:51:00] [INFO ] [scheduler-2]- test1...done
[16:51:00] [INFO ] [scheduler-3]- test2...done
[16:52:00] [INFO ] [scheduler-4]- test1...done
[16:52:00] [INFO ] [scheduler-2]- test2...
[16:53:00] [INFO ] [scheduler-4]- test1...done
可以看到
task 是多线程执行的
不同的 task 是并行的
同一个 task 是串行的
结论
上述 2 次 task 卡死的原因有 2 个
task 被配置为单线程模式
http 请求未设置超时时间
事实上卡死的说法并不完全正确,实际情况是用于执行 task 的线程无法释放,导致其他 task 被阻塞,这时 jetty 本身是正常的,客户端调用接口不受影响
应该认识到,就算是配置为多线程,依然是存在线程全部被占用无法释放的可能性的
思考
用注解的话,是多线程还是单线程呢?
Last updated