OpenResty (简称OR) 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的Lua Api,第三方模块以及常用的依赖项,基于这些能力,我们可以利用OR快速方便的搭建能够处理超高并发的,极具动态性和扩展的Web应用、Web服务和动态网关。
这篇文章选择OR中具有重要意义的一些模块、命令、Api和框架介绍如下:
基础的Http处理
OR 作为一款 Web 服务器,提供了如:
- ngx.print() | ngx.say()
- ngx.req.get_headers()
- ngx.resp.get_headers()
- ngx.header.HEADER
- ngx.exit()
等函数和方法控制http请求的输入和输出,我们可以通过这些 api 灵活的控制 Web Server 中包括输入输出,转发到上游或者发起子请求在内的各个环节。
ngx.print
同ngx.say(), 通过该 api 可以指定返回的请求的消息体,其中每一次函数的调用,都是在nginx的输出链上增加了一个结点,所以我们可以多次调用该函数而不用担心后者会覆盖前者,如:
location =/hello {
content_by_lua_block {
ngx.print("Hello")
ngx.print(" ")
ngx.print("World!")
}
}
GET /hello 将会得到如下结果
======
Hello World!
请求头和响应头控制
ngx.exit() 和 ngx.status
ngx.exit,通过该 api可以指定请求响应体的 http status, 如:
location =/error {
content_by_lua_block {
return ngx.exit(500)
}
}
GET /error
======
500 Internal Server Error
500 Internal Server Error
openresty
当我们访问 /error 接口的时候,可以得到一个默认的 500 页面,如上所示,当然我们可以通过ngx.status + ngx.print 的组合方式得到自定义的错误页面,如:
location =/error {
content_by_lua_block {
ngx.status = 500
ngx.print("error here!")
return ngx.exit(500)
}
}
GET /erro
======
error here!
以上方式同样适用于其它错误,如 404、502、503 等页面,通过这样的方式我们可以更灵活的控制当这些错误,从而返回更友好的页面或者其它输出,当然你也可用使用 nginx 原生的 error_page 指令对错误页面进行控制,只是灵活性相对差一些
ngx.timer.at
在OR内部通过 nginx 事件机制实现的定时器,我们可以通过它来实现延迟运行的任务逻辑,甚至于通过一些特殊的调用方法实现定时任务的功能,比如:
local delay = 5
local handler
handler = function (premature)
-- do some routine job in Lua just like a cron job
if premature then
return
end
local ok, err = ngx.timer.at(delay, handler)
if not ok then
ngx.log(ngx.ERR, "failed to create the timer: ", err)
return
end
end
local ok, err = ngx.timer.at(delay, handler)
if not ok then
ngx.log(ngx.ERR, "failed to create the timer: ", err)
return
end
可以看到我们给ngx.timer.at的第二个参数是这个函数体本身,通过这样的写法,我们可以实现每个delay的间隔执行一次handler
当然这种“不友好”的技巧在v0.10.9这个版本之后被ngx.timer.every给“优化”了(上述技巧依旧有效)。
当然除此之外,这个API还有其它“非凡”的意义。
OR的各个API其实都有其“生命周期”或者说作用域,比如其中非常重要的cosocket,它的作用域如下
rewrite_by_lua , access_by_lua, content_by_lua , ngx.timer., ssl_certificate_by_lua , ssl_session_fetch_by_lua
也就是说我们只能在上述阶段使用 cosocket,由于很多第三方组件的实现都是基于它,比如
resty-redis,resty-memcached,resty-mysql
所以 cosocket 的作用域就等同于上述这些依赖它的模块的作用域
我们再来看看ngx.timer.at的作用域
init_worker_by_lua , set_by_lua, rewrite_by_lua , access_by_lua, content_by_lua*,
header_filter_by_lua , body_filter_by_lua, log_by_lua , ngx.timer., balancer_by_lua*,
ssl_certificate_by_lua , ssl_session_fetch_by_lua, ssl_session_store_by_lua*
这意味着,如果我们希望在 cosocket 不支持的作用域内使用它,那么我们可以通过 ngx.timer.at 的方式桥接,完成“跨域”, 比如:
init_worker_by_lua_block {
local handler = function()
local redis = require "resty.redis"
redc = redis:new(anyaddr)
redc:set('foo', 'bar')
end
ngx.timer.at(0, handler)
}
这就是前文所说的,这个API的”非凡“意义,当然我更期望的是 cosocket 能够支持更多的作用域 ^_^!
跨进程共享 ngx.shared.DICT
通常来说,我们会将一些系统的配置写入lua代码中,通过require的方式在不同worker之间共享,这种看起来像是跨worker的共享方式其实是利用了”一个模块只加载一次“的特性(lua_code_cache on), 但是这种方式只适合于一些只读数据的共享,并不适合一些可写的(严格来说, 可以通过一些技术手段实现这个级别的缓存,由于能实现table类型的缓存,它的效率并不差,这里不做讨论),或者动态变化的数据,比如从关系型数据库、kv数据库,上游、第三方服务器获取的数据的缓存。
ngx.shared.DICT 方法集提供了解决跨 worker 且自带互斥锁的共享数据方案,配合 cjson 或者 resty-dkjson 这样的 json 解析库,极大丰富了这个API集合的应用空间,需要注意的是:
- 它需要在nginx的配置文件中被预先定义好大小且不可动态扩容, 如: lua_shared_dict cache 16m;
- 尽可能的使用 cjson 而非 dkjson, 它们之间效率相差 50 倍以上;
- 它是一个内存数据集,并不具备持久化的能力;
- 宁愿定义多个容量更小的存储体,而非一个更大的;
ssl_certificate_by_lua_xxx
nginx 是支持SNI的,所以我们可以在一台主机上为同一个IP、不同域名的“租户”绑定不同的证书,你只需要在nginx的配置中设置多个server block, 并为每个 block 配置server_name以及指定其证书和私钥即可;
http
{
server {
listen 443 ssl;
server_name www.foo.com;
ssl_certificate cert/foo.pem;
ssl_certificate_key cert/foo.key;
location / {
proxy_pass http://foo;
}
}
server {
listen 443 ssl;
server_name www.bar.com;
ssl_certificate cert/bar.pem;
ssl_certificate_key cert/bar.key;
location / {
proxy_pass http://bar;
}
}
}
虽然上述方式可行,但是在更新证书的时候就会显得非常麻烦,特别是当你拥有多个主机且每个主机上不止一个证书的时候,证书的逐个替换,nginx 服务的重启都是非常麻烦且容易出错的步骤,即便是通过自动化的代码完成上述步骤,但是多个主机的更换证书的协调性问题依然不容忽视。
ssl_certificate_by_lua_xxx 确实提供了一种更优雅的方式
前文提到的 cosocket 在 ssl_certificate_by_lua_xxx 阶段是被支持的,所以我们可以在这个阶段动态的为终端加载其对应的证书,如:
server {
listen 443 ssl;
server_name test.com;
# 虽然我们可以动态的加载证书,但是为了避免nginx报错,这里需要配置用于占位的证书和私钥
ssl_certificate placeholder.crt;
ssl_certificate_key placeholder.key;
ssl_certificate_by_lua_block {
local ssl = require "ngx.ssl"
-- 清理掉当前的证书,为后续加载证书做准备
local ok, err = ssl.clear_certs()
if not ok then
ngx.log(ngx.ERR, "clear current cert err:", err)
return ngx.exit(500)
end
-- x509 格式的证书有两种存储格式 PEM 和 DER,这里只描述PEM格式
-- 获取 Server Name Indication 简称(SNI)
local sni = ssl.server_name()
-- 这里我们假设已经通过cosocket实现了一个从数据库获取证书的函数
-- 该函数以sni为索引查询对应的证书并返回
local cert, err = get_pem_format_cert_by_server_name(sni)
-- cert_of_x509 是一个cdata类型的数据
local cert_of_x509, err = ssl.parse_pem_cert(cert)
local ok, err = ssl.set_cert(cert_of_x509)
if not ok then
ngx.log(ngx.ERR, "set cert failed, err:", err)
return ngx.exit(500)
end
--- 这里还需要设置对应的私钥,相关函数请参考如下
}
}
ssl.parse_pem_cert
ssl. parse_pem_priv_key
通过上述方式,我们可以通过直接修改存储介质中某个域名对应的证书,从而实现多节点的证书和私钥替换,更进阶一些,我们甚至可以通过一些技术手段让“租户或者用户”自己保留私钥,即实现所谓的key-less,这个我们后面文章再讲。
ssl_session_fetch_by_lua_xxx 和 ssl_session_store_by_lua_xxx
https 原理
https 的安全性和性能开销是不可调和的矛盾,在tcp三次握手之后的通信密钥协商过程中,由于服务端需要经过 RSA 非对称密钥解密,在由此生成对称密钥(称为primary key),这会带来大量的 cpu 开销,在短连接场景下开销更为明显。
通常情况下,我们可以通过 session cookie 的方式缓存 primary key,这样可以避免每次链接都进行 RSA 加解密从而降低 cpu 开销,但是这种方式对客户端的 openssl 版本有要求(不能低于某个版本 忘记了~~),且不能实用于状态无关的分布式场景,这时候便需要用到 ssl_session_fetch_by_xxx 和 ssl_session_store_by_xxx 命令。
ssl_session_fetch_by_lua_block {
local ssl_sess = require "ngx.ssl.session"
local sess_id, err = ssl_sess.get_session_id()
if not sess_id then
ngx.log(ngx.ERR, "failed to get session ID: ", err)
return
end
-- my_lookup_ssl_session_by_id()函数是自定义的session 获取函数,
-- 在这个 ssl_session_fetch_by 阶段,是用可以使用 cosocket 的,所以我们可以在这个函数里访问redis cluster 或者 memcached 之类的存储
local sess, err = my_lookup_ssl_session_by_id(sess_id)
if not sess then
if err then
ngx.log(ngx.ERR, "failed to look up the session by ID ",
sess_id, ": ", err)
return
end
-- 没有找到缓存,直接返回,走协商逻辑,做 RSA 加解密运算
return
end
-- 找到缓存,复用 primary key,节约cpu开销
local ok, err = ssl_sess.set_serialized_session(sess)
if not ok then
ngx.log(ngx.ERR, "failed to set SSL session for ID ", sess_id,
": ", err)
-- consider it as a cache miss...
return
end
}
ssl_session_store_by_lua_block {
local ssl_sess = require "ngx.ssl.session"
local sess_id, err = ssl_sess.get_session_id()
if not sess_id then
ngx.log(ngx.ERR, "failed to get session ID: ", err)
-- just give up
return
end
local sess, err = ssl_sess.get_serialized_session()
if not sess then
ngx.log(ngx.ERR, "failed to get SSL session from the ",
"current connection: ", err)
-- just give up
return
end
-- 由于在这个阶段不能使用cosocket,所以我们只能通过前文所诉的timer.at的嫁接办法来实现存储
local function save_it(premature, sess_id, sess)
local sess, err = my_save_ssl_session_by_id(sess_id, sess)
if not sess then
if err then
ngx.log(ngx.ERR, "failed to save the session by ID ",
sess_id, ": ", err)
return ngx.exit(ngx.ERROR)
end
return
end
end
-- timer.at 嫁接
local ok, err = ngx.timer.at(0, save_it, sess_id, sess)
if not ok then
ngx.log(ngx.ERR, "failed to create a 0-delay timer: ", err)
return
end
}
server {
listen 443 ssl;
server_name test.com;
...
}
另外,如果你在测试上述加密保持方法,记得在nginx配置中加上 ssl_session_tickets off; 否则上述逻辑不会生效。
Web 框架
待续