使用OpenResty(Nginx+Lua)开发高性能Web应用,openrestynginx


在互联网公司,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类似,无外乎就是知道接收请求、参数解析、功能处理、返回响应这几步的API是什么样子的。

 

4、开发环境

我们可以使用OpenResty来搭建开发环境,OpenResty将Nginx核心、LuaJIT、许多有用的Lua库和Nginx第三方模块打包在一起;这样开发人员只需要安装OpenResty,不需要了解Nginx核心和写复杂的C/C++模块就可以,只需要使用Lua语言进行Web应用开发了。

如何安装可以参考《跟我学Nginx+Lua开发》。

 

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,查看源码会发现【<link type="text/css" rel="stylesheet" href="//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、项目搭建 

Java代码  
  1. /export/App/nginx-app  
  2.  -------bin(脚本)  
  3.  ------------start.sh  
  4.  ------------stop.sh  
  5.  -------config(配置文件)  
  6.  ------------nginx.conf  
  7.  ------------domain  
  8.  ----------------nginx_product.conf  
  9.  ------------resources.properties  
  10.  -------lua(业务代码)  
  11.  ------------init.lua  
  12.  ------------product_controller.lua  
  13.  -------template(模板)  
  14.  --------------prodoct.html  
  15.  -------lualib(公共Lua库)  
  16.  ------------jd  
  17.  ----------------product_util.lua  
  18.  ----------------product_data.lua  
  19.  ------------resty  
  20.  ----------------redis.lua  
  21.  ----------------template.lua  

  

 整个项目结构从启停脚本、配置文件、公共组件、业务代码、模板代码几块进行划分。

 

1、启停脚本

启停脚本放在项目目录/export/App/nginx-app/bin/下。

start.sh是启动和更新脚本,即如果nginx没有启动则启动起来,否则reload: 

Java代码  
  1. if nginx没启动 then  
  2.   sudo /export/servers/nginx/sbin/nginx  -t -c /export/App/nginx-app/config/nginx.conf  
  3.   sudo /export/servers/nginx/sbin/nginx  -c /export/App/nginx-app/config/nginx.conf  
  4. else  
  5.   sudo /export/servers/nginx/sbin/nginx  -t  
  6.   sudo /export/servers/nginx/sbin/nginx  -s reload  
  7. end    

stop.sh是停止Nginx脚本: 

Java代码  
  1. sudo /export/servers/nginx/sbin/nginx  -s quit   

 

2、配置文件

配置文件放在/export/App/nginx-app/config目录下,包括了nginx.conf配置文件、nginx项目配置文件和资源配置文件。

 

nginx.confg配置文件  

Java代码  
  1. worker_processes  1;  
  2. events {  
  3.     worker_connections  1024;  
  4. }  
  5. http {  
  6.    include       mime.types;  
  7.    default_type  text/html;  
  8.    #gzip相关  
  9.    #超时时间  
  10.    #日志格式  
  11.    #反向代理配置  
  12.   
  13.    #lua依赖路径  
  14.    lua_package_path  "/export/App/nginx-app/lualib/?.lua;;";  
  15.    lua_package_cpath  "/export/App/nginx-app/lualib/?.so;;";  
  16.   
  17.    #server配置  
  18.    include /export/App/nginx-app/config/domains/*;  
  19.   
  20.    #初始化脚本  
  21.    init_by_lua_file "/export/App/nginx-app/lua/init.lua";  
  22. }   

对于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项目配置文件

/export/App/nginx-app/config/domains/nginx_product.conf用于配置当前web应用的一些server相关的配置: 

Java代码  
  1. #upstream  
  2. upstream item_http_upstream {  
  3.     server 192.168.1.1 max_fails=2 fail_timeout=30s weight=5;  
  4.     server 192.168.1.2 max_fails=2 fail_timeout=30s weight=5;  
  5. }  
  6. #缓存  
  7. lua_shared_dict item_local_shop_cache 600m;  
  8. server {  
  9.      listen                   80;  
  10.      server_name              item.jd.com item.jd.hk;  
  11.      #模板文件从哪加载  
  12.     set $template_root "/export/App/nginx-app/template ";  
  13.      #url映射  
  14.         location ~* "^/product/(\d+)\.html$" {  
  15.             rewrite /product/(.*)    http://item.jd.com/$1 permanent;  
  16.         }  
  17.     location ~* "^/(\d{6,12})\.html$" {  
  18.             default_type text/html;  
  19.             charset gbk;  
  20.             lua_code_cache on;  
  21.             content_by_lua_file "/export/App/nginx-app/lua/product_controller.lua";  
  22.         }  
  23. }   

我们需要指定如upstream、共享字典配置、server配置、模板文件从哪加载、url映射,比如我们访问http://item.jd.com/1856584.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:

Java代码  
  1. --加载Lua模块库  
  2. local template = require("resty.template")    
  3. --1、获取请求参数中的商品ID  
  4. local skuId = ngx.req.get_uri_args()["skuId"];  
  5. --2、调用相应的服务获取数据  
  6. local data = api.getData(skuId)  
  7.   
  8. --3、渲染模板  
  9. local func = template.compile("product.html")    
  10. local content = func(data)    
  11. --4、通过ngx API输出内容    
  12. ngx.say(content)     

开发完成后将项目部署到测试环境,执行start.sh启动nginx然后进行测试。

详细的开发过程和API的使用,请参考《跟我学Nginx+Lua开发》。此处不做具体编码实现。

 

参考源码:Nginx+Lua(OpenResty) HelloWorld

 

四、基于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://p.3.local/prices/mgets$is_args$args,然后配合resolver来实现;或者在lua中进行http调用;如果DNS遇到性能瓶颈可以考虑在本机部署如dnsmasq来缓存;或者考虑使用balancer_by_lua功能实现动态upstream;

5、为响应添加处理服务器IP的响应头,方便定位问题;

6、根据业务设置合理的超时时间;

7、走CDN的业务当发生错误时返回的500/503/302/301等非正常响应不要设置缓存。


案例分享:

用lua扩展你的Nginx


一. 概述

Nginx是一个高性能,支持高并发的,轻量级的web服务器。目前,Apache依然web服务器中的老大,但是在全球前1000大的web服务器中,Nginx的份额为22.4%。Nginx采用模块化的架构,官方版本的Nginx中大部分功能都是通过模块方式提供的,比如Http模块、Mail模块等。通过开发模块扩展Nginx,可以将Nginx打造成一个全能的应用服务器,这样可以将一些功能在前端Nginx反向代理层解决,比如登录校验、js合并、甚至数据库访问等等。     但是,Nginx模块需要用C开发,而且必须符合一系列复杂的规则,最重要的用C开发模块必须要熟悉Nginx的源代码,使得开发者对其望而生畏。淘宝的agentzh和chaoslawful开发的ngx_lua模块通过将lua解释器集成进Nginx,可以采用lua脚本实现业务逻辑,由于lua的紧凑、快速以及内建协程,所以在保证高并发服务能力的同时极大地降低了业务逻辑实现成本。     本文向大家介绍ngx_lua,以及我在使用它开发项目的过程中遇到的一些问题。

二. 准备

首先,介绍一下Nginx的一些特性,便于后文介绍ngx_lua的相关特性。


Nginx进程模型

Nginx采用多进程模型,单Master—多Worker,由Master处理外部信号、配置文件的读取及Worker的初始化,Worker进程采用单线程、非阻塞的事件模型(Event Loop,事件循环)来实现端口的监听及客户端请求的处理和响应,同时Worker还要处理来自Master的信号。由于Worker使用单线程处理各种事件,所以一定要保证主循环是非阻塞的,否则会大大降低Worker的响应能力。

Nginx处理Http请求的过程

表面上看,当Nginx处理一个来自客户端的请求时,先根据请求头的host、ip和port来确定由哪个server处理,确定了server之后,再根据请求的uri找到对应的location,这个请求就由这个location处理。实际Nginx将一个请求的处理划分为若干个不同阶段(phase),这些阶段按照前后顺序依次执行,也就是说NGX_HTTP_POST_READ_PHASE在第一个,NGX_HTTP_LOG_PHASE在最后一个。
[plain] view plain copy
  1. <span style="font-size:10px;">NGX_HTTP_POST_READ_PHASE,     //0读取请求phaseNGX_HTTP_SERVER_REWRITE_PHASE,//1这个阶段主要是处理全局的(server block)的rewriteNGX_HTTP_FIND_CONFIG_PHASE,   //2这个阶段主要是通过uri来查找对应的location,然后根据loc_conf设置r的相应变量NGX_HTTP_REWRITE_PHASE,       //3这个主要处理location的rewriteNGX_HTTP_POST_REWRITE_PHASE,  //4postrewrite,这个主要是进行一些校验以及收尾工作,以便于交给后面的模块。NGX_HTTP_PREACCESS_PHASE,     //5比如流控这种类型的access就放在这个phase,也就是说它主要是进行一些比较粗粒度的access。NGX_HTTP_ACCESS_PHASE,        //6这个比如存取控制,权限验证就放在这个phase,一般来说处理动作是交给下面的模块做的.这个主要是做一些细粒度的accessNGX_HTTP_POST_ACCESS_PHASE,   //7一般来说当上面的access模块得到access_code之后就会由这个模块根据access_code来进行操作NGX_HTTP_TRY_FILES_PHASE,     //8try_file模块,就是对应配置文件中的try_files指令,可接收多个路径作为参数,当前一个路径的资源无法找到,则自动查找下一个路径   NGX_HTTP_CONTENT_PHASE,       //9内容处理模块   NGX_HTTP_LOG_PHASE            //10log模块  

每个阶段上可以注册handler,处理请求就是运行每个阶段上注册的handler。Nginx模块提供的配置指令只会一般只会注册并运行在其中的某一个处理阶段。

比如,set指令属于rewrite模块的,运行在rewrite阶段,deny和allow运行在access阶段。


子请求(subrequest)

其实在Nginx 世界里有两种类型的“请求”,一种叫做“主请求”(main request),而另一种则叫做“子请求”(subrequest)。 所谓“主请求”,就是由 HTTP 客户端从 Nginx 外部发起的请求。比如,从浏览器访问Nginx就是一个“主请求”。 而“子请求”则是由 Nginx 正在处理的请求在 Nginx 内部发起的一种级联请求。“子请求”在外观上很像 HTTP 请求,但实现上却和 HTTP 协议乃至网络通信一点儿关系都没有。它是 Nginx 内部的一种抽象调用,目的是为了方便用户把“主请求”的任务分解为多个较小粒度的“内部请求”,并发或串行地访问多个 location 接口,然后由这些 location 接口通力协作,共同完成整个“主请求”。当然,“子请求”的概念是相对的,任何一个“子请求”也可以再发起更多的“子子请求”,甚至可以玩递归调用(即自己调用自己)。

当一个请求发起一个“子请求”的时候,按照 Nginx 的术语,习惯把前者称为后者的“父请求”(parent request)。

[plain] view plain copy
  1. location /main {    echo_location /foo;     # echo_location发送子请求到指定的location    echo_location /bar;}location /foo {    echo foo;}location /bar {    echo bar;}  

输出:

$ curl location/main
$ foo   03.  bar

这里,main location就是发送2个子请求,分别到foo和bar,这就类似一种函数调用。

“子请求”方式的通信是在同一个虚拟主机内部进行的,所以 Nginx 核心在实现“子请求”的时候,就只调用了若干个 C 函数,完全不涉及任何网络或者 UNIX 套接字(socket)通信。我们由此可以看出“子请求”的执行效率是极高的。

协程(Coroutine)

协程类似一种多线程,与多线程的区别有: 

1. 协程并非os线程,所以创建、切换开销比线程相对要小。 

2. 协程与线程一样有自己的栈、局部变量等,但是协程的栈是在用户进程空间模拟的,所以创建、切换开销很小。

3. 多线程程序是多个线程并发执行,也就是说在一瞬间有多个控制流在执行。而协程强调的是一种多个协程间协作的关系,只有当一个协程主动放弃执行权,另一个协程才能获得执行权,所以在某一瞬间,多个协程间只有一个在运行。 

4. 由于多个协程时只有一个在运行,所以对于临界区的访问不需要加锁,而多线程的情况则必须加锁。 

5. 多线程程序由于有多个控制流,所以程序的行为不可控,而多个协程的执行是由开发者定义的所以是可控的。 

Nginx的每个Worker进程都是在epoll或kqueue这样的事件模型之上,封装成协程,每个请求都有一个协程进行处理。这正好与Lua内建协程的模型是一致的,所以即使ngx_lua需要执行Lua,相对C有一定的开销,但依然能保证高并发能力。


三. ngx_lua

原理
ngx_lua将Lua嵌入Nginx,可以让Nginx执行Lua脚本,并且高并发、非阻塞的处理各种请求。Lua内建协程,这样就可以很好的将异步回调转换成顺序调用的形式。ngx_lua在Lua中进行的IO操作都会委托给Nginx的事件模型,从而实现非阻塞调用。开发者可以采用串行的方式编写程序,ngx_lua会自动的在进行阻塞的IO操作时中断,保存上下文;然后将IO操作委托给Nginx事件处理机制,在IO操作完成后,ngx_lua会恢复上下文,程序继续执行,这些操作都是对用户程序透明的。 每个NginxWorker进程持有一个Lua解释器或者LuaJIT实例,被这个Worker处理的所有请求共享这个实例。每个请求的Context会被Lua轻量级的协程分割,从而保证各个请求是独立的。 ngx_lua采用“one-coroutine-per-request”的处理模型,对于每个用户请求,ngx_lua会唤醒一个协程用于执行用户代码处理请求,当请求处理完成这个协程会被销毁。每个协程都有一个独立的全局环境(变量空间),继承于全局共享的、只读的“comman data”。所以,被用户代码注入全局空间的任何变量都不会影响其他请求的处理,并且这些变量在请求处理完成后会被释放,这样就保证所有的用户代码都运行在一个“sandbox”(沙箱),这个沙箱与请求具有相同的生命周期。 得益于Lua协程的支持,ngx_lua在处理10000个并发请求时只需要很少的内存。根据测试,ngx_lua处理每个请求只需要2KB的内存,如果使用LuaJIT则会更少。所以ngx_lua非常适合用于实现可扩展的、高并发的服务。

典型应用

官网上列出: 

· Mashup’ing and processing outputs of various nginx upstream outputs(proxy, drizzle, postgres, redis, memcached, and etc) in Lua, · doing arbitrarily complex access control and security checks in Luabefore requests actually reach the upstream backends, · manipulating response headers in an arbitrary way (by Lua) · fetching backend information from external storage backends (likeredis, memcached, mysql, postgresql) and use that information to choose whichupstream backend to access on-the-fly, · coding up arbitrarily complex web applications in a content handlerusing synchronous but still non-blocking access to the database backends andother storage, · doing very complex URL dispatch in Lua at rewrite phase, · using Lua to implement advanced caching mechanism for nginxsubrequests and arbitrary locations.
Hello Lua!
[plain] view plain copy
  1. # nginx.confworker_processes 4;events {     worker_connections 1024;}http {    server {        listen 80;        server_name localhost;        location=/lua {            content_by_lua ‘                ngx.say("Hello, Lua!")            ';        }    }}  
输出:
$ curl 'localhost/lua'
Hello,Lua!

这样就实现了一个很简单的ngx_lua应用,如果这么简单的模块要是用C来开发的话,代码量估计得有100行左右,从这就可以看出ngx_lua的开发效率。

Benchmark
通过和nginx访问静态文件还有nodejs比较,来看一下ngx_lua提供的高并发能力。 返回的内容都是”Hello World!”,151bytes 通过.ab -n 60000   取10次平均
从图表中可以看到,在各种并发条件下ngx_lua的rps都是最高的,并且基本维持在10000rps左右,nginx读取静态文件因为会有磁盘io所以性能略差一些,而nodejs是相对最差的。通过这个简单的测试,可以看出ngx_lua的高并发能力。 ngx_lua的开发者也做过一个测试对比nginx+fpm+php和nodejs,他得出的结果是ngx_lua可以达到28000rps,而nodejs有10000多一点,php则最差只有6000。可能是有些配置我没有配好导致ngx_lua rps没那么高。


ngx_lua安装

ngx_lua安装可以通过下载模块源码,编译Nginx,但是推荐采用openresty。Openresty就是一个打包程序,包含大量的第三方Nginx模块,比如HttpLuaModule,HttpRedis2Module,HttpEchoModule等。省去下载模块,并且安装非常方便。 ngx_openresty bundle: openresty ./configure --with-luajit&& make && make install 默认Openresty中ngx_lua模块采用的是标准的Lua5.1解释器,通过--with-luajit使用LuaJIT。
ngx_lua的用法
ngx_lua模块提供了配置指令和Nginx API。 配置指令:在Nginx中使用,和set指令和pass_proxy指令使用方法一样,每个指令都有使用的context。        Nginx API:用于在Lua脚本中访问Nginx变量,调用Nginx提供的函数。 下面举例说明常见的指令和API。

配置指令

set_by_lua和set_by_lua_file

和set指令一样用于设置Nginx变量并且在rewrite阶段执行,只不过这个变量是由lua脚本计算并返回的。
语法:set_by_lua$res <lua-script-str> [$arg1 $arg2 ...]

配置:

[plain] view plain copy
  1. location =/adder {    set_by_lua $res"            local a = tonumber(ngx.arg[1])                local b = tonumber(ngx.arg[2])                return a + b"$arg_a$arg_b;        echo$res;}  
输出:
$ curl 'localhost/adder?a=25&b=75'
$ 100

set_by_lua_file执行Nginx外部的lua脚本,可以避免在配置文件中使用大量的转义。

配置:

[plain] view plain copy
  1. location =/fib {        set_by_lua_file $res "conf/adder.lua" $arg_n;        echo $res;}</span>  


adder.lua:

[plain] view plain copy
  1. local a=tonumber(ngx.arg[1])local b=tonumber(ngx.arg[2])return a + b  

输出:
$ curl 'localhost/adder?a=25&b=75
$ 100

access_by_lua和access_by_lua_file

运行在access阶段,用于访问控制。Nginx原生的allow和deny是基于ip的,通过access_by_lua能完成复杂的访问控制,比如,访问数据库进行用户名、密码验证等。

配置:

[plain] view plain copy
  1. location /auth {    access_by_lua '        if ngx.var.arg_user == "ntes" then            return        else            Ngx.exit(ngx.HTTP_FORBIDDEN)        end    ';    echo'welcome ntes';}  
输出:
$ curl 'localhost/auth?user=sohu'
$ Welcome ntes

$ curl 'localhost/auth?user=ntes'
$ <html>
<head><title>403 Forbidden</title></heda>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>ngx_openresty/1.0.10.48</center>
</body>
</html>

rewrite_by_lua和rewrite_by_lua_file

实现url重写,在rewrite阶段执行。

配置:
[plain] view plain copy
  1. location =/foo {        rewrite_by_lua 'ngx.exec("/bar")';    echo'in foo';}location =/bar {        echo'in bar';}  
输出:
$ curl 'localhost/lua'
$ Hello, Lua!

content_by_lua和content_by_lua_file

Contenthandler在content阶段执行,生成http响应。由于content阶段只能有一个handler,所以在与echo模块使用时,不能同时生效,我测试的结果是content_by_lua会覆盖echo。这和之前的hello world的例子是类似的。


配置(直接响应):
[plain] view plain copy
  1. location =/lua {        content_by_lua 'ngx.say("Hello, Lua!")';}  

输出:
$ curl 'localhost/lua'
$ Hello, Lua!

配置(在Lua中访问Nginx变量):
[plain] view plain copy
  1. location =/hello {        content_by_lua '            local who = ngx.var.arg_who            ngx.say("Hello, ", who, "!")        ';}  

输出:
$ curl 'localhost/hello?who=world
$ Hello, world!

Nginx API
Nginx API被封装ngx和ndk两个package中。比如ngx.var.NGX_VAR_NAME可以访问Nginx变量。这里着重介绍一下ngx.location.capture和ngx.location.capture_multi。

ngx.location.capture
语法:res= ngx.location.capture(uri, options?)     用于发出一个同步的,非阻塞的Nginxsubrequest(子请求)。可以通过Nginx subrequest向其它location发出非阻塞的内部请求,这些location可以是配置用于读取文件夹的,也可以是其它的C模块,比如ngx_proxy, ngx_fastcgi, ngx_memc, ngx_postgres, ngx_drizzle甚至是ngx_lua自己。     Subrequest只是模拟Http接口,并没有额外的Http或者Tcp传输开销,它在C层次上运行,非常高效。Subrequest不同于Http 301/302重定向,以及内部重定向(通过ngx.redirection)。

配置:
[plain] view plain copy
  1. location =/other {    ehco 'Hello, world!';}# Lua非阻塞IOlocation =/lua {    content_by_lua '        local res = ngx.location.capture("/other")        if res.status == 200 then            ngx.print(res.body)        end    ';}  

输出:
$ curl  'http://localhost/lua'
$ Hello, world!

实际上,location可以被外部的Http请求调用,也可以被内部的子请求调用。每个location相当于一个函数,而发送子请求就类似于函数调用,而且这种调用是非阻塞的,这就构造了一个非常强大的变成模型,后面我们会看到如何通过location和后端的memcached、redis进行非阻塞通信。
ngx.location.capture_multi

语法:res1,res2, ... = ngx.location.capture_multi({ {uri, options?}, {uri, options?}, ...})     与ngx.location.capture功能一样,可以并行的、非阻塞的发出多个子请求。这个方法在所有子请求处理完成后返回,并且整个方法的运行时间取决于运行时间最长的子请求,并不是所有子请求的运行时间之和。

配置:
[plain] view plain copy
  1. # 同时发送多个子请求(subrequest)location =/moon {    ehco 'moon';}location =/earth {    ehco 'earth';}location =/lua {    content_by_lua '        local res1,res2 = ngx.location.capture_multi({ {"/moon"}, {"earth"} })        if res1.status == 200 then            ngx.print(res1.body)        end        ngx.print(",")        if res2.status == 200 then            ngx.print(res2.body)        end    ';}  


输出:
$ curl  'http://localhost/lua'
$ moon,earth

注意
在Lua代码中的网络IO操作只能通过Nginx Lua API完成,如果通过标准Lua API会导致Nginx的事件循环被阻塞,这样性能会急剧下降。     在进行数据量相当小的磁盘IO时可以采用标准Lua io库,但是当读写大文件时这样是不行的,因为会阻塞整个NginxWorker进程。为了获得更大的性能,强烈建议将所有的网络IO和磁盘IO委托给Nginx子请求完成(通过ngx.location.capture)。     下面通过访问/html/index.html这个文件,来测试将磁盘IO委托给Nginx和通过Lua io直接访问的效率。     通过ngx.location.capture委托磁盘IO:

配置:

[plain] view plain copy
  1. location / {    internal;    root html;}location /capture {    content_by_lua '        res = ngx.location.capture("/")        echo res.body    ';}  
通过标准lua io访问磁盘文件:

配置:
[plain] view plain copy
  1. location /luaio{    content_by_lua '        local io = require("io")        local chunk_SIZE = 4096        local f = assert(io.open("html/index.html","r"))        while true do            local chunk = f:read(chunk)            if not chunk then                break            end            ngx.print(chunk)            ngx.flush(true)        end        f:close()    ';}  

这里通过ab去压,在各种并发条件下,分别返回151bytes、151000bytes的数据,取10次平均,得到两种方式的rps。     静态文件:151bytes
1000 3000 5000 7000 10000  capture  11067 8880 8873 8952 9023  Lua io     11379 9724 8938 9705 9561

静态文件:151000bytes,在10000并发下内存占用情况太严重,测不出结果        这种情况下,文件较小,通过Nginx访问静态文件需要额外的系统调用,性能略逊于ngx_lua。
1000 3000 5000 7000    10000  capture    3338 3435 3178 3043         /  Lua io      3174 3094 3081 2916         /

在大文件的情况,capture就要略好于ngx_lua。      这里没有对Nginx读取静态文件进行优化配置,只是采用了sendfile。如果优化一下,可能nginx读取静态文件的性能会更好一些,这个目前还不熟悉。所以,在Lua中进行各种IO时,都要通过ngx.location.capture发送子请求委托给Nginx事件模型,这样可以保证IO是非阻塞的。

四. 小结

这篇文章简单介绍了一下ngx_lua的基本用法,后一篇会对ngx_lua访问redis、memcached已经连接池进行详细介绍。

五. 进阶

在之前的文章中,已经介绍了ngx_lua的一些基本介绍,这篇文章主要着重讨论一下如何通过ngx_lua同后端的memcached、redis进行非阻塞通信。

Memcached

在Nginx中访问Memcached需要模块的支持,这里选用HttpMemcModule,这个模块可以与后端的Memcached进行非阻塞的通信。我们知道官方提供了Memcached,这个模块只支持get操作,而Memc支持大部分Memcached的命令。 Memc模块采用入口变量作为参数进行传递,所有以$memc_为前缀的变量都是Memc的入口变量。memc_pass指向后端的Memcached Server。

配置:
[plain] view plain copy
  1. #使用HttpMemcModulelocation =/memc {    set $memc_cmd $arg_cmd;    set $memc_key $arg_key;    set $memc_value $arg_val;    set $memc_exptime $arg_exptime;    memc_pass '127.0.0.1:11211';}  
输出:
$ curl  'http://localhost/memc?cmd=set&key=foo&val=Hello'
$ STORED
$ curl  'http://localhost/memc?cmd=get&key=foo'
$ Hello

这就实现了memcached的访问,下面看一下如何在lua中访问memcached。

配置:
[plain] view plain copy
  1. #在Lua中访问Memcachedlocation =/memc {    internal;   #只能内部访问    set $memc_cmd get;    set $memc_key $arg_key;    memc_pass '127.0.0.1:11211';}location =/lua_memc {    content_by_lua '        local res = ngx.location.capture("/memc", {            args = { key = ngx.var.arg_key }        })        if res.status == 200 then            ngx.say(res.body)        end    ';}  

输出:
$ curl  'http://localhost/lua_memc?key=foo'
$ Hello

通过lua访问memcached,主要是通过子请求采用一种类似函数调用的方式实现。首先,定义了一个memc location用于通过后端memcached通信,就相当于memcached storage。由于整个Memc模块时非阻塞的,ngx.location.capture也是非阻塞的,所以整个操作非阻塞。


Redis

访问redis需要HttpRedis2Module的支持,它也可以同redis进行非阻塞通行。不过,redis2的响应是redis的原生响应,所以在lua中使用时,需要解析这个响应。可以采用LuaRedisModule,这个模块可以构建redis的原生请求,并解析redis的原生响应。

配置:
[plain] view plain copy
  1. #在Lua中访问Redislocation =/redis {    internal;   #只能内部访问    redis2_query get $arg_key;    redis2_pass '127.0.0.1:6379';}location =/lua_redis {#需要LuaRedisParser    content_by_lua '        local parser = require("redis.parser")        local res = ngx.location.capture("/redis", {            args = { key = ngx.var.arg_key }        })        if res.status == 200 then            reply = parser.parse_reply(res.body)            ngx.say(reply)        end    ';}  

输出:
$ curl  'http://localhost/lua_redis?key=foo'
$ Hello

和访问memcached类似,需要提供一个redis storage专门用于查询redis,然后通过子请求去调用redis。

Redis Pipeline
在实际访问redis时,有可能需要同时查询多个key的情况。我们可以采用ngx.location.capture_multi通过发送多个子请求给redis storage,然后在解析响应内容。但是,这会有个限制,Nginx内核规定一次可以发起的子请求的个数不能超过50个,所以在key个数多于50时,这种方案不再适用。
幸好redis提供pipeline机制,可以在一次连接中执行多个命令,这样可以减少多次执行命令的往返时延。客户端在通过pipeline发送多个命令后,redis顺序接收这些命令并执行,然后按照顺序把命令的结果输出出去。在lua中使用pipeline需要用到redis2模块的redis2_raw_queries进行redis的原生请求查询。

配置:

[plain] view plain copy
  1. #在Lua中访问Redislocation =/redis {    internal;   #只能内部访问    redis2_raw_queries $args$echo_request_body;    redis2_pass '127.0.0.1:6379';}location =/pipeline {    content_by_lua 'conf/pipeline.lua';}  

pipeline.lua

[plain] view plain copy
  1. -- conf/pipeline.lua filelocal parser=require(‘redis.parser’)local reqs={    {‘get’, ‘one’}, {‘get’, ‘two’}}-- 构造原生的redis查询,get one\r\nget two\r\nlocal raw_reqs={}for i, req in ipairs(reqs)do      table.insert(raw_reqs, parser.build_query(req))endlocal res=ngx.location.capture(‘/redis?’..#reqs, {body=table.concat(raw_reqs, ‘’)})if res.status and res.body then       -- 解析redis的原生响应       local replies=parser.parse_replies(res.body, #reqs)       for i, reply in ipairs(replies)do          ngx.say(reply[1])       endend  

输出:
$ curl  'http://localhost/pipeline'
$ first
  second

Connection Pool

前面访问redis和memcached的例子中,在每次处理一个请求时,都会和后端的server建立连接,然后在请求处理完之后这个连接就会被释放。这个过程中,会有3次握手、timewait等一些开销,这对于高并发的应用是不可容忍的。这里引入connection pool来消除这个开销。 连接池需要HttpUpstreamKeepaliveModule模块的支持。

配置:
[plain] view plain copy
  1. http {    # 需要HttpUpstreamKeepaliveModule    upstream redis_pool {        server 127.0.0.1:6379;        # 可以容纳1024个连接的连接池        keepalive 1024 single;    }    server {        location=/redis {            …            redis2_pass redis_pool;        }    }}  

这个模块提供keepalive指令,它的context是upstream。我们知道upstream在使用Nginx做反向代理时使用,实际upstream是指“上游”,这个“上游”可以是redis、memcached或是mysql等一些server。upstream可以定义一个虚拟server集群,并且这些后端的server可以享受负载均衡。keepalive 1024就是定义连接池的大小,当连接数超过这个大小后,后续的连接自动退化为短连接。连接池的使用很简单,直接替换掉原来的ip和端口号即可。      有人曾经测过,在没有使用连接池的情况下,访问memcached(使用之前的Memc模块),rps为20000。在使用连接池之后,rps一路飙到140000。在实际情况下,这么大的提升可能达不到,但是基本上100-200%的提高还是可以的。

小结

这里对memcached、redis的访问做个小结。 1. Nginx提供了强大的编程模型,location相当于函数,子请求相当于函数调用,并且location还可以向自己发送子请求,这样构成一个递归的模型,所以采用这种模型实现复杂的业务逻辑。 2. Nginx的IO操作必须是非阻塞的,如果Nginx在那阻着,则会大大降低Nginx的性能。所以在Lua中必须通过ngx.location.capture发出子请求将这些IO操作委托给Nginx的事件模型。 3. 在需要使用tcp连接时,尽量使用连接池。这样可以消除大量的建立、释放连接的开销。


五、参考文资料

深入 Nginx:我们是如何为性能和规模做设计的

  http://blog.jobbole.com/88766/

Nginx变量漫谈/配置指令的执行顺序

  http://blog.sina.com.cn/openresty

ngx_lua文档

  https://github.com/openresty/lua-nginx-module#readme

OpenResty最佳实践

  https://moonbingbing.gitbooks.io/openresty-best-practices/content/lua/brief.html

跟我学Nginx+Lua开发

  http://jinnianshilongnian.iteye.com/blog/2190344

构建需求响应式亿级商品详情页

  http://jinnianshilongnian.iteye.com/blog/2235572

京东商品详情页服务闭环实践

  http://jinnianshilongnian.iteye.com/blog/2258111 

Upsync:微博开源基于Nginx容器动态流量管理方案

  http://toutiao.com/a6254279391729139970/  

nginx lua模块常用的指令 - CSDN博客 http://blog.csdn.net/imlsz/article/details/42297383

相关内容

    暂无相关文章