ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CentOS搭建Java与Tomcat:从环境准备到生产加固全攻略

CentOS搭建Java与Tomcat:从环境准备到生产加固全攻略

CentOS服务器上搭Java和Tomcat,这活儿看起来就两条命令的事,真上手时坑一点也不少。我从最早在自己电脑上照着网文一步步敲到现在能给生产环境做部署方案,中间走过的弯路、踩过的坑攒了不少,今天全倒出来聊一聊。这篇文章就是给准备在CentOS上从零搭建Java Web运行环境的同学看的,不管是自己练手、公司内网部署,还是接手一台陌生的服务器,照着这个思路走,能少折腾好几个晚上。

我会把安装过程拆成几段来讲:装之前要确认什么、JDK怎么选怎么装、环境变量怎么配才不出错、Tomcat怎么规划和启动,以及部署完成之后建议顺手做掉的长稳配置。文章里的命令我都按企业里最常见的CentOS 7.x和CentOS 8.x分别备注,遇到版本差异会单独说清楚。

1. 动手前先确认环境:版本、架构、用户和目录都要想清楚

很多教程上来就让你yum install java或者直接扔一个安装包链接,结果装到一半才发现系统是64位还是32位都没搞清,或者GCC版本不对,白白浪费时间。我在一台CentOS机器上踩过最无语的坑,是前人装过一个老版本的MySQL自带的OpenJDK,导致我后续配环境变量时各种串台。所以拿到一台机器,第一件事不是下载,而是把系统的底细摸清楚。

1.1 确认CentOS版本和CPU架构

装Java之前先确认几件事,命令如下:

cat /etc/redhat-release uname -m

/etc/redhat-release会告诉你具体是CentOS 7还是8,uname -m会告诉你架构,x86_64就是64位,aarch64就是ARM架构。为什么这个重要?因为JDK的安装包分平台,下载错了直接装不上,或者装了跑不起来,报错还特别奇怪。

另外要看系统里是不是已经有Java了,避免版本冲突:

java -version which java

如果系统里已经带了OpenJDK,但版本和你后续项目需要的不一致,建议先记录下位置,后面配置环境变量时处理干净,不然两个Java互相抢优先级,启动Tomcat时用的JDK不是你想用的那个,排查起来相当折磨人。

1.2 用专用账号跑Tomcat,别用root凑合

这是我强烈建议你提前做的事。很多新手图省事,直接root解压、root启动Tomcat,短期没事,时间长了问题就来了。Tomcat跑在root下,万一Web应用有漏洞被上传了Webshell,攻击者拿到的就是root权限,整台服务器直接裸奔。正确的做法是创建专用运行账号:

useradd -M -s /sbin/nologin tomcat

-M表示不创建家目录,-s /sbin/nologin表示禁止这个账号登录Shell,很好体现了"最小权限"的思路。这个账号只用来启动Tomcat进程、读写必要目录,不登录不交互,即使应用被攻破,能造成的危害也小得多。这个习惯我在生产环境用了很久,建议从一开始就养成。

1.3 规划目录结构

目录规划不做好,后面升级、迁移、日志清理都难受。我在某服务器上见过把Tomcat解压到/tmp下的操作,重启一次机器整个环境人间蒸发,真是哭笑不得。这里给出一个实践下来比较顺手的结构:

/app/ # 应用总目录 /app/jdk # JDK安装目录 /app/tomcat # Tomcat安装目录 /data/tomcat # Tomcat数据目录,存放日志、webapps等 /data/logs # 独立日志目录

为什么要单独建数据目录?因为Tomcat自带的目录结构里,logs和webapps都在解压目录下,而解压目录属于程序目录,升级时要整体替换。如果把日志和部署包和程序混在一起,升级时要么把日志弄丢,要么为了保日志还得先复制来复制去,操作繁琐还容易出错。所以拆开是值得的,这也是我实践中吃过亏之后学乖的。

2. JDK安装:系统源不一定合适,手动解压更可控

CentOS自带的软件源里确实有OpenJDK,敲一条yum install -y java-1.8.0-openjdk就能装完。但这里面有几个坑:一是默认源里的JDK版本不一定跟你们项目要求一致,二是OpenJDK的安装路径比较分散,JAVA_HOME找起来费劲,后续配置容易出错。所以我个人更推荐手动下载JDK压缩包再解压,虽然多几步操作,但路径完全可控,升级也方便。

2.1 选JDK 8还是JDK 11、17

版本选择没有标准答案,主要看你们的项目跑在什么环境里。

  • 老旧项目、Spring Boot 2.x早期版本、大量使用JSP的老系统,JDK 8最稳。
  • 新项目、Spring Boot 2.5+、Spring Cloud 2021+,JDK 11、17都很常见,性能和生态都更现代。
  • 如果是做新系统从零起步,可以认真考虑JDK 17,毕竟它是LTS版本,支持周期长,而且近年主流的中间件、框架都已经全面兼容17。

具体的版本建议去官方页面或国内镜像站下载,需要下载tar.gz格式的压缩包,不要下载.rpm,因为我们这里选择解压方式安装。例如下载JDK 8的Linux x64版本时,选择类似jdk-8uXXX-linux-x64.tar.gz的文件。版本编号里的uXXX是更新版本号,数字越大表示补丁越新,建议选以该系列里较新的版本。

2.2 解压安装与目录规范

下载完上传到服务器刚才规划好的目录,然后解压:

mkdir -p /app/jdk tar -zxvf jdk-8uXXX-linux-x64.tar.gz -C /app/jdk cd /app/jdk mv jdk1.8.0_XXX jdk8

这里把解压出来的目录改名为jdk8,是为了去掉版本号,以后路径好记。如果想升级,只需要重新解压新版本,再改软链接指向新目录就行:

ln -s /app/jdk/jdk8 /app/jdk/default

后面环境变量就指向/app/jdk/default,升级时把软链接切换一下指向新版本目录,其他什么都不用动。这个思路在服务器多、环境多的时候特别省事。

3. JAVA_HOME环境变量配置:三个经典坑

JDK装完,环境变量是关键。配置不对,java -version不管用,Tomcat也不认,而且报错信息时常含糊,能让你排查半天。环境变量本来是想配给所有用户用的,实际上很容易只对root生效,普通用户拿不到,这是常见的第一个坑。

3.1 全局配置文件的正确写法

在/etc/profile.d/下新建一个专门文件,比如java.sh:

vi /etc/profile.d/java.sh

写入如下内容:

export JAVA_HOME=/app/jdk/default export PATH=$JAVA_HOME/bin:$PATH export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar

写完后:

chmod +x /etc/profile.d/java.sh source /etc/profile.d/java.sh

这里要解释一下为什么放在/etc/profile.d/而不是直接改/etc/profile。CentOS的登录Shell会遍历/etc/profile.d/下所有以.sh结尾的脚本并执行,把独立的配置拆成一个专属文件,比在/etc/profile里追加一大段更清晰,也不会影响其他人往这个文件里写内容。

CLASSPATH这行,现在很多教程其实已经不太写了,因为JDK 8以后某些场景下不设置也能正常工作。但如果你们的项目里有老代码还在用javac直接编译运行的,保留这个变量会省去一些麻烦,所以我习惯写上。

3.2 非登录Shell的环境变量坑

/etc/profile只对登录Shell生效。如果你用脚本通过ssh执行远程命令,或者某些自动化工具通过非交互方式调用Java,可能加载不到这个环境变量。解决办法是在~/.bashrc里也加一份:

echo 'export JAVA_HOME=/app/jdk/default' >> ~/.bashrc echo 'export PATH=$JAVA_HOME/bin:$PATH' >> ~/.bashrc

这里要提醒一句,如果Tomcat进程是通过systemd启动的,它不会读/etc/profile也不会读~/.bashrc,而是直接从service文件里读环境变量。所以稍后配置Tomcat自启时,我会在service文件里显式写上JAVA_HOME的路径,这就绕开了所有Shell环境问题。

3.3 验证环境变量的必做检查

配置完之后要做三次验证:

java -version which java echo $JAVA_HOME

三条结果都符合预期,才算配好。java -version显示你安装的版本,which java指向/app/jdk/default/bin/java,echo $JAVA_HOME打印正确的路径。这三条对应一个比一个仔细,任何一条异常都说明环境变量或者文件权限有问题。

如果which java显示的路径不是/app/jdk/default/bin/java,多半是PATH顺序有问题或者系统里有残留的旧Java,排查优先级很高。我在外面排查过不少机器,都是旧版OpenJDK占了/usr/bin/java,导致新装的JDK完全没生效。

4. Tomcat安装与目录规划:程序目录和部署内容要分离

JDK装好了,下面就到Tomcat。Tomcat的下载和解压比JDK还简单,但目录规划这步我希望你认真对待。Tomcat默认的目录结构和功能,我先拿一张表给你理清楚,再去讲怎么调整。

4.1 Tomcat目录结构与账目对照

Tomcat解压后默认目录:

目录/文件作用建议
bin启动、停止脚本保留在程序目录
conf核心配置:server.xml、web.xml等建议单独备份
libTomcat依赖的Jar包保留在程序目录
logs日志输出重定向到数据目录
temp临时文件系统自动管理
webappsWeb应用部署目录建议重定向或单独管理
workJSP编译后的临时目录系统自动管理

如果直接解压完就用,所有东西塞在一个目录里,用是能用,但当我第一次做版本升级时,因为webapps和logs都在程序目录里,差点把线上系统停了半天。所以我的做法是:程序目录只放Tomcat的二进制和配置模板,部署包和日志分开存放。

4.2 手动安装Tomcat的操作路径

mkdir -p /app/tomcat tar -zxvf apache-tomcat-9.0.XX.tar.gz -C /app/tomcat cd /app/tomcat mv apache-tomcat-9.0.XX tomcat9

然后建立软链接:

ln -s /app/tomcat/tomcat9 /app/tomcat/default

数据目录单独创建并调整权限:

mkdir -p /data/tomcat/{logs,webapps} chown -R tomcat:tomcat /data/tomcat

接下来修改Tomcat的配置文件,让它把日志写到数据目录。在conf/server.xml里找到<Host>标签下的<Context>,或者在conf/Catalina/localhost/下创建配置文件来重定向。一个简洁的思路是修改conf/catalina.properties里的java.io.tmpdir和日志相关路径参数,但最常见的做法还是直接改启动脚本里的CATALINA_OUT:

vi /app/tomcat/tomcat9/bin/catalina.sh

找到CATALINA_OUT的赋值,把输出指向:

export CATALINA_OUT=/data/tomcat/logs/catalina.out

注意这个文件只是定义了标准输出和错误输出的定向,Tomcat自己用Log4j等框架产生的日志,还需要在conf/logging.properties里调整路径。把logging.properties里的${catalina.base}/logs改成/data/tomcat/logs即可,或用sed批量替换。

4.3 Tomcat专属账号的权限分配

前面创建的tomcat账号,在这里正式派上用场:

chown -R tomcat:tomcat /app/tomcat chown -R tomcat:tomcat /data/tomcat

权限分配有个细节值得注意:Tomcat安装目录本身,虽然给了tomcat账号写权限,但生产环境为了安全,可以把conf目录改成只读,启动时Tomcat会读配置,不需要频繁写。真要改配置的时候,用root先编辑再chmod回只读,这套流程在高安全要求的服务器上很实用。

5. 启动Tomcat并验证:端口、防火墙、权限和日志,一处都不能漏

环境变量配好,权限也调好了,下面到了最让人兴奋也最容易卡壳的环节:启动。我试过第一次启动Tomcat时,端口一直不通,结果发现是防火墙;后来又遇到权限不足,日志里报Permission denied,路径查了半天才发现是logs目录的属主不对。所以我把这块容易出问题的点都整理出来,跟着顺序走能避坑。

5.1 环境检查和首次启动前要做的配置

启动前先确认这样几件事:

第一,JAVA_HOME是否在Tomcat的启动环境里可见。如果之前没有定义全局环境变量,或者用tomcat账号启动时加载不到,可以在Tomcat的bin/setenv.sh里显式指定,这是Tomcat官方支持的附加脚本,启动时如果存在会被自动加载:

mkdir -p /app/tomcat/tomcat9/bin cat > /app/tomcat/tomcat9/bin/setenv.sh << 'EOF' JAVA_HOME=/app/jdk/default CATALINA_HOME=/app/tomcat/tomcat9 CATALINA_BASE=/app/tomcat/tomcat9 CATALINA_OUT=/data/tomcat/logs/catalina.out JAVA_OPTS="-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m" EOF

JAVA_OPTS这里给的是一个基础模板,Xms是初始堆内存,Xmx是最大堆内存,Metaspace是JDK 8以后存放类元数据的空间。具体数值要根据机器的物理内存和应用负载来调整,一般来说初始堆和最大堆建议设为一样,避免运行中动态扩容带来的性能抖动。

第二,检查端口是否被占用:

ss -lntp | grep 8080

如果8080被占用,需要修改conf/server.xml里的<Connector port="8080">改成其他端口,或者先查清是什么进程占用的,再做处理。

第三,确认防火墙放行。CentOS 7和8默认可能开着firewalld,可以用以下命令放行Tomcat端口:

firewall-cmd --zone=public --add-port=8080/tcp --permanent firewall-cmd --reload

如果用的是云服务器,除了系统防火墙,云平台的安全组规则也要一并检查。这部分经常被忽略,系统防火墙明明放行了,端口还是不通,我遇到过好几次,最后都是云平台安全组的锅。

5.2 首次启动的完整验证流程

启动脚本在bin目录下,启动前先切到tomcat账号:

su -s /bin/bash tomcat -c "/app/tomcat/tomcat9/bin/startup.sh"

或者用脚本加上daemon方式启动。如果 startup.sh 没有执行权限,先chmod +x补上。

启动后,马上验证进程和端口:

ps -ef | grep tomcat ss -lntp | grep 8080

如果进程在但端口没起,基本是配置或权限问题。接着看日志:

tail -f /data/tomcat/logs/catalina.out tail -f /app/tomcat/tomcat9/logs/localhost.log

日志里常见的报错,我列一张表方便对照:

报错关键词原因处理办法
Permission denied数据目录或临时目录没有tomcat账号的写权限检查并修正属主和权限
Port 8080 already in use端口被其他进程占用改Tomcat端口,或停掉占用进程
JAVA_HOME is not defined correctlyTomcat找不到JDK路径检查setenv.sh和JAVA_HOME配置
Directory ... does not exist指定的日志或部署目录不存在提前创建并设置权限
ZIP file too large / corruptwar包损坏重新上传并校验MD5

这些坑我早期全都踩过一遍,最有迷惑性的是JAVA_HOME is not defined correctly。明明在命令行里echo $JAVA_HOME是正确的,但Tomcat启动脚本就是报错。原因就是Tomcat的启动脚本是独立进程,加载不到你当前Shell的环境变量,或者在catalina.sh脚本开头解析路径时,因为路径里有软链接而解析失败。解决办法就是在setenv.sh里写死路径,不要依赖全局环境变量。

5.3 通过访问验证Web环境是否真正可用

Tomcat启动后,在浏览器里访问http://服务器IP:8080,能看到Tomcat默认首页,说明基础环境OK。接着部署一个最简单的测试应用来验证JSP和Servlet容器都正常工作,比如在webapps下放一个test/目录,里面写个index.jsp,内容就一行:

<%= new java.util.Date() %>

访问时能看到动态生成的日期,说明JSP引擎在跑,JDK和Tomcat的配合没有大问题。这一步别看简单,它能帮你在投入真实业务前快速排除一批环境层级的问题,强烈建议不要省略。

5.4 一个容易误导的启动错误:重复执行startup.sh

这个坑我要单独拿出来讲。Tomcat启动时如果startup.sh被执行了两次,并不会报错,因为Tomcat会检测到已有实例,第二次执行可能不会把新进程拉起来,但也没有明显的报错输出。很多人这时候就以为启动不了,开始四处排查配置,结果一看进程,原来第一个实例早就跑起来了。所以启动时先看进程,再判断端口,不要一看到脚本执行完没输出就慌了。

实际上在部分Tomcat版本里,重复执行startup.sh还会出现"端口被占用导致启动失败"但不影响原实例的情况,容易让人以为之前启动了东西把端口占了,其实占端口的正好是上一次启动的Tomcat自己。

6. 让环境长期稳定运行:开机自启、JVM参数与安全加固

手动启动的Tomcat,重启机器后就没了,而且JVM内存不调,并发一上来就频繁Full GC,接口一个接一个超时;不对应用做最小权限加固,一个上传功能的漏洞就能让整个服务器被攻破。这些等到出事再处理就晚了,所以建议装完环境之后,趁热把这些事情一并做完。

6.1 配置systemd开机自启

在生产上,Tomcat不应该用startup.sh裸奔,而是交给systemd托管,这样开机自动启动,异常退出还能自动拉起。写一个service文件:

vi /etc/systemd/system/tomcat.service

内容大致如下:

[Unit] Description=Apache Tomcat Web Application Container After=network.target [Service] Type=forking User=tomcat Group=tomcat Environment=JAVA_HOME=/app/jdk/default Environment=CATALINA_HOME=/app/tomcat/tomcat9 Environment=CATALINA_BASE=/app/tomcat/tomcat9 Environment=CATALINA_OUT=/data/tomcat/logs/catalina.out ExecStart=/app/tomcat/tomcat9/bin/startup.sh ExecStop=/app/tomcat/tomcat9/bin/shutdown.sh Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target

写完后执行:

systemctl daemon-reload systemctl enable tomcat systemctl start tomcat systemctl status tomcat

我在这里特别把JAVA_HOME、CATALINA_HOME等环境变量写进了service文件,原因前面提到过:systemd启动的进程不会读Shell的profile文件,如果不在这里写,启动脚本会因找不到JAVA_HOME而失败。另外Type=forking是因为Tomcat的startup.sh脚本启动后会返回,但实际Tomcat进程会在后台持续运行,这个类型就是为这类启动方式设计的。

6.2 JVM参数的基本调优

机器是多少内存,JVM的堆就应该给多少,没有固定公式,但有几个基本思路。比如一台4G内存的服务器,分配给Tomcat的堆内存建议不超过2G,要给系统本身和后续可能的数据库、缓存留余地。基础配置可以改成:

JAVA_OPTS="-server -Xms1g -Xmx1g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -XX:+UseG1GC"

-server选择server模式,-Xms和-Xmx一致,避免动态扩容。如果机器是2G内存,那-Xms、-Xmx给512m比较稳妥。设置不宜过大,否则系统可用内存被掏空,操作系统频繁使用交换分区,性能更差。

再补一个实用的JVM监控参数,方便出问题时从日志里定位:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/tomcat_heapdump.hprof

这样JVM内存溢出时能留下堆内存快照,后续用分析工具排查确实好使。这个参数我在生产事故排查中帮了不少忙,属于花很小成本换很大便利的配置。

6.3 常见安全问题与基本加固

装完环境顺手加固一遍,比自己出事后慌慌张张补漏洞要强太多。这里的加固集中在几个方面,都是实操中验证有效的做法:

一是管理端口的访问限制。Tomcat的管理后台(manager、host-manager)默认是绑在本地的,不向外提供服务,通过修改webapps/manager/META-INF/context.xml里的allow字段来控制访问来源。如果业务不需要管理后台,最简单的是直接把manager和host-manager这两个目录从webapps下移走或改名,不给攻击者留下可趁之机。

二是去掉默认的示例应用。Tomcat自带的examples、docs、ROOT这些默认应用,如果没有实际用途,建议一并清理,减少暴露面。

三是隐藏版本号。Tomcat默认会在错误页面和响应头里暴露版本号,攻击者可以根据版本号查找已知漏洞。修改conf/server.xml里的<Connector>,添加:

server="Apache Tomcat/9"

这样响应头里的Server字段就不会把精确版本号泄露出去了。

四是不要用admin这种默认用户名。配置Tomcat用户时,密码强度和用户名复杂度都要注意,如果不需要用户认证功能,可以完全不配tomcat-users.xml。

6.4 日志轮转与磁盘空间

长期运行的服务器上,Tomcat日志增长快得超出想象。catalina.out如果不做轮转,会在几个月内挤爆磁盘。最简单的方式是用系统自带的logrotate:

vi /etc/logrotate.d/tomcat

内容示例:

/data/tomcat/logs/catalina.out { daily rotate 7 compress missingok copytruncate }

daily表示每天轮转一次,rotate 7保存7份,compress压缩旧日志,copytruncate在Tomcat不重开文件描述符的情况下也能正常轮转。这一点很关键,因为Tomcat的catalina.out是长时间保持打开的文件,直接用mv改名再建新文件,Tomcat还会继续往原文件描述符里写,日志会写到已改名的文件里,磁盘空间照样涨。copytruncate先复制再清空,避开了这个问题。

我处理过一台线上机器,就是没配日志轮转,catalina.out涨到十几个G,把根分区挤到100%,数据库都连不上了。配上logrotate之后,这类风险就基本解除了。

实测下来,把开机自启、JVM参数和日志轮转这几件事做掉,Tomcat的环境算是真正站稳了,之后日常要操心的就只有业务本身,而不用反复折腾底层环境了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进