在互联网公司,Nginx
能够说是标配组件,但是主要场景还是负载平衡、反向代理、代理缓存、限流等场景;而把Nginx
作为一个 Web 容器运用的还不是那么普遍。Nginx
的高性能是大家公认的,而 Nginx 开发主要是以 C/C++模块的方式停止,整体学习和开发本钱偏高;假如有一种简单的言语来完成 Web 应用的开发,那么Nginx绝对是把好的瑞士军刀;目前Nginx团队也开端认识到这个问题,开发了 nginxScript:能够在Nginx中运用 JavaScript 停止动态配置一些变量和动态脚本执行;而目前市面上用的十分成熟的扩展是由章亦春将 Lua 和 Nginx 粘合的 ngx_lua 模块,并且将 Nginx 中心、LuaJIT、ngx_lua 模块、许多有用的 Lua 库和常用的第三方 Nginx 模块组合在一同成为 OpenResty,这样开发人员就能够装置 OpenResty,运用 Lua 编写脚本,然后部署到 Nginx Web 容器中运转。从而十分轻松就能开发出高性能的 Web 效劳。
接下来我们就认识下 Nginx、Lua、ngx_lua 模块和 ngx_lua 到底能开发哪些类型的 web 应用。
一、ngx_lua 简介
1、Nginx 优点
Nginx 设计为一个主进程多个工作进程的工作形式,每个进程是单线程来处置多个衔接,而且每个工作进程采用了非阻塞 I/O 来处置多个衔接,从而减少了线程上下文切换,从而完成了公认的高性能、高并发;因而在生成环境中会经过把 CPU 绑定给 Nginx 工作进程从而提升其性能;另外由于单线程工作形式的特性,内存占用就十分少了。
Nginx 更改配置重启速度十分快,能够毫秒级,而且支持不中止 Nginx 停止晋级 Nginx 版本、动态重载 Nginx配置。
Nginx 模块也是十分多,功用也很强劲,不只能够作为 http 负载平衡,Nginx 发布 1.9.0 版本还支持 TCP 负载平衡,还能够很容易的完成内容缓存、web 效劳器、反向代理、访问控制等功用。
2、Lua 的优点
Lua 是一种轻量级、可嵌入式的脚本言语,这样能够十分容易的嵌入到其他言语中运用。另外 Lua 提供了协程并发,即以同步伐用的方式停止异步执行,从而完成并发,比起回调机制的并发来说代码更容易编写和了解,排查询题也会容易。Lua 还提供了闭包机制,函数能够作为 First Class Value 停止参数传送,另外其完成了标志肃清渣滓搜集。
由于 Lua 的小巧轻量级,能够在 Nginx 中嵌入 Lua VM,恳求的时分创立一个 VM,恳求完毕的时分回收 VM。
3、什么是 ngx_lua
ngx_lua 是 Nginx 的一个模块,将 Lua 嵌入到 Nginx 中,从而能够运用 Lua 来编写脚本,这样就能够运用 Lua 编写应用脚本,部署到 Nginx 中运转,即 Nginx 变成了一个 Web 容器;这样开发人员就能够运用 Lua 言语开发高性能Web 应用了。
ngx_lua 提供了与 Nginx 交互的很多的 API,关于开发人员来说只需求学习这些 API 就能够停止功用开发,而关于开发 web 应用来说,假如接触过 Servlet 的话,其开发和 Servlet 相似,无外乎 4、开发环境
我们可以使用 OpenResty 来搭建开发环境,OpenResty 将 Nginx 核心、LuaJIT、许多有用的 Lua 库和 Nginx 第三方模块打包在一起;这样开发人员只需要安装 OpenResty,不需要了解 Nginx 核心和写复杂的 C/C++模块就可以,只需要使用 Lua 语言进行
如何安装可以参考《跟我学 Nginx+Lua 开发》。
Web 应用开发了。
5、OpenResty 生态
OpenResty 提供了一些常用的 ngx_lua 开发模块:如
lua-resty-memcached
lua-resty-mysql
lua-resty-redis
lua-resty-dns
lua-resty-limit-traffic
lua-resty-template
这些模块涉及到如 mysql 数据库、redis、限流、模块渲染等常用功能组件;另外也有很多第三方的 ngx_lua 组件供我们使用,对于大部分应用场景来说现在生态环境中的组件已经足够多了;如果不满足需求也可以自己去写来完成自己的需求。
6、场景
理论上可以使用 ngx_lua 开发各种复杂的 web 应用,不过 Lua 是一种脚本/动态语言,不适合业务逻辑比较重的场景,适合小巧的应用场景,代码行数保持在几十行到几千行。目前见到的一些应用场景:
web 应用:会进行一些业务逻辑处理,甚至进行耗 CPU 的模板渲染,一般流程:mysql/redis/http 获取数据、业务处理、产生 JSON/XML/模板渲染内容,比如京东的列表页/商品详情页;
接入网关:实现如数据校验前置、缓存前置、数据过滤、API 请求聚合、AB 测试、灰度发布、降级、监控等功能,比如京东的交易大 Nginx 节点、无线部门正在开发的无线网关、单品页统一服务、实时价格、动态服务;
Web 防火墙:可以进行 IP/URL/UserAgent/Referer 黑名单、限流等功能;
缓存服务器:可以对响应内容进行缓存,减少到后端的请求,从而提升性能;
其他:如静态资源服务器、消息推送服务、缩略图裁剪等。
二、基于 Nginx+Lua 的常用架构模式
1、负载均衡
如上图,我们首先通过 LVS+HAProxy 将流量转发给核心 Nginx 1 和核心 Nginx 2,即实现了流量的负载均衡,此处可以使用如轮训、一致性哈希等调度算法来实现负载的转发;然后核心 Nginx 会根据请求特征如“Host:item.jd.com”,转发给相应的业务 Nginx 节点如单品页 Nginx 1。此处为什么分两层呢?
1、核心 Nginx 层是无状态的,可以在这一层实现流量分组(内网和外网隔离、爬虫和非爬虫流量隔离)、内容缓存、请求头过滤、故障切换(机房故障切换到其他机房)、限流、防火墙等一些通用型功能;
2、业务 Nginx 如单品页 Nginx,可以在在业务 Nginx 实现业务逻辑、或者反向代理到如 Tomcat,在这一层可以实现内容压缩(放在这一层的目的是减少核心 Nginx 的 CPU 压力,将压力分散到各业务 Nginx)、AB 测试、降级;即这一层的 Nginx 跟业务有关联,实现业务的一些通用逻辑。
不管是核心 Nginx 还是业务 Nginx,都应该是无状态设计,可以水平扩容。
业务 Nginx 一般会把请求直接转发给后端的业务应用,如 Tomcat、PHP,即将请求内部转发到相应的业务应用;当有的 Tomcat 出现问题了,可以在这一层摘掉;或者有的业务路径变了在这一层进行 rewrite;或者有的后端 Tomcat 压力太大也可以在这一层降级,减少对后端的冲击;或者业务需要灰度发布时也可以在这一层 Nginx 上控制。
2、单机闭环
所谓单机闭环即所有想要的数据都能从本服务器直接获取,在大多数时候无需通过网络去其他服务器获取。
如上所示,主要有三种应用模式:
2.1、第一张图应用场景是 Nginx 应用谁也不依赖,比如我们的 Cookie 白名单应用,其目的是不在白名单中的 Cookie 将被清理,防止大家随便将 Cookie 写到 jd.om 根下;大家访问 http://www.jd.com 时,会看到一个 http://ccc.jd.com/cookie_check 的请求用来清理 Cookie 的;对于这种应用非常简单,不需要依赖数据源,直接单应用闭环即可。
2.2、第二张图,是读取本机文件系统,如静态资源合并:比如访问http://item.jd.com/1856584.html
,查看源码会发现【//misc.360buyimg.com/jdf/1.0.0/unit/??ui-base/1.0.0/ui-base.css,shortcut/2.0.0/shortcut.css,global-header/1.0.0/global-header.css,myjd/2.0.0/myjd.css,nav/2.0.0/nav.css,shoppingcart/2.0.0/shoppingcart.css,global-footer/1.0.0/global-footer.css,service/1.0.0/service.css”/>
】这种请求,即多个请求合并为一个发给服务端,服务端进行了文件资源的合并;
目前有成熟的 Nginx 模块如 nginx-http-concat 进行静态资源合并;因为我们使用了 OpenResty,那么我们完全可以使用 Lua 编写程序实现该功能,比如已经有人写了 nginx-lua-static-merger 来实现这个功能。
还一些业务型应用场景如下图所示
商品页面是由商品框架和其他维度的页面片段(面包屑、相关分类、商家信息、规格参数、商品详情)组成;或者首页是由首页框架和一些页面片段(分类、轮播图、楼层 1、楼层 N)组成;分维度是因为不同的维度是独立变化的。对于这种静态内容但是需要进行框架内容嵌入的方式,Nginx 自带的 SSI(Server Side Include)可以很轻松的完成;也可以使用 Lua 程序更灵活的完成(读取框架、读取页面片段、合并输出)。
比如商品页面的架构我们可以这样:
首先接收到商品变更消息,商品页面同步 Worker 会根据消息维度生成相关的页面推送到 Nginx 服务器;Nginx 应用再通过 SSI 输出。目前京东商品详情页没有再采用这种架构,具体架构可以参考《构建需求响应式亿级商品详情页》。
对于首页的架构是类似的,因为其特点(框架变化少,楼层变化较频繁)和个性化的要求,楼层一般实现为异步加载。
2.3、 第三张图和第二张图的不同处是不再直接读取文件系统,而是读取本机的 Redis 或者 Redis 集群或者如 SSDB 这种持久化存储或者其他存储系统都是可以的,比如直接说的商品页面可以使用 SSDB 进行存储实现。文件系统一个很大的问题是当多台服务器时需要 Worker 去写多台服务器,而这个过程可以使用 SSDB 的主从实现。
此处可以看到,不管是图二还是图三架构,都需要 Worker 去进行数据推送;假设本机数据丢了可怎么办?因此实际大部分应用不会是完全单机闭环的,而是会采用如下架构:
即首先读本机,如果没数据会回源到相应的 Web 应用从数据源拉取原始数据进行处理。这种架构的大部分场景本机都可以命中数据,只有很少一部分情况会回源到 Web 应用。
如京东的实时价格/动态服务就是采用类似架构。
3、分布式闭环
单机闭环会遇到如下两个主要问题: 1、数据不一致问题(比如没有采用主从架构导致不同服务器数据不一致);2、遇到存储瓶颈(磁盘或者内存遇到了天花板)。
解决数据不一致的比较好的办法是采用主从或者分布式集中存储;而遇到存储瓶颈就需要进行按照业务键进行分片,将数据分散到多台服务器。
如采用如下架构,按照尾号将内容分布到多台服务器。
即第一步先读取分布式存储(JIMDB 是京东的一个分布式缓存/存储系统,类似于 Redis);如果不命中则回源到 Tomcat 集群(其会调用数据库、服务总线获取相关数据)来获取相关数据。可以参考《构建需求响应式亿级商品详情页》来获取更详细的架构实现。
JIMDB 集群会进行多机房主从同步,各自机房读取自己机房的从 JIMDB 集群,如下图
4、接入网关
接入网关也可以叫做接入层,即接收到流量的入口,在入口我们可以进行如下事情:
4.1、核心接入 Nginx 会做如下事情:
1、动态负载均衡;1、普通流量走一致性哈希,提升命中率;热点流量走轮训减少单服务器压力;2、根据请求特征将流量分配到不同分组并限流(爬虫、或者流量大的 IP);3、动态流量(动态增加 upstream 或者减少 upstream 或者动态负载均衡)可以使用 balancer_by_lua 或者微博开源的 upsync;
2、防 DDOS 攻击限流:可以将请求日志推送到实时计算集群,然后将需要限流的 IP 推送到核心 Nginx 进行限流;
3、非法请求过滤:比如应该有 Referer 却没有,或者应该带着 Cookie 却没有 Cookie;
4、请求聚合:比如请求的是http://c.3.cn/proxy?methods=a,b,c
,核心接入 Nginx 会在服务端把 Nginx 并发的请求并把结果聚合然后一次性吐出;
5、请求头过滤:有些业务是不需要请求头的,因此可以在往业务 Nginx 转发时把这些数据过滤掉;
6、缓存服务:使用 Nginx Proxy Cache 实现内容页面的缓存;
4.2、业务 Nginx 会做如下事情:
1、缓存:对于读服务会使用大量的缓存来提升性能,我们在设计时主要有如下缓存应用:首先读取 Nginx 本地缓存 Shared Dict 或者 Nginx Proxy Cache,如果有直接返回内容给用户;如果本地缓存不命中,则会读取分布式缓存如 Redis,如果有直接返回;如果还是不命中则回源到 Tomcat 应用读取 DB 或调用服务获取数据。另外我们会按照维度进行数据的缓存。
2、业务逻辑:我们会进行一些数据校验/过滤逻辑前置(如商品 ID 必须是数字)、业务逻辑前置(获取原子数据,然后在 Nginx 上写业务逻辑)。
3、细粒度限流:按照接口特征和接口吞吐量来实现动态限流,比如后端服务快扛不住了,那我们就需要进行限流,被限流的请求作为降级请求处理;通过 lua-resty-limit-traffic 可以通过编程实现更灵活的降级逻辑,如根据用户、根据 URL 等等各种规则,如降级了是让用户请求等待(比如 sleep 100ms,这样用户请求就慢下来了,但是服务还是可用)还是返回降级内容。
4、降级:降级主要有两种:主动降级和被动降级;如请求量太大扛不住了,那我们需要主动降级;如后端挂了或者被限流了或者后端超时了,那我们需要被动降级。降级方案可以是:1、返回默认数据如库存默认有货;2、返回静态页如预先生成的静态页;3、部分用户降级,告诉部分用户等待下再操作;4、直接降级,服务没数据,比如商品页面的规格参数不展示;5、只降级回源服务,即可以读取缓存的数据返回,实现部分可用,但是不会回源处理;
5、AB 测试/灰度发布:比如要上一个新的接口,可以通过在业务 Nginx 通过 Lua 写复杂的业务规则实现不同的人看到不同的版本。
6、服务质量监控:我们可以记录请求响应时间、缓存响应时间、反向代理服务响应时间来详细了解到底哪块服务慢了;另外记录非 200 状态码错误来了解服务的可用率。
京东的交易大 Nginx 节点、无线部门正在开发的无线 Nginx 网关、和单品页统一服务都是接入网关的实践,而单品页统一服务架构可以参考《京东商品详情页服务闭环实践》。
5、Web 应用
此处所说的 Web 应用指的是页面模板渲染类型应用或者 API 服务类型应用;比如京东列表页/商品详情页就是一个模板渲染类型的应用,核心业务逻辑都是使用 Lua 写的,部署到 Nginx 容器。目前核心业务代码行数有 5000 多行,模板页面有 2000 多行,涉及到大量的计算逻辑,性能数据可以参考《构建需求响应式亿级商品详情页》。
整体处理过程和普通 Web 应用没什么区别:首先接收请求并进行解析;然后读取 JIMDB 集群数据、如果没有则回源到 Tomcat 获取;然后进行业务逻辑处理;渲染模板;将响应内容返回给用户。
三、如何使用 Nginx+Lua 开发 Web 应用
开发一个 Web 应用我们需要从项目搭建、功能开发、项目部署几个层面完成。
3.1、项目搭建
/export/App/nginx-app
-------bin(脚本)
------------start.sh
------------stop.sh
-------config(配置文件)
------------nginx.conf
------------domain
----------------nginx_product.conf
------------resources.properties
-------lua(业务代码)
------------init.lua
------------product_controller.lua
-------template(模板)
--------------prodoct.html
-------lualib(公共 Lua 库)
------------jd
----------------product_util.lua
----------------product_data.lua
------------resty
----------------redis.lua
----------------template.lua
整个项目结构从启停脚本、配置文件、公共组件、业务代码、模板代码几块进行划分。
1、启停脚本
启停脚本放在项目目录/export/App/nginx-app/bin/下。
start.sh 是启动和更新脚本,即如果 nginx 没有启动则启动起来,否则 reload:
ifnginx 没启动 then
sudo/export/servers/nginx/sbin/nginx-t-c/export/App/nginx-app/config/nginx.conf
sudo/export/servers/nginx/sbin/nginx-c/export/App/nginx-app/config/nginx.conf
else
sudo/export/servers/nginx/sbin/nginx-t
sudo/export/servers/nginx/sbin/nginx-sreload
end
stop.sh 是停止 Nginx 脚本:
1
sudo/export/servers/nginx/sbin/nginx-squit
2、配置文件
配置文件放在/export/App/nginx-app/config 目录下,包括了 nginx.conf 配置文件、nginx 项目配置文件和资源配置文件。
nginx.confg 配置文件
对于 nginx.conf 会进行一些通用的配置,如工作进程数、超时时间、压缩、日志格式、反向代理等相关配置;另外需要指定如下配置:
lua_package_path、lua_package_cpath 指定我们依赖的通用 Lua 库从哪里加载;
include /export/App/nginx-app/config/domains/*:用于加载 server 相关的配置,此处通过*可以在一个 nginx 下指定多个 server 配置;
init_by_lua_file “/export/App/nginx-app/lua/init.lua”:执行项目的一些初始化配置,比如加载配置文件。
nginx 项目配置文件
worker_processes1; events{ worker_connections1024; } http{ includemime.types; default_typetext/html; #gzip 相关 #超时时间 #日志格式 #反向代理配置 #lua 依赖路径 lua_package_path"/export/App/nginx-app/lualib/?.lua;;"; lua_package_cpath"/export/App/nginx-app/lualib/?.so;;"; #server 配置 include/export/App/nginx-app/config/domains/*; #初始化脚本 init_by_lua_file"/export/App/nginx-app/lua/init.lua"; }
/export/App/nginx-app/config/domains/nginx_product.conf 用于配置当前 web 应用的一些 server 相关的配置:
#upstream upstreamitem_http_upstream{ server192.168.1.1max_fails=2fail_timeout=30sweight=5; server192.168.1.2max_fails=2fail_timeout=30sweight=5; } #缓存 lua_shared_dictitem_local_shop_cache600m; server{ listen80; server_nameitem.jd.comitem.jd.hk; #模板文件从哪加载 set$template_root"/export/App/nginx-app/template "; #url 映射 location~*"^/product/(\d+)\.html$"{ rewrite/product/(.*)http://item.jd.com/\ permanent; } location~*"^/(\d{6,12})\.html$"{ default_typetext/html; charsetgbk; lua_code_cacheon; content_by_lua_file"/export/App/nginx-app/lua/product_controller.lua"; } }
我们需要指定如 upstream、共享字典配置、server 配置、模板文件从哪加载、url 映射,比如我们访问http://item.jd.com/xxxx.html
将交给/export/App/nginx-app/lua/product_controller.lua 处理;也就是说我们项目的入口就有了。
资源配置文件 resources.properties 包含了我们的一些比如开关的配置、缓存服务器地址的配置等等。
3、业务代码
/export/App/nginx-app/lua/目录里存放了我们的 lua 业务代码,init.lua 用于读取如 resources.properties 来进行一些项目初始化;product_controller.lua 可以看成 Java Web 中的 Servlet,接收、处理、响应用户请求。
4、模板
模板文件放在/export/App/nginx-app/template/目录下,使用相应的模板引擎进行编写页面模板,然后渲染输出。
5、公共 Lua 库
存放了一些如 redis、template 等相关的公共 Lua 库,还有一些我们项目中通用的工具库如 product_util.lua。
到此一个简单的项目的结构就介绍完了,对于开发一个项目来说还会牵扯到分模块等工作,不过对于我们这种 Lua 应用来说,建议不要过度抽象,尽量小巧即可。
3.2、功能开发
接下来就需要使用相应的 API 来实现我们的业务了,比如 product_controller.lua:
--加载 Lua 模块库
localtemplate=require("resty.template")
--1、获取请求参数中的商品 ID
localskuId=ngx.req.get_uri_args()["skuId"];
--2、调用相应的服务获取数据
localdata=api.getData(skuId)
--3、渲染模板
localfunc=template.compile("product.html")
localcontent=func(data)
--4、通过 ngxAPI 输出内容
ngx.say(content)
开发完成后将项目部署到测试环境,执行 start.sh 启动 nginx 然后进行测试。
四、基于 Nginx+Lua 的常用功能总结
到此我们对于 Nginx 开发已经有了一个整体的认识,对于 Nginx 粘合 Lua 来开发应用可以说是一把锋利的瑞士军刀,可以帮我们很容易的解决很多问题,可以开发 Web 应用、接入网关、API 网关、消息推送、日志采集等应用,不过个人认为适合开发业务逻辑单一、核心代码行数较少的应用,不适合业务逻辑复杂、功能繁多的业务型或者企业级应用;最后我们总结下基于 Nginx+Lua 的常用架构模式中一些常见实践和场景:
动态负载均衡;
防火墙(DDOS、IP/URL/UserAgent/Referer 黑名单、防盗链等);
限流;
降级;
AB 测试/灰度发布;
多级缓存模式;
服务端请求聚合;
服务质量监控。
一些问题
1、在开发 nginx 应用时使用 UTF-8 编码可以减去很多麻烦;
2、GBK 转码解码时使用 GB18030,否则一些特殊字符会出现乱码;
3、cjson 库对于如\uab1 这种错误的 unicode 转码会失败,可以使用纯 Lua 编写的 dkjson;
4、社区版 nginx 不支持 upstream 的域名动态解析;可以考虑proxy_pass http://a.cn/prices/mgets$is_args$args
,然后配合 resolver 来实现;或者在 lua 中进行 http 调用;如果 DNS 遇到性能瓶颈可以考虑在本机部署如 dnsmasq 来缓存;或者考虑使用 balancer_by_lua 功能实现动态 upstream;
5、为响应添加处理服务器 IP 的响应头,方便定位问题;
6、根据业务设置合理的超时时间;
7、走 CDN 的业务当发生错误时返回的 500/503/302/301 等非正常响应不要设置缓存。业务当发生错误时返回的 500/503/302/301 等非正常响应不要设置缓存。