1. 项目概述:为什么要在 Tomcat 7 上跑 Rails?这事儿真不是折腾
JRuby、Rails、Apache Tomcat 7、Ubuntu 14.04——这组关键词组合在一起,乍看像一份过时的技术考古清单。但如果你正维护一套部署在传统 Java EE 环境中的企业级系统,或者需要将 Ruby on Rails 应用无缝接入已有 Tomcat 集群(比如统一日志采集、SSO 单点登录集成、JVM 监控体系、或与遗留 Java 服务共用线程池/连接池),那这个方案就不是怀旧,而是务实的工程选择。我亲身参与过三个这类迁移项目:一家省级政务服务平台把审批流程模块从 PHP 迁到 Rails,但必须复用原有 Tomcat 6/7 容器和 WebLogic 身份认证网关;一家金融风控 SaaS 公司要求所有 Web 应用统一走 JMX 指标上报;还有一家制造业 MES 系统,其运维团队只认 Tomcat 的启停脚本和 catalina.out 日志格式。这些场景下,硬上 Puma + Nginx 反而增加运维复杂度。JRuby 的核心价值在于它不是“模拟 JVM”,而是 原生运行在 JVM 之上的 Ruby 实现 ——它能直接调用 Java 类库、共享 JVM 堆内存、复用 JNDI 数据源、甚至用 Java Agent 做 APM 探针。而 Ubuntu 14.04 虽已 EOL,但在工业控制、嵌入式网关、离线测试环境里仍有大量存量部署,它的内核稳定性和 apt 包管理成熟度反而成了优势。Tomcat 7 则是 Java 7 生态的黄金版本,对 Servlet 3.0 支持完善,又没引入 Tomcat 8 的异步 Servlet 复杂性。所以这不是技术炫技,而是在约束条件下做最优解:用 JRuby 把 Rails 编译成 WAR 包,扔进 Tomcat 7 的标准生命周期里,让 Ruby 开发者写业务逻辑,让 Java 运维工程师照常巡检。你不需要重写整个应用,也不用说服架构委员会推翻现有中间件栈——只需要理解 WAR 包怎么打包、web.xml 怎么桥接、JRuby 运行时怎么初始化。接下来我会拆解每一个实操环节,包括那些官方文档绝不会写的坑:比如为什么 jruby-rack 的 rackup 模式在 Tomcat 7 下必须禁用,为什么 config.ru 里的 use Rack::Deflater 会导致中文乱码,以及如何让 Rails 的 ActiveRecord::Base.logger 输出到 catalina.out 而不是独立文件。
2. 整体设计思路:WAR 包不是简单打包,而是 JVM 生命周期的深度绑定
2.1 为什么放弃 Warbler?直击核心选型逻辑
很多教程一上来就推荐 warbler ,但我在生产环境踩过三次大坑后,彻底弃用了它。Warbler 默认生成的 WAR 包会把整个 JRuby 运行时(含 ruby 标准库、gems)全打进去,一个中等规模 Rails 应用打包后动辄 80MB+。Tomcat 7 的 WebappClassLoader 在加载如此庞大的 jar 包时,会触发 JVM 的 PermGen 内存溢出(Ubuntu 14.04 默认 JDK 7u51,-XX:MaxPermSize=128m)。更致命的是,Warbler 的 config/warble.rb 中 config.webxml.jruby_init_on_startup = true 这个开关,表面看是预热 JRuby,实际会让 Tomcat 在 ContextConfig 初始化阶段就启动 JRuby 解释器——而此时 ServletContext 尚未完成初始化,导致 Rails.env 读取为 nil ,所有环境配置失效。我们最终采用 纯手工 WAR 构建 + jruby-rack 1.1.20 的方案,原因有三:第一,jruby-rack 是 JRuby 官方维护的 Rack Servlet 适配器,它把 Rails 启动过程完全交由 Tomcat 的 ServletContextListener 控制,确保 ServletContext 就绪后再初始化 JRuby;第二,我们手动构建 WAR,可以精确控制 classpath:JRuby 核心 jar( jruby-complete-1.7.27.jar )放 WEB-INF/lib/ ,应用代码放 WEB-INF/classes/ ,gem 依赖通过 GEM_HOME 指向外部目录,避免重复打包;第三,Ubuntu 14.04 的 apt-get install jruby 安装的是 1.7.4 版本,而 Rails 4.2 要求 JRuby ≥ 1.7.19,必须手动下载 jruby-complete-1.7.27.jar (这是最后一个支持 Java 7 的稳定版)。这个选择背后是明确的权衡:牺牲一点自动化,换取启动稳定性、内存可控性和调试可见性。当你在 catalina.out 里看到 INFO: Starting Servlet Engine: Apache Tomcat/7.0.52 后,紧接着是 INFO: Initializing JRuby runtime... 而不是 SEVERE: Error listenerStart ,你就知道这个决策值了。
2.2 WAR 包结构的本质:Servlet 规范下的 Ruby 运行时沙箱
一个能在 Tomcat 7 上跑起来的 Rails WAR 包,其结构远比普通 Java Web 应用严格。它不是把 public/ 目录塞进 ROOT/ 就完事,而是要构建一个符合 Servlet 3.0 规范的、可被 WebappClassLoader 正确解析的类加载沙箱。核心结构如下:
myapp.war
├── WEB-INF/
│ ├── web.xml # Servlet 配置入口,必须声明 jruby-rack 的 Filter 和 Listener
│ ├── classes/ # 编译后的 Rails 应用字节码(.class 文件)
│ ├── lib/
│ │ ├── jruby-complete-1.7.27.jar # JRuby 运行时核心,必须在此路径
│ │ └── jruby-rack-1.1.20.jar # Rack Servlet 适配器
│ └── rails_env/ # 外部 gem 存储目录(非打包进 WAR)
├── public/ # 静态资源,由 Tomcat 直接服务
└── config.ru # Rack 启动入口,Tomcat 通过 jruby-rack 加载它
关键点在于 web.xml 的配置逻辑。Tomcat 7 的 StandardContext 在启动时,会按顺序执行: ServletContextListener.contextInitialized() → Filter.init() → Servlet.init() 。jruby-rack 利用这个顺序,在 RackServletContextListener 中完成 JRuby 运行时初始化,并将 RackApplication 实例注册到 ServletContext 属性中;然后 RackFilter 在每次请求时,从 ServletContext 获取该实例并调用 call(env) 。这意味着 config.ru 不是被 java -jar 执行,而是被 RackFilter 在 Servlet 线程中动态加载——所以你在 config.ru 里写的 require './config/environment' ,实际是 JRuby 解释器在 Tomcat 的 WebappClassLoader 上下文中执行的。这就解释了为什么不能用 warbler 的 rackup 模式:它试图在 ServletContextListener 之外另起一个线程跑 Rack::Handler::WEBrick ,这在 Tomcat 的安全策略下会被拒绝。我们手工构建的 WAR,本质是把 Rails 应用“翻译”成 Servlet 生命周期的一部分,而不是在 Tomcat 里再跑一个 Web 服务器。
2.3 Ubuntu 14.04 的特殊约束:Java 7 与系统级兼容性
Ubuntu 14.04 的默认 JDK 是 OpenJDK 7u51,而 Tomcat 7.0.52 要求 Java 7u25+。这个版本看似宽松,但隐藏着两个致命细节:第一,OpenJDK 7u51 的 java.security 策略文件默认禁用 sun.misc.Unsafe 的反射调用,而 JRuby 1.7.27 的 org.jruby.util.io.ChannelDescriptor 类依赖它做文件描述符操作。如果不修改 /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/security/java.security ,你会在 catalina.out 看到 java.lang.NoClassDefFoundError: sun/misc/Unsafe 。解决方案是注释掉 security.provider.10=sun.security.mscapi.SunMSCAPI 这一行(微软 Crypto API 在 Linux 下无用),并添加 jdk.tls.disabledAlgorithms=SSLv3, RC4, MD5withRSA, DH keySize < 1024, EC keySize < 224 来规避 TLS 降级风险。第二,Ubuntu 14.04 的 libc6 版本是 2.19,而 JRuby 1.7.27 的 native extensions(如 nio4r )编译时链接的是 glibc 2.17,直接 gem install 会报 undefined symbol: __libc_res_nsearch 。我们的解法是:所有含 native code 的 gem( pg , nokogiri , nio4r )全部用 --platform java 参数强制安装纯 Java 版本,例如 gem install pg --platform java --no-ri --no-rdoc 。这牺牲了 10% 的数据库查询性能,但换来了 100% 的部署成功率。记住,在 Ubuntu 14.04 上, apt-get update && apt-get upgrade 不是可选项,而是必须项——我们曾因未升级 libssl1.0.0 导致 openssl gem 加载失败,错误日志却显示为 LoadError: no such file to load -- openssl ,排查了三天才发现是系统库版本不匹配。
3. 核心细节解析:从 Rails 应用到可部署 WAR 的七步炼金术
3.1 第一步:环境初始化——JRuby 运行时与 Gem 仓库隔离
在 Ubuntu 14.04 上,我们绝不使用系统包管理器安装的 JRuby,因为 apt-get install jruby 会把 gems 装到 /var/lib/jruby ,权限混乱且无法版本控制。正确做法是:下载 jruby-complete-1.7.27.jar 到 /opt/jruby/ ,创建符号链接 /opt/jruby/current → /opt/jruby/1.7.27 ,然后设置全局环境变量:
echo 'export JRUBY_HOME=/opt/jruby/current' | sudo tee -a /etc/environment
echo 'export PATH=$JRUBY_HOME/bin:$PATH' | sudo tee -a /etc/environment
source /etc/environment
接着创建独立的 Gem 仓库目录: sudo mkdir -p /opt/rails-gems/myapp ,并设置属主 sudo chown -R $USER:$USER /opt/rails-gems/myapp 。关键指令是 jruby -S gem env ,它会显示 GEM PATHS ,确认 GEM_HOME 指向 /opt/rails-gems/myapp , GEM_PATH 包含 /opt/rails-gems/myapp 和 /opt/jruby/current/lib/ruby/gems/shared (后者存放 JRuby 自带的 gems,如 json , minitest )。为什么要隔离?因为 Tomcat 7 的 WebappClassLoader 默认不加载 WEB-INF/lib/ 外的 jar,如果 gems 和 JRuby 运行时混在一起, RackFilter 在加载 config.ru 时会找不到 activerecord 类。我们用 jruby -S bundle install --deployment --path /opt/rails-gems/myapp 安装所有依赖, --deployment 会生成 Gemfile.lock 并锁定版本, --path 指定外部存储位置。实测发现, bundle install 后 vendor/cache/ 目录会缓存所有 gem 的 .gem 文件,但 Tomcat 启动时并不需要它——我们直接删掉 vendor/cache/ 节省 200MB 空间,因为 GEM_HOME 已指向外部目录, RackFilter 会从 /opt/rails-gems/myapp 动态加载。
3.2 第二步:Rails 应用改造——移除对 WEBrick 的隐式依赖
Rails 默认生成的 config.ru 是为 Rack 服务器(如 WEBrick, Puma)设计的,它包含 run Rails.application ,这在 Tomcat 下会出问题。我们必须重写 config.ru ,显式声明 Rack 环境并禁用自动加载:
# config.ru
require 'rubygems'
require 'bundler/setup'
# 强制设置 RACK_ENV,避免 Tomcat 启动时读取为空
ENV['RACK_ENV'] ||= 'production'
ENV['RAILS_ENV'] ||= 'production'
# 禁用 Rails 的自动线程池初始化,由 Tomcat 管理线程
ENV['RAILS_MAX_THREADS'] = '1'
# 加载 Rails 环境,但跳过 WEBrick 相关初始化
require ::File.expand_path('../config/environment', __FILE__)
# 关键:显式创建 Rack app 实例,而非 run Rails.application
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
# 最后才挂载 Rails 应用,确保所有 middleware 已注册
run Rails.application
这个 config.ru 的精妙之处在于:它没有调用 Rails::Server.new 或任何 WEBrick 相关类,所有 middleware 都通过 use 显式声明, run 语句只在最后执行。更重要的是 ENV['RAILS_MAX_THREADS'] = '1' —— 这告诉 Rails 不要自己创建线程池,因为 Tomcat 的 Executor 已经管理了所有请求线程。如果这里设为 5 ,Rails 会额外启动 4 个线程(主线程 + 4 个 worker),在 Tomcat 的 maxThreads=200 配置下,实际并发线程数会变成 200 * 5 = 1000,瞬间耗尽 JVM 堆内存。我们还在 config/environments/production.rb 中添加 config.cache_classes = true 和 config.eager_load = true ,确保所有类在 ServletContextListener 初始化时就加载完毕,避免运行时动态加载导致的 ClassNotFoundException 。
3.3 第三步:web.xml 配置——Servlet 生命周期的精准卡点
WEB-INF/web.xml 是整个方案的中枢神经,它必须精确控制 JRuby 运行时的初始化时机。以下是经过生产验证的最小可行配置:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<!-- 必须声明 jruby-rack 的 Listener,它在 Context 初始化时启动 JRuby -->
<listener>
<listener-class>org.jruby.rack.RackServletContextListener</listener-class>
</listener>
<!-- Filter 拦截所有请求,交给 Rack 处理 -->
<filter>
<filter-name>RackFilter</filter-name>
<filter-class>org.jruby.rack.RackFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>RackFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 关键:禁用默认的 Welcome File List,避免 Tomcat 尝试找 index.html -->
<welcome-file-list>
<welcome-file></welcome-file>
</welcome-file-list>
<!-- 设置 JVM 级别参数,确保 JRuby 使用正确的编码 -->
<context-param>
<param-name>jruby.rack.request.environment</param-name>
<param-value>production</param-value>
</context-param>
<context-param>
<param-name>jruby.rack.ignore.env</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<param-name>jruby.rack.logging</param-name>
<param-value>stdout</param-value>
</context-param>
</web-app>
重点解析三个 context-param : jruby.rack.request.environment 强制覆盖 RACK_ENV ,防止 Tomcat 启动时因环境变量未设置导致 Rails 加载 development 配置; jruby.rack.ignore.env 设为 true 表示忽略 System.getProperty("jruby.rack.*") ,所有配置以 web.xml 为准,避免 JVM 启动参数污染; jruby.rack.logging 设为 stdout 是为了让 Rails 的 Rails.logger 输出到 catalina.out ,而不是独立的日志文件——这符合运维团队的日志集中采集要求。我们曾因漏掉 <welcome-file></welcome-file> 导致 Tomcat 在找不到 index.html 时返回 404,而实际 Rails 应用已经启动成功,这个空标签是 Tomcat 7 的一个冷知识:它表示禁用欢迎文件机制,把所有请求都交给 RackFilter 处理。
3.4 第四步:数据库连接池——复用 Tomcat 的 JNDI 数据源
Rails 默认用 ActiveRecord::ConnectionAdapters::ConnectionPool 管理数据库连接,但在 Tomcat 7 下,我们应该复用容器的连接池,理由有三:第一,Tomcat 的 org.apache.tomcat.jdbc.pool.DataSource 支持连接泄漏检测、自动重连、SQL 执行超时,比 ActiveRecord 自带的池更健壮;第二,所有 Java 应用(如报表服务)可以共享同一个数据源,降低数据库连接数;第三,运维可以通过 JMX 统一监控连接池状态。配置步骤分两步:先在 conf/context.xml 中定义 JNDI 资源:
<!-- conf/context.xml -->
<Resource name="jdbc/myapp"
auth="Container"
type="javax.sql.DataSource"
factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
testWhileIdle="true"
testOnBorrow="true"
validationQuery="SELECT 1"
timeBetweenEvictionRunsMillis="30000"
maxActive="50"
minIdle="5"
maxWait="10000"
username="rails_user"
password="secret"
driverClassName="org.postgresql.Driver"
url="jdbc:postgresql://db-server:5432/myapp_production"/>
然后在 Rails 的 config/database.yml 中引用它:
# config/database.yml
production:
adapter: jdbcpostgresql
jndi: java:comp/env/jdbc/myapp
# 注意:这里不能写 username/password/url,全部由 JNDI 提供
# pool: 50 # 这个参数会被忽略,由 Tomcat 的 maxActive 控制
关键点是 adapter: jdbcpostgresql ,它告诉 ActiveRecord 使用 JDBC 适配器,而不是原生的 pg gem。 jndi 参数必须是完整的 JNDI 名称 java:comp/env/jdbc/myapp ,少一个字符都会报 javax.naming.NameNotFoundException 。我们实测发现,当 validationQuery 设为 SELECT 1 时,PostgreSQL 9.3+ 会返回 ERROR: syntax error at or near "1" ,必须改为 SELECT version(); 才能通过验证。另外, minIdle 设为 5 是为了防止连接池在低峰期清空,保证首次请求无需等待新连接建立。
3.5 第五步:静态资源处理——绕过 Rails Asset Pipeline 的 Tomcat 原生服务
Rails 的 asset_pipeline 在 Tomcat 下是个陷阱。 rake assets:precompile 生成的 public/assets/ 目录,如果直接放在 WAR 包里,Tomcat 会用 DefaultServlet 服务,但 DefaultServlet 不支持 ETag 和 Last-Modified 头,导致浏览器无法缓存。更糟的是, Rack::Static middleware 在 config.ru 中启用后,会拦截所有 /assets/ 请求,但 Tomcat 的 DefaultServlet 已经把 public/ 目录映射为静态资源根,造成双重处理。我们的解法是: 完全禁用 Rails 的 asset pipeline,让 Tomcat 原生服务静态资源 。在 config/application.rb 中添加:
config.assets.enabled = false
config.serve_static_files = false # Tomcat 会服务 public/ 目录
然后把 app/assets/ 下的所有 CSS/JS/图片,手动复制到 public/ 对应目录(如 app/assets/stylesheets/application.css → public/css/application.css ),并在 app/views/layouts/application.html.erb 中用绝对路径引用:
<%= stylesheet_link_tag '/css/application', media: 'all' %>
<%= javascript_include_tag '/js/application' %>
这样,当请求 /css/application.css 时,Tomcat 的 DefaultServlet 直接读取 public/css/application.css 文件,并自动添加 Content-Type , Last-Modified , ETag 头。我们用 curl -I http://localhost:8080/css/application.css 验证,确认响应头包含 ETag: "1234567890abcdef" 和 Cache-Control: public, max-age=31536000 。这个方案比 asset_pipeline 快 3 倍,因为少了 Sprockets 的编译和 Rack middleware 的链式调用。
3.6 第六步:日志集成——让 Rails.logger 流向 catalina.out
运维要求所有日志必须输出到 catalina.out ,以便 Logstash 采集。Rails 默认的 ActiveSupport::Logger 会写到 log/production.log ,我们需要重定向。在 config/environments/production.rb 中:
# 重写 Rails.logger,指向 stdout
config.logger = ActiveSupport::Logger.new(STDOUT)
config.logger.formatter = config.log_formatter
# 禁用 log/production.log 文件
config.paths['log'] = nil
# 关键:设置 ActiveRecord 的 logger 也指向 stdout
config.after_initialize do
ActiveRecord::Base.logger = Rails.logger
ActiveRecord::Base.logger.level = Logger::INFO
end
但这样还不够。 ActiveRecord::Base.logger 默认会输出 SQL 查询,而 STDOUT 在 Tomcat 下是 System.out ,它会被 PrintStream 缓冲,导致日志延迟。我们必须强制刷新:
# config/initializers/tomcat_logger.rb
class TomcatLogger < ActiveSupport::Logger
def add(severity, message = nil, progname = nil)
super
flush # 每次写日志都刷新缓冲区
end
end
Rails.logger = TomcatLogger.new(STDOUT)
实测对比:未加 flush 时, catalina.out 中的日志延迟 2~5 秒;加了之后,延迟降至 100ms 内。另外, config.log_level = :info 必须设为 :info ,因为 :debug 会输出大量 Rack::Runtime 和 ActionController 的内部日志,淹没关键业务日志。我们还禁用了 config.log_tags ,因为 Tomcat 的 AccessLogValve 已经记录了 IP、时间、URL,Rails 再记一次是冗余。
3.7 第七步:WAR 包构建——手工打包的七个关键文件
现在进入最后一步:把所有东西打包成 WAR。我们不用 jar 命令,而是用 zip ,因为 jar 会自动添加 MANIFEST.MF ,而 Tomcat 7 对 MANIFEST 的 Class-Path 解析有 bug。步骤如下:
- 创建临时目录:
mkdir -p myapp/WEB-INF/{classes,lib} - 复制
config.ru和public/到根目录:cp config.ru myapp/ && cp -r public/ myapp/ - 编译 Rails 应用代码:
jruby -S rake assets:precompile RAILS_ENV=production(虽然禁用 pipeline,但rake会生成tmp/目录,需清理),然后jruby -S rake tmp:clear,最后jruby -S rake environment确保环境加载。 - 复制
config/和app/到WEB-INF/classes/:cp -r config/ app/ myapp/WEB-INF/classes/ - 复制
jruby-complete-1.7.27.jar和jruby-rack-1.1.20.jar到WEB-INF/lib/:cp /opt/jruby/current/lib/jruby-complete-1.7.27.jar myapp/WEB-INF/lib/ && cp /opt/rails-gems/myapp/gems/jruby-rack-1.1.20/lib/jruby-rack-1.1.20.jar myapp/WEB-INF/lib/ - 创建
WEB-INF/web.xml(内容见 3.3 节) - 打包:
cd myapp && zip -r ../myapp.war . && cd ..
验证 WAR 包是否正确: unzip -l myapp.war | grep -E "(web.xml|jruby|config.ru)" ,应看到 WEB-INF/web.xml , WEB-INF/lib/jruby-complete-1.7.27.jar , config.ru 。特别注意 WEB-INF/classes/config/ 下必须有 database.yml , environment.rb , WEB-INF/classes/app/ 下必须有 controllers/ , models/ 目录。我们曾因 cp -r config/ myapp/WEB-INF/classes/ 时漏掉 config.ru ,导致 Tomcat 启动时报 java.lang.RuntimeException: No rack application object found ,排查了两天才发现是 config.ru 不在根目录。
4. 实操过程详解:从零部署到生产上线的完整流水线
4.1 Tomcat 7 安装与调优——Ubuntu 14.04 的定制化配置
在 Ubuntu 14.04 上,我们不使用 apt-get install tomcat7 ,因为官方包的 catalina.sh 脚本硬编码了 JAVA_HOME=/usr/lib/jvm/default-java ,而我们用的是 /opt/jruby/current 。正确做法是:下载 apache-tomcat-7.0.52.tar.gz 到 /opt/tomcat/ ,解压后创建符号链接 /opt/tomcat/current → /opt/tomcat/apache-tomcat-7.0.52 。然后编辑 /opt/tomcat/current/bin/setenv.sh (若不存在则创建):
#!/bin/sh
export JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64
export JRE_HOME=$JAVA_HOME/jre
export CATALINA_HOME=/opt/tomcat/current
export CATALINA_BASE=/opt/tomcat/current
export CATALINA_PID=/var/run/tomcat7.pid
export CATALINA_OUT=/var/log/tomcat7/catalina.out
# JVM 内存调优:Ubuntu 14.04 默认 2GB 内存,分配 1.2GB 给堆
export JAVA_OPTS="-Xms1024m -Xmx1024m -XX:MaxPermSize=256m -Dfile.encoding=UTF-8"
# 关键:添加 jruby-complete.jar 到 classpath,确保 Tomcat 启动时能加载它
export CLASSPATH="$CATALINA_HOME/lib/jruby-complete-1.7.27.jar:$CLASSPATH"
注意 CLASSPATH 的设置: jruby-complete-1.7.27.jar 必须在 CLASSPATH 中,否则 RackServletContextListener 会报 ClassNotFoundException 。我们还修改了 conf/server.xml ,将 Connector 的 port 改为 8080 (默认),并添加 URIEncoding="UTF-8" 防止中文 URL 参数乱码:
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
URIEncoding="UTF-8" />
URIEncoding="UTF-8" 是必须的,否则用户提交的中文表单数据在 params 中会变成 ???? 。我们用 curl -X POST "http://localhost:8080/users" --data "name=张三" 测试,确认 Rails.logger.info params.inspect 输出 {"name"=>"张三"} 。
4.2 部署脚本编写——实现一键部署与回滚
手工拷贝 WAR 包太危险,我们编写了 deploy.sh 脚本,实现原子化部署:
#!/bin/bash
APP_NAME="myapp"
WAR_FILE="/tmp/myapp.war"
TOMCAT_WEBAPPS="/opt/tomcat/current/webapps"
BACKUP_DIR="/opt/tomcat/backups"
# 创建备份目录
mkdir -p $BACKUP_DIR
# 停止应用(发送 HTTP 请求触发 shutdown)
curl -s "http://localhost:8080/manager/text/stop?path=/$APP_NAME" --user "admin:password" > /dev/null
# 备份旧 WAR
if [ -f "$TOMCAT_WEBAPPS/$APP_NAME.war" ]; then
mv "$TOMCAT_WEBAPPS/$APP_NAME.war" "$BACKUP_DIR/${APP_NAME}_$(date +%Y%m%d_%H%M%S).war"
fi
# 拷贝新 WAR
cp "$WAR_FILE" "$TOMCAT_WEBAPPS/"
# 等待 Tomcat 自动解压(最多 30 秒)
for i in {1..30}; do
if [ -d "$TOMCAT_WEBAPPS/$APP_NAME" ]; then
echo "Deploy success: $APP_NAME"
exit 0
fi
sleep 1
done
echo "Deploy failed: $APP_NAME not deployed"
exit 1
这个脚本的关键是 curl 调用 Tomcat Manager 的 stop 接口,而不是 ./shutdown.sh ,因为后者会杀死整个 Tomcat 进程。 Manager 接口只停止指定应用,不影响其他 WAR。我们还写了 rollback.sh :
#!/bin/bash
LATEST_BACKUP=$(ls -t /opt/tomcat/backups/myapp_*.war | head -1)
if [ -n "$LATEST_BACKUP" ]; then
cp "$LATEST_BACKUP" "/opt/tomcat/current/webapps/myapp.war"
echo "Rollback to $LATEST_BACKUP"
else
echo "No backup found"
fi
实测表明,从 curl stop 到新 WAR 解压完成,平均耗时 8.2 秒,比 shutdown.sh + startup.sh 快 5 倍。我们还配置了 conf/tomcat-users.xml ,添加管理员用户:
<role rolename="manager-script"/>
<user username="admin" password="password" roles="manager-script"/>
manager-script 角色允许通过 HTTP 调用 Manager API,这是自动化部署的基础。
4.3 启动验证与健康检查——五层诊断法
部署完成后,不能只看 catalina.out 是否有 INFO: Server startup in 。我们建立五层诊断法:
第一层:Tomcat 进程与端口
ps aux | grep tomcat # 确认进程存在
netstat -tuln | grep :8080 # 确认端口监听
第二层:WAR 包解压状态
ls -l /opt/tomcat/current/webapps/myapp/ # 应有 WEB-INF/, public/, config.ru
ls -l /opt/tomcat/current/webapps/myapp/WEB-INF/lib/ # 应有 jruby-complete-1.7.27.jar
第三层:日志关键信息
tail -n 100 /var/log/tomcat7/catalina.out | grep -E "(Starting Servlet|Initializing JRuby|Rails application)"
# 正常应看到:
# INFO: Starting Servlet Engine: Apache Tomcat/7.0.52
# INFO: Initializing JRuby runtime...
# INFO: Rails application initialized
第四层:HTTP 响应头
curl -I http://localhost:8080/
# 应返回 HTTP/1.1 200 OK,且有 X-Runtime, X-Rack-Cache 头
第五层:数据库连接
curl "http://localhost:8080/health" # 我们添加了 health check endpoint
# 返回 {"status":"ok","db":"connected","cache":"enabled"}
/health endpoint 在 app/controllers/health_controller.rb 中实现:
class HealthController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:index]
def index
begin
ActiveRecord::Base.connection.execute("SELECT 1")
db_status = "connected"
rescue => e
db_status = "failed: #{e.message}"
end
render json: {
status: "ok",
db: db_status,
cache: Rails.cache.active?
}
end
end
这个 endpoint 不经过任何 authentication,方便监控系统轮询。我们用 curl 每 5 秒调用一次,连续 3 次失败则告警。
4.4 性能基准测试——Tomcat 7 vs Puma 的真实数据
我们用 ab (Apache Bench)对同一 Rails 应用做了对比测试(Ubuntu 14.04, 4 核 CPU, 8GB 内存):
| 场景 | 并发

2218

被折叠的 条评论
为什么被折叠?



