
做中间件运维这十年我接手过太多“装完能跑、换台机器就废”的 TW8下文一律用这个简称现场安装目录散落在/root下、数据源密码写在控制台里没人知道、应用包版本靠文件名里的日期区分、出问题只能翻三小时前的控制台滚动日志。TW8 是一款国产 Java 应用服务器中间件本身能力没问题问题几乎全部出在“部署”这件事没有被当成一个工程对待。所谓TW8 项目化部署核心就一句话把一次性的手工安装变成一套可重复执行、可版本化、可回滚的部署资产——包含目录骨架、配置模板、静默脚本、启停托管、巡检清单五件套。这篇内容面向正在做信创改造、需要批量交付 TW8 实例的运维和实施同学也适合刚接手中间件、只会点控制台的新手。我会按“为什么这么做—目录怎么摆—环境怎么备—装怎么装—参数怎么算—包怎么发—日志怎么盯—坑怎么排”的顺序讲透每一步都给出可以直接抄走的结构和脚本参数部分全部附上推导过程而不是甩一堆数字让你自己猜。1. 为什么要把 TW8 部署做成“项目”而不是“装一次能跑就行”1.1 从“装好能跑”到“每次装都一样”差在哪手工装 TW8 的典型画面是这样的拿到安装包解压到某个顺手的位置控制台点下一步改个端口启动应用传上去能访问收工。这个过程在单台测试机上完全没问题甚至很爽。但它埋了四个雷安装路径不固定导致脚本无法复用端口、JVM、数据源全靠记忆填第二台机器必然有偏差应用包没有版本号回滚只能靠“我记得上一个包叫 war_new2_final”日志没有轮转磁盘满了才发现服务挂了三天。项目化部署要做的就是把这四个雷提前拆掉。它不追求“一键全自动”这种听起来很美的东西而是追求可重现任意一个环境、任意一台新机器用同一套资产部署出来的实例除了 IP 和数据库连接串其他全部一致。这个目标听起来朴素但真正做到之后排查问题的效率会发生质变——因为你终于可以确定“配置不一样”不再是嫌疑对象了。心得我判断一个 TW8 部署是否合格标准很土——换一个从没接触过这个项目的人只给他部署资产他能不能在 40 分钟内装出一个可用的实例。做不到就说明还有东西活在老员工的脑子里。1.2 项目化部署到底解决了哪几个真问题具体拆开来它解决的是五类问题。第一是环境漂移开发、测试、生产三套环境的 JVM 参数、连接池大小、日志级别各不相同上线后性能表现完全不同最后谁都说服不了谁。第二是交付审计信创项目验收时经常要问“这个实例装了什么版本、改过哪些参数”没有配置基线就只能现场编。第三是批量扩容从 2 个实例扩到 8 个手工装是线性耗时脚本装是常数耗时。第四是故障恢复磁盘挂了重建实例有资产是半小时没资产是半天。第五是责任边界配置在版本库里、变更走提交出了问题能查到是谁在什么时候改了什么。这五条里面第四条最容易被低估。我经历过一次机房存储故障同一批 6 个实例全挂有部署资产的那套 40 分钟恢复完毕另一套纯手工的从下午两点搞到晚上九点最后还是靠人肉回忆才把数据源配回去。从那以后我坚持一个原则部署脚本和配置模板必须进版本库和代码同等地位不能放在某台跳板机的家目录里。2. 部署包的目录结构与版本约定2.1 一套自己定的目录骨架TW8 自身的安装目录结构是固定的不建议去动它但在它外面套一层我们自己的项目目录是完全必要的。我惯用的骨架是三层交付资产层、运行实例层、数据持久层。交付资产层放部署包和脚本可以整包拷贝运行实例层是 TW8 实际安装的位置数据持久层独立挂盘放日志、上传文件、会话持久化数据这样重装实例时数据不用动。/opt/tw8-deploy/ # 交付资产层进版本库 ├── packages/ # 安装包按版本建子目录 │ ├── tw8-8.0.x/ │ └── tw8-8.0.y/ ├── templates/ # 配置模板层 │ ├── common/ # 全环境通用 │ ├── env-dev/ # 环境差异化 │ └── env-prod/ ├── scripts/ # 脚本层 │ ├── 01-init-os.sh │ ├── 02-install.sh │ ├── 03-create-domain.sh │ ├── 04-deploy-app.sh │ ├── 05-rollback-app.sh │ └── 06-healthcheck.sh └── inventory/ # 实例清单一台机器一行 /opt/tw8/ # 运行实例层安装目录 /data/tw8/ # 数据持久层独立挂盘 ├── logs/ ├── upload/ └── session/这个骨架的关键设计在templates的分层。common放所有环境都一样的配置比如协议、字符集、静态资源缓存策略env-*只放差异项数据库地址、日志级别、JVM 堆大小。部署时按“先 common 后 env”的顺序合并覆盖生成最终配置文件。这样做的直接好处是改一个通用参数只需要动一个文件而不是去 8 台机器上分别改。注意不要把packages跟着版本库一起塞进 Git 仓库安装包动辄几百兆仓库会迅速膨胀。正确做法是仓库里只放包的校验值sha256和下载路径说明CI 环节或部署机上单独同步包。2.2 配置文件的分层模板层、环境层、实例层三层配置的分工要说清楚否则维护一段时间就会乱。模板层是“这个中间件应该长什么样”由架构或中间件负责人维护改动需要评审。环境层是“这套环境有什么特殊”由环境负责人维护比如测试环境日志级别设成 DEBUG、生产环境关掉自动部署避免误传。实例层是“这一台机器有什么特殊”只在极少数情况下使用比如某台机器内存特别大堆可以给到 8G。实例层要慎用、要留痕。我的做法是实例级覆盖必须在一个overrides.properties里且文件头强制要求写注释说明原因和申请人。没有注释的覆盖项巡检脚本直接告警。这个约束听起来麻烦但它防止的是“某天有人临时改了个参数忘了改回来”这种最恶心的事故——这类问题排查起来毫无线索因为配置文件本身看起来是正常的。配置合并的实现不用搞得很复杂一段 shell 就够先拷贝 common 下的文件到实例目录再用 env 目录下的同名文件覆盖最后用 overrides 覆盖。合并完成后计算一次配置文件指纹比如md5sum汇总把指纹记录到部署日志里。将来要对比两台机器的配置差异比指纹就行不用逐个文件 diff。2.3 版本号与回滚位的设计版本号不要用日期用语义化版本8.0.x-app1.2.3。前半段是中间件版本后半段是应用版本两段独立演进。实例目录里保留current和previous两个软链接current指向当前运行的应用目录previous指向上一个可用版本。发布时新建一个版本目录写好之后把current切过去重启生效回滚时把current切回previous重启即可。软链接切换这个做法比“覆盖原文件”强太多。覆盖式发布的问题是一旦新包有问题旧包已经被覆盖了你手上只剩一个坏包。而目录式发布保留了完整的上一版回滚是秒级的。代价是磁盘占用翻倍但这个代价完全值得——一个应用包通常几十到几百兆比起事故停机时间这点磁盘不值一提。版本目录的保留策略也要定至少保留最近 3 个版本超过的按时间清理。清理脚本要写成“只删不在 current 和 previous 里的目录”防止手滑把正在跑的删掉。这个脚本我建议加个 dry-run 模式先打印要删的目录人工确认后再执行。3. 环境准备装之前必须确认的六件事3.1 操作系统架构与 JDK 的匹配TW8 运行在 JVM 上所以第一步永远是确认 JDK。三件事必须核对架构对不对x86_64 还是 aarch64、位数对不对必须 64 位、版本对不对。TW8 8 版本线一般支持 JDK 8 和 JDK 11 两条路部分版本支持到 17具体以你拿到的发布说明为准。信创环境里 ARM 架构服务器很常见这里最容易出的问题是装了 x86 的 JDK 包java -version直接报“cannot execute binary file”然后开始怀疑人生。确认命令很简单但要养成习惯别跳过uname -m # 输出 x86_64 或 aarch64 getconf LONG_BIT # 必须输出 64 java -version 21 # 确认版本与供应商 which java # 确认用的是哪个 java避免被旧版本抢先which java这一步经常被忽略。真实场景是机器上原本装了系统自带的 OpenJDK 8你又装了一个 JDK 11 到/usr/local但 PATH 没改结果 TW8 启动时用的还是 8然后某些特性不生效你查了半天配置文件。解决办法是在启停脚本里显式指定JAVA_HOME不依赖环境的 PATH。这一条我写进了所有项目的启停脚本模板里属于血泪换来的固定动作。JDK 选择上还有一个小细节优先选与中间件官方发布说明匹配的版本别追新。我见过为了用某个新 GC 特性把 JDK 升到很新的版本结果中间件某些内部 API 反射失败报一堆看不懂的错最后退回原版本解决。中间件这类基础软件稳定压倒一切。3.2 用户、目录、权限与内核参数TW8 绝对不能用 root 跑。标准做法是建一个专用系统用户比如twuser家目录设成安装目录的上级禁止登录 shell。目录权限按“用户可写、组可读、其他不可访问”来设日志目录和数据目录单独授权。groupadd -r twgrp useradd -r -g twgrp -d /opt/tw8 -s /sbin/nologin twuser mkdir -p /opt/tw8 /data/tw8/{logs,upload,session} chown -R twuser:twgrp /opt/tw8 /data/tw8 chmod 750 /opt/tw8 /data/tw8内核参数这边主要关注三样文件句柄数、进程数、TCP 相关参数。文件句柄数是最容易踩的默认 1024 在高并发下分分钟打满表现是“Too many open files”然后连接建立失败。我一般直接给到 65535在/etc/security/limits.conf或者 systemd 的 unit 里配LimitNOFILE65535。后者的好处是只对这一个服务生效不影响系统全局。TCP 参数里net.ipv4.tcp_tw_reuse和somaxconn是常用项但要说明白前者影响的是主动关闭连接后的端口复用后者影响的是 accept 队列长度。参数值不要照抄网上的“万能优化”要看你的实际连接模型。短连接多、QPS 高的场景somaxconn给到 1024 甚至更大有意义长连接为主的内部服务调大了也没什么感知。注意改limits.conf之后必须重新登录才生效而 systemd 托管的服务根本不读这个文件。如果你用 systemd 起服务改完limits.conf发现没效果不是改错了是压根没生效——得去 unit 文件里配。3.3 端口规划表含冲突检查端口规划要在装机前定死写进清单。TW8 涉及的主要端口有这么几类用途建议端口说明是否对外HTTP 业务端口8080应用对外服务主端口是HTTPS 业务端口8443需要 TLS 时启用是管理控制台9060部署、配置、监控入口内网限定集群通信9100多实例组集群时使用内网JMX 监控9999接监控采集用内网AJP 转发8009前置 Web 服务器转发时用内网端口冲突检查别只查netstat因为可能被其他命名空间或容器占着。我的做法是装之前跑一遍探测脚本对每个计划端口尝试 bind 一次能 bind 成功才算可用for p in 8080 8443 9060 9100 9999 8009; do if timeout 1 bash -c echo /dev/tcp/127.0.0.1/$p 2/dev/null; then echo PORT $p OCCUPIED else echo PORT $p FREE fi done还要提醒一句同一台机器上跑多个 TW8 实例多 domain时上面所有端口都要按实例偏移比如实例 1 用 8080/9060实例 2 用 8081/9061。这个偏移规则要写进清单不能临场心算因为一旦算错两个实例抢同一个端口后启动的那个直接失败而日志里的报错信息往往只写“Address already in use”不告诉你是哪个实例。4. 静默安装与实例初始化4.1 安装包解压式部署与静默参数TW8 的安装包通常提供图形化和命令行两种方式。生产环境一律走命令行因为图形化需要 X11 转发既不安全也不可复现。命令行安装的核心是一个响应文件也叫静默安装配置文件把安装路径、JDK 路径、管理端口、管理员账号这些参数一次性写进去然后非交互执行。# silent-install.properties INSTALL_PATH/opt/tw8 JAVA_HOME/usr/local/jdk11 HTTP_PORT8080 CONSOLE_PORT9060 ADMIN_USERtwadmin ADMIN_PASSWORD_FILE/opt/tw8-deploy/inventory/admin.pwd INSTALL_MODEconsole这里有两个细节值得说。第一密码不要明文写在响应文件里用ADMIN_PASSWORD_FILE指向一个权限 600 的文件脚本读取后注入。第二INSTALL_MODE选 console 而不是 GUI避免安装程序去探测显示环境。参数名在不同小版本之间可能有出入拿到包之后先用--help或查看包内的模板文件确认别硬套。心得安装完第一件事不是启动是把安装目录整个tar一份留作基线。后面如果配置改乱了可以直接对照基线找差异比对着文档猜快得多。我一般把这份基线放在/opt/tw8-deploy/packages/tw8-8.0.x/baseline.tar.gz。4.2 实例domain创建与最小可用验证一个安装目录下可以创建多个独立实例每个实例有自己的配置、应用、日志和端口。项目化部署里实例划分要跟业务划分对齐一个业务线一个实例还是按部署单元划分这个决定要提前定。我倾向按部署单元划分因为不同应用对 JVM 参数、连接池的要求差别很大混在一起只能取折中值谁都不满意。创建实例的命令行方式大致如下参数名以实际版本为准/opt/tw8/bin/domain.sh create \ --name app01 \ --base /opt/tw8/domains/app01 \ --http-port 8080 \ --console-port 9060 \ --jvm-opts -Xms4g -Xmx4g -XX:MetaspaceSize256m实例创建完先做最小可用验证不要急着传应用包。验证顺序是启动实例 → 看启动日志有没有 ERROR → 访问控制台 → 访问默认页 → 检查端口监听 → 检查日志目录是否正常写入。这五步任何一步不过都要停下来解决不要抱着“先传应用试试”的心态。因为一旦应用也传上去出了问题你就分不清是中间件的问题还是应用的问题。启动日志的检查有个技巧不要用grep -i error一把梭会抓到一堆无害的告警。更靠谱的是看“服务器启动完成”这类标志性日志有没有出现以及从启动开始到完成用了多久。正常情况下冷启动在几十秒级别如果超过两三分钟说明在某个环节卡住了常见是熵池不足、DNS 解析慢、类扫描过多。4.3 一个可复用的初始化脚本骨架把上面的步骤串起来就是一个初始化脚本。写脚本的要点是每一步都要可重入重复执行不产生副作用。比如创建用户前先判断是否存在创建目录用mkdir -p写配置前先备份。这样脚本跑到一半失败修完可以直接从头再跑一遍不用手工清理残局。#!/usr/bin/env bash set -euo pipefail INSTANCE${1:?usage: $0 instance-name} BASE/opt/tw8/domains/${INSTANCE} TPL/opt/tw8-deploy/templates ENV${DEPLOY_ENV:-prod} # 1. 目录准备可重入 mkdir -p ${BASE}/{conf,logs,temp} mkdir -p /data/tw8/logs/${INSTANCE} # 2. 配置合并common - env - overrides cp -f ${TPL}/common/*.properties ${BASE}/conf/ cp -f ${TPL}/env-${ENV}/*.properties ${BASE}/conf/ if [[ -f ${BASE}/conf/overrides.properties ]]; then echo using instance overrides fi # 3. 记录配置指纹 md5sum ${BASE}/conf/*.properties | sort -k2 ${BASE}/conf/.fingerprint # 4. 创建实例并启动 /opt/tw8/bin/domain.sh create --name ${INSTANCE} --base ${BASE} /opt/tw8/bin/domain.sh start ${INSTANCE} # 5. 等待健康 for i in $(seq 1 60); do if timeout 2 bash -c echo /dev/tcp/127.0.0.1/8080 2/dev/null; then echo instance ${INSTANCE} is up; exit 0 fi sleep 2 done echo start timeout 2; exit 1这个骨架里最值得学的是第 5 步的健康等待。很多脚本启动完就echo 成功其实服务还在初始化。加一段带超时的轮询才能真正判断成功与否。轮询间隔 2 秒、最长 120 秒这个取值是基于大多数 Java 应用冷启动时间来的如果你的应用特别重比如启动时加载几万个类的规则引擎可以调到 300 秒。5. 应用上线前的关键配置数据源、JVM、线程5.1 数据源与连接池参数怎么算连接池大小是 TW8 部署里最容易被拍脑袋决定的参数。给小了高并发下请求排队等连接响应时间飙升给大了数据库端连接数爆掉反而拖垮全局。正确的算法是从数据库侧倒推设单实例峰值 QPS 为 Q单次请求平均持有连接时间为 T 秒单位换算注意则理论上需要的并发连接数约为Q × T。举例Q 300T 0.02 秒即 20 毫秒需要 6 个连接。但这是理论值实际要给冗余因为 T 会有长尾。经验做法是在理论值基础上乘以 3 到 5得到初始值然后压测验证。所以这个例子初始值给 20 到 30 比较合理。再往上一层是数据库总连接数的约束。假设数据库max_connections是 800实际可用要扣掉管理连接和其他系统占用按 700 算你的应用有 8 个实例那么每实例的上限就是700 / 8 ≈ 87还要留一部分给批处理和其他应用所以每实例给到 40 到 50 是安全区。这个约束必须写进配置注释里否则将来扩容实例时没人会想起来回头改连接池直到数据库拒绝连接。其他几个参数的含义和取值建议参数含义建议取值说明初始连接数实例启动时预建连接最大值的 1/4避免启动瞬间批量建连最大连接数连接池上限按上面公式算与数据库容量联动最小空闲数保底空闲连接最大值的 1/8防止流量骤降后频繁重建获取连接超时等连接的最长时间3000~5000 ms超过就快速失败别无限等空闲检测间隔探活周期60~120 s配合验证语句使用连接最大存活时间强制回收时间1800 s避开数据库侧超时最后那个“最大存活时间”很关键但常被漏掉。数据库和中间的网络设备负载均衡、防火墙通常有连接空闲超时比如 30 分钟。如果连接池里的连接活得比它还久就会拿到一个“看起来活着实际已断”的连接报错五花八门。把连接最大存活时间设得比网络设备超时短一些比如 1800 秒对 3600 秒让应用自己主动换连接就能绕开这个坑。注意验证语句不要用复杂 SQL用最简单的SELECT 1之类。有些连接池在获取连接时会执行验证语句如果用重查询等于每次取连接都多跑一次业务查询性能直接砍半。5.2 JVM 堆与元空间的经验取值JVM 参数没有万能公式但有一批稳定的经验规则可以起步。第一条-Xms和-Xmx设成一样大。理由很实在——堆动态扩缩本身有成本而且在容器或虚拟化环境里堆反复增长容易触发操作系统层面的内存分配抖动。一次给够行为最可预测。第二条堆大小取决于机器内存和应用特性。通用比例是堆占物理内存的 50% 到 60%留出空间给元空间、线程栈、堆外内存Netty 直接内存、NIO buffer和操作系统页缓存。比如 16G 内存的机器堆给 8G 到 10G 比较稳不要给到 14G那样一旦有点堆外内存泄漏就是 OOM Killer 直接杀进程。第三条GC 选择。JDK 8 上堆小于 4G 用默认的并行收集器就够堆大于 4G 建议上 G1配合-XX:MaxGCPauseMillis200这个目标停顿时间。JDK 11 及以后默认就是 G1不用显式指定但可以考虑加-XX:UseStringDeduplication省点内存代价是少量 CPU。元空间的设置经常被忽略。默认元空间无上限一旦应用大量生成类比如热部署、动态代理、脚本引擎元空间会一直涨最终触发 Full GC 甚至 OOM。稳妥做法是设初始值 256M、上限 512M并且配合-XX:HeapDumpOnOutOfMemoryError保留现场。# 8G 堆 / 16G 内存机器的参考配置 -Xms8g -Xmx8g -XX:MetaspaceSize256m -XX:MaxMetaspaceSize512m -XX:MaxGCPauseMillis200 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/tw8/logs/heapdump -Xlog:gc*:file/data/tw8/logs/gc.log:time,uptime:filecount5,filesize50M最后一行日志参数在 JDK 8 上写法不同要用-Xloggc加-XX:PrintGCDetails选参数前先确认 JDK 大版本。堆转储路径要确保目录存在且用户有写权限否则真出事的时候转储文件生成失败等于白配。5.3 线程池与超时的联动关系业务线程池大小的经典估算线程数 ≈ QPS × 平均响应时间秒。500 QPS、平均 RT 200 毫秒算出来是 100 个线程。但这只是平均值长尾请求可能耗时几秒所以实际要留冗余或者更聪明的做法是按分位数算用 P99 响应时间代替平均值。500 QPS × P99 2 秒 1000 线程这个数字显然不现实说明系统在长尾上已经有问题了加大线程池只会掩盖问题。所以线程池调优的正确顺序是先看 RT 分布找出长尾在哪里再决定是优化慢查询、加缓存还是加线程。盲目加线程的后果是线程上下文切换开销上升吞吐量反而下降而且内存会吃紧每个线程栈默认 1M。超时设置要和线程池联动。假设线程池 200 个线程请求超时设 30 秒那么最坏情况下 200 个线程全部卡住 30 秒新的请求全部排队服务表现就是“假死”。合理做法是把超时压到业务能接受的最短时间比如 5 秒这样线程被占用 5 秒就释放系统有自我恢复能力。这个原则在中间件层面同样适用连接超时、读超时、请求超时都要设且要一层比一层短形成梯度。心得我排查“服务假死”类问题第一件事是抓线程栈看有多少线程卡在同一个地方。如果 200 个线程全卡在一个数据库读上那不是线程池的问题是那个查询或者那个连接池的问题。堆栈永远是最好的线索。6. 应用包部署与自动化发布6.1 三种部署方式的选择TW8 支持三种应用部署路径控制台上传、自动部署目录投放、命令行部署。三者各有适用场景别混用。控制台上传适合调试和紧急修复可视化能立刻看到部署结果和错误信息。缺点是没法自动化也不留痕。自动部署目录适合开发自测把包扔进autodeploy目录中间件自动加载。缺点是行为不够可控扫描周期、加载顺序都可能影响结果而且生产环境开着自动部署是很危险的——万一有人误传一个包进去直接就上线了没有任何审批。命令行部署适合生产可以写进脚本、进流水线结果可判断。我的项目里生产环境一律关掉自动部署只走命令行。开发环境可以开着方便。测试环境视情况如果测试团队需要频繁换包开着更省事但要在文档里写明。6.2 发布脚本与回滚脚本发布脚本的核心逻辑是新建版本目录 → 拷贝应用包 → 检查包完整性 → 停止实例如果必须 → 切换current软链接 → 启动 → 健康检查 → 失败自动回滚。写成脚本大概是这个形状#!/usr/bin/env bash set -euo pipefail APP${1:?need app name} PKG${2:?need package path} VER${3:?need version} ROOT/opt/tw8/apps/${APP} NEW${ROOT}/${VER} # 校验包完整性 sha256sum -c ${PKG}.sha256 || { echo package broken; exit 1; } mkdir -p ${NEW} unzip -q -o ${PKG} -d ${NEW} [[ -f ${NEW}/WEB-INF/web.xml ]] || { echo bad package structure; exit 1; } # 记录上一个版本 PREV$(readlink -f ${ROOT}/current || true) [[ -n ${PREV} ]] ln -sfn ${PREV} ${ROOT}/previous # 切换 ln -sfn ${NEW} ${ROOT}/current /opt/tw8/bin/domain.sh restart app01 || true # 健康检查 for i in $(seq 1 30); do code$(curl -s -o /dev/null -w %{http_code} http://127.0.0.1:8080/health || true) [[ ${code} 200 ]] { echo deploy ok; exit 0; } sleep 3 done # 回滚 echo health check failed, rolling back [[ -n ${PREV} ]] ln -sfn ${PREV} ${ROOT}/current /opt/tw8/bin/domain.sh restart app01 exit 1几个地方值得解释。包完整性校验不是形式主义网络传输截断导致 war 包损坏的情况我遇到过不止一次表现是部署报“zip 文件格式错误”如果没有校验你会怀疑是中间件有问题。检查WEB-INF/web.xml存在是在验证包结构防止有人传了个空的或者目录结构不对的包。自动回滚则是最后一道保险宁可回滚也不要让一个有问题的版本在线跑着。回滚脚本本身要简单到“闭着眼睛也能跑”。我的回滚脚本只有十几行读previous切current重启健康检查。不加任何花哨逻辑因为回滚往往发生在半夜、人手紧张的时候脚本越简单越可靠。6.3 灰度与并行目录切换如果业务允许灰度发布比全量重启友好得多。做法是同一台机器上起两个实例端口不同前置负载均衡按权重分流先给新实例 10% 流量观察一段时间再逐步加。TW8 支持多实例共存只要端口和实例目录分开就行。灰度的关键是会话处理。如果应用把会话存在本地内存那么用户在两台实例之间跳转就会掉登录。解决方案有两个一是会话粘滞负载均衡按 cookie 或源 IP 转发简单但不够优雅二是会话共享把会话持久化到共享存储或做成无状态 token更彻底但要改应用。我一般建议新项目从一开始就做无状态别依赖本地会话老项目改造代价大的先用粘滞过渡。这个决定要写在部署文档里因为它是“部署架构”层面的约束不是某个实例的参数。并行目录切换还有一个好处是回滚成本极低。新旧实例同时在跑切回去只是改一下负载均衡的权重连重启都不用。这也是我做重要版本发布时最愿意采用的方案。7. 启停托管、日志与巡检7.1 systemd 托管与优雅停机用nohup起中间件是能跑但不管重启、不管资源限制、不管优雅停机。生产环境应该用 systemd 托管。一个可用的 unit 大概长这样[Unit] DescriptionTW8 app01 Afternetwork.target [Service] Typeforking Usertwuser Grouptwgrp EnvironmentJAVA_HOME/usr/local/jdk11 LimitNOFILE65535 ExecStart/opt/tw8/bin/domain.sh start app01 ExecStop/opt/tw8/bin/domain.sh stop app01 TimeoutStopSec90 Restarton-failure RestartSec10 [Install] WantedBymulti-user.target有两个参数要注意。TimeoutStopSec默认是 90 秒但有些应用的优雅停机等正在处理的请求完成、关闭连接池、刷盘需要更长时间如果超时时间太短systemd 会直接SIGKILL表现为日志里没有停机记录、数据可能有残留。我一般设 120 到 180 秒具体看应用最长的请求处理时间。LimitNOFILE必须显式配前面提过systemd 不读limits.conf。这个坑很隐蔽因为你用ulimit -n在登录 shell 里看到的是 65535但服务进程实际拿到的是 1024只有查看/proc/pid/limits才能发现。注意Typeforking要求启动脚本正确地 fork 并交出主进程 PID否则 systemd 追踪不到真实进程。如果发现systemctl status显示 active 但进程实际已经挂了多半是这里的问题改成Typesimple配合前台启动参数通常能解决。7.2 日志分级、切割与保留策略日志要分三类中间件日志启动、部署、异常、应用日志业务输出、访问日志请求记录。三类放在不同目录用不同策略切割。混在一起是灾难因为业务日志量大会把中间件的错误信息淹没。切割策略中间件日志按天或按大小切保留 15 到 30 天访问日志按天切保留 7 到 15 天应用日志按天切保留 7 天。这些数字不是随便定的取决于磁盘容量和排查需求。如果一块 200G 的盘日增日志 5G那最多只能留 30 天还得留出余量应对异常时日志暴涨。切割工具有两种选择用中间件自带的轮转能力如果支持或者用系统层面的定时任务 脚本。我倾向于统一用系统层的方案好处是策略一致、好统一管理坏处是需要写脚本。脚本核心就是按日期重命名 删除超期文件 通知进程重开文件句柄如果用logrotate的copytruncate模式则不用通知但会有丢日志风险。磁盘水位必须监控。我的经验阈值是 80% 告警、90% 严重告警。因为日志写满磁盘导致的故障是所有故障里最冤的一种——它完全可预防而且一旦发生中间件会因为写日志失败而出现各种奇怪的行为排查起来方向完全跑偏。7.3 每日巡检清单巡检不是为了写报告是为了提前发现问题。清单要短长了没人做。我一般定六项检查项方法阈值处置实例进程存活进程检查必须存在不存在立即上报端口监听端口探测必须监听检查启动日志磁盘使用率df 80%清理日志堆使用率JMX 或 GC 日志 75%观察趋势准备扩容错误日志增量当日 ERROR 计数无突增抽样查看健康接口HTTP 探测200分析应用日志这里面“错误日志增量”最容易做出价值。做法是每天固定时间统计前 24 小时的 ERROR 数量和过去 7 天的均值比较如果涨了 3 倍以上就告警。这样能抓到那种“每天多几条一周后爆发”的慢性问题。单纯的“有没有 ERROR”告警太粗正常的应用也不可能零错误告警疲劳之后就被无视了。8. 常见问题与排查实录8.1 启动阶段的高频故障启动阶段的问题相对好查因为有明确的时间点和日志。最典型的三类第一类是端口占用报错很直白Address already in use。排查方法是ss -lntp找到占用进程。但要注意有时候占用者是你自己上一次没停干净的实例。所以启停脚本里要有“启动前检查是否已有同实例进程”的逻辑发现残留就先处理而不是硬启。第二类是权限问题表现为日志目录写不进去、临时目录不可写。Permission denied报错有时候藏在几百行日志中间。快速定位方法是用同一用户身份手工执行一次启动看哪个路径报错。第三类是类加载失败报ClassNotFoundException或NoClassDefFoundError。这个问题的根因通常是依赖冲突——应用自带了一个某个库的版本中间件自身也带了一个类加载器优先级导致加载了错误的版本。解决办法是通过中间件的类加载配置把某些包设为“优先使用应用侧”或“优先使用中间件侧”。这也是为什么我一直建议用干净的环境部署别把测试机的lib目录混进来。心得遇到类冲突先别急着改配置。把-verbose:class打开看看到底加载了哪个 jar 里的类、从哪个路径来的。这一步能省掉 80% 的猜测。8.2 运行期的内存、连接与慢请求运行期问题难查因为没有明确的失败点表现往往是“变慢”或“偶发失败”。内存问题看 GC 日志如果老年代回收后占用仍持续上升说明有对象泄漏如果 Full GC 频繁但回收后能降下来说明堆不够。前者要找代码后者要加堆。连接问题看连接池指标活跃连接数长期贴着上限说明池子小了或者有慢 SQL 占着连接不放活跃连接数很低但请求超时说明获取连接这一步就卡住了可能是数据库侧有问题。这两个现象看起来都是“慢”但根因完全相反不能一概而论。慢请求的排查工具是线程栈采样。连续采三到五次每次间隔几秒如果多次采样里同一批线程都卡在同一个方法上那就是热点。这个方法的成本极低jstack一条命令但效果非常好尤其在没法接 APM 的环境里。# 连续采样观察线程堆积位置 for i in 1 2 3 4 5; do jstack -l $(pgrep -f tw8.*app01 | head -1) /tmp/js-$i.txt sleep 5 done grep -A3 java.lang.Thread.State /tmp/js-*.txt | grep at | sort | uniq -c | sort -rn | head -20这段脚本跑完你会拿到出现频次最高的调用栈位置。如果前面是数据库驱动的方法那问题在 SQL如果是某个业务方法自旋那是代码逻辑如果是Object.wait或者park那是在等锁或者等 IO。8.3 排查速查表把常见现象和可能原因整理成表方便现场快速定位现象优先排查常见根因启动超时无响应启动日志末尾类扫描过多、DNS 解析慢、熵池不足启动报端口占用ss -lntp残留进程、端口规划冲突部署报包格式错误包 sha256传输截断、打包工具不兼容应用能起但 404部署目录结构上下文路径不匹配、包结构不符偶发连接超时连接池指标池太小、慢 SQL、连接失效内存持续上涨GC 日志对象泄漏、缓存无上限线程数触顶jstack 采样下游超时未设、锁竞争磁盘写满df -h日志未轮转、堆转储文件堆积速查表的价值在于把“从哪开始看”这个最耗时的决策标准化。现场排障最怕的不是不会解决而是不知道先看什么东看一下西看一下半小时就没了。9. 升级、回滚与交付物清单9.1 小版本升级该按什么顺序走中间件小版本升级比如从 8.0.x 到 8.0.y看起来简单但操作顺序错了照样出事。我固定的顺序是先在测试环境用生产配置的副本完整走一遍包括应用部署和压力验证确认没问题后在生产环境按实例逐个升级绝不批量操作每个实例升级前先备份配置目录和当前应用版本目录升级后跑健康检查通过再动下一个。逐个升级而不是批量是因为万一新版本有兼容性问题你只损失一个实例的可用性还有回旋余地。批量升级一旦出问题就是全站故障而且这时候你连回滚都要排队做。升级前的备份要说清楚备什么配置目录含所有 properties、应用版本目录、JDK 路径信息、systemd unit 文件。这四样齐了理论上可以在半小时内完全恢复到升级前状态。至于中间件安装目录本身如果包不大也一并备份更保险。9.2 交付物清单与验收要点一个合格的 TW8 项目化部署交付物我通常列这么几项缺一项都算没交付完交付物内容验收要点部署资产包脚本、模板、清单能在干净机器上一次跑通配置基线各环境最终配置及差异说明配置指纹可复现端口与资源规划表端口、内存、磁盘分配与实际情况一致启停与巡检脚本systemd unit、巡检清单实际执行过并留记录故障处理手册常见问题与处置步骤至少覆盖速查表全部条目版本记录中间件版本、应用版本、变更历史可追溯每次变更验收时我最看重的不是文档写得多漂亮而是能不能现场复现。让交付方在一台新机器上按资产包跑一遍跑得通才算数。文档里写“修改配置文件中的数据库地址”但没说改哪个文件、哪个键名这种交付物在真实场景里毫无价值——接手的人还是要打电话问。还有一条容易被忽略清单文件里要记录每台机器的实例名、端口、内存分配、所属环境、负责人。这份清单是运维日常工作的索引比任何文档都常用。我的习惯是把它做成一个简单的 CSV 或者 properties 文件跟着资产包一起进版本库每次扩容或变更都提交一次形成天然的变更历史。最后分享一个我踩过不止一次的坑部署脚本里千万别用相对路径。cd到某个目录再执行脚本看似没问题但只要有人从别的目录调用或者被 CI 以不同工作目录执行所有相对路径全部失效报错还特别难懂。所有路径用绝对路径或者在脚本开头强制cd $(dirname $0)。这行代码很短但能省掉很多莫名其妙的“文件找不到”。