mita2 database life

主にMySQLに関するメモです

MySQL SYSTEM_USER 権限を持ったユーザの操作には SYSTEM_USER 権限 が必要

動的権限と静的権限

「動的権限は、従来の静的権限が細分化されたもの。静的権限を持っていれば、動的権限はREVOKEしても影響はない」と考えていました。 例えば、SUPER 権限 (静的) を持っていれば、BINLOG_ADMIN 権限 (動的) がなくとも、バイナリログをパージできます。

mysql> SHOW GRANTS;
+-------------------------------------------+
| Grants for super@localhost                |
+-------------------------------------------+
| GRANT SUPER ON *.* TO `super`@`localhost` |
+-------------------------------------------+
1 row in set (0.00 sec)

mysql>  PURGE BINARY LOGS TO 'binlog.000008';
Query OK, 0 rows affected (0.01 sec)

しかし、SYSTEM_USER 権限は、静的権限では満たせない仕様がありました。

dev.mysql.com

SYSTEM_USER 権限によるアカウント保護

簡単に言うと、SYSTEM_USER を持つユーザ(システムユーザ)の権限を変更したり、ユーザを削除するには、SYSTEM_USER 権限を持っている必要があります。 SUPER 等の静的権限では、システムユーザを変更できません。

スマートスタイルさんの記事がわかりやすいです。

https://blog.s-style.co.jp/2020/06/6097/

システムユーザを作成する。

mysql> CREATE USER 'system_user'@'localhost';
Query OK, 0 rows affected (0.09 sec)

mysql> GRANT SYSTEM_USER ON *.* TO  'system_user'@'localhost';
Query OK, 0 rows affected, 1 warning (0.04 sec)
-- 自分 (root) から SYSTEM_USER を 剥奪
mysql> REVOKE SYSTEM_USER ON *.* FROM 'root'@'localhost';
Query OK, 0 rows affected, 1 warning (0.01 sec)

システムユーザに権限を付与することも、削除することもできなくなりました。

mysql> GRANT SELECT ON *.* TO 'system_user'@'localhost';
ERROR 1227 (42000): Access denied; you need (at least one of) the SYSTEM_USER privilege(s) for this operation

mysql> DROP USER 'system_user'@'localhost';
ERROR 1227 (42000): Access denied; you need (at least one of) the SYSTEM_USER privilege(s) for this operation

SYSTEM_USER 権限を復活(付与)する

SYSTEM_USER 権限の付与は SUPER 権限を持っていれば可能です。

mysql> GRANT SYSTEM_USER  ON *.* TO 'root'@'localhost';
Query OK, 0 rows affected, 1 warning (0.01 sec)

まとめ

  • SYSTEM_USER 権限を REVOKE してしまうと、例え SUPER 権限をもってても、システムユーザの変更はできない
  • SUPER権限を持つユーザを持っていれば、SYSTEM_USER権限 を付与することは可能

「マネージドサービスを実装するために、追加された仕組みなんだろうなぁ」と思いました

BCP/DR:バックアップ・レプリカ・マルチリージョンクラスターの比較

  • 目標復旧時点(Recovery Point Objective = RPO)
  • 目標復旧時間(Recovery Time Objective = RTO)

バックアップの遠隔地保管

  • RPO

    • RPO はバックアップ頻度に依存
    • 日次バックアップであれば、RPOは1日、最大で1日分のデータがロスト
  • RTO

    • 比較的、長い
    • データベースのサイズに比例
    • 特にサイズの大きいデータベースのリカバリは時間がかかる
  • コスト
    • ストレージ保管コスト+バックアップを転送するコストがかかる
    • 基本的には低コストだが、クラウドではネットワーク転送コストが高くつくため、バックアップのサイズが大きいと、レプリカのほうが安くつく場合がある

非同期レプリカを遠隔地に設置

  • RPO
    • ミリ秒〜数秒レベル
    • トランザクションログ(MySQLでいうとバイナリログ)を転送するのにかかる時間だけ考えれば良い
    • MySQL の Applier Thread の遅延はRPOに影響しない。SHOW SLAVE STATUSSeconds_Behind_Master の 値 は RPOには関係ない。データの転送は Receiver IO Thread がバイナリログを読み取った時点で完了している。
  • RTO
    • 比較的、短め
    • データのリカバリが不要
    • 復旧時に、レプリカの台数やスペックを変更する場合はその時間も考慮
  • コスト
    • レプリカの台数や構成に依存
    • すぐに切り替えられるよう、本番と同じ構成にすると高コスト
    • 最小構成であれば比較的低コスト
      • テーブルを限定することも可能

マルチリージョンクラスタ

分散データベースを想定。DBサーバを複数のリージョンに配置し、複数のリージョンにまたがった単一のクラスターを構成する。 データは複数のリージョンに同期的に書き込まれる(複製される)。

  • RPO
    • RPO: 0。データロストなし。
  • RTO
    • 最短。タイムアウトの設定にもよるが、秒レベルで障害サイトが切り離される。
    • 復旧作業自体が不要。
  • コスト
    • 基本的に高い
  • その他
    • クエリの性能(レイテンシ)が悪化する
    • 切り戻しが容易
    • 分散データベースを用いるため、3リージョン以上

ざっくりとしたまとめ

手法 RPO RTO コスト
日次バックアップの遠隔地保管 1日 長め 低め
レプリカ 数秒 構成に依存 中ぐらい
マルチリージョンクラスタ 秒レベル 高い

MySQL バージョンアップ時の動的権限の追加処理について理解する

動的権限 / Dynamic Privilege

MySQL 8.0 で、動的権限と呼ばれる、従来より細かい範囲で操作を許可できる権限が追加されました。

gihyo.jp

バージョンアップ時に自動的に動的権限が付与される

一部の静的権限(従来の権限)を保持しているユーザには、MySQL バージョンアップ時に動的権限が自動的に付与されます。 例えば、MySQL 5.7 で作成した SUPER 権限保持ユーザの権限は、MySQL 8.0 へのアップグレードで以下のように変化します。

mysql> SELECT @@version;
+-----------+
| @@version |
+-----------+
| 5.7.44    |
+-----------+
1 row in set (0.00 sec)

mysql> SHOW GRANTS FOR super57@'localhost';
+---------------------------------------------+
| Grants for super57@localhost                |
+---------------------------------------------+
| GRANT SUPER ON *.* TO 'super57'@'localhost' |
+---------------------------------------------+
1 row in set (0.00 sec)
  • 8.0.28 バージョンアップ後
mysql> SELECT @@version;
+-----------+
| @@version |
+-----------+
| 8.0.28    |
+-----------+
1 row in set (0.00 sec)

mysql> SHOW GRANTS FOR super57@'localhost' \G
*************************** 1. row ***************************
Grants for super57@localhost: GRANT SUPER ON *.* TO `super57`@`localhost`
*************************** 2. row ***************************
Grants for super57@localhost: GRANT AUDIT_ABORT_EXEMPT,AUDIT_ADMIN,AUTHENTICATION_POLICY_ADMIN,BINLOG_ADMIN,BINLOG_ENCRYPTION_ADMIN,CLONE_ADMIN,CONNECTION_ADMIN,ENCRYPTION_KEY_ADMIN,GROUP_REPLICATION_ADMIN,INNODB_REDO_LOG_ENABLE,PERSIST_RO_VARIABLES_ADMIN,REPLICATION_APPLIER,REPLICATION_SLAVE_ADMIN,RESOURCE_GROUP_ADMIN,RESOURCE_GROUP_USER,ROLE_ADMIN,SERVICE_CONNECTION_ADMIN,SESSION_VARIABLES_ADMIN,SET_USER_ID,SYSTEM_USER,SYSTEM_VARIABLES_ADMIN,TABLE_ENCRYPTION_ADMIN,XA_RECOVER_ADMIN ON *.* TO `super57`@`localhost`
2 rows in set (0.00 sec)

SUPER 権限は将来的に、削除が予定されています。互換性の維持のため、このように自動的に動的権限へと変換してくれるのでしょう。

バージョンアップ後に作成したユーザには動的権限の自動追加はされない

バージョンアップの過程で、動的権限の追加は行われます。同じ静的権限を付与したとしても、バージョンアップ後に改めて作成したユーザには、動的権限は追加されません。 同じ GRANT 文を実行して作成したユーザでも、バージョンアップを経たかどうかで表示される権限が異なってしまいます。

  • MySQL 8.0.28 で新たにユーザ作成した場合
mysql> SELECT @@version;
+-----------+
| @@version |
+-----------+
| 8.0.28    |
+-----------+
1 row in set (0.00 sec)

mysql> CREATE USER super8028@'localhost' IDENTIFIED BY 'XXXXXXXXXXXXX';
Query OK, 0 rows affected (0.01 sec)

mysql> GRANT SUPER ON *.* TO super8028@'localhost';
Query OK, 0 rows affected, 1 warning (0.00 sec)

-- 新規に作成したSUPERを持つユーザ、動的権限は明示的に指定されない限り付与されない
mysql> SHOW GRANTS FOR super8028@'localhost' \G
*************************** 1. row ***************************
Grants for super8028@localhost: GRANT SUPER ON *.* TO `super8028`@`localhost`
1 row in set (0.00 sec)

ただ、実際に、許可されるオペレーションに差はないはずです。 動的権限は基本的に、静的権限が細分化されたものであり、静的権限(例えばSUPER)を保持していれば、動的権限がなくても同様の操作が許可されているはずです。

マイナーアップデートでも発生する

MySQL 8.0 ではマイナーバージョンで追加された、動的権限があります。とみたさんの MySQL Parameters で見ると非常にわかりやすいです。

マイナーバージョンアップ時も動的権限の追加は発生します。動的権限の追加は、権限ごとに「その動的権限を保持しているユーザがいないこと」が発動(権限追加)の条件になります。

  • 例) BINLOG_ENCRYPTION_ADMIN 権限を追加する処理
-- Add the privilege BINLOG_ENCRYPTION_ADMIN for every user who has the privilege SUPER
-- provided that there isn't a user who already has the privilige BINLOG_ENCRYPTION_ADMIN.
SET @hadBinLogEncryptionAdminPriv = (SELECT COUNT(*) FROM global_grants WHERE priv = 'BINLOG_ENCRYPTION_ADMIN');
INSERT INTO global_grants SELECT user, host, 'BINLOG_ENCRYPTION_ADMIN', IF(grant_priv = 'Y', 'Y', 'N')
FROM mysql.user WHERE super_priv = 'Y' AND @hadBinLogEncryptionAdminPriv = 0;
COMMIT;

この結果、アップグレードパスとユーザ作成のタイミングによって、権限の異なるユーザが出来てしまい、ややこしいです。 例えば、MySQL 5.7 → 8.0.28 → 8.0.40 とバージョンアップし、それぞれのバージョンでユーザを追加したとします。最終的にそれぞれのユーザの権限は以下になります。

-- MySQL 5.7 時点で作成したユーザ
mysql> SHOW GRANTS FOR super57@'localhost' \G
*************************** 1. row ***************************
Grants for super57@localhost: GRANT SUPER ON *.* TO `super57`@`localhost`
*************************** 2. row ***************************
Grants for super57@localhost: GRANT AUDIT_ABORT_EXEMPT,AUDIT_ADMIN,AUTHENTICATION_POLICY_ADMIN,BINLOG_ADMIN,BINLOG_ENCRYPTION_ADMIN,CLONE_ADMIN,CONNECTION_ADMIN,ENCRYPTION_KEY_ADMIN,GROUP_REPLICATION_ADMIN,INNODB_REDO_LOG_ENABLE,PERSIST_RO_VARIABLES_ADMIN,REPLICATION_APPLIER,REPLICATION_SLAVE_ADMIN,RESOURCE_GROUP_ADMIN,RESOURCE_GROUP_USER,ROLE_ADMIN,SERVICE_CONNECTION_ADMIN,SESSION_VARIABLES_ADMIN,SET_USER_ID,SYSTEM_USER,SYSTEM_VARIABLES_ADMIN,TABLE_ENCRYPTION_ADMIN,XA_RECOVER_ADMIN,TELEMETRY_LOG_ADMIN ON *.* TO `super57`@`localhost`
2 rows in set (0.00 sec)

-- MySQL 8.0.28 時点で作成したユーザ
-- AUDIT_ABORT_EXEMPT などは、super57 がすでに、保持しているため追加されない
mysql> SHOW GRANTS FOR super8028@'localhost' \G
*************************** 1. row ***************************
Grants for super8028@localhost: GRANT SUPER ON *.* TO `super8028`@`localhost`
*************************** 2. row ***************************
Grants for super8028@localhost: GRANT TELEMETRY_LOG_ADMIN ON *.* TO `super8028`@`localhost`
2 rows in set (0.00 sec)

-- MySQL 8.0.40 時点で作成したユーザ
mysql> SHOW GRANTS FOR super8040@'localhost' \G
*************************** 1. row ***************************
Grants for super8040@localhost: GRANT SUPER ON *.* TO `super8040`@`localhost`
1 row in set (0.00 sec)

追加された動的権限を剥奪してもバージョンアップで復活する

上記で記載したように、各動的権限が追加される条件は、バージョンではなく、「その動的権限を保持しているユーザがいないこと」です。 そのため、静的権限だけで運用しようと自動的に追加された動的権限を剥奪したとしても、またバージョンアップをすると、復活してしまいます *1

追加される動的権限の対応表

mysql_system_tables_fix.sql に動的権限の付与処理が書かれています。動的権限が追加される権限を整理してみました (8.0のみ、8.4 は確認してません)。

SUPER だけでなく、CREATE USER 権限 や SELECT 権限 に対して付与される動的権限もあります。また、SYSTEM_VARIABLES_ADMINAUTHENTICATION_POLICY_ADMIN のような、動的権限 → 動的権限 の対応も存在しているようです。

権限を管理するツールを自前で開発していると、この自動追加処理の内容を加味して実装する必要があり面倒でした。 個人的には、この処理は、MySQLのバージョンアップ時に強制せず、任意のタイミングで管理者が別途、実行できるほうが良かったように思います。

※赤字はDynamic Privilege

対象となる権限 追加される Dynamic Privilege
SUPER AUDIT_ADMIN
BACKUP_ADMIN
BINLOG_ADMIN
BINLOG_ENCRYPTION_ADMIN
CLONE_ADMIN
CONNECTION_ADMIN
ENCRYPTION_KEY_ADMIN
GROUP_REPLICATION_ADMIN
INNODB_REDO_LOG_ENABLE
PERSIST_RO_VARIABLES_ADMIN
REPLICATION_APPLIER
REPLICATION_SLAVE_ADMIN
RESOURCE_GROUP_ADMIN
RESOURCE_GROUP_USER
ROLE_ADMIN
SERVICE_CONNECTION_ADMIN
SESSION_VARIABLES_ADMIN
SET_USER_ID
SYSTEM_USER
SYSTEM_VARIABLES_ADMIN
TABLE_ENCRYPTION_ADMIN
TELEMETRY_LOG_ADMIN
XA_RECOVER_ADMIN
RELOAD INNODB_REDO_LOG_ARCHIVE
FLUSH_OPTIMIZER_COSTS
FLUSH_STATUS
FLUSH_USER_RESOURCES
FLUSH_TABLES
CREATE USER APPLICATION_PASSWORD_ADMIN
PASSWORDLESS_USER_ADMIN
SET_USER_ID SYSTEM_USER
SELECT SHOW_ROUTINE
SYSTEM_VARIABLES_ADMIN AUTHENTICATION_POLICY_ADMIN
SYSTEM_USER AUDIT_ABORT_EXEMPT
FIREWALL_EXEMPT
SYSTEM_VARIABLES_ADMIN SENSITIVE_VARIABLES_OBSERVER

まとめ

  • バージョンアップ時に動的権限が自動的に追加されるケースがある
  • バージョンアップを経たかどうかで、権限が変わる。環境ごとに権限が意図せず異っている場合、この処理が原因かも。

*1:一部の権限は mysql.session などのシステムユーザにも付与されるため、復活しないものもあると思います

MySQL 各バージョンの caching_sha2_password 対応状況まとめ

自分用のメモです。

MySQL 5.7

  • デフォルトは mysql_native_password
  • caching_sha2_password は利用できない
  • ただし、クライアントは caching_sha2_password をサポート

MySQL 8.0

  • デフォルト は caching_sha2_password
    • default_authentication_plugin パラメータを変更し、デフォルトを mysql_native_password にすることも可能
  • mysql_native_password も 利用可能

MySQL 8.4

  • デフォルト は caching_sha2_password
  • default_authentication_plugin パラメータは削除された
  • mysql_native_password を利用するには、mysql_native_password=on 設定が必要

MySQL 9.0 (Innovation Release)

  • mysql_native_password は削除
  • クライアントは引き続き、mysql_native_password をサポート
$ rpm -qli mysql-community-client-plugins-9.0.1-1.el9.x86_64 | grep native
/usr/lib64/mysql/plugin/mysql_native_password.so

クラウドの設定状況

2024.09.21 時点の状況です。

8.0 世代 の default_authentication_plugin の値

クラウドでは default_authentication_pluginmysql_native_password に調整されている。

default_authentication_plugin の値
MySQL Community caching_sha2_password
AWS RDS mysql_native_password
AWS Aurora mysql_native_password
Google Cloud SQL mysql_native_password

8.4 世代

AWS RDS MySQL はデフォルトで、mysql_native_password=on してくれている。

Cloud SQLmysql_native_password は有効化されているものの、新規のパスワードには利用できない

mysql> CREATE USER user84 IDENTIFIED WITH mysql_native_password BY 'MySQL8.4';
ERROR 4052 (HY000): Invalid plugin "mysql_native_password" specified as 1 factor during "CREATE USER".

8.4 にアップグレードする前に作られたユーザのみ継続利用できる。

mysql_native_password
MySQL Community -
AWS RDS *1 ON
AWS Aurora -
Google Cloud SQL

*1:RDS Preview 版で確認

MySQL ruby ドライバの caching_sha2_password 平文接続の挙動がバラバラだった件

caching_sha2_password は 少なくとも1回はセキュアな経路を必要とする

caching_sha2_password 形式のパスワードを使った、平文の接続 をする場合、その過程で、少なくとも一度は事前に、暗号化されたセキュアな経路を通して認証成功させる必要があります。 サーバがキャッシュ(パスワードのハッシュ)を生成するのに、平文のパスワードを要求するためです。

この「少なくとも一度は〜」の背景は、MySQL 運用・管理 実践入門で詳しく解説されていました。気になる人は読むと良いでしょう

「少なくとも一度は〜」と書きましたが、厳密に言うと「1度」だけ済まない可能性があります。 キャッシュはメモリ上にしか保持されないため、サーバの再起動で消えます。また、FLUSH PRIVILEGE コマンドでもクリアされます。

キャッシュが未生成だとエラーになるドライバがある

キャッシュ未生成時、サーバがセキュアな経路を要求してきたときの挙動はドライバ依存です。 go-sql-driver/mysqlphp_mysqlnd では、セキュアの経路を自動的にフォールバックし、何事もなく接続できます。コードの変更も不要です。

rubyのドライバの対応状況を確認してみると、挙動の違いがあって、面白かったです。

ドライバ名 caching_sha2 平文接続のサポート caching_sha2 キャッシュ未作成時
ruby-mysql ⚪︎ ⚪︎ エラーなし (get_server_public_key=trueが必要)
mysql2 ⚪︎ × エラー
trilogy × サポートされていない -

なお、この仕様を意識する必要があるのは平文接続の場合のみです。常にセキュアな通信をする SSL/TLS 接続やUnix Domain Socket では意識する必要ありません。

ruby-mysql

オプションに get_server_public_key=true を付けておけば、キャッシュ未作成時もエラーなく接続可能でした。mysql コマンドにあるオプションと同じですね。 サーバの公開鍵を利用して、自動的に「初回のセキュアな経路」を作成します。

my = Mysql.connect(URI::Parser.new.escape("mysql://#{username}:#{password}@#{hostname}:#{port}/?ssl_mode=disabled&get_server_public_key=true"))

mysql2

サーバサイドにキャッシュが未作成だとエラーになります。回避するためのオプション等は提供されていなそうでした。 ワークアラウンドとして、このエラーをトラップして、何らか別手段 (rubyで書く必要もない) で一度認証を成功させてキャッシュを作ることになりそうです。

Mysql2::Error: Authentication plugin 'caching_sha2_password' reported error: Authentication requires secure connection.

一旦、キャッシュが作成されしまえば、以降は問題なく接続可能です。

trilogy

trilogycaching_sha2_password 形式の認証は SSLUnix Domain Socket のみサポートです。平文は利用できません。 サーバにキャッシュが作成済みであろうと、なかろうと、caching_sha2_password と 平文接続を組み合わせた時点で、エラーになります。

Trilogy::BaseConnectionError: trilogy_auth_recv: caching_sha2_password requires either TCP with TLS or a unix socket: TRILOGY_UNSUPPORTED

PR にも明確に書かれていました。

chosen on purpose to only implement the path where TLS or a unix socket is used. We will not be implementing the non-TLS/non-unix socket path. https://github.com/trilogy-libraries/trilogy/pull/165

trilogyで平文接続を利用している場合は、caching_sha2_password 導入とあわせて、SSL/TLS接続への変更が必要になります。

ruby-mysqlmysql2 では、キャッシュが生成済みであれば、エラーは出ません。 これらのドライバでは「初回のみセキュアな経路を必要とする」仕様を見逃しやすく、サーバ再起動などでキャッシュがなくなったときに、ある日突然、エラーに直面するリスクがあるように感じます。

trilogy はサーバサイドのキャッシュの有無に依存せず、常にエラーになるため、事前に問題に気づきやすく、一番、安全側に倒した選択とも言えそうです。

試したバージョン

$ gem list | egrep 'mysql|trilogy'
mysql2 (0.5.6)
ruby-mysql (4.1.0)
trilogy (2.8.1)

$ dpkg -l | grep mysql
ii  libmysqlclient-dev     8.0.39-0ubuntu0.24.04.1  amd64 MySQL database development files
ii  libmysqlclient21:amd64 8.0.39-0ubuntu0.24.04.1  amd64 MySQL database client library
ii  mysql-client-core-8.0  8.0.39-0ubuntu0.24.04.1  amd64 MySQL database core client binaries
ii  mysql-common           5.8+1.1.0build1          all   MySQL database common files, e.g. /etc/mysql/my.cnf

検証用のコード

require 'uri'

require 'mysql'
require 'mysql2'
require 'trilogy'

port     = 3307
hostname = '192.168.103.123'
username = 'app'
password = 'Password'
q        = 'show status like "Ssl_cipher"'

# mysql2
begin
    my2 = Mysql2::Client.new(:host => hostname, :port => port, :username => username, :password => password, :ssl_mode => :disabled)

    my2.query(q).each do |row|
      p row
    end
    # clear sha256 cache if successfuly loggin
    my2.query("FLUSH PRIVILEGES")
rescue => e
  p e
end

# ruby-mysql
begin
    my = Mysql.connect(URI::Parser.new.escape("mysql://#{username}:#{password}@#{hostname}:#{port}/?ssl_mode=disabled&get_server_public_key=true"))
    my.query(q).each do |row|
      p row
    end
    # clear sha256 cache if successfuly loggin
    my.query("FLUSH PRIVILEGES")
rescue => e
  p e
end


# trilogy
begin
    tri = Trilogy.new(host: hostname, port: port, username: username, password: password)
    tri.query(q).each_hash do |row|
     p row
    end
    # clear sha256 cache if successfuly loggin
    tri.query("FLUSH PRIVILEGES")
rescue => e
  p e
end
$ ruby test.rb
#<Mysql2::Error: Authentication plugin 'caching_sha2_password' reported error: Authentication requires secure connection.>
["Ssl_cipher", ""]
#<Trilogy::BaseConnectionError: trilogy_auth_recv: caching_sha2_password requires either TCP with TLS or a unix socket: TRILOGY_UNSUPPORTED>

MySQL go-sql-driver/mysql ドライバで意図せず utf8mb4_0900_ai_ci が使われるケース

Background

MySQL 8.0 で utf8mb4のデフォルトの COLLATION (照合順序) が utf8mb4_general_ci から utf8mb4_0900_ai_ci に変更されました。 COLLATE を指定せず、SET NAMES <CHARSET> を実行すると、そのキャラクタセットのデフォルトの collation が利用されます。

つまり、SET NAMES utf8mb4 の結果、5.7 まではクライアントのCOLLATIONとして utf8mb4_general_ci が使われますが、8.0 からは utf8mb4_0900_ai_ci が使われます。

-- MySQL 5.7 だと utf8mb4_general_ci
mysql> SET NAMES utf8mb4;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @@SESSION.collation_connection;
+--------------------------------+
| @@SESSION.collation_connection |
+--------------------------------+
| utf8mb4_general_ci             |
+--------------------------------+
1 row in set (0.00 sec)

-- MySQL 8.0 だと utf8mb4_0900_ai_ci
mysql> SET NAMES utf8mb4;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @@SESSION.collation_connection;
+--------------------------------+
| @@SESSION.collation_connection |
+--------------------------------+
| utf8mb4_0900_ai_ci             |
+--------------------------------+
1 row in set (0.00 sec)

-- COLLATION を明示的に指定が必要
mysql> SET NAMES utf8mb4 COLLATE utf8mb4_general_ci;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @@SESSION.collation_connection;
+--------------------------------+
| @@SESSION.collation_connection |
+--------------------------------+
| utf8mb4_general_ci             |
+--------------------------------+
1 row in set (0.00 sec)

MySQL 8.0 にバージョンアップした際に、意図せず utf8mb4_0900_ai_ci が使われてしまうケースがないか調査しました。 ドライバは go-sql-driver/mysql のみを確認しています。他のドライバは挙動が異なると思います。

前提

default_collation_for_utf8mb4 を変更せず、skip_character_set_client_handshake も有効化できない環境を前提としています。 これらのパラメータが変更できる場合は、サーバサイドの設定で影響を回避可能なはずです(確かめてませんが)。

まとめ

パターン ドライバのオプション 影響有無
指定なし 影響なし
collation のみ指定 collation=utf8mb4_general_ci 影響なし
charset のみ指定 charset=utf8mb4 影響あり
charset と collation を両方指定 & go-sql-driver/mysql v1.8 以降 charset=utf8mb4&collation=utf8mb4_general_ci 影響なし
charset と collation を両方指定 & go-sql-driver/mysql v1.8 より前 charset=utf8mb4&collation=utf8mb4_general_ci 影響あり

詳しい説明

charset も collation も指定していないケース (影響なし)

go-sql-driver/mysql のデフォルト collation は utf8mb4_general_ci のため utf8mb4_general_ci が暗黙的に利用されます。collation のみを指定したケースと同様の動作です。 SET NAMES は実行されません。

charset のみ指定 (影響あり)

冒頭で、記載した COLLATE 指定なしの SET NAMES utf8mb4 が実行されます。 MySQLサーバの utf8mb4 のデフォルト collation が変更された影響を受けます。

charset と collation を両方指定

go-sql-driver/mysql が v1.8 より前 (影響あり)

v1.8 より前では、charset と collation の両方指定した場合 (charset=utf8mb4&collation=utf8mb4_general_ci)、以下のように、作用します。

  1. ハンドシェイク時に utf8mb4_general_ci を指定し接続
  2. 接続後、COLLATE 無しで、SET NAMES utf8mb4 を改めて実行(結果として、ハンドシェイク時の collation は無視される)

これにより、MySQLサーバの utf8mb4 のデフォルト collation が変更された影響を受けます。 明示的に両方指定するのが一番確実だと思いがちですが、実際は期待と異なった挙動をするようです。

v1.8 より前では、charset とcollationがちぐはぐな組み合わせでも接続可能です (v1.8 以降ではError 1253 (42000): COLLATION ~ is not valid for CHARACTER SET ~)。 SET NAMES する際に、ドライバのオプションに指定した collation が使われないからです。

charset=latin1&collation=utf8mb4_general_ci だと、SET NAMES latin1 が実行され、最終的に利用される COLLATION は latin1 のデフォルトである latin1_swedish_ci になります。

このような動作をすることから、v1.7 のドキュメントには charset は指定せず、collation だけを指定するように記載があります。

Usage of the charset parameter is discouraged because it issues additional queries to the server. Unless you need the fallback behavior, please use collation instead.

go-sql-driver/mysql が v1.8 以降(影響なし)

v1.8.0 (2024/03/09 Release) で、charset と collation を両方指定した場合、COLLATEを含めて、SET NAMES <CHARSET> COLLATE <COLLATION> が実行されるようになりました。v1.8.0 以降 では問題は発生しなくなっています。

Use SET NAMES charset COLLATE collation. by @methane in #1437 https://github.com/go-sql-driver/mysql/releases

methane.hatenablog.jp

両方指定している場合は、v.1.8.0 へバージョンアップしてから MySQL 8.0 へ移行する必要があります。

参考資料

sejima さんもこのあたりについて詳しく調査されています。

labs.gree.jp

結果一覧

検証用のコード

package main

import (
    "database/sql"
    "fmt"

    _ "github.com/go-sql-driver/mysql"
)

var (
    scope, vname, vvalue *string
    masterConfig = "root:Password@(192.168.0.100:3306)/"
    parameters = []map[string]string{
        {},
        {"charset": "utf8mb4"},
        {"collation": "utf8mb4_unicode_ci"},
        {"collation": "utf8mb4_general_ci"},
        {"charset": "utf8mb4", "collation": "utf8mb4_unicode_ci"},
        {"charset": "utf8mb4", "collation": "utf8mb4_general_ci"},
        {"charset": "latin1"},
        {"charset": "latin1", "collation": "latin1_bin"},
        /* {"charset": "latin1", "collation": "utf8mb4_unicode_ci"}, */
    }
)

func main() {
    for i, m := range parameters {
        dsn := ""

        if charset, ok := m["charset"]; ok {
            if collation, ok := m["collation"]; ok {
                dsn = masterConfig + "?charset=" + charset + "&collation=" + collation
            } else {
                dsn = masterConfig + "?charset=" + charset
            }
        } else {
            if collation, ok := m["collation"]; ok {
                dsn = masterConfig + "?collation=" + collation
            } else { // nothing
                dsn = masterConfig
            }
        }

        fmt.Printf("%d. charset = %s, collation = %s\n", i+1, m["charset"], m["collation"])

        db, err := sql.Open("mysql", dsn)
        if err != nil {
            panic(err)
        }
        defer db.Close()
        q := fmt.Sprintf(`SELECT /* %d. charset = %s, collation = %s */ 'session', VARIABLE_NAME, VARIABLE_VALUE FROM performance_schema.session_variables WHERE VARIABLE_NAME = 'collation_connection'
                                  UNION ALL
                                  SELECT 'global ', VARIABLE_NAME, VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME IN ('collation_connection', 'collation_server', 'default_collation_for_utf8mb4')`, i+1, m["charset"], m["collation"])

        rows, err := db.Query(q)
        if err != nil {
            panic(err)
        }
        defer rows.Close()

        for rows.Next() {
            if err := rows.Scan(&scope, &vname, &vvalue); err != nil {
                panic(err)
            }
            fmt.Printf("%s\t%s\t%s\n", *scope, *vname, *vvalue)
        }
    }

}

MySQL Go のドライバ と caching_sha2_password

mysql_native_password の廃止が近づいてきた...

MySQL 8.4 では mysql_native_password がデフォルトで無効化されてしまいました。 そろそろ、mysql_native_password 方式の利用の撲滅に本気にならないといけないようです。

sakaik.hateblo.jp

mysql_native_password 方式 から、よりセキュアな caching_sha2_password 方式に変更することになります。 認証まわりの変更には接続ドライバの対応も必要。Goのドライバの対応状況や挙動を確認しました。

caching_sha2_password に 対応しているバージョン

結論から言うと、go-sql-driver/mysqlVersion 1.4.1 (2018-11-14 リリース) *1 以降を利用していれば、問題なし。 特にコードの変更も必要なし。なお、SSL接続ではなく、平文接続で検証しています (MySQL 8.0のcaching_sha2_password + 非SSL接続が転ける が発生しないかの確認)。

念の為、サーバ側 の default_authentication_plugin 設定が mysql_native_password, caching_sha2_password の 2パターン、実際のユーザのパスワードが mysql_native_password, caching_sha2_password の2パターン、の組み合わせ、合計4パターンをテストしました。 いずれのパターンでも、 1.4.1 以降であれば問題なく接続できました。


なお、v1.3 だと、caching_sha2_password を使ったユーザに接続しようとすると以下のエラーになる。

$ ./main  app
panic: this authentication plugin is not supported

default_authentication_plugin と Auth Switch Request

go-sql-driver/mysql では、クライアントは サーバ側default_authentication_plugin 設定に指定された認証方式で認証をまず試みる。 そして、実際のユーザの認証方法(Authentication plugin (sha2 or native)) が異なっていた場合、サーバは改めて、正しい方式で認証しなおすようクライアントにリクエストします。

以下のパケットキャプチャで「Auth Switch Request」となっている部分

packetcapture

caching_sha2_password への切り替え過程では、default_authentication_plugin と ユーザの Authentication plugin が異なる期間が発生してしまう。 この期間は接続処理がAuth Switch Requestぶん、1往復増えるので、コネクションプーリングをしていないレイテンシのある環境では注意が必要かもしれない。

あと、caching_sha2_password への切り替えが終わったら、default_authentication_pluginも(もし、mysql_native_password に変更していたら)caching_sha2_passwordに忘れずに変更したい。 そうしないと、1往復無駄が発生し続ける。

このあたりはドライバによって挙動が異なる模様。以下のページで詳しく解説されています。

dev.mysql.com

mysql_native_password を使い続けたい場合

古いドライバを使っていて、mysql_native_password を使い続けたい場合、default_authentication_pluginmysql_native_password に変更したほうが良い。 古いドライバでは、ユーザのパスワードが mysql_native_password だったとしても、default_authentication_plugincaching_sha2_password というだけでエラーになってしまうパターンが存在した(以下の表のNo.7)。

まとめの表

matrix

*1: caching_sha2_password は v1.4.0 で入ったが、v1.4.1 でバグが修正されている