Nginx + Node.js + Java 的软件栈部署实践
# 起
关于前后端分享的思考,我们已经有五篇文章阐述思路与设计。本文介绍淘宝网[![f](http://f.ydr.me/http://shoucang.taobao.com)收藏夹](http://shoucang.taobao.com/)将 Node.js 引入传统技术栈的具体实践。
淘宝网线上应用的传统软件栈结构为 Nginx + Velocity + Java,即:
![](https://i.alipayobjects.com/i/localhost/png/201405/2kbuaECNpd.png)
在这个体系中,Nginx 将请求转发给 Java 应用,后者处理完事务,再将数据用 Velocity 模板渲染成最终的页面。
引入 Node.js 之后,我们势必要面临以下几个问题:
1. 技术栈的拓扑结构该如何设计,部署方式该如何选择,才算是科学合理?
2. 项目完成后,该如何切分流量,对运维来说才算是方便快捷?
3. 遇到线上的问题,如何最快地解除险情,避免更大的损失?
4. 如何确保应用的健康情况,在负载均衡调度的层面加以管理?
# 承
## 系统拓扑
按照我们在[![f](http://f.ydr.me/http://ued.taobao.org/blog/2014/04/xtpl/)前后端分离的思考与实践(二)- 基于前后端分离的模版探索](http://ued.taobao.org/blog/2014/04/xtpl/)一文中的思路,Velocity 需要被 Node.js 取代,从而让这个结构变成:
![](https://i.alipayobjects.com/i/localhost/png/201405/2kcIkX7AXl.png)
这当然是最理想的目标。然而,在传统栈中首次引入 Node.js 这一层毕竟是个新尝试。为了稳妥起见,我们决定只在收藏夹的宝贝收藏页面([![f](http://f.ydr.me/http://shoucang.taobao.com/item_collect.htm)shoucang.taobao.com/item_collect.htm](http://shoucang.taobao.com/item_collect.htm))启用新的技术,其它页面沿用传统方案。即,由 Nginx 判断请求的页面类型,决定这个请求究竟是要转发给 Node.js 还是 Java。于是,最后的结构成了:
![](https://i.alipayobjects.com/i/localhost/png/201405/2kc0OxfysP.png)
## 部署方案
上面的结构看起来没什么问题了,但其实新问题还等在前面。在传统结构中,Nginx 与 Java 是部署在同一台服务器上的,Nginx 监听 80 端口,与监听高位 7001 端口的 Java 通信。现在引入了 Node.js ,需要新跑一个监听端口的进程,到底是将 Node.js 与 Nginx + Java 部署在同一台机器,还是将 Node.js 部署在单独的集群呢?
我们来比较一下两种方式各自特点:
![](http://gtms03.alicdn.com/tps/i3/TB14LwSFFXXXXaUXFXXXx3jKpXX-1566-426.png)
淘宝网收藏夹是一个拥有千万级日均 PV 的应用,对稳定性的要求性极高(事实上任何产品的线上不稳定都是不能接受的)。如果采用同集群部署方案,只需要一次文件分发,两次应用重启即可完成发布,万一需要回滚,也只需要操作一次基线包。性能上来说,同集群部署也有一些理论优势(虽然内网的交换机带宽与延时都是非常乐观的)。至于一对多或者多对一的关系,理论上可能做到服务器更加充分的利用,但相比稳定性上的要求,这一点并不那么急迫需要去解决。所以在收藏夹的改造中,我们选择了同集群部署方案。
## 灰度方式
为了保证最大程度的稳定,这次改造并没有直接将 Velocity 代码完全去掉。应用集群中有将近 100 台服务器,我们以服务器为粒度,逐渐引入流量。也就是说,虽然所有的服务器上都跑着 Java + Node.js 的进程,但 Nginx 上有没有相应的转发规则,决定了获取这台服务器上请求宝贝收藏的请求是否会经过 Node.js 来处理。其中 Nginx 的配置为:
~~~
location = "/item_collect.htm" {
proxy_pass http://127.0.0.1:6001; # Node.js 进程监听的端口
}
~~~
只有添加了这条 Nginx 规则的服务器,才会让 Node.js 来处理相应请求。通过 Nginx 配置,可以非常方便快捷地进行灰度流量的增加与减少,成本很低。如果遇到问题,可以直接将 Nginx 配置进行回滚,瞬间回到传统技术栈结构,解除险情。
第一次发布时,我们只有两台服务器上启用了这条规则,也就是说大致有不到 2% 的线上流量是走 Node.js 处理的,其余的流量的请求仍然由 Velocity 渲染。以后视情况逐步增加流量,最后在第三周,全部服务器都启用了。至此,生产环境 100% 流量的商品收藏页面都是经 Node.js 渲染出来的(可以查看源代码搜索 Node.js 关键字)。
# 转
灰度过程并不是一帆风顺的。在全量切流量之前,遇到了一些或大或小的问题。大部分与具体业务有关,值得借鉴的是一个技术细节相关的陷阱。
## 健康检查
在传统的架构中,负载均衡调度系统每隔一秒钟会对每台服务器 80 端口的特定 URL 发起一次 `get` 请求,根据返回的 HTTP Status Code 是否为 `200` 来判断该服务器是否正常工作。如果请求 1s 后超时或者 HTTP Status Code 不为 `200`,则不将任何流量引入该服务器,避免线上问题。
这个请求的路径是 Nginx -> Java -> Nginx,这意味着,只要返回了 `200`,那这台服务器的 Nginx 与 Java 都处于健康状态。引入 Node.js 后,这个路径变成了 Nginx -> Node.js -> Java -> Node.js -> Nginx。相应的代码为:
~~~
var http = require('http');
app.get('/status.taobao', function(req, res) {
http.get({
host: '127.1',
port: 7001,
path: '/status.taobao'
}, function(res) {
res.send(res.statusCode);
}).on('error', function(err) {
logger.error(err);
res.send(404);
});
});
~~~
但是在测试过程中,发现 Node.js 在转发这类请求的时候,每六七次就有一次会耗时几秒甚至十几秒才能得到 Java 端的返回。这样会导致负载均衡调度系统认为该服务器发生异常,随即切断流量,但实际上这台服务器是能够正常工作的。这显然是一个不小的问题。
排查一番发现,默认情况下, Node.js 会使用 `HTTP Agent` 这个类来创建 HTTP 连接,这个类实现了 socket 连接池,每个主机+端口对的连接数默认上限是 5。同时 `HTTP Agent` 类发起的请求中默认带上了 `Connection: Keep-Alive`,导致已返回的连接没有及时释放,后面发起的请求只能排队。
最后的解决办法有三种:
* 禁用 `HTTP Agent`,即在在调用 `get` 方法时额外添加参数 `agent: false`,最后的代码为:
~~~
var http = require('http');
app.get('/status.taobao', function(req, res) {
http.get({
host: '127.1',
port: 7001,
agent: false,
path: '/status.taobao'
}, function(res) {
res.send(res.statusCode);
}).on('error', function(err) {
logger.error(err);
res.send(404);
});
});
~~~
* 设置 `http` 对象的全局 socket 数量上限:
~~~
http.globalAgent.maxSockets = 1000;
~~~
* 在请求返回的时候及时主动断开连接:
~~~
http.get(options, function(res) {
}).on("socket", function (socket) {
socket.emit("agentRemove"); // 监听 socket 事件,在回调中派发 agentRemove 事件
});
~~~
实践上我们选择第一种方法。这么调整之后,健康检查就没有再发现其它问题了。
# 合
Node.js 与传统业务场景结合的实践才刚刚起步,仍然有大量值得深入挖掘的优化点。比比如,让 Java 应用彻底中心化后,是否可以考分集群部署,以提高服务器利用率。或者,发布与回滚的方式是否能更加灵活可控。等等细节,都值得再进一步研究。
- 开始
- 微信小程序
- 获取用户信息
- 记录
- HTML
- HTML5
- 文档根节点
- 你真的了解script标签吗?
- 文档结构
- 已经落后的技术
- form表单
- html实体
- CSS
- css优先级 & 设计模式
- 如何编写高效的 CSS 选择符
- 笔记
- 小计
- flex布局
- 细节体验
- Flex
- Grid
- tailwindcss
- JavaScript
- javascript物语
- js函数定义
- js中的数组对象
- js的json解析
- js中数组的操作
- js事件冒泡
- js中的判断
- js语句声明会提前
- cookie操作
- 关于javascript你要知道的
- 关于innerHTML的试验
- js引擎与GUI引擎是互斥的
- 如何安全的修改对象
- 当渲染引擎遇上强迫症
- 不要使用连相等
- 修改数组-对象
- 算法-函数
- 事件探析
- 事件循环
- js事件循环中的上下文和作用域的经典问题
- Promise
- 最佳实践
- 页面遮罩加载效果
- 网站静态文件之思考
- 图片加载问题
- 路由及转场解决方案
- web app
- 写一个页面路由转场的管理工具
- 谈编程
- 技术/思想的斗争
- 前端技术选型分析
- 我想放点html模板代码
- 开发自适应网页
- 后台前端项目的开发
- 网站PC版和移动版的模板方案
- 前后端分离
- 淘宝前后端分离
- 前后端分离的思考与实践(一)
- 前后端分离的思考与实践(二)
- 前后端分离的思考与实践(三)
- 前后端分离的思考与实践(四)
- 前后端分离的思考与实践(五)
- 前后端分离的思考与实践(六)
- 动画
- 开发小技巧
- Axios
- 屏幕适配
- 理论基础
- 思考
- flexible.js原理
- 实验
- rem的坑,为什么要设置成百分比,为什么又是62.5%
- 为什么以一个标准适配的,其它宽度也能同等适配
- 自适应、响应式、弹性布局、屏幕适配
- 适配:都用百分比?
- 番外篇
- 给你看看0.5px长什么样?
- 用事实证明viewport scale缩放不会改变rem元素的大小
- 为什么PC端页面缩放不会影响rem元素
- 究竟以哪个为设备独立像素
- PC到移动端初试
- 深入理解px
- 响应式之栅格系统
- 深入理解px(二)
- 一篇搞定移动端适配
- flex版栅格布局
- 其他
- 浏览器加载初探
- 警惕你的开发工具
- JS模块化
- webpack
- 打包原理
- 异步加载
- gulp
- 命名规范
- 接口开发
- sea.js学习
- require.js学习
- react学习
- react笔记
- vue学习
- vue3
- 工具、技巧
- 临时笔记
- 怎么维护好开源项目
- 待办
- 对前端MVV*C框架的思考
- jquery问题
- 临时
- 好文
- 节流防抖