阿里云
社区时光机
发表主题 回复主题
  • 37162阅读
  • 3回复

[干货分享]对症下药:Tomcat停机过程分析与线程处理方法

级别: 码农
发帖
112
云币
305
`[sFh%:  
Vx$;wU Y  
根据上面的分析,造成异常的主要原因就是线程没有及时终止。所以解决办法的关键就是如何在容器终止之前,优雅地终止用户启动的线程 =:&xdphZ+  
PT2;%=f  
创建己的Listener作为终止线程的通知者 U*[E+Uq}:N  
}*6BaB  
根据分析,项目中主要用到用户创建的线程,包括四种:
  • m=01V5_  
    Thread
  • ~(V\.hq  
    Executors
  • 8 ??-H0P  
    Timer
  • b)I-do+  
    Scheduler
Eb ILAJ  
!1)aie+p6  
所以最直接的想法就是建立一种对这些组件的管理模块,具体做法分为两步: 13s/m&  
  • m;dwt1'Zw  
    第一步:创建一个基于Listener的管理模块,并将上面提到的四种类型的类实例交由模块管理。
  • M1kA-Xr  
    第二步:在Listener监听到Tomcat停机时,触发其管理的实例对应的结束方法。比如Thread触发interrupt()方法,ExecutorService触发shutdown()或者shutdownNow()方法(依赖具体策略选择)等。
,R[$S"]!SH  
Y*c]C;%=  
值得注意的是,对于用户创建的Thread需要响应Interrupt事件,即在isInterrupted()返回true或在捕获到InterruptException后,退出线程。事实上,创建不响应Interrupt事件的线程是一种非常不好的设计。 FW|& iS$  
l53i {o  
创建自己Listener的优点是可以主动在监听到事件时阻塞销毁进程,为用户线程做清理工作争取些时间,因为此时Spring还没有销毁,程序的状态一切正常。 :.x(( FU  
>38>R0k35  
缺点就是对代码侵入性大,并且依赖于使用者的编码。 <T_3s\  
GY]P(NU  
使用Spring提供的TaskExecutor @N Yl4N  
#|b*l/t8  
为了应对在webapp中管理自己线程的目的,Spring提供了一套TaskExcutor的工具。其中的ThreadPoolTaskExecutor与Java5中的ThreadPoolExecutor非常类似,只是生命周期会被Spring管理,Spring框架停止时,Executor也会被停止,用户线程会收到中断异常。同时,Spring还提供了ScheduledThreadPoolExecutor,对于定时任务或者要创建自己线程的需求可以用这个类。对于线程管理,Spring提供了非常丰富的支持,具体可以看这里: z)3TB&;  
:lB*kmg  
https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#scheduling。 aRMlE*yW  
!vwio!  
使用Spring框架的优点是对代码侵入性小,对代码依赖性也相对较小。 .C^P6S2oJ  
c|?(>  
缺点是Spring框架不保证线程中断与Bean销毁的时间先后顺序,即如果一个线程在捕获InterruptException后,再通过Spring去getBean时,依然会触发IllegalSateException。同时使用者依然需要检查线程状态或者在Sleep中触发中断,否则线程依然不会终止。 [9">}l  
E`fG9:6l]  
其它需要提醒的 |o5eG><  
R0-0  
在上面的解决方法中,无论是在Listener中阻塞主线程的停止操作,还是在Spring框架中不响应interrupt状态,都能为线程继续做一些事情争取些时间。但这个时间不是无限的。在catalina.sh中,stop部分的脚本中我们可以看到(这里删繁就简体现一下): =*\.zr  
i6if\B  
#Tomcat停机脚本摘录 O*:87:I d  
#第一次正常停止 cI2Ps3~"Q  
eval "\"$_RUNJAVA\"" $LOGGING_MANAGER $JAVA_OPTS \ +dCDk* /m  
    -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\"" \ _w\9 \<%  
    -Dcatalina.base="\"$CATALINA_BASE\"" \ h[remR# 3\  
    -Dcatalina.home="\"$CATALINA_HOME\"" \ 7h 54j  
    -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \ v}$Q   
    org.apache.catalina.startup.Bootstrap "$@" stop roS" q~GS,  
#如果终止失败 使用kill -15 GY$Rkg6d  
if [ $? != 0 ]; then !PA:#]J  
    kill -15 `cat "$CATALINA_PID"` >/dev/null 2>&1 eft=k}  
#设置等待时间 $nE{%?n-#  
SLEEP=5 8T3,56 >  
if [ "$1" = "-force" ]; then CPJ<A,V  
    shift 8:TX9`,  
    #如果参数中有-force 将强制停止 E22o-nI?1  
    FORCE=1 #*.!J zOg  
fi N'2?Zb  
while [ $SLEEP -gt 0 ]; do /]U$OP*0  
    sleep 1 %p}vX9U')  
    SLEEP=`expr $SLEEP - 1 ` CF;Gy L1M  
done wC{sP"D  
#如果需要强制终止 kill -9 iV{_?f1jo  
if [ $FORCE -eq 1 ]; then oKn$g[,SJh  
    kill -9 $PID )g:5}+  
fi zB%~=@Q^6  
d8wGXNd7B  
从上面的停止脚本可以看到,如果配置了强制终止(我们服务默认配置了),你阻塞终止进程去做自己的事的时间只有5秒钟。这期间还有其它线程在做一些任务以及线程真正开始终止到发现终止的时间(比如从当前到下一次调用isInterrupted的时间),考虑到这些的话,最大阻塞时间应该更短。 B /;(#{U;  
{vD$odi  
从上面的分析中也可以看到,如果服务中有比较重要又耗时的任务,又希望保证一致性的话,最好的办法就是在阻塞的宝贵的5秒钟时间里记录当前执行进度,等到服务重启的时候检测上次执行进度,然后从上次的进度中恢复。 o9xc$hX}  
x]7:MG$  
建议每个任务的执行粒度(两个isInterrupted的检测间隔)至少要控制在最大阻塞时间内,以留出足够时间做终止以后的记录工作。
级别: 架构狮
发帖
1243
云币
1801
只看该作者 沙发  发表于: 01-30
级别: 禁止发言
发帖
14
云币
17
只看该作者 板凳  发表于: 01-31
用户被禁言,该主题自动屏蔽!
级别: 码农
发帖
112
云币
305
只看该作者 地板  发表于: 01-31
发表主题 回复主题
« 返回列表上一主题下一主题

限100 字节
如果您在写长篇帖子又不马上发表,建议存为草稿
 
验证问题: 38 + 45 = ?
上一个 下一个