
接手任何一个Java Web项目第一件事基本就是打开Tomcat的conf/server.xml扫一眼。这文件藏着一台服务器的所有家底端口、线程池、虚拟主机、连接器配置、部署路径全在这一张XML里。我见过不少项目出问题最后追根溯源都落在server.xml某个看似不起眼的属性上。这篇文章我把这些年调过的、踩过的配置点全部拉出来捋一遍尽量做到每一条都能直接对着文档实操不绕弯子。1. 先搞懂server.xml在Tomcat里到底扮演什么角色1.1 server.xml管的是“容器骨架”不是业务代码Tomcat启动的时候bin/catalina.shWindows下是catalina.bat会调用org.apache.catalina.startup.Bootstrap它负责初始化整个容器。初始化的核心动作之一就是读取conf/server.xml然后按照这个文件定义的层级结构在内存里把整个Tomcat实例构建出来。换句话说server.xml定义的是Tomcat的“骨架”业务代码部署进去之后跑在上面的War包和class文件都寄居在它声明的容器节点里。这套骨架模型是这样的Server └── Service ├── Connector (HTTP/1.1) ├── Connector (AJP/1.3) └── Engine ├── Host │ ├── Context │ ├── Context │ └── ... ├── Host └── ...理解这棵树是关键。Server是根一个Tomcat实例只有一个ServerServer里面可以有多个Service每个Service是“连接器 引擎”的组合每个Service里可以配多个Connector但只能有一个EngineEngine下面挂着若干个Host每个Host代表一个虚拟主机Host下面又挂Context一个Context就是一个Web应用。很多刚接触的人会问这不是和Nginx的配置很像吗确实概念上有共通之处。Nginx里server{}块定义站点、location块定义路径路由Tomcat里的Host对应站点Context对应应用路径。但Tomcat的层级更偏“Java应用容器”一点它有一个Nginx没有的东西——Engine。Engine是Servlet引擎的核心负责把请求路由到正确的Host和Context上。1.2 改配置前必须知道的“有效期”问题这个坑几乎每个新手都会踩改了server.xml直接在管理界面点重启结果没生效。原因很简单——server.xml的加载只发生在Tomcat进程启动时而不是应用重新加载时。Context的reloadabletrue能触发的只是应用级别的重载重新加载WEB-INF/classes和lib下的jar它对server.xml本身的变更没有任何反应。所以我个人的习惯是改server.xml之前先确认这次改动的范围。如果只是调一个Connector的port或maxThreads那必须完整停掉再启动Tomcat进程如果只是改Host下的Context配置虽然理论上有JMX操作可以实现热更新但生产环境我从来不赌这个一律全量重启干净利落。还有一个细节Tomcat启动时如果server.xml有语法错误进程会直接起不来catalina.out日志里会抛出一堆org.xml.sax.SAXParseException。所以改完配置别急着启动服务先在命令行跑一下catalina configtest这个命令专门用来校验server.xml的合法性。我现在每次改完都先做这个动作几秒钟就能省下后面排查的大把时间。2. server.xml核心元素逐个拆解2.1 Server节点整个实例的“根管理者”先看一个最小化的Server节点Server port8005 shutdownSHUTDOWN Listener classNameorg.apache.catalina.startup.VersionLoggerListener / Listener classNameorg.apache.catalina.core.AprLifecycleListener SSLEngineon / Listener classNameorg.apache.catalina.core.JreMemoryLeakPreventionListener / Listener classNameorg.apache.catalina.mbeans.GlobalResourcesLifecycleListener / Listener classNameorg.apache.catalina.core.ThreadLocalLeakPreventionListener / GlobalNamingResources Resource nameUserDatabase authContainer typeorg.apache.catalina.UserDatabase descriptionUser database factoryorg.apache.catalina.users.MemoryUserDatabaseFactory pathnameconf/tomcat-users.xml / /GlobalNamingResources Service nameCatalina ... /Service /Serverport和shutdown这两个属性是配套的它们管的是“关闭端口”。shutdown指定的字符串是关闭指令任何人只要能连上这个端口并发送这个字符串就能把Tomcat进程杀掉。注意这里说的是“任何人”只要网络可达就能做到虽然Tomcat官方默认值一直是SHUTDOWN但这个默认值其实早就不是什么秘密了。我处理线上环境时会把shutdown改成一长串随机字符串同时把port从8005改成一个大一点的随机端口比如18690。配合防火墙规则只允许本机和运维跳板机访问这个端口。有个同事曾经因为图省事没改这个配置被安全扫描工具直接探测到8005端口并尝试发送SHUTDOWN指令。虽然那次因为网络策略没造成实际损失但这事的教训我记得很牢——server.xml里默认值并不代表安全值。Listener是事件监听器负责在容器生命周期不同阶段做初始化或清理工作。默认的几个Listener各有各的用处我重点说两个常见的JreMemoryLeakPreventionListener解决JDK类加载器在重新部署时导致的内存泄漏问题这个没事别动它。AprLifecycleListener负责加载APRApache Portable Runtime本地库开启了APR之后Tomcat的静态文件处理能力和SSL性能都会有明显提升。默认配置一般是够用的我基本不往Server节点上增加自定义Listener真要加也是通过org.apache.catalina.LifecycleListener接口去扩展这东西牵一发动全身非必要不动。2.2 Service节点连接器与引擎的“绑定关系”Service节点本身就是一个逻辑分组它把Connector和Engine打包整体对外提供服务。一个典型的默认配置Service nameCatalina Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 / Engine nameCatalina defaultHostlocalhost ... /Engine /Service这里的核心逻辑是所有Connector接收到的请求统一交给它所属的Service下的那个Engine处理。如果一个Service里配了三个Connector比如一个HTTP、一个HTTPS、一个AJP它们接收到的请求最终都会汇入同一个Engine。什么时候需要多个Service比如同一个Tomcat进程上你想让一组应用走HTTP 8080、另一组应用独立跑在一个不同的Engine上且它们之间逻辑彻底隔离这时候就可以声明两个Service。但说实话这种需求在常规业务里非常少见绝大多数项目一辈子都是一个默认的Catalina就够用了。多Service的配置复杂度远大于收益别为了“看起来灵活”去拆拆了以后日志管理、监控采集都会变成双份工作。2.3 Host节点一台机器上挂多个“虚拟站点”Host节点对应的是虚拟主机它和Nginx的server{}块作用几乎一样让一台Tomcat能根据请求头的Host字段路由到不同的站点。默认配置是这样的Engine nameCatalina defaultHostlocalhost Host namelocalhost appBasewebapps autoDeploytrue unpackWARstrue ... /Host /Engine这里面四个属性需要理解透彻name这个虚拟主机的名字一般用域名表示。请求进来后Tomcat会用请求头里的Host去匹配各个Host节点的name匹配不到的会落到Engine上配置的defaultHost。appBase这个虚拟主机存放应用的目录。默认是webapps也就是把War包丢进webapps目录就会自动被扫描到。autoDeploy是否开启自动部署。开启后Tomcat会定时扫描appBase目录发现新War包或新的解压目录就自动部署删除目录就自动卸载。开发环境非常方便生产环境我一般会关掉。unpackWARs部署时是否自动解压War包。如果为falseTomcat会直接从War包读取资源不生成解压目录。那种只读的静态资源类应用可以这么做但有JSP的场景我不建议因为JSP编译依赖解压后的目录结构直接跑War包容易出各种奇怪的Path问题。这一节先不展开多Host部署细节后面专门用一章来讲。3. Connector配置级别最高的调优战场3.1 Connector四件套port/protocol/threads/timeout的搭配逻辑Connector是server.xml里属性最多、也最容易被改坏的地方。先把最常见的几个属性拿来说Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 maxThreads200 minSpareThreads10 acceptCount100 maxConnections8192 URIEncodingUTF-8 compressionon compressionMinSize2048 noCompressionUserAgentsgozilla, traviata compressableMimeTypetext/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json/逐个拆解port是监听端口一个Tomcat可以同时监听多个端口只要声明多个Connector即可但端口不能冲突这个不用多说。protocol决定用什么协议栈接收请求默认的HTTP/1.1用的是Java原生的NIO实现Java 9之后的Tomcat 9里HTTP/2的支持也在这个协议基础上做扩展。connectionTimeout是等待请求数据的最长毫秒数默认20秒一般不用调运营类页面跳转慢的可以考虑调大但别超过30秒。maxThreads是Tomcat处理请求的线程池上限。这里有个常见的误解线程数设得越大越好。其实线程数超过一定阈值之后CPU上下文切换的成本就会吞噬掉并发的收益。我个人的参考区间是200~400具体要看一次请求的平均耗时。如果是纯接口服务平均响应在100ms内的200线程已经能扛住挺高的QPS但如果接口里有一堆慢IO调用比如远程HTTP调用、慢SQL那线程池需要相应调大不然请求会在Tomcat的请求队列里排队反映到客户端就是连接建立成功但迟迟没有响应。acceptCount是操作系统中可用的连接等待队列长度。当Tomcat线程池已经满负荷新的连接请求会进入这个队列等待。acceptCount设得太大会导致响应时间漂移——客户端连接是建立成功了但实际处理时间可能已经是几秒以后了。设得太小则会出现Connection refused之类的错误。经验值是保持和maxThreads差不多大小可以在100~200之间浮动。maxConnections是Tomcat能同时接收的最大连接数当这个值被撑满后后续的连接请求会进入acceptCount队列等待。这里注意它和maxThreads是两码事maxThreads限制的是处理线程maxConnections限制的是连接本身。NIO模式下maxConnections默认是10000对于绝大多数场景够用了。3.2 编码问题的源头URIEncoding中文乱码是Java Web老生常谈的话题而其中很大一部分根源在URIEncoding。Tomcat 8及以上版本默认已经是UTF-8了所以新项目不用操心。但如果你的项目是Tomcat 7时代的老古董或者有需求要用GET请求传中文参数请确认Connector上有没有显式设置URIEncodingUTF-8。我之前遇到过的一个情况是个老系统GET请求带中文关键字搜索乱码问题排查了一圈数据库连接URL、过滤器的request.setCharacterEncoding都设置了UTF-8结果还是乱。最后打开server.xml一看Connector上压根没写URIEncoding用的还是平台默认编码Windows下是GBKLinux下是UTF-8所以部署到Linux环境正常、在Windows测试环境就乱。把URIEncodingUTF-8加上去之后问题立刻消失。这个坑属于“环境相关的隐蔽问题”排查时很难想到是server.xml的配置项缺了。3.3 compression静态资源性能的隐藏开关compressionon开启的是HTTP响应压缩等价于给静态资源和文本响应加了一层gzip。很多团队完全不知道Tomcat自带了这功能还专门在前面挂一层Nginx做压缩。其实用Tomcat直接对外服务的时候把压缩开起来能立刻看到效果。判断要不要开压缩的简单标准响应内容是文本类HTML/JS/CSS/JSON/XML且响应体超过1KB开压缩基本都有收益。对于图片、视频这些已经压缩过的二进制格式compressableMimeType里别加image/*和application/pdf加了反而浪费CPU。compressionMinSize是触发压缩的最小字节数默认是2048字节也就是2KB。小于这个值的不压因为压缩后可能比原文还大白白浪费CPU。这个值我一般保持默认没有特殊需要不用去动。还有一个两端配合的细节如果前面挂了Nginx做反向代理而Nginx已经开启了gzip那Tomcat这边就不需要再开compression了。双重压缩不仅浪费CPU某些浏览器还会因为响应里出现两次压缩标记而出渲染问题。我见过团队里配置了Nginx gzip又同时给Tomcat开了压缩结果肉眼看不出问题但压测数据明显不如只开一层的时候好看。3.4 AJP Connector老协议新取舍默认server.xml里在Tomcat 9之前还会有AJP连接器的配置Connector port8009 protocolAJP/1.3 redirectPort8443 /AJP协议是Apache HTTP Server和Tomcat之间专用的二进制通信协议早期架构里经常用Apache或者Nginx通过AJP协议把请求转发给Tomcat。到了现代Nginx对HTTP协议的反向代理已经做得很成熟直接proxy_pass http://tomcat_upstream;就能解决AJP的使用场景大幅缩减。我的态度很明确没有历史包袱的新项目不要开AJP Connector。原因有三它暴露了一个额外的端口扩大了攻击面。它在高并发下的稳定性表现不如纯HTTP的反向代理方案。AJP协议的历史漏洞比如曾经的Ghostcat漏洞虽然已经修复但每多一个协议就多一份维护成本。如果确实需要关闭AJP最简单的做法就是把Connector port8009 protocolAJP/1.3 ... /这一整行注释掉。注意注释节点时要确认整个Connector元素的标签对完整别只注释了一半。3.5 SSL配置HTTPS直配的最优解现在的正规项目基本都有HTTPS的需求。以前常规做法是Nginx终结SSL然后把HTTP流量转发到Tomcat但Tomcat其实完全可以直接配置HTTPS。用JDK自带的keytool生成证书后在server.xml里加一个ConnectorConnector port8443 protocolorg.apache.coyote.http11.Http11NioProtocol maxThreads200 SSLEnabledtrue SSLHostConfig Certificate certificateKeystoreFileconf/ssl/keystore.p12 certificateKeystoreTypePKCS12 certificateKeystorePasswordyourpassword / /SSLHostConfig /Connector这里推荐用PKCS12格式而不是老式的JKS因为PKCS12是行业标准格式后续想迁移到其他中间件或者Nginx时不需要再转换格式。如果用的是PEM证书链比如从云厂商证书控制台下载的Tomcat 8.5及以上版本也支持直接配置Certificate certificateFileconf/ssl/fullchain.pem certificateKeyFileconf/ssl/privkey.pem typeRSA /有几个证书配置的注意事项想提一下证书文件和server.xml的相对路径是相对于$CATALINA_BASE的默认是Tomcat安装目录。路径写错时Tomcat启动会直接报FileNotFound异常。修改了Connector的SSL配置后必须重启进程才生效这不是热加载能覆盖的范畴。如果同时配置了HTTP和HTTPS两个Connector记得在HTTP的Connector上设置redirectPort8443这样HTTP请求会在客户端侧被301重定向到HTTPS端口。如果前面还有Nginx那HTTPS大概率已经终结在Nginx这一层了Tomcat侧不需要重复配置SSL保持纯HTTP即可。捎带说一句redirectPort这个属性在“前面有Nginx”的架构里几乎用不上因为重定向动作是Nginx做的跟Tomcat没关系。4. Host与Context多站点、多应用部署的几种姿势4.1 单端口、多域名靠Host节点分流现在的业务经常会碰到“一个Tomcat要同时服务好几个域名”的需求。比如有www.a.com和www.b.com两个站点端口都是8080希望通过域名字段自动分流。实现方式就是在Engine节点下加两个HostEngine nameCatalina defaultHosta.com Host namea.com appBasewebapps-a autoDeployfalse unpackWARstrue Context path docBasea-app / /Host Host nameb.com appBasewebapps-b autoDeployfalse unpackWARstrue Context path docBaseb-app / /Host /Engine这套配置的请求路由逻辑是请求进来Tomcat先匹配Host字段找到对应的Host节点再根据URL路径匹配Context。两个Host的appBase最好是不同的目录避免出现两个Host扫描到同一份应用目录的冲突。一个容易犯的错两个Host节点里都设置appBasewebapps在这个目录下放了两个War包然后指望Tomcat自动把包对应到不同的Host上。这是不成立的Tomcat扫描appBase是每个Host独立进行的同一个War包会被两个Host同时认领。正确的做法是给每个Host单独建目录。4.2 docBase与appBase的关系可能和你想得不一样appBase和docBase是XML里最容易搞混的一对属性。简单区分appBase是“站点级”的扫描目录Host节点会盯着这个目录找可以部署的应用。docBase是“应用级”的实际路径指定这个Context对应的Web应用目录或War包完整路径在哪里。默认情况下往webapps目录丢一个demo.warTomcat自动解压并部署后生成的Context的docBase就是webapps/demo。这就是“依托appBase的自动部署模式”。手动声明Context时docBase可以直接指向Tomcat外部的绝对路径这在团队协作里很常用开发机代码在磁盘任意位置只要把docBase指过去就完全不必把项目复制到webapps下。比如Host namelocalhost appBasewebapps autoDeploytrue unpackWARstrue Context path/demo docBase/home/apps/demo reloadabletrue / /Host这里path/demo意味着访问入口是http://localhost:8080/demo/即使docBase在完全不同的机器路径上也一样生效。4.3 Context path配置的几个易错点配置Context的path属性有几个需要留神的点如果想把某个应用挂在站点根路径下访问即http://localhost:8080/直接命中path需要设置为空字符串path。注意不是path/写/在某些版本的Tomcat里会启动报错或导致无法匹配。如果Host节点开启了autoDeploy同时在server.xml里手动声明了Context这两者有可能冲突。Tomcat的设计是autoDeploy只扫描appBase下没有在server.xml里显式声明过的应用。如果同一个应用既被手动声明又被自动扫描到启动日志里通常会打出警告行为在不同版本间也有差异。稳妥的做法是手动管的应用目录就不要放在appBase扫描范围内或者不要显式声明已经放到appBase里的应用。修改path后只重启应用不重启Tomcat是有可能不生效的。我自己的实践是Context级别的调整一律全量重启省心。4.4 优雅关闭自动部署生产的稳定之选autoDeploytrue在开发阶段是神器改完代码拷个War包进去就自动生效了。但生产环境开着自动部署有时候会搞出一些匪夷所思的事运维通过脚本同步文件时War包只传了一半Tomcat恰好在那个时间点扫描到了半包解压失败然后整个应用变成不可用状态。自动部署意味着应用的启停是由Tomcat的BackgroundProcess线程触发的它和人工停机部署的时机不可控日志排查的时候很难对齐时间线。某些文件监听和重载机制在自动部署模式下会和servlet的上下文初始化逻辑产生竞态特别是依赖Spring Boot外置配置的应用。我的生产配置习惯是autoDeployfalse并且关掉reloadable所有的应用发版都走完整的Tomcat重启流程。虽然重启多花几秒钟但换来的是行为可预期。发版这种事确定性比速度重要。5. Valve与并发控制藏在Host里的安全阀5.1 AccessLogValve不用改代码的请求审计很多团队对访问日志的诉求是“Nginx里有就够了”但如果你的Tomcat直接对外暴露或者中间经过了一层负载均衡后你想看应用的耗时分布AccessLogValve能帮大忙。它挂在Host节点下配置一段XML就能记录所有经过这个虚拟主机的HTTP请求细节Host namelocalhost appBasewebapps Valve classNameorg.apache.catalina.valves.AccessLogValve directorylogs prefixlocalhost_access_log suffix.txt pattern%h %l %u %t quot;%rquot; %s %b %D / /Host这里pattern里的%D是处理请求耗时的毫秒数对排查慢请求非常有用。结合%s返回状态码和%b响应字节数基本能还原一个请求的完整画像。要注意的是directory属性是相对$CATALINA_HOME/logs目录的。日志文件按天滚动滚动时机会在Tomcat的日志管理线程中处理不会影响主请求链路。有几次排查线上问题我都是先翻access_log按%D倒序找出耗时超过几秒的请求再看对应的URL参数很快就能定位到是某个慢SQL还是某次远程调用阻塞。没有这层日志的话排查的起点就是大海捞针。5.2 RemoteAddrValveIP白名单的轻量实现内网管理后台一类功能最简单的保护方式就是IP白名单。如果这层控制放在Tomcat层面做RemoteAddrValve是最轻量的方案Valve classNameorg.apache.catalina.valves.RemoteAddrValve allow10\.0\.0\.\d|192\.168\.1\.100 deny/allow和deny支持正则表达式规则是先匹配deny再匹配allow。空白的deny表示“没有拒绝列表”。这里我特意用了192\.168\.1\.100这种带转义的正则写法因为点号在正则里是“任意字符”的意思如果不转义192.168.1.100这条规则实际上会匹配192x168x1x100这类字符串这会让白名单形同虚设风险极大。注意在生产环境配置IP白名单之前要做确认别把自己的办公网IP段写错了。我见过把allow写了个不存在网段结果所有人包括管理员都被挡在门外的案例。也别指望这个能做到细粒度的用户权限管理它只适合粗粒度的网络层过滤。5.3 使用RewriteValve做路径重写Tomcat 9及以上版本支持RewriteValve作用类似Apache的mod_rewrite。需要在Host节点下声明Valve classNameorg.apache.catalina.valves.RewriteValve /然后去conf/Catalina/localhost/rewrite.config文件夹里写重写规则^/old-path/(.*) /new-path/$1 [R301,L]我一般只在实际业务确实需要URL兼容的时候才用这个功能。比如把一个老接口路径换成新路径时用RewriteValve做301跳转可以避免客户端因为硬切换而出现404。但要注意RewriteValve的规则是用Tomcat自己的语法写的和Nginx的rewrite语法很像但有差异。建议先在一台测试环境把规则验证全了再上生产否则一个正则错误可能导致整个站点的路由都错乱。6. 部署、启动参数与JMX把server.xml玩出花6.1 通过配置文件覆盖端口和目录CATALINA_BASE的力量很多运维同事直接用一份Tomcat安装包启动多个实例修改server.xml改到怀疑人生后来发现缩略配置方案把server.xml、web.xml、context.xml这些文件全部复制到另一个目录然后用CATALINA_BASE指定这个目录启动。export CATALINA_BASE/opt/tomcat-instance-8081 export CATALINA_HOME/opt/tomcat bin/catalina.sh start这意味着多实例部署每个实例都共用同一套二进制但配置文件互相独立。同一个Tomcat包启动三个不同端口的服务只需要在CATALINA_BASE里分别维护三份server.xml。这个模式非常适合同一台机器上隔离跑多套环境省掉大量重复复制安装包的操作。6.2 在server.xml里设置JVM参数不行但可以通过setenv.sh配合网上有个惯性说法“在server.xml里设置JVM参数”严格来说这是错误路径。server.xml只负责容器结构JVM参数比如-Xmx、-XX:MaxMetaspaceSize是通过bin/setenv.shWindows下是setenv.bat设置的。setenv.sh是启动脚本启动前会读取的额外脚本默认不存在手动创建后放些参数即可JAVA_OPTS-Xms1024m -Xmx2048m -XX:MaxMetaspaceSize512m -Dfile.encodingUTF-8文件放到CATALINA_BASE/bin下只对这个实例生效。6.3 JMX远程监控管理控制台的另类打开方式Tomcat有很多运行时的状态连接数、线程数、堆内存可以通过JMX暴露出来。在server.xml里声明一个Listener启动JMXListener classNameorg.apache.catalina.mbeans.JmxRemoteLifecycleListener rmiRegistryPortPlatform10001 rmiServerPortPlatform10002 useLocalPortsfalse /还需要配合setenv.sh设置CATALINA_OPTS开启JMX远程认证CATALINA_OPTS-Dcom.sun.management.jmxremote \ -Dcom.sun.management.jmxremote.authenticatefalse \ -Dcom.sun.management.jmxremote.sslfalse \ -Dcom.sun.management.jmxremote.port10000提醒一点JMX不带认证直接暴露到公网等于门户大开。生产环境务必配合防火墙限定来源IP并且建议开启authenticatetrue配用户名密码。Tomcat内置的JMX账号密码文件是conf/jmxremote.access和conf/jmxremote.password要一起配置到位。6.4 优雅停机与端口优雅规避在server.xml中预留的余量早期使用Tomcat时经常遇到一个问题进程被kill后8080端口还在被占用导致新进程起不来。这在JDK 8和Tomcat 9上已经不是常态因为Server节点默认的port关闭机制会等存量请求处理完再退出。但为了进一步保障我通常会在启动脚本里结合catalina.sh stop配合超时判断来处理这其实和server.xml没直接关系只是我在实际运维中统计过绝大多数“端口被占用”的告警都和上次停机方式有关。server.xml里能直接帮上忙的是设置Server的port为一个自动化运维脚本能“预测”的端口然后统一在部署脚本里通过该端口发关闭指令if /bin/bash -c echo SHUTDOWN | nc -w 3 127.0.0.1 18690 /dev/null 21; then echo Tomcat stopped gracefully else echo Port not responding, check instance fi这样一份脚本配合多套实例时只要每个实例的Server端口不同发版时就能精准优雅地逐个停机。7. 常见问题排查清单实时更新版这里整理一份我在群里和现场见到的高频问题直接对号入座现象大概率原因排查命令/方法启动报Address already in useConnector端口被占用netstat -tlnp | grep 8080找到占用进程启动报SAXParseExceptionserver.xml语法错误catalina configtest校验访问页面404无反应Context的path或docBase配置不当查看logs/catalina.out确认Context是否成功启动中文参数乱码Connector未配置URIEncoding检查URIEncodingUTF-8是否加好页面能连上但响应极慢线程池打满请求在队列排队jstack线程栈看http-nio-8080-exec-*线程状态磁盘日志疯狂膨胀AccessLogValve保留了默认pattern且没有滚动策略检查logs目录给日志安排logrotate管理后台被外部访问RemoteAddrValve未配置或配置错误检查Host节点下Valve的allow正则重启后配置丢失改的不是CATALINA_BASE下的配置文件查看echo $CATALINA_BASE指向的目录有几个补充经验想单独说端口被占用的坑。有时候netstat看不到占用但Tomcat还是起不来可能是Server关闭端口被占。这种情况在Windows上尤其常见之前遇到过8005端口由java进程占用但netstat过滤条件不对看不出来又排查了半天才确认。另一个可能是IPv6和IPv4双栈下监听了[::]:8080和0.0.0.0:8080的进程互相冲突。404的排查顺序。新手遇到Tomcat下404第一反应就是代码问题。但正确顺序是先看应用是否真的部署成功logs目录下的localhost.logContext启动失败会记录详细异常比catalina.out更值得看。确认应用部署OK再往前排查Nginx路由、Context path。我之前排查过一个“源服务器未能找到目标资源的表示”的问题这个提示本身是Tomcat 404页面的英文描述翻了中文后的形态最后定位是Nginx的proxy_pass路径少了斜杠把/api转发成了/api/到Tomcat就匹配不上Context了。线程池满时的排查思路。如果Connection refused或者超时频繁但CPU和内存都不高先看maxThreads和acceptCount是不是太小。我一般先用jstack -l pid | grep http-nio数一下有多少活跃线程偏高就调大maxThreads大部分情况能临时缓解。但要根治还得看有多少线程阻塞在什么样的调用链上有可能是外部依赖慢导致线程全被挂起。动态刷新与重启的判断。server.xml改了端口、Connector、Valve、Listener、SSL证书、线程池相关参数这些全部要重启Tomcat进程。别浪费时间去找“热加载”方案官方就没有这个机制。如果只是改Context的reloadable它会自动重载应用但改了Context的path和docBase还是要重启才完全可靠。8. 手把手配一份生产级server.xml综合前面所有内容我来给一份经过大量项目验证的参考配置模板。你们可以根据自己的实际情况调整参数和路径?xml version1.0 encodingUTF-8? Server port18690 shutdownA7E1F2D10E9D77409C318F41 Listener classNameorg.apache.catalina.startup.VersionLoggerListener / Listener classNameorg.apache.catalina.core.AprLifecycleListener SSLEngineon / Listener classNameorg.apache.catalina.core.JreMemoryLeakPreventionListener / Listener classNameorg.apache.catalina.mbeans.GlobalResourcesLifecycleListener / Listener classNameorg.apache.catalina.core.ThreadLocalLeakPreventionListener / GlobalNamingResources Resource nameUserDatabase authContainer typeorg.apache.catalina.UserDatabase descriptionUser database factoryorg.apache.catalina.users.MemoryUserDatabaseFactory pathnameconf/tomcat-users.xml / /GlobalNamingResources Service nameCatalina Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 maxThreads300 minSpareThreads20 acceptCount200 maxConnections10000 URIEncodingUTF-8 compressionon compressionMinSize2048 noCompressionUserAgentsgozilla, traviata compressableMimeTypetext/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json/ !-- 如果没有历史包袱AJP直接注释掉不对外开放 -- !-- Connector port8009 protocolAJP/1.3 redirectPort8443 / -- Engine nameCatalina defaultHostlocalhost Host namelocalhost appBasewebapps autoDeployfalse unpackWARstrue Valve classNameorg.apache.catalina.valves.AccessLogValve directorylogs prefixlocalhost_access_log suffix.txt pattern%h %l %u %t quot;%rquot; %s %b %D / !-- Context path/demo docBase/home/apps/demo reloadablefalse / -- /Host /Engine /Service /Server这份模板的几个默认选择我解释一下Server端口从8005改成了18690shutdown改成随机串。这个改动不费事但能直接消除一个默认端口的暴露面。autoDeployfalse应用部署全部走可控发布。maxThreads300是折中值。如果你的业务单次请求响应很快可以往下调到200节省内存和线程切换开销如果慢IO请求多往上调到400~500再多就没必要了核心瓶颈通常在外部依赖。AcceptCount200和maxThreads300是配套的整体上给系统留出了约500的并发缓冲压力突增时不会立刻拒绝连接。AJP连接器默认注释掉减少端口和协议暴露。历史老项目如果依赖Apache mod_jk转发那就要按需保留。AccessLogValve开启了耗时记录%D方便后续排查性能问题。9. 写在最后的几条实战体会server.xml这个文件表面上是Tomcat的配置实际上它决定了一台服务节点能承受多大的流量、能多稳地跑在网络上。我前面提到每个参数选型背后的逻辑这里再总结几点真正从故障现场学到的教训。第一改配置前先备份一份完好的server.xml。这个动作看起来多余但在你改动一个正则、一个端口、一个线程池参数后发现服务起不来时它就是救命稻草。我的习惯是在server.xml同目录建一个backup/文件夹每次改完配置先cp server.xml backup/server.xml.$(date %Y%m%d%H%M%S)再动文件。版本号自己带日期回溯时清楚得很。第二线上如果出现找不到问题的间歇性故障先看server.xml里有没有改了但不生效的配置项。很多团队用发布工具推送整个Tomcat目录发布工具可能在推送过程中把server.xml回滚到了旧版本导致你以为改了、其实线上跑的还是老配置。这种“配置漂移”的排查难度极高但可以通过发布后自动执行catalina configtest加采集配置文件的MD5来防御。第三server.xml不是和Tomcat“绑定死”的文件。同一个Tomcat装在不同目录配置文件可以各自独立这也是CATALINA_BASE机制存在的意义。不要在一个Tomcat安装目录里直接改文件来适应所有环境而是为每个环境建一份独立的CATALINA_BASE。这套模式越早建立后面环境和环境之间的干扰越少。最后说一句关于“调优”的话。网上很多文章喜欢堆一堆大数字什么maxThreads1000、acceptCount500看起来性能就上去了。其实真正负责任的调优是拿压测数据说话的先确认业务平均响应时间、期望的QPS再结合机器核数和内存反推线程池参数最后通过压测验证逐步收敛到最优区间。server.xml的每一个数字后面都应该有一段压测和监控数据的支撑而不是拍脑袋。希望这篇配置详解能让你在下次打开server.xml的时候不再对着里面的标签发怵。从端口到虚拟主机从并发到日志每一个配置项都是有它存在的理由的理解它们Tomcat这个容器才算真正握在了自己手里。