一、OpenResty概述

1 OpenResty 背景

随着宽带网络的快速普及和移动互联网的高速发展,网站需要为越来越多的用户提供服务,处理越来越多的并发请求,要求服务器必须具有很高的性能才能应对不断增长的需求和突发的访问高峰。
在超高并发请求的场景下,很多常用的服务开发框架都会显得“力不从心”,服务能力严重下降,很难优化。

很多国内外大型网站都在使用OpenResty开发后端应用,而且越来越多,知名的国外公司有Adobe、CloudFlare、Dropbox、GitHub等,国内则有12306、阿里、爱奇艺、京东、美团、奇虎、新浪等,充分地证明了OpenResty的优秀。

2 OpenResty概念


官方介绍
OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

OpenResty通过汇聚各种设计精良的Nginx模块,从而将Nginx有效地变成一个强大的通用web应用平台。这样,web开发人员和系统工程师可以使用Lua脚本语言调动Nginx支持的各种C以及 Lua模块,快速构造出足以胜任10K乃至1000K以上单机并发连接的高性能web应用系统。


更广义地来看,OpenResty不仅仅是一个单纯的web服务开发套件。

3 OpenResty三个特性

3.1 详尽的文档和测试用例

OpenResty 的文档非常详细,作者把每一个需要注意的点都写在了文档中。为了方便起见,OpenResty还自带了一个命令行工具restydoc,专门用来帮助开发者通过 shell 查看文档,避免编码过程被打断。如下:

restydoc -s ngx.say
restydoc -s proxy_pass

OpenResty t目录,包含所有的测试案例。每一个测试案例都包含完整的NGIN配置和Lua代码,以及测试的输入数据和预期的输出数据。

3.2 同步非阻塞

OpenResty在诞生之初就支持了协程,并基于此实现了同步非阻塞的编程模式。
什么是同步非阻塞。先说同步,这个很简单,就是按照代码来顺序执行。比如下面这段伪码:

local res, err = query-mysql(sql)
local value, err = query-redis(key)

在 OpenResty 中,上面的伪码就可以直接实现同步非阻塞,而不用任何显式的关键字。这里也再次体现了,让开发者用起来更简单,是 OpenResty 的理念之一。

举个栗子
1、你在家做饭,用普通的汤锅,米放进去,就站在锅边,傻等饭熟。——这叫同步阻塞

2、还是用普通的汤锅,米放进去,然后继续回去打游戏,过一会就来看一次。——这叫同步非阻塞

3.3 动态

动态是Lua天生的优势。通过 OpenResty 中lua-nginx-module模块中提供的 Lua APl,可以动态地控制路由、上游、SSL证书、请求、响应等。甚至更进一步,可以在不重启OpenResty 的前提下,修改业务的处理逻辑,并不局限于 OpenResty 提供的Lua API。

4 OpenResty应用场景

OpenResty 通过 lua 脚本扩展 nginx 功能,可提供负载均衡、请求路由、安全认证、服务鉴权、流量控制与日志监控等服务。很多大厂比如京东、360 都在生产环境是用了这个应用程序。2015年底老罗营销锤子的时候顺便让这个程序在程序员圈中火了一把。

  • web应用
  • 接入网关
  • Web防火墙
  • 缓存服务器
  • 其他

5 学习的重点在哪里

讲了这么多OpenResty的重点特性,你又该怎么学呢?我认为,学习需要抓重点,围绕主线来展开,而不是 眉毛胡子一把抓,这样,你才能构建出脉络清晰的知识体系。

  • 同步非阻塞的编程模式;
  • OpenResty不同阶段的作用;
  • LuaJIT 和 Lua 的不同之处;
  • OpenResty API 和周边库;
  • 协程和 cosocket;
  • 单元测试框架和性能测试工具;
  • 火焰图和周边工具链;
  • 性能优化。

二、Lua语言进阶

1 什么是LuaJIT

我们学习了 Lua,它小巧快速,非常适合嵌入在各种环境里运行,所以被选定OpenResty 的工作语言。但为了追求极致的性能,OpenResty 里使用的却不是官方 Lua解释器,而是一个非官方实现一 LuaJIT

LuaJIT 是Lua 语言的另一个实现,包括一个汇编语言编写的解释器和一个 JIT 编译器。前者使用的是汇编语言,速度比 Lua 官方的解释器还要快很多,而后者可以把 Lua 语言用 Just-In- Timer 技术直接编译为目标机器码,使运行速度成倍提升,达到或接近 C 代码的程度。

Lua 和 LuaJIT 的区别

  • Lua 非常高效
  • LuaJIT 的执行速度

2 LuaJIT环境安装

2.1 下载OpenResty

http://openresty.org/cn/download.html

2.2 解压OpenResty

2.3 idea配置LuaJIT

3 goto语句

continue 是循环控制里一个非常重要的功能,可以跳过循环体后面的语句,立即重新开始下一次循环,但 Lua 语言不支持 continue ,这让很多程序员非常不适应。

LuaJIT goto 语句与 语言里的 goto 很类似 需要先使 :: label :: 形式定义标签,之后就可以随时用
goto 改变程序的流程,跳转到指定的标签。

-- 利用goto 实现contiune 方式
for i = 1, 10 doif i == 2 thengoto continueendprint("i = ",i)::continue::
end
-- 跳转标签 ::continue:: 名字不是固定。 完全可以改用任意的标识符LuaJIT goto 也有一些限制,不能随意跳转,例如不能跳入跳出函数
--不能跳入下级 语句块。

4 table库

为了更高效地操作表, LuaJIT 增强了 table 库,为它添加了一些新函数,其中较有用的有 table.new table.clear table.clone

table.new
专门用来创建表的

local tab_new = require("table.new") -- 加载table.new函数
-- 表内数组的元素个数 散列元素的数量 resize
local t = tab_new(10,0) -- 预先分配10个元素数组空间
t["a"] = 10
t["b"] = 20

table.clear

-- table.clear 把表置为空表 但是保留之前分配的内存 。
--local tab_clear = require("table.clear")
--tab_clear(t)

table.clone

-- table.clone 高效 浅 拷贝
local tab_clone = require("table.clone")
local t2 = tab_clone(t)
for k,v in pairs(t2) doprint(v)
end

5 FFI库

ffi是LuaJIT 里最有价值的一个库,它极大地简化了在 Lua 代码里调用C接口的工作,不需要编写烦琐的 Lua/C 绑定函数,只要在 Lua 代码里嵌入 函数或数据结构的声明,无须额外的代码即可直接访问,非常方便,而且执行效率比传统的战方式更高。

ffi 库不仅可以调用系统函数和 OpenResty 内部的 函数,还可以加载 so 形式的动态库,调用动态库里的函数,从而轻松灵活地扩展 Lua 的功能。

ffi 库 词汇

FFI.API

local ffi = require "ffi"

ffi.cdef
语法: ffi.cdef(def)
功能: 声明 C 函数或者 C 的数据结构,数据结构可以是结构体、枚举或者是联合体,函数可以是 C 标准函数,或者第三方库函数,也可以是自定义的函数,注意这里只是函数的声明,并不是函数的定义。声明的函数应该要和原来的函数保持一致。

ffi.cdef[[
typedef struct foo { int a, b; } foo_t; /* Declare a struct and typedef. */
int printf(const char *fmt, ...); /* Declare a typical printf function.
*/
]]

注意: 所有使用的库函数都要对其进行声明,这和我们写 C 语言时候引入 .h 头文件是一样的。

顺带一提的是,并不是所有的 C 标准函数都能满足我们的需求,那么如何使用 第三方库函数 或 自定义的函数 呢,这会稍微麻烦一点,不用担心,你可以很快学会。: ) 首先创建一个 myffi.c ,其内容是:

int add(int x, int y)
{return x + y;
}

接下来在 Linux 下生成动态链接库:

gcc -g -o libmyffi.so -fpic -shared myffi.c

为了方便我们测试,我们在 LD_LIBRARY_PATH 这个环境变量中加入了刚刚库所在的路径,因为编译器在查找动态库所在的路径的时候其中一个环节就是在LD_LIBRARY_PATH 这个环境变量中的所有路径进
行查找。命令如下所示。

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:your_lib_path

在 Lua 代码中要增加如下的行:

ffi.load(name [,global])

ffi.load 会通过给定的 name 加载动态库,返回一个绑定到这个库符号的新的 C 库命名空间,在 POSIX 系统中,如果 global 被设置为 ture ,这个库符号被加载到一个全局命名空间。另外这个name 可以是一个动态库的路径,那么会根据路径来查找,否则的话会在默认的搜索路径中去找动态库。在 POSIX 系统中,如果在 name 这个字段中没有写上点符号 . ,那么 .so 将会被自动添加进去,例如 ffi.load("z") 会在默认的共享库搜寻路径中去查找 libz.so ,在 windows 系统,如果没有包含点号,那么 .dll 会被自动加上。
下面看一个完整例子:

local ffi = require "ffi"
local myffi = ffi.load('myffi')
ffi.cdef[[
int add(int x, int y); /* don't forget to declare */
]]
local res = myffi.add(1, 2)
print(res) -- output: 3 Note: please use luajit to run this script.

除此之外,还能使用 ffi.C (调用 ffi.cdef 中声明的系统函数) 来直接调用 add 函数,记得要在ffi.load 的时候加上参数 true ,例如 ffi.load('myffi', true)
完整的代码如下所示:

local ffi = require "ffi"
ffi.load('myffi',true)
ffi.cdef[[
int add(int x, int y); /* don't forget to declare */
]]
local res = ffi.C.add(1, 2)
print(res) -- output: 3 Note: please use luajit to run this script.

6 虚变量

当一个方法返回多个值时,有些返回值有时候用不到,要是声明很多变量来一一接收,显然不太合适(不是不能)。Lua 提供了一个虚变量(dummy variable)的概念, 按照惯例以一个下划线(“_”)来命名,用它来表示丢弃不需要的数值,仅仅起到占位的作用。

-- string.find (s,p) 从string 变量s的开头向后匹配 string
-- p,若匹配不成功,返回nil,若匹配成功,返回第一次匹配成功的起止下标。
local start, finish = string.find("hello", "he") --start 值为起始下标,finish--值为结束下标
print ( start, finish ) --输出 1 2
local start = string.find("hello", "he") -- star值为起始下标
print ( start ) -- 输出 1
local _,finish = string.find("hello", "he") --采用虚变量(即下划线),接收起始下标值,然后丢弃,finish接收结束下标值
print ( finish ) --输出 2
print ( _ ) --输出 1, `_` 只是一个普通变量,我们习惯上不会读取它的值

代码倒数第三行,定义了一个用 local 修饰的 虚变量 (即 单个下划线)。使用这个虚变量接收string.find() 第一个返回值,忽略不用,直接使用第二个返回值。
虚变量不仅仅可以被用在返回值,还可以用在迭代等。
在for循环中的使用:

-- test.lua 文件
local t = {1, 3, 5}
print("all data:")
for i,v in ipairs(t) doprint(i,v)
end
print("")
print("part data:")
for _,v in ipairs(t) doprint(v)
end

当有多个返回值需要忽略时,可以重复使用同一个虚变量:
多个占位:

-- test.lua 文件
function foo()return 1, 2, 3, 4
end
local _, _, bar = foo(); -- 我们只需要第三个
print(bar)

7 调用代码前先定义函数

Lua 里面的函数必须放在调用的代码之前,下面的代码是一个常见的错误:

-- test.lua 文件
local i = 100
i = add_one(i)
function add_one(i)return i + 1
end

我们将得到如下错误:

# luajit test.lua
luajit: test.lua:2: attempt to call global 'add_one' (a nil value)
stack traceback:test.lua:2: in main chunk[C]: at 0x0100002150

因此在函数定义之前使用函数相当于在变量赋值之前使用变量,Lua 世界对于没有赋值的变量,默认都是 nil,所以这里也就产生了一个 nil 的错误。

一般地,由于全局变量是每个请求的生命期,因此以此种方式定义的函数的生命期也是每个请求的。为了避免每个请求创建和销毁 Lua closure 的开销,建议将函数的定义都放置在自己的 Lua module 中。
例如:

-- my_module.lua
local _M = {_VERSION = "0.1"}
function _M.foo()-- your codeprint("i'm foo")
end
return _M

然后,再在 content_by_lua_file 指向的 .lua 文件中调用它:

local my_module = require "my_module"
my_module.foo()

因为 Lua module 只会在第一次请求时加载一次(除非显式禁用了 lua_code_cache 配置指令),后续请求便可直接复用。

8 什么是JIT

LuaJIT 的运行时环境包括一个用手写汇编实现的 Lua 解释器和一个可以直接生成机器代码的 JIT 编译器。

JIT 编译器不支持的原语被称为 NYI(Not Yet Implemented)原语。比较完整的 NYI 列表在这篇文档里面:
http://wiki.luajit.org/NYI

所谓“让更多的 Lua 代码被 JIT 编译”,其实就是帮助更多的 Lua 代码路径能为 JIT 编译器所接受。这一般通过两种途径来实现:

  1. 调整对应的 Lua 代码,避免使用 NYI 原语。
  2. 增强 JIT 编译器,让越来越多的 NYI 原语能够被编译。

8.1 基础库的支持情况

8.2 字符串库

8.3 表

8.4 math 库


简单测试一下:


总结:
效率方面相差大概9.1倍。换句话说标准lua需要177%的时间才能完成同样的工作。

9 总结

  • Lua语言本身运行效率就很高,而 OpenResty 为了追求性能的极致,用的是更高效LuaJIT 。它利用了汇编语言和即时编译技术,可以把 Lua 源码程序即时编译成本地机器码,成倍地提升运行速度。
  • 因为 LuaJIT 代码的优化效果非常明显,所以我们在 OpeηResty 里编写 Lua码时也需要注意尽量避免使用 pairs unpack io. *等不可编译的“ NYI ”原语,让 LuaJIT的即时编译发挥出最大的作用,让OpenResty 应用以最高速度运行。

三、OpenResty核心

1 环境搭建

实践的前提是搭建环境,介绍2种常见的OpenResty 的安装。

1.1 包管理安装

1.1.1 安装yum-utils

yum install -y yum-utils

1.1.2 给yum 添加安装openresty的repo

yum-config-manager --add-repo https://openresty.org/package/rhel/openresty.repo

1.1.3 安装openresty

yum install -y openresty

1.1.4 启动openresty

systemctl start openresty

1.1.5 配置openresty环境变量



1.2 源码安装

我们首先要在官网下载 OpenResty 的源码包。官网上会提供很多的版本,各个版本有什么不同也会有说明,我们可以按需选择下载。

依赖库安装
将这些相关的库 perl 5.6.1+,libreadline, libpcre, libssl 安装在系统中。 按照以下步骤:

  1. 输入以下命令 yum install gcc readline-devel pcre-devel openssl-devel perl ,一次性安装需要的库。
  2. 相关库安装成功。安装成功后会有 “Complete!” 字样。

OpenResty 安装

1.2.1 在命令行中切换到源码包所在目录。

wget https://openresty.org/download/openresty-1.15.8.2.tar.gz

1.2.2 解压源码包

tar -zxvf openresty-1.15.8.2.tar.gz

1.2.3 切换工作目录

cd openresty-1.15.8.2

1.2.4 配置安装目录及需要激活的组件。

使用选项 --prefix=install_path,指定安装目录(默认为/usr/local/openresty)。

使用选项 --with-Components 激活组件,--without 则是禁止组件。 你可以根据自己实际需要选择
with 或 without。如下命令,OpenResty 将配置安装在 /opt/openresty 目录下(注意使用 root
用户),并激活luajit、http_iconv_module 并禁止 http_redis2_module 组件。./configure --prefix=/usr/local/openresty \--with-luajit \--without-http_redis2_module \--with-http_iconv_module

1.2.5 编译安装

gmake
gmake install

测试安装后的效果

2 HelloWorld

HelloWorld 是我们亘古不变的第一个入门程序。但是 OpenResty 不是一门编程语言,跟其他编程语言的 HelloWorld 不一样,让我们看看都有哪些不一样吧。

2.1 基本配置

作为web服务,我们应该依据实际情况决定应用的服务能力,例如开多少个worker进程、可能的最大并发数量等。

“Hello world“应用的功能很简单,所以我们只开启一个 worker进程,并发连接最多512个,其他的都使用默认配置。

开启服务器,编写配置文件

cd openresty/
ll
cd ngonx/
ll
cd cof/
ll
cp nginx.conf hello.conf
ll
vim hello.conf
# 设置worker进程数量 1
worker processes 1;
# 设置并发连接需要events块
events {# 单个worker最大连接数worker_connections 512;
}

2.2 服务配置

接下来需要决定 Web 服务的协议和端口号,我们使用最常用的 HTTP 协议,端口 80 ,域名任意。配置 HT TP 务需要编写 http {}配置块,并在里面使用指令 server listen serveame 依次定义端口号和域名

# 定义HTTP服务
http{# server块 定义web服务server{# 服务使用端口号listen 80;# http服务对应任意域名server_name *.*;}
}

2.3 处理请求

有了 Web 服务,我们还要有处理请求时的 URL 入口。因为Hello World,应用总是返回唯一的结果,所以应当使用“ location /”来匹配所有 URI:

http{server {listen 80;server_name *.*;# location块 匹配任意URLlocation /helloworld {}}
}

2.4 应用程序

经过前面的 个步骤,现在 Web 服务的基本框架已经建立起来了,缺的只是服务的内这是要真正编写 Lua 代码的地方。
OpenResty 提供一个专用指令“content_by_lua_block ”,可以在配置文件里书Lua 代码,产生响应内容:

content_by_lua_block {ngx.print("hello openresty")
}


完整例子:

# 定义http服务
http {# server快 定义web服务server {# 服务使用端口号listen 80;# http服务对应任意域名server_name localhost;# location快 匹配任意URIlocation /helloworld {# 我们第一个openresty应用content_by_lua_block {# 打印经典hello world程序ngx.print("Hello , OpenResty")}}}
}

3 运行命令

我们简单地了解启动和停止 OpenResty 的方法,本节将再介绍几个常用的运行参数,使用它们可以更好地管理 OpeResty 应用。

3.1 -c

-c ” 参数要求 OpenResty 运行指定的配置文件
示例:

openresty -c conf/hello.conf #要求OpenResty运行配置文件conf

如果报错,把配置文件中的注释删掉试试!

3.2 -p

“-p path ”是“-c ”的增强版,它设置了完整的 OpenResty 环境。“path ”指定了OpenResty 的工作目录, OpenResty 会使用这个目录下的 conf/nginx conf 运行,日志文件存放在 logs 目录
示例:

openresty -p #设置工作目录为/ opt/openresty

3.3 -s (signal)

可以快速地停止或者重启 OpenResty

  • stop : 强制立即停止服务,未完成的请求会被直接关闭
  • quit : 停止服务,但必须在处理完当前所有请求之后
  • reload : 重启服务,重新加载配置文件和 Lua 代码,服务不会中断
  • reopen : 只重新打开日志文件,服务不会中断,常用于切分日志( rotate )
    示例:
openresty -s reload -c x.conf
openresty -s stop -p /application

3.4 -t (test)

-t ”或“-T ”参数可以测试配置文件是否正确,后者同时还会打印出文件内容方便检查:

bin/openresty -t #检查默认的配置文件
bin/openresty -T #检查默认的配置文件并打印输出

示例:

bin/openrest y -t -c x. nf #检查指定的配置文件 conf

3.5 -v

-v-V 参数可以显示OpenResty的版本信息(不需要root权限),两者的区别是“-V”可以显示的更多,包括编译器版本、操作系统版本、定制的编译参数等信息:

openresty -v

4 目录结构

最简单的 OpenResty 用,只有一个配置文件,应用代码写在了配置文件里。但实际的项目要比它复杂很多,配置文件和应用代码最好分离管理维护,此外还会有其他的监控脚本、日志文件、数据文件等,必须要用很好的目录层次把它们组织起来。

mkdir application
ll
mkdir conf
mkdir bin
mkdir logs
mkdir service
cd service/
mkdir conf
mkdir etc
mkdir http
mkdir stream
mkdir utils
cd conf/
mkdir http
touch nginx.conf
mkdir streamtree
application 应用主目录
├── bin 脚本目录存放各种脚本文件
├── conf 配置目录,存放nginx配置文件
│   ├── http 存放http服务的配置文件
│   ├── nginx.conf 主配置文件
│   └── stream 存放http服务的配置文件
├── logs 日志目录存放nginx的日志文件
└── service 应用程序目录 存放Lua代码├── conf 应用程序的配置├── etc 其他数据文件├── http http服务代码├── stream tcp/udp服务代码└── utils 通用的工具代码

这样我们就可以使用 “-p ”参数 在一个整洁环境里运行openresty应用

openresty -p `pwd` # 当前目录运行openresty应用

5 运行机制

OpenResty基于Nginx,把 web服务的整个生命周期和请求处理流程清晰地划分出了若干个阶段(Phase)—这是OpenResty与其他web服务开发环境的最显著差异。

5.1 处理阶段

一个Web 服务的生命周期可以分成三个阶段

OpenResty 目前关注的是initingrunning 这两个阶段,并做了更细致的划分。

1、initing 阶段:

initing 阶段在OpenResty 里分为三个子阶段:

configuration :读取配置文件,解析配置指令,设置运行参数
master-initing :配置文件解析完毕,master进程初始化公用的数据
worker-initing :worker 进程自己的初始化,进程专用的数据

2、running 阶段:

在running 阶段,收到客户端请求后,OpenResty对每个请求都会使用一个专门的“流水线”顺序进行处理,流水线上就是OpenResty 定义的处理阶段。

ssl:SSL/TLS 安全通信和验证
preread:在正式处理之前“预读”数据,接收 HTTP 请求头:
rewrite:检查、改写URI,实现跳转/重定向
access:访问权限控制
content:产生响应内容
filter:对content阶段产生的内容进行过滤加工处理
log:请求处理完毕,记录日志,或者其他的收尾工作

5.2 执行程序

OpenResty 提供了一些“ xxx_by_lua ”指令,开发Web应用时使用它们就可以在这些阶段里插入 Lua代码,执行业务逻辑

init_by_lua:master-initing阶段,初始化全局配置或模块init_worker_by_lua:worker-initing阶段,初始化进程专用功能ssl_certificate_by_lua:ssl阶段,在“握手”时设置安全证书set_by_lua:rewrite阶段,改写Nginx变量rewrite_by_lua:rewrite阶段,改写URI,实现跳转/重定向access_by_lua:access阶段,访问控制或限速content_by_lua:content阶段,产生响应内容balancer_by_lua:content阶段,反向代理时选择后端服务器header_filter_by_lua:filter阶段,加工处理响应头body_filter_by_lua:filter阶段,加工处理响应体log_by_lua:log阶段,记录日志或其他的收尾工作

注意:在HTTP 处理过程中没有“ preread_by_lua ”,即Preread 阶段只能由OpenResty 内部读取HTTP请求头,用户不能介入干预(但TCP/UDP 协议是允许的)。
使用示例:

init_worker_by_lua_block { -- worker-initing 阶段
... -- 启动定时器,定时从Redis里获取数据
}
rewrite_by_lua_block{ -- rewrite阶段,通常是检查、改写URI
... -- 但也可以操作响应体,做编码解码工作
}
access_by_lua_block{ -- access阶段,通常做权限控制
... -- 检查权限,例如ip地址、访问次数
}
content_by_lua_block{ -- content阶段, Lua产生响应内容
... -- 主要的业务逻辑,产生向客户端输出的内容
}
body_filter_by_lua_block{ -- filter 阶段,加工处理响应数据
... -- 可以对数据编码、加密或者附加额外数据
}
log_by_lua_block{ -- log阶段,请求结束后的收尾工作
... -- 可以向某个后端发送处理完毕的“回执”
}

这些指令通常都有三种形式(少数例外) :

xxx_by_lua:执行字符串形式的Lua代码
xxx_by_lua_block:功能相同,但指令后是{...}的Lua代码块xxx_by_lua_file:功能相同,但执行磁盘上的源码文件。【 推荐】

推荐使用“xxx_by_lua_file ”方式,它彻底分离了配置文件与业务代码,让两者可以独立部署,而且文件形式也让我们更容易以模块的方式管理组织 Lua 程序。

location ~ ^/(\w+) {content_by_lua_file service/http/$1.lua
}

指令执行顺序和阶段:

5.3 定时任务

接受请求发送响应是 Web Server 最主要的工作,也可以看成是服务器的“前台任务”。但在这些“前台任务”之外,还有很多与请求无关的“后台任务”需要处理,例如发送心跳、分析统计、更新内部数据等。

ngx.timer.*

5.4 流程图

6 功能接口

OpenResty 为用户提供了上百个功能接口,可分为如下几类:

  • 基础功能: 系统信息、日志、时间日期、数据编码、正则表达式等功能;
  • 高级功能: 共享内存、定时器、轻量级线程、信号量等功能;
  • 请求处理: 处理TCP/UDP/HTTP协议,响应或拒绝请求;
  • 访问后端: 无阻塞地与各种后端服务通信(如Redis、MySQL);
  • 反向代理: 管理上游服务器集群,健康检查;
  • 负载均衡: 自定义负载均衡策略,选择上游服务器;
  • 安全通信: SSL、oSCP、WAF等密码、证书、安全相关功能。

大部分位于ngx全局表 , 无须 require 即可访问。 他们基于 nginx 的事件机制和 lua 协程特性。

ngx > luajit > lua

7 核心库

OpenResty 自带了很多 Lua 库〈位于安装目录的 lualib 内), lua-resty-core 是其中最重要的一个,它使用 ffi 重新实现了 OpenResty 里原有的大多数函数,并增加了一些新的功能。

lua-resty-core 建议 在openresty 应用里面总是启用 lua-resty-core

init_by_lua_block{ 必须在init阶段初始化require "resty.core" -- 显示加载resty。core库collectgarbage("collect") -- 要求luavm回收清理内存
}

8 web应用开发流程

使用OpenResty开发 Web应用可以简略地分为设计、开发、测试和调优四个基本步骤。

9 基础功能

9.1 系统信息

OpenResty 在表 ngx.config 里提供了六个功能接口,可以获取自身的一些信息:

  • debug: 是否是 Debug版本;
  • prefix: 工作目录,也就是启动时“-p”参数指定的目录;
  • nginx_version: 大版本号,即内部 Nginx的版本号;
  • nginx_configure: 编译时使用的配置参数;
  • subsystem: 当前所在的子系统,取值为“http”或“stream";
  • ngx_lua_version: 当前所在子系统的版本号。

这几个功能接口都很简单,需要注意的是 prefixnginx_configure 这两个接口是函数的形式,示例代码如下:

ngx.say(ngx.config.debug)
ngx.say(ngx.config.prefix())
ngx.say(ngx.config.nginx_version)
ngx.say(ngx.config.nginx_configure())
ngx.say(ngx.config.subsystem())
ngx.say(ngx.config.ngx_lua_version())

9.2 运行日志

函数ngx.log( log_level )记OpenResty的运行日志,用法很类 Lua 的标准库函数 print ,可以接受任意多个参数,记录任意信息。
ngx.log 第一个参数 日志级别

ngx.STDERR: 日志直接打印到标准输出,最高级别的日志;
ngx.EMERG: 发生了紧急情况(emergency),需要立即处理;
ngx.ALERT: 发生严重错误,可能需要报警给运维系统;
ngx.CRIT: 发生严重错误(critical);
ngx.ERR: 普通的错误,业务中发生了意外;
ngx.WARN: 警告信息,业务正常,但可能要检查警告的来源;
ngx.NOTICE: 提醒信息,仅仅是告知,通常可以忽略;
ngx.INFO: 一般的信息;
ngx.DEBUG: 调试用的信息,只有debug版本才会启用。
print() = ngx.NOTICE

业务逻辑关键点 INFO 或者 print , 捕获error err级别。

2021/08/26 16:04:14 [error] 48483#0: *11 [lua] content_by_lua(nginx.conf:19):5:
ERRR ....., client: 192.168.66.10, server: localhost, request: "GET /test
HTTP/1.1", host: "192.168.66.50"

9.3 时间日期

对于 Web 服务器来说,随时能够获取正确的时间与日期是非常重要的, OpenResty 为此提供了很多时间日期相关函数,可以满足绝大多数应用场景。

这些时间日期函数不会引发昂贵的系统调用(除了 ngx.update time ),几乎没有成本,所以在我们的应用程序中应当尽量使用它们操作时间而不是 Lua 标准库里的 OS 。
当前时间

ngx.say(ngx.today()):本地时间,格式是“yyyy-mm-dd”,不含时分秒;
ngx.say(ngx.localtime()) :本地时间,格式是“yyyy-mm-dd hh : mm: ss";
ngx.say(ngx.utctime()) : UTc时间,格式是“yvyy-mm-dd hh :mm: ss”.

时间戳
获取当前的时间戳可以使用两个函数:

ngx.time
ngx.now

格式化时间戳

ngx.http_time:把时间戳转换为http时间格式;
ngx.cookie_time:把时间戳转换为cookie时间格式;
ngx.parse_http_time :解析http时间格式,转换为时间戳。
local secs = 1514880339 一个时间戳
ngx.say (ngx.http_time(secs)) 转换为http时间格式
ngx.say(ngx okie_time (secs)) 转换为cookie时间格式
local str = "Tue, 02 Jan 2018 08 : 05 : 39 GMT" 一个http时间格式
ngx.say (ngx.parse_http_time (str)) --转换时间戳bND

更新时间
ngx.localtime/ngx.time/ngx.now等函数获取的时间基于OpenResty内部缓存的时间,与实际时间相比可能存在微小的误差,如果想要随时获得准确的时间可以先调用函数 ngx.update_time,然后再调用时间函数,例如:

ngx.update_time () --强制更新内部缓存的时间
ngx.now () --之后就可以获得更准确的时间

ngx.update_time会使用系统函数gettimeofday ()强制更新时间,成本较高,除非必要应当尽量少用。
睡眠
让程序短暂 “睡眠”是应用开发中的一个常 操作,常用来等待某项工作的完成
ngx.sleep是openResty提供的同步非阻塞的睡眠函数,可以“睡眠”任意的时间长度但不会阻塞整个服务,这时OpenResty会基于协程机制转而处理其他的请求,等睡眠时间到再“回头”继续执行ngx.sleep后续的代码。

ngx.sleep(1.0)

9.4 数据编码

开发 Web 服务通常需要处理各种数据编码格式,OpenResty 目前内建支持的有 Base64 ,JSON两种格式,并通过 opm 安装扩展库支持 MessagePack

Base64
Base64 格式使用 64 个字符,可以把任意数据转换为 ASCII 码可见字符串,应用得非常普遍OpenResty 使用 ngx encode_base64 gx.decode_base64 这两个函数实现了标准的Base64 编码和解码

local str = "1234"
local enc = ngx.encde_base64(str)
local dee= ngx.decode_base64(enc)

JSON
JSON 是一种基于纯文本的轻量级数据交换格式,起源于 JavaScript ,但现在己经成为了所有应用开发的通用数据格式,比起庞大的 XML/SOAP ,简单、易读易修改是它的最大特点。

OpeResty 使用 cjsoη 库操作 JSON 数据 它采用 语言实现,速度非常快。

cjson cjson.safe

local cjson = require "cjson.safe"
local str = cjson.encode({name="wcc",age=18})
local obj = cjson.decode(str)
ngx.say(obj.name)

在 gitHub 上有另一 JSON 项目 lua_resty_json 据称解码速度比 cjson 还要快,但它并不含在OpenResty 里,也未加入 opm 仓库。

9.5 正则表达式

OpenResty 的正则表达式函数都有一个名为 options 的参数,它是一个字符串,用来定制匹配行为

a: "锚定"模式,仅从最开始的位置匹配;
d: 启用DFA模式,确保匹配最长的可能字符串;
D: 允许重复的命名捕获( duplicate named pattern);
i: 忽略大小写,即大小写不敏感;
j: 启用PCRE一JIT编译,通常与“o”联用;
j: 兼容Javascript的正则表达式语法;
m: 多行模式;
o: 正则表达式仅编译一次( compile once),随后缓存;
s 单行模式;
u: 支持UTF-8编码;
u: 同“u”,但不验证UTF-8编码的正确性;
x: 启用“扩展”模式,类似Perl的“/x”。

这些参数可以联合使用,同时指定多个功能例如

local str =”abcd-123 ”
ngx.re.match(str, [[\w+]], "ad")
ngx.re.match(str, [[\w+z]] ,"ij")
ngx.re.match (str, [[z.\d+(?#xxx)]] ,"ijox")

匹配

ngx.re.match

local str = "abcd-123"
local m = ngx.re.match(str,[[\d{3}]],"jo")
local t = ngx.re.match(str,[[(.*)123$]],"jo")
local tt = ngx.re.match(str,"[A-Z]+","jo")
ngx.say(t[0])
ngx.say(m[0])
ngx.say(tt)

查找

local found = ngx.re.find (str,123,jo)
local found = ngx.re.find (str,[[\d+]],jo)

替换

local str =”abcd-123 ”
str = ngx.re.sub(str,”ab ”,”cd”)

切分

local ngx_re_split = require("ngx.re").split
local str ="a,b,c,d"
local res = ngx_re_split(str,",")
assert(res and #res== 4)

9.6 高速缓存

Cache (高速缓存)是构建计算机软硬件系统时常用的一种手段,它保存了频繁访问的数据,从而缩减了高速上层访问低速下层的时间,能够提高系统的整体运行效率。

常见的缓存算法

  • LRU (Least recently used) 最近最少使用,如果数据最近被访问过,那么将来被访问的几率也更高。
  • LFU (Least frequently used) 最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。
  • FIFO (Fist in first out) 先进先出, 如果一个数据最先进入缓存中,则应该最早淘汰掉。
lua-resty-lru-cache
resty-lrucache 高命中率低更新场景
resty-lru-cache.pureffi 低命中率高更新场景

使用缓存
cache 对象的功能接口十分简单易用,提供基本的 set/get/delete 等操作,用起来就像是一个Key-Value 的散列表,缓存内的元素也可以是任何 Lua 数据(数字、字符串、函数、表等),无须序列化或反序列化。



创建缓存
set 方法向缓存里添加一个元素:cache:set(key, value, ttl) 一向缓存添加元素

cache:set("name","wcc")

添加元素时可以用参数 ttl 指定过期时间,单位是秒,如果不提供 ttl 那么就永不过期(但仍然会被 LRU算法淘汰)。
使用缓存

ngx.say(cache:get("name"))

删除缓存

cache:delete("name") --删除缓存中的一个元素
cache:fulsh_all() -- 快速清空缓存内的所有元素

10 HTTP服务

基于高效的 Nginx 平台和小巧紧凑的 Lua 语言,我们可以在 OpenResty 里以脚本编程的方式轻易构建出高性能的 HTTP 服务,实现 Web 容器和 RESTful 应用架构。

10.1 常量

OpenResty 使用一些常量来表示 HTTP 状态码和请求方法,这些明确命名的常量会让代码更具可读性。

10.1.1 状态码

状态码表示HTTP请求的处理状态,目前RFC规范里有一百多个,在openResty里只定义了少量最常见的,例如:

当然,在编写代码时不使用这些常量,直接用 200 、404 这样的数字字面值也是可以的,两者完全等价, OpenResty 对此没有强制要求。
例如:



10.1.2 请求方式

HTTP 协议里有 GET/POST/PUT 等方法,相应地 OpeRe sty 也定义了这些常量,例如:

ngx.HTTP GET 读操作,获取数据:
ngx.HTTP HEAD 读操作,获取元数据:
ngx.HTTP POST 写操作,提交数据:
ngx.HTTP PUT 写操作,更新数据;
ngx .HTTP DELETE 写操作,删除数据:
ngx.HTTP PATCH 写操作,局部更新数据。

10.2 变量

OpenResty 使用表 ngx.var 操作 Nginx 变量,里面包含了所有的内置变量和自定义变量,可以用名字直接访问。

利用好 ngx.var 能够获取 OpenResty 里的很多信息,例如请求地址、请求参数、请求头、客户端地址、收发宇节数等( 可参考 Nginx 文档或 restydoc )。

10.2.1 读变量

ngx.var 读取 Nginx 变量非常容易,“ . ” 或 “[]”的用法都允许,示例如下:

ngx.say(ngx.var.uri) --输出变 $url ,请求的 URI
ngx.say(ngx.var['http_host']) -- 输出变量丰 http host ,请求头里的 ho st

10.2.2 写变量

Nginx 内置的变量绝大多数是只读的,只有 $args$limit_rate 等极少数可以直接修改,强行修改只读变量会导致程序运行错误;

ngx.var.limit_rate = 1024*2 改写限速变 2K
ngx.var.uri = "unchangeable " 二不可修改,会在运行日志里记录错误信息

10.3 基本信息

通常我们的程序是从 Rewrite 阶段才开始处理请求,而在这之前的 Preread 阶段 OpenResty 己经“预先”从客户端获取了一些基本的信息,包括来源、起始时间和请求头文本,并准备了一个存放临时数据的位置供我们随后使用。

10.3.1 请求来源

函数 ngx req.is internal 用来判断本次请求是否是由“外部”发起的

is_internal = ngx.req.is_internal () 检查是否是一个 “内部” 请求

10.3.2 起始时间

函数 ngx req start time 可以获取服务器开始处理本次请求时的时间戳,精确到毫秒,使用它可以随时计算出请求的处理时间,相当于request time 但更廉价。例如

local request_time = ngx.now () - ngx.req.start_time ()

10.3.3 请求头

函数 ngx .req.raw_header 可以获得 HTTP 请求头的原始文本

local h = ngx.req.raw_header () -- 获取请求头原始字符串

10.3.4 暂存数据

OpenResty 把请求处理划分成“ rewrite “,“ access ”,“ content ”等若干个阶段个阶段执行的都是彼此独立的程序,由于作用域的原因内部变量不能共用,如果想要在各个阶段间传递数据就需要使用 ngx.ctx ,它比仅能存储字符串的

ngx.var.xxx 更灵活。

rewrite_by_lua_block { -- rewrite 阶段local len = ngx.var.content length -- 使用变量获取文本长度ngx.ctx.len = tonumber(len) -- 转换为数字,存入 ctx
}
content_by_lua_block { -- content 阶段ngx.say( ngx.ctx.len ) -- ngx ctx 里的变量在其他阶段仍然可用
}

10.4 请求行

HTTP 请求行里的信息包括请求方法、URI 、HTTP版本等 可以用 ngx.var获取

$request: 完整的请求行(包含请求方法、URI、版本号等);
$scheme: 协议的名字,如“http”或“https";
$request_method: 请求的方法;
$request_uri: 请求的完整URI(即地址+参数);
$uri: 请求的地址,不含“?”及后面的参数;
$document_uri: 同$uri;
$args: UR工里的参数,即“?”后的字符串;
$arg_xXx: UR工里名为“xxx”的参数值。

因为 ngx.var 的方式效率不高,而且是只读的,所以 OpenResty 在表 ngx.req 里提供了数个专门操作请求行的函数。

这些函数多用在“ rewrite by lua ”阶段,改写 URI 的各种参数,实现重定向跳转。

10.4.1 版本

函数口ngx.req.http_version 以数字形式返回请求行里的 HTTP 协议版本号 相当于$server protocol

10.4.2 方法

函数 ngx.req.get_methodngx.req set_method 相当于变量 $requestmethod ,可以读写当前的请求方法。但两者的接口不太对称,前者的返回值是字符串 ,而者的参数却不能用字符串,

10.4.3 地址

ngx.req.set_uri
ngx.uri = "a + b = c #!"
ngx.escape_uri() --编码
ngx.unescape_uri() -- 解码

10.4.4 参数

OpenResty 提供五个函数操作 URI 里的参数( $args )。
获取 URI 参数

函数 ngx.req.get_uri_args --用来获取 URI 里的参数:
args = ngx.req.get_uri_args (max args) --获取 URI 里的参数local args = ngx.req.get_uri_args(20) --最多解析出 20 个参数for k,v in pairs(args) do --使用 pairs 函数遍历解析出的参数ngx.say (” args :”, k,”=”, v) -- 逐个输出参数
end

获取 POST 参数
URI 参数也可以使用请求体传递,这时要使用另外一个函数 ngx.req. get_post_args 来解析获取。
ngx.req.get_post args 的用法与 ngx .req.get uri_args 基本相同,但因为参数位于请求体,所以必须要先调用口ngx req.read_body 读取数据,而且还要保证请求体不能存储在临时文件里

ngx.req.read_body () -- 必须先读取请求体数据
local args = ngx.req.get_post_args (10) -- 然后才能解析参数

10.5 请求头

HTTP 请求头包含多个“ Key Value ,的形式的字段,非常适合用Lua 里的表来管理,OpenResty 里操作起来也很方便

10.5.1 读取数据

local headers = ngx.req.get_headers() -- 解析请求头
ngx.say(headers.host)

10.5.2 改写数据

ngx.req.set_header("Accept","firefox")
local headers = ngx.req.get_headers() -- 解析请求头
ngx.say(headers.host)
ngx.say(headers.Accept)
ngx.req.clear_header("Accept")

10.6 请求体

请求体是 HTTP 请求头之后的数据,通常由 POST PUT 方法发送,可以从客户端得到大块的数据。

10.6.1 丢弃数据

很多时候我们并不关心请求体(例如 GET /HEAD 方法〉,调用函数 ngx .req.discarbody 就可以明确地“丢弃”请求体:

ngx.req.discard_body()

10.6.2 读取数据

出于效率考虑,OpenResty 不会主动读取客户端发迭的请求体数据

1 调用函数 ngx.req.read_body ,开始读取请求体数据;
2 调用函数 ngx.req.get_body_data 获取数据,相当于 $request_body;
3 如果得到是 nil ,可能是数据过大,存放在了磁盘文件里,调用函数 ngx.req.getbody_fil 可以获得相应的临时文件名(相当于 $request_body _ file );
ngx.req.read_body() -- 要求读取请求体数据 同步非阻塞
local data = ngx.req.get_body_data() -- 读取完毕 获取数据
ngx.say("body:" , data)

10.6.3 改写数据

ngx.req.set_body_data("yyyy") -- 改写请求题的数据
ngx.req.get_body_data()

10.7 响应头

HTTP 协议里的响应头包括状态行和响应头字段,OpenResty 会设置它们的默认值,但我们也可以任意修改。

10.7.1 修改数据

ngx.header['Service'] = "my openresty"
ngx.header.date = nil
ngx.header.new_fieid = "xxx"

10.7.2 过滤数据

响应头数据在发送到客户端的“途中”会经过 OpenResty filter 阶段,即“ headerfilter_by_lua ”,在这里也可以改写状态码和头字段,它可以配合 “content_b y_lua ” proxy pass ”等指令变更客户端最终收到的数据,例如:

if ngx.header.etag thenngx.header.etag = nil
end
ngx.header["Cache-Control"] = "max-age=300"

10.8 响应体

在OpenResty 里发送响应体很简单,不需要考虑“令人头疼”的缓冲、异步、回调、分块等问题、OpenResty 会自动处理这一切。

10.8.1 发送数据

local data = {'wcc','kalista'}ngx.say(data)for _,v in ipairs(data) dongx.print(v) -- 发送一部分数据ngx.flush(true) -- 刷新缓冲区
end

10.8.2 过滤数据

body_filter_by_lua_block {if ngx.arg[2] thenngx.arg[1] = ngx.arg[1]..'xx'end
}

10.9 流程控制

OpenResty 里有四个特别的函数用来控制 HTTP 处理流程,包括重定向和提前结束处理:

ngx.redirect 标准的 301/302 重定向跳转
ngx.exec 跳转到内部的其他 location
ngx.exit 立即结束请求的处理
ngx.eof 发送 EOF 标志,后续不会再有响应数据

10.9.1 重定向请求

rewrite_by_lua_block{ngx.redirect("https://www.github.com”) 一跳转到外部网站,默认是 302ngx.redirect("/new_path”, 301) 跳转到其他 location ,状态码 301
}

10.9.2 终止请求

rewrite_by_lua_block{-- ngx.redirect("/exec")-- ngx.exec("/exec")if not ngx.var.arg_name thenngx.exit(400) --参数缺失elsengx.exec("/exec")end
}

10.10 综合示例

使用之前介绍的 OpenResty 指令和功能接口,开发一个略复杂的 HTTP 应用。

10.10.1 功能描述

这个应用实现了基本的时间服务,具体功能是:

  1. 只支持 GET POST 方法:
  2. 只支持 HTTP 1.1 /2 协议
  3. 只允许某些用户访问服务;
  4. GET 方法获取当前时间,以 http 时间格式输出;
  5. POST 方法在请求体里传入时间戳,服务器转换为 http 时间格式输出;
  6. 可以使用 URI 参数“ need_encode = l “输出会做Base64 编码。

10.10.2 设计

依据 OpenResty 的阶段式处理逻辑,可以把整个应用划分为四个部分,每部分是一个独立的 Lua源码文件:

rewrite_by_lua :正确性检查,拒绝错误得请求方式;
access_by_lua : 使用白名单做访问控制;
content_by_lua : 产生响应内容
body_filter_by_lua : 加工数据,base64编码

10.10.3 检查正确性

vim /opt/appliction/service/http/rewrite_example.lua
local method = ngx.req.get_method() -- 获取请求方式
if method ~= 'GET' and method ~= 'POST' then -- 必须是get或者postngx.header['Allow'] = 'get , post' -- 方法错误了返回 Allow字段ngx.exit(405) -- 返回状态码 405 结束请求
endlocal ver = ngx.req.http_version() -- 获取协议得版本号
if ver < 1.1 then -- 不能低于1.1ngx.exit(400) -- 返回状态码400结束请求
endngx.ctx.encode = ngx.var.arg_need_encode -- 在ngx.ctx 里面存储编码标致量 获取uri 参数 need_encode 字段
ngx.header.content_length = nil



10.10.4 白名单访问控制

local white_list = {"192.168.66.10"}
local ip = ngx.var.remote_addr -- 获取ngx.var 获取客户端地
for _,v in ipairs(white_list) doif v == ip thenngx.say("success")breakelsengx.log(ngx.ERR,ip,"is blocakd")ngx.exit(403) -- 返回状态码 403 结束breakend
end

10.10.5 产生响应内容

在前两个节课我们己经阻挡了大部分的错误请求,所以 content 阶段就可以“安心”地编写主要的业务逻辑代码:

local function action_get()
ngx.req.discard_body() --显示丢失请求体得数据local t = ngx.time()
ngx.say(ngx.http_time(t)) -- 转换为http格式输出end
local function action_post()ngx.req.read_body() -- 要求非阻塞读取请求体local data = ngx.req.get_body_data() -- 获取请求体得数据local num = tonumber(data)if not num thenngx.log(ngx.ERR,"XXX")ngx.exit(400) --返回状态码 400 结束请求endngx.say(ngx.http_time(num)) --http格式输出
endlocal actions = {GET = action_get,POST = action_post}
local method = ngx.req.get_method() -- 获取对应得处理函数
actions[method]()

10.10.6 加工数据

过滤阶段的工作比较简单,判断条件已经在之前的“ rewrite_by ua ”阶段存储在了ngx.ctx 表里,直接使用即可:

if ngx.status ~= ngx.HTTP_OK thenreturn
end
if ngx.ctx.encode thenngx.arg[1] = ngx.encode_base64(ngx.arg[1]) --对数据进行base64 编码
end

11 访问后端

Web 服务通常不会仅限于本机资源的“单打独斗”,它必须利用 Re dis MySQL 等数据库服务存储缓存、会话和其他数据,利用 Kafka RabbitMQ 等消息队列服务异步发送消息,以及访问 Tomcat PHP 等业务服务,访问 ZooKeeper Consul 等配置服务,综合协调这些后端才能为最终用户呈现出一个功能完备的应用服务。

1 简介

在openResty里有两种访问后端服务的方式:子请求location.capture 和协程套接字 cosocket,两者都是完全非阻塞的,方便易用而且效率极高,不需要编写“晦涩难懂”的回调函数就可以实现高性能的并发编程。

1.1 子请求

ngx .location.capture 是较“传统”的方式,基于 Nginx 平台内部的子请求机制,需要配合 Nginx 反向代理模块(如口ngx_proxy , ngx_redis2 , ngx_fastcgi 等)“间接地”访问后端服务,接口参数较多,调用成本也略高。

1.2 cosocket(协程套接字)

cosocket (即 " corountine based socket ")是 OpenResty 独有的特性 它结合Nginx 的事件机制和Lua 的协程特性,以同步非阻塞的方式实现了 socket2 编程,高效与任意的后端服务通信。

2 子请求

子请求方式使用的函数是ngx .location.capture 在调用前必须预先在配置文件里配置好它将 “捕获” 的 location。location内部通常使用的是各种反向代理模块,利用" xxx pass " 访问后端服务。受nginx平台限制,ngx.localtion.capture 只能用在“rewrite_by_lua” '“access_by_lua"和"content_by_lua” 这三个执行阶段。

2.1 接口说明

ngx.location.capture 的形式是:

res = ngx.location.capture (uri , options) --发起子请求调用

它“ 调用 ”本 server 内的名为 "uri " 的 location ,第二个参数是可选的,以表的方传递发起子请求时的额外数据,并 以改写原始 请求信息。表里 字段有:

  • method: 子请求的方法,必须使用 节里的数字常量
  • args : 子请求的 URI 参数,可以字符串 可以是表
  • body :子请求的 body 数据,必须是 ua 字符串
  • ctx : 子请求使用的 ngx ctx 临时数据
  • vars : 子请求可能用到的变量,存储在表里

函数执行后会同步非阻塞地等待请求执行完毕,最后返回一个表,包含四个字段:

  • status 子请求的 响应状态码,相 当于 ngx .status;
  • header 子请求的响应头,相当于 ngx. header;
  • body 子请求的响应体:
  • truncated 错误标志位, body 数据是否被意外截断。

2.2 用法:

local res = ngx.location.capture (uri, -- 发起一个子请求{method = ngx.HTTP_POST , -- 修改子请求的方法 可以改成postargs = { ... },          -- 请求参数body = ...                -- 添加请求体
})
if res.status = ngx.HTTP_OK then --检查子请求的状态码ngx.print(res.body)          -- 获取响应体数据任意处理完毕
end

2.3 应用示例:

location /hello {proxy_set_header Host $host;proxy_pass http://192.168.66.10:8080/hello;}location /test01 {content_by_lua_file service/http/http_example.lua;
}

http_example.lua

local capture = ngx.location.capture
local res = capture("/hello")
if res.status ~= ngx.HTTP_OK thenngx.exit(res.status)
end
if res.truncated thenngx.log (ngx.ERR,"XXX")
end
ngx.say(res.body)

2.4 给你们一点建议

首先,它的扩展性不够灵活,如果要访问新的后端必须要改写配置文件,配置新的 locatio 口。而且,如果后端服务没有对应的 Nginx模块就需要使用 语言开发,而C语言的开发难度高、周期长是众所周知的,这就限制了 ngx.location.capture 用范围.其次, ngx.location.capture 使用的子请求机制会“完整”地捕获全部响应内容,需要使用较大的缓冲区,如果响应内容很多会造成大 内存占用, 良费系统资源.

建议尽量不使用口ngx.location.capture ,而是改用 cosocket ,它的底层运行机制与ngx .locationcapture 基本相同,但成本更低,更灵活可控。

3 协程套接字


cosocket API和指令简介

创建对象: ngx.socket.tcp.
设置超时: tcpsock:settimeout和 tcpsock:settimeouts。
建立连接: tcpsock:connect
发送数据: tcpsock:send。
接受数据: tcpsock:receive、tcpsock:receiveany和tcpsock:receiveuntil。
连接池: tcpsock:setkeepalive。
关闭连接: tcpsock:close。

案例

local sock = ngx.socket.tcp()
sock:settimeout(1000)
local ok, err = sock:connect("127.0.0.1",9999)
local bytes, err = sock:send("hello")
local data, err, partial = sock:receive()
if err thenngx.say("is err",err)return
end
ngx.say(data)

4 HTTP客户端

HTTP 协议是目前网络世界里应用的最广泛的协议,它简单方便、适用性强,不仅是普通Web 网站,很多应用服务器也基于 RESTful 风格提供 HTTP 协议的调用接口。

OpenResty 目前没有官方的 HTTP 库,但另有一个功能很完善的 HTTP 客户端库 lua-resty-http ,支持HTTPl.0/1.1 ,可以使用

4.1 opm 安装:

OPM(openresty package manager) 是openresty自带的包管理器。

opm search http #搜索 HTTP 相关库
opm install pintsized/lua-resty-http #安装 HTTP 客户端库
lua-resty-http 库需要显式加载后才能使用,即
local http = require "resty.http" --加载 lua-resty-http

4.2 创建对象

在访问 HTTP 服务器之前,我们必须调用 new 方法创建连接对象:

httpc, err = http:new() --创建 http 连接对象建对象之后还需要设置超时时间,但函数名与 cosocket 略有不同:
httpc:set_timeout (time) --注意名字里有一个下画线!

4.3 发送请求

lua-resty-http 库支持 HTTP 协议的各种特性,如 SSL 、代理、流式收发数据等,有的用法比较复
杂,这里只介绍一个简单的接口: request uri ,但足以应对大多数场景,更多功能可参考GitHub 文档。

函数 request uri 请求指定的 URI ,并获取响应结果:

res, err = httpc: request_uri (uri, params)

发送 HTTP 请求
参数 uri 是要访问的网址, params 是一个表,指定 HTTP 请求的参数,包括:

local res, err= httpc:request_uri( -- 发送 HTTP 请求,默认是 GET'http://127.0.0.1', -- 指定 IP 地址{path =’/echo ’, -- 指定具体路径query = {name =’ chrono'}} -- 请求的参数 使用lua表
)
if not res then --检查请求是否成功ngx.say (' failed to request : ', err}
return
end
for k,v in pairs(res.headers) do -- 输出响应头ngx.say(k,' =>' , v) -- 输出响应头
end
ngx.say(res.body) -- 输出响应体

5 DNS客户端

TCP/IP协议使用IP地址来标识主机,但纯数字的地址很难记忆和使用,于是“域名”(DomainName)应运而生,它代替了麻烦的数字串,使用易读的文字来标识主机。

例如,我要去西湖边的“外婆家”,这就是名称,然后通过地址簿。查看到底是那条路多少号。

Nginx自带了标准的域名解析功能,使用指令resolver在配置文件里指定DNS服务器,自动解析域名。

例如:

resolver 8.8.8.8 8.8.4.4 valid=30s; #指定两个 DNS ,缓存 30 秒
223.5.5.5
223.6.6.6

resolver指令的功能较简单,如果想要在OpenResty里更灵活地实现域名解析功能就要使用lua-resty-dns库,它基于 cosocket,完全无阻塞,是一个非常高效易用的DNS客户端。
lua-resty-dns 库需要显式加载后才能使用,即:

local resolver = require "resty.dns.resolver" -- 加载 lua-resty-dns

5.1 创建对象

在访问 DNS 服务器之前,我们必须调用口new 方法创建解析对象:

r , err = resolver:new(opts) --创建 DNS 解析对象

new方法的参数opts是一个表有四个相关的字段

nameservers : DNS 服务器地址数组,可以指定多个,使用简单的轮询算法:
retrans : DNS 服务器的重试次数,可以省略,默认是 次:
timeout : 单次 DNS 查询的超时时间,可以省略,默认是 秒;
no_recurse : 递归查询标志位,默认是 false ,即允许递归查询。

示例:

local r, err = resolver:new{nameservers={"8.8.8.8",{8.8.4.4,53},{4.2.2.1},"4,2,2,2"},timeout=1000
}
if not r thenngx.say("failed to init resolver :" ,err)
end

5.2 查询地址

有了解析对象后,就可以调用 query 方法向 DNS 服务器发送查询请求:

answers, err= r:query(name) --查询域名对应的IP地址
local answers,err = r:query("www.openresty.org")
if not answers thenngx.say("failed to query:",err)return
end
if answers.errcode thenngx.say("error code:",answers.errcode,":",answers.errstr)
end
for _,rec in ipairs(answers ) dongx.say(rec.name, " ",rec.address,rec.ttl)
end

6 Redis客户端

Redis 是近几年内非常流行的内存 K V 存储系统,以速度快和丰富的数据类型而闻名,可以用在缓存、消息队列、数据库等领域,许多国内外知名公司都是它的用户。

OpenResty 发行包内置了 lua-resty- redis库,它基于 cosocket 实现了非阻塞的 Redis 客户端,支持 Red is 的所有命令以及管道操作。

lua-resty-redis 库需要显式加载后才能使用 即:
local redis = require( "resty.redis") 加载 lua-resty-redis

6.1 创建对象

rds,err = redis:new() --创建redis连接对象
rds:set_timeout(time) -- 注意名字里有一个下划线

6.2 建立连接

local redis = require("resty.redis")
local rds = redis:new()
rds:set_timeout(1000)
local ok , err = rds:connect("192.168.66.100",6379)
if not ok thenngx.say("failed to connect :" ,err)rds:close()return
end
res,err = rds:get("name")
ngx.say(res)

6.3 关闭连接

Redis 操作完毕或者出错,应该调用函数 close 关闭连接,释放 cosocket 资源
ok,err = rds:close() -- 关闭连接

6.4执行命令

rds:set("name","wcc")
rds:get("name")
rds:hset("zelda","bow",2017)
rds:hget("zelda","bow")
rds:del("list")
rds:lpop("list")

Docker启动Redis

[root@centos7 /]# docker run --name myredis -p 6379:6379 -v /docker/redis/data:/data -v /docker/redis/conf/redis.conf:/etc/redis/redis.conf -d redis redis-server /etc/redis/redis.conf

7 Mysql客户端

MySQ 是一个非常著名的关系型数据库,特点是开源、免费 性能高 、功能丰富,在很多企业内部都得到了广泛的应用。 OpenResty 基于 cosocket 提供了高效客户端库 lua-resty-mysql ,默认内置OpenResty 发行包内,可以直接使用。

7.1 创建对象

-- 加载mysql库
local mysql = require("resty.mysql")
-- 创建数据库对象
local db,err = mysql:new()
db:set_timeout(1000)

7.2 建立连接

函数connect使用地址、端口、用户名、密码等参数连接数据库;

ok,err errcode , sqlstate = db:connect (options --连接数据库服务器

参数 options 是一个表,指定连接 MySQL 务器 各种参数,较常用的手段有:

host : 服务器名字
port : 端口号
path : 文件名
database : 数据库
user : 用户名
password :密码
charset: 使用的字符集
local mysql = require ” resty .mysql”
local db , err = mysql:new ()
if not db then
ngx.say (” new ms yql failed :”, err)
return
end
db:set_timeout(1000)
local opts = { --指定连接服务器的各种参数host ="127.0.0.1", -- 服务器地址port = 3306 , --服务器端口号database = 'openresty', --数据库名user = 'chrono’, --登录用户名password = ’ xxxxxx ’, --登录密码
local ok , err = db:connect (opts) --连接数据库服务器
if not ok the --检查是否连接成功ngx.say("connect msyql failed :"", err}
db:close() --连接失败需要及时关 闭释放连
return
end

7.3 关闭连接

db:close()

7.4 简单查询

local function close_db(db)
if not db thenreturn
end
db:close()
end
-- 1. 加载myql库
local mysql = require("resty.mysql")
-- 2. 创建数据库对象
local db,err = mysql:new()
db:set_timeout(1000)
if not db thenngx.say("new mysql error",err)return
end
-- 3. 建立连接
local opts = {host = "192.168.66.100", -- 连接服务器地址port = 3306, -- 数据库服务端口号database = "openresty", --数据库名字user = "root", -- 用户名password = "123456" --密码
}
local ok, err,errno,sqlstate = db:connect(opts)
if not ok thenngx.say("connect mysql failed :",err)return close_db(db )
end
local res,err,errcode,sqlstate
res,err = db:query("select * from dog",5)
for i , rows in ipairs(res) dofor k ,v in pairs(rows) dongx.say(k,"=",v)end
end--建表--local create_sql = "create table test_lua(id int primary key auto_increment,ch varchar(20))"--res,err,errno,sqlstate = db:query(create_sql)--if not res then-- ngx.say("create table error:",err,",errno:",errno,",sqlstate:",sqlstate)-- return close_db(db)--end--新增sqllocal insert_sql = "insert into test_lua(ch) values('hello 崔春驰')"res,err,errno,sqlstate = db:query(insert_sql)if not res thenngx.say("insert rows error:",err,",errno:",errno,",sqlstate:",sqlstate)return close_db(db)endngx.say("insert rows:",res.affected_row,",id:",res.insert_id,",res:",type(res))--更新local update_sql = " update test_lua set ch = 'hello2' where id = 1"res,err,errno,sqlstate = db:query(update_sql)if not res thenngx.say("update rows error:",err,",errno:",errno,",sqlstate:",sqlstate)return close_db(db)endngx.say("update rows:",res.affected_row,",res:",type(res))--查询local select_sql = " select id,ch from test_lua"res,err,errno,sqlstate = db:query(select_sql)if not res thenngx.say("select rows error:",err,",errno:",errno,",sqlstate:",sqlstate)return close_db(db)endfor i,row in ipairs(res) dofor name,id in pairs(row) dongx.say("select rows :",i,":",name,"=",id)endendngx.say("select rows:",res.affected_row,",res:",type(res))--防止sql注入local ch_param = ngx.req.get_uri_args()["ch"]ngx.say("ch_param:",ch_param)if not ch_param thench_param = ''end--使用ngx.quote_sql_str防止sql注入local query_sql = " select id,ch from test_lua where ch =" .. ngx.quote_sql_str(ch_param)res,err,errno,sqlstate = db:query(query_sql)if not res thenngx.say("select rows error:",err,",errno:",errno,",sqlstate:",sqlstate)return close_db(db)endfor i,row in ipairs(res) dofor name,id in pairs(row) dongx.say("select rows :",i,":",name,"=",id)endend--删除local delete_sql = "delete from test_lua where id in (2,3,4)"res,err,errno,sqlstate = db:query(delete_sql)if not res thenngx.say("delete rows error:",err,",errno:",errno,",sqlstate:",sqlstate)return close_db(db)endngx.say("delete rows:",res.affected_rows,""

12 高级功能

1 共享内存

共享内存是进程间通信( IPC )的一种常用手段,它在系统内存里开辟了一个特别的区域,多个进程可以共享所有权读写数据,比起信号、管道、消息队列、套接字等其他方式来说速度更快,也更加灵活实用。

OpenResty 内置了强大的共享内存功能,不仅支持简单的数据存取,还支持原子计数和队列操作,用起来就像是一个微型的 Redis 数据库,极大地便利了 worker 进程间的通信和协作,而且还能够演化出许多新的用途,例如缓存、进程锁、流量统计等。

openresty支持三种 布尔 数字 字符串

1.1 配置指令

在使用OpenResty 的共享内存功能之前,需要先在配置文件里定义,格式是:

lua_shared_dict dict size; # 定义一个名字为dict的共享内存 大小为 size
lua_shared_dict shmem 1m; # 定义一个1m 的共享内存 名字为shmem指令 lua_shared_diet 定义的共享内存 diet 会在 OpenResty 里表示为 ngx.shared
表里的一个对象,即 ngx.shared.dict ,可以在任意的执行阶段里使用,通常的形式是:
local diet = ngx.shared.dict 获取共享内存对象
操作共享内存对象需要使用 " : "具体用法与 Redis 命令类似,学习起来非常容易。

1.2 写操作

共享内存对象有五种写入数据的方法:

set : 写入一个值,如果内存已满,会使用LRU算法淘汰数据
safe_set : 类似set 但内存满时不会淘汰数据而是直接写入失败
add: 类似set 但只有key 不存在时才会写入
safe_add:类似add但内存满时不会淘汰数据,而是写入失败
replace: 与add 相反只有key存在时才会写入

1.3 读操作

向共享内存写入数据后就可以用getget_stale方法获取数据:

get : 从共享内存里获取一个值,无数据活过期会返回nil
get_stale : 类似get 但即使数据过期也能得到数据
-- ok , err = shmem:set("num",1,0.05)
-- add 新增数据 如果有改数据则新增失败
--shmem:add("num",10)
-- replace 改写 如果该写不存在的key 就失败
--shmem:replace("num",20)

1.4 删除操作

delete 方法可以手动删除共享内存里的数据

dict:delete(key)

1.5 计数操作

与Redis类似 。 openresty 使用方法incr提供原子计数操作

--local v = shmem:incr("count",1,0)
--ngx.say(v)

1.6 队列操作

OpenResty 的共享内存也提供类似 Redis 的队列操作

lpush/rpush : 在队列两端添加数据
lpop/rpop : 在队列两端弹出数据
llen :获的队列的元素数量
shmem:lpush("name","wcc")
shmem:rpush("name","kalista")
shmem:rpush("name","baizhana")
ngx.say(shmem:lpop("name"))
ngx.say(shmem:rpop("name"))
ngx.say(shmem:llen("name"))

1.7 过期操作

dict:ttl(key)
dict:expire()

2 定时器

定时器是 Web 服务器一项很重要的功能,它与具体的请求处理无关,在服务器的后台“静默”运行常用来延后或者周期性执行必要的任务。

2.1 配置指令

lua_max_running_timers num 排队等待 1024
lua_max_pending_timers num 最多可运行定时任务 256

2.2 单次任务

定时器主要使用的函数是 ngx.timer.at 它创建一个定时任务,当时间到时就在后台执行预设的回调函数

local function once_task(premuture,uri)if preemuture thenngx.log(ngx.WARN,":TASK ")end-- httplocal http = require("resty.http")local httpc = http:new() -- http连接对象local res,err = httpc:request_uri("http://192.168.66.10:8080",{path='/hello'})ngx.log(ngx.ERR,"request to task success :",res.body)endlocal ok , err = ngx.timer.at(0.1,once_task,ngx.var.uri) -- 启动定时器 。传递一个参数if not ok thenngx.say("timer failed :" ,err)end

2.3 周期任务

函数 ngx .timer.every 为这种需求提供了便捷操作,它的接口、功能与 ngx.timer at完全相同,但顾名思义,启动的是一个“周期性”的定时器,每隔 几 秒都会执行一次回调函数

local function cycle_task (premuture,name)if premuture then --检查进程是否处于退出阶段return --结束任务endngx.log(ngx.ERR,"我执行了...")endlocal ok , err = ngx.timer.every(6,cycle_task,"shmem") -- 启动定时器 。传递一个参数if not ok thenngx.say("timer failed :" ,err)end

3 进程管理

OpenResty 的进程模型基于 Nginx ,通常以master/worker 多进程方式提供服务,各 worker 进程互相平等且独立,由 maste 进程通过信号管理各个 worker 进程。

ngx.process 库
local process = require("ngx.process")

3.1 进程类型

single :单一进程
master : 监控进程,即master进程
signaller : 信号进程 -s 参数的进程
worker : 工作进程 最常用的进程对外提供服务
helper : 辅助进程
privileged agent : 特权进程

3.2 工作进程

OpenResty有数个函数用来操作 worker 进程。

ngx.worker.count
ngx.worker.pid
ngx.worker.id

3.3 监控进程

master 进程在 OpenResty 里用到的不 多,因为它仅经过了 con figurationmaster-initing 两个阶段,随后就 fork 变成了 worker 进程 。大多数时间我们都不能操纵 master 进程

3.4 特权进程

特权进程就是一个特殊的work进程。关闭所有监听端口 不对外提供服务。 像一个 沉默的聋子init_by_lua 阶段运行

init_by_lua_block {local process = require("ngx.process") -- 显示记载 ngx.processlocal ok,err = process.enable_privileged_agent() -- 启动特性进程-- 关闭所有的监听端口 特权进程不能接受 rewrite access contentif not ok thenngx.log(ngx.ERR,"failed:",err)end-- ngx.timer.*-- get_master_id kill -9 pid
}

主要使用特权进程做定时器的业务,不会影响正常的前台请求处理服务。 因为他不响应请求。 而且权限很大。

13 HTTPS服务

HTTP 协议是明文传输,在如今的网络世界中显得越来越不安全,容易被监听、篡改或劫持,有很大的安全隐患。

1 什么是HTTPS?

HTTPS和HTTP都是数据传输的应用层协议,区别在于HTTPS比HTTP安全

要说HTTPS,搞IT的甚至是不搞IT的,都知道:“HTTPS比HTTP安全”。因此但凡要开发涉及网络传输的项目时,得到的需求一定有:“要用HTTPS”。

那么,“HTTPS是什么?”
维基百科对HTTPS的解释是:

超文本传输安全协议(英语:HyperText Transfer Protocol Secure,缩写:HTTPS;常称为HTTPover TLS、HTTP over SSL或HTTP Secure)是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,但利用SSL/TLS来加密数据包。HTTPS开发的主要目的,是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。这个协议由网景公司(Netscape)在1994年首次提出,随后扩展到互联网上。

抓重点:HTTPS=HTTP over SSL/TLS,也就是说,HTTPS在传输层TCP和应用层HTTP之间,多走了一层SSL/TLS。

由此可见,TLS/SSL是HTTPS的核心! 那么,这个TLS/SSL又是何方神圣呢?

SSL/TLS协议作用在传输层和应用层之间,对应用层数据进行加密传输。

SSL和TLS都是加密协议。SSL,全称Secure Socket Layer,在1994年由网景公司(Netscape)最早提出1.0版本;TLS,全称Transport Layer Security,则是1999年基于SSL3.0版本上改进而来的。官方建议弃用SSL而保留和采用TLS,但是由于历史原因,SSL仍然存在,而且很多人已经习惯SSL这个名词,因此现在索性就叫成SSL/TLS。
肯定有不少同学不假思索:“当然是HTTP不安全,HTTPS安全,所以选择HTTPS呗!”那么HTTPS比HTTP“好”在哪里?
维基百科上对HTTP的解释如下:

设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。
HTTP的发展是由蒂姆·伯纳斯-李于1989年在欧洲核子研究组织(CERN)所发起。HTTP的标准制定由万维网协会(World Wide Web Consortium,W3C)和互联网工程任务组(Internet Engineering TaskForce,IETF)进行协调,最终发布了一系列的RFC,其中最著名的是1999年6月公布的 RFC 2616,定义了HTTP协议中现今广泛使用的一个版本——HTTP 1.1。

HTTP协议是为了传输网页超文本(文本、图像、多媒体资源),以及规范客户端和服务器端之间互相请求资源的方法的应用层协议。但是这个 HTTP 1.1 版本存在一个很大的问题-明文传输
(Plaintext/Clear Text),这个问题在互联网时代的今天是致命的,一旦数据在公共网络中被第三方截获,其通信内容轻而易举地就被窃取了。

因此,HTTPS应运而生,它被公认的三大优势有:

  1. 数据加密,防窃听
  2. 身份验证,防冒充
  3. 完整性校验,防篡改

接下来将从HTTPS的3个优势展开说明,即:

  • 数据加密,即HTTPS是怎么进行数据加密的,将介绍HTTPS中的对称加密和非对称加密
  • 身份验证,即HTTPS是怎么让客户端相信“发给我数据的服务端是我想要的服务器”,将介绍HTTPS的CA和证书
  • 完整性校验,即HTTPS是怎么做数据完整性校验以防止被篡改,将介绍HTTPS的哈希

2 密码学

密码学的历史很古老,基本原理是字符的移位和替换,但 HTTPS 使用的则是现代密码学,它起源于 20 世纪中后期,与数学和计算机科学密切相关。

2.1 对称加密算法

对称加密算法只有一个密钥,加密和解密都使用这个密钥,所以被称为“对称”。它加密解密的速度很快,但密钥难以安全地分发和保管。
常见的对称加密算法有 DES、 AES、TEA 、ChaCha 等。

2.2 非对称加密算法

非对称加密算法有两个密钥,一个是公开的,称为公钥( public key ),另一个是私密的,称为私钥( private key ),两个密钥都可以互相加密解密。它解决了密钥的分发问题,公钥可以公开给任何人使用,而私钥需要保密非对称加密算法都基于复杂的数学单向函数,需要大 的计算,所以加密解密速度较慢。

常见的非对称加密算法有 RSA ECC DH 等。

2.3 数字签名

基于非对称加密算法,私钥持有者使用私钥对数据的摘要做加密运算,形成“签名”。其他人可以使用对应的公钥解密签名并验证摘要,从而保证数据不可能被篡改。因为私钥只能由持有者所有,所以也可以验证唯 性(不可否认性〉

2.4 数字证书

非对称加密算法中的公钥是公开的,有可能被替换或伪造。数字证书是由权威可信机构(也就是通常所说的 CA )颁发、能够证明公钥持有者身份的电子文件,包含了公钥信息和 CA数字签名,确保公钥是可信的数字证书是目前网络世界安全体系的基础,现行的标准是 .509 ,格式 PEM (可见字符〉和 DER (二进制)两种

这个时候就要使用数字证书了,数字证书认证机构(CA)处于客户端与服务器双方都可信赖的第三方机
构的立场上。
服务端向 CA 申请数字证书,审核通过后 CA 会向申请者签发认证文件-证书,包含以下内容:

  • 证书的发布机构
  • 证书的有效期
  • 公钥
  • 证书所有者
  • 签名
    拿到数字证书后,服务器传递数字证书给客户端。

客户端怎么校验数字证书

3 生成证书


申请者需要将自己的信息及其公钥放入证书请求中。但在实际操作过程中,所需要提供的是私钥而非公钥,因为它会自动从私钥中提取公钥。另外,还需要将提供的数据进行数字签名(使用单向加密),保证该证书请求文件的完整性和一致性,防止他人盗取后进行篡改,例如黑客将为www.baidu.com所申请的证书请求文件中的公司名改成对方的公司名称,如果能够篡改成功,则签署该证书请求时,所颁发的证书信息中将变成他人信息。

3.1 创建一个cert文件夹

用来存放证书和服务器私钥

cd /opt/application
mkdir cert

3.2 进入cert目录下

# 创建服务器私钥,命令会提醒输入一个密码
[root@localhost cert]# openssl genrsa -des3 -out server.key 4096
Generating RSA private key, 4096 bit long modulus (2 primes)
.............++++
............................................................++++
e is 65537 (0x010001)
Enter pass phrase for server.key:
Verifying - Enter pass phrase for server.key:
[root@localhost cert]# ll
total 4
-rw-------. 1 root root 3311 Sep 2 11:15 server.key
生成4096字节的服务器私钥

3.3 创建签名请求的证书(CSR)

openssl req -new -key server.key -out server.csr

3.4 加载SSL

# 使用上述私钥时除去必须的口令(注意,所谓除去,其实就是将必须的私钥密码写入到了私钥文件里面了,
更新了原来的私钥文件)
cp server.key server.key.org
openssl rsa -in server.key.org -out server.key

3.5 通过openssl的x509指令生成证书文件

openssl x509 -req -days 3650 -in server.csr -signkey server.key -out server.crt

4 服务配置

在OpenResty里搭建HTTPS 服务需要使用三个核心指令,指定服务器的监听端口、证书和私钥:

配置文件

server{listen 84 ssl;server_name *.*;ssl_certificate /opt/application/cert/server.crt; # 证书文件ssl_certificate_key /opt/application/cert/server.key; # 私钥文件ssl_session_cache shared:SSL:5m;ssl_session_timeout 10m; # 会话超时时间ssl_prefer_server_ciphers on; #优先使用服务器加密算法
}

5 应用开发

由于 HTTPS 是“ HTTP + SSL/TLS ”,除了在建立连接的握手阶段外,整个请求的加密解密过程对于OpenResty 来说是完全透明的,所以我们可以如同开发 HTTP 服务一样,使用rewrite_by _lua “access_by_lu ” content_by_lua ” “ log_by_lua ”等阶段来实现任意的服务逻辑。

listen 84 ssl;
server_name *.*;
access_log logs/https_access.logmain buffer=2k flush=1s;
ssl_certificate /opt/application/cert/server.crt;
ssl_certificate_key /opt/application/cert/server.key;
ssl_session_timeout 10m;
ssl_prefer_server_ciphers on;
location / {#deny all;content_by_lua_block {ngx.say("hello openresty with https");}
}

6 加载证书

使用指令“ ssl_certificate/ssl_certificate_key ”静态加载证书(磁盘文件)有诸多不便,必须为每一个虚拟主机分配独立的 IP 地址,编写独立的 server {}配置块,代码重复,工作单调低效。

ssl_certificate_by_lua 配合ngx.ssl库可以实现动态加载证书功能
ngx.var ngx.req ngx.say 不可用
openresty 支持 PEM DER

6.1 清理证书

在动态加载证书之前仍然要使用指令“ ssl certificate ”和“ ssl certificate key "设置证书和私钥文件,这是由 Nginx 平台内部机制决定的(没有这两个指令或文件格式错误会报错无法启动),但实际上我们并不会使用这两个文件,需要把它们清除,为后续动态加载的证书和私钥“腾出空间”。

-- 显式加载ngx.ssl
local ssl = require("ngx.ssl")
-- 清理证书
local ok,err = ssl.clear_certs()
if not ok thenngx.log(ngx.ERR,"failed to clear existing")return ngx.exit(ngx.ERR)
end

6.2 设置证书

动态设置证书只需要两个简单步骤

  1. 解析证书 ssl.parse_pem_cert
  2. 设置证书 ssl.set_cert

6.3 设置私钥

动态设置私钥的步骤与设直证书类似,同样使用两个函数:

  1. ssl.parse_pem_priv_key() 解析私钥
  2. ssl_set_priv_key() 设置私钥
    示例:
-- 显式加载ngx.ssl
local ssl = require("ngx.ssl")
-- 清理证书
local ok,err = ssl.clear_certs()
if not ok thenngx.log(ngx.ERR,"failed to clear existing")return ngx.exit(ngx.ERR)
end
-- 加载证书
local function get_pem(file)local f = io.open(file,'r')if not f thenreturnendlocal str = f:read("*a")ngx.log(ngx.ERR,str)f:close()return str
end
local prefix = ngx.config.prefix()
-- 解析证书
local cret ,err = ssl.parse_pem_cert(get_pem(prefix.."/cert/server.crt"))
if not cret thenngx.log(ngx.ERR,"FAILE parsePem_cert",err)
end
-- 设置证书
local ok,err = ssl.set_cert(cret)
if not ok thenngx.log(ngx.ERR,err)
end
-- 解析私钥
local key , err = ssl.parse_pem_priv_key(get_pem(prefix.."/cert/server.key"))
if not key thenngx.log(ngx.ERR,err)
end
-- 加载私钥
local ok ,err = ssl.set_priv_key(key)
if not ok thenngx.log(ngx.ERR,err)
end
--

四、OpenResty性能优化

1 阻塞函数

1.1 执行外部命令

在很多的场景下,开发者并不只是把 OpenResty 当作 web 服务器,而是会赋予更多业务的逻辑在其中。这种情况下,就有可能需要调用外部的命令和工具,来辅助完成一些操作了。

比如杀掉某个进程:

os.execute("kill -HUP " .. pid)

或者是拷贝文件、使用 OpenSSL 生成密钥等耗时更久的一些操作:

os.execute(" cp test.exe /tmp ")
os.execute(" openssl genrsa -des3 -out private.pem 2048 ")

解决方案:
方案一:如果有 FFI 库可以使用,那么我们就优先使用 FFI 的方式来调用。
方案二:使用基于 ngx.pipelua-resty-shell 库。

local shell = require("resty.shell")
shell.run("comand")

1.2 磁盘 I/O

我们再来看下,处理磁盘 I/O 的场景。在一个服务端程序中,读取本地的配置文件是一个很常见的操作,比如下面这段代码:

local path = "/conf/apisix.conf"
local file = io.open(path, "rb")
local content = file:read("*a")
file:close()

解决方案:

local ngx_io = require "ngx.io"
local path = "/conf/apisix.conf"
local file, err = ngx_io.open(path, "rb")
local data, err = file: read("*a")
file:close()

1.3 日志问题

ngx.log(ngx.WARN, "info")

解决方案
你还可以把日志发送到远端的日志服务器上,这样就可以用 cosocket 来完成非阻塞的网络通信了,也就是把阻塞的磁盘 I/O 丢给日志服务,不要阻塞对外的服务。你可以使用 lua-resty-logger-socket ,来完成这样的工作

local logger = require "resty.logger.socket"
if not logger.initted() thenlocal ok, err = logger.init{host = 'xxx',port = 1234,flush_limit = 1234,drop_limit = 5678,}
local msg = "foo"
local bytes, err = logger.log(msg)

2 字符串

2.1 处理请求要短、平、快

OpenResty 是一个 Web 服务器,所以经常会同时处理几千、几万甚至几十万的终端请求。想要在整体上达到最高性能,我们就一定要保证单个请求被快速地处理完成,并回收内存等各种资源。

2.2 避免产生中间数据

避免中间的无用数据,可以说是 OpenResty 编程中最为主要的优化理念。这里,我先给你举一个小例子,来讲解下什么是中间的无用数据。我们来看下面这段代码:

resty -e 'local s= "hello"
s = s .. " world"
s = s .. "!"
print(s)

2.3 字符串是不可变的

我着重强调,在 Lua 中,字符串是不可变的。
具体例子

resty -e 'local begin = ngx.now()
local s = ""
-- for 循环,使用 .. 进行字符串拼接
for i = 1, 100000 dos = s .. "a"
end
ngx.update_time()
print(ngx.now() - begin)

解决后例子

$ resty -e 'local begin = ngx.now()
local t = {}
-- for 循环,使用数组来保存字符串,每次都计算数组长度
for i = 1, 100000 dot[#t + 1] = "a"
end
-- 使用数组的 concat 方法拼接字符串
local s = table.concat(t, "")
ngx.update_time()
print(ngx.now() - begin)

3 Table

table 也是性能的拦路虎

3.1 尽量复用,避免不必要的 table 创建。

你先记住这一点,下面,我们就从 table 的创建、元素的插入、清空、循环使用等方面,分别来介绍相关的优化。
第一步,自然是创建数组。在 Lua 中,我们创建数组的方式很简单:

local t = {}

上面这行代码,就创建了一个空数组;当然,你也可以在创建的时候,就加上初始化的数据:

local color = {first = "red", "blue", third = "green", "yellow"}

不过,第二种写法对于性能的损失比较大,原因在于每次新增和删除数组元素的时候,都会涉及到数组的空间分配、 resize 和 rehash 。

LuaJIT 中的 table.new(narray, nhash) 函数
local new_tab = require "table.new"
local t = new_tab(100, 0)
for i = 1, 100 do
t[i] = i
end

3.2 自己计算 table 下标

有了 table 对象之后,下一步就是向它里面增加元素了。最直接的方法,就是调用 table.insert 这个函数来插入元素

local new_tab = require "table.new"
local t = new_tab(100, 0)
for i = 1, 100 do
table.insert(t, i)
end

或者是先获取当前数组的长度,通过下标的方式来插入元素:

local new_tab = require "table.new"
local t = new_tab(100, 0)
for i = 1, 100 dot[#t + 1] = i
end

这一点又该如何解决呢?让我们看下 lua-resty-redis 这个官方的库是如何做的吧:

local function _gen_req(args)local nargs = #argslocal req = new_tab(nargs * 5 + 1, 0)req[1] = "*" .. nargs .. "\r\n"local nbits = 2for i = 1, nargs dolocal arg = args[i]req[nbits] = "$"req[nbits + 1] = #argreq[nbits + 2] = "\r\n"req[nbits + 3] = argreq[nbits + 4] = "\r\n"nbits = nbits + 5endreturn req
en

3.3 循环使用单个 table

既然 table 这么来之不易,我们自然要好好珍惜,尽量做到重复使用。不过,循环利用也是有条件的。我们先要把 table 中原有的数据清理干净,以免对下一个使用者造成污染。

这时, table.clear 函数就派上用场了。从它的名字你就能看出它的作用,它会把数组中的所有数据清空,但数组的大小不会变。也就是说,你用 table.new(narray, nhash) 生了一个长度为 100 的数组,clear 后,长度还是 100。
为了让你能够更清楚它的实现,下面我给出了一个代码示例,它兼容了标准 Lua:

local ok, clear_tab = pcall(require, "table.clear")if not ok thenclear_tab = function (tab)for k, _ in pairs(tab) dotab[k] = nilendend
end

4 缓存

在硬件层面,大部分的计算机硬件都会用缓存来提高速度,比如 CPU 会有多级缓存,RAID 卡也有读写缓存。而在软件层面,我们用的数据库就是一个缓存设计非常好的例子。在 SQL 语句的优化、索引设计以及磁盘读写的各个地方,都有缓存。

缓存两个原则

  • 一是越靠近用户的请求越好。比如,能用本地缓存的就不要发送 HTTP 请求,能用 CDN 缓存的就不要打到源站,能用 OpenResty 缓存的就不要打到数据库。
  • 二是尽量使用本进程和本机的缓存解决。因为跨了进程和机器甚至机房,缓存的网络开销就会非常大,这一点在高并发的时候会非常明显。

    OpenResty 中有两个缓存的组件:shared dict 缓存和 lru 缓存。前者只能缓存字符串对象,缓存的数据有且只有一份,每一个 worker 都可以进行访问,所以常用于 worker 之间的数据通信。后者则可以缓存所有的 Lua 对象,但只能在单个 worker 进程内访问,有多少个 worker,就会有多少份缓存数据。

4.1 共享字典缓存

第一个问题,缓存数据的序列化。由于共享字典中只能缓存字符串对象,所以,如果你想要缓存数组,就少不了要在 set 的时候要做一次序列化,在 get 的时候做一次反序列化:

resty --shdict='dogs 1m' -e 'local dict = ngx.shared.dogsdict:set("Tom", require("cjson").encode({a=111}))print(require("cjson").decode(dict:get("Tom")).a)'

4.2 LRU 缓存

lru 缓存的接口只有 5 个: new 、 set 、 get 、 delete 和 flush_all 。和上面问题相关的就只有get 接口,让我们先来了解下这个接口是如何使用的:

resty -e 'local lrucache = require "resty.lrucache"
local cache, err = lrucache.new(200)
cache:set("dog", 32, 0.01)
ngx.sleep(0.02)
local data, stale_data = cache:get("dog")
print(stale_data)'

5 缓存风暴

5.1 什么是缓存风暴?


缓存风暴伪代码:

-- 查询共享内存
local value = get_from_cache(key)
if not value then--查询数据库 或者 请求请求tomcat phpvalue = query_db(sql)set_to_cache(value, timeout = 60)
end
return value

5.2 如何避免缓存风暴?

5.2.1 主动更新缓存

在 OpenResty 中,我们可以这样来实现。首先,使用 ngx.timer.every 来创建一个定时任务,每分钟运行一次,去 MySQL 数据库中获取最新的数据,并放入共享字典中:

local function query_db(premature, sql)local value = query_db(sql)set_to_cache(value, timeout = 60)
end
local ok, err = ngx.timer.every(60, query_db, sql)

5.2.2 lua-resty-lock

使用 OpenResty 中的 lua-resty-lock 这个库来加锁,这样的担心就大可不必了。 lua-resty-lock 是 OpenResty 自带的 resty 库,它底层是基于共享字典,提供非阻塞的 lock API。

resty --shdict='locks 1m' -e 'local resty_lock = require "resty.lock"local lock, err = resty_lock:new("locks")local elapsed, err = lock:lock("my_key")-- query db and update cachelocal ok, err = lock:unlock()ngx.say("unlock: ", ok)'

5.2.3 lua-resty-shcache

在上面 lua-resty-lock 的实现中,你需要自己来处理加锁、解锁、获取过期数据、重试、异常处理等各种问题,还是相当繁琐的。所以,这里我再给你介绍一个简单的封装: lua-resty-shcache

local shcache = require("shcache")
local my_cache_table = shcache:new(ngx.shared.cache_dict{ external_lookup = lookup,encode = cmsgpack.pack,decode = cmsgpack.decode,},{ positive_ttl = 10, -- cache good data for 10snegative_ttl = 3, -- cache failed lookup for 3sname = 'my_cache', -- "named" cache, useful for debug / report})local my_table, from_cache = my_cache_table:load(key)

6 多级缓存

6.1 什么是多级缓存

所谓多级缓存,即在整个系统架构的不同系统层级进行数据缓存,以提升访问效率,这也是应用最广的方案之一。

整个多级缓存系统被分为三层,应用层 nginx 缓存,分布式 redis 缓存集群,tomcat 堆内缓存。

6.2 缓存热点

6.3 lua-resty-mlcache

接下来,我们再来介绍下,在 OpenResty 中被普遍使用的缓存封装: lua-resty-mlcache 。它使用 shared dictlua-resty-lrucache ,实现了多层缓存机制。我们下面就通过两段代码示例,来看看这个库如何使用:

local mlcache = require "resty.mlcache"
local cache, err = mlcache.new("cache_name", "cache_dict", {lru_size = 500, -- size of the L1 (Lua VM) cachettl = 3600, -- 1h ttl for hitsneg_ttl = 30, -- 30s ttl for misses
})
if not cache thenerror("failed to create mlcache: " .. err)
end

请求处理时的逻辑代码:

local function fetch_user(id)return db:query_user(id)
end
local id = 123
local user , err = cache:get(id , nil , fetch_user , id)
if err thenngx.log(ngx.ERR , "failed to fetch user: ", err)return
end
if user thenprint(user.id) -- 123
end

你可以看到,这里已经把多层缓存都给隐藏了,你只需要使用 mlcache 的对象去获取缓存,并同时设置好缓存失效后的回调函数就可以了。这背后复杂的逻辑,就可以被完全地隐藏了。

从图中你可以看到,mlcache 把数据分为了三层,即 L1、L2 和 L3。

L1 缓存就是 lua-resty-lrucache。每一个 worker 中都有自己独立的一份,有 N 个 worker,就会有 N份数据,自然也就存在数据冗余。由于在单 worker 内操作 lrucache 不会触发锁,所以它的性能更高,适合作为第一级缓存。

L2 缓存是 shared dict。所有的 worker 共用一份缓存数据,在 L1 缓存没有命中的情况下,就会来查询L2 缓存。ngx.shared.DICT 提供的 API,使用了自旋锁来保证操作的原子性,所以这里我们并不用担心竞争的问题;

L3 则是在 L2 缓存也没有命中的情况下,需要执行回调函数去外部数据库等数据源查询后,再缓存到 L2中。在这里,为了避免缓存风暴,它会使用 lua-resty-lock ,来保证只有一个 worker 去数据源获取数据。

7 漏桶和令牌桶

OpenResty 现在主要被用于作为接入层的 Web 应用,比如 WAF 和 API 网关,这些都要应对刚刚提到的正常和不正常的突发流量。毕竟,如果不能处理好突发流量,后端的服务就很容易被打垮,业务也就无法正常响应了。所以今天,我们就专门来看下,应对突发流量的方法。

流量控制
流量控制是 WAF 和 API 网关都必备的功能之一,它通过一些算法对入口流量进行疏导和控制,来保证上游的服务能够正常运行,从而让系统整体保持健康。

7.1 漏桶算法

7.2 令牌桶算法

令牌桶算法和漏桶算法的目的都是一样的,用来保证后端服务不被突发流量打垮,不过这两者的实现方式并不相同。

令牌桶(Token-Bucket)是目前最常采用的一种流量测量方法,用来评估流量速率是否超过了规定值。这里的令牌桶是指网络设备的内部存储池,而令牌则是指以给定速率填充令牌桶的虚拟信息包。

当数据流到达设备时首先会根据数据的大小从令牌桶中取出与数据大小相当的令牌数量用来传输数据(RFC 标准中定义的大小以 b/s 为单位)。也就是说要使数据被传输必须保证令牌桶里有足够多的令牌,如果令牌数量不够,则数据会被丢弃或缓存。这就可以限制报文的流量只能小于等于令牌生成的速度,达到限制流量的目的。

代码实现:

lua_shared_dict my_limit_req_store 10m;
[root@VM_82_178_centos ~]# grep lua_shared_dict
/usr/local/openresty/nginx/conf/nginx.conf#lua_shared_dict log_list 1024m;lua_shared_dict my_limit_req_store 10m;
[root@VM_82_178_centos limit]# cat
/usr/local/openresty/nginx/conf/limit_lua/limit.req.lua
local limit_req = require "resty.limit.req"
local lim, err = limit_req.new("my_limit_req_store", 2, 0)-- 这里设置rate=2/s,漏桶桶容量设置为0,(也就是来多少水就留多少水)
-- 因为resty.limit.req代码中控制粒度为毫秒级别,所以可以做到毫秒级别的平滑处理if not lim thenngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)return ngx.exit(500)
end
local key = ngx.var.binary_remote_addr
local delay, err = lim:incoming(key, true)
if not delay thenif err == "rejected" thenreturn ngx.exit(403)end--此处如果请求超过每秒2次的话,就会报错403 ,禁止访问ngx.log(ngx.ERR, "failed to limit req: ", err)return ngx.exit(500)
end
if delay >= 0.001 thenlocal excess = errngx.sleep(delay)
end

8 火焰图

一个正常的火焰图,应该呈现出如官网给出的样例(官网的火焰图是抓 C 级别函数):

8.1 什么是火焰图

火焰图和直方图、曲线图一样,是一种分析数据的方式,它可以更直观、更形象地展示数据,让人很容易发现数据中的隐藏信息。之所以叫火焰图,是因为这种图很像一簇火焰。

火焰图展现的一般是从进程(或线程)的堆栈中采集来的数据,即函数之间的调用关系。从堆栈中采集数据有很多方式,下面是几种常见的采集工具:

  • Performance Event
  • SystemTap
  • DTrace
  • OProfile
  • Gprof

数据采集到了,怎么分析它呢?为此,Brendan Gregg开发了专门把采样到的堆栈轨迹(Stack Trace)
转化为直观图片显示的工具——Flame Graph,这样就很容易生成火焰图了。

可见,火线图本身其实很简单,难的是从火焰图中发现问题,并且能够解释这种现象,从而找到优化系统或者解决问题的方法。

8.2 什么时候使用

一般来说,当发现 CPU 的占用率和实际业务应该出现的占用率不相符,或者对 Nginx worker 的资源使用率(CPU,内存,磁盘 IO )出现怀疑的情况下,都可以使用火焰图进行抓取。另外,对 CPU 占用率低、吐吞量低的情况也可以使用火焰图的方式排查程序中是否有阻塞调用导致整个架构的吞吐量低下。
常用的火焰图有三种:

lj-lua-stacks.sxx 用于绘制 Lua 代码的火焰图
sample-bt 用于绘制 C 代码的火焰图
sample-bt-off-cpu 用于绘制 C 代码执行过程中让出 CPU 的时间(阻塞时间)的火焰图

这三种火焰图的用法相似,输出格式一致,所以接下的章节中我们只介绍最为常用的 lj-lua-stacks.sxx

8.3 如何安装火焰图

# 安装perf
yum install perf -y //yum方式安装perf
# 可视化生成器
git clone https://github.com/brendangregg/FlameGraph.git

perf采集数据

# perf record -F 99 -a -g -- sleep 60 //对CPU所有进程以99Hz采集,它的执
行频率是 99Hz(每秒99次),如果99次都返回同一个函数名,那就说明 CPU 这一秒钟都在执行同一个函数,可能存在性能问题。执行60秒后会弹出如下图提示表示采集完成,在当前目录会生成一个perf.data的文件

生成火焰图

# perf script -i perf.data &> perf.unfold //生成脚本文件
# ./FlameGraph/stackcollapse-perf.pl perf.unfold &> perf.folded
# ./FlameGraph/flamegraph.pl perf.folded > perf.svg //执行完成后生成perf.svg图片,可以下载到本地,用浏览器打开 perf.svg,如下图

五、OpenResty开发测试

1 单元测试

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。

准备工作

需要下载lua-resty-test-master 包

1.1 ngxin示例配置

#you do not need the following line if you are using
#the ngx_openresty bundle:
lua_package_path "/path/to/lua-resty-redis/lib/?.lua;;";
server {location /test {content_by_lua_file test_case_lua/unit/test_example.lua;}
}

1.2 编写测试文件

service/http/test_example.lua:local iresty_test = require "resty.iresty_test"
local tb = iresty_test.new({unit_name="example"})
function tb:init( )self:log("init complete")
end
function tb:test_00001( )
error("invalid input")
end
function tb:atest_00002()self:log("never be called")
end
function tb:test_00003( )self:log("ok")
end
-- units test
tb:run()

结果:

[root@localhost test]# resty -ILIB test_example.lua
0.000 [example] unit test start
0.000 [example] init complete
0.000   \_[test_0000003] ↓ok
0.000   \_[test_0000003] PASS
0.000   \_[test_000001] fail test_example.lua:11: invalid input
0.000 [example] unit test complete

2 性能测试


性能测试应该有两个方向:

  • 单接口压力测试
  • 生产环境模拟用户操作高压力测试

生产环境模拟测试,目前我们都是交给公司的 QA 团队专门完成的。这块我只能粗略列举一下:

  • 获取 1000 用户以上生产用户的访问日志(统计学要求 1000 是最小集合)
  • 计算指定时间内(例如 10 分钟),所有接口的触发频率
  • 使用测试工具(loadrunner, jmeter 等)模拟用户请求接口
  • 适当放大压力,就可以模拟 2000、5000 等用户数的情况

2.1 wrk安装

wrk 是一款针对 Http 协议的基准测试工具,它能够在单机多核 CPU 的条件下,使用系统自带的高性能 I/O 机制,如 epoll,kqueue 等,通过多线程和事件模式,对目标机器产生大量的负载。

git clone https://github.com/wg/wrk
make

2.2 wrk参数

使用方法: wrk <选项> <被测HTTP服务的URL>
Options:-c, --connections <N> 跟服务器建立并保持的TCP连接数量-d, --duration <T> 压测时间-t, --threads <N> 使用多少个线程进行压测-s, --script <S> 指定Lua脚本路径-H, --header <H> 为每一个HTTP请求添加HTTP头--latency 在压测结束后,打印延迟统计信息--timeout <T> 超时时间-v, --version 打印正在使用的wrk的详细版本信息<N>代表数字参数,支持国际单位 (1k, 1M, 1G)<T>代表时间参数,支持时间单位 (2s, 2m, 2h)

2.3 简单压测及结果分析

wrk -t8 -c200 -d30s --latency http://www.bing.com
8 threads and 200 connectionsThread Stats   Avg     Stdev   Max     +/- Stdev
Latency        46.67ms 215.38ms 1.67s    95.59%
Req/Sec         7.91k  1.15k    10.26k   70.77%Latency Distribution
50%     2.93ms
75%     3.78ms
90%     4.73ms
99%     1.35s
1790465 requests in 30.01s, 684.08MB read
Requests/sec: 59658.29
Transfer/sec: 22.79MB

以上是使用8个线程200个连接,对bing首页进行了30秒的压测,并要求在压测结果中输出响应延迟信息。

Running 30s test @ http://www.bing.com (压测时间30s)
8 threads and 200 connections (共8个测试线程,200个连接)Thread Stats     Avg     Stdev   Max     +/- Stdev(平均值) (标准差)(最大值)(正负一个标准差所占比例)
Latency       46.67ms  215.38ms 1.67s    95.59%
(延迟)
Req/Sec        7.91k   1.15k    10.26k   70.77%
(处理中的请求数)Latency Distribution (延迟分布)
50%     2.93ms
75%     3.78ms
90%     4.73ms
99%     1.35s (99分位的延迟:%99的请求在1.35s以内)
1790465 requests in 30.01s, 684.08MB read (30.01秒内共处理完成了1790465个请求,读取
了684.08MB数据)
Requests/sec: 59658.29 (平均每秒处理完成59658.29个请求)
Transfer/sec: 22.79MB (平均每秒读取数据22.79MB)

3 灰度发布

3.1 背景

互联网产品开发有个非常特别的地方,就是不停的升级,升级,再升级。采用敏捷开发的方式,基本上保持每周或者每两周一次的发布频率,系统升级总是伴随着各种风险,新旧版本兼容的风险,用户使用习惯突然改变而造成用户流失的风险,系统宕机的风险,500错误服务不可用的风险等等。为了避免这些风险,很多产品都采用了灰度发布的策略,其主要思想就是把影响集中到一个点,然后再发散到一个面,出现意外情况后很容易就回退,即使影响也是可控的。

3.2 概述

灰度发布是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。

3.3 代码实现

创建 app1 app2项目

[root@localhost nginx]# pwd
/usr/local/openresty/nginx
[root@localhost nginx]# cd html/
[root@localhost html]# mkdir -p test1 test2
[root@localhost html]# cd test1
[root@localhost test1]# vim index.html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to OpenResty!</title>
<style>body {width: 35em;margin: 0 auto;font-family: Tahoma, Verdana, Arial, sans-serif;}
</style>
</head>
<body>
<h1>Welcome to OpenResty!</h1>
<p>If you see this page, the OpenResty web platform is successfully installed
and
working. Further configuration is required.</p><p>For online documentation and support please refer to
<h2>===========app1================</h2>
<p><em>Thank you for flying OpenResty.</em></p>
</body>
</html>[root@localhost html]# cd test2
[root@localhost test1]# vim index.html<!DOCTYPE html><html><head><title>Welcome to OpenResty!</title><style>body {width: 35em;margin: 0 auto;font-family: Tahoma, Verdana, Arial, sans-serif;}</style></head><body><h1>Welcome to OpenResty!</h1><p>If you see this page, the OpenResty web platform is successfully installedand working. Further configuration is required.</p><p>For online documentation and support please refer to<h2>===========app2======新版项目==========</h2><p><em>Thank you for flying OpenResty.</em></p></body></html>赋值权限
[root@localhost nginx]# chmod -R 777 html

创建app1.conf app2.conf

#编辑test1.conf
[root@localhost vhosts]# vim test1.conf
server {listen  9091;server_name *.*;location / {root root /opt/application/html/app1;index index.html index.htm;}
}
#编辑test2.conf
[root@localhost vhosts]# vim app2.conf
server {listen  9092;server_name *.*;location / {root /opt/application/html/app2;index index.html index.htm;}
}

编写lua脚本

[root@localhost test]# vim test.lua
ngx.header.content_type="text/plian"
--获取请求参数
--参考地址:https://github.com/openresty/lua-nginx-module#lua_need_request_body
--demo http://127.0.0.1?ver=1.0 http://127.0.0.1?ver=2.0
ngx.req.read_body()
--获取请求数据
local post_args_tab,err=ngx.req.get_post_args()
--ngx.say(type(post_args_tab))
if not post_args_tab thenngx.say('请拼接提交参数')return
end
--获取请求参数
local request_data={}
for k,v in pairs(post_args_tab) dorequest_data[k]=v
end
--判断版本控制参数是否存在
if not request_data['ver'] thenngx.say('ver not is null')return
end
--[[
--循环查看数据
for key,item in pairs(request_data) dongx.say('key',key)ngx.say('item',item)
end
ngx.say(request_data['ver'])
ngx.exit(200)
--]]--根据版本号判断跳转,初始化灰度发布的版本号
local ver=math.floor(2.0)--版本是2.0跳转 test2 [代表是新版代码灰度发布]
--使用的知识点是ngx.exec内部重定向
--地址:https://github.com/openresty/lua-nginx-module#ngxexec
if math.floor(request_data['ver']) == ver then--ngx.say(2.0)ngx.exec("@test2")return
end--其它版本跳转test
--ngx.say(1.0)
ngx.exec("@test1")

关闭nginx代码缓存,修改lua脚本不需要重启

lua_code_cache off;

编辑nginx.conf,引入lua脚本,使用nginx_proxy upstream

[root@localhost nginx]# vim conf/nginx.conf
#test1(默认项目代码)upstream test1 {server 192.168.11.200:9091;
}
#test2(新版代码)upstream test2 {server 192.168.11.200:9092;
}server {listen 80;server_name localhost;#charset koi8-r;#access_log logs/host.access.log main;location / {root html;index index.html index.htm;#引入lua脚本content_by_lua_file /usr/local/openresty/lualib/myself_project/test/test.lua;}#默认环境location @test1 {proxy_pass http://test1;}#新版代码(灰度发布)location @test2 {proxy_pass http://test2;}#error_page 404   /404.html;# redirect server error pages to the static page /50x.html#error_page 500 502 503 504     /50x.html;location = /50x.html {root html;}
}#引入vhosts
include vhosts/*.conf;

六、OpenResty实战案例

1 商品详情定向流量分发

1.1 背景


缓存命中率低
代码实现:

---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by wcc.
--- DateTime: 2021/9/7 16:05
---
-- 根据商品id 做流量得定向分发
-- 获取请求商品id
local uri_args = ngx.req.get_uri_args()
-- 获取商品id
local productId = uri_args["productId"]
if not productId thenngx.log(ngx.ERR,"参数不正确。。。")return
end--- 设置分发地址 -- 使用redis技术动态获取
local hosts = {"192.168.66.100","192.168.66.101","192.168.66.102"}-- 获取id 得hash值 取模做负载均衡 获取到一个地址
local hash,err = ngx.crc32_long(productId)
if not hash thenngx.log(ngx.ERR,"HASH failed ")return
end-- 取模 获取hosts 个数
local index = (hash % table.getn(hosts)) +1
-- http://192.168.66.100
backend = "http://"..hosts[index]
-- 引入http 包
local http = require("resty.http")
local httpc = http:new()
-- 发送请求
local resp,err = httpc:request_uri(backend,{method="GET",keepalive_timeout=60})
if not resp thenngx.say(ngx.ERR,err)return
end
ngx.status = res.status
ngx.say(resp.body)
httpc:close()

2 WAF防火墙

2.1 什么是WAF

Web应用防护系统(也称为:网站应用级入侵防御系统。英文:Web Application Firewall,简称: WAF)。利用国际上公认的一种说法:Web应用防火墙是通过执行一系列针对HTTP/HTTPS的安全策略来专门为Web应用提供保护的一款产品。

2.2 实现WAF

实现WAF的方式有两种:

  1. 使用nginx+lua来实现WAF,须在编译nginx的时候配置上lua
  2. 部署OpenResty,不需要在编译nginx的时候指定lua

这里我们采用的是第二种
WAF一句话描述,就是解析HTTP请求(协议解析模块),规则检测(规则模块),做不同的防御动作(动作模块),并将防御过程(日志模块)记录下来。所以本文中的WAF的实现由五个模块(配置模块、协议解析模块、规则模块、动作模块、错误处理模块)组成。

2.3 WAF的功能

  1. 支持IP白名单和黑名单功能,直接将黑名单的IP访问拒绝。
  2. 支持URL白名单,将不需要过滤的URL进行定义。
  3. 支持 User-Agent 的过滤,匹配自定义规则中的条目,然后进行处理(返回403)。
  4. 支持CC攻击防护,单个URL指定时间的访问次数,超过设定值,直接返回403。
  5. 支持Cookie过滤,匹配自定义规则中的条目,然后进行处理(返回403)。
  6. 支持URL过滤,匹配自定义规则中的条目,如果用户请求的URL包含这些,返回403。
  7. 支持URL参数过滤,原理同上。
  8. 支持日志记录,将所有拒绝的操作,记录到日志中去。
  9. 日志记录为 JSON 格式,便于日志分析,例如使用 ELKStack 进行攻击日志收集、存储、搜索和展示。

2.4 具体实现

下载 waf

# 下载waf
git clone https://github.com/unixhot/waf.git
# 拷贝waf
cp -r ./waf/waf /usr/local/openresty/nginx/conf/
# 创建openresty包软连接
ln -s /usr/local/openresty/lualib/resty/
/usr/local/openresty/nginx/conf/waf/resty

waf 部署

lua_shared_dict limit 50m;
lua_package_path "/usr/local/openresty/nginx/conf/waf/?.lua";
init_by_lua_file "/usr/local/openresty/nginx/conf/waf/init.lua";
access_by_lua_file "/usr/local/openresty/nginx/conf/waf/access.lua";

waf 的模块

[root@linux-node1 waf]# pwd
/usr/local/openresty/nginx/conf/waf
[root@linux-node2 waf]# cat config.lua
--WAF config file,enable = "on",disable = "off"
--waf status
config_waf_enable = "on" #是否开启配置
--log dir
config_log_dir = "/tmp/waf_logs" #日志记录地址
--rule settingconfig_rule_dir = "/usr/local/nginx/conf/waf/rule-config"#匹配规则缩放地址--enable/disable white urlconfig_white_url_check = "on" #是否开启url检测--enable/disable white ipconfig_white_ip_check = "on" #是否开启IP白名单检测--enable/disable block ipconfig_black_ip_check = "on" #是否开启ip黑名单检测--enable/disable url filteringconfig_url_check = "on" #是否开启url过滤--enalbe/disable url args filteringconfig_url_args_check = "on" #是否开启参数检测--enable/disable user agent filteringconfig_user_agent_check = "on" #是否开启ua检测--enable/disable cookie deny filteringconfig_cookie_check = "on" #是否开启cookie检测--enable/disable cc filteringconfig_cc_check = "on" #是否开启防cc攻击--cc rate the xxx of xxx secondsconfig_cc_rate = "10/60" #允许一个ip60秒内只能访问10次--enable/disable post filteringconfig_post_check = "on" #是否开启post检测--config waf output redirect/htmlconfig_waf_output = "html" #action一个html页面,也可以选择跳转--if config_waf_output ,setting urlconfig_waf_redirect_url = "http://www.baidu.com"config_output_html=[[ #下面是html的内容<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta http-equiv="Content-Language" content="zh-cn" /><title>网站防火墙</title></head><body><h1 align="center"> 请安全上网,注意操作规范。</body></html>]]

检测顺序:先检查白名单,通过即不检测;再检查黑名单,不通过即拒绝,检查UA,UA不通过即拒绝;检查cookie;URL检查;URL参数检查,post检查;

2.4.1 模拟sql注入即url攻击

日志显示如下,记录了UA,匹配规则,URL,客户端类型,攻击的类型,请求的数据

http:192.168.66.50/hello/a.sql

2.4.2 模拟URL参数检测

2.4.3 cc攻击

安装ab压测工具
yum -y install httpd-tools
ab -c 100 -n 100 http://127.0.0.1/hello

2.4.4 模拟ip白名单

将请求ip放入ip白名单中
[root@linux-node1 rule-config]# echo “192.168.66.10”
>>/usr/local/openresty/nginx/conf/waf/rule-config/whiteip.rule

2.4.4 模拟ip黑名单

将请求ip放入ip黑名单中
[root@linux-node1 rule-config]# echo “192.168.66.10”
>>/usr/local/openresty/nginx/conf/waf/rule-config/blackip.rule

3 微服务API网关

3.1 什么是网关

从一个房间到另一个房间,必须必须要经过一扇门,同样,从一个网络向另一个网络发送信息,必须经过一道“关口”,这道关口就是网关。顾名思义,网关(Gateway)就是一个网络连接到另一个网络的“关口”。

3.2 那什么是 api 网关呢?

在微服务流行起来之前,api 网关就一直存在,最主要的应用场景就是开放平台,也就是 open api; 这种场景大家接触的一定比较多,比如阿里的开放平台。微服务流行起来后,api网关就成了上层应用集成的标配组件。

3.3 为什么需要网关?

把 API 网关放到微服务的最前端,让 API 网关变成所有应用请求的入口。这样就可以简化客户端实现和微服务应用程序之间的沟通方式。

3.4 OpenResty 为什么能做网关

过网关,可以对 api 访问的前置操作进行统一的管理,比如鉴权、限流、负载均衡、日志收集、请求分片等。所以 API 网关的核心是所有客户端对接后端服务之前,都需要统一接入网关,通过网关层将所有非业务功能进行处理。

网关作用

  • 统一入口
  • 安全:黑名单、权限身份认证
  • 限流:实现微服务访问流量计算,基于流量计算分析进行限流,可以定义多种限流规则。
  • 缓存:数据缓存
  • 日志:日志记录
  • 监控:记录请求响应数据,api耗时分析,性能监控
  • 重试:异常重试
  • 熔断: 降级

需要更加高级功能:

  1. 云原生友好,架构要变得轻巧,便于容器化;
  2. 对接 Prometheus、Zipkin、Skywalking 等统计、监控组件;
  3. 支持 gRPC 代理,以及 HTTP 到 gRPC 之间的协议转换,把用户的 HTTP 请求转为内部服务的gPRC 请求;
  4. 承担 OpenID Relying Party 的角色,对接 Auth0、Okta 等身份认证提供商的服务,把流量安全作为头等大事来对待;
  5. 通过运行时动态执行用户函数的方式来实现 Serverless,让网关的边缘节点更加灵活;
  6. 不锁定用户,支持混合云的部署架构;
  7. 最后,网关节点要状态无关,可以随意地扩容和缩容。

3.5 遇到得问题:

  • 依赖 MySQL 等关系型数据库。
  • 插件不能热加载。
  • 代码结构复杂, 难以掌握。

现有框架

3.6 API 网关的核心组件和概念

  • 路由
  • 插件
  • Schema
  • 存储

3.7 APISIX介绍

APISIX 是一个云原生、高性能、可扩展的微服务 API 开源网关,基于OpenResty(Nginx+Lua)和etcd 来实现,对比传统的API网关,具有动态路由和热插件加载的特点。系统本身自带前端,可以手动配置路由、负载均衡、限速限流、身份验证等插件,操作方便。APISIX是用 Lua 语言开发,语言相对简单,容易上手,同时可以按自己的需求进行系统的二次开发以及开发自己的插件

APISIX功能
APISIX的功能有很多,包括 动态路由 、 url 重写 、 动态上游 、 IP黑白名单 、 A/B测试 、 灰度发布 、 限速限流 、 监控报警 、 健康检查 等等,本文只介绍几个比较常用的功能,其他功能具体可以查看 APISIX 的官方文档说明.

  • 服务热启动功能
  • 热插件功能
  • 动态负载均衡
  • 数据集群
  • 监控

    etcd介绍
    etcd是CoreOS团队于2013年6月发起的开源项目,它的目标是构建一个高可用的分布式键值(key-value)数据库。etcd内部采用 raft 协议作为一致性算法,etcd基于Go语言实现。

etcd作为服务发现系统,有以下的特点:

  • 简单:安装配置简单,而且提供了HTTP API进行交互,使用也很简单
  • 安全:支持SSL证书验证
  • 快速:根据官方提供的benchmark数据,单实例支持每秒2k+读操作
  • 可靠:采用raft算法,实现分布式系统数据的可用性和一致性

3.8 APISIX安装

3.8.1 安装etcd

tar -zxcvf etcd-v3.3.11-linux-amd64.tar.gz
3.8.2 安装APISIX
# 安装 APISIX
yum install -y apisix-1.2-0.el7.noarch.rpm
# 启动 APISIX
$ apisix start

3.9 Getway服务部署


3.10 APISIX动态负载均衡配置


3.11 动态限流限速

APISIX 内置了三个限流限速插件:

limit-count:基于“固定窗口”的限速实现。
limit-req:基于漏桶原理的请求限速实现。
limit-conn:限制并发请求(或并发连接)。
rate 指定得请求速率 秒
burst 请求速率超过多少请求就拒绝
rejected_code 当请求阈值被拒绝得时候就返回状态码
key 用来做请求计数得依据

3.12 动态身份验证

APISIX 内置了四个身份验证插件:

key-auth:基于 Key Authentication 的用户认证。
JWT-auth:基于 JWT (JSON Web Tokens) Authentication 的用户认证。
basic-auth:基于 basic auth 的用户认证。
wolf-rbac:基于 RBAC 的用户认证及授权。

配置JWT-auth插件

exp token 超时时间
key 不同得consumer 对象对应不同得值 。 唯一
secret 加密密钥
algorithm 加密算法

高性能web平台【OpenResty入门与实战】相关推荐

  1. 01 如何学习Python Web开发从入门到实战

    Python Web开发从入门到实战 前言: Python Web是学校所学的课程,我希望在学习的同时通过写笔记的形式来记录我学习以及由学校学习转而自身对此方向感兴趣的一个过程,更多还是让自己在课程结 ...

  2. 高性能web平台【Lua语言快速入门】

    Lua快速入门 一.Lua概述 1.1 Lua是什么 Lua 是一个小巧精妙的脚本语言,诞生于巴西的大学实验室,这个名字在葡萄牙语里的含义是"美丽的月亮".Lua开发小组的目标是开 ...

  3. python从入门到实战django_Python Web开发从入门到实战(Django+Bootstrap微课视频版)

    部分基础知识篇 章Python Web环境搭建 1.1Python Web概述 1.1.1Python语言简介 1.1.2Python Web的优势 1.2安装Python 1.3安装开发工具VS C ...

  4. 【译】MochiWeb(基于Erlang的高性能WEB服务器)实用入门教程

    原文出处:http://alexmarandon.com/articles/mochiweb_tutorial/ 翻译出处:http://www.cnblogs.com/ken-zhang/archi ...

  5. OpenResty 入门教程

    1.OpenResy简介和安装 OpenResty是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库.第三方模块以及大多数的依赖项.用于方便地搭建能够处理超 ...

  6. Swoole入门到实战打造高性能赛事直播平台(完整版)

    Swoole入门到实战打造高性能赛事直播平台(技术分享交流) 下载地址:https://download.csdn.net/download/lxw1844912514/11451621

  7. Nginx高性能Web服务器实战教程PDF

    网站 更多书籍点击进入>> CiCi岛 下载 电子版仅供预览及学习交流使用,下载后请24小时内删除,支持正版,喜欢的请购买正版书籍 电子书下载(皮皮云盘-点击"普通下载" ...

  8. 用Nginx+Lua(OpenResty)开发高性能Web应用

    在互联网公司,Nginx可以说是标配组件,但是主要场景还是负载均衡.反向代理.代理缓存.限流等场景:而把Nginx作为一个Web容器使用的还不是那么广泛.Nginx的高性能是大家公认的,而Nginx开 ...

  9. 使用Nginx+Lua(OpenResty)开发高性能Web应用

    在互联网公司,Nginx可以说是标配组件,但是主要场景还是负载均衡.反向代理.代理缓存.限流等场景:而把Nginx作为一个Web容器使用的还不是那么广泛.Nginx的高性能是大家公认的,而Nginx开 ...

最新文章

  1. Oracle简单常用的数据泵导出导入(expdp/impdp)命令举例(上)
  2. 比特币交易平台 php,比特币PHP离线交易开发包
  3. windows-DLL注入
  4. python爬虫外贸客户_python 爬虫抓取亚马逊数据
  5. python + opencv: 解决不能读取视频的问题
  6. Rust: 如何在windows环境中用Atom中玩转它?--new
  7. delphi打印机编程
  8. matlab无法打开excel的问题
  9. 第一周学习前端html的知识总结与感悟
  10. iOS新知识学习之React Native开发工具集
  11. php毕设代做,客户管理系统,java,jsp,php,好毕设为你指导如何完成一个客户管理系统...
  12. docker 搭建私有仓库registry (多用户)
  13. EXCEL输入数字编号总是变成日期的解决办法
  14. RTP H264 NAL
  15. mysql集群和mongodb集群_mongodb分布式集群架构
  16. Python练习——古典兔子问题(函数封装、条件循环)
  17. 数据库之《会员管理系统》
  18. [转]Half Life 2 Source 引擎介绍
  19. 自由天空XP/2K3封装工具 Easy Sysprep v2.0 正式版封装教程
  20. 深度学习中的遥感影像数据集

热门文章

  1. WZOI-263细菌繁殖
  2. 通讯录管理系统-C++课程设计(试错版)
  3. 查看域名证书到期时间
  4. 用Yolact模型训练自己的数据集
  5. Linux基础系列修炼---笔记1
  6. 给技术管理者的10个锦囊
  7. MATLAB实现自编码器(一)——Autoencoder类和训练设置
  8. DirectX游戏编程入门——第一部分(Windows和DirectX游戏编程引言) —— 初识DirectX
  9. linux shell编程 ppt,Linux常用命令与Shell基本编程.ppt
  10. Qgis 使用QuickOSM插件导入开源地图