OpenResty一些重要特性的整理

OpenResty (简称OR) 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的Lua Api,第三方模块以及经常使用的依赖项,基于这些能力,咱们能够利用OR快速方便的搭建可以处理超高并发的,极具动态性和扩展的Web应用、Web服务和动态网关。html

这篇文章选择OR中具备重要意义的一些模块、命令、Api和框架介绍以下:mysql

基础的Http处理

OR 做为一款 Web 服务器,提供了如:nginx

  • ngx.print() | ngx.say()
  • ngx.req.get_headers()
  • ngx.resp.get_headers()
  • ngx.header.HEADER
  • ngx.exit()

    等函数和方法控制http请求的输入和输出,咱们能够经过这些 api 灵活的控制 Web Server 中包括输入输出,转发到上游或者发起子请求在内的各个环节。git

ngx.print

同ngx.say(), 经过该 api 能够指定返回的请求的消息体,其中每一次函数的调用,都是在nginx的输出链上增长了一个结点,因此咱们能够屡次调用该函数而不用担忧后者会覆盖前者,如:github

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, 如:redis

location =/error {
    content_by_lua_block {
        return ngx.exit(500)    
    }
}

GET /error
======
<html>
<head><title>500 Internal Server Error</title></head>
<body bgcolor="white">
<center><h1>500 Internal Server Error</h1></center>
<hr><center>openresty</center>
</body>
</html>

当咱们访问 /error 接口的时候,能够获得一个默认的 500 页面,如上所示,固然咱们能够经过ngx.status + ngx.print 的组合方式获得自定义的错误页面,如:sql

location =/error {
    content_by_lua_block {
        ngx.status = 500
        ngx.print("error here!")
        return ngx.exit(500)    
    }
}

GET /erro
======
error here!

以上方式一样适用于其它错误,如 40四、50二、503 等页面,经过这样的方式咱们能够更灵活的控制当这些错误,从而返回更友好的页面或者其它输出,固然你也可用使用 nginx 原生的 error_page 指令对错误页面进行控制,只是灵活性相对差一些数据库

ngx.timer.at

在OR内部经过 nginx 事件机制实现的定时器,咱们能够经过它来实现延迟运行的任务逻辑,甚至于经过一些特殊的调用方法实现定时任务的功能,好比:json

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

固然除此以外,这个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(anyaddre)
        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集合的应用空间,须要注意的是:

  1. 它须要在nginx的配置文件中被预先定义好大小且不可动态扩容, 如: lua_shared_dict cache 16m;
  2. 尽量的使用 cjson 而非 dkjson, 它们之间效率相差 50 倍以上;
  3. 它是一个内存数据集,并不具有持久化的能力;
  4. 宁愿定义多个容量更小的存储体,而非一个更大的;

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

待续

Web 框架

待续

相关文章
相关标签/搜索