MySQL 8.0.22 DNS SRV レコードサポート

MySQL 8.0.22 の新機能で DNS SRV レコードのサポートというのがあったので試してみた。 https://dev.mysql.com/doc/refman/8.0/en/connecting-using-dns-srv.html

MySQLサーバー3台 (a.example.com, b.example.com, c.example.com)とそれに接続するためのクライアントの計4台を docker-compose で作成する。

Dockerfile

FROM ubuntu
RUN apt update
RUN apt install -y mysql-client libmysqlclient-dev gcc unbound bind9-dnsutils
RUN rm -f /etc/unbound/unbound.conf.d/root-auto-trust-anchor-file.conf
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT /entrypoint.sh

docker-compose.yml

services:
  client:
    build: .
    hostname: client
    volumes:
      - ./resolv.conf:/etc/resolv.conf
      - ./unbound-example.conf:/etc/unbound/unbound.conf.d/example.conf
      - .:/work
    networks:
      test:
        ipv4_address: 192.168.100.100
  a:
    image: mysql:8.0.22
    hostname: a
    networks:
      test:
        ipv4_address: 192.168.100.101
    environment:
      - MYSQL_ALLOW_EMPTY_PASSWORD=1
    entrypoint: bash -c '/entrypoint.sh mysqld & sleep infinity'
  b:
    image: mysql:8.0.22
    hostname: b
    networks:
      test:
        ipv4_address: 192.168.100.102
    environment:
      - MYSQL_ALLOW_EMPTY_PASSWORD=1
    entrypoint: bash -c '/entrypoint.sh mysqld & sleep infinity'
  c:
    image: mysql:8.0.22
    hostname: c
    networks:
      test:
        ipv4_address: 192.168.100.103
    environment:
      - MYSQL_ALLOW_EMPTY_PASSWORD=1
    entrypoint: bash -c '/entrypoint.sh mysqld & sleep infinity'

networks:
  test:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.100.0/24

unbound-example.conf

server:
        interface: 127.0.0.1
        local-zone: "example.com." static
        local-data: "a.example.com. IN A 192.168.100.101"
        local-data: "b.example.com. IN A 192.168.100.102"
        local-data: "c.example.com. IN A 192.168.100.103"
        local-data: "_mysql._tcp.example.com. IN SRV 1 0 3306 a.example.com"
        local-data: "_mysql._tcp.example.com. IN SRV 2 0 3306 b.example.com"
        local-data: "_mysql._tcp.example.com. IN SRV 3 0 3306 c.example.com"

resolv.conf

nameserver 127.0.0.1

entrypoint.sh

#!/bin/bash
unbound
sleep infinity

以上のファイルを同じディレクトリに置いて docker-compose を実行する。

% docker-compose up -d
Creating network "m_test" with driver "bridge"
Creating m_c_1      ... done
Creating m_a_1      ... done
Creating m_client_1 ... done
Creating m_b_1      ... done

% docker-compose exec client bash

現状はこんな感じ。_mysql._tcp.examlpe.com の SRV レコードの値は優先度が高い順に a.example.com, b.example.com, c.example.com

root@client:/# dig _mysql._tcp.example.com srv +short
1 0 3306 a.example.com.
2 0 3306 b.example.com.
3 0 3306 c.example.com.

何回実行しても最優先の a に繋がる。

root@client:/# mysql -s -N --dns-srv-name _mysql._tcp.example.com -e 'select @@hostname'
a
root@client:/# mysql -s -N --dns-srv-name _mysql._tcp.example.com -e 'select @@hostname'
a
root@client:/# mysql -s -N --dns-srv-name _mysql._tcp.example.com -e 'select @@hostname'
a

a の mysqld を落とすと b に繋がる。

root@client:/# mysql -h a.example.com -e shutdown
root@client:/# mysql -s -N --dns-srv-name _mysql._tcp.example.com -e 'select @@hostname'
b

b を落とすと c に繋がり、c も落とすとエラーになる。

root@client:/# mysql -h b.example.com -e shutdown
root@client:/# mysql -s -N --dns-srv-name _mysql._tcp.example.com -e 'select @@hostname'
c
root@client:/# mysql -h c.example.com -e shutdown
root@client:/# mysql -s -N --dns-srv-name _mysql._tcp.example.com -e 'select @@hostname'
ERROR 2003 (HY000): Can't connect to MySQL server on 'c.example.com' (111)

次に優先度は同一にして Weight(分散割合)を変更してみる。

unbound-example.conf を次のように変更する。

server:
        interface: 127.0.0.1
        local-zone: "example.com." static
        local-data: "a.example.com. IN A 192.168.100.101"
        local-data: "b.example.com. IN A 192.168.100.102"
        local-data: "c.example.com. IN A 192.168.100.103"
        local-data: "_mysql._tcp.example.com. IN SRV 1 10 3306 a.example.com"
        local-data: "_mysql._tcp.example.com. IN SRV 1 20 3306 b.example.com"
        local-data: "_mysql._tcp.example.com. IN SRV 1 30 3306 c.example.com"
% docker-compose down
Stopping m_client_1 ... done
Stopping m_a_1      ... done
Stopping m_b_1      ... done
Stopping m_c_1      ... done
Removing m_client_1 ... done
Removing m_a_1      ... done
Removing m_b_1      ... done
Removing m_c_1      ... done
Removing network m_test

% docker-compose up -d
Creating network "m_test" with driver "bridge"
Creating m_client_1 ... done
Creating m_b_1      ... done
Creating m_c_1      ... done
Creating m_a_1      ... done

% docker-compose exec client bash

root@client:/# dig _mysql._tcp.example.com srv +short
1 10 3306 a.example.com.
1 20 3306 b.example.com.
1 30 3306 c.example.com.

何回実行しても c に繋がる。

root@client:/# mysql -s -N --dns-srv-name _mysql._tcp.example.com -e 'select @@hostname'
c
root@client:/# mysql -s -N --dns-srv-name _mysql._tcp.example.com -e 'select @@hostname'
c
root@client:/# mysql -s -N --dns-srv-name _mysql._tcp.example.com -e 'select @@hostname'
c
root@client:/# mysql -s -N --dns-srv-name _mysql._tcp.example.com -e 'select @@hostname'
c
root@client:/# mysql -s -N --dns-srv-name _mysql._tcp.example.com -e 'select @@hostname'
c

SRV レコードのどれに接続するのかはプログラムが制御するんだけど、mysql コマンドは実行する度にプロセスが異なるし、前回何に繋いだかなんて覚えてないので、毎回 Weight が一番高い c が選択されるということなんだろう。

というわけで C API で簡単なプログラムを書いてみた。

#include <stdio.h>
#include <string.h>
#include <mysql/mysql.h>

int main(int argc, char *argv[])
{
  MYSQL my;

  mysql_init(&my);
  for (int i = 0; i < 30; i++) {
    if (mysql_real_connect_dns_srv(&my, "_mysql._tcp.example.com", "root", NULL, NULL, 0) == NULL)
      goto error;
    if (mysql_real_query(&my, "select @@hostname", 17) != 0)
      goto error;
    MYSQL_RES *res;
    if ((res = mysql_store_result(&my)) == NULL)
      goto error;
    MYSQL_ROW row;
    while ((row = mysql_fetch_row(res))) {
      unsigned long *lengths;
      lengths = mysql_fetch_lengths(res);
      printf("%.*s\n", (int)lengths[0], row[0]);
    }
    mysql_close(&my);
  }
  exit(0);

error:
  puts(mysql_error(&my));
  exit(1);
}

同じプロセス内で30回接続している。

root@client:/work# gcc test.c -lmysqlclient
root@client:/work# ./a.out
c
b
c
c
c
b
b
c
b
c
b
c
b
b
c
c
c
c
a
c
a
b
a
c
a
b
a
a
c
b
root@client:/work# ./a.out | sort | uniq -c
      6 a
     10 b
     14 c

なんとなく Weight に応じた割合で接続先が選ばれてる感じになった。


SRV レコードの話。

SRV レコードは MX レコードを汎用化したようなもので、レコード名の先頭はサービスとプロトコル(TCP, UDP)。

RFC6186では次のような例が示されてる。

_submission._tcp SRV 0 1 587 mail.example.com.
_imap._tcp       SRV 0 1 143 imap.example.com.
_imaps._tcp      SRV 0 1 993 imap.example.com.
_pop3._tcp       SRV 0 1 110 pop3.example.com.
_pop3s._tcp      SRV 0 1 995 pop3.example.com.

SRV レコードに対応したメールアプリは example.com というドメイン名さえ知っていれば SMTP/POP/IMAP の接続先を DNS で得ることができる。

というような感じなんで、MySQL の場合はレコード名の先頭は _mysql._tcp 固定なのでわざわざ指定する必要はないはずなんだけど、なんで指定させてるんだろう。mysql_real_connect_dns_srv() 関数の中で _mysql._tcp を先頭につければいいのに…。