原《第13章 Nginx与Openresty》
Nginx
反向代理
Nginx作为一个高性能且高可用的HTTP服务器,反向代理、负载均衡和流量管控是它最拿手的三样本领。
作为反向代理的搜素引擎,可以让用户不必记住并在浏览器的地址栏上输入每种提供服务的网站网址,而只需要在搜索出来的结果上轻点鼠标,即可访问网站服务,这正是典型的反向代理服务。

以Java应用为例,一般都是以8080
端口为入口,但如果想改为80
端口,就可以用Nginx来代理
。
> vi nginx.conf
......
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://localhost:9529;
proxy_set_header Host $host:$server_port;
}
}
......
然后再编写一个简单的Web
应用,用于验证配置Nginx之后的反向代理服务。
@RestController
public class NginxReverseProxy {
@GetMapping("/")
public String index() {
return "Hello Nginx";
}
}
不管是访问http://localhost:9529/,还是访问http://localhost/,都能成功。
负载均衡
除了最基本的反向代理,Nginx还可以提供负载均衡服务,它可以将海量请求分摊到多个服务器上分别执行,从而减轻单台服务器的访问压力,这一能力也使得Nginx成为互联网应用的标准配置。
而负载均衡一般都需要同时配置反向代理,通过反向代理来跳转到指定的服务器上。
Nginx目前支持自带三种负载均衡策略。
轮询访问策略
:这是Nginx默认的负载均衡策略。在这种策略下,每个请求按顺序轮流地分配到不同的后端服务器,如果某些后端服务器宕机或离线,Nginx也能自动剔除它。权重分配策略
:在这种负载均衡策略下,每个请求按指定轮询几率访问后端服务,权重Weight
和访问比率成正比,这种策略常用于后端服务器性能不均的情况,如有些服务器配置高,那么访问权重就可以相应高一些。而配置较低的服务器则访问权重也就相应低一点。它同样也能自动剔除宕机或离线的后端服务器。iphash约束策略
:轮询和权重的方式只能满足无状态的或者幂等的业务应用,但有时业务需要满足某些客户从头到尾只能访问某个指定服务器的条件约束。因此,这种情况就需要采用iphash方式来分配后端服务器了。
可以准备2 ~ 3台虚拟机,在每台虚拟机中安装好JDK
环境,再编写一个最简单的SpringBoot应用,该应用只需让访问者知道Nginx将路由分配到了哪一台服务上即可。
/**
* 一个Springboot应用
*
*/
@RestController
public class NginxLoadBalance {
@GetMapping("/test")
public String test(final String username) {
return "hello, " + username + " from server01";
}
}
/**
* 另一个Springboot应用
*
*/
@RestController
public class NginxLoadBalance {
@GetMapping("/test")
public String test(final String username) {
return "hello, " + username + " from server02";
}
}
这两段代码分别在两个SpringBoot项目中,它们唯一的区别就是server编号不同。
将它们打包成jar文件后,分别部署到其中的两台虚拟机上。再利用其中一台服务器安装Nginx,作为负载均衡服务。
实现Nginx默认的轮询策略,只需要稍稍修改nginx.conf
配置文件即可。
> vi nginx.conf
......
upstream test {
server [ip]:8080
server [ip]:9090
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://test;
proxy_set_header Host $host:$server_port;
}
}
......
重启Nginx,然后重复访问服务地址/test?username=test
。
第n次访问会返回server01。
第n+1次访问会返回server02。
这说明轮询访问策略已经生效。
权重分配策略只需要在配置中加入权重关键字weight
即可。
> vi nginx.conf
......
upstream test {
server [ip]:8080 weight=7
server [ip]:9090 weight=3
}
......
重启Nginx后,在10次访问中,第一个服务会被访问7次,而第二个服务则会被访问3次。
配置iphash
策略则更为简单,只需在upstream
中增加一个ip_hash
关键字即可。
> vi nginx.conf
......
upstream test {
ip_hash
server [ip]:8080
server [ip]:9090
}
......
除了Nginx自带的三种负载均衡策略外,还有另外两种常用的第三方策略,分别是fair
策略和一致性hash
策略,它们可以在Github上下载并安装。
fair
策略是按后端服务器的响应时间来分配请求的,响应时间短的优先分配,有利于服务端的资源利用。一致性hash
策略可以根据参数的不同将请求均匀映射到后端服务器。
它们的配置也和iphash
策略类似,此处就不再赘述。
流量管控
在互联网应用中,很多场景中都会涉及到海量的高并发请求,例如秒杀。如果不对这些请求做限制,那么服务器将很快会被冲垮。就像在12306买票一样,如果全国人民都一窝蜂去抢票,那服务器是无论如何也扛不住这种瞬时压力的。
所谓令牌桶,其算法思想如下。
令牌以固定速率产生,并缓存到令牌桶。
令牌桶放满时,多余的令牌将被直接丢弃。
请求进来时,先进入待处理的请求队列。
处理请求时需要从桶里拿到相应数量的令牌作为处理“凭证”。
当桶里没有令牌时,请求处理被拒绝。

令牌桶是一种常用于网络流量整形和速率限制的算法,只有持有令牌的请求才会被处理,这也是令牌桶名称的由来。由于它允许突发流量的存在,所以更适合流量突发的应用场景,例如,秒杀。
相比于令牌桶,漏桶限流的核心算法思想是缓存请求、匀速处理、多余丢弃
。正如其名,漏桶不管外部水量是否突然增加或减少,其底部始终保持着匀速的出水量,这正是漏桶算法名称的由来。

可以看到,水(也就是请求)从上方进入漏桶,从下方流出(被处理)。来不及流出的流量会被缓存在桶中,以固定速率流出,桶满后多余的水(流量)则会溢出(被丢弃)。因此,漏桶算法可以屏蔽流量的陡然变化,所以它更适合需要平滑流量的场景。
Nginx限流模块使用的正是漏桶算法,它有两种实现限流的方式。
限制访问频率,就是限制指定时间内每个用户的访问次数。
限制并发连接数,就是限制某段时间内访问资源的用户数。
为了测试Nginx的限流效果,可以安装apache的ab
压测工具,执行以下命令即可。
> yum -y install httpd-tools
> cd /usr/bin
> ab -V
ab
的使用也非常简单。
> ./ab -n1000 -c100 -t1 -s5 http://localhost/test?username=test`
这条命令表示:执行1000次,每次有100个并发请求到指定服务,并在1秒之内完成请求,超时时间5秒。
测试后的结果如下图所示。

压测工具准备好之后,再来修改Nginx配置。
> vi nginx.conf
limit_req_zone $binary_remote_addr zone=case1:10m rate=10r/s;
server {
listen 80;
server_name localhost;
location / {
limit_req_zone=case1;
}
}
然后执行下面的两条命令。
> ./ab -n1000 -c10 -t1 -s5 http://localhost/test?username=test
> ./ab -n1000 -c10 -t2 -s5 http://localhost/test?username=test
压测输出的结果显示Complete requests:30397;Failed requests:30387
。
从结果来看,请求了30397次,但失败了30387次,仅10次并发请求成功,这完全符合Nginx设置的限流要求。
既然Nginx可以限制流量,那是不是也可以拓展流量呢?
答案是完全可以,这就是其流量拷贝功能。其实对于流量拷贝的需求场景还是比较多的,例如,为了确保开发出来的应用能够立即应用到生产环境,就需要将生产环境的数据和流量拷贝到开发环境,这样做的好处显而易见。
可以验证功能是否正常,以及服务的性能。
用真实有效的流量请求去验证,又不用造数据,不影响线上正常访问。
可以用来排查线上问题,同时,这也是一种测试方式。
这可以理解为给流量拉分支
。

修改Nginx配置。
> vi nginx.conf
server {
listen 8080;
access_log /home/work/logs/nginx/8080.log;
}
server {
listen 9090;
access_log /home/work/logs/nginx/9090.log;
}
server {
listen 80;
server_name localhost;
location / {
mirror /mirror1;
mirror /mirror2;
proxy_pass http://test;
}
location = /mirror1 {
proxy_pass http://127.0.0.1:8080;
}
location = /mirror2 {
proxy_pass http://127.0.0.1:9090;
}
}
重启Nginx服务后,可以在远程终端中执行tail
命令,查看端口的日志输出。
> tail -f /home/work/logs/nginx/8080.log
> tail -f /home/work/logs/nginx/9090.log
在浏览器中访问http://nginx服务地址/test?username=test,并观察服务器端口日志输出的变化。

可以看到,mirror1
和mirror2
拷贝了/test
的访问流量。
OpenResty的协程
进程一般是应用程序的启动实例,进程拥有代码、打开的文件、需处理的数据、独立的内存空间等资源,例如,独立部署的jar
包、运行的Redis、MongoDB程序等,它们都是独立运行的进程,它相当于一个大管家。
而线程从属于进程,是应用程序的实际执行者,它相当于是做具体工作的家丁或者仆役。一个进程至少包含一个主线程,或者包含多个子线程,线程拥有自己的栈空间。
协程是一种比线程更加轻量级的存在,是线程中的线程
,协程也拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协程共享全局变量和其它内容。

和线程由CPU调度不同,协程不被操作系统管理,而是完全由线程内部控制,由程序显式的进行,需要多个程序彼此协作才能实现功能,这就是协程名字的由来。协程是通过特殊的函数来实现的——这个特殊的函数可以在某个地方挂起
,之后可以重新在其他地方继续运行。
一个线程之内可有多个这样特殊的函数,也就是可以有多个协程同时运行,但多个协程的运行只能是串行
的——一个协程运行时,其他协程必须要挂起。
协程是Lua中引入的概念,由于OpenResty是对Lua的封装,因此也自然就具备了协程特性。
Coroutine库
resume()
和yeild()
这两个方法是Lua协程的核心,它们都是由coroutine
提供的方法。
coroutine.create(function)
:表示传入一个函数作为参数来创建协程,返回coroutine
,当遇到resume()
时就唤醒函数调用。coroutine.resume(coroutine [v1, v2, ...])
:它是协程的核心函数,用来启动或再次启动一个协程,使其由挂起状态变成运行状态,该函数相当于在执行协程中的方法,v1, v2, ...
是执行协程时传递给它的参数。首次执行协程
coroutine
时,参数v1...
会传递给协程的函数。再次执行协程
coroutine
时,参数v1...
会作为协程yeild()
的返回值。resume()
返回值有三种情况。如果协程的函数执行完毕,协程正常终止,就返回
true
和函数的返回值。如果协程的函数在执行时,协程调用了
yeild()
方法,那么resume()
返回true
和传入函数的第一个参数。如果协程在执行过程中发生错误,那么
resume()
返回false
与错误消息。
coroutine.yield()
:使正在执行的函数挂起,传递给yeild()
的参数会作为resume()
的额外返回值。
也就是说,首次调用时,resume()
的参数会直接传递给协程函数;非首次调用时,resume()
的另一个参数会成为yield()
的返回值,而yield()
的参数也会成为resume()
额外的返回值。
这么说有点绕,画张图就明白了。

为了验证coroutine.create()
和coroutine.resume()
方法,在/usr/local/openresty/nginx/conf
文件夹中创建一个名为xiecheng.conf
的文件,并在其中加入如下代码。
> vi xiecheng.conf
server {
listen 80;
server_name _;
location /xiecheng {
default_type 'text/html';
content_by_lua '
co = coroutine.create(function (a, b)
ngx.print("resume args : "..a..", "..b.." - ")
yreturn = coroutine.yield()
ngx.print ("yreturn : "..yreturn.." - ")
end)
ngx.print(coroutine.resume(co, 1, 2), "<br/>")
ngx.print(coroutine.resume(co, 3, 4), "<br/>")
ngx.print(coroutine.resume(co, 5, 6))
';
}
}
随后修改/usr/local/openresty/nginx/conf/nginx.conf
,找到include lua.conf
这一行,并将其内容改为include xiecheng.conf
。
重启OpenResty,在浏览器中访问http://虚拟机IP地址/xiecheng,可以看到三次调用coroutine.resume()
方法的结果都不同,而这正是resume()
返回值的三种情况。

另一个比较重要的方法是coroutine.wrap(function)
,它返回的是一个函数,每次调用这个函数就相当于在调用coroutine.resume()
。
调用这个函数时传入的参数,就相当于在调用resume()
时传入除协程句柄外的其他参数。但跟resume()
不同的是,它并不是在保护模式下执行的,若执行崩溃会直接向外抛出异常。
修改xiecheng.conf
文件,在其中加入如下代码。
> vi xiecheng.conf
......
location /xiecheng2 {
default_type 'text/html';
content_by_lua '
co = coroutine.wrap(function (a, b)
ngx.print("resume args : "..a..", "..b.." - ")
yreturn = coroutine.yield()
ngx.print ("yreturn : "..yreturn.." - ")
end)
ngx.print("co type is : "..ngx.print(type(co)), "<br/>")
ngx.print(ngx.print(co(1, 2)), "<br/>")
ngx.print(ngx.print(co(3, 4)))
';
}
重启OpenResty,在浏览器中访问http://虚拟机IP地址/xiecheng2,可以看到结果如下,这说明warp()
方法和调用coroutine.resume(co, a, b)
的结果是等效的。

coroutine
库中其他与协程相关的方法还包括下面这些。
isyieldable()
:表示如果正在运行的协程可以挂起,则返回true
(只有主协程(线程)和C函数是无法让出的)。running()
:用来判断当前执行的协程是不是主线程,如果是就返回true
。status(function)
:返回表示协程状态的字符串。running
:正在执行中的协程。suspended
:还未结束却被挂起(调用了yeild
或还没开始运行)的协程。normal
:协程Aresume()
协程B时,协程A所处的状态就是normal
。dead
:发生错误或正常终止的协程,如果这时候对它调用resume
,将返回false
和错误消息,就像刚才展示的那样。
验证这些方法的代码笔者已经写在了xiecheng.conf
文件的xiecheng3
方法中。
> vi xiecheng.conf
location /xiecheng3 {
default_type 'text/html';
content_by_lua '
function status(a)
ngx.print(a.." - r1 status : "..coroutine.status(r1)..", r2 status : "..coroutine.status(r2), "<br/>")
end
r1 = coroutine.create(function(a)
ngx.print("r1 arg is : "..a, "<br/>")
status(2)
local rey = coroutine.yield("r1")
ngx.print("yeild r1 return is " .. rey, "<br/>")
status(4)
ngx.print("point 1<br/>")
coroutine.yield("b")
ngx.print("point 2<br/>")
end)
r2 = coroutine.create(function(a, b)
ngx.print("r2 arg is : "..a..", "..b, "<br/>")
status(1)
local stat, res1 = coroutine.resume(r1, 1)
ngx.print("resume r1 return is : "..res1, "<br/>")
status(3)
local stat2, res2 = coroutine.resume(r1, 2)
ngx.print("resume r1 again return is : "..res2, "<br/>")
status(31)
local stat3, res3 = coroutine.resume(r1, 3)
ngx.print("resume r1 again2 return is : "..res3, "<br/>")
local arg = coroutine.yield("r2")
end)
stat, mainre = coroutine.resume(r2, 1, 2)
status(5)
ngx.print("last return is "..mainre, "<br/>")
';
}

从上图及代码,可以稍微总结一下关于OpenResty协程的知识点了。
所有的协程都是通过
resume()
和yield()
这两个方法来完成协作的,这是协程的核心所在,可以说没有它们,就没有协程。resume()
和yield()
都是由开发者控制的,除此之外,不会有任何其他外部干预,但线程就不一样。函数从哪里挂起,恢复时就从哪里开始执行。关于这一点,可以尝试在
r1
中的coroutine.yield("b")
前后各加上一行ngx.print()
语句来验证。
并行调度
无论有多少个方法,如果不加干预,在Lua中都会始终以串行
的方式来执行。
为了验证这一特性,修改/usr/local/openresty/nginx/conf/lua.conf
文件,在其中增加如下代码。
> vi lua.conf
......
location /test1 {
default_type 'text/html';
echo_sleep 2;
echo test1 : $arg_test;
}
location /test2 {
default_type 'text/html';
echo_sleep 2;
echo test2 : $arg_test;
}
......
将之前修改过的/usr/local/openresty/nginx/conf/nginx.conf
再改回include lua.conf
。
在浏览器中访问http://虚拟机IP地址/test1?test=1或者http://虚拟机IP地址/test2?test=1,可以看到,test1或test2都是在经过2秒之后浏览器才给出响应。

在lua.conf
中再增加一个接口,让它顺序地访问/test1
和/test2
这两个接口。
> vi lua.conf
......
location /allexecute {
default_type 'text/html';
content_by_lua '
local t1 = ngx.now()
local r1 = ngx.location.capture("/test1", {args = ngx.req.get_uri_args()})
local r2 = ngx.location.capture("/test2", {args = ngx.req.get_uri_args()})
local t2 = ngx.now()
ngx.print(r1.body, "<br/>", r2.body, "<br/>", tostring(t2 - t1))
';
}
......
重启OpenResty,在浏览器中访问http://虚拟机IP地址/allexecute?test=1”,可以看到如下结果,这说明/test1
和/test2
确实是串行执行的。

但这种串行方式,可以通过capture_multi
改变,实现并行执行。
在lua.conf
中再增加一个接口,让它通过capture_multi
并行地执行/test1
和/test2
接口。
> vi lua.conf
......
location /concurrency {
default_type 'text/html';
content_by_lua '
local t1 = ngx.now()
local r1, r2 = ngx.location.capture_multi({
{"/test1", {args = ngx.req.get_uri_args()}},
{"/test2", {args = ngx.req.get_uri_args()}}
})
local t2 = ngx.now()
ngx.print(r1.body, "<br/>", r2.body, "<br/>", tostring(t2 - t1))';
}
......
重启OpenResty,在浏览器中访问http://虚拟机IP地址/concurrency?test=1”,可以看到如下结果,这次/test1
和/test2
已经并行执行了。

OpenResty与LUA
感谢支持
更多内容,请移步《超级个体》。