openresty 简介
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)。
openresty 安装
下载页面:http://openresty.org/cn/download.html
# 安装依赖 apt-get install libpcre3-dev \ libssl-dev perl make build-essential curl # 解压源码 tar -xzvf openresty-VERSION.tar.gz # 配置:默认, --prefix=/usr/local/openresty 程序会被 安装到/usr/local/openresty目录。 ./configure make -j2 sudo make install cd ~ export PATH=/usr/local/openresty/bin:$PATH
启动、关闭、重启 openresty
# 指定配置启动 openresty openresty -p . -c conf/nginx.conf # 优雅退出 openresty -p . -s quit # 重启 openresty openresty -p . -s reload
openresty 应用场景
奇虎360的所有服务端团队都在使用,京东、百度、魅族、知 乎、优酷、新浪这些互联网公司都在使用。有用来写 WAF (web application firewall)、有做 CDN 调度、有做广告系统、消息推送系统,API server 的。还有用在非常关键的业务上,比如高可用架构分享的京东商品详情页。
1 在请求真正到达上游服务之前,Lua 可以随心所欲的做复杂的访问控制和安全检测
2 随心所欲的操控响应头里面的信息
3 从外部存储服务(比如 Redis,Memcached,MySQL, Postgres)中获取后端信息,并用这些信息来实时选择哪一个后端来完成业务访问
4 在内容 handler 中随意编写复杂的 Web 应用,使用同步但依然非阻塞的方式,访问后端数据库和其他存储
5 在 rewrite 阶段,通过 Lua 完成非常复杂的 URL dispatch
6 用 Lua 可以为 nginx 子请求和任意 location,实现高级缓存机制
lua-nginx-module
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
一般会在一次请求中被调用多次,因为这是实现基于 HTTP 1.1 chunked 编码的所谓“流式输出”的。
log_by_lua
该阶段总是运行在请求结束的时候,用于请求的后续操作, 如在共享内存中进行统计数据,如果要高精确的数据统计, 应该使用 body_filter_by_lua
嵌入原理
openresty 是在nginx处理的阶段末尾加上我们的方法,补充功能,原来实现的功能不受影响。
责任链模式
ngx.exit(status)
如果status == 0 只打断当前责任链,如果status>=200 则打断整个责任链,直接退出。
ngx.redirect()
cosocket
openresty 为 nginx 添加的最核心的功能就是 cosocket;自 cosocket 加入,可以在 http 请求处理中访问第三方服务; cosocket 主要依据 nginx 中的事件机制和 lua 的协程结合后实现了非阻塞网络 io;在业务逻辑使用层面上可以通过同步非阻塞的方式来写代码;
引入 cosocket 后,nginx 中相当于有了多条并行同步逻辑线 (lua 协程),nginx 中单线程负责唤醒或让出其中 lua 协程; 唤醒或让出依据来源于协程运行的条件是否得到满足;
问题:比较 openresty 、skynet、zvnet 的 lua 虚拟机抽象和 lua 协程抽象?
黑名单用户 nginx.conf
worker_processes 8; events { worker_connections 10240; } http { lua_shared_dict bklist 1m; init_worker_by_lua_file ./app/init_worker.lua; server { listen 9000; location / { access_by_lua_block { local black_list = { ["192.168.44.1"] = true } if black_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_v1.lua (将黑名单ip放入redis)
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
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 一个worker定时往共享内存中刷数据,(黑名单ip)
if ngx.worker.id() ~= 0 then return end local redis = require "resty.redis" local bklist = ngx.shared.bklist 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)
反向代理
worker_processes 8; events { worker_connections 10240; } # http http { server { listen 8989; location / { 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) } } location /jump_here { content_by_lua_block { ngx.say("jump_here hello", "\t", ngx.var.remote_addr) } body_filter_by_lua_block { local chunk = ngx.arg[1] ngx.arg[1] = chunk:gsub("hello", "mark") } } } } 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; } }
cosocket 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