mita2 database life

主にMySQLに関するメモです

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 世代

まだぜんぜん8.4は提供されてなかった。。。確認できたのはRDSだけ。 RDS はデフォルトで、mysql_native_password=on してくれている。

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 でバグが修正されている

TiDB metadata lock 有効な場合の DDL の挙動

TiDB の DDL の挙動を確認した。

まとめ

  • tidb_enable_metadata_lock が有効だと、DDLは先行のトランザクションによって待たされることがある
  • metadata lock 待ちが発生していても、(MySQL と違って) DMLは影響を受けない
  • metadata lock の状況は mysql.tidb_mdl_view で観測できる
    • (MySQL と違って) SHOW PROCESSLIST では表示されない

環境

TiDB 8.0.11 / tidb_enable_metadata_lock = ON で試します。tidb_enable_metadata_lock はデフォルト で ON です。

mysql> SHOW GLOBAL VARIABLES LIKE 'version';
+---------------+--------------------+
| Variable_name | Value              |
+---------------+--------------------+
| version       | 8.0.11-TiDB-v8.0.0 |
+---------------+--------------------+
1 row in set (0.02 sec)

mysql> SHOW GLOBAL VARIABLES LIKE 'tidb_enable_metadata_lock';
+---------------------------+-------+
| Variable_name             | Value |
+---------------------------+-------+
| tidb_enable_metadata_lock | ON    |
+---------------------------+-------+
1 row in set (0.03 sec)

検証

ロングトランザクションを実行開始し、ALTER TABLEを別セッションで実行します。

mysql> BEGIN;
Query OK, 0 rows affected (0.02 sec)

mysql> UPDATE t1 SET c1 = 'updated' WHERE pk = 1;
Query OK, 1 row affected (0.08 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql>  ALTER TABLE t1 MODIFY COLUMN c2 INT NULL;

ALTER TABLE は先行するトランザクションが終わるまで、metadata lock 待ちで待たされます。 metadata lock で待たされている様子は、(MySQL と違って) SHOW PROCESSLIST には表示されません。

-- ALTER TABLE は待たされているが、SHOW PROCESSLIST では観測できない
mysql> SHOW PROCESSLIST;
+------------+------+-----------------+------+---------+------+----------------------------+------------------------------------------+
| Id         | User | Host            | db   | Command | Time | State                      | Info                                     |
+------------+------+-----------------+------+---------+------+----------------------------+------------------------------------------+
| 1497366540 | root | 127.0.0.1:38330 | test | Sleep   |   76 | in transaction; autocommit | NULL                                     |
| 1497366538 | root | 127.0.0.1:49314 | test | Query   |    6 | autocommit                 | ALTER TABLE t1 MODIFY COLUMN c2 INT NULL |
| 1497366546 | root | 127.0.0.1:50144 | test | Query   |    0 | autocommit                 | SHOW PROCESSLIST                         |
+------------+------+-----------------+------+---------+------+----------------------------+------------------------------------------+
3 rows in set (0.01 sec)

tidb_mdl_view で、metadata lock 待ちを観測できます。SQL_DIGESTS でブロックしているトランザクションの内容が見れるのは便利ですね。

mysql> SELECT * FROM mysql.tidb_mdl_view \G
*************************** 1. row ***************************
     job_id: 120
    db_name: test
 table_name: t1
      query: ALTER TABLE t1 MODIFY COLUMN c2 INT NULL
 session_id: 1497366540
 start_time: 2024-05-01 03:41:18.621000
SQL_DIGESTS: ["begin","update `t1` set `c1` = ? where `pk` = ?"]
1 row in set (0.20 sec)

ALTER TABLE が待たされている状態で、後続のDMLALTER TABLE の完了を待つかどうかを確認します。

mysql> UPDATE t1 SET c1 = 'updated' WHERE pk = 10;
Query OK, 1 row affected, 1 warning (0.22 sec)
Rows matched: 1  Changed: 1  Warnings: 1

→ 待たされませんでした。

先行するトランザクションCOMMIT すると、ALTER TABLE が開始されます。 なお、ALTER TABLEが開始されても、DMLはブロックされることなくオンラインで実行可能です。

Aurora の wait_timeout の挙動が MySQL と違った

TL;DR

  • MySQL は wait_timeout に設定した値ぴったりに、接続が切られる
  • Aurora は wait_timeout の値 + 最大1分 に、接続が切られる
  • エラーメッセージも違う

wait_timeout とは

wait_timeout はアイドルセッションに対するタイムアウトを指定するパラメータです。 SQLが実行されず、アイドル状態(Sleep)がしばらく続くとそのセッションは自動的に切断されます。

wait_timeout (interactive_timeoutもあわせてます)を20秒に設定した状態で、Vanilla MySQL と Aurora の挙動を比べてみましょう。

(RDS MySQL 8.0 / Aurora V3 で比較しています)

MySQL

Sleep状態 の時間 が wait_timeout (20秒) に達したら、接続が切れます。シンプルな挙動です。

エラーメッセージは

ERROR 4031 (HY000): The client was disconnected by the server because of inactivity. See wait_timeout and interactive_timeout for configuring this behavior.

です。wait_timeout に達してセッションが切断されたことがわかるエラーメッセージになっています。これは、MySQL 8.0.24 で入った改善で、エラーメッセージがわかりやすくなりました。

AWS Aurora

MySQLと違って wait_timeout に達してもすぐ切断されません。wait_timeout ピッタリではなく、何秒か経ってから切られます。以下のスクショでは、34秒経過してから切られました。 どうも何回か試していると、wait_timeout + 1分以内に接続が切られるようでした*1

また、エラーメッセージは

ERROR 2013 (HY000): Lost connection to MySQL server during query

となっており、接続が切れたことだけしかわかりません(このエラーメッセージは wait_timeout 以外のケースでも出ます)。

あまり困ることはなさそうですが、こういった接続の扱い部分で、MySQL/Auroraで差があるのは意外でした。

*1:同じことを書いてる人がいます https://ahmedahamid.com/amazon-aurora-mysql-and-wait-timeout/

MySQL Shell dumpInstance の分割ロジック その2

この記事は MySQL Advent Calendar 2023 の23日目の記事です。

MySQL Shell の dumpInstance の chunk ロジックの説明エントリーその2です。 前回の記事では、主キーが数値型以外のケースを説明しました。主キーが数値型以外では、LIMIT句を利用して線形にレンジを求めるロジックが使われます。

今回の記事では、主キーが数値型のケースのロジック (chunk_integer_column) を説明します。

キーが数値型の場合 (chunk_integer_column)

主キーが数値型のテーブルに対する dumpInstance では、BETWEEN句を使ってテーブルを chunk に分割して(小分けにして)、バックアップが取得が行われます。

SELECT SQL_NO_CACHE `pk_int`,`col1` FROM `t`.`pkint_tbl`
WHERE (`pk_int` BETWEEN 873811 AND 1048572)

このBETWEEN の始まりと終わりを決めるロジックは constant_step()adaptive_step() の2種類があります。

  • constant_step

    • AUTO_INCREMENTのような歯抜けのない連続したPKを想定したロジック
    • 単純に PKの範囲を chunk 数で割って区切りを求めるシンプルなロジック
  • adaptive_step

    • 値が連続していないケースを想定したロジック
    • 2分探索を用いてレンジを動的に決めるアルゴリズム

constant_step or adaptive_step どちらが使われるか

PKの空間(PKの最大値 - 最小値)に対して、行が入ってない空間が 10% 以下である場合に、 constant_step() が採用され、そうでなければ、adaptive_step() が採用されます。

    const bool use_constant_step =
        estimated_chunks < 2 ||
        (index_range > info.row_count
             ? index_range - info.row_count
             : info.row_count - index_range) <= row_count_accuracy;

dumper.cc#L1271-L1280

以下の図の左のように、行数とPKの空間が近ければ、constant_step() が採用されます。 逆に右のように、行が存在していない空間が大きければ、adaptive_step() です。

constant_step の詳細

PKの範囲を chunk 数で割って区切りを求めるシンプルなロジックです。テーブルを触ることなく、計算だけで、BETWEENの値が決まります。

例) PKが 10000 〜 49999 (歯抜けなし、4万件) で、chunk 数が 4 の場合

chunk 数
  = (max(PK) - min(PK) + 1) / rows_per_chunk
  = (49999 - 10000 + 1) / rows_per_chunk
  = 40000 / (bytes_per_chunk(オプション指定) / average_row_length)
SELECT 〜 FROM `t`.`pkint_tbl` WHERE (`pk_int` BETWEEN 10000 AND 19999)
SELECT 〜 FROM `t`.`pkint_tbl` WHERE (`pk_int` BETWEEN 20000 AND 29999)
SELECT 〜 FROM `t`.`pkint_tbl` WHERE (`pk_int` BETWEEN 30000 AND 39999)
SELECT 〜 FROM `t`.`pkint_tbl` WHERE (`pk_int` BETWEEN 40000 AND 49999)

adaptive_step の詳細

一言でいうと二分探索。BETWEEN のレンジを動的に {広げ,狭め} ていき、EXPLAIN を利用して、結果行数を見積もる。 「期待するchunkあたりの行数 ± 10% 」の値におさまっていたら、レンジを確定させます。

初期に仮定したレンジが広すぎたパターンの例)

EXPLAIN SELECT COUNT(*) FROM `t`.`pkint_sparse_tbl`
WHERE `pk_int` BETWEEN 20854261 AND 21369182 ORDER BY `pk_int`

まとめ

  • MySQL Shell の dumpInstance で使われる、chunk ロジックには3種類ある
    • chunk_integer_column - constant_step
    • chunk_integer_column - adaptive_step
    • chunk_non_integer_column
  • 意図したサイズに分割してバックアップされるよう、がんばって実装されていた

MySQL Advent Calendar 2023

明日は、YoshiyukiItoh さんです!

qiita.com