[编者按——本文最初发表于 2016 年,现已进行更新,改为使用更新之后修改过的 NGINX 功能。有关详细信息,请参阅下文“借助 NGINX JavaScript 模块进行高级日志记录”和“NGINX Plus 仪表盘”两节。]
我们在 NGINX Plus R5 中引入了 TCP 负载均衡,并不断在后续版本中添加新功能以及 UDP 负载均衡支持。本文探讨了 TCP 负载均衡的关键要求以及 NGINX Plus 如何满足这些要求。
为了探讨 NGINX 的功能,我们将使用一个简单的测试环境,来代表具有可扩展数据库后端的应用的关键组件。有关构建该测试环境的完整说明,请参阅附录。
在该环境中,NGINX 充当数据库服务器的反向代理,监听 MySQL 默认端口 3306。这为客户端提供了一个简单的接口,同时后端 MySQL 节点可以向外扩展(甚至脱机),且不会对客户端产生任何影响。我们将 MySQL 命令行工具用作客户端,代表测试环境中的前端应用。
本文描述的许多功能都适用于 NGINX 开源版和 NGINX Plus。为简单起见,我们全文只提 NGINX,并明确指出 NGINX 开源版不具备的功能。
我们将探讨以下用例:
TCP 负载均衡
在为任何应用配置负载均衡之前,我们最好先了解应用是如何连接到数据库的。我们的大多数测试都使用 mysql(1)
命令行工具连接 Galera 集群、运行查询,然后关闭连接。然而,许多应用框架都使用连接池来最大限度地减少延迟并高效利用数据库服务器资源。
TCP 负载均衡在 stream
配置上下文中进行配置,因此我们在 nginx.conf 主文件中添加了一个 stream
块来创建基本的 MySQL 负载均衡配置。
这可以将我们的 TCP 负载均衡配置与主配置文件分隔开来。然后我们在与 nginx.conf 相同的目录下创建 stream.conf。请注意,默认情况下,conf.d 目录是留给 http
配置上下文的,因此无法向该目录添加 stream
配置文件。
首先,我们定义一个名为 galera_cluster 的上游
组,其中包含 Galera 集群中的三个 MySQL 节点。在我们的测试环境中,它们都支持在本地主机上通过唯一的端口号进行访问。zone
指令定义了所有 NGINX worker 进程共享的内存容量,以维持负载均衡状态。Server{}
块配置了 NGINX 处理客户端的方式。NGINX 监听 MySQL 默认端口 3306,并将所有流量转发到在上游
块中定义的 Galera 集群。
要测试此基本配置是否正常工作,我们可以使用 MySQL 客户端返回我们所连接的 Galera 集群中的节点的主机名。
$ echo "SHOW VARIABLES WHERE Variable_name = 'hostname'" | mysql --protocol=tcp --user=nginx --password=plus -N 2> /dev/null
hostname node1
要检查负载均衡是否正常工作,我们可以重复执行以下命令。
$ !!;!!;!!
hostname node2
hostname node3
hostname node1
这表明默认的轮询负载均衡算法运行正确。然而,如果我们的应用使用连接池访问数据库(如上文所述),那么以轮询方式打开集群连接可能会导致每个节点上的连接数量不均衡。此外,我们不能将连接视为给定的工作负载,因为连接可能处于空闲状态(等待应用查询)或正在处理查询。因此,对于 TCP 长连接,更合适的负载均衡算法是 Least Connections,配置 least_conn
指令:
现在,当客户端打开到数据库的新连接时,NGINX 会选择当前连接数最少的集群节点。
高可用性和健康检查
跨集群共享数据库工作负载的一大优势在于它还可提供高可用性。进行上述配置后,NGINX 将服务器标记为“不可用”,如果无法建立新的 TCP 连接则停止向其发送 TCP 数据包。
除了以这种方式处理不可用的服务器之外,NGINX 还可以配置为执行自动、主动的健康检查,以便在发送客户端请求之前就检测到不可用的服务器。此外,我们还可以使用应用级别的健康检查来测试服务器的可用性,也就是说我们可以向每个服务器发送请求,然后从得到的响应来看服务器是否运行状况良好。这将使我们的配置扩展如下。
在此示例中,match
块定义了启动 MySQL 协议版本 10 握手所需的请求和响应数据。server{}
块中的 health_check
指令应用此模式,并确保 NGINX 仅将 MySQL 连接转发给实际能够接受新连接的服务器。在这种情况下,我们每 20 秒执行一次健康检查,每发生一次故障便从 TCP 负载均衡池中排除一个服务器,并在连续两次成功的健康检查后恢复负载均衡。
日志记录和诊断
NGINX 支持灵活地记录日志,因此它所有的 TCP/UDP 处理进程都可以被记录下来,以便进行调试或离线分析。对于 TCP 协议(例如 MySQL),NGINX 会在连接关闭时写入日志条目。log_format
指令定义了该日志中出现的值。我们可以从 Stream 模块中的任何变量中进行选择。我们在 stream.conf 文件顶部的 stream
上下文中定义了日志格式。
在 server{}
块中添加 access_log
指令以启用日志记录,并指定日志文件的路径以及上个代码段中定义的日志格式的名称。
这会生成日志条目,示例见下。
$ tail -3 /var/log/nginx/galera_access.log
192.168.91.1 [23/Jul/2021:17:42:18 +0100] TCP 200 369 1611 127.0.0.1:33063 0.000 0.003 12.614 12.614
192.168.91.1 [23/Jul/2021:17:42:18 +0100] TCP 200 369 8337 127.0.0.1:33061 0.001 0.001 11.181 11.181
192.168.91.1 [23/Jul/2021:17:42:19 +0100] TCP 200 369 1611 127.0.0.1:33062 0.001 0.001 10.460 10.460
借助 NGINX JavaScript 模块实现高级日志记录
NGINX JavaScript 是“NGINX 原生”编程配置语言。它是面向 NGINX 和 NGINX 的一种独特的JavaScript 实现,专为服务器端用例和按请求处理进程而设计。
[编者按——NGINX JavaScript 模块的用例有很多,以下用例只是其中之一。有关所有用例的列表,请参阅《NGINX JavaScript 模块的用例》。
如下所示,这部分的代码已更新,以反映 NGINX JavaScript 的实现自博客最初发布以来的变化。
- 使用 NGINX JavaScript 0.2.4 中引入的 Stream 模块的重构会话对象
- 使用 NGINX JavaScript 0.4.0 中引入的
js_import
指令。在 NGINX R23及以后版本中,它取代了已弃用的js_include指令。 要了解更多信息,请参阅 NGINX JavaScript 模块的参考文档-配置示例部分显示了 NGINX 配置和 JavaScript 文件的正确语法。
]
在 TCP/UDP 负载均衡的 Stream 模块中,NGINX JavaScript 支持访问请求和响应包的内容。这意味着我们可以检查与 SQL 查询对应的客户端请求,并提取有用元素(例如 SQL 方法:比如 SELECT
和UPDATE
)。然后,NGINX JavaScript 可以将此类值设为常规 NGINX 变量。在下面的示例中,我们在 /etc/nginx/sql_method.js 中插入了我们的 JavaScript 代码。
我们向 getSqlMethod()
函数传递了一个表示当前数据包的 JavaScript 对象。该对象的属性(例如 fromUpstream
和 buffer
)为我们提供了我们需要的关于数据包及其上下文的信息。
我们首先检查 TCP 数据包是否来自客户端,因为我们不需要检查来自上游 MySQL 服务器的数据包。此处,我们感兴趣的是第三个客户端数据包,因为前两个数据包包含了握手和身份验证信息。第三个客户端数据包包含了 SQL 查询。然后,将此字符串的开头与 methods
数组中定义的其中一个 SQL 方法进行比较。当我们发现一个匹配时,我们将结果存储在全局变量 $method
中并在错误日志里写入一个条目。NGINX JavaScript 日志记录被写入错误日志中关于严重性的 info
,因此默认情况下不会出现。
当对同名的 NGINX 变量求值时,会调用 setSqlMethod()
函数。在这种情况下,变量由 NGINX JavaScript 全局变量 $method
(通过调用 getSqlMethod()
函数获得)填充。
请注意,该 NGINX JavaScript 代码专为 MySQL 命令行客户端设计,用于执行单个查询。它不能准确捕获复杂的查询或者长期连接上的多个查询 —— 尽管代码可以适应这些用例。有关安装和启用 NGINX JavaScript 模块的说明,请参阅“NGINX Plus 管理指南”。
为了在日志中加入 SQL 方法,我们在 log_format
指令中添加了 $sql_method
变量。
我们还需要扩展我们的配置,以告知 NGINX 如何以及何时执行 NGINX JavaScript 代码。
首先,我们使用 js_import
指令指定 NGINX JavaScript 代码的位置,并使用 js_set
指令告知 NGINX 在需要计算 $sql_method
变量时调用 setSqlMethod()
函数。在 server{}
块中,我们使用 js_filter
指令指定每次处理数据包时调用的函数。我们还可以为 error_log
指令添加 info
选项,以启用 NGINX JavaScript 日志记录。
添加这些附加配置后,我们的访问日志现在如下所示。
$ tail -3 /var/log/nginx/galera_access.log
192.168.91.1 [23/Jul/2021:17:42:18 +0100] TCP 200 369 1611 127.0.0.1:33063 0.000 0.003 12.614 12.614 UPDATE
192.168.91.1 [23/Jul/2021:17:42:18 +0100] TCP 200 369 8337 127.0.0.1:33061 0.001 0.001 11.181 11.181 SELECT
192.168.91.1 [23/Jul/2021:17:42:19 +0100] TCP 200 369 1611 127.0.0.1:33062 0.001 0.001 10.460 10.460 UPDATE
NGINX Plus 仪表盘
[编者按 – 本节进行了更新,改为引用 NGINX Plus API,它取代并弃用了本文最初讨论的单独的扩展 Status 模块。]
除了详细记录 MySQL 的活动,我们还可以通过 NGINX Plus 实时活动监控仪表盘观察实时指标和上游 MySQL 服务器的健康状况(NGINX 开源版提供了一组比较微观的指标,且仅通过 Stub Status API 提供)。
NGINX Plus 仪表盘从 NGINX Plus R7 开始被引入,它提供了 NGINX Plus API 的 Web 接口。为了实现这一点,我们在一个单独的 /etc/nginx/conf.d/dashboard.conf 文件中的 http
上下文中添加了一个新的 server{}
块:
我们还必须使用 status_zone
指令更新 stream.conf 中的 server{}
块,以便为 MySQL 服务收集监控数据。
进行此配置后,NGINX Plus 仪表盘就可以在端口 8080 上使用了。在下面的屏幕截图中,我们可以看到三个 MySQL 服务器,每个服务器都显示了许多进行中连接的详细信息和当前的健康状况。可以看出,监听 33062 端口的节点以前曾短暂中断过 18.97 秒(在 DT 列列出)。
并发写入的考虑因素
Galera 集群将每个 MySQL 服务器节点呈现为执行读写操作的主数据库。许多应用的读写比都非常高,因此与来自多主数据库集群的灵活性相比,同时由多个客户端更新的相同表行的风险完全可以接受。在并发写入风险较高的情况下,我们提供了两个选项。
- 创建两个单独的上游组,一个用于读取,另一个用于写入,且每个上游组都监听不同的端口。将集群中的一个或多个节点用于写入,其中所有节点都包含在读取组中。必须更新客户端代码,以便为读写操作选择适当的端口。我们在《借助 NGINX Plus 实现高级 MySQL 负载均衡》一文中讨论了该方法,它适用于具有许多 MySQL 服务器节点的高度可扩展的环境。
- 使用一个上游组,并修改客户端代码以检测写入错误。当检测到写入错误时,代码在并发结束后、再次尝试之前呈指数级递减。我们在《借助 NGINX Plus 和 Galera 集群实现 MySQL 高可用性》一文中讨论了该方法,它适用于小集群,其中专门的写入集群节点会损害高可用性。
结语
我们在本文中探讨了负载均衡 TCP(或 UDP)应用(例如 MySQL)的几个基本方面。NGINX 提供了一个功能齐全的 TCP/UDP 负载均衡器,无论流量类型如何,都可帮助您交付具有出色性能、可靠性、安全性及可扩展性的应用。
如欲试用 NGINX Plus,请立即下载 30 天免费试用版,或与我们联系以讨论您的用例。
附录:创建测试环境
测试环境是安装在虚拟机上的,方便隔离和重复使用。但这并不代表不能将其安装到物理“裸机”服务器上。
安装 NGINX Plus
请参阅“NGINX Plus 管理指南”。
为 MySQL 安装 Galera 集群
在此示例中,我们使用每个节点的 Docker 容器在单个主机上安装 Galera 集群。以下操作说明改编自“通过 Docker 开启 Galera 入门之旅”,并假设 Docker 引擎和 MySQL 命令行工具都已安装。
-
创建一个基本的 MySQL 配置文件 (my.cnf),并由 Docker 镜像复制到每个 Galera 容器中。
-
拉取 Galera 的基本 Docker 镜像。
$ sudo docker pull erkules/galera:basic
-
创建第一个 Galera 节点 (node1),并将默认的 MySQL 端口显示为 33061。
$ sudo docker run -p 33061:3306 --detach=true --name node1 -h node1 erkules/galera:basic --wsrep-cluster-name=local-test --wsrep-cluster-address=gcomm://
-
创建第二个 Galera 节点 (node2)。将 MySQL 端口显示为 33062,并链接到 node1,用于集群间通信。
$ sudo docker run -p 33062:3306 --detach=true --name node2 -h node2 --link node1:node1 erkules/galera:basic --wsrep-cluster-name=local-test --wsrep-cluster-address=gcomm://node1
-
使用与 node2 相同的方式创建第三个、也是最后一个 Galera 节点。将 MySQL 端口显示为 33063。
$ sudo docker run -p 33063:3306 --detach=true --name node3 -h node3 --link node1:node1 erkules/galera:basic --wsrep-cluster-name=local-test --wsrep-cluster-address=gcomm://node1
-
创建一个名为 nginx 的用户账户,用于从主机远程访问集群。这是通过运行 Docker 容器内的
mysql(1)
命令来执行的。$ sudo docker exec -ti node1 mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'nginx'@'172.17.0.1' IDENTIFIED BY 'plus'"
-
使用 TCP 协议验证您是否可以从主机连接到 Galera 集群。
$ mysql --protocol=tcp -P 33061 --user=nginx --password=plus -e "SHOW DATABASES" mysql: [Warning] Using a password on the command line interface can be insecure. +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | +--------------------+
-
最后,对另一个集群节点运行相同的命令,以确认 nginx 用户账户已被复制,且集群运行正常。
$ mysql --protocol=tcp -P 33062 --user=nginx --password=plus -e "SHOW DATABASES" mysql: [Warning] Using a password on the command line interface can be insecure. +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | +--------------------+