基于NodeJS的前后端分离的思考与实践(六)Nginx + Node.js + Java 的软件栈部署实践


Posted in NodeJs onSeptember 26, 2014

淘宝网线上应用的传统软件栈结构为 Nginx + Velocity + Java,即:
基于NodeJS的前后端分离的思考与实践(六)Nginx + Node.js + Java 的软件栈部署实践
在这个体系中,Nginx 将请求转发给 Java 应用,后者处理完事务,再将数据用 Velocity 模板渲染成最终的页面。

引入 Node.js 之后,我们势必要面临以下几个问题:

技术栈的拓扑结构该如何设计,部署方式该如何选择,才算是科学合理?项目完成后,该如何切分流量,对运维来说才算是方便快捷?遇到线上的问题,如何最快地解除险情,避免更大的损失?如何确保应用的健康情况,在负载均衡调度的层面加以管理?承系统拓扑

按照我们在前后端分离的思考与实践(二)- 基于前后端分离的模版探索一文中的思路,Velocity 需要被 Node.js 取代,从而让这个结构变成:

基于NodeJS的前后端分离的思考与实践(六)Nginx + Node.js + Java 的软件栈部署实践

这当然是最理想的目标。然而,在传统栈中首次引入 Node.js 这一层毕竟是个新尝试。为了稳妥起见,我们决定只在收藏夹的宝贝收藏页面(shoucang.taobao.com/item_collect.htm)启用新的技术,其它页面沿用传统方案。即,由 Nginx 判断请求的页面类型,决定这个请求究竟是要转发给 Node.js 还是 Java。于是,最后的结构成了:

基于NodeJS的前后端分离的思考与实践(六)Nginx + Node.js + Java 的软件栈部署实践

部署方案

上面的结构看起来没什么问题了,但其实新问题还等在前面。在传统结构中,Nginx 与 Java 是部署在同一台服务器上的,Nginx 监听 80 端口,与监听高位 7001 端口的 Java 通信。现在引入了 Node.js ,需要新跑一个监听端口的进程,到底是将 Node.js 与 Nginx + Java 部署在同一台机器,还是将 Node.js 部署在单独的集群呢?
我们来比较一下两种方式各自特点:

基于NodeJS的前后端分离的思考与实践(六)Nginx + Node.js + Java 的软件栈部署实践

淘宝网收藏夹是一个拥有千万级日均 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 应用彻底中心化后,是否可以考分集群部署,以提高服务器利用率。或者,发布与回滚的方式是否能更加灵活可控。等等细节,都值得再进一步研究。

NodeJs 相关文章推荐
NodeJS学习笔记之Http模块
Jan 13 NodeJs
NodeJS创建基础应用并应用模板引擎
Apr 12 NodeJs
nodejs入门教程四:URL相关模块用法分析
Apr 24 NodeJs
浅析 NodeJs 的几种文件路径
Jun 07 NodeJs
nodejs使用http模块发送get与post请求的方法示例
Jan 08 NodeJs
nodejs简单读写excel内容的方法示例
Mar 16 NodeJs
原生nodejs使用websocket代码分享
Apr 07 NodeJs
nodejs读取并去重excel文件
Apr 22 NodeJs
nodejs取得当前执行路径的方法
May 13 NodeJs
Nodejs实现多文件夹文件同步
Oct 17 NodeJs
nodejs基础之多进程实例详解
Dec 27 NodeJs
nodejs制作小爬虫功能示例
Feb 24 NodeJs
基于NodeJS的前后端分离的思考与实践(五)多终端适配
Sep 26 #NodeJs
基于NodeJS的前后端分离的思考与实践(四)安全问题解决方案
Sep 26 #NodeJs
基于NodeJS的前后端分离的思考与实践(三)轻量级的接口配置建模框架
Sep 26 #NodeJs
基于NodeJS的前后端分离的思考与实践(二)模版探索
Sep 26 #NodeJs
基于NodeJS的前后端分离的思考与实践(一)全栈式开发
Sep 26 #NodeJs
Nodejs Post请求报socket hang up错误的解决办法
Sep 25 #NodeJs
Nodejs实现的一个简单udp广播服务器、客户端
Sep 25 #NodeJs
You might like
PHP反转字符串函数strrev()函数的用法
2012/02/04 PHP
accesskey 提交
2006/06/26 Javascript
开发跨浏览器javascript常见注意事项
2009/01/01 Javascript
js 浏览器事件介绍
2012/03/30 Javascript
深入理解JavaScript系列(14) 作用域链介绍(Scope Chain)
2012/04/12 Javascript
JS实现悬浮移动窗口(悬浮广告)的特效
2013/03/12 Javascript
javascript date格式化示例
2013/09/25 Javascript
浅谈JS日期(Date)处理函数
2014/12/07 Javascript
JavaScript中的值类型转换介绍
2014/12/31 Javascript
javascript实现树形菜单的方法
2015/07/17 Javascript
jQuery实现响应鼠标背景变化的动态菜单效果代码
2015/08/27 Javascript
基于node实现websocket协议
2016/04/25 Javascript
微信小程序商城项目之商品属性分类(4)
2017/04/17 Javascript
史上最全JavaScript常用的简写技巧(推荐)
2017/08/17 Javascript
利用JQUERY实现多个AJAX请求等待的实例
2017/12/14 jQuery
vue渲染时闪烁{{}}的问题及解决方法
2018/03/28 Javascript
详解VSCode配置启动Vue项目
2019/05/14 Javascript
解决vue打包 npm run build-test突然不动了的问题
2020/11/13 Javascript
[01:47]2018年度DOTA2最具人气解说-完美盛典
2018/12/16 DOTA
Python实现读取并保存文件的类
2017/05/11 Python
python实现数据写入excel表格
2018/03/25 Python
pytorch GAN生成对抗网络实例
2020/01/10 Python
查看keras各种网络结构各层的名字方式
2020/06/11 Python
Python判断变量是否是None写法代码实例
2020/10/09 Python
利用pipenv和pyenv管理多个相互独立的Python虚拟开发环境
2020/11/01 Python
CSS3+font字体文件实现圆形半透明菜单具体步骤(图解)
2013/06/03 HTML / CSS
html5 touch事件实现触屏页面上下滑动(二)
2016/03/10 HTML / CSS
美国现代家具购物网站:LexMod
2019/01/09 全球购物
可持续未来的时尚基础:Alternative Apparel
2019/05/06 全球购物
教师研修随笔感言
2014/01/23 职场文书
刑事和解协议书范本
2014/11/19 职场文书
公司员工离职感言
2015/08/03 职场文书
师德培训心得体会2016
2016/01/09 职场文书
mysql连接查询中and与where的区别浅析
2021/07/01 MySQL
《Estab Life》4月6日播出 正式PV、主视觉图公开
2022/03/20 日漫
Python接口自动化之文件上传/下载接口详解
2022/04/05 Python