You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

151 lines
15 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 23 | 负载均衡选择Nginx还是OpenResty
你好,我是陶辉。
在[\[第21讲\]](https://time.geekbang.org/column/article/252741) 介绍AKF立方体时我们讲过只有在下游添加负载均衡后才能沿着X、Y、Z三个轴提升性能。这一讲我们将介绍最流行的负载均衡Nginx、OpenResty看看它们是如何支持AKF扩展体系的。
负载均衡通过将流量分发给新增的服务器提升了系统的性能。因此我们对负载均衡最基本的要求就是它的吞吐量要远大于上游的应用服务器否则扩展能力会极为有限。因此目前性能最好的Nginx以及在Nginx之上构建的OpenResty通常是第一选择。
系统接入层的负载均衡常通过Waf防火墙承担着网络安全职责系统内部的负载均衡则通过权限、流量控制等功能承担着API网关的职责CDN等边缘节点上的负载均衡还会承担边缘计算的任务。如果负载均衡不具备高度开放的设计或者推出时间短、社区不活跃**我们就无法像搭积木一样,从整个生态中低成本地构建出符合需求的负载均衡。**
很幸运的是Nginx完全符合上述要求它性能一流而且非常稳定。从2004年诞生之初Nginx的模块化设计就未改变过这样16年来累积下的各种Nginx模块都可以复用。它的[2-clause BSD-like license](https://opensource.org/licenses/BSD-2-Clause) 源码许可协议极其开放即使修改源码后仍然可作商业用途因此Nginx之上延伸出了TEngine、OpenResty、Kong等生态这大大扩展了Nginx的能力边界。
接下来我们就以Nginx以及建立了Lua语言生态的OpenResty为例看看负载均衡是怎样扩展系统的以及Nginx和同源的OpenResty有何不同。
## 负载均衡是如何扩展系统提升性能的?
通过AKF立方体X轴扩展系统时负载均衡只需要能够透传协议并选择负载最低的上游应用作为流量分发对象即可。这样三层网络层、四层传输层负载均衡都可用于扩展系统甚至在单个局域网内你还可以使用二层数据链路层负载均衡。其中分发流量的路由算法既可以使用RoundRobin轮转算法也可以基于TCP连接或者UDP Session使用最少连接数算法如下图所示
![](https://static001.geekbang.org/resource/image/a6/yd/a614d25af06a6439874a12d7748afyyd.jpg)
然而基于AKF Y轴扩展系统时负载均衡必须根据功能来分发请求也就是说它必须解析完应用层协议才能明白这是什么请求。因此如LVS这样工作在三层和四层的负载均衡就无法满足需求了我们需要Nginx这样的七层应用层负载均衡它能够从请求中获取到描述功能的关键信息并以此为依据路由请求。比如当HTTP请求中的URL描述功能时Nginx就可以用location匹配URL再基于location来路由请求如下图所示
![](https://static001.geekbang.org/resource/image/19/73/198619be633e7db39e6c8c817078b673.jpg)
基于AKF Z轴扩展时如果只是使用了网络报文中的源IP地址那么三层、四层负载均衡都能胜任。然而如果需要帐号、访问对象等用户信息扩展系统仍然只能使用七层负载均衡从请求中获得。比如Nginx可以通过$变量获取到URL参数或者HEADER头部的值再以此作为路由算法的输入参数。
因此,**七层负载均衡是分布式系统提升性能的必备工具。**除了基于各种路由策略分发流量提高性能及可用性如宕机迁移负载均衡还需要完成上、下游协议间的适配、转换。例如考虑到信息安全跑在公网上的外部协议常基于TLS/SSL协议而在效率优先的企业内网中一般不会使用大幅降低性能的TLS协议因此负载均衡需要拥有卸载或者装载TLS层的能力。
再比如下游客户端多样且难以保持一致比如IE6这个古董浏览器仍然存在于当下的互联网中因此常使用HTTP协议与服务器通讯而上游组件则依据开发团队或者系统架构的特点会选择CGI、uWSGI、gRPC等协议这样负载均衡还得拥有转换各种协议的功能。Nginx可以通过反向代理模块轻松适配各类协议如下所示通过stream模块Nginx也支持四层负载均衡
![](https://static001.geekbang.org/resource/image/ey/93/eyy0dfeeb4783de585789f1c5b768393.jpg)
从性能角度Nginx支持C10M级别的并发连接其原因你可以参考本专栏第1、2部分的内容。从功能角度良好的模块化设计使得Nginx可以完成各类协议的适配不只包括第3部分课程介绍过的通用协议甚至支持Redis、MySQL等专有协议。因此Nginx是目前最好用的负载均衡。
## Nginx上为什么可以执行Lua代码
OpenResty也非常流行其实它就是Nginx只是通过扩展的C模块支持在Nginx中嵌入Lua语言这样Lua模块构建出的生态就可以与C模块协作使用大幅度提升了开发效率。我们先来看下OpenResty与Nginx之间的关系。
OpenResty源代码由**官方Nginx、第三方C模块、Lua语言模块以及一些工具脚本**构成。编译Nginx时OpenResty将自己的第三方C模块按照Nginx的规则添加到可执行文件中包括ngx\_http\_lua\_module和ngx\_stream\_lua\_module这两个C模块它们允许Lua语言运行在Nginx进程中如下图所示
![](https://static001.geekbang.org/resource/image/a7/8e/a73f1f14bec13dddd497aeb4a5393b8e.jpg)
**Lua模块既能够享受Nginx的高性能又通过“协程”**(参见[\[第5讲\]](https://time.geekbang.org/column/article/233629)**、Lua脚本语言提高了开发效率**这也是OpenResty最大的优点。我们先来看看Lua语言是怎么嵌入到Nginx中的。
**Nginx在进程启动、处理请求时提供了许多钩子函数允许第三方C模块将其代码放在这些钩子函数中执行。同时Nginx还允许C模块自行解析nginx.conf中出现的配置项。**这种架构允许OpenResty将Lua代码写进nginx.conf文件再基于[LuaJIT](https://luajit.org/) 即时编译到Nginx中执行。
ngx\_http\_lua\_module模块也正是通过OpenResty提供的以下11个指令嵌入Lua代码的
* 在Nginx启动时嵌入Lua代码包括master进程启动时执行的**init\_by\_lua**指令以及每个worker进程启动时执行的**init\_worker\_by\_lua**指令。
* 在重写URL、访问权限控制等预处理阶段嵌入Lua代码包括解析TLS协议后的**ssl\_certificate\_by\_lua**指令基于openssl的回调函数实现设置动态变量的**set\_by\_lua**指令重写URL阶段的**rewrite\_by\_lua**指令,以及控制访问权限的**access\_by\_lua**指令。
* 生成HTTP响应时嵌入Lua代码包括直接生成响应的**content\_by\_lua**指令,连接上游服务前的**balancer\_by\_lua**指令,处理响应头部的**header\_filter\_by\_lua**指令,以及处理响应包体的**body\_filter\_by\_lua**指令。
* 记录access.log日志时嵌入Lua代码通过**log\_by\_lua**指令实现。
如下图所示:
![](https://static001.geekbang.org/resource/image/97/c3/9747cc2830cb65513ea0f4e5603b9fc3.jpg)
ngx\_stream\_lua\_module模块与之类似这里不再赘述。
当然如果Lua代码只是可以在Nginx进程中执行它是无法处理用户请求的。我们还需要让Lua代码与Nginx中的C代码互相调用去获取、改变HTTP请求、响应的内容。因此ngx\_http\_lua\_module和ngx\_stream\_lua\_module这两个模块通过[FFI](https://luajit.org/ext_ffi.html) 技术将C函数通过Ngx库中的Lua API暴露给纯Lua代码如下图所示
![](https://static001.geekbang.org/resource/image/b3/58/b30b937e7866b9e588ce3e0427b1b758.jpg)
这样通过nginx.conf文件中的11个指令以及FFI技术提供的SDKLua代码才真正可以处理请求Lua生态从这里开始延伸因此OpenResty上还提供了进一步提高开发效率的Lua模块库参见/usr/local/openresty/lualib/目录。关于FFI技术及OpenResty提供的HTTP SDK你可以参考 [《Nginx核心知识100讲》中的第147-154课](https://time.geekbang.org/course/intro/100020301)。
## Nginx与OpenResty的差别在哪里
清楚了OpenResty与Nginx间的相同之处我们再来看二者默认编译出的Nginx可执行程序有何不同之处。
首先看版本差异。当你在[官网](http://nginx.org/en/download.html)下载Nginx时会发现有3类版本Mainline、Stable和Legacy。其中Mainline是单号版本**它是含有最新功能的主线版本迭代速度最快。Stable是mainline版本稳定运行一段时间后将单号大版本转换为双号的稳定版本**比如1.18.0就是由1.17.10转换而来。Legacy则是曾经的稳定版本如下图所示
![](https://static001.geekbang.org/resource/image/63/59/63yy17990a31578a4e3ba00af7b67859.jpg)
你可以通过源代码中的CHANGES文件通过4种不同类型的变更查看版本间的差异包括
* 表示新功能的**Feature**比如下图中HTTP服务新增的auth\_delay指令。
* 表示已修复问题的**Bugfix**。
* 表示已知特性变更的**Change**比如Nginx曾经允许HTTP请求头部中出现多个Host头部但在1.17.9这个Change之后这类HTTP请求将作为非法请求处理。
* 表示安全升级的**Security**比如1.15.6版本就修复了CVE-2018-16843等3个安全问题。
![](https://static001.geekbang.org/resource/image/c3/7e/c366e0f928dfc149da5a4df8cb15c27e.jpg)
当你安装好了OpenResty或者Nginx后你可以通过nginx -v命令查看它们的版本。你会发现**2014年以后发布的OpenResty都是运行在单号Mainline版本上的**
```
# /usr/local/nginx/sbin/nginx -v
nginx version: nginx/1.18.0
# /usr/local/openresty/nginx/sbin/nginx -v
nginx version: openresty/1.15.8.3
```
通常我们会选择稳定的Stable版本OpenResty为什么会选择单号的Mainline版本呢这是因为**Nginx与OpenResty的版本发布频率完全不同**2012年后Nginx每年大约发布10多个版本如下图所示versions统计了每年发布的版本数图中其他3条拆线统计了每年feature、bugfix、change的变更次数
![](https://static001.geekbang.org/resource/image/bd/68/bd39d1fd2abbcf1787a34aeb008fa368.jpg)
OpenResty每年的版本更新频率是个位数特别是从2018年到现在OpenResty只发布了4个版本。所以**为了使用尽量运行在最新版本上OpenResty选择了Mainline单号版本。**
你可能会想OpenResty并没有修改Nginx的源代码为什么不能由用户在官方Nginx上自行添加C模块实现OpenResty的安装呢这源于部分OpenResty C模块没有按照Nginx架构指定的顺序添加到模块列表中而且它们的编译环境也过于复杂。因此OpenResty放弃了Nginx官方的configure文件用户需要使用OpenResty改造过的configure脚本编译Nginx。
再来看模块间的差异。如果你留意OpenResty与Nginx间二进制文件的体积会发现使用默认配置时**OpenResty的可执行文件大了5倍**如下所示:
```
# ls -s --block-size=1 /usr/local/openresty/nginx/sbin/nginx
16437248 /usr/local/openresty/nginx/sbin/nginx
# ls -s --block-size=1 /usr/local/nginx/sbin/nginx
3851568 /usr/local/openresty/nginx/sbin/nginx
```
这由2个原因所致。
首先官方Nginx提供的四层负载均衡功能由18个STREAM模块实现、TLS协议处理功能默认都是不添加到Nginx中的而OpenResty的configure脚本将其改为了默认模块。当然如果你在编译官方Nginx时加入以下选项
```
./configure --with-stream --with-stream_ssl_module --with-stream_ssl_preread_module --with-http_ssl_module
```
那么从官方模块上Nginx就与OpenResty完全一致了此时再观察二进制文件的体积会发现它翻了一倍
```
# ls -s --block-size=1 /usr/local/openresty/nginx/sbin/nginx
6999144 /usr/local/openresty/nginx/sbin/nginx
```
其次OpenResty添加了近20个第三方C模块除了前文介绍过支持Lua语言的2个模块外还有支持Redis、Memcached、MySQL等服务的模块。这些模块编译时还需要链接依赖的软件库因此它们又将Nginx可执行文件的体积增加了1倍多。
除版本、模块外OpenResty与Nginx间还有一些小的差异比如Nginx使用了GCC编译器的-O1优化参数而OpenResty则使用了-O2优化参数。再比如官方Nginx最新版本的Makefile支持upgrade参数简化了热升级操作。当然这些小改动并不重要只要修改configure脚本就能做到。
我们到底该如何在二者中选择呢我认为如果不使用Lua语言那么我建议使用Nginx。官方Nginx的Stable版本更稳定可执行文件的体积也更小。**如果你需要使用OpenResty、TEngine中的部分C模块可以通过add-module选项将其加入到官方Nginx中。**
如果所有C模块都无法满足业务需求你就应该选择OpenResty。注意Lua语言给你带来极大灵活性的同时也会引入许多不确定性。比如如果你调用了会导致进程休眠的Lua阻塞函数比如封装了系统调用的原生Lua库或者第三方服务提供的同步SDK将会导致Nginx正在处理数万并发请求的C模块同时进入休眠从而让服务的性能大幅度下降。
## 小结
这一讲我们介绍了负载均衡的工作方式以及Nginx、OpenResty这两个最流行的负载均衡之间的异同。
任何负载均衡都能从AKF X轴水平扩展系统但只有能够解析应用层协议获取到请求的功能、用户身份、访问对象等信息才能够沿AKF Y轴、Z轴全方位扩展系统。因此七层负载均衡是分布式系统提升性能的利器。
Nginx的开放式架构允许第三方模块通过10多个钩子函数在不同的生命周期中处理请求。同时还允许C模块自行解析nginx.conf配置文件。这样OpenResty就通过2个C模块将Lua代码用LuaJIT编译到Nginx中执行并通过FFI技术将C函数暴露给Lua代码。这就是OpenResty允许Lua语言与C模块协同处理请求的原因。
OpenResty虽然就是Nginx但由于版本发布频率低于官方Nginx因此使用了单号的Mainline版本以获得Nginx的最新特性。由于OpenResty默认加入了四层负载均衡和TLS协议处理功能还新增了近20个第三方C模块这造成它编译出的Nginx体积大了5倍。如果无须使用Lua语言就能够满足业务需求我推荐你使用Nginx。
## 思考题
最后留给你一道思考题。Nginx、OpenResty都有着丰富、活跃的生态这也是我们选择开源软件的一个前提。这节课我们提到开放的设计是形成生态的一个必要因素然而这二者都有许多没有形成生态的竞争对手你认为开源软件构建出庞大的生态还需要哪些充分条件呢欢迎你在留言区与大家一起探讨。
感谢阅读如果你觉得这节课让你对负载均衡有了更深的了解搞清楚了OpenResty与Nginx间的差别也欢迎把它分享给你的朋友。