Skip to content

Commit 8776f1b

Browse files
Herv� COMMOWICKwtarreau
Herv� COMMOWICK
authored andcommitted
[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)
1 parent aa2f389 commit 8776f1b

File tree

4 files changed

+171
-31
lines changed

4 files changed

+171
-31
lines changed

doc/configuration.txt

+31-9
Original file line numberDiff line numberDiff line change
@@ -3350,17 +3350,39 @@ no option logasap
33503350
logging.
33513351

33523352

3353-
option mysql-check
3354-
Use Mysql health checks for server testing
3353+
option mysql-check [ user <username> ]
3354+
Use MySQL health checks for server testing
33553355
May be used in sections : defaults | frontend | listen | backend
33563356
yes | no | yes | yes
3357-
Arguments : none
3358-
3359-
The check consists in parsing Mysql Handshake Initialisation packet or Error
3360-
packet, which is sent by MySQL server on connect. It is a basic but useful
3361-
test which does not produce any logging on the server. However, it does not
3362-
check database presence nor database consistency, nor user permission to
3363-
access. To do this, you can use an external check with xinetd for example.
3357+
Arguments :
3358+
user <username> This is the username which will be used when connecting
3359+
to MySQL server.
3360+
3361+
If you specify a username, the check consists of sending two MySQL packet,
3362+
one Client Authentication packet, and one QUIT packet, to correctly close
3363+
MySQL session. We then parse the MySQL Handshake Initialisation packet and/or
3364+
Error packet. It is a basic but useful test which does not produce error nor
3365+
aborted connect on the server. However, it requires adding an authorization
3366+
in the MySQL table, like this :
3367+
3368+
USE mysql;
3369+
INSERT INTO user (Host,User) values ('<ip_of_haproxy>','<username>');
3370+
FLUSH PRIVILEGES;
3371+
3372+
If you don't specify a username (it is deprecated and not recommended), the
3373+
check only consists in parsing the Mysql Handshake Initialisation packet or
3374+
Error packet, we don't send anything in this mode. It was reported that it
3375+
can generate lockout if check is too frequent and/or if there is not enough
3376+
traffic. In fact, you need in this case to check MySQL "max_connect_errors"
3377+
value as if a connection is established successfully within fewer than MySQL
3378+
"max_connect_errors" attempts after a previous connection was interrupted,
3379+
the error count for the host is cleared to zero. If HAProxy's server get
3380+
blocked, the "FLUSH HOSTS" statement is the only way to unblock it.
3381+
3382+
Remember that this does not check database presence nor database consistency.
3383+
To do this, you can use an external check with xinetd for example.
3384+
3385+
The check requires MySQL >=4.0, for older version, please use TCP check.
33643386

33653387
Most often, an incoming MySQL server needs to see the client's IP address for
33663388
various purposes, including IP privilege matching and connection logging.

src/cfgparse.c

+62
Original file line numberDiff line numberDiff line change
@@ -2904,13 +2904,75 @@ int cfg_parse_listen(const char *file, int linenum, char **args, int kwm)
29042904
}
29052905
else if (!strcmp(args[1], "mysql-check")) {
29062906
/* use MYSQL request to check servers' health */
2907+
if (warnifnotcap(curproxy, PR_CAP_BE, file, linenum, args[1], NULL))
2908+
err_code |= ERR_WARN;
2909+
29072910
free(curproxy->check_req);
29082911
curproxy->check_req = NULL;
29092912
curproxy->options &= ~PR_O_HTTP_CHK;
29102913
curproxy->options &= ~PR_O_SMTP_CHK;
29112914
curproxy->options2 &= ~PR_O2_SSL3_CHK;
29122915
curproxy->options2 &= ~PR_O2_LDAP_CHK;
29132916
curproxy->options2 |= PR_O2_MYSQL_CHK;
2917+
2918+
/* This is an exemple of an MySQL >=4.0 client Authentication packet kindly provided by Cyril Bonte.
2919+
* const char mysql40_client_auth_pkt[] = {
2920+
* "\x0e\x00\x00" // packet length
2921+
* "\x01" // packet number
2922+
* "\x00\x00" // client capabilities
2923+
* "\x00\x00\x01" // max packet
2924+
* "haproxy\x00" // username (null terminated string)
2925+
* "\x00" // filler (always 0x00)
2926+
* "\x01\x00\x00" // packet length
2927+
* "\x00" // packet number
2928+
* "\x01" // COM_QUIT command
2929+
* };
2930+
*/
2931+
2932+
if (*(args[2])) {
2933+
int cur_arg = 2;
2934+
2935+
while (*(args[cur_arg])) {
2936+
if (strcmp(args[cur_arg], "user") == 0) {
2937+
char *mysqluser;
2938+
int packetlen, reqlen, userlen;
2939+
2940+
/* suboption header - needs additional argument for it */
2941+
if (*(args[cur_arg+1]) == 0) {
2942+
Alert("parsing [%s:%d] : '%s %s %s' expects <username> as argument.\n",
2943+
file, linenum, args[0], args[1], args[cur_arg]);
2944+
err_code |= ERR_ALERT | ERR_FATAL;
2945+
goto out;
2946+
}
2947+
mysqluser = args[cur_arg + 1];
2948+
userlen = strlen(mysqluser);
2949+
packetlen = userlen + 7;
2950+
reqlen = packetlen + 9;
2951+
2952+
free(curproxy->check_req);
2953+
curproxy->check_req = (char *)calloc(1, reqlen);
2954+
curproxy->check_len = reqlen;
2955+
2956+
snprintf(curproxy->check_req, 4, "%c%c%c",
2957+
((unsigned char) packetlen & 0xff),
2958+
((unsigned char) (packetlen >> 8) & 0xff),
2959+
((unsigned char) (packetlen >> 16) & 0xff));
2960+
2961+
curproxy->check_req[3] = 1;
2962+
curproxy->check_req[8] = 1;
2963+
memcpy(&curproxy->check_req[9], mysqluser, userlen);
2964+
curproxy->check_req[9 + userlen + 1 + 1] = 1;
2965+
curproxy->check_req[9 + userlen + 1 + 1 + 4] = 1;
2966+
cur_arg += 2;
2967+
} else {
2968+
/* unknown suboption - catchall */
2969+
Alert("parsing [%s:%d] : '%s %s' only supports optional values: 'user'.\n",
2970+
file, linenum, args[0], args[1]);
2971+
err_code |= ERR_ALERT | ERR_FATAL;
2972+
goto out;
2973+
}
2974+
} /* end while loop */
2975+
}
29142976
}
29152977
else if (!strcmp(args[1], "ldap-check")) {
29162978
/* use LDAP request to check servers' health */

src/checks.c

+77-21
Original file line numberDiff line numberDiff line change
@@ -848,8 +848,9 @@ static int event_srv_chk_w(int fd)
848848

849849
/*
850850
* This function is used only for server health-checks. It handles the server's
851-
* reply to an HTTP request or SSL HELLO. It calls set_server_check_status() to
852-
* update s->check_status, s->check_duration and s->result.
851+
* reply to an HTTP request, SSL HELLO or MySQL client Auth. It calls
852+
* set_server_check_status() to update s->check_status, s->check_duration
853+
* and s->result.
853854
854855
* The set_server_check_status function is called with HCHK_STATUS_L7OKD if
855856
* 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)
10011002
set_server_check_status(s, HCHK_STATUS_L7STS, desc);
10021003
}
10031004
else if (s->proxy->options2 & PR_O2_MYSQL_CHK) {
1004-
/* MySQL Error packet always begin with field_count = 0xff
1005-
* contrary to OK Packet who always begin whith 0x00 */
10061005
if (!done && s->check_data_len < 5)
10071006
goto wait_more_data;
10081007

1009-
if (*(s->check_data + 4) != '\xff') {
1010-
/* We set the MySQL Version in description for information purpose
1011-
* FIXME : it can be cool to use MySQL Version for other purpose,
1012-
* like mark as down old MySQL server.
1013-
*/
1014-
if (s->check_data_len > 51) {
1015-
desc = ltrim(s->check_data + 5, ' ');
1016-
set_server_check_status(s, HCHK_STATUS_L7OKD, desc);
1008+
if (s->proxy->check_len == 0) { // old mode
1009+
if (*(s->check_data + 4) != '\xff') {
1010+
/* We set the MySQL Version in description for information purpose
1011+
* FIXME : it can be cool to use MySQL Version for other purpose,
1012+
* like mark as down old MySQL server.
1013+
*/
1014+
if (s->check_data_len > 51) {
1015+
desc = ltrim(s->check_data + 5, ' ');
1016+
set_server_check_status(s, HCHK_STATUS_L7OKD, desc);
1017+
}
1018+
else {
1019+
if (!done)
1020+
goto wait_more_data;
1021+
/* it seems we have a OK packet but without a valid length,
1022+
* it must be a protocol error
1023+
*/
1024+
set_server_check_status(s, HCHK_STATUS_L7RSP, s->check_data);
1025+
}
1026+
}
1027+
else {
1028+
/* An error message is attached in the Error packet */
1029+
desc = ltrim(s->check_data + 7, ' ');
1030+
set_server_check_status(s, HCHK_STATUS_L7STS, desc);
1031+
}
1032+
} else {
1033+
unsigned int first_packet_len = ((unsigned int) *s->check_data) +
1034+
(((unsigned int) *(s->check_data + 1)) << 8) +
1035+
(((unsigned int) *(s->check_data + 2)) << 16);
1036+
1037+
if (s->check_data_len == first_packet_len + 4) {
1038+
/* MySQL Error packet always begin with field_count = 0xff */
1039+
if (*(s->check_data + 4) != '\xff') {
1040+
/* We have only one MySQL packet and it is a Handshake Initialization packet
1041+
* but we need to have a second packet to know if it is alright
1042+
*/
1043+
if (!done && s->check_data_len < first_packet_len + 5)
1044+
goto wait_more_data;
1045+
}
1046+
else {
1047+
/* We have only one packet and it is an Error packet,
1048+
* an error message is attached, so we can display it
1049+
*/
1050+
desc = &s->check_data[7];
1051+
//Warning("onlyoneERR: %s\n", desc);
1052+
set_server_check_status(s, HCHK_STATUS_L7STS, desc);
1053+
}
1054+
} else if (s->check_data_len > first_packet_len + 4) {
1055+
unsigned int second_packet_len = ((unsigned int) *(s->check_data + first_packet_len + 4)) +
1056+
(((unsigned int) *(s->check_data + first_packet_len + 5)) << 8) +
1057+
(((unsigned int) *(s->check_data + first_packet_len + 6)) << 16);
1058+
1059+
if (s->check_data_len == first_packet_len + 4 + second_packet_len + 4 ) {
1060+
/* We have 2 packets and that's good */
1061+
/* Check if the second packet is a MySQL Error packet or not */
1062+
if (*(s->check_data + first_packet_len + 8) != '\xff') {
1063+
/* No error packet */
1064+
/* We set the MySQL Version in description for information purpose */
1065+
desc = &s->check_data[5];
1066+
//Warning("2packetOK: %s\n", desc);
1067+
set_server_check_status(s, HCHK_STATUS_L7OKD, desc);
1068+
}
1069+
else {
1070+
/* An error message is attached in the Error packet
1071+
* so we can display it ! :)
1072+
*/
1073+
desc = &s->check_data[first_packet_len+11];
1074+
//Warning("2packetERR: %s\n", desc);
1075+
set_server_check_status(s, HCHK_STATUS_L7STS, desc);
1076+
}
1077+
}
10171078
}
10181079
else {
10191080
if (!done)
10201081
goto wait_more_data;
1021-
/* it seems we have a OK packet but without a valid length,
1082+
/* it seems we have a Handshake Initialization packet but without a valid length,
10221083
* it must be a protocol error
10231084
*/
1024-
set_server_check_status(s, HCHK_STATUS_L7RSP, s->check_data);
1085+
desc = &s->check_data[5];
1086+
//Warning("protoerr: %s\n", desc);
1087+
set_server_check_status(s, HCHK_STATUS_L7RSP, desc);
10251088
}
10261089
}
1027-
else {
1028-
/* An error message is attached in the Error packet,
1029-
* so we can display it ! :)
1030-
*/
1031-
desc = ltrim(s->check_data + 7, ' ');
1032-
set_server_check_status(s, HCHK_STATUS_L7STS, desc);
1033-
}
10341090
}
10351091
else if (s->proxy->options2 & PR_O2_LDAP_CHK) {
10361092
if (!done && s->check_data_len < 14)

tests/test-sql.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ listen mysql_1
2121
bind :3307
2222
mode tcp
2323
balance roundrobin
24-
option mysql-check
24+
option mysql-check user haproxy
2525
server srv1 127.0.0.1:3306 check port 3306 inter 1000 fall 1
2626
# server srv2 127.0.0.2:3306 check port 3306 inter 1000 fall 1
2727
# server srv3 127.0.0.3:3306 check port 3306 inter 1000 fall 1

0 commit comments

Comments
 (0)