BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

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

| 作者 Nginx 关注 0 他的粉丝 ,译者 薛命灯 关注 24 他的粉丝 发布于 2017年6月6日. 估计阅读时间: 24 分钟 | AICon 关注机器学习、计算机视觉、NLP、自动驾驶等20+AI热点技术和最新落地成功案例。

这是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

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

有具体的使用场景吗? by Zhuang Daoyu

举例中的3个mysql实例,简单的负责均衡,似乎没有任何意义?mysql目前只能每个实例单独存自己的数据?应该没有办法做到连任何一个mysql实例,执行动作都可以?

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

1 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT