第一次独立部署:catalina.out 里那几行看不懂的报错
九月份,师傅让我自己往测试服务器部署一次项目。我以为就是把 war 包扔进去,结果从下午两点折腾到晚上八点。这篇把我那天遇到的四个坑和后来整理的排查顺序记下来,免得下次再花六个小时。
环境:CentOS 7.4,JDK 8u151,Tomcat 8.5.32,MySQL 5.7。
坑一:启动 8 分钟才起来,卡在 Creation of SecureRandom
第一次启动,执行 ./startup.sh 之后 catalina.out 停在这儿不动了:
14:32:07.415 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 482113 ms
482 秒。我一度以为挂了。用 jstack 抓了一下主线程:
$ jps -l
12034 org.apache.catalina.startup.Bootstrap
$ jstack 12034 | grep -A 15 '"main"'
"main" #1 prio=5 os_prio=0 tid=0x00007f8c4c009800 nid=0x2f02 runnable
java.lang.Thread.State: RUNNABLE
at java.io.FileInputStream.readBytes(Native Method)
at java.io.FileInputStream.read(FileInputStream.java:255)
at sun.security.provider.SeedGenerator$URLSeedGenerator.getSeedBytes(...)
at sun.security.provider.SeedGenerator.generateSeed(SeedGenerator.java:144)
at sun.security.provider.SecureRandom$SeederHolder.<clinit>(SecureRandom.java:206)
...
at org.apache.catalina.util.SessionIdGeneratorBase.start(SessionIdGeneratorBase.java:266)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5214)
栈很清楚:Tomcat 启动时生成 Session ID 需要随机数,JDK 默认的 /dev/random 是阻塞式的,熵池不够就一直卡着。虚拟机的熵池来源少,尤其容易中招。
验证一下机器的熵:
$ cat /proc/sys/kernel/random/entropy_avail
128
果然很低(正常应该在 1000 以上)。三种改法:
# 方式一:改 JRE 的 java.security(一劳永逸,推荐)
$ vi $JAVA_HOME/jre/lib/security/java.security
securerandom.source=file:/dev/./urandom
# 方式二:在 Tomcat 启动参数里加(我用的这个,不用动 JDK)
$ vi bin/setenv.sh
CATALINA_OPTS="$CATALINA_OPTS -Djava.security.egd=file:/dev/./urandom"
# 方式三:装熵池补充工具
$ yum install -y rng-tools && systemctl start rngd
注意那个路径是 /dev/./urandom 而不是 /dev/urandom,中间的 ./ 不能省。JDK 8 的 SeedGenerator 里有段代码会对路径做字符串判断,如果匹配到 /dev/urandom 会被替换回 /dev/random,写上 ./ 才能绕过去。这个坑我查了半天才在源码里看到。
改完之后启动时间从 482113 ms 降到 6234 ms。
坑二:端口被占,Address already in use
第二次启动时报错:
14:51:22.103 SEVERE [main] org.apache.catalina.core.StandardService.initInternal
Failed to initialize connector [Connector[HTTP/1.1-8080]]
org.apache.catalina.LifecycleException: Protocol handler initialization failed
Caused by: java.net.BindException: Address already in use
一般是上一个 Tomcat 没停干净。定位:
$ netstat -tlnp | grep 8080
tcp6 0 0 :::8080 :::* LISTEN 11892/java
# CentOS 7 默认没装 netstat,用 ss
$ ss -tlnp | grep 8080
LISTEN 0 100 :::8080 :::* users:(("java",pid=11892,fd=48))
$ ps -fp 11892
UID PID PPID C STIME TTY TIME CMD
root 11892 1 3 14:20 ? 00:00:38 /usr/local/jdk8/bin/java -Dcatalina.base=...
是我之前那个卡住的进程还在。这里有个教训:不要用 kill -9 停 Tomcat,正常的 shutdown.sh 会触发 shutdown hook,让连接池、线程池优雅关闭。实在停不掉再 kill -9,但之后要检查有没有残留的临时文件。
$ ./bin/shutdown.sh
# 等待 10 秒,再确认
$ ps -ef | grep catalina | grep -v grep
另外 Tomcat 8.5 默认会监听三个端口,都要检查:
- 8080:HTTP 连接器
- 8005:shutdown 端口,被占的话 shutdown.sh 会失效
- 8009:AJP 连接器,不用就注释掉
server.xml里那行
坑三:控制台和页面的中文全是问号
服务起来了,但接口返回的中文是 ???,catalina.out 里的中文日志也乱码。
三个地方要对齐:
# 1. server.xml 的 Connector 加 URIEncoding
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
URIEncoding="UTF-8" />
这个只解决 GET 请求 URL 里带中文的问题。
# 2. bin/catalina.sh 里设置文件编码
CATALINA_OPTS="$CATALINA_OPTS -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8"
# 3. 确认系统 locale
$ locale
LANG=en_US.UTF-8
LC_ALL=
我那天的情况是 LANG 是 POSIX,导致 file.encoding 被 JDK 推断成 ANSI_X3.4-1968(也就是 ASCII)。改法:
$ vi /etc/locale.conf
LANG="en_US.UTF-8"
$ source /etc/locale.conf
POST 请求体的乱码还得配 CharacterEncodingFilter,Spring Boot 项目默认已经配了,传统 SSM 项目要在 web.xml 里手动加。
坑四:war 包解压了但访问 404
日志显示部署完成,浏览器访问却是 404:
15:20:33.221 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployWAR
Deploying web application archive [/usr/local/tomcat/webapps/order-service.war]
15:20:41.907 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployWAR
Deployment of web application archive [...] has finished in 8,686 ms
我排查的顺序:
# 1. 看解压出来的目录叫什么名字
$ ls webapps/
order-service.war order-service/
# 2. 看 Tomcat 认出来的 context path
$ grep -i "context path" logs/catalina.out
Deploying web application archive [/usr/local/tomcat/webapps/order-service.war]
我的问题其实很蠢——我访问的是 http://ip:8080/,但 context path 是 /order-service。ROOT 目录里还躺着 Tomcat 默认的欢迎页,所以没报 404 而是显示了那只猫。
真要部署到根路径,有几种做法,但直接改名 ROOT.war 之外,更稳妥的是在 server.xml 的 Host 里加 Context:
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
<Context path="" docBase="/usr/local/tomcat/webapps/order-service" reloadable="false" />
</Host>
这里有个新手常踩的雷:docBase 指向 war 包解压后的目录,而 appBase 是 webapps。此时 war 和目录都在 appBase 下,Tomcat 会部署两次,一次按 context path /order-service(自动部署),一次按 path=""(配置部署)。日志里会看到两条 deployWAR,应用被初始化两遍,定时任务跑两次。
正确做法是把应用放在 appBase 之外:
<Context path="" docBase="/data/apps/order-service" reloadable="false" />
另外 autoDeploy="true" 在生产环境建议关掉,它会在运行时扫描 webapps 目录变化,被测试环境的同学误传一个 war 上去就会触发重新部署。
内存参数怎么配
Tomcat 默认没有 JVM 参数配置,用的是 JVM 默认值,堆最大只有物理内存的 1/4。在 bin/setenv.sh 里配(这个文件默认不存在,自己新建):
#!/bin/sh
CATALINA_OPTS="$CATALINA_OPTS -server"
CATALINA_OPTS="$CATALINA_OPTS -Xms2g -Xmx2g"
CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"
CATALINA_OPTS="$CATALINA_OPTS -Xss512k"
CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
CATALINA_OPTS="$CATALINA_OPTS -XX:+HeapDumpOnOutOfMemoryError"
CATALINA_OPTS="$CATALINA_OPTS -XX:HeapDumpPath=/data/dump/"
CATALINA_OPTS="$CATALINA_OPTS -Djava.security.egd=file:/dev/./urandom"
CATALINA_OPTS="$CATALINA_OPTS -Dfile.encoding=UTF-8"
几个说明:
-Xms和-Xmx设成一样,避免运行时扩容触发 Full GC。这个建议师傅反复强调过。- JDK 8 里永久代换成了 Metaspace,写
-XX:PermSize会收到警告ignoring option PermSize。我们项目依赖多,Metaspace 给到 512m。 -XX:+HeapDumpOnOutOfMemoryError一定要开。OOM 之后没有 dump 文件,就只能重启了事,下次还犯。- 放在
CATALINA_OPTS而不是JAVA_OPTS,因为JAVA_OPTS会同时作用于 start 和 stop,而 stop 那个进程不需要 2G 堆。
我现在的排查清单
遇到起不来,按这个顺序走一遍,基本不会超过十分钟:
ps -ef | grep catalina:确认没有残留进程。ss -tlnp | grep -E '8080|8005|8009':端口占用。tail -200f logs/catalina.out:先看有没有SEVERE,再看Caused by。tail -f logs/localhost.2018-09-14.log:应用自身初始化失败(比如 Spring 容器启动失败、数据库连接不上)会在这儿,不在 catalina.out。- 卡住不动就
jstack看主线程栈。 ls -l webapps/和df -h:磁盘满了会导致 war 解压一半失败,症状是部署成功但 class 不全,报 NoClassDefFoundError。
第六条是我后来才加上去的。有次测试环境磁盘被日志打满,war 解压出来只有一半的 class,报错信息完全指不到根因,我查了一下午。