MySQLにおけるlocalhostと127.0.0.1

先日、会社のインターンの人にこんなことを聞かれた。

「localhostと127.0.0.1ってどう違うんですか?」

/etc/hosts に書いてあるとか、そういう月並みな説明はできるけど、よく考えたらうまく説明できない。 どのようなしくみで動いているのかなどをきちんと理解しているわけではないことがわかってしまったのだ。

この記事では libcgethostbyname などの UNIX の話というよりは、Web開発で MySQLDATABASE_HOSTlocalhost127.0.0.1 に変えたら動いた! について少し深掘りして考えてみる。

libc/etc/nsswitch.conf に関しては、別途調べて別記事にまとめて書く。


MySQL に接続する方法は2つある。

  • unix domain socket
  • tcp

unix domain socket は同一マシン内でのプロセス間通信のことだ(今はこれくらいの理解)

tcp でもつなぐことができるのだが、 unix domain socket のほうがはるかに高速なので基本的にはこちら使うのが一般的らしい。

参考記事はこちら。


今回は以前あった、「laravelからMySQLにつながらない」という問題を再考した。

まずphp自体は名前解決どうしているんだろうか、と思い php/php-srcの対象のコードを読んだ。

https://github.com/php/php-src/blob/master/main/network.c#L1307-L1309

PHPAPI struct hostent*	php_network_gethostbyname(char *name) {
#if !defined(HAVE_GETHOSTBYNAME_R)
	return gethostbyname(name);
#else
	if (FG(tmp_host_buf)) {
		free(FG(tmp_host_buf));
	}

	FG(tmp_host_buf) = NULL;
	FG(tmp_host_buf_len) = 0;

	memset(&FG(tmp_host_info), 0, sizeof(struct hostent));

	return gethostname_re(name, &FG(tmp_host_info), &FG(tmp_host_buf), &FG(tmp_host_buf_len));
#endif
}

gethostbyname ちゃんと使っている….無問題。なんでだろう。

そしたら詳しいおっさんが、「MySQLとかはlocalhostで指定するとunix domain socketを優先しようとする」などと教えてくれたので、 mysql-server を読みに行った。

repoは mysql/mysql-serverだ。

対象のコードは以下。 "localhost" という文字列をgrepすると見つかった。

https://github.com/mysql/mysql-server/blob/8.0/sql-common/client.cc#L4275-L4322

#if defined(HAVE_SYS_UN_H)
  if (!net->vio &&
      (!mysql->options.protocol ||
       mysql->options.protocol == MYSQL_PROTOCOL_SOCKET) &&
      (unix_socket || mysql_unix_port) &&
      (!host || !strcmp(host, LOCAL_HOST))) {
    my_socket sock = socket(AF_UNIX, SOCK_STREAM, 0);
    DBUG_PRINT("info", ("Using socket"));
    if (sock == SOCKET_ERROR) {
      set_mysql_extended_error(mysql, CR_SOCKET_CREATE_ERROR, unknown_sqlstate,
			       ER_CLIENT(CR_SOCKET_CREATE_ERROR), socket_errno);
      goto error;
    }

    net->vio =
	vio_new(sock, VIO_TYPE_SOCKET, VIO_LOCALHOST | VIO_BUFFERED_READ);
    if (!net->vio) {
      DBUG_PRINT("error", ("Unknow protocol %d ", mysql->options.protocol));
      set_mysql_error(mysql, CR_CONN_UNKNOW_PROTOCOL, unknown_sqlstate);
      closesocket(sock);
      goto error;
    }

    host = LOCAL_HOST;
    if (!unix_socket) unix_socket = mysql_unix_port;
    host_info = (char *)ER_CLIENT(CR_LOCALHOST_CONNECTION);
    DBUG_PRINT("info", ("Using UNIX sock '%s'", unix_socket));

    memset(&UNIXaddr, 0, sizeof(UNIXaddr));
    UNIXaddr.sun_family = AF_UNIX;
    strmake(UNIXaddr.sun_path, unix_socket, sizeof(UNIXaddr.sun_path) - 1);

    if (mysql->options.extension && mysql->options.extension->retry_count)
      my_net_set_retry_count(net, mysql->options.extension->retry_count);

    if (vio_socket_connect(net->vio, (struct sockaddr *)&UNIXaddr,
			   sizeof(UNIXaddr), get_vio_connect_timeout(mysql))) {
      DBUG_PRINT("error",
		 ("Got error %d on connect to local server", socket_errno));
      set_mysql_extended_error(mysql, CR_CONNECTION_ERROR, unknown_sqlstate,
			       ER_CLIENT(CR_CONNECTION_ERROR), unix_socket,
			       socket_errno);
      vio_delete(net->vio);
      net->vio = 0;
      goto error;
    }
    mysql->options.protocol = MYSQL_PROTOCOL_SOCKET;
  }
#elif defined(_WIN32)

以下の部分が該当箇所だ。パフォーマンスのためか、 LOCAL_HOST の場合は socket でつないでるっぽい(if文が分かりづらい….)

if (!net->vio
    &&
    (!mysql->options.protocol || mysql->options.protocol == MYSQL_PROTOCOL_SOCKET)
    &&
    (unix_socket || mysql_unix_port)
    &&
    (!host || !strcmp(host, LOCAL_HOST))) { my_socket sock = socket(AF_UNIX, SOCK_STREAM, 0); /* ... */}

試しに、 /etc/hosts で以下のようにしたらどうなるだろうか。

MySQLにつないでみる。

~ (*´ω`*) < mysql -u root --host="hoge" -P 3306
Welcome to the MySQL monitor.  Commands end with ; or \g.
[省略]

netstatの情報はこんな感じだ。

~ (*´ω`*) < netstat -an | grep 3306
tcp4       0      0  127.0.0.1.3306         127.0.0.1.50816        ESTABLISHED
tcp4       0      0  127.0.0.1.50816        127.0.0.1.3306         ESTABLISHED
tcp4       0      0  127.0.0.1.3306         *.*                    LISTEN
tcp4       0      0  127.0.0.1.50765        127.0.0.1.3306         TIME_WAIT

localhostでつなぐとこんな感じ。

~ 。+゚(∩´﹏`)゚+。 < mysql -u root -h localhost
Welcome to the MySQL monitor.  Commands end with ; or \g.
[省略]

netstatの情報はこんな感じだ。

~ (*´ω`*) < netstat -an | grep 3306
tcp4       0      0  127.0.0.1.3306         *.*                    LISTEN
tcp4       0      0  127.0.0.1.50816        127.0.0.1.3306         TIME_WAIT

tcpで動いていないことが確認できた。


正しく説明するにはC言語読む必要が出てくるし、詳しいおっさんが近くにいないと厳しい。

先駆者はいたけどまぁ書いてもよいよね。

MySQLでlocalhostと127.0.0.1の違い