本文 首发于 🌱 煎茶转载 请注明 来源

OpenResty完全开发指南:构建百万级别并发的Web应用 罗剑锋 131个笔记

◆ 第1章 总论

[插图]

OpenResty使用四位数字作为版本号,形式是:a.b.c.x,其中前三位数字是内部Nginx的版本,作为大版本号,第四位数字是OpenResty自己的发布版本号,也就是小版本号

​​/usr/local/openresty/ #安装主目录
├── bin #存放可执行文件
├── luajit #LuaJIT运行库
├── lualib #Lua组件
├── nginx #Nginx核心运行平台
├── pod #参考手册(restydoc)使用的数据
└── site #包管理工具(opm)使用的数据​​

利用UNIX的“Shebang”(#! ),在脚本文件里的第一行指定resty作为解释器

[插图]

◆ 第2章 Nginx平台

Nginx是一个高性能、高稳定的轻量级HTTP、TCP、UDP和反向代理服务器

配置HTTP相关的功能需要使用指令http{},定义OpenResty里对外提供的HTTP服务,通常的形式是: ​​http { #http块开始,所有的HTTP相关功能
 server { #server块,第一个Web服务
 listen 80; #监听80端口
 location uri { #location块,需指定URI
 … #定义访问此URI时的具体行为
 } #location块结束
 } #server块结束
 server { #server块,第二个Web服务
 listen xxx; #监听xxx端口
 … #其他location定义
 } #server块结束
} #http块结束​​ 由于http块内容太多,如果都写在一个文件里可能会造成配置文件过度庞大,难以维护。

反向代理(Reverse Proxy)是现今网络中一种非常重要的技术,它位于客户端和真正的服务器(即所谓的后端)之间,接受客户端的请求并转发给后端,然后把后端的处理结果返回给客户端

◆ 第3章 Lua语言

Sol”这个词在葡萄牙语(巴西官方语言)里是太阳的意思,而“Lua”的意思就是月亮。

知名的应用有Adobe Lightroom、Fire-fox、Redis等,而游戏则有《魔兽世界》《愤怒的小鸟》《我的世界》等。

Lua语言提供六种基本的数据类型

Lua的字符串形式非常灵活,单引号或者双引号都可以,字符串里也允许使用转义符

Lua还用“[[…]]”的形式支持raw string,括号内的字符不会转义,在写正则表达式或者字符串里有引号、斜线的时候非常方便

与多行注释类似,“[[…]]”的形式也支持在括号中间插入“=”,而且如果“[[”后面是一个换行,那么Lua会自动忽略这个换行,在书写大量文字时是个非常方便的特性

Lua字符串的另外一个特点是只读的

Lua语言在内部使用一个全局散列表来管理所有的字符串,所以多个相同的字符串不会占用多份内存

Lua语言里的变量有作用域的概念,分为局部变量和全局变量,名字区分大小写

局部变量需要使用关键字“local”声明,作用域仅限本代码块(文件内或语句块内),没有关键字“local”声明的变量都是全局变量,而且不需要声明就可以直接使用

在Lua里应当尽量少使用全局变量,多使用局部变量

因为“局部化”,解释器查找的速度也更快

一个比较常用的全局变量是:“_”(下画线,也是合法的变量名)

Lua语言里没有“常量”,实践中我们通常用全大写名字的变量来表示常量

Lua里的运算有算术运算、关系运算、逻辑运算、字符串运算等

不等比较使用的是“~=”

在执行大于或小于比较操作时Lua会检查变量的类型,如果类型不同就会出错

但“==”和“~=”的行为则不同,如果类型不同会直接返回false

为了避免发生意外,必须用函数tonumber()/tostring()显式转换数字或字符串后再做比较运算

Lua的逻辑运算符有and、or和not三个

nil和false认为是假,其他都是真,包括数字0

x and y,如果x是真,返回y,否则返回x

x or y,与and操作正好相反,如果x是真,返回x,否则返回y

not x,只返回true/false,对x取反

Lua对字符串连接操作提供了一个特别的运算符“..”

连接运算还可以自动把数字转换为字符串,无须显式调用tostring()函数:

字符串连接操作应当尽量少用,因为每一次字符串连接就会创建一个新的字符串对象

如果多次操作超长字符串(例如几十MB的大块数据)就可能会导致LuaVM内存耗尽,发生错误。

计算字符串的长度可以用另一个特别的运算符“#”

字符串的关系运算基于字符序(例如最常用的ASCII码表)逐个检查,但相等比较是直接计算内部保存的散列值

很多时候对nil运算都会导致错误

如果一个变量可能是nil,最好使用or运算给它一个默认值

Lua里的语句包括赋值语句、分支语句和循环语句

Lua语句的格式非常自由,不强制要求缩进。语句末尾可以使用“;”表示结束,但不是必需的

使用“do…end”的形式就声明了一个语句(代码)块

Lua使用“=”在变量里存储一个确定类型的值

Lua还允许用逗号分隔,在一个语句里声明或赋值多个变量

如果给一个变量赋值为nil,就表示删除了这个变量

Lua的分支语句只有一种,就是if-else

多重分支需要使用“elseif”

Lua语言的循环语句有while、repeat-until和for三种

repeat-until循环与while差不多,但条件判断与while意义是相反的

Lua的for循环语句有两种形式:数值循环和范围循环

for的数值循环类似其他语言的标准for语句,但形式上要简洁一些

变量var从m前进(或后退)到n,执行循环体里的语句,参数step是用于控制var前进或后退的步长,可以省略,默认是1

for循环里的变量var会自动声明为局部变量

可以用break或者return直接跳出循环,用法与其他语言相同,但必须在语句块的最后——也就是后面紧跟着end/until关键字

如果想要在任意的位置结束循环,可以使用“do break end”的形式

OpenResty使用的LuaJIT扩展支持goto,可以变通实现continue

在Lua语言里函数是一类特殊的变量,它持有一个语句块,能够使用参数执行语句块(也就是“调用”),然后返回结果

Lua的函数就是变量,也可以(最好)使用local局部化。

可以传入任意数量的实参,少的默认值是nil,多的则被忽略

函数的返回值使用return语句,可以用逗号分隔返回多个值,但如果被调用时使用圆括号包围则只会返回一个值

Lua函数的参数都是传值(但表除外)

如果调用时只有一个传入的参数,而且这个参数是字符串或者表,那么Lua允许省略函数的“()”,直接在函数名后写参数

但Lua的表更加灵活,能够模拟出array、list、dict、set、map等常见数据结构,或者其他任意复杂的结构

Lua表里作为索引的key可以是任何非nil值,所以当key类型是整数时表就相当于数组,key类型是字符串时表就相当于字典或关联数组

Lua表对value的类型没有任何限制,当然也可以是另外一个表,从而实现多个表的嵌套。

Lua里定义表使用花括号“{}”

直接使用“{}”就是一个空表,在里面简单地列出表内的元素就声明了一个数组形式的表,使用“key=value”的形式就可以声明为字典形式的表

使用“key=value”的形式时key不需要用单引号或双引号,如果必须要用(例如key里有空格或者其他特殊符号)则要使用“[key]=value”的形式

定义表时的逗号“,”也可以改用分号“;”,两者没有不同,但可以做一些形式上的区分

Lua的表是动态的数据结构,不仅可以访问已有的元素,也可以随时向表里添加或删除元素

操作表里的元素需要使用方括号“[]”

当表作为数组来使用时整数下标索引必须从1开始计数

如果key是字符串我们也可以直接使用点号“.”来操作

运算符“#”可以计算形式表里的数组元素数量,配合for循环可以实现遍历数组

对于字典形式的表,暂时没有办法能够直接获取元素的数量,使用“#”会返回0

for循环语句的第二种形式——范围循环主要用于遍历表里的元素,但需要两个标准库函数的配合:ipairs()和pairs()。

模块就是一个函数集合,通常表现为一个Lua表,里面有模块作者提供的各种功能函数,使用点号“.”即可访问

使用require函数可以加载模块,参数是模块所在的文件名(省略后缀)

通常我们需要用变量来保存require函数的返回结果

require在加载模块的同时会执行文件里的代码

如果使用字符串作为key,那么表本身就是对象,可以任意存储变量和函数

封装”方面,Lua不提供private、public这样的修饰词,表里的所有成员都是公开的

如果想要实现私有成员,那么可以在模块文件里用local修饰

“多态”特性对于Lua来说非常简单,由于表是动态的,里面的成员都能够在运行时随意替换,没有编译型语言静态绑定的烦恼。

继承”在Lua语言里是不提倡的特性,替代方案是使用“原型”(prototype)模式,从一个“原型”对象“克隆”出一个新对象,然后再动态变更其属性,从而达到与“继承”类似的效果。

“原型”模式需要使用Lua的高级特性“元表”(metatable)和函数setmetatable()。

元表描述了表的基本行为,有些类似C++或者Python里的操作符重载,我们需要用的是“__index”元方法,它重载了Lua里查找key的操作,也就是table.key。

函数setmetatable(t, meta)把表t的元表设置为meta并返回t。

如果meta里设置了“__index”方法,那么对t的操作t.key也会同样作用到meta上,即meta.key。这样,表t就“克隆”了表meta的所有成员,表meta就成为了表t的“原型”。 可以通过下面的例子来进一步理解Lua的“原型”操作: ​​local proto = {} – 首先声明一个原型对象,暂时是空表
function proto.go() – 为表添加一个方法,即成员函数
 print(“go pikachu”)
end
local mt = { __index = proto } – 定义元表,注意重载了“__index”
local obj = setmetatable({}, mt) – 调用setmetatable设置元表,返回新表
obj.go() – 新对象是原型的“克隆”,可以执行原型的操作​​ 代码里的关键操作是定义元表mt,里面只需要设置“__index”方法,然后再使用函数setmetatable从mt克隆出一个新的对象。 这两个步骤也可以合并为一次操作:

Lua为面向对象的方式使用表内成员函数提供一个特殊的操作符“:”,它的功能与“.”基本相同,但在调用函数时会隐含传入一个“self”参数

“:”和self不仅可以用在函数调用时,也可以用在函数定义时

“:”其实是一种“语法糖”,是简化的“.”写法

建议尽量使用“:”,它更简洁一些

应该用OpenResty自己的ngx.re系列函数

过table.insert的效率并不高,我们可以用如下的方式更高效地在末尾添加元素:

a[#a + 1] = ’nginx’ – 利用“#”运算符获取长度来添加元素​​

io库里是操作文件的函数,由于文件通常存储在磁盘上,而且是阻塞操作,速度很慢,在OpenResty里应当尽量少用。

在读取数据时,可以使用参数“*a”(即all)读取整个文件,或者“*l”(即line)读取一行,使用数字则读取指定长度的字节

os库包含有操作系统和时间日期相关的函数

形象(但不很准确)地来说,“闭包”就是一个“活的函数”,存在于程序的“高维空间”,可以任意操作函数外部的数据

如果想要Lua代码更加健壮,我们可以使用保护模式来执行可能出错的函数

pcall(protected call)是base库里的一个特殊函数,它“保护调用”一个函数,绝对不会出错,并以true/false返回调用结果

◆ 第4章 LuaJIT环境

LuaJIT是Lua语言的另一个实现,包括一个汇编语言编写的解释器和一个JIT编译器

使用“::label::”的形式定义标签,之后就可以随时用goto改变程序的流程

LuaJIT增强了table库,为它添加了一些新函数,其中较有用的有table.new、table.clear和table.clone。

函数table.clear把表置为空表,但保留之前分配的内存

函数table.clone是OpenResty的LuaJIT分支独有特性,可以高效地“浅”拷贝表(shallow clone)

ffi库不仅可以调用系统函数和OpenResty内部的C函数,还可以加载so形式的动态库,调用动态库里的函数

LuaJIT总是先用解释器运行编译后的字节码,并在运行时做“热点分析”,如果某段代码足够“热”,就会自动触发JIT编译器,尝试把字节码再编译成本地的机器码,让程序能够以最快的速度运行。

有的Lua函数因为实现的代价较高所以不会被编译,只能以字节码的形式运行,这些被称为NYI(Not Yet Implemented)。

◆ 第5章 开发概述

OpenResty提供一个专用指令“content_by_lua_block”,可以在配置文件里书写Lua代码,产生响应内容

启动应用需要使用“-c”参数,让OpenResty以指定的配置文件运行:/usr/local/openresty/bin/openresty -c “pwd/hello.conf”

5.1节的例子是最简单的OpenResty应用,只有一个配置文件,应用代码写在了配置文件里。但实际的项目要比它复杂很多,配置文件和应用代码最好分离管理维护,此外还会有其他的监控脚本、日志文件、数据文件等,必须要用很好的目录层次把它们组织起来。 通常一个OpenResty应用的目录结构如下:[插图] ​​path/to/application #应用的主目录
├── bin #脚本目录,存放各种脚本文件
├── conf #配置目录,存放Nginx配置文件
│ ├── http #存放HTTP服务的配置文件
│ ├── stream #存放TCP/UDP服务的配置文件

在OpenResty里ngx_lua和stream_lua分别属于两个不同的子系统,但指令的功能和格式基本相同

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

开发者必须较好地理解这些阶段的含义和作用,再结合自己的实际业务需求,选择恰当的阶段编写代码实现功能

OpenResty使用“定时器”来周期性地(一次或多次)执行“后台任务”。

这些接口大部分位于全局表ngx里,无须require即可访问(不过也有部分例外)

与Lua自带的标准库函数不同,它们基于Nginx的事件机制和Lua的协程特性,都是“100% nonblocking”的,能够让我们轻松编写出同步非阻塞的高效代码

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

◆ 第6章 基础功能

ngx.sleep是OpenResty提供的同步非阻塞的睡眠函数,可以“睡眠”任意的时间长度但不会阻塞整个服务,这时OpenResty会基于协程机制转而处理其他的请求,等睡眠时间到再“回头”继续执行ngx.sleep后续的代码。

MessagePack是一种二进制数据编码格式,与JSON相比更加小巧紧凑,适合序列化传输大批量的数据。

OpenResty在表ngx.re里提供六个正则表达式相关函数,它们的底层实现是PCRE库,速度极快,完全可以代替Lua标准库的字符串匹配函数

正则替换也有两个函数:ngx.re.sub和ngx.re.gsub,我们在使用时最好不要加“o”选项(原因见6.5.2节

ngx.re.gsub是ngx.re.sub的增强版,可以执行多次正则替换

Cache的容量通常都是有限的,需要使用某种算法更新淘汰数据,较常见的有FIFO、LFU、LRU等

OpenResty基于LRU算法提供了一个方便易用的Cache库lua-resty-lru-cache,并且支持过期时间功能(expire)

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

◆ 第7章 HTTP服务

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

◆ 第8章 访问后端

必须利用Redis、MySQL等数据库服务存储缓存、会话和其他数据,利用Kafka、RabbitMQ等消息队列服务异步发送消息,以及访问Tomcat、PHP等业务服务,访问ZooKeeper、Consul等配置服务,综合协调这些后端才能为最终用户呈现出一个功能完备的应用服务。