CLOVER🍀

That was when it all began.

HAProxyでTCPロードバランシング

以前にnginxを使ってTCPのロードバランシングを行いましたが、今度はHAProxyを使って
TCPのロードバランシングを行ってみたいと思います。

nginxのTCPロードバランシングを試す - CLOVER

今回も、お題的にはMySQLのロードバランシングをしてみたいと思います。

HAProxyでMySQLのロードバランシング

nginxの時と同様、今回もMySQLサーバーが次のように3台稼働しているものとします。

  • server1 (172.17.0.2)
  • server2 (172.17.0.3)
  • server3 (172.17.0.4)

特にレプリケーションを組んでいるわけではなく、単純にそれぞれ別個で構築します。

で、別々のサーバーにアクセスしていることがわかるように、以下のようにテーブルを作成してデータを登録します。

-- server1
mysql> CREATE TABLE server(name VARCHAR(10), PRIMARY KEY(name));
mysql> INSERT INTO server VALUES('server1');

-- server2
mysql> CREATE TABLE server(name VARCHAR(10), PRIMARY KEY(name));
mysql> INSERT INTO server VALUES('server2');

-- server3
mysql> CREATE TABLE server(name VARCHAR(10), PRIMARY KEY(name));
mysql> INSERT INTO server VALUES('server3');

MySQL側の準備は以上です。

HAProxyでMySQL向けにロードバランスの設定を行う

今回のHAProxyは、Ubuntu Linuxにapt-getでインストールしたものを使用するのですが、デフォルトの設定ファイルは
以下のような状態になっています。
/etc/haproxy/haproxy.cfg

global
	log /dev/log	local0
	log /dev/log	local1 notice
	chroot /var/lib/haproxy
	stats socket /run/haproxy/admin.sock mode 660 level admin
	stats timeout 30s
	user haproxy
	group haproxy
	daemon

	# Default SSL material locations
	ca-base /etc/ssl/certs
	crt-base /etc/ssl/private

	# Default ciphers to use on SSL-enabled listening sockets.
	# For more information, see ciphers(1SSL). This list is from:
	#  https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
	ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS
	ssl-default-bind-options no-sslv3

defaults
	log	global
	mode	http
	option	httplog
	option	dontlognull
        timeout connect 5000
        timeout client  50000
        timeout server  50000
	errorfile 400 /etc/haproxy/errors/400.http
	errorfile 403 /etc/haproxy/errors/403.http
	errorfile 408 /etc/haproxy/errors/408.http
	errorfile 500 /etc/haproxy/errors/500.http
	errorfile 502 /etc/haproxy/errors/502.http
	errorfile 503 /etc/haproxy/errors/503.http
	errorfile 504 /etc/haproxy/errors/504.http

で、MySQLに対する…要はTCPでのバランシングを行う際にはlistenディレクティブを用いるそうなのですが、
ここにいきなりmode tcpのlistenディレクティブを追記すると警告されます。

listen mysql
    bind 0.0.0.0:3306
    mode tcp
    balance roundrobin
    server mysql1 172.17.0.2:3306
    server mysql2 172.17.0.3:3306
    server mysql3 172.17.0.4:3306
$ sudo service haproxy start
 * Starting haproxy haproxy                                                                                                                                                                          [WARNING] 347/175142 (52) : parsing [/etc/haproxy/haproxy.cfg:24] : 'option httplog' not usable with proxy 'mysql' (needs 'mode http'). Falling back to 'option tcplog'.
[WARNING] 347/175142 (53) : parsing [/etc/haproxy/haproxy.cfg:24] : 'option httplog' not usable with proxy 'mysql' (needs 'mode http'). Falling back to 'option tcplog'.

直前のdefautsディレクティブがHTTP向けの設定となっているところが、気に入らないようですね。

defaultsディレクティブが再度登場すると、内容がリセットされるそうなので、ここはdefaultsディレクティブごと
再定義してみました。
Proxies

defaults
	log	global
	mode	http
	option	httplog
	option	dontlognull
## 〜省略〜

defaults
    log global
    mode tcp
    option tcplog
    timeout connect 5000
    timeout client  50000
    timeout server  50000

listen mysql
    bind 0.0.0.0:3306
    mode tcp
    balance roundrobin
    server mysql1 172.17.0.2:3306
    server mysql2 172.17.0.3:3306
    server mysql3 172.17.0.4:3306

これで、HAProxyを再起動すると、今度は警告されません。

設定の意味ですが、bindでバインドするアドレスとポートの指定、modeでTCP向けに指定し、serverでバックエンドのサーバーを指定します。
バランシングはラウンドロビンです。

serverの直後には名前を設定しますが、今回は「mysql1〜3」としました。

動作確認

それでは、動作確認してみます。確認には、次のようなGroovyスクリプトを使用しました。
current-server.groovy

@Grab('mysql:mysql-connector-java:6.0.5')
@GrabConfig(systemClassLoader = true)
import groovy.sql.Sql

Sql.withInstance('jdbc:mysql://localhost:3306/practice?useUnicode=true&characterEncoding=utf-8&characterSetResults=utf-8&useServerPrepStmts=true&useLocalSessionState=true&elideSetAutoCommits=true&alwaysSendSetIsolation=false&useSSL=false', 'kazuhira', 'password') { sql ->
  sql.eachRow('SELECT name FROM server') { row -> println(row.name) }
}

確認。

$ groovy current-server.groovy 
server1
$ groovy current-server.groovy 
server2
$ groovy current-server.groovy 
server3
$ groovy current-server.groovy 
server1
$ groovy current-server.groovy 
server2
$ groovy current-server.groovy 
server3

ラウンドロビンで振り分けられていそうな感じですね?

ところで、今回の設定ではヘルスチェックが入っていないので、背後にいるMySQLサーバーを落とした後で
アクセスされると、エラーになってしまいます。

$ groovy current-server.groovy 
Caught: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
	at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:590)
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:57)
	at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:1606)
	at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:633)
	at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:347)
	at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:219)
	at current-server.run(current-server.groovy:5)
Caused by: com.mysql.cj.core.exceptions.CJCommunicationsException: Communications link failure

これでは困りますね、ヘルスチェックを設定してみましょう。

MySQL向けのヘルスチェックを設定する

MySQL向けのヘルスチェックを設定するには、「mysql-check」を使用するとよいみたいです。

listen mysql
    bind 0.0.0.0:3306
    mode tcp
    option mysql-check user haproxy

    balance roundrobin

    server mysql1 172.17.0.2:3306 check port 3306 inter 3000 fall 3
    server mysql2 172.17.0.3:3306 check port 3306 inter 3000 fall 3
    server mysql3 172.17.0.4:3306 check port 3306 inter 3000 fall 3

mysql-check」では、接続確認に使用するユーザーを指定します。ここでは、「haproxy」としました。

    option mysql-check user haproxy

このユーザーは、MySQL側にも作成しておきます。

mysql> create user haproxy@localhost;
Query OK, 0 rows affected (0.00 sec)

mysql> create user haproxy@'%';
Query OK, 0 rows affected (0.00 sec)

また、次の設定で各サーバーに3秒ごとにヘルスチェックを行い、3回失敗すると切り離されます。

    server mysql1 172.17.0.2:3306 check port 3306 inter 3000 fall 3
    server mysql2 172.17.0.3:3306 check port 3306 inter 3000 fall 3
    server mysql3 172.17.0.4:3306 check port 3306 inter 3000 fall 3

ここで、1台MySQLを停止すると、切り離されます。

$ groovy current-server.groovy 
server2
$ groovy current-server.groovy 
server3
$ groovy current-server.groovy 
server2
$ groovy current-server.groovy 
server3

停止していたサーバーを、復帰させるとアクセスできるようになります。

$ groovy current-server.groovy 
server1
$ groovy current-server.groovy 
server2
$ groovy current-server.groovy 
server3

他のサーバーがダウンしないとアクセスしないサーバーを作り出す

今回、せっかく3台MySQLがあるので、2台をレプリケーションのスレーブに見立てて、
1台をマスター、通常はスレーブにのみアクセスしスレーブがすべて停止するとマスターへ
アクセスするような設定を行ってみます。

変更した結果は、こちら。

listen mysql
    bind 0.0.0.0:3306
    mode tcp
    option mysql-check user haproxy

    balance roundrobin

    server mysql1 172.17.0.2:3306 check port 3306 inter 3000 fall 3 backup
    server mysql2 172.17.0.3:3306 check port 3306 inter 3000 fall 3
    server mysql3 172.17.0.4:3306 check port 3306 inter 3000 fall 3

今回は、「mysql1」をマスターと見立てました。なにが変わったかというと、最後に「backup」の設定が
追加されています。
※他の記事では、最後の行にbackupを置いていることが多いみたいですが

    server mysql1 172.17.0.2:3306 check port 3306 inter 3000 fall 3 backup

この状態でアクセスすると、「mysql1」にはアクセスが振り分けられなくなります。

$ groovy current-server.groovy 
server2
$ groovy current-server.groovy 
server3
$ groovy current-server.groovy 
server2
$ groovy current-server.groovy 
server3

その後、「mysql2」と「mysql3」を落としてアクセスすると、「mysql1」にアクセスが
振り分けられるようになります。

$ groovy current-server.groovy 
server1
$ groovy current-server.groovy 
server1

OKそうですね。