高性能Web网关:OpenResty 基础讲解
一:概述
OpenResty是由国人章亦春开发的一个基于Nginx的可伸缩的Web平台。
openresty 是一个基于 nginx 与 lua 的高性能 web 平台,其内部集成了大量精良的 lua 库、第三方模块以及大数的依赖项。用于方便搭建能够处理超高并发、扩展性极高的动态 web 应用、web 服务和动态网关。
openresty 通过汇聚各种设计精良的 nginx 模块,从而将 nginx有效地变成一个强大的通用 Web 应用平台。这样,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)。
让我们先复习一下Nginx
Nginx采用的是master-worker模型,也就是一个master进程管理多个worker进程,基本的时间处理都放在worker进程中,master进程负责全局初始化以及对worker进行的管理。
OpenResty中,每个worker进程使用一个LuaVM,当请求被分配到worker时,将在这个LuaVM中创建一个coroutine协程,协程之间数据隔离,每个协程都具有独立的全局变量。
二:应用场景
在请求真正到达上游服务之前,Lua 可以随心所欲的做复杂的访问控制和安全检测;
随心所欲的操控响应头里面的信息;
从外部存储服务(比如 Redis,Memcached,MySQL,Postgres)中获取后端信息,并用这些信息来实时选择哪一个后端来完成业务访问;
在内容 handler 中随意编写复杂的 Web 应用,使用同步但依然非阻塞的方式,访问后端数据库和其他存储;
在 rewrite 阶段,通过 Lua 完成非常复杂的 URL dispatch用 Lua 可以为 nginx 子请求和任意 location,实现高级缓存机制;
三:Nginx的模块
nginx 采用模块化设计,使得每一个 http 模块可以仅专注于完成一个独立的、简单的功能,而一个请求的完整处理过程可以由无数个 http 模块共同合作完成。为了灵活有效地指定下一个http 处理模块是哪一个;http 框架依据常见的的处理流程将处理阶段划分为 11 个阶段,其中每一个阶段都可以由任意多个http 模块流水式地处理请求。
openresty 将 lua 脚本嵌入到 nginx 阶段处理的末尾模块下;这样以来并不会影响 nginx 原有的功能,而是在 nginx 基础上丰富它的功能;
嵌入 lua 的优点是:使用 openresty 开发,不需要重新编译,直接修改 lua 脚本,重新启动即可
Lua模块的指令顺序
init_by_lua:在 nginx 重新加载配置文件时,运行里面 lua 脚本,常用于全局变量的申请。例如 lua_shared_dict 共享内存的申请,只有当 nginx 重启后,共享内存数据才清空,这常用于统计。
set_by_lua:设置一个变量,常用与计算一个逻辑,然后返回结果,该阶段不能运行Output API、Control API、Subrequest API、Cosocket API
rewrite_by_lua:在 access 阶段前运行,主要用于 rewrite url;
access_by_lua:主要用于访问控制,这条指令运行于 nginx access 阶段的末尾,因此总是在 allow 和 deny 这样的指令之后运行,它们同属 access 阶段。可用来判断请求是否具备访问权限;
content_by_lua:阶段是所有请求处理阶段中最为重要的一个,运行在这个阶段的配置指令一般都肩负着生成内容(content)并输出HTTP 响应。
header_filter_by_lua:一般只用于设置 Cookie 和 Headers 等。
body_filter_by_lua:一般会在一次请求中被调用多次,因为这是实现基于 HTTP1.1 chunked 编码的所谓“流式输出”的。
log_by_lua:该阶段总是运行在请求结束的时候,用于请求的后续操作,如在共享内存中进行统计数据,如果要高精确的数据统计,应该使用 body_filter_by_lua
Lua嵌入的原理:
cosocket:
openresty 为 nginx 添加的最核心的功能就是 cosocket;自cosocket 加入,可以在 http 请求处理中访问第三方服务;
cosocket 主要依据 nginx 中的事件机制(reactor)和 lua 的协程结合后实现了非阻塞网络 io;在业务逻辑使用层面上可以通过同步非阻塞的方式来写代码;
对于reactor中新的连接进来,由于是非阻塞的,刚连接进来并不知道是否是连接成功,他会一直到添加到epoll红黑树中去,并注册写事件,当触发写事件,我们才知道客户端连接成功了。而对于lua的协程来说,他是同步的,会一直阻塞等待连接成功。因此将这俩一结合,实现了非阻塞的网络IO。当代码运行到阻塞连接的时候,lua协程会让出操作,然后运行其他程序,当连接成功,会再次恢复操作。这样就形成同步非阻塞的方式了。
引入 cosocket 后,nginx 中相当于有了多条并行同步逻辑线(lua 协程),nginx 中单线程负责唤醒或让出其中 lua 协程;唤醒或让出依据来源于协程运行的条件是否得到满足;
四:实现重定向和黑名单
1:nginx.conf
openresty -p . -c conf/nginx.conf //启动
openresty -p . -s stop //关闭
在下面的代码实现中一直遵循上面的11个阶段执行 。当碰到启动不了openresty的时候,多看看他的出错日志即可。
我们在这个conf文件中实现了黑名单功能,以及重新定向功能。我们在location / 中实现了最基础的黑名单功能,他的局限就在于,每次添加黑名单就需要我们重新启动openresty,十分不方便。并且在这里还实现了重新定向的功能,跳到百度或者自己写的代码中。
worker_processes 4;events{worker_connections 1024;
}http{error_log ./logs/error.log info; #在这里我们需要自己手动创建logslua_shared_dict bklist 1m; #由于每次都连接数据库很浪费资源,于是我们使用共享内存,这里使用的是字典init_worker_by_lua_file ./appp/init_worker.lua; #我们在启动openrest的时候,把他在init_worker的时候加入进去。server{listen 8989;location / {rewrite_by_lua_block { --执行重定向功能local args = ngx.req.get_uri_args() -- 获取参数if args["jump"] == "1" thenreturn ngx.redirect("http://baidu.com")elseif args["jump"]=="2" thenreturn ngx.redirect("/jump_here") -- 重定向到/jump_hereend}access_by_lua_block { -- 访问控制local black_list = { --创建黑名单,但是在这里我们每次添加新的黑名单["192.168.137.1"] = true --就需要重新启动,很麻烦,因此我们选择使用数据库-redis}if black_list[ngx.var.remote_addr] thenngx.exit(403)end}content_by_lua_block { -- 内容生成器ngx.say("hello", "\t", ngx.var.remote_addr)}}location /jump_here{content_by_lua_block {ngx.say("jump_here", "\t", ngx.var.remote_addr)}body_filter_by_lua_block { --修改body的内容local chunk = ngx.arg[1]ngx.arg[1] = chunk:gsub("jump_here", "xinqingpu")}log_by_lua_block {local request_method = ngx.var.request_methodlocal request_uri = ngx.var.request_urilocal msg = string.format("request_method:%s\trequest_uri:%s", request_method, request_uri)ngx.log(ngx.INFO,msg) --输出到日志中去}}location /back1 {access_by_lua_file ./appp/back1.lua; --加载我们自己写的文件:黑名单文件content_by_lua_block {ngx.say("back1", "\t", ngx.var.remote_addr)}}location /back2 {access_by_lua_file ./appp/back2.lua; content_by_lua_block {ngx.say("back2", "\t", ngx.var.remote_addr)}}}
}
2:完善黑名单的功能
由于上面的黑名单十分垃圾,所以我们采用数据库:redis的方式。我们将黑名单存储到数据库中,这样我们就不需要重新启动了,但是还有一个问题,就是我们每次访问都需要加载数据库,会很浪费时间,以及影响性能。
--这个模块是使用Redis的,这样只需要我们将黑名单的参数添加到redis中即可,不用重启了
local redis = require "resty.redis" --加载Redis的模块local red = redis:new() --创建一个Redis对象local ok ,err = red:connect("127.0.0.1",6379) --连接Redis
if not ok thenreturn ngx.exit(301) --exit里面的参数只要大于200,就会被nginx认为是错误的,会打断全部的断点。
end --而小于200只会打断当前所在的断点。
--断点就是跳出当前的运行位置,如果全部打断,那就打断全部的阶段,打断一个的话,跳出当前的阶段进入到下一个中,因为有11个阶段
local ip = ngx.var.remote_addrlocal exists,err = red:sismember("black_list",ip) --访问哪个表,传入参数if exists == 1 then --在黑名单中return ngx.exit(403)
end
3:使用共享内存完美解决
因为nginx运行在共享内存中,所以我们把数据也存到共享内存中,提高速度,并减少redis的访问次数。
--这个模块在开始的时候就加载了
if ngx.worker.id() ~= 0 then --在这里我们要先判断worker的数量,因为在这个位置我们还没将master进行fork,所以work的数量为0return --我们指的当前位置是在11个阶段当中的位置
endlocal redis = require "resty.redis"
local bklist = ngx.shared.bklist --加载一下共享内存local function update_blacklist() --更新黑名单,我们这里是使用共享内存访问redis,每几秒刷新一下。local red = redis:new()local ok, err = red:connect("127.0.0.1", 6379)if not ok thenreturnendlocal black_list,err = red:smembers("black_list")bklist:flush_all() --先清空一下共享内存for _, v in pairs(black_list) do --加载共享内存bklist:set(v, true)endngx.timer.at(5, update_blacklist) --每5秒刷新一次,将此函数加载到ngx的定时器中。
endngx.timer.at(5, update_blacklist) --开始调用
我们调用的是下面的函数库,上面的是用来初始化所有的worker进程的。
local bklist = ngx.shared.bklistlocal ip = ngx.var.remote_addrif bklist:get(ip) then --如果我们的ip被查到在黑名单中就报错return ngx.exit(403)
end
五:实现负载均衡
我们还可以使用openresty来实现负载均衡,其中我们先使用最简单的方式,也就是手写需要转发的ip地址,然后下面需要我们使用lua代码动态加载服务器或者转发的地址。
worker_processes 4;events {worker_connections 1024;
}http {error_log ./logs/error.log info;upstream ups {server 192.168.137.132:8100; --他的后面还是别的服务器server 192.168.137.132:8200;server 192.168.137.132:8300;}server {listen 8999;location / {proxy_pass http://ups;}}
}#下面全都是tcp裸连接
stream {upstream tcp_ups {server 127.0.0.1:8888; --我们可以把我们需要连接的服务器,放在这里,我们通过tcp进行连接这里,会连接到我们需要的服务器。}server {listen 9000;proxy_pass tcp_ups;proxy_protocol on; --我们采用这个,实现ip地址的保留}#为了更加方便的添加转发服务器,我们将服务器放到数据库中server {listen 9001;content_by_lua_file ./app/proxy.lua; --这里是动态加载我们的服务器}
}
下面的代码是我们转发过来的地址,我们会接收转发过来的ip地址。我们会发现ip地址丢失了。这里的丢失是我们客户端的ip丢失了。
worker_processes 4;events {worker_connections 1024;
}http {error_log ./logs/error2.log info;server {listen 8100;location / {content_by_lua_block {local headers = ngx.req.get_headers()local cjson = require "cjson"ngx.say(cjson.encode(headers))ngx.say(8100,"\t",ngx.var.remote_addr)}}}server {listen 8200;location / {content_by_lua_block {local headers = ngx.req.get_headers()local cjson = require "cjson"ngx.say(cjson.encode(headers))ngx.say(8200,"\t",ngx.var.remote_addr)}}}server {listen 8300;location / {content_by_lua_block {local headers = ngx.req.get_headers()local cjson = require "cjson"ngx.say(cjson.encode(headers))ngx.say(8300,"\t",ngx.var.remote_addr)}}}}
下面就是我们lua的代码了,我们可以动态的加载不同的服务器了,不需要我们手动写道openresty中了,并且我们通过这个代码,还可以做到保留原客户端的ip地址不丢失。
--这里的nginx既有服务端也有客户端的性质,所以有两个套接字,一个用于连接redis,一个用于接收客户端的数据
local sock, err = ngx.req.socket() --创建nginx的套接字,用来连接redisif err thenngx.log(ngx.INFO, err)
endlocal upsock, okupsock = ngx.socket.tcp() --创建客户端连接上来的套接字,客户端是tcp,所以这里也是tcp的套接字ok, err = upsock:connect("127.0.0.1", 8989) --这里是redis的端口,通过tcp套接字来连接这个redis。
if not ok thenngx.log(ngx.INFO, "connect error:"..err)
endupsock:send(ngx.var.remote_addr .. '\n') --由于会丢失ip,我们在这里写下客户端源地址,并发送给redislocal function handle_upstream()local datafor i=1, 1000 dolocal reader = upsock:receiveuntil("\n", {inclusive = true}) --全都是存的服务器data, err, _ = reader()if err thensock:close()upsock:close()returnendsock:send(data) --我们从redis中获取一个服务器,然后我们向这个服务器发送数据。 end
endngx.thread.spawn(handle_upstream) --创建线程,后台运行这个函数local datawhile true dolocal reader = sock:receiveuntil("\n", {inclusive = true})data, err, _ = reader()if err thensock:close()upsock:close()returnendupsock:send(data) --这里是一直接收客户端连接nginx的数据,然后再将收到的数据再发送给客户端。回报文。
end
--实现了一个简单的代理功能,用于在 Nginx 服务器和 Redis 服务器之间转发数据。它使用了 Nginx 的 Lua 模块和 Lua 的套接字库来实现网络通信。
本文讲解的是Openresty,其中主要是Lua的使用,重新定向,黑名单,负载均衡,对于我本人来说,对于nginx写一些模块并没有使用openresty方便,因为nginx需要使用c语言来写,然后加载动态库等等,它可以直接使用lua进行编程,十分方便。0voice · GitHub