MySQL 8.3.0 で --character-set-client-handshake が削除された件について ~ go-sql-driver/mysqlとmysqlndを添えて ~

こんにちわ。せじまです。

相変わらずPHPよくわかんないんですが、今日もPHPの話をします。あと、goの話もちょっとだけします。書いてるうちに少し長文になってしまいましたが、内容としてはゆるふわと言えるんじゃないでしょうか。

でははじめます。

はじめに

MySQL 8.2.0 の release notes で

The --character-set-client-handshake server option, originally intended for use with upgrades from very old versions of MySQL, is now deprecated, and a warning is issued whenever it is used. You should expect this option to be removed in a future version of MySQL; applications depending on this option should begin migration away from it as soon as possible. (WL #13220)

と書かれていた --character-set-client-handshake が、8.3.0 では

The --character-set-client-handshake and --old-style-user-limits server options were formerly used for compatibility with very old versions of MySQL which are no longer supported or maintained. Since they no longer serve any useful purpose, both options have been removed. (WL #13221, WL #13229)

ということで削除されました。8.2.0で deprecated と告知されて三ヶ月後の8.3.0で削除されたので、確かに as soon as possible ですね。さすが Innovation Release です。

--character-set-client-handshake は、MySQL 8.0公式ドキュメントの A.11 MySQL 8.0 FAQ: MySQL Chinese, Japanese, and Korean Character Sets

However, some Asian customers prefer the MySQL 4.0 behavior. To make it possible to retain this behavior, we added a mysqld switch, --character-set-client-handshake, which can be turned off with --skip-character-set-client-handshake. If you start mysqld with --skip-character-set-client-handshake, then, when a client connects, it sends to the server the name of the character set that it wants to use. However, the server ignores this request from the client.

と記述されていたものです。
アジアのユーザとしては、 --skip-character-set-client-handshake がなくなることによってどのような影響が出るか気になるところなので、公式mysqlクライアント、Go、PHPでどのような影響がでそうか、軽く調べてみました。

予め雑なまとめ

最初にざっくりまとめておきます。

  • 公式のmysqlクライアント、go-sql-driver/mysql、PHPのmysqlndで、 character set や collation のデフォルトの振る舞いはそれぞれ異なる。
    • 公式のmysqlクライアントはlibmysqlclientのMYSQL_AUTODETECT_CHARSET_NAMEを使っている。
    • go-sql-driver/mysql は内部でデフォルトのcollationを定義している。 1.5.0 でデフォルトの collation が utf8_general_ci から utf8mb4_general_ci になった。
    • PHPのmysqlndはサーバのgreet packetで返ってきたcharacter set/collationをデフォルトで使う。
  • PHPのmysqlndのデフォルトは --skip-character-set-client-handshake に近い振る舞いをしている。--skip-character-set-client-handshake を使っていたPHPユーザは、 --skip-character-set-client-handshake がなくても、今までと近い感覚で扱えると思われる。
  • PHP以外のプログラミング言語やクライアントは、それぞれの振る舞いを確認した上で、必要に応じて明示的にcharacter setやcollationを設定したほうが良い。
  • 現在、 --skip-character-set-client-handshake を有効にして運用している場合、go-sql-driver/mysqlのようにutf8mb4_general_ciをデフォルトにして接続している環境だと、将来MySQLのバージョンを上げたとき、予期せぬcollationが使われる可能性がある。
    • go-sql-driver/mysqlのようにクライアント側でutf8mb4_general_ciを設定していても、--skip-character-set-client-handshake が設定されていると、 collation_serverの値が使われる。
  • init_connect でものすごい頑張ると、character set や collation についてある程度融通は効く。ただし、PHPにおけるSET NAMESのような問題を内包している可能性はある。

では詳細に移ります。

公式の mysql クライアントの振る舞い(libmysqlclientのMYSQL_AUTODETECT_CHARSET_NAMEの振る舞い)

公式のMySQLのaptリポジトリから sudo apt-get install mysql-client する

default-character-set=auto になっていて、この auto ってなんぞ?って話ですが

https://github.com/mysql/mysql-server/blob/mysql-8.0.36/client/mysql.cc#L189
https://github.com/mysql/mysql-server/blob/mysql-8.0.36/include/mysql_com.h#L71

公式ドキュメントにあるMYSQL_AUTODETECT_CHARSET_NAMEです。公式ドキュメント曰く

各クライアントは、Unix システムの LANG または LC_ALL ロケール環境変数の値や Windows システムのコードページ設定など、オペレーティングシステムの設定に基づいて、使用する文字セットを自動検出できます。 ロケールが OS から利用できるシステムの場合、クライアントはコンパイル時のデフォルトを使用するのではなく、このロケールを使用してデフォルトの文字セットを設定します。

というやつですね。Ubuntu 22.04 LTS on WSL2とdockerで試してみましょう。

した後、LANGを切り替えたりしながらmysqlクライアントで接続してみます。

LANG=ja_JP.eucjpにすると、character_set_client、character_set_connection、character_set_resultsがujisになりました。
なお、LANG=ja_JP.eucjpであっても、--default-character-setや--init-commandで上書き可能です。

このように、mysqlクライアントで設定されたcharacter setをmysqldはcharacter_set_clientに設定するわけですが、--character-set-client-handshake=off(--skip-character-set-client-handshake)にすることによって、character_set_serverの値で強制することができます。具体的には、次のようになります。

go-sql-driver/mysql の振る舞い

go-sql-driver/mysql ではデフォルトのcharacter set(正確にはcollation)が定義されており、1.5 (2020-01-07) でデフォルトが utf8_general_ci から utf8mb4_general_ci になりました。

Update collations and make utf8mb4 default #877
README: update default collation to utf8mb4_general_ci #1054

雑に検証用のコードを書いて確認してみます。次のコードは、go 1.18.1、github.com/go-sql-driver/mysql v1.7.1で確認しました。

go-sql-driver/mysql も MySQL 8.0 もデフォルトのままだと、collation_connectionはutf8mb4_general_ci、collation_serverはutf8mb4_0900_ai_ciになります。いずれも character set としてはutf8mb4ですが、utf8mb4_general_ciとutf8mb4_0900_ai_ciは振る舞いが大きく異なるので、Goでutf8mb4を使うなら、意図したcollationが適用されているか注意が必要です。

--character-set-server=ujis のとき、go-sql-driver/mysqlはデフォルトのままだと、character_set_client は utf8mb4 になります。

--character-set-client-handshake=off にすると、collation_connectionもcollation_serverと同様にutf8mb4_0900_ai_ciになります。

また、 --character-set-server=ujis かつ --character-set-client-handshake=off にすると、character_set_clientはujisになります。

go-sql-driver/mysql の デフォルトの collation が MySQL 8.0 のデフォルトと異なるというのは要注意ですが、 go-sql-driver/mysql の振る舞いは比較的素直なものに見えます。

PHPのmysqlnd

PHPよくわからないので、まずは https://www.php.net/downloads.php から 8.1.27 のソースコードをダウンロードしてデバッグビルドします。

デバッグビルドすることで mysqli_debug が使えるようになるので、次のような検証用のスクリプトを書いてみます。

--character-set-client-handshake の設定を変えずに MySQL 8.0 のデフォルトの設定で繋いでみると、 mysqlnd では collation_connection は utf8mb4_0900_ai_ci になりました。

次に、 --character-set-server=ujis で起動してみると、今度はcharacter_set_clientがujisになりました。

なんでやねん?

そうお思いの方もいらっしゃることでしょう。私は思いました。これがPHPです。

mysqli_optionsのドキュメントには次のように書かれています。

MySQLnd always assumes the server default charset. This charset is sent during connection hand-shake/authentication, which mysqlnd will use.

ここで、デバッグビルドすることで有効にできたmysqli_debugが活きてきます。出力したログを見ると、php_mysqlnd_greet_read()charset_noを読んでいることがわかります。

mysqldの設定がデフォルトのときは charset_no=255 が

--character-set-server=ujis のときは charset_no=12 が返ってきています。

ここでMySQLの認証周り、handshakeについて振り返ってみます。

https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase.html#sect_protocol_connection_phase_initial_handshake

The initial handshake starts with the server sending the Protocol::Handshake packet.

とありますね。Handshake Packetですが

https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_handshake_v10.html

を見ると、

int<1> character_set default server a_protocol_character_set, only the lower 8-bits

といったフィールドがあります。character set のIDは、Source Code Documentation
を参照すると、information_schema.collationsから collation_name を取得できるとわかります。

実際に information_schema.collations にSELECTしてみると

charset_no=255はutf8mb4_0900_ai_ci、charset_no=12はujis_japanese_ciとわかります。

mysqlnd_command::handshake() からソースコードを読み進めていくと、 greet_packet.charset_no を関数の引数で渡していって

https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_commands.c#L603-L658
https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_auth.c#L202-L224
https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_auth.c#L36-L122
https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_auth.c#L233-L289

session_options->charset_name が設定されていなければ、関数の引数で渡されてきたserver_charset_noをauth_packet.charset_noに設定しています。

auth_packet.charset_no と実際の character set や collation のマッピングは

https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_auth.c#L310
https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_charset.c#L741-L752
https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_charset.c#L496

https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_charset.c#L688

というように、mysqlnd内部で定義されているわけですね。

mysqlnd における SET NAMES の注意事項

SET NAMES ステートメントを使うことでcharacter_set_client、character_set_connection および character_set_resultsを変更することが可能ではあるんですが、クライアントライブラリによっては問題が発生することもあります。例えば、PHPのmysqli_real_escape_stringのドキュメントには

サーバーレベルで設定するなり API 関数 mysqli_set_charset() を使うなりして、 文字セットを明示しておく必要があります。この文字セットが mysqli_real_escape_string() に影響を及ぼします。詳細は 文字セットの概念 を参照ください。

とあります。

具体的には、mysqlnd_conn_data::escape_string()mysqlnd_cset_escape_slashes() を呼ぶと

といった箇所がありますが、この cset->mb_valid() や cset->mb_charlen() が、さきほど見た

の mysqlnd_mbcharlen_ujis や check_mb_ujis 、mysqlnd_mbcharlen_utf8 や check_mb_utf8_valid なわけですね。
例えば、 utf8mb4_general_ci は

https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_charset.c#L532

mysqlnd_mbcharlen_utf8とcheck_mb_utf8_validなので、utf8mb4_0900_ai_ciと互換性がありそうですけど、mysqlnd_mbcharlen_ujisやcheck_mb_ujisのujisでは文字列のエスケープがうまく動かなそうだ、というところです。

mb_charlenやmb_validなども切り替えるために、PHPのmysqli_real_escape_stringのドキュメントには、mysqli_set_charset()を使うよう書いてあるわけですね。

こういったエスケープに関する振る舞いは、各プログラミング言語のライブラリ間で挙動が異なる可能性も否めません。ライブラリでcharacter setをどのように設定すべきなのかは、ライブラリのドキュメント等確認されるのがよろしいでしょう。

おまけ: init_connet

「ujisとutf8mb4くらい違うならしょうがないけど、collationくらいならなんとかインチキできんのか?」

そうお考えの方もいらっしゃるでしょう。私は考えます。

というわけで、 --skip-character-set-client-handshake を使わずに --init-connect で collation を強制的に上書きしてみようと思います。

次のように指定しつつmysqld起動して

まずはmysqlクライアントで試してみましょう。rootはSUPER権限があるので、init_connect の影響を受けません。LANG=ja_JP.eucjpではcharacter_set_client=ujisになります。また、collation_serverなどはMySQL8.0のデフォルトであるutf8mb4_0900_ai_ciになります。

SUPER権限もCONNECTION_ADMIN権限もないユーザでアクセスすると、次のように書き換わります。

次は、go-sql-driver/mysqlで確認してみましょう。sed を使って test.go のアカウントとパスワードを書き換えます。

次のように、 --character-set-client-handshake=off を使わなくても、collation_serverやcollation_databaseをutf8mb4_general_ciで置き換えることが可能です。

ただし、これはcollation_connectionやcollation_serverなど書き換えられただけなので、これでクライアントライブラリが文字列のエスケープなど正しく実行できるかどうかは、ライブラリのドキュメントなど要確認ですね。

※今回、 --init-connect で default_collation_for_utf8mb4 というシステム変数を指定しました。デフォルトのcollationをutf8mb4_0900_ai_ciから変更する場合、default_collation_for_utf8mb4についても考慮した方が良いので指定しましたが、このシステム変数についても解説しようとすると、それだけで記事が一本書けてしまうシロモノになります。ゆえに、今回の記事ではdefault_collation_for_utf8mb4の詳細については触れません。
実際、北川さんがdefault_collation_for_utf8mb4などで解説記事を一本執筆されていますので、気になる方はこちらの記事を参照してください。

おわりに

振り返ってみると、個人的には

  • character setやcollationのデフォルトの振る舞いがクライアントライブラリ側で異なっているので、 --skip-character-set-client-handshake でサーバ側の設定を強制させることにより、各プログラミング言語ごとの振る舞いの違いを吸収しやすかった。

という一面もあったんじゃないかと思いました。ただ、古いソースコードや機能を整理していくことは、ソフトウェアを長期的にメンテナンスしていく上で大切なことだと思いますから、 --skip-character-set-client-handshake を廃止するメリットも理解できるところです。少なくとも、プログラミング言語のライブラリごとにこれだけ振る舞いが異なって、さらに、 --character-set-client-handshake の on/off でまた振る舞いが変わるのであれば、複雑怪奇という他ありません。分岐する条件は減らせるなら減らしたいと思うのは、自然なことだろうと思います。

とりあえず、PHPで --skip-character-set-client-handshake の設定された mysqld 使ってた方々は、そんなに考えることはないかもしれません。ただ、PHP以外のプログラミング言語を使われていて今後MySQLのバージョンアップを予定されている方々は、character set や collation の設定を、一度見直してみるとよろしいのかもしれません。

References