Nginx Lua学习

Nginx Lua学习

概念

Nginx 是可扩展的,可用于处理各种使用场景。本内容中,我们一起学习使用 Lua 扩展 Nginx 的功能。

Lua 是一种轻量、小巧的脚本语言,用标准 C 语言编写并以源代码形式开发。设计的目的是为了嵌入到其他应用程序中,从而为应用程序提供灵活的扩展和定制功能。

特性

跟其他语言进行比较,Lua 有其自身的特点:

  • 轻量级

    Lua 用标准 C 语言编写并以源代码形式开发,编译后仅仅一百余千字节,可以很方便的嵌入到其他程序中。

  • 可扩展

    Lua 提供非常丰富易于使用的扩展接口和机制,由宿主语言(通常是 C 或 C++)提供功能,Lua 可以使用它们,就像内置的功能一样。

  • 支持面向过程编程和函数式编程

应用场景

Lua 在不同的系统中得到大量应用,场景的应用场景如下:

游戏开发、独立应用脚本、Web 应用脚本、扩展和数据库插件、系统安全上。

Lua的安装

在 Linux 上安装 Lua 非常简单,只需要下载源码包并在终端解压、编译即可使用。

Lua 的官网地址为:https://www.lua.org

image

点击 download 可以找到对应版本的下载地址,我这里使用最新版 lua-5.4.4,其对应的资源链接地址为 https://www.lua.org/ftp/lua-5.4.4.tar.gz,也可以在 Linux 使用 wget 命令直接下载

1
wget https://www.lua.org/ftp/lua-5.4.4.tar.gz
  • 我这里下载在 /opt/lua
1
2
3
mkdir /opt/lua
cd /opt/lua
wget https://www.lua.org/ftp/lua-5.4.4.tar.gz
  • 解压
1
tar -zxvf lua-5.4.4.tar.gz
  • 检测是否满足 Lua 需要的环境
1
2
cd /opt/lua/lua-5.4.4
make linux test

如果在执行 make linux test 失败,报如下错误(如果没有,则编译安装):

image

说明当前系统缺少 libreadline-dev 依赖包,需要通过命令来进行安装:

1
yum install -y readline-devel
  • 编译安装
1
make install
  • 验证是否安装成功
1
2
[root@master lua-5.4.4]# lua -v
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio

第一个Lua程序

Lua 和 C/C++ 语法非常相似,整体上比较清晰,简洁。条件语句、循环语句、函数调用都与 C/C++ 基本一致。如果对 C/C++ 不太熟悉,也没关系,因为天下语言是一家,基本上理解起来都不会太困难。下面一点一点进行讲解。

大家需要知道的是,Lua 有两种交互方式,分别是:交互式和脚本式,这两者的区别,下面我们分别来讲解下:

交互式

交互式是指可以在命令行输入程序,然后回车就可以看到运行的效果。

Lua 交互式编程模式可以通过命令 lua -ilua 来启用:

1
2
3
lua -i
// 或者
lua
1
2
3
[root@master lua-5.4.4]# lua
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio
>

在命令行中输入如下命令,并按回车,会有输出在控制台:

1
print("Hello world")
1
2
> print("HelloWorld")
HelloWorld

CTRL + C 交互式终端。

脚本式

脚本式是将代码保存到一个以 lua 为扩展名的文件中并执行的方式。

方式一:

我们需要一个文件名为 hello.lua,在文件中添加要执行的代码,然后通过命令 lua hello.lua 来执行,会在控制台输出对应的结果。

创建 hello.lua 文件

1
2
3
mkdir lua_demo
cd lua_demo
vim hello.lua

hello.lua 文件内容

1
print("HelloWorld")

执行 hello.lua 文件:

1
lua hello.lua
1
2
[root@master lua_demo]# lua hello.lua
HelloWorld

不想每次都是用 lua hello.lua 来执行该文件,可不可以直接执行 hello.lua 文件?

方式二:

将 hello.lua 做如下修改

1
2
#!/usr/local/bin/lua
print("Hello World!!!")

第一行用来指定 Lua 解释器命令所在位置为 /usr/local/bin/lua,加上 # 号标记,解释器会忽略它。一般情况下 #! 就是用来指定用哪个程序来运行本文件。

但是 hello.lua 并不是一个可执行文件,需要通过 chmod 来设置可执行权限,最简单的方式为:

1
chmod 755 hello.lua

然后执行该文件

1
./hello.lua
1
2
[root@master lua_demo]# ./hello.lua
Hello World!!!

补充一点,如果想在交互式中运行脚本式的 hello.lua 中的内容,我们可以使用一个 dofile 函数,如:

1
dofile("lua_demo/hello.lua")
1
2
3
4
[root@master lua_demo]# lua
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio
> dofile("hello.lua")
Hello World!!!

::: warning

在 Lua 语言中,连续语句之间的分隔符并不是必须的,也就是说后面不需要加分号,当然加上也不会报错。

:::

在 Lua 语言中,表达式之间的换行也起不到任何作用。如以下四个写法,其实都是等效的

1
2
3
4
5
6
7
8
9
10
11
12
13
# 写法一:换行,不加分号
a=1
b=a+2

# 写法二:换行,加分号
a=1;
b=a+2;

# 写法三:不换行,加分号
a=1; b=a+2;

# 写法四:不换行,不加分号
a=1 b=a+2

不建议使用第四种方式,可读性太差。

Lua的注释

关于 Lua 文件的注释要分两种,第一种是单行注释,第二种是多行注释。

单行注释的语法为:

1
-- 注释内容

多行注释的语法为:

1
2
3
4
--[[
注释内容
注释内容
--]]
1
2
3
4
5
6
#!/usr/local/bin/lua
print("Hello World!!!")
-- print("HelloWorld")
--[[
print("HelloWorld2")
--]]

如果想取消多行注释,只需要在第一个–之前在加一个-即可,如:

1
2
3
4
---[[
注释内容
注释内容
--]]

Lua标识符

换句话说标识符就是我们的变量名,Lua 定义变量名以一个字母 A 到 Z 或 a 到 z 或下划线 _ 开头后加上 0 个或多个字母,下划线,数字(0 到 9)。这块建议大家最好不要使用下划线加大写字母的标识符,如 _VERSION,因为 Lua 的保留字也是这样定义的,容易发生冲突。注意 Lua 是区分大小写字母的。

Lua关键字

下列是 Lua 的关键字,大家在定义常量、变量或其他用户自定义标识符都要避免使用以下这些关键字:

and break do else
elseif end false for
function if in local
nil not or repeat
return then true until
while goto

一般约定,以下划线开头连接一串大写字母的名字(比如 _VERSION)被保留用于 Lua 内部全局变量。这个也是上面我们不建议这么定义标识符的原因。

Lua运算符

Lua中支持的运算符有算术运算符、关系运算符、逻辑运算符、其他运算符。

算术运算符

符号 作用 例子
+ 加法 10 + 20 –> 30
- 减法 20 - 10 –> 10
* 乘法 10 * 20 –> 200
/ 除法 20 / 10 –> 2
% 取余 3 % 2 –> 1
^ 乘幂 10 ^ 2 –> 100
- 符号 -10 –> -10

关系运算符

符号 作用 例子
== 等于 10 == 10 –> true
~= 不等于 10 ~= 10 –> false
> 大于 20 > 10 –> true
< 小于 20 < 10 –> false
>= 大于等于 20 >= 10 –> true
<= 小于等于 20 <= 10 –> false

逻辑运算符

符号 作用 例子
and 逻辑与 A and B(等价于 Java 的 a && b)
or 逻辑或 A or B(等价于 Java 的 a || b)
not 逻辑非 not A(取反,如果 A 为 true,则返回 false)

逻辑运算符可以作为 if 的判断条件,返回的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
A = true
B = true

A and B --> true
A or B --> true
not A --> false

A = true
B = false

A and B --> false
A or B --> true
not A --> false

A = false
B = true

A and B --> false
A or B --> true
not A --> true

其他运算符

符号 作用 例子
.. 连接两个字符串 “HELLO “..”WORLD” –> HELLO WORLD
# 一元预算法,返回字符串或表的长度 #”HELLO” –> 5
1
2
3
4
5
6
[root@master lua_demo]# lua
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio
> "Hello".."World"
HelloWorld
> #"HelloWorld"
10

Lua全局变量&局部变量

在 Lua 语言中,全局变量无须声明即可使用。在默认情况下,变量总是认为是全局的,如果未提前赋值,默认为 nil:

1
2
3
4
5
6
7
8
9
10
[root@master lua_demo]# lua
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio
> print(b)
nil
> b=100
> print(b)
100
> b=nil
> print(b)
nil

要想声明一个局部变量,需要使用 local 来声明

1
2
3
4
5
6
7
[root@master lua_demo]# lua
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio
> local a=100
> print(a)
nil
> local a=100 print(a)
100

如上所示,终端交互式的 local 声明的变量在同一行使用,换行了则离开了该变量的作用域。如果使用了 function 之类的结构或者在文件里使用 local,则可以换行,具体往下看。

Lua数据类型

Lua 有 8 个数据类型

数据类型名 作用
nil 空,无效值
boolean 布尔,true | false
number 数值
string 字符串
function 函数
table
thread 线程
userdata 用户数据

可以使用 type 函数测试给定变量或者的类型:

1
2
3
4
5
6
7
8
9
print(type(nil))				--> nil
print(type(true)) --> boolean
print(type(1.1*1.1)) --> number
print(type("Hello world")) --> string
print(type(io.stdin)) --> userdata
print(type(print)) --> function
print(type(type)) --> function
print(type{}) --> table
print(type(type(X))) --> string

nil

nil 是一种只有一个 nil 值的类型,它的作用可以用来与其他所有值进行区分。当想要移除一个变量时,只需要将该变量名赋值为 nil,垃圾回收就会会释放该变量所占用的内存。

boolean

boolean 类型具有两个值,true 和 false。boolean 类型一般被用来做条件判断的真与假。在 Lua 语言中,只会将 false 和 nil 视为假,其他的都视为真,特别是在条件检测中 0 和空字符串都会认为是真,这个和我们熟悉的大多数语言不太一样。

number

在 Lua5.3 版本开始,Lua 语言为数值格式提供了两种选择 integer(整型)和 float(双精度浮点型),和其他语言不太一样,float 不代表单精度类型。

数值常量的表示方式:

1
2
3
4
4			--> 4
0.4 --> 0.4
4.75e-3 --> 0.00475
4.75e3 --> 4750.0
1
2
3
4
5
6
7
8
9
10
[root@master lua_demo]# lua
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio
> 4
4
> 0.4
0.4
> 4.75e-3
0.00475
> 4.75e3
4750.0

不管是整型还是双精度浮点型,使用 type() 函数来取其类型,都会返回的是 number

1
2
type(3)	--> number
type(3.3) --> number
1
2
3
4
5
6
[root@master lua_demo]# lua
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio
> type(3)
number
> type(3.3)
number

所以它们之间是可以相互转换的,同时,具有相同算术值的整型值和浮点型值在 Lua 语言中是相等的

string

Lua 语言中的字符串即可以表示单个字符,也可以表示一整本书籍。在 Lua 语言中,操作 100K 或者 1M 个字母组成的字符串的程序很常见。

可以使用单引号或双引号来声明字符串

1
2
3
4
a = "hello"
b = 'world'
print(a) --> hello
print(b) --> world
1
2
3
4
5
6
7
8
[root@master lua_demo]# lua
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio
> a = "Hello"
> b = "World"
> print(a)
Hello
> print(b)
World

如果声明的字符串比较长或者有多行,则可以使用如下方式进行声明

1
2
3
4
5
6
7
8
9
10
html = [[
<html>
<head>
<title>Lua-string</title>
</head>
<body>
<a href="http://www.lua.org">Lua</a>
</body>
</html>
]]

table

table 是 Lua 语言中最主要和强大的数据结构。使用 table 表时,Lua 语言可以以一种简单、统一且高效的方式表示数组、集合、记录和其他很多数据结构。Lua 语言中的表本质上是一种辅助数组。这种数组比 Java 中的数组更加灵活,可以使用数值做索引,也可以使用字符串或其他任意类型的值作索引(除 nil 外)。

创建表的最简单方式:

1
a = {}

创建数组方式一

我们都知道数组就是相同数据类型的元素按照一定顺序排列的集合,那么使用 table 如何创建一个数组呢?

1
arr = {"TOM","JERRY","ROSE"}

要想获取数组中的值,我们可以通过如下内容来获取:

1
2
3
4
print(arr[0])		-- nil
print(arr[1]) -- TOM
print(arr[2]) -- JERRY
print(arr[3]) -- ROSE

从上面的结果可以看出来,数组的下标默认是从 1 开始的

创建数组方式二

上述创建数组,也可以通过如下方式来创建:

1
2
3
4
arr = {}
arr[1] = "TOM"
arr[2] = "JERRY"
arr[3] = "ROSE"

创建数组方式三

表的索引即可以是数字,也可以是字符串等其他的内容,所以也可以将索引更改为字符串来创建:

1
2
3
4
arr = {}
arr["X"] = 10
arr["Y"] = 20
arr["Z"] = 30

当然,如果想要获取这些数组中的值,可以使用下面的方式

1
2
3
4
5
6
7
8
9
-- 方式一
print(arr["X"])
print(arr["Y"])
print(arr["Z"])

-- 方式二
print(arr.X)
print(arr.Y)
print(arr.Z)

创建数组方式四

当前 table 的灵活不仅于此,还有更灵活的声明方式:

1
arr = {"TOM",X=10,"JERRY",Y=20,"ROSE",Z=30}

如何获取上面的值?

1
2
3
4
5
6
7
arr[1]       -- TOM
arr["X"] -- 10
arr.X -- 10
arr[2] -- JERRY
arr["Y"] -- 20
arr.Y --20
arr[3] -- ROSE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@master lua_demo]# lua
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio
> arr = {"TOM",X=10,"JERRY",Y=20,"ROSE",Z=30}
> arr[0]
nil
> arr[1]
TOM
> arr[2]
JERRY
> arr[3]
ROSE
> arr["X"]
10
> arr["Y"]
20
> arr["Z"]
30

function

在 Lua 语言中,函数(Function)是对语句和表达式进行抽象的主要方式。

定义函数的语法为:

1
2
3
function functionName(params)

end

函数被调用的时候,传入的参数个数与定义函数时使用的参数个数不一致的时候,Lua 语言会通过抛弃多余参数和将不足的参数设为 nil 的方式来调整参数的个数。

1
2
3
4
5
6
7
8
9
10
-- 函数
function f(a,b)
print(a,b)
end

-- 调用函数
f() --> nil nil
f(2) --> 2 nil
f(2,6) --> 2 6
f(2.6.8) --> 2 6 (8 被丢弃)

可变长参数函数

1
2
3
4
5
6
7
8
9
10
-- 函数
function add(...)
a,b,c=... -- 按顺序令 a,b,c 等于多个参数的前三个
print(a)
print(b)
print(c)
end

-- 调用函数
add(1,2,3) --> 1 2 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@master lua_demo]# lua
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio
> function f(a,b)
>> return a,b
>> end
> x,y = f(11,12)
> print(x,y)
11 12
> function add(...)
>> a,b,c=...
>> print(a)
>> print(b)
>> print(c)
>> end
> add(1,2,3)
1
2
3

函数返回值可以有多个,这点和 Java 不太一样

1
2
3
4
5
6
7
-- 函数
function f(a,b)
return a,b
end

-- 调用函数
x,y = f(11,22) --> x=11,y=22

thread

thread 翻译过来是线程的意思,在 Lua 中,thread 用来表示执行的独立线路,用来执行协同程序。

userdata

userdata 是一种用户自定义数据,用于表示一种由应用程序或 C/C++ 语言库所创建的类型。

Lua控制结构

Lua 语言提供了一组精简且常用的控制结构,包括用于条件执行的证 以及用于循环的 while、repeat 和 for。所有的控制结构语法上都有一个显式的终结符:end 用于终结 if、for 及 while 结构,until 用于终结 repeat 结构。

if判断

if 语句先测试其条件,并根据条件是否满足执行相应的 then 部分或 else 部分。else 部分是可选的。

1
2
3
4
5
6
7
8
9
10
11
12
13
function testif(a)
if a > 0 then
print("a是正数")
end
end

function testif(a)
if a > 0 then
print("a是正数")
else
print("a是负数")
end
end

如果要编写嵌套的 if 语句,可以使用 elseif。 它类似于在 else 后面紧跟一个if。根据传入的年龄返回不同的结果,如

1
2
3
4
5
6
7
8
9
10
11
function show(age)
if age<=18 then
return "青少年"
elseif age>18 and age<=45 then
return "青年"
elseif age>45 and age<=60 then
return "中年人"
elseif age>60 then
return "老年人"
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
> function show(age)
>> if age<=18 then
>> return "青少年"
>> elseif age>18 and age<=45 then
>> return "青年"
>> elseif age>45 and age<=60 then
>> return "中年人"
>> elseif age>60 then
>> return "老年人"
>> end
>> end
> show(59)
中年人

while循环

顾名思义,当条件为真时 while 循环会重复执行其循环体。Lua 语言先测试 while 语句的条件,若条件为假则循环结束;否则,Lua 会执行循环体并不断地重复这个过程。

语法:

1
2
3
while 条件 do
循环体
end

例子:实现数组的循环

1
2
3
4
5
6
7
function testWhile()
local i = 1
while i <= 10 do
print(i)
i = i + 1
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> function testwhile()
>> local i=1
>> while i<=10 do
>> print(i)
>> i=i+1
>> end
>> end
> testwhile()
1
2
3
4
5
6
7
8
9
10

repeat循环

顾名思义, repeat-until 语句会重复执行其循环体直到条件为真时结束。由于条件测试在循环体之后执行,所以循环体至少会执行一次。

语法:

1
2
3
repeat
循环体
until 条件

例子:

1
2
3
4
5
6
7
function testRepeat()
local i = 10
repeat
print(i)
i = i - 1
until i < 1
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> function testRepeat()
>> local i=10
>> repeat
>> print(i)
>> i=i-1
>> until i<1
>> end
> testRepeat(10)
10
9
8
7
6
5
4
3
2
1

for循环

数值型 for 循环

语法:

1
2
3
for param = exp1,exp2,exp3 do
循环体
end

param 的值从 exp1 变化到 exp2 之前的每次循环会执行循环体,并在每次循环结束后将步长(step)exp3 赋值给到 param 上。exp3 可选,如果不设置默认为 1。

1
2
3
for i = 1,100,10 do
print(i)
end
1
2
3
4
5
6
7
8
9
10
11
12
13
> for i=1,100,10 do
>> print(i)
>> end
1
11
21
31
41
51
61
71
81
91

泛型 for 循环

泛型 for 循环通过一个迭代器函数来遍历所有值,类似于 Java 中的 foreach 语句。

语法:

1
2
3
for i,v in ipairs(x) do
循环体
end

i 是数组索引值,v 是对应索引的数组元素值,ipairs 是 Lua 提供的一个迭代器函数,用来迭代数组,x 是要遍历的数组。

例如:

1
2
3
4
5
arr = {"TOME","JERRY","ROWS","LUCY"}

for i,v in ipairs(arr) do
print(i,v)
end
1
2
3
4
5
6
7
8
> arr ={"TOM","JERRY","ROWS","LUCY"}
> for i,v in ipairs(arr) do
>> print(i,v)
>> end
1 TOM
2 JERRY
3 ROWS
4 LUCY

但是如果将 arr 的值进行修改为:

1
arr = {"TOME","JERRY","ROWS",x="JACK","LUCY"}

同样的代码在执行的时候,就只能看到和之前一样的结果,而其中的 x 为 JACK 就无法遍历出来,缺失了数据,如果解决呢?

我们可以将迭代器函数由 ipairs 变成 pairs,如

1
2
3
4
5
6
7
8
9
> arr = {"TOM","JERRY","ROWS",x="JACK","LUCY"}
> for i,v in pairs(arr) do
>> print(i,v)
>> end
1 TOM
2 JERRY
3 ROWS
4 LUCY
x JACK

Java学习之路--静态成员的继承问题

看两段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ExtendsDemo {
public static void main(String[] args) {
Cat c = new Cat();
c.test();
}
}

class Cat extends Animal{}

class Animal{
public static String schoolName ="黑猫";
public static void test(){
System.out.println(schoolName);
}
}

/**
* 输出结果:
* 黑猫
*/

这段代码表明静态的父类成员依旧是可以被继承到子类的,否则不可能通过子类引用访问到这个静态的父类方法。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ExtendsDemo {
public static void main(String[] args) {
Cat c = new Cat();
Animal a = new Cat();

c.test();
a.test();

}
}

class Cat extends Animal{
public static String schoolName = "白猫";
public static void test(){
System.out.println(schoolName);
}
}

class Animal{
public static String schoolName ="黑猫";
public static void test(){
System.out.println(schoolName);
}
}

/**
* 输出结果:
* 白猫
* 黑猫
*/

这段代码表明,虽然静态成员可以被继承,但是和子类的同名静态方法并不构成重写关系,子类的同名静态方法会将父类同名静态方法隐藏,因此也不支持多态。


Nginx 部署与集群

Nginx 部署与集群

Nginx与Tomcat部署

前面已经将 Nginx 的大部分内容进行了讲解,我们都知道了 Nginx 在高并发场景和处理静态资源是非常高性能的,但是在实际项目中除了静态资源还有就是后台业务代码模块,一般后台业务都会被部署在 Tomcat、weblogic 或者是 websphere 等 Web 服务器上。那么如何使用 Nginx 接收用户的请求并把请求转发到后台 Web 服务器?

image

步骤分析:

  • 在服务器 A 上准备 Tomcat 环境,并在 Tomcat 上部署一个 Web 项目。这步骤在 环境准备(Tomcat)
  • 在服务器 B 上准备 Nginx 环境,使用 Nginx 接收请求,并把请求分发到 Tomcat 上。这步骤在 环境准备(Nginx)

环境准备(Tomcat)

本次将采用 Tomcat 作为后台 Web 服务器。

  • 在 服务器 A 上准备一个 Tomcat
  • 下载 Tomcat,这里使用的是 apache-tomcat-9.0.54.tar.gz
  • 进入上传目录,将 Tomcat 进行解压缩
1
2
3
mkdir /usr/local/tomcat

tar -zxf apache-tomcat-9.0.54.tar.gz -C /usr/local/tomcat

准备一个 Web 项目,将其打包为 War 包,这里是 demo.war

  • 将写好的 War 包上传到 Tomcat 目录下的 webapps 包下
  • 将 Tomcat 进行启动,进入 Tomcat 的 bin 目录下,执行命令:
1
./startup.sh

启动完成 Tomcat 后,进行访问测试

浏览器访问:

  • 静态资源:http://192.168.200.146:8080/demo/index.html

image

  • 动态资源:http://192.168.200.146:8080/demo/getAddress

动态资源可以是端口号,此时的端口是 8080image

自此,服务器 A 的 Tomcat 部署已经实现。

demo.war 的内容有什么呢?

其实你可以自己制作一个 war 包,这里说明一下,demo.war 里有两个图片,和一个 index.html

index.html 文件引用了两个图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/jquery.min.js"></script>
<script>
$(function(){
$.get('http://192.168.200.133/demo/getAddress',function(data){
$("#msg").html(data);
});
});
</script>
</head>
<body>
<img src="images/logo.png"/>
<h1>Nginx如何将请求转发到后端服务器</h1>
<h3 id="msg"></h3>
<img src="images/mv.png"/>
</body>
</html>

可以看出,当访问 index.html 时,它会主动去请求 /getAddress,这个请求返回端口号,即把 8080 当作动态资源,显示到页面上,如果是 9090 端口访问,则返回 9090 显示页面上。你也可以直接访问 /getAddress,直接获取端口号。

环境准备(Nginx)

我们已经在 Tomcat 实现了两个效果,那么现在需要把 Tomcat 的地址放到 Nginx 里,由 Nginx 帮我们代理这个 Tomcat 地址,这样我们访问 Nginx,实际上就是访问 Tomcat。

  1. 使用 Nginx 的反向代理,将请求转给 Tomcat 进行处理。
1
2
3
4
5
6
7
8
9
10
upstream webservice {
server 192.168.200.146:8080; # 服务器 A 的 Tomcat 地址
}
server{
listen 80;
server_name localhost;
location /demo {
proxy_pass http://webservice;
}
}
  1. 启动访问服务器 B,测试是否代理到服务器 A 的 Tomcat,效果如图:

image

学习到这,可能大家会有一个困惑,明明直接通过 Tomcat 就能访问,为什么还需要多加一个 Nginx,这样不是反而是系统的复杂度变高了么? 那接下来我们从两个方便给大家分析下这个问题,

  • 第一个使用 Nginx 实现动静分离
  • 第二个使用 Nginx 搭建 Tomcat 的集群

动静分离

什么是动静分离?

  • 动:后台应用程序的业务处理
  • 静:网站的静态资源(html,javaScript,css,images 等文件)
  • 分离:将两者进行分开部署访问,提供用户进行访问。

举例说明就是以后所有和静态资源相关的内容都交给 Nginx 来部署访问,非静态内容则交个类似于 Tomcat 的服务器来部署访问。

为什么要动静分离?

前面我们介绍过 Nginx 在处理静态资源的时候,效率是非常高的,而且 Nginx 的并发访问量也是名列前茅,而 Tomcat 则相对比较弱一些,所以把静态资源交给 Nginx 后,可以减轻 Tomcat 服务器的访问压力并提高静态资源的访问速度。

动静分离以后,降低了动态资源和静态资源的耦合度。如动态资源宕机了也不影响静态资源的展示。

如何实现动静分离?

实现动静分离的方式很多,比如静态资源可以部署到 CDN、Nginx 等服务器上,动态资源可以部署到 Tomcat、weblogic 或者 websphere 上。这里使用 Nginx + Tomcat 来实现动静分离。

需求分析

如下图,因为 Nginx 处理静态资源性能高,所以我们把静态资源放在 Nginx 服务器上,然后把动态资源放到 Tomcat 服务器上。当访问 Nginx 的静态资源时,Nginx 会去访问 Tocmat 获取动态资源。实现动静分离。

image

实现步骤

  • 将 demo.war 项目中的静态资源(两个图片)都删除掉,重新打包生成一个 War 包

    这时候 War 包只留下动态资源,而静态资源要放到 Nginx 上。

  • 将新的 War 包部署到 Tomcat 中,把之前部署的内容删除掉

    • 进入到 tomcat 的 webapps 目录下,将之前的 demo 目录和 demo.war 包删除掉
    • 将新的 War 包复制到 webapps 下
    • 将 tomcat 启动
  • 在 Nginx 所在的服务器 B 上创建如下目录,并将对应的静态资源放入指定的位置

image

1
2
3
4
5
6
7
8
mkdir /usr/local/nginx/html/web/images
mkdir /usr/local/nginx/html/web/js

cp logo.png /usr/local/nginx/html/web/images
cp mv.png /usr/local/nginx/html/web/images
cp jquery.min.js /usr/local/nginx/html/web/js

vim /usr/local/nginx/html/web/index.html

其中 index.html 页面的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/jquery.min.js"></script>
<script>
$(function(){
$.get('http://192.168.200.133/demo/getAddress',function(data){
$("#msg").html(data);
});
});
</script>
</head>
<body>
<img src="images/logo.png"/>
<h1>Nginx如何将请求转发到后端服务器</h1>
<h3 id="msg"></h3>
<img src="images/mv.png"/>
</body>
</html>

第 9 行代码,它访问的这个地址是服务器 B 的 Nginx,通过这个地址让 Nginx 去获取服务器 A 的 Tomcat 动态资源。

  • 在配置文件配置 Nginx 的静态资源与动态资源的访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
upstream webservice{
server 192.168.200.146:8080; # 服务器 A 的 Tocmat
}
server {
listen 80;
server_name localhost;

# 动态资源从 Tomcat 获取
location /demo { # index.html 第 9 行代码触发该 location
proxy_pass http://webservice;
}
# 静态资源从自己身上获取
location ~/.*\.(png|jpg|gif|js){
root html/web;
gzip on;
}

location / {
root html/web;
index index.html index.htm;
}
}
  • 启动测试,访问 http://192.168.200.133

image

假如某个时间点,由于某个原因导致 Tomcat 后的服务器宕机了,我们再次访问 Nginx,会得到如下效果:

image

用户还是能看到页面,只是缺失 Tomcat 的动态资源,这就是前后端耦合度降低的效果,并且整个请求只和后的服务器交互了一次,js 和 images 都直接从 Nginx 服务器里返回,提供了效率,降低了后端服务器的压力。

Tomcat集群搭建

在使用 Nginx 和 Tomcat 部署项目的时候,我们使用的是一台 Nginx 服务器和一台 Tomcat 服务器,效果图如下:

image

那么问题来了,如果 Tomcat 的真的宕机了,整个系统就会不完整,所以如何解决上述问题?

一台服务器容易宕机,那就多搭建几台 Tomcat 服务器,这样的话就提升了后的服务器的可用性。这也就是我们常说的集群,搭建 Tomcat 的集群需要用到了 Nginx 的反向代理和赋值均衡的知识,具体如何来实现?

我们先来分析下原理:

用户请求到 Nginx,Nginx 使用负载均衡对三个 Tomcat 服务器进行访问,如果一个 Tomcat 服务器宕机了,那么还有两个 Tomcat 服务器可以使用。

如图:

image

环境搭建

  1. 准备 3 台 Tomcat 服务器,使用端口进行区分(实际环境应该是三台服务器),修改 Tomcat 的 server.xml,将端口修改分别修改为 8080、8180、8280
1
2
3
cp -r apache-tomcat-9.0.54 tomcat01
cp -r apache-tomcat-9.0.54 tomcat02
cp -r apache-tomcat-9.0.54 tomcat03

修改三个 Tocmat 配置文件的端口

1
2
3
vim tomcat01/conf/server.xml
vim tomcat02/conf/server.xml
vim tomcat03/conf/server.xml

修改的内容位置如下:

image

  1. 在 Nginx 对应的配置文件中添加如下内容:
1
2
3
4
5
6
7
8
9
10
11
12
upstream webservice{
server 192.168.200.146:8080; # tomcat01
server 192.168.200.146:8180; # tomcat02
server 192.168.200.146:8280; # tomcat03
}
server{
listen 80;
server_name localhost;
location /demo {
proxy_pass http://webservice;
}
}
  1. 启动 Tomcat 并访问测试
1
http://192.168.200.146:8080/demo/getAddress

image

1
http://192.168.200.146:8180/demo/getAddress

image

1
http://192.168.200.146:8280/demo/getAddress

image

好了,完成了上述环境的部署,我们已经解决了 Tomcat 的高可用性,一台服务器宕机,还有其他两条对外提供服务,同时也可以实现后台服务器的不间断更新。

但是新问题出现了,上述环境中,如果是 Nginx 宕机了呢,那么整套系统都将服务对外提供服务了,这个如何解决?

Nginx集群搭建

针对于上面提到的问题,我们来分析下要想解决上述问题,需要面临哪些问题?

image

需要两台以上的 Nginx 服务器对外提供服务,这样的话就可以解决其中一台宕机了,另外一台还能对外提供服务,但是如果是两台 Nginx 服务器的话,会有两个 IP 地址,用户该访问哪台服务器,用户怎么知道哪台是好的,哪台是宕机了的?

Keepalived

使用 Keepalived 来解决,Keepalived 软件由 C 编写的,最初是专为 LVS 负载均衡软件设计的,Keepalived 软件主要是通过 VRRP 协议实现高可用功能。

VRRP介绍

VRRP(Virtual Route Redundancy Protocol)协议,翻译过来为虚拟路由冗余协议。VRRP 协议将两台或多台路由器设备虚拟成一个设备,对外提供虚拟路由器 IP,而在路由器组内部,如果实际拥有这个对外 IP 的路由器如果工作正常的话就是 MASTER,MASTER 实现针对虚拟路由器IP的各种网络功能。其他设备不拥有该虚拟 IP,状态为 BACKUP,处了接收 MASTER 的 VRRP 状态通告信息以外,不执行对外的网络功能。当主机失效时,BACKUP 将接管原先 MASTER 的网络功能。

看图分析:VRRP 把两个 Nginx 分成两个路由(VRRP 路由 1 和 VRRP 路由 2),并生成一个 Virtual 路由,用户访问的是 Virtual 路由,该路由会去访问两个 Nginx 生成的 VRRP 路由。那么到底访问谁呢?VRRP 会给两个路由分配角色,一个是 Master(老大),另一个是 Backup(备份),所以访问的是 Master 角色的路由,当 Master 角色路由宕机了,才会找到 Backup 备份路由。

image

从上面的介绍信息获取到的内容就是 VRRP 是一种协议,那这个协议是用来干什么的?

  1. 选择协议

    VRRP 可以把一个虚拟路由器的责任动态分配到局域网上的 VRRP 路由器中的一台。其中的虚拟路由即 Virtual 路由是由 VRRP 路由群组创建的一个不真实存在的路由,这个虚拟路由也是有对应的 IP 地址。而且 VRRP 路由 1 和 VRRP 路由 2 之间会有竞争选择,通过选择会产生一个 Master 路由和一个 Backup 路由。

  2. 路由容错协议

    Master 路由和 Backup 路由之间会有一个心跳检测,Master 会定时告知 Backup 自己的状态,如果在指定的时间内,Backup 没有接收到这个通知内容,Backup 就会替代 Master 成为新的 Master。Master 路由有一个特权就是虚拟路由和后端服务器都是通过 Master 进行数据传递交互的,而备份节点则会直接丢弃这些请求和数据,不做处理,只是去监听 Master 的状态。

用了 Keepalived 后,解决方案如图下:

image

看图分析:VIP 是虚拟路由,是专门给用户发送请求。一旦用户发送请求到 VIP,VIP 就会发送给 Master(主)的 Nginx,如果 Master(主)Nginx 宕机了,才会发送给 Backup(备份) Nginx 路由。

环境搭建

环境准备

VIP IP Nginx IP 主机名 主/从
192.168.200.133(服务器 A) keepalived1 Master
192.168.200.222
192.168.200.122(服务器 B) keepalived2 Backup

确保服务器 A 和服务器 B 的 Nginx 配置保持一致。

keepalived 的安装步骤如下:

  • 步骤1:从官方网站下载 keepalived,官网地址 https://keepalived.org/
  • 步骤2:将下载的资源上传到服务器,这里是 keepalived-2.0.20.tar.gz
  • 步骤3:在 /opt 目录下创建 keepalived 目录,方便管理资源
1
mkdir /opt/keepalived
  • 步骤4:将压缩文件进行解压缩,解压缩到指定的目录
1
tar -zxf keepalived-2.0.20.tar.gz -C /opt/keepalived
  • 步骤5:对 keepalived 进行配置,编译和安装
1
2
3
4
5
cd /opt/keepalived/keepalived-2.0.20

./configure --sysconf=/etc --prefix=/usr/local # 安装到 /usr/local 目录下,可修改

make && make install

两台 Nginx 服务器都要安装 keepalive。

安装完成后,有两个文件需要我们认识下:

  • /etc/keepalived/keepalived.conf:keepalived 的系统配置文件,我们主要操作的就是该文件

image

  • /usr/local/sbin 目录下的 keepalived:这是系统配置脚本,用来启动和关闭 keepalived

image

Keepalived配置文件介绍

打开 keepalived.conf 配置文件

这里面会分三部:

  • 第一部分是 global 全局配置
  • 第二部分是 vrrp 相关配置
  • 第三部分是 LVS 相关配置。

这里主要是使用 keepalived 实现高可用部署,没有用到 LVS,所以我们重点关注的是前两部分。

打开 keepalived.conf 文件

1
vim /etc/keepalived/keepalived.conf

文件内容部分介绍如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# global全局部分
global_defs {

notification_email { # 通知邮件,当 keepalived 切换 Master 和 Backup 时需要发 email 给具体的邮箱地址
tom@itcast.cn
jerry@itcast.cn
}
notification_email_from kele@youngkbt.com # 设置发件人的邮箱信息

smtp_server 192.168.200.1 # 指定 smpt 服务地址

smtp_connect_timeout 30 # 指定 smpt 服务连接超时时间

router_id LVS_DEVEL # 运行 keepalived 服务器的一个标识,可以用作发送邮件的主题信息

# 默认是不跳过检查。检查收到的 VRRP 通告中的所有地址可能会比较耗时,设置此命令的意思是,如果通告与接收的上一个通告来自相同的 master 路由器,则不执行检查(跳过检查)
vrrp_skip_check_adv_addr

vrrp_strict # 严格遵守 VRRP 协议

vrrp_garp_interval 0 # 在一个接口发送的两个免费 ARP 之间的延迟。可以精确到毫秒级。默认是 0

vrrp_gna_interval 0 # 在一个网卡上每组消息之间的延迟时间,默认为 0
}

这里需要修改的是 5、6、8 行代码。

VRRP 部分可以包含以下四个子模块:

  1. vrrp_script
  2. vrrp_sync_group
  3. garp_group
  4. vrrp_instance

我们会用到第一个(vrrp_script)和第四个(vrrp_instance)。

vrrp_instance

vrrp_instance 模块内容:

{3,5,6,7,13}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 设置 keepalived 实例的相关信息,VI_1 为 VRRP 实例名称
vrrp_instance VI_1 {
state MASTER # 有两个值可选 MASTER 主,BACKUP 备
interface ens33 # vrrp 实例绑定的接口,用于发送 VRRP 包[当前服务器使用的网卡名称]
virtual_router_id 51 # 指定 VRRP 实例 ID,范围是 0-255
priority 100 # 指定优先级,优先级高的将成为 MASTER
advert_int 1 # 指定发送 VRRP 通告的间隔,单位是秒。这里是心跳检查的时间
authentication { # vrrp 之间通信的认证信息
auth_type PASS # 指定认证方式。PASS 简单密码认证(推荐)
auth_pass 1111 # 指定认证使用的密码,最多 8 位
}
virtual_ipaddress { # 虚拟 IP 地址设置虚拟 IP 地址,供用户访问使用,可设置多个,一行一个
192.168.200.222
}
}

vrrp_instance 模块中我们修改的是第 3、5、6、7、13 行代码。

第 3 行代码是说明当前 Nginx 服务器的角色是 Master 还是 Backup。分别在服务器 A 和 B 进行角色配置。

第 5 行代码是 VIP 的 ID,如果使用相同的虚拟路由 VIP,请保持 ID 一致。

第 6 行代码是优先级,请让 Master 服务器的优先级大于 Backup 服务器的优先级。如 100 > 90。

第 7 行代码是 Master 和 Backup 之间通信的间隔时间,如果无法通信,说明 Master 已经宕机,则切换为 Backup。

第 13 行代码是用户访问的虚拟 IP 地址,即 VIP,它会发送给 Nginx 服务器。

服务器配置

Keepalived 的具体配置内容如下:

服务器A

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
global_defs {
notification_email {
tom@itcast.cn
jerry@itcast.cn
}
notification_email_from zhaomin@itcast.cn
smtp_server 192.168.200.1
smtp_connect_timeout 30
router_id keepalived1
vrrp_skip_check_adv_addr
vrrp_strict
vrrp_garp_interval 0
vrrp_gna_interval 0
}

vrrp_instance VI_1 {
state MASTER
interface ens33
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.200.222
}
}

服务器B

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
! Configuration File for keepalived

global_defs {
notification_email {
tom@itcast.cn
jerry@itcast.cn
}
notification_email_from zhaomin@itcast.cn
smtp_server 192.168.200.1
smtp_connect_timeout 30
router_id keepalived2
vrrp_skip_check_adv_addr
vrrp_strict
vrrp_garp_interval 0
vrrp_gna_interval 0
}

vrrp_instance VI_1 {
state BACKUP
interface ens33
virtual_router_id 51
priority 90
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.200.222
}
}

访问测试

  1. 启动 keepalived 之前,先使用命令 ip a,查看 192.168.200.133192.168.200.122 这两台服务器的 IP 情况

image

  1. 分别启动两台服务器的 keepalived
1
2
3
cd /usr/local/sbin

./keepalived

再次通过 ip a 查看 IP

image

此时发现服务器 A 多出了 192.168.200.222,正是配置的虚拟路由 VIP,而服务器 B 并没有,说明服务器 A 是 Master,优先级高于服务器 B。

  1. 当把 192.168.200.133 服务器 A 上的 keepalived 关闭后,再次查看 IP

image

说明当 Master 服务器 A 宕机后,服务器 B 由 Backup 晋升为 Master。

通过上述的测试,我们会发现,虚拟 IP(VIP)会在 Master 节点上,当 Master 节点上的 keepalived 出问题以后,因为 Backup 无法收到 Master 发出的 VRRP 状态通过信息,就会直接升为 Master 。VIP 也会「漂移」到新的 Master 。

上面测试和 Nginx 有什么关系?

我们把 192.168.200.133 服务器 A 的 keepalived 再次启动下,由于它的优先级高于 192.168.200.122 服务器 B,所有它会再次成为 Master,VIP 也会「漂移」过去。

我们通过浏览器访问:

1
http://192.168.200.222/

image

如果把 192.168.200.133 服务器 A 的 keepalived 进程关闭掉

1
kill keepalived

再次访问相同的地址,效果如图:

image

虽然效果成功实现了,但是此时是我们手动把服务器上的 keepalived 关闭,才让 VIP 进行切换。

而什么时候关闭 keepalived 呢?

应该是在 keepalived 所在服务器的 Nginx 出现问题后,把 keepalived 关闭掉,就可以让 VIP 执行另外一台服务器。但是现在这所有的操作都是通过手动来完成的,我们如何能让系统自动判断当前服务器的 Nginx 是否正确启动,如果没有,要能让 VIP 自动进行「漂移」,这个问题该如何解决?往下看。

vrrp_script

keepalived 只能做到对网络故障和 keepalived 本身的监控,即当出现网络故障或者 keepalived 本身出现问题时,进行切换。但是这些还不够,我们还需要监控 keepalived 所在服务器上的其他业务,比如 Nginx,如果 Nginx 出现异常了,而 keepalived 却保持正常,是无法完成系统的正常工作的,因此需要根据业务进程的运行状态决定是否需要进行主备切换,这个时候,我们可以通过编写脚本对业务进程进行检测监控。

首先我们要知道 keepalived 的 vrrp_script 的配置模板:

1
2
3
4
5
6
vrrp_script 脚本名称
{
script "脚本位置"
interval 3 # 执行时间间隔
weight -20 # 动态调整 vrrp_instance 的优先级
}

实现步骤:

  • 编写脚本,这里的脚本名是 ck_nginx.sh,位置在 /etc/keepalived 路径下
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
num=`ps -C nginx --no-header | wc -l` # 查询 Nginx 的进程数

if [ $num -eq 0 ];then # 如果 Nginx 的进程数等于 0
/usr/local/nginx/sbin/nginx # 则可执行文件 nginx,启动 Nginx 服务

sleep 2 # 阻塞 2 秒

if [ `ps -C nginx --no-header | wc -l` -eq 0 ]; then # 再次查询 Nginx 的进程数
killall keepalived # 如果 Nginx 的进程数不等于 0,则杀死 keepalived 进程

fi
fi
  • ps 命令用于显示当前进程 (process) 的状态。
  • -C(command):指定命令的所有进程
  • –no-header:排除标题

命令的效果如图:

image

代表目前的 num = 3。

这个脚本其实就是判断 Nginx 是否启动还是宕机了,如果没有启动,则重新启动。重新启动后再次查看 Nginx 是否启动成功,如果没有启动,说明 Nginx 宕机了,则杀死 keepalived 进程,这样,另一台服务器的 Nginx 就晋升为 Master。

  • 为脚本文件设置权限
1
chmod 755 ck_ngi
  • 将脚本添加到 Master 服务器 A 的 keepalived 的配置文件里
1
vim /etc/keepalived/keepalived.conf

添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vrrp_script ck_nginx {
script "/etc/keepalived/ck_nginx.sh" # 执行脚本的位置
interval 2 # 执行脚本的周期,秒为单位
weight -20 # 权重的计算方式
}
vrrp_instance VI_1 {
state MASTER
interface ens33
virtual_router_id 10
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.200.111
}
track_script {
ck_nginx
}
}
  • 如果效果没有出来,可以使用 tail -f /var/log/messages 查看日志信息,找对应的错误信息
  • 两个 Nginx 启动后,关闭 Master 的 Nginx,通过 ip a 查看 Backup 的 Nginx 的IP,是否晋升为 Master

问题思考

通常如果 Master 服务死掉后,Backup 会变成 Master,但是当原来的 Master 服务又恢复了,它会和原来的 Backup 会抢占 VIP,这样就会发生两次切换,这对业务繁忙的网站来说是不好的。所以我们要在配置文件加入 nopreempt 非抢占,但是这个参数只能用于 Backup 的服务器,所以我们在用配置的时候,最好 Master 和 Backup 的 state 都设置成 Backup,这样它们只能通过 priority 优先级来竞争。