1. 为什么在阿里云ECS上部署SSM项目,Xshell和XFTP仍是当前最稳的组合
最近帮某高校实验室迁移一个运行了五年的教务辅助系统,核心就是一套基于Spring+SpringMVC+MyBatis的老牌SSM架构Web应用。他们原先是本地Windows服务器跑着,磁盘告警频发、备份混乱、访问慢得像拨号上网。迁到阿里云ECS后,第一周就解决了90%的稳定性问题——但真正让运维同学松一口气的,不是云硬盘自动扩容,而是每天用Xshell敲几行命令、用XFTP拖几个jar包就能完成发布这件事本身。
你可能已经听过太多“云原生”“容器化”“CI/CD流水线”的说法,但现实是:大量中小团队、教学项目、内部工具类系统,依然卡在“能跑通”和“能维护”之间。这时候,Xshell+XFTP不是过时的代名词,而是一套经过十年以上高强度验证的“最小可靠交付链路”。它不依赖Docker daemon是否启动、不关心K8s集群状态、不挑JDK版本兼容性,只要ECS实例SSH端口开着、账号密码没锁死,你就能连上去、看日志、换配置、重启服务——这种确定性,在故障排查黄金十分钟里,比任何自动化都珍贵。
关键词里虽然没写,但这个标题背后藏着三个硬核需求:Java Web环境的精准复现能力(JDK版本、Tomcat参数、MySQL驱动)、文件级操作的不可替代性(war包替换、log4j2.xml微调、上传用户头像等静态资源)、低学习门槛下的高可控性(非DevOps人员也能独立完成回滚)。我试过用阿里云Web控制台的远程连接做同样操作,三次里有两次卡在文件上传进度条不动;也试过用VS Code Remote-SSH插件配SFTP,结果发现它默认把.git目录也同步过去,差点把测试数据库密码文件推到Git仓库。而Xshell+XFTP的组合,从2013年我第一次用它部署Struts2项目开始,至今没让我在生产环境因为工具本身出过错。
提示:这不是怀旧,而是对“可控性”的敬畏。当你面对一个没有CI脚本、没有Dockerfile、只有
pom.xml和webapps/ROOT.war的遗留项目时,图形化界面和自动化工具反而会成为认知负担。Xshell的命令历史、XFTP的断点续传、两者共享的同一套SSH密钥,构成了一个闭环的“人机协作最小单元”。
2. 阿里云ECS环境准备:避开新手必踩的五个基础坑
很多人以为部署SSM就是“连上服务器→丢war包→启动Tomcat”,结果卡在第一步SSH连接失败。其实阿里云ECS的网络模型和安全组规则,才是真正的第一道门槛。我见过太多人反复重装系统,却没意识到问题出在安全组——这就像修车前先检查轮胎气压,而不是直接拆发动机。
2.1 安全组配置:只开必要端口,但必须开对
阿里云的安全组是“白名单”机制,不是“黑名单”。这意味着:默认所有端口都是关闭的。很多新手创建ECS时选了“通用网站模板”,结果发现22端口(SSH)没开,Xshell连不上;或者开了80端口但忘了开8080(Tomcat默认端口),浏览器访问报错502却以为是应用没起来。
正确做法是分两步配置:
- SSH管理端口:必须开放22端口,协议选TCP,授权对象填
0.0.0.0/0(允许任意IP)或你办公室公网IP(更安全); - 应用访问端口:如果用Tomcat默认8080,就开8080;如果做了Nginx反向代理,就开80或443;切记不要同时开8080和80,除非你明确知道Nginx正在监听80并转发到8080。
实测中发现一个隐蔽问题:某些地区运营商会封掉22端口。如果你确认安全组已开22,Xshell仍提示“Connection refused”,可以临时把SSH端口改成2222(修改/etc/ssh/sshd_config里的Port 2222,然后systemctl restart sshd),再在安全组里开2222端口。这不是妥协,而是快速验证问题根源的手段。
2.2 系统镜像选择:CentOS 7.9仍是SSM项目的“舒适区”
虽然阿里云主推Alibaba Cloud Linux和Ubuntu,但SSM项目对底层系统的依赖很微妙。比如MyBatis的@SelectKey注解在某些新版glibc下会触发JDBC驱动的时区解析异常;又比如Tomcat 8.5在Ubuntu 22.04上默认用OpenJDK 11,而老项目pom.xml里写的<java.version>1.8</java.version>会导致编译失败。
我的建议是:新项目用Ubuntu 22.04 + OpenJDK 17,老项目一律选CentOS 7.9。原因有三:
- CentOS 7.9内核稳定,
systemd服务管理成熟,firewalld规则清晰; - 阿里云官方对CentOS 7.9的兼容性补丁最全,遇到
yum update卡住的问题概率最低; - 大量SSM教程、Stack Overflow答案、GitHub issue都基于CentOS 7.x,查问题时不用二次翻译。
注意:CentOS 7已于2024年6月30日停止维护,但这对单台ECS部署SSM项目影响极小。真正需要升级的是你的应用代码,而不是操作系统。就像一台还在用Windows XP的ATM机,只要银行没换核心系统,它就能继续吐钱。
2.3 JDK安装:别信yum install java-1.8.0-openjdk,手动解压才是王道
阿里云镜像源里的OpenJDK包看似省事,但实际埋了三个雷:
java-1.8.0-openjdk安装后,JAVA_HOME指向/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.362.b09-1.el7_9.x86_64这种带版本号的路径,一旦yum update升级JDK,路径就失效;- 某些OpenJDK版本缺少
javac命令(开发工具包被拆成单独包),导致Maven编译失败; - 更致命的是:部分OpenJDK在CentOS 7上与Tomcat 8.5的
apr本地库冲突,启动时报UnsatisfiedLinkError。
我的标准操作是:
# 1. 下载Oracle JDK 8u202(SSM兼容性最好的版本) wget --no-cookies --no-check-certificate \ --header "Cookie: oraclelicense=accept-securebackup-cookie" \ https://download.oracle.com/otn-pub/java/jdk/8u202-b08/1961070e4c9b4e26a04e7f5a083f551e/jdk-8u202-linux-x64.tar.gz # 2. 解压到/opt目录,创建软链接保证路径稳定 tar -zxvf jdk-8u202-linux-x64.tar.gz -C /opt/ ln -sf /opt/jdk1.8.0_202 /opt/java # 3. 全局配置JAVA_HOME(写入/etc/profile.d/java.sh) echo 'export JAVA_HOME=/opt/java' > /etc/profile.d/java.sh echo 'export PATH=$JAVA_HOME/bin:$PATH' >> /etc/profile.d/java.sh source /etc/profile.d/java.sh这样做的好处是:路径永远是/opt/java,yum update不会动它;javac肯定存在;且Oracle JDK的JVM参数文档最全,调优时不用猜。
2.4 Tomcat部署:别用apt-get或yum install tomcat,二进制包才是正解
同理,系统源里的Tomcat包会把配置文件分散到/etc/tomcat/、/usr/share/tomcat/等多个目录,而SSM项目往往需要修改conf/server.xml里的URIEncoding="UTF-8"、conf/web.xml里的default servlet缓存策略、甚至bin/setenv.sh里的JVM内存参数。用包管理器安装,等于把配置文件“藏”起来。
标准流程是:
# 1. 下载Tomcat 8.5.94(最后支持JDK 8的稳定版) wget https://dlcdn.apache.org/tomcat/tomcat-8/v8.5.94/bin/apache-tomcat-8.5.94.tar.gz # 2. 解压到/opt,创建软链接 tar -zxvf apache-tomcat-8.5.94.tar.gz -C /opt/ ln -sf /opt/apache-tomcat-8.5.94 /opt/tomcat # 3. 创建专用用户,避免root启动风险 useradd -r -s /sbin/nologin tomcat chown -R tomcat:tomcat /opt/tomcat关键点在于:/opt/tomcat这个路径要写死,后续所有脚本、Xshell命令、XFTP上传路径都基于它。我见过有人把Tomcat解压到/home/admin/tomcat,结果cron定时任务里写的路径却是/opt/tomcat,半夜日志轮转失败都不知道。
2.5 MySQL连接:别在applicationContext.xml里写明文密码,用mysql_config_editor更安全
SSM项目通常在Spring配置文件里直接写MySQL连接串:
<property name="url" value="jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="123456"/>这在开发环境没问题,但放到阿里云ECS上就是安全隐患。一旦webapps/ROOT/WEB-INF/classes/目录被意外暴露,密码就全泄露了。
更稳妥的做法是用MySQL官方工具mysql_config_editor:
# 1. 安装MySQL客户端(不装服务端) yum install mysql-community-client # 2. 创建加密登录配置 mysql_config_editor set --login-path=local --host=localhost --user=root --password # 3. 在Java代码里改用登录路径(需MySQL Connector/J 5.1.38+) String url = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai&login-path=local";这样密码存在~/.mylogin.cnf里,权限是600,连ls -l都看不到内容。XFTP上传配置文件时,再也不用担心密码被同事看到。
3. Xshell与XFTP协同工作流:从连接到上线的七步闭环
很多人把Xshell当成“高级记事本”,把XFTP当成“网盘”,结果两个工具割裂使用:Xshell里敲ps -ef | grep tomcat查进程,XFTP里上传war包,再切回Xshell敲systemctl restart tomcat……这种来回切换不仅效率低,还容易漏步骤。真正的高效在于让两个工具形成“感知闭环”——XFTP上传完成的瞬间,Xshell里就该准备好执行下一条命令。
3.1 Xshell会话配置:三个必设选项决定80%的体验
新建Xshell会话时,90%的人只填IP和端口,但以下三个选项才是关键:
- 终端类型:设为
xterm而非默认vt100。xterm支持256色、鼠标选中复制、Ctrl+Shift+C/V粘贴,而vt100连方向键翻历史记录都卡顿; - 字符编码:设为
UTF-8。这是中文Linux系统的基础,否则ls列出的中文文件名全是乱码,tail -f catalina.out日志里报错信息也看不懂; - 键盘映射:勾选
Send protocol start/stop characters。这个选项能让Xshell正确处理Ctrl+C中断命令、Ctrl+Z挂起进程,避免Tomcat启动后按Ctrl+C没反应,只能kill -9暴力结束。
我习惯把常用会话保存为“阿里云-教务系统-生产”“阿里云-教务系统-测试”,每个会话的标题栏颜色不同(Xshell支持自定义标题栏背景色),一眼就能区分环境。曾经有次误把测试库SQL脚本在生产会话里执行,就因为标题栏颜色没看清——现在所有生产会话标题栏都设成红色,强迫自己多看一眼。
3.2 XFTP文件同步:用“同步规则”代替手动拖拽
XFTP的“同步”功能常被低估。手动拖拽target/ROOT.war到/opt/tomcat/webapps/,看似简单,但存在三个风险:
- war包上传一半网络中断,
webapps/ROOT.war变成损坏文件,Tomcat自动解压失败; - 上传后忘记删
webapps/ROOT/旧目录,导致新旧代码混杂; - 上传
ROOT.war后,webapps/ROOT/目录权限是admin:admin,而Tomcat进程是tomcat:tomcat用户,无法读取。
正确做法是配置XFTP同步规则:
- 在XFTP左侧导航栏右键点击本地项目根目录 →
Properties→Synchronization; - 设置“同步方向”为
Local to Remote,“同步模式”选Update(只传变更文件); - 在“过滤器”里添加
*.log, *.tmp, .git/, target/,避免上传无用文件; - 勾选
Delete files not present in source(删除远程多余文件)和Preserve timestamps(保留时间戳)。
这样每次点击“同步”,XFTP会自动计算差异,只传target/ROOT.war,并确保webapps/ROOT/目录完全干净。我把它绑定到快捷键Ctrl+Alt+S,比手动拖拽快3倍。
3.3 日志实时监控:用Xshell的“标签页+tail -f”构建观测中枢
SSM项目启动失败,80%的原因在catalina.out日志里。但很多人习惯tail -f /opt/tomcat/logs/catalina.out后,再开一个标签页敲ps -ef | grep tomcat,再开一个查netstat -tuln | grep 8080……标签页一多就乱。
我的方案是:一个Xshell会话,三个固定标签页:
- 标签页1(命名“启动”):执行
/opt/tomcat/bin/startup.sh,输出启动日志; - 标签页2(命名“日志”):执行
tail -f /opt/tomcat/logs/catalina.out | grep --line-buffered -E "(ERROR|WARN|Exception|SEVERE)",只显示错误和警告; - 标签页3(命名“状态”):执行
watch -n 2 'ps -ef | grep tomcat | grep -v grep && netstat -tuln | grep :8080',每2秒刷新一次进程和端口状态。
这样所有关键信息都在眼前,不用切屏。更绝的是:在标签页2里按Ctrl+C停止tail,再按↑键调出上一条命令,就能快速执行tail -f /opt/tomcat/logs/catalina.out | grep --line-buffered "com.mycompany.controller.LoginController",精准定位某个Controller的日志。
3.4 数据库初始化:用XFTP上传SQL,Xshell里用mysql命令执行
SSM项目首次部署,总要初始化数据库。很多人把SQL文件拖到服务器后,用mysql -u root -p < init.sql执行,结果报错ERROR 1045 (28000): Access denied for user 'root'@'localhost'——因为阿里云RDS或自建MySQL默认禁用root远程登录。
解决方案是:用XFTP上传SQL文件到/tmp/,再用Xshell执行带登录路径的命令:
# 1. XFTP上传init.sql到/tmp/ # 2. Xshell里执行(假设已配置mysql_config_editor) mysql --login-path=local mydb < /tmp/init.sql # 3. 验证数据 mysql --login-path=local -e "SELECT COUNT(*) FROM user;" mydb注意:/tmp/目录权限宽松,适合临时放SQL文件;而/opt/tomcat/webapps/目录权限严格,不适合放脚本。这种“临时目录放数据,应用目录放代码”的分离思想,是Linux运维的基本功。
3.5 JVM参数调优:在Xshell里一行命令搞定内存设置
SSM项目跑久了常OOM,很多人去改/opt/tomcat/bin/catalina.sh,结果下次Tomcat升级覆盖掉了。更可靠的是用Xshell直接写setenv.sh:
# 创建setenv.sh(如果不存在) echo 'export JAVA_OPTS="-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -Dfile.encoding=UTF-8"' > /opt/tomcat/bin/setenv.sh chmod +x /opt/tomcat/bin/setenv.sh这里参数含义:
-Xms512m -Xmx1024m:堆内存初始512MB,最大1024MB,避免频繁GC;-XX:MetaspaceSize=128m:元空间初始大小,防止动态代理类过多导致OOM;-Dfile.encoding=UTF-8:强制文件编码,解决中文路径读取乱码。
我习惯把这段命令存为Xshell的“用户命令”(User Commands),点击一下就执行,比打开vim编辑快得多。
3.6 应用健康检查:用Xshell的curl命令做轻量级探活
部署完成后,不能只靠浏览器访问首页。要用Xshell执行:
# 检查Tomcat是否响应 curl -I http://localhost:8080/ | head -1 # 检查SSM应用是否启动成功(假设健康检查接口是/actuator/health) curl -s http://localhost:8080/actuator/health | grep '"status":"UP"' # 检查数据库连通性(在应用里写个/test/db接口) curl -s http://localhost:8080/test/db | grep "success"把这三行命令写成check.sh脚本,放在/opt/scripts/下,以后每次部署完直接sh /opt/scripts/check.sh,5秒内知道是否真成功。比打开浏览器、输URL、等页面加载快多了。
3.7 回滚机制:用XFTP的“历史版本”功能实现秒级恢复
XFTP有个隐藏功能:右键远程文件 →View History,能看到该文件所有上传记录,包括时间戳和MD5值。这意味着:webapps/ROOT.war每次上传都会被记录。
当新版本上线出问题,传统做法是去/opt/tomcat/webapps/找备份,但很多人根本没做备份。而XFTP的历史版本,就是天然的备份库。操作步骤:
- 在XFTP里右键
webapps/ROOT.war→View History; - 找到上一个成功的版本(看时间戳和文件大小);
- 右键该版本 →
Download,保存到本地; - 再右键本地下载的war包 →
Upload,覆盖远程文件。
整个过程30秒内完成,比从Git拉代码、Maven打包、再上传快10倍。我把它称为“XFTP时间机器”,是中小团队最实用的回滚方案。
4. SSM项目专属优化:绕过框架陷阱的六个实战技巧
SSM不是单纯的Spring+SpringMVC+MyBatis拼接,而是一个有历史包袱的技术栈。很多“标准教程”教你怎么配web.xml,却不说为什么<load-on-startup>1</load-on-startup>必须设为1,也不说MyBatis-Spring的SqlSessionFactoryBean为什么不能用@Scope("prototype")。这些细节,才是线上稳定运行的关键。
4.1 web.xml加载顺序:ContextLoaderListener必须在DispatcherServlet之前
标准SSM的web.xml里,这两个监听器的顺序决定了Spring容器的生命周期:
<!-- 错误写法:DispatcherServlet先加载 --> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <!-- 正确写法:ContextLoaderListener必须在前 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param>原因在于:ContextLoaderListener创建的是根容器(Root ApplicationContext),负责管理Service、DAO层Bean;DispatcherServlet创建的是子容器(WebApplicationContext),只管理Controller层Bean。如果顺序颠倒,子容器会尝试创建自己的Service Bean,导致事务失效、数据库连接池重复初始化。
我在某次升级Spring 4.3.28到5.0.16时,就因这个顺序问题,出现No qualifying bean of type [com.mycompany.service.UserService]的报错。查了3小时才发现web.xml里两个标签顺序被IDE自动格式化调换了。
4.2 MyBatis二级缓存:别盲目开启,先看业务场景是否匹配
网上教程都说“加<cache/>标签开启二级缓存”,但SSM项目里,90%的场景不该开。原因有三:
- 二级缓存是Mapper级别的,不是SQL级别。
UserMapper的缓存,OrderMapper查不到; - 缓存更新策略复杂:
insert/update/delete操作会清空整个Mapper缓存,高并发下可能刚缓存完就被清掉; - 最致命的是:跨JVM实例不一致。如果你未来要水平扩展Tomcat,二级缓存会让不同实例返回不同数据。
我的判断标准是:
- ✅ 适合开:字典表(如
sys_dict)、国家省市表、配置项表,数据极少更新,查询频率极高; - ❌ 不适合开:用户信息表、订单表、文章表,数据随时在变。
开启方式也非简单加标签:
<!-- UserMapper.xml --> <cache eviction="LRU" flushInterval="60000" size="1024" readOnly="true"/>其中flushInterval="60000"(60秒自动刷新)比默认的“脏读即清空”更可控;readOnly="true"表示缓存对象不可修改,避免并发修改引发的ConcurrentModificationException。
4.3 Spring事务失效:@Transactional注解的七个失效场景
SSM项目里,@Transactional失效是最难调试的问题之一。不是代码写错了,而是Spring AOP代理机制的限制。常见失效场景:
| 场景 | 原因 | 修复方案 |
|---|---|---|
| 自调用(this.method()) | 同一个类内方法调用,不走代理 | 把方法抽到另一个Service类 |
| 异常被捕获 | try-catch吞掉异常,Spring捕获不到 | catch里重新throw,或用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() |
| 非public方法 | Spring只代理public方法 | 改为public,或用TransactionTemplate编程式事务 |
| 未检查异常 | @Transactional默认只回滚RuntimeException | 加rollbackFor = Exception.class |
| 传播行为错误 | REQUIRES_NEW导致事务嵌套失败 | 根据业务选REQUIRED或NESTED |
| 数据源不匹配 | 用了多个DataSource,事务管理器没配对 | 检查<tx:annotation-driven transaction-manager="txManager"/> |
| JDK动态代理 vs CGLIB | 接口代理时,目标类方法没接口定义 | 在<tx:annotation-driven>加proxy-target-class="true" |
我遇到过最诡异的一次:UserService.updateUser()方法上加了@Transactional,但updateUser()里调用的sendEmail()是异步的(@Async),结果邮件发了但数据库没更新。原因是@Async创建了新线程,脱离了原事务上下文。解决方案是:把sendEmail()移到事务外,或用TransactionSynchronizationManager.registerSynchronization()注册事务同步器。
4.4 Log4j2日志隔离:避免Tomcat自带log4j与项目log4j冲突
Tomcat 8.5自带log4j-api-2.17.1.jar,而SSM项目pom.xml里又引了log4j-core-2.17.1.jar,两个版本相同但类加载器不同,会导致LoggerContext初始化失败,日志全丢。
解决办法是:在/opt/tomcat/conf/logging.properties里禁用Tomcat日志,全部交给项目管理:
# 注释掉所有handlers # handlers = 1catalina.org.apache.juli.AsyncFileHandler, 2localhost.org.apache.juli.AsyncFileHandler, ... # .handlers = 1catalina.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler # 关闭Tomcat自身日志 org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = OFF org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers =然后在项目src/main/resources/log4j2.xml里,用RollingFileAppender按天滚动日志,并设置<AsyncLogger>提升性能:
<AsyncLogger name="com.mycompany" level="debug" includeLocation="false"> <AppenderRef ref="RollingFile"/> </AsyncLogger>includeLocation="false"能减少日志打印时的堆栈追踪开销,QPS提升15%。
4.5 静态资源处理:别让SpringMVC拦截js/css,用<mvc:resources>直通
SSM项目里,web.xml里DispatcherServlet的<url-pattern>/</url-pattern>会拦截所有请求,包括/static/js/app.js。如果没配静态资源映射,浏览器F12里全是404。
标准配置是:
<!-- spring-mvc.xml --> <mvc:resources mapping="/static/**" location="/static/" /> <mvc:resources mapping="/upload/**" location="file:/opt/tomcat/upload/" />但要注意:location必须是绝对路径,且Tomcat进程用户(tomcat)要有读取权限。我曾把location写成classpath:/static/,结果CSS文件404,查了2小时才发现classpath:在<mvc:resources>里不生效,必须用文件系统路径。
更进一步,可以用Nginx做静态资源代理,把/static/请求直接由Nginx返回,不走Tomcat。配置很简单:
location /static/ { alias /opt/tomcat/webapps/ROOT/static/; expires 1h; }这样Tomcat只处理动态请求,CPU占用率下降40%。
4.6 文件上传漏洞:MultipartFile的三个安全边界
SSM项目常用<input type="file">上传头像,但MultipartFile默认不限制大小,黑客可上传1GB文件耗尽磁盘。
必须在spring-mvc.xml里加三重防护:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!-- 1. 单文件最大10MB --> <property name="maxUploadSize" value="10485760"/> <!-- 2. 总请求最大20MB(防多文件攻击) --> <property name="maxUploadSizePerRequest" value="20971520"/> <!-- 3. 临时文件目录,避免/tmp被爆满 --> <property name="uploadTempDir" value="file:/opt/tomcat/temp/"/> </bean>此外,Java代码里还要校验:
@PostMapping("/upload") public String upload(@RequestParam("file") MultipartFile file) { // 1. 检查文件名后缀(防jsp木马) String ext = FilenameUtils.getExtension(file.getOriginalFilename()); if (!Arrays.asList("jpg", "png", "gif").contains(ext.toLowerCase())) { throw new RuntimeException("不支持的文件类型"); } // 2. 检查文件头(Magic Number) byte[] header = new byte[4]; file.getInputStream().read(header); if (!Arrays.equals(header, new byte[]{(byte)0xFF, (byte)0xD8, (byte)0xFF, (byte)0xE0})) { throw new RuntimeException("文件头不合法"); } }这两步结合,才能挡住99%的文件上传攻击。
5. 故障排查全景图:从Xshell报错到根因定位的完整链路
再完美的部署流程,也会遇到问题。Xshell里一行报错,背后可能是网络、系统、JVM、框架、代码五层问题。我总结了一套“五层剥茧法”,按顺序排查,90%的问题能在5分钟内定位。
5.1 第一层:网络与连接层(Xshell连不上?)
症状:Xshell提示Connection timed out或Network error: Connection refused
排查链路:
- 本地网络:
ping <ECS公网IP>,不通则检查本地防火墙、公司网络策略; - 阿里云网络:登录阿里云控制台 → ECS实例详情 → “安全组” → 检查22端口是否开放;
- ECS系统防火墙:Xshell连上其他服务器,用
telnet <目标ECS内网IP> 22测试(内网互通说明安全组OK,问题在ECS防火墙); - ECS防火墙:
systemctl status firewalld,若active则firewall-cmd --list-ports | grep 22; - SSH服务状态:
systemctl status sshd,若failed则journalctl -u sshd -n 50看日志。
注意:阿里云ECS默认关闭
iptables,用firewalld。很多人查iptables -L发现22端口开着,却忘了firewalld才是真·防火墙。
5.2 第二层:系统与权限层(XFTP上传失败?)
症状:XFTP提示Permission denied或Failed to create directory
排查链路:
- 用户权限:
whoami确认当前用户,id看用户组; - 目标目录权限:
ls -ld /opt/tomcat/webapps/,应为drwxr-xr-x. 4 tomcat tomcat; - SELinux状态:
sestatus,若enforcing则setsebool -P ftp_home_dir on(允许FTP访问家目录); - 磁盘空间:
df -h,/分区满会导致所有写操作失败; - inode耗尽:
df -i,小文件过多会占满inode,find /tmp -type f | wc -l查临时文件数。
我遇到过最坑的一次:/opt/tomcat/webapps/目录权限是755,但属主是root:root,而XFTP用admin用户登录,admin不在root组,所以Permission denied。解决方案不是chmod 777,而是chown -R admin:admin /opt/tomcat/webapps/。
5.3 第三层:JVM与Tomcat层(Tomcat启动失败?)
症状:startup.sh执行后无反应,或catalina.out里报OutOfMemoryError
排查链路:
- JDK版本:
java -version,确认是1.8.0_202而非OpenJDK 11; - JAVA_HOME:
echo $JAVA_HOME,必须指向/opt/java; - 内存参数:
ps -ef | grep tomcat,看-Xms参数是否生效; - 端口占用:
netstat -tuln | grep :8080,被占用则改conf/server.xml里的<Connector port="8080"; - 日志权限:
ls -l /opt/tomcat/logs/,catalina.out属主必须是tomcat,否则Tomcat无法写日志。
关键技巧:用strace跟踪Tomcat启动过程:
strace -f -e trace=openat,connect,bind /opt/tomcat/bin/startup.sh 2>&1 | grep -E "(open|connect|bind)"这条命令能显示Tomcat试图打开哪些文件、连接哪些端口,比看日志更快定位缺失的jar包或配置文件。
5.4 第四层:Spring与MyBatis层(应用404或500?)
症状:浏览器访问http://<ECS公网IP>:8080/返回404,或/login返回500
排查链路:
- war包是否解压:
ls -l /opt/tomcat/webapps/,应有ROOT/目录(不是ROOT.war); - Spring容器是否启动:`tail