Rails 多台服务器上 js 不一致的问题

发现问题

周二发布正式的时候,发生了匪夷所思的事情。
打开发布的网页,有一定的几率操作会没有反应。但在测试的时候却从没有这个问题发生。通过调试工具查看网页,发现一半的网页会加载 application-b1a 开头的文件,而另一半会加载 application-745 开头的文件。加载 applicaton-745 时会报 404,使得操作没有反应。

这里简单地说下发布的情况。该服务会发布到 A、B 两台服务器上,nginx 接收请求并转发到 A、B 上的实例。
查看正式服务器上的文件发现,A 上存在 application-b1a 文件,B 上存在 application-745 文件。看来 nginx 会查找 服务器 A 上的文件,由于找不到 application-745 而返回 404。先把 B 上的 applicaiton-b1a 复制到 A,临时修复这个问题。

排查原因

为什么两个服务器上生成的 js 文件名会不一致?我们先简单地回顾下 Asset Pipeline 生成 js 的过程。正式环境上, Asset Pipeline 会预编译文件,生成类似于 application-908e25f4bf641868d8683022a5b62f54.js 的文件。其中 908e 这串表示摘要,是根据文件的内容生成的 MD5,当 js 文件发生改变时生成的摘要也会发生变化。

1
2
3
4
5
// 引用 js
<%= javascript_include_tag "application" %>
// 生成的 html
<script src="/assets/application-908e25f4bf641868d8683022a5b62f54.js"></script>

换句话说,若 js 内容一样,生成的摘要也应该是一样的。不应该啊!两个服务器部署的代码应该是一致的。为了保险起见,我还特定查看了相关文件的 MD5 值,完全相同。

输入是相同的,而产生的结果却不一样,问题应该出在处理步骤上。于是我查看了正式环境的配置,发现了以下这条。

1
config.assets.js_compressor = :uglifier

uglifier 是用 ruby 封装 UglifyJS 的 gem,而 UglifyJS 是依赖 node.js 对 js 进行压缩的。查看了下两台服务器的 node 版本,A 是 0.10,B 是 6.10。问题的原因终于找到了。node 的版本不一致,导致其压缩的结果不一样,使得生成的最终文件也不相同。

解决方案

先在 A 上升级了 node 版本,使得 A、B 两台服务器的 node 版本一致。然后删除 tmp/cache/assets/sprockets 下的缓存,再执行 RAILS_ENV=production bin/rake assets:precompile,最后重新发布,使最新生成的结果能被实例加载。
注意这里必须先删除缓存,不然执行 precompile 时不会生成最新的结果。

反思

为什么这个问题会突然出现呢?原来是 B 上的 node 版本由于其他应用的需求进行了升级,使得 A、B 两台版本不一致了。
这种一个馒头引发的血案防不胜防,可见应用之间依赖的隔离是多么重要。联想到近几年类似 docker 的解决方案大受欢迎也就不足为怪了。

参考