高性能web网关之openresty
- 一、openresty 简介
- 二、openresty 安装
- 三、openresty开发实践 —— content_by_lua 阶段
- 四、openresty开发实践 —— rewrite_by_lua 阶段
- 五、openresty开发实践 —— body_filter_by_lua 阶段
- 六、openresty开发实践 —— 黑名单
- 6.1、基础版
- 6.2、进阶版
- 6.3、高阶版
 
- 七、openresty开发实践 —— 反向代理
- 总结
- 后言
一、openresty 简介
-  openresty 是一个基于 nginx 与 lua 的高性能 web 平台,其内部集成了大量精良的 lua 库、第三方模块以及大数的依赖项。用于方便搭建能够处理超高并发、扩展性极高的动态 web 应用、web 服务和动态网关。 
-  openresty 通过汇聚各种设计精良的 nginx 模块,从而将 nginx有效地变成一个强大的通用 Web 应用平台。这样,可以使用 Lua 脚本语言调动 Nginx 支持的各种C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。 
-  openresty 的目标是让你的 Web 服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型(多reactor 模型),不仅仅对 HTTP 客户端请求(stream),甚至于对远程后端诸如MySQL、PostgreSQL、Memcached 以及 Redis etcd kafka grpc等都进行一致的高性能响应(upstream)。 
二、openresty 安装
(1)下载源压缩包:
wget https://openresty.org/download/openresty-1.21.4.1.tar.gz
(2)安装依赖:
sudo apt-get install libpcre3-dev libssl-dev perl make build-essential curl
(3)解压源码:
tar -xzvf openresty-1.21.4.1.tar.gz
(4)配置:默认, --prefix=/usr/local/openresty 程序会被安装到/usr/local/openresty目录。
cd openresty-1.21.4.1
./configure
(5)编译和安装:
make -j2
sudo make install
(6)设置环境:
cd ~
export PATH=/usr/local/openresty/bin:$PATH
(7)测试:
~$ openresty -v
nginx version: openresty/1.21.4.1
启动、关闭、重启 openresty:
# 指定配置启动 openresty
# 需要指定工作目录,示例中的 . 表示当前目录为工作目录。
openresty -p . -c conf/nginx.conf
# 优雅退出
openresty -p . -s quit
# 重启 openresty
openresty -p . -s reload
三、openresty开发实践 —— content_by_lua 阶段
(1)新建一个项目文件夹,项目文件夹新建三个子文件夹,分别是app、conf、logs,分别用来存放编写的应用程序、配置文件、日志文件。
mkdir my_openresty
cd my_openresty
mkdir app
mkdir conf
mkdir logs
(2)在conf下创建nginx.conf文件,输入以下内容:
worker_processes 2;
events {
    worker_connections 10240;
}
#######################
# 以下为配置块
#######################
# http 协议
http {
    #虚拟主机
    server {
        listen 8989;
        # 处理http请求
        # 捕获和处理
        location / {
            # 内容处理,在配置中写代码
            content_by_lua_block {
                ngx.say("hello","\t",ngx.var.remote_addr)
            }
        }
    }
}
#
# tcp 使用stream
#
# stream{
#    
#}
(3)openresty启动nginx:
openresty -p . -c conf/nginx.conf
(4)查看nginx启动状态:
ps aux | grep nginx
执行结果:
fly       15341  0.0  0.0  33264  1272 ?        Ss   17:23   0:00 nginx: master process openresty -p . -c conf/nginx.conf
fly       15342  0.0  0.3  37516  7276 ?        S    17:23   0:00 nginx: worker process
fly       15343  0.0  0.3  37516  7276 ?        S    17:23   0:00 nginx: worker process
fly       15345  0.0  0.0  15984   968 pts/2    S+   17:23   0:00 grep --color=auto nginx
(5)在浏览器输入服务器IP和端口,可以看到如下的结果:
 
四、openresty开发实践 —— rewrite_by_lua 阶段
nginx.conf文件,输入以下内容:
worker_processes 2;
events {
    worker_connections 10240;
}
#######################
# 以下为nginx配置块
#######################
# http 协议
http {
    #虚拟主机
    server {
        listen 8989;
        # 处理http请求
        # 捕获和处理
        location / {
            # 初始化数据,可以在此阶段加载耗时模块、设置全局变量
            # init_by_lua_block {
            #     gloabl_a=100
            # }
            # 用于执行内部url重写或外部重定向
            rewrite_by_lua_block {
                local args = ngx.req.get_uri_args()
                if args["jump"] == "1" then
                    return ngx.redirect("http://baidu.com")
                elseif args["jump"] == "2" then
                    return ngx.redirect("/jump_here")
                end
            }
            # 内容处理,在配置中写代码
            content_by_lua_block {
                ngx.say("hello","\t",ngx.var.remote_addr)
            }
        }
        #  rewrite_by_lua不止能跳转到外部,也可以内部跳转
        location /jump_here {
            # 内容处理,在配置中写代码
            content_by_lua_block {
                ngx.say("hello, jump_here","\t",ngx.var.remote_addr)
            }
        }
    }
}
#
# tcp 使用stream
#
# stream{
#    
#}
没有启动openresty,则输入如下命令启动:
openresty -p . -c conf/nginx.conf
如果是之前已经启动了,只需要reload即可:
openresty -p . -s reload
五、openresty开发实践 —— body_filter_by_lua 阶段
nginx.conf文件,输入以下内容:
worker_processes 2;
events {
    worker_connections 10240;
}
#######################
# 以下为nginx配置块
#######################
# http 协议
http {
    #虚拟主机
    server {
        listen 8989;
        # 处理http请求
        # 捕获和处理
        location / {
            # 用于执行内部url重写或外部重定向
            rewrite_by_lua_block {
                local args = ngx.req.get_uri_args()
                if args["jump"] == "1" then
                    return ngx.redirect("http://baidu.com")
                elseif args["jump"] == "2" then
                    return ngx.redirect("/jump_here")
                end
            }
            # 内容处理,在配置中写代码
            content_by_lua_block {
                ngx.say("hello","\t",ngx.var.remote_addr)
            }
        }
        #  rewrite_by_lua不止能跳转到外部,也可以内部跳转
        location /jump_here {
            # 内容处理,在配置中写代码
            content_by_lua_block {
                ngx.say("hello, jump_here","\t",ngx.var.remote_addr)
            }
            # 用于修改应答body的内容
            body_filter_by_lua_block {
                local chunk=ngx.arg[1]
                ngx.arg[1]=chunk:gsub("hello","FLY.")
            }
        }
    }
}
#
# tcp 使用stream
#
# stream{
#    
#}
执行效果:
 
六、openresty开发实践 —— 黑名单
黑名单功能一般在access_by_lua阶段实现,实现访问控制。
6.1、基础版
新建nginx_new.conf文件,输入以下内容:
worker_processes 2;
events {
    worker_connections 10240;
}
#######################
# 以下为nginx配置块
#######################
# http 协议
http {
    #虚拟主机
    server {
        listen 8989;
        location / {
            access_by_lua_block {
                local block_list={
                    ["192.168.0.105"]=true
                }
                if block_list[ngx.var.remote_addr] then
                    return ngx.exit(403)
                end
            }
            
             # 内容处理,在配置中写代码
            content_by_lua_block {
                ngx.say("hello","\t",ngx.var.remote_addr)
            }
        }
    }
}
没有启动openresty,则输入如下命令启动:
openresty -p . -c conf/nginx_new.conf
如果是之前已经启动了,只需要reload即可:
openresty -p . -s reload
执行效果:
 
注意,示例中的IP是写死在代码中的,在实际使用中不会这样来,一般存储在其他地方,比如redis。
6.2、进阶版
修改nginx_new.conf文件内容:
worker_processes 2;
events {
    worker_connections 10240;
}
#######################
# 以下为nginx配置块
#######################
# http 协议
http {
    #虚拟主机
    server {
        listen 8989;
        location / {
            # 用于访问控制
            access_by_lua_block {
                local block_list={
                    ["192.168.0.105"]=true
                }
                if block_list[ngx.var.remote_addr] then
                    return ngx.exit(403)
                end
            }
            
             # 内容处理,在配置中写代码
            content_by_lua_block {
                ngx.say("hello","\t",ngx.var.remote_addr)
            }
        }
        location /black_v1 {
            # 用于访问控制,通过文件
            access_by_lua_file ./app/black_v1.lua;
            
            # 内容处理,在配置中写代码
            content_by_lua_block {
                ngx.say("hello","\t",ngx.var.remote_addr)
            }
        }
    }
}
black_v1.lua文件内容为:
local redis = require "resty.redis"
local red=redis:new()
local ok,err=red:connect("127.0.0.1",6379)
if not ok then
    return ngx.exit(301)
end
local ip=ngx.var.remote_addr
local exists,err=red:sismember("black_list",ip)
if exists ==1 then
    return ngx.exit(403)
end
只需要reload即可:
openresty -p . -s reload
注意,要记得先运行redis,同时添加IP地址到KEY中。
127.0.0.1:6379> SADD black_list 192.168.0.105
(integer) 1
127.0.0.1:6379> keys *
1) "black_list"
127.0.0.1:6379> SMEMBERS black_list
1) "192.168.0.105"
127.0.0.1:6379> 
执行效果:
 
 如果不知道有哪些接口可以使用,可以通过如下命令查询:
restydoc resty.redis
虽然把IP存储在了redis中,没有写死在代码里,但是每一次请求都要访问redis,这会导致整个系统的吞吐量降低;可以将这些数据写到共享内存中。
6.3、高阶版
redis+共享内存方式。而且为了保证数据有效性,需要定期将redis中的数据拉取到共享内存中;那么就需要在init_worker_by_lua阶段添加定时器。
修改nginx_new.conf文件内容:
worker_processes 2;
events {
    worker_connections 10240;
}
#######################
# 以下为nginx配置块
#######################
# http 协议
http {
    # 创建共享内存
    lua_shared_dict bklist 1m;
    # 初始化数据,定时器
    init_worker_by_lua_file ./app/init_worker.lua;
    
    #虚拟主机
    server {
        listen 8989;
        location / {
            # 用于访问控制
            access_by_lua_block {
                local block_list={
                    ["192.168.0.105"]=true
                }
                if block_list[ngx.var.remote_addr] then
                    return ngx.exit(403)
                end
            }
            
             # 内容处理,在配置中写代码
            content_by_lua_block {
                ngx.say("hello","\t",ngx.var.remote_addr)
            }
        }
        location /black_v1 {
            # 用于访问控制,通过文件
            access_by_lua_file ./app/black_v1.lua;
            # 内容处理,在配置中写代码
            content_by_lua_block {
                ngx.say("hello","\t",ngx.var.remote_addr)
            }
        }
        location /black_v2 {
            
            # 用于访问控制,通过文件
            access_by_lua_file ./app/black_v2.lua;
            # 内容处理,在配置中写代码
            content_by_lua_block {
                ngx.say("hello","\t",ngx.var.remote_addr)
            }
        }
    }
}
black_v2.lua文件内容为:
local bklist=ngx.shared.bklist
local ip=ngx.var.remote_addr
if bklist:get(ip) then
    return ngx.exit(403)
end
init_worker.lua文件内容为:
-- 只需要一个进程拉取数据即可。
if ngx.worker.id() ~=0 then
    return
end
-- 获取共享内存
local bklist =ngx.shared.bklist
local redis=require "resty.redis"
local function update_blacklist()
    local red=redis:new()
    local ok,err=red:connect("127.0.0.1",6379)
    if not ok then
        return
    end
    local black_list,err=red:smembers("black_list")
    bklist:flush_all()
    for _, v in pairs(black_list) do
        bklist:set(v,true);
    end
    ngx.timer.at(5,update_blacklist)
end
ngx.timer.at(5,update_blacklist)
只需要reload即可:
openresty -p . -s reload
注意,要记得先运行redis,同时添加IP地址到KEY中。
127.0.0.1:6379> SADD black_list 192.168.0.105
(integer) 1
127.0.0.1:6379> keys *
1) "black_list"
127.0.0.1:6379> SMEMBERS black_list
1) "192.168.0.105"
127.0.0.1:6379> 
执行效果:
 
七、openresty开发实践 —— 反向代理
nginx.conf文件内容:
worker_processes 4;
events {
    worker_connections 10240;
}
stream {
    upstream ups {
        server 127.0.0.1:8888;
    }
    server {
        listen 9999;
        proxy_pass ups;
        proxy_protocol on;
    }
    server {
        listen 9000;
        content_by_lua_file ./app/proxy.lua;
    }
}
proxy.lua文件内容:
local sock, err = ngx.req.socket()
if err then
    ngx.log(ngx.INFO, err)
end
local upsock, ok
upsock = ngx.socket.tcp()
ok, err = upsock:connect("127.0.0.1", 8989)
if not ok then
    ngx.log(ngx.INFO, "connect error:"..err)
end
upsock:send(ngx.var.remote_addr .. '\n')
local function handle_upstream()
    local data
    for i=1, 1000 do
        local reader = upsock:receiveuntil("\n", {inclusive = true})
        data, err, _ = reader()
        if err then
            sock:close()
            upsock:close()
            return
        end
        sock:send(data)
    end
end
ngx.thread.spawn(handle_upstream)
local data
while true do
    local reader = sock:receiveuntil("\n", {inclusive = true})
    data, err, _ = reader()
    if err then
        sock:close()
        upsock:close()
        return
    end
    upsock:send(data)
end
没有启动openresty,则输入如下命令启动:
openresty -p . -c conf/nginx.conf
如果是之前已经启动了,只需要reload即可:
openresty -p . -s reload
总结
-  nginx基础上嵌入lua产生openresty,在openresty基础上封装一个框架产生kong。直接在nginx上开发是比较复杂的,需要熟悉nginx的配置、nginx数据结构、nginx模块构建等,很难进行业务开发;nginx基础上嵌入lua产生的openresty可以进行业务的开发;openresty基础上封装一个框架并添加一些分布式特性就产生kong,即分布式api网关。 
-  openresty中引用了luajit,性能上接近c语言。 
-  nginx是多进程架构,master进程会fork出n个(CPU核心数)worker进程;worker进程间通过共享内存通信;worker进程负责处理请求,通过锁解决惊群问题和控制新的连接接入(负载均衡);请求沿着责任链进行处理。 
-  openresty中的lua是嵌入在责任链的最后一层,即openresty是对nginx功能的补充,不会影响原来在nginx上实现的功能。 
-  责任链的打断:ngx.exit()或者ngx.redirect()可以打断责任链。restydoc -s func() 可以查看命令查看接口描述。 
-  nginx中直接反向代理可能造成后端服务器(upstream)无法获取到客户端的IP(客户端IP丢失),这是比较严重的问题,我们需要再转发的时候也要把IP地址转发到后端服务器(使用 proxy protocol v1 或 proxy protocol v2)。 
stream {
	upstream ups {
		server 127.0.0.1:8888;
	}
	server {
		listen 9999;
		proxy_pass	ups;
		proxy_protocol	on;
	}
}
后言
本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对c/c++linux系统提升感兴趣的读者,可以点击链接查看详细的服务:C/C++服务器开发 。



















