nginScript 系列:通过 TCP 负载均衡和 Galera 集群来扩展 MySQL

NGINX Plus 在

R5

版本里就引入了 TCP 的负载均衡,在随后的版本里不断地添加新特性,包括支持UDP 的负载均衡。在这篇文章里,我们将探讨 NGINX Plus 是如何实现 TCP 负载均衡的。

为了了解 NGINX Plus 的特性,我们使用了一个简单的测试环境,这个环境包含了应用程序所必需的组件,包括一个可伸缩的数据库。

(点击放大图像)

MySQL 负载均衡测试环境

在这个环境里,NGINX Plus 作为数据库服务器的反向代理,监听 MySQL 的 3306 端口。反向代理为客户端提供了一个简单的接口,后端的 MySQL 节点可以自由伸缩(甚至离线),不会对客户端有任何影响。我们使用MySQL 命令行工具作为客户端,在测试环境里充当前端应用。

本文所描述的很多特性在开源的 NGINX 和 NGINX Plus 里都有提供。不过,为了简单起见,我们通篇使用 NGINX Plus,有些在 NGINX 里没有的特性我们会明确指明。

我们将探讨如下几个应用场景。

TCP 负载均衡

高可用和健康检查

日志和诊断

并发写入

TCP 负载均衡

在为应用程序配置负载均衡之前,需要了解应用程序是如何连接到数据库的。我们使用 MySQL 命令行工具 mysql 连接到 Galera 集群,运行查询,然后关闭连接。不过,在实际当中,很多应用框架使用连接池来减小延迟,有效利用数据库的资源。

TCP 的负载均衡是在 stream context 里配置的,所以我们在 nginx.conf 文件里增加了一个 stream 配置块来配置我们的 MySQL 负载均衡。

stream { include stream.conf; }

我们的 TCP 负载均衡配置与主配置文件是分开的。我们在相同的目录创建 stream.conf 文件。要注意,在默认情况下,conf.d 目录被保留用于 http context 配置,如果在这里添加 stream 配置是不会生效的。

upstream galera_cluster { server 127.0.0.1:33061; # node1 server 127.0.0.1:33062; # node2 server 127.0.0.1:33063; # node3 zone tcp_mem 64k; } server { listen 3306; # MySQL 默认端口 proxy_pass galera_cluster; }

首先,我们定义了一个 upstream 组,名字叫作 galera_cluster,包含了 Galera 集群里的三个 MySQL 节点。在我们的测试环境里,可以分别通过本地的不同端口访问它们。

zone 指令指定了一些内存,NGINX Plus 的工作线程用它来维护负载均衡的状态。server{}配置块定义了 NGINX Plus 是如何处理客户端的。NGINX Plus 监听 MySQL 的默认端口 3306,并将流量转向到 Galera 集群。

为了验证配置的正确性,我们可以使用 MySQL 客户端来获取它所连接的 MySQL 节点的机器名。

$ 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

这足以说明轮询负载均衡算法是正常的。不过,如果我们的应用程序使用连接池来访问数据库,那么就有可能导致每个节点的连接数不均衡。另外,我们无法保证每一个连接的负载是均等的,因为处理查询的连接有可能很空闲也有可能很忙。另一种负载均衡算法叫作最少连接数(Least Connections),可以使用 least_conn 指令来配置。

upstream galera_cluster { server 127.0.0.1:33061; # node1 server 127.0.0.1:33062; # node2 server 127.0.0.1:33063; # node3 zone tcp_mem 64k; least_conn; }

现在,如果有客户端连接到数据库,NGINX Plus 会选择集群里具有最少连接数的节点。

高可用和健康检查

在集群里进行负载均衡的最大好处是它可以提供高可用性。基于上述的配置,如果一个新的 TCP 连接建立失败,NGINX Plus 就把这台服务器标记为“down”,并停止向它发送 TCP 数据包。

除了能够探测到宕机的服务器,NGINX Plus 还能自动进行自发的健康检查。因此,在客户端发送请求到那些不可用的服务器之前,NGINX Plus 能够提前检测到它们(这个特性只在 NGINX Plus 里提供)。另外,我们可以通过应用程序级别的健康检测来测试服务器的可用性。我们向每一台服务器发送请求,如果服务器返回响应,说明它运行正常。我们在配置里添加了一些内容。

upstream galera_cluster { server 127.0.0.1:33061; # node1 server 127.0.0.1:33062; # node2 server 127.0.0.1:33063; # node3 zone tcp_mem 64k; least_conn; } match mysql_handshake { send \x00; expect ~* \x00\x00; # 用于过滤握手响应数据包中的空值 } server { listen 3306; # MySQL 默认端口 proxy_pass galera_cluster; proxy_timeout 2s; health_check match=mysql_handshake interval=20 fails=1 passes=2; }

在这个例子里,match 配置块定义了初始化一个 MySQL 握手协议需要的请求和响应数据。server 配置块里的 health_check 指令使用了由 match 配置块定义的模式,并确保 NGINX Plus 只会向可用的服务器发起 MySQL 连接。我们每 20 秒执行一次健康检查,如果连接服务器失败一次,就把这个服务器从 TCP 负载均衡池里移除,如果连续两次健康检查成功,那么就重新把服务器放回负载均衡池。

日志和诊断

NGINX Plus 提供了灵活的日志,所有的 TCP 和 UDP 处理过程都可以被记录下来,用于调试和离线分析。对于使用了 TCP 协议的系统,比如 MySQL,NGINX Plus 会在每次连接关闭之后记录一条日志。log_format 指令指定哪些值可以出现在日志里。我们可以选择出现在 Stream 模块里的任意可用变量值。我们在 stream.conf 文件最上面的 stream context 里定义日志格式。

log_format mysql '$remote_addr [$time_local] $protocol $status $bytes_received ' '$bytes_sent $upstream_addr $upstream_connect_time ' '$upstream_first_byte_time $upstream_session_time $session_time';

在 server 配置块里使用 access_log 指令来启用日志,并指定日志文件的路径和之前配置过的日志格式的名字。

server { ... access_log /var/log/nginx/galera_access.log mysql; }

这样的配置将生成如下格式的日志。

$ tail -3 /var/log/nginx/galera_access.log 192.168.91.1 [16/Nov/2016: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 [16/Nov/2016: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 [16/Nov/2016:17:42:19 +0100] TCP 200 369 1611 127.0.0.1:33062 0.001 0.001 10.460 10.460

通过 nginScript 使用高级日志

nginScript 是 NGINX 的"原生"可编程配置语言。它是为 NGINX 和 NGINX Plus 专门实现的 JavaScript,也是专门为服务器端的使用场景而设计的。

在 Stream 模块里,可以通过 nginScript 访问请求和响应消息里的数据包。也就是说,我们可以查看从客户端发出的 SQL 查询请求,并从中抽取有用的元素,比如 SQL 的 SELECT 或 UPDATE 方法。nginScript 可以把这些值变成普通的 NGINX 变量。在这个例子里,我们的 JavaScript 代码被放在 /etc/nginx/sql_method.js 文件里。

var method = "-"; // 全局变量 var client_messages = 0; function getSqlMethod(s) { if ( !s.fromUpstream ) { client_messages++; if ( client_messages == 3 ) { // SQL 语句出现在第 3 个数据包里 var query_text = s.buffer.substr(1,10).toUpperCase(); var methods = ["SELECT", "UPDATE", "INSERT", "SHOW", "CREATE", "DROP"]; var i = 0; for (; i 0 ) { s.log("SQL method: " + methods[i]); // 记录错误日志 method = methods[i]; return s.OK; // 停止查找 } } } } return s.OK; } function setSqlMethod() { return method; }

getSqlMethod() 函数接收一个表示当前数据包的 JavaScript 对象。

这个对象的属性 fromUpstream 和 buffer 为我们提供了数据包和上下文的信息。

我们先检查 TCP 数据包是否来自客户端,因为我们不需要处理来自上游 MySQL 服务器的数据包。我们需要第三个数据包,因为第一个是握手信息,第二个是认证信息。第三个数据包包含了 SQL 查询字符串。我们将这个字符串的开头部分与数组里定义的 SQL 方法列表进行比较,如果找到一个匹配的字符串,就把它保存到全局变量 $method 里,并往错误日志里写入一条日志。因为 nginScript 日志是以“info”级别写到错误日志里的,所以默认情况下不会显示出来。

在计算同名的 NGINX 变量时,setSqlMethod() 函数会被调用。在这个时候,从 getSqlMethod() 函数里获得的全局变量 $method 将被用于计算新变量。

要注意,这段 nginScript 代码可以用于处理 MySQL 命令行客户端发出的简单查询,但不能用于处理复杂的查询或多次查询,尽管可以通过修改代码来处理它们。

我们将 $sql_method 变量包含在 log_format 指令里,这样 SQL 的方法就能够被记录到日志里。

log_format mysql '$remote_addr [$time_local] $protocol $status $bytes_received ' '$bytes_sent $upstream_addr $upstream_connect_time ' '$upstream_first_byte_time $upstream_session_time $session_time '

我们还要告诉 NGINX Plus 如何以及何时执行 nginScript 代码。

js_include /etc/nginx/sql_method.js; js_set $sql_method setSqlMethod; server { ... js_filter getSqlMethod; error_log /var/log/nginx/galera_error.log info; # 用于在 nginScript 代码里调用记录日志的方法 access_log /var/log/nginx/galera_access.log mysql; }

首先,我们通过 js_include 指令指定了 nginScript 代码文件的位置,并使用 js_set 指令告诉 NGINX Plus 在计算 $sql_method 变量时调用 setSqlMethod() 函数。

然后,我们在 server 配置块里使用 js_filter 指令指定了每次处理完一个数据包之后要调用的函数。另外,我们还可以增加 error_log 指令来启用 nginScript 日志。

经过这些配置,我们的访问日志看起来是这样的。

$ tail -3 /var/log/nginx/galera_access.log 192.168.91.1 [16/Nov/2016: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 [16/Nov/2016: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 [16/Nov/2016:17:42:19 +0100] TCP 200 369 1611 127.0.0.1:33062 0.001 0.001 10.460 10.460 UPDATE

NGINX Plus 仪表盘

除了可以记录 MySQL 活动的详细信息,我们还可以在 NGINX Plus 的实时活动监控仪表盘上实时地观察上游 MySQL 服务器的度量指标和健康情况(开源版本的 NGINX 只提供了少数的几个度量指标,而且只能通过 API 获得)。NGINX Plus 的仪表盘是在 R7 版本里引入的,并为 JSON Status API 提供了一个 Web 界面。我们在 /etc/nginx/conf.d/dashboard.conf 文件里添加 server 配置块来启用这个功能.

server { listen 8080; location /status { status; } # 启用 JSON Status API location = /status.html { root /usr/share/nginx/html; } #deny all; # 在生产环境里保护远程地址 #allow 192.168.0.0/16; # 只允许私有网络访问 }

我们还要更新 stream.conf 里的 server 配置块,使用 status_zone 指令来启用对从 MySQL 服务收集来的数据进行监控。

server { ... status_zone galera_cluster; }

这样配置之后,NGINX Plus 的仪表盘就可以使用了,端口为 8080。从屏幕截图可以看到我们的三个 MySQL 服务器,每个服务器有很多连接,还有每个服务器的健康情况。我们可以看到,监听 33062 端口的节点之前发生了 18.97 秒的宕机(DT 一列)。

NGINX Plus 的实时活动监控仪表盘跟踪负载均衡池里 MySQL 服务器的健康情况

并发写入

Galera 集群将每个 MySQL 节点看成一个主数据库,执行读取和写入操作。大部分应用程序来的读写比率比较高,集群多主数据库为我们带来了很大的灵活性,所以同一个表被多个客户端更新的风险是完全可接受的。不过,如果发生并发写入的风险很高,我们有两个解决方案。

创建两个单独的上游组,一个用于读,一个用于写,每个组监听不同的端口。使用一个或多个节点进行写操作,其他节点则放在读分组里。更新客户端,选择合适的端口进行读写操作。

只使用一个上游组,改写客户端代码,让它们检测写操作错误。在检测到写操作错误时,客户端暂停一段时间,等待并发结束后再进行尝试。

总结

在这篇文章里,我们探讨了对 TCP 应用(比如 MySQL)进行负载均衡需要考虑到的几个问题。NGINX Plus 提供了全功能的 TCP/UDP 负载均衡器,用于交付高性能、可靠、安全和可伸缩的应用。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20190221A0UO9G00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励