[MINOR] add better support to "mysql-check"

The MySQL check has been revamped to be able to send real MySQL data,
and to avoid Aborted connects on MySQL side.
It is however backward compatible with older version, but it is highly
recommended to use the new mode, by adding "user <username>" on the
"mysql-check" line.

The new check consists in sending two MySQL packet, one Client
Authentication packet, with "haproxy" username (by default), and one
QUIT packet, to correctly close MySQL session. We then parse the Mysql
Handshake Initialisation packet and/or Error packet. It is a basic but
useful test which does not produce error nor aborted connect on the
server.
(cherry picked from commit a1e4dcfe5718311b7653d7dabfad65c005d0439b)
This commit is contained in:
Herv COMMOWICK 2010-10-18 15:58:36 +02:00 committed by Willy Tarreau
parent aa2f389cbb
commit 8776f1b3a0
4 changed files with 170 additions and 30 deletions

View file

@ -3350,17 +3350,39 @@ no option logasap
logging.
option mysql-check
Use Mysql health checks for server testing
option mysql-check [ user <username> ]
Use MySQL health checks for server testing
May be used in sections : defaults | frontend | listen | backend
yes | no | yes | yes
Arguments : none
Arguments :
user <username> This is the username which will be used when connecting
to MySQL server.
The check consists in parsing Mysql Handshake Initialisation packet or Error
packet, which is sent by MySQL server on connect. It is a basic but useful
test which does not produce any logging on the server. However, it does not
check database presence nor database consistency, nor user permission to
access. To do this, you can use an external check with xinetd for example.
If you specify a username, the check consists of sending two MySQL packet,
one Client Authentication packet, and one QUIT packet, to correctly close
MySQL session. We then parse the MySQL Handshake Initialisation packet and/or
Error packet. It is a basic but useful test which does not produce error nor
aborted connect on the server. However, it requires adding an authorization
in the MySQL table, like this :
USE mysql;
INSERT INTO user (Host,User) values ('<ip_of_haproxy>','<username>');
FLUSH PRIVILEGES;
If you don't specify a username (it is deprecated and not recommended), the
check only consists in parsing the Mysql Handshake Initialisation packet or
Error packet, we don't send anything in this mode. It was reported that it
can generate lockout if check is too frequent and/or if there is not enough
traffic. In fact, you need in this case to check MySQL "max_connect_errors"
value as if a connection is established successfully within fewer than MySQL
"max_connect_errors" attempts after a previous connection was interrupted,
the error count for the host is cleared to zero. If HAProxy's server get
blocked, the "FLUSH HOSTS" statement is the only way to unblock it.
Remember that this does not check database presence nor database consistency.
To do this, you can use an external check with xinetd for example.
The check requires MySQL >=4.0, for older version, please use TCP check.
Most often, an incoming MySQL server needs to see the client's IP address for
various purposes, including IP privilege matching and connection logging.

View file

@ -2904,6 +2904,9 @@ stats_error_parsing:
}
else if (!strcmp(args[1], "mysql-check")) {
/* use MYSQL request to check servers' health */
if (warnifnotcap(curproxy, PR_CAP_BE, file, linenum, args[1], NULL))
err_code |= ERR_WARN;
free(curproxy->check_req);
curproxy->check_req = NULL;
curproxy->options &= ~PR_O_HTTP_CHK;
@ -2911,6 +2914,65 @@ stats_error_parsing:
curproxy->options2 &= ~PR_O2_SSL3_CHK;
curproxy->options2 &= ~PR_O2_LDAP_CHK;
curproxy->options2 |= PR_O2_MYSQL_CHK;
/* This is an exemple of an MySQL >=4.0 client Authentication packet kindly provided by Cyril Bonte.
* const char mysql40_client_auth_pkt[] = {
* "\x0e\x00\x00" // packet length
* "\x01" // packet number
* "\x00\x00" // client capabilities
* "\x00\x00\x01" // max packet
* "haproxy\x00" // username (null terminated string)
* "\x00" // filler (always 0x00)
* "\x01\x00\x00" // packet length
* "\x00" // packet number
* "\x01" // COM_QUIT command
* };
*/
if (*(args[2])) {
int cur_arg = 2;
while (*(args[cur_arg])) {
if (strcmp(args[cur_arg], "user") == 0) {
char *mysqluser;
int packetlen, reqlen, userlen;
/* suboption header - needs additional argument for it */
if (*(args[cur_arg+1]) == 0) {
Alert("parsing [%s:%d] : '%s %s %s' expects <username> as argument.\n",
file, linenum, args[0], args[1], args[cur_arg]);
err_code |= ERR_ALERT | ERR_FATAL;
goto out;
}
mysqluser = args[cur_arg + 1];
userlen = strlen(mysqluser);
packetlen = userlen + 7;
reqlen = packetlen + 9;
free(curproxy->check_req);
curproxy->check_req = (char *)calloc(1, reqlen);
curproxy->check_len = reqlen;
snprintf(curproxy->check_req, 4, "%c%c%c",
((unsigned char) packetlen & 0xff),
((unsigned char) (packetlen >> 8) & 0xff),
((unsigned char) (packetlen >> 16) & 0xff));
curproxy->check_req[3] = 1;
curproxy->check_req[8] = 1;
memcpy(&curproxy->check_req[9], mysqluser, userlen);
curproxy->check_req[9 + userlen + 1 + 1] = 1;
curproxy->check_req[9 + userlen + 1 + 1 + 4] = 1;
cur_arg += 2;
} else {
/* unknown suboption - catchall */
Alert("parsing [%s:%d] : '%s %s' only supports optional values: 'user'.\n",
file, linenum, args[0], args[1]);
err_code |= ERR_ALERT | ERR_FATAL;
goto out;
}
} /* end while loop */
}
}
else if (!strcmp(args[1], "ldap-check")) {
/* use LDAP request to check servers' health */

View file

@ -848,8 +848,9 @@ static int event_srv_chk_w(int fd)
/*
* This function is used only for server health-checks. It handles the server's
* reply to an HTTP request or SSL HELLO. It calls set_server_check_status() to
* update s->check_status, s->check_duration and s->result.
* reply to an HTTP request, SSL HELLO or MySQL client Auth. It calls
* set_server_check_status() to update s->check_status, s->check_duration
* and s->result.
* The set_server_check_status function is called with HCHK_STATUS_L7OKD if
* an HTTP server replies HTTP 2xx or 3xx (valid responses), if an SMTP server
@ -1001,36 +1002,91 @@ static int event_srv_chk_r(int fd)
set_server_check_status(s, HCHK_STATUS_L7STS, desc);
}
else if (s->proxy->options2 & PR_O2_MYSQL_CHK) {
/* MySQL Error packet always begin with field_count = 0xff
* contrary to OK Packet who always begin whith 0x00 */
if (!done && s->check_data_len < 5)
goto wait_more_data;
if (*(s->check_data + 4) != '\xff') {
/* We set the MySQL Version in description for information purpose
* FIXME : it can be cool to use MySQL Version for other purpose,
* like mark as down old MySQL server.
*/
if (s->check_data_len > 51) {
desc = ltrim(s->check_data + 5, ' ');
set_server_check_status(s, HCHK_STATUS_L7OKD, desc);
if (s->proxy->check_len == 0) { // old mode
if (*(s->check_data + 4) != '\xff') {
/* We set the MySQL Version in description for information purpose
* FIXME : it can be cool to use MySQL Version for other purpose,
* like mark as down old MySQL server.
*/
if (s->check_data_len > 51) {
desc = ltrim(s->check_data + 5, ' ');
set_server_check_status(s, HCHK_STATUS_L7OKD, desc);
}
else {
if (!done)
goto wait_more_data;
/* it seems we have a OK packet but without a valid length,
* it must be a protocol error
*/
set_server_check_status(s, HCHK_STATUS_L7RSP, s->check_data);
}
}
else {
/* An error message is attached in the Error packet */
desc = ltrim(s->check_data + 7, ' ');
set_server_check_status(s, HCHK_STATUS_L7STS, desc);
}
} else {
unsigned int first_packet_len = ((unsigned int) *s->check_data) +
(((unsigned int) *(s->check_data + 1)) << 8) +
(((unsigned int) *(s->check_data + 2)) << 16);
if (s->check_data_len == first_packet_len + 4) {
/* MySQL Error packet always begin with field_count = 0xff */
if (*(s->check_data + 4) != '\xff') {
/* We have only one MySQL packet and it is a Handshake Initialization packet
* but we need to have a second packet to know if it is alright
*/
if (!done && s->check_data_len < first_packet_len + 5)
goto wait_more_data;
}
else {
/* We have only one packet and it is an Error packet,
* an error message is attached, so we can display it
*/
desc = &s->check_data[7];
//Warning("onlyoneERR: %s\n", desc);
set_server_check_status(s, HCHK_STATUS_L7STS, desc);
}
} else if (s->check_data_len > first_packet_len + 4) {
unsigned int second_packet_len = ((unsigned int) *(s->check_data + first_packet_len + 4)) +
(((unsigned int) *(s->check_data + first_packet_len + 5)) << 8) +
(((unsigned int) *(s->check_data + first_packet_len + 6)) << 16);
if (s->check_data_len == first_packet_len + 4 + second_packet_len + 4 ) {
/* We have 2 packets and that's good */
/* Check if the second packet is a MySQL Error packet or not */
if (*(s->check_data + first_packet_len + 8) != '\xff') {
/* No error packet */
/* We set the MySQL Version in description for information purpose */
desc = &s->check_data[5];
//Warning("2packetOK: %s\n", desc);
set_server_check_status(s, HCHK_STATUS_L7OKD, desc);
}
else {
/* An error message is attached in the Error packet
* so we can display it ! :)
*/
desc = &s->check_data[first_packet_len+11];
//Warning("2packetERR: %s\n", desc);
set_server_check_status(s, HCHK_STATUS_L7STS, desc);
}
}
}
else {
if (!done)
goto wait_more_data;
/* it seems we have a OK packet but without a valid length,
/* it seems we have a Handshake Initialization packet but without a valid length,
* it must be a protocol error
*/
set_server_check_status(s, HCHK_STATUS_L7RSP, s->check_data);
desc = &s->check_data[5];
//Warning("protoerr: %s\n", desc);
set_server_check_status(s, HCHK_STATUS_L7RSP, desc);
}
}
else {
/* An error message is attached in the Error packet,
* so we can display it ! :)
*/
desc = ltrim(s->check_data + 7, ' ');
set_server_check_status(s, HCHK_STATUS_L7STS, desc);
}
}
else if (s->proxy->options2 & PR_O2_LDAP_CHK) {
if (!done && s->check_data_len < 14)

View file

@ -21,7 +21,7 @@ listen mysql_1
bind :3307
mode tcp
balance roundrobin
option mysql-check
option mysql-check user haproxy
server srv1 127.0.0.1:3306 check port 3306 inter 1000 fall 1
# server srv2 127.0.0.2:3306 check port 3306 inter 1000 fall 1
# server srv3 127.0.0.3:3306 check port 3306 inter 1000 fall 1