2020 Google开发者大会重磅开幕 了解详情

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

2017 年 6 月 05 日

这是 nginScript 系列文章的第三篇,将介绍如何使用 nginScript 将客户端循序渐进地重定向到新的服务器。查看第一篇“ nginScript 简介”,第二篇“使用nginScript 将客户端重定向到新服务器”。

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 < methods.length; i++ ) {
if ( query_text.search(methods[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 节点看成一个主数据库,执行读取和写入操作。大部分应用程序来的读写比率比较高,集群多主数据库为我们带来了很大的灵活性,所以同一个表被多个客户端更新的风险是完全可接受的。不过,如果发生并发写入的风险很高,我们有两个解决方案。

  1. 创建两个单独的上游组,一个用于读,一个用于写,每个组监听不同的端口。使用一个或多个节点进行写操作,其他节点则放在读分组里。更新客户端,选择合适的端口进行读写操作。我们的博客上有一篇文章“ Advanced MySQL Load Balancing with NGINX Plus ”详细讨论了这种方法,通过使用多个 MySQL 服务器节点构建高度伸缩的环境。
  2. 只使用一个上游组,改写客户端代码,让它们检测写操作错误。在检测到写操作错误时,客户端暂停一段时间,等待并发结束后再进行尝试。我们的博客上另一个篇文章“ MySQL High Availability with NGINX Plus and Galera Cluster ”详细讨论了这个方法,它使用很小的一个集群,让几个专门的节点负责处理写操作,可以保证很高的可用性。

总结

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

查看英文原文: Scaling MySQL with TCP Load Balancing and Galera Cluster

2017 年 6 月 05 日 17:56 1475
用户头像

发布了 321 篇内容, 共 106.7 次阅读, 收获喜欢 98 次。

关注

评论

发布
暂无评论
发现更多内容

游戏夜读 | 写游戏用什么语言?

game1night

用友2019财报:你们看到的是数字,我却看到了office

人称T客

往日之歌

彭宏豪95

Java 25周年:MovedByJava之观点

范学雷

Java 架构 编程语言

程序员的修行之路-人生是一场修行

牧马人

程序员人生

3亿办公族合力,第三代SaaS抵达战场

人称T客

职场“潜”规则

宋俊毅

个人成长 职场 新人 人才培养 能力模型

Spring Security+Spring Data Jpa 强强联手,安全管理只有更简单!

江南一点雨

Java spring Spring Boot spring security

一个前端工程师与死神的较量

陈辰

前端工程 压力 医院 生活质量 工程师

python实现·十大排序算法之冒泡排序(Bubble Sort)

南风以南

Python 排序算法 冒泡排序

BPM产业数字观察:中国市场趋向成熟,蛰伏的BPM即将醒来

人称T客

Redis 命令执行过程(下)

程序员历小冰

redis 源码分析

SaaS生态比拼,谁会是这场PK中的主角?

人称T客

忙于数字化转型,你避坑了吗?

人称T客

程序员的修行之路-培养工作兴趣

牧马人

程序员人生

5天掌握以太坊 dApp 开发

陈东泽 EuryChen

比特币 区块链 智能合约 以太坊 dapp

Elastic Stack 系列专辑

Yezhiwei

elasticsearch Logstash Kibana ELK Elastic Stack

kotlin 200行代码开发一个简化版Guice

陈吉米

Java kotlin guice ioc mynlp

一个产品最不重要的东西

Neco.W

产品 外包 产品经理

金蝶2019财报在此——比头条更精彩

人称T客

Spring Security 如何将用户数据存入数据库?

江南一点雨

Java spring Spring Cloud Spring Boot spring security

用Serverlss部署一个基于深度学习的古诗词生成API

刘宇

自然语言处理 深度学习 Serverless

一文搞懂Spring依赖注入

麦叔

Java开发架构篇:DDD模型领域层决策规则树服务设计

小傅哥

领域驱动设计 DDD 小傅哥 重构

分布式系统选主怎么玩

奈学教育

分布式系统

教你快速升职加薪(毒鸡汤,慎服……)

无箭的丘比特

团队管理 企业文化 个人成长 团队建设

市场调研分析师走向末法时代

人称T客

kube-prometheus抓取jvm监控指标

天飞

Java JVM Prometheus kubernete

Enhanced Github:一个 GitHub 专用的好插件

非著名程序员

GitHub 程序员 效率工具

回“疫”录(22):我以为结束了,其实才开始

小天同学

疫情 回忆录 现实纪录 纪实

为什么要云原生?

Aaron_涛

架构 云原生

微服务治理平台化探索

微服务治理平台化探索

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