それが僕には楽しかったんです。

僕と MySQL と時々 MariaDB

OSS にちょっと技術的な貢献するまでの流れ

はじめに

どうも、最近 Hulu でナルトが全話配信されていることに気がつき週末を溶かしたけんつです。
以前、MySQL の公式 Docker イメージにプルリクを投げたらようやくマージされたので、何をやったのか流れをまとめようかなと思って書いている。

よく「OSS に貢献してみよう」と言われるとドキュメントの翻訳や修正などが手頃で OSS 貢献の入門には丁度良いと言われる。
もう少し技術的なプルリクを出したい時にどうすればいいかは情報がなかったので、それらに意欲がある人に向けて少しでも役に立てばというモチベーションで書いている。

やったこと

github.com

MySQL の公式 Docker イメージに設定することが非推奨になっているオプションが明示的に設定されている箇所を見つけたので、該当するオプションを削除するプルリクを出した。

プルリクエストを投げるまでの流れ

問題が見つかる

MySQL の勉強をしようと思って Docker イメージを起動してログを見ていたら素のイメージでも Warning がいくつか発生していることに気がつき全部原因を調べればそれなりに勉強になると思って原因を調べることにしたのだが
そのなかで、どうやら非推奨オプションを明示的に指定しているとわかったので余計なログを抑制する名目で修正することにした。


Warning が発生している場合は非推奨の何かを使っていることがあり、それらは将来的に修正することがあったりする。
なのでそういう場合は積極的に修正しても良いし、何よりバグ見つけて貢献より圧倒的にハードルが低いがドキュメント修正よりは技術的な内容になりがちなのでおすすめしたい。

信頼できる情報をまとめておく

今回は該当するオプションが非推奨であることが MySQL 8.0 の公式ドキュメントとリリースノートに記載されていたのでそれを根拠に修正を進める。
後々、プルリクの概要欄などに使えるのでどこかにメモしておくと良い。

Symbolic link support as described here, along with the --symbolic-links option that controls it, is deprecated and will be removed in a future version of MySQL. In addition, the option is disabled by default.
https://dev.mysql.com/doc/refman/8.0/en/symbolic-links-to-tables.html:

Symbolic link support as described at Using Symbolic Links for MyISAM Tables on Unix, along with the --symbolic-links option that controls it, is now deprecated and will be removed in a future MySQL version. In addition, the option is now disabled by default. The related have_symlink system variable also is deprecated and will be removed in a future MySQL version.
https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-2.html:

リポジトリを fork する

対象のリポジトリを fork する。
今回 fork したのは以下の対象となる以下のリポジトリ
github.com


fork したやつがこれ。このリポジトリに修正を加えていく。
github.com

README を確認する。

README の Contribute みたいな項目を設けている場合は、プルリクにどういう要件があるのか明記されていることが多いので確認する。
それに従わないと修正してもマージされないことがあるので確認する。

今回は特になかったが、例えば以前ドキュメントにプルリクを出した thephpleague/oauth2-client では以下のようにコントリビュートの要件がかなり明確に決められている。

Contributing

Contributions are welcome and will be fully credited.

We accept contributions via Pull Requests on Github.

Pull Requests

  • PSR-2 Coding Standard - The easiest way to apply the conventions is to install PHP Code Sniffer.
  • Add tests! - Your patch won't be accepted if it doesn't have tests.
  • Document any change in behaviour - Make sure the README and any other relevant documentation are kept up-to-date.
  • Consider our release cycle - We try to follow SemVer. Randomly breaking public APIs is not an option.
  • Create topic branches - Don't ask us to pull from your master branch.
  • One pull request per feature - If you want to do more than one thing, send multiple pull requests.
  • Send coherent history - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting.
  • Ensure tests pass! - Please run the tests (see below) before submitting your pull request, and make sure they pass. We won't accept a patch until all tests pass.
  • Ensure no coding standards violations - Please run PHP Code Sniffer using the PSR-2 standard (see below) before submitting your pull request. A violation will cause the build to fail, so please make sure there are no violations. We can't accept a patch if the build fails.

Testing

The following tests must pass for a build to be considered successful. If contributing, please ensure these pass before submitting a pull request.

$ ./vendor/bin/parallel-lint src test
$ ./vendor/bin/phpunit --coverage-text
$ ./vendor/bin/phpcs src --standard=psr2 -sp

Happy coding!

https://github.com/thephpleague/oauth2-client/blob/master/CONTRIBUTING.md

過去にマージされた修正から傾向を掴む

もし明確なコントリビュートの要件がない場合は、過去にマージされたプルリクから何があるとレビュワーにとって嬉しいのかを判断できる場合があるので見てみると良い。
ブランチの命名規則や、プルリクの概要、コミットの粒度など。

ある程度みたら何がクローズされているかも見れば、修正は正しいがマージされないケースを知ることができることがある。
master ブランチにプッシュしているなど。

問題箇所を修正する

ブランチを切る

コントリビュートの要件にしたがってブランチを切る。
今回は特になく、fork したリポジトリの master ブランチからプルリクを飛ばしても良いみたいだったのでそうしている。

修正する

問題の箇所を修正する。

今回は以下の設定を削除している。
github.com

ビルド、テスト、実行

ビルドが通り、テストをパスし、実行しても不具合がないことの確認をする。

今回は Dockerfile からイメージをビルドして、docker run している。
その段階でログを確認して、対象の Warning が出力されないことを確認している。
CI を利用していたので、正しく動作していることが確認できたらプッシュして、CI でテストがパスされればここのフェーズは完了。
テストが落ちた場合は、適宜修正する。

プルリクを作成する

プッシュしたら Github の Web UI からプルリクを作成できるので実行する。
その段階でプルリクの概要を記載する必要がある。

概要を記載する

ここは空でもいい場合があるが、自分は空でいい場合でも書く派なので書く。
何を書くかというと以下の3点を意識すると良いと思う。

  • プルリクで何を修正、追加したのか(この時判断材料となるドキュメントがあれば載せる)
  • 予期する結果がどういうものか(ログや実行結果など)
  • 必要となる設定、変更後の利用方法(ビルド方法など、利用方法に変更がある場合)

ここまで書いたらプルリクの作成を完了する。

マージされることを天に祈る

レビューが返ってきたら、適宜修正する。


やることをやり切ったらあとは祈るだけ。

まとめ

  • Warning はバグ潰しよりも遭遇し、非推奨の何かが絡んでくることが多く修正しやすい
  • コントリビュートの要件は遵守する
  • 概要は要点を抑えてしっかり書く

余談

書いていて思ったがバグに遭遇した場合も検証したことはないが、今までみた OSS のプルリクをみる限りこれでいけると思う。

MySQL 8.0.15 の前後で変わった文字列と DATE 型の比較について

はじめに

そういえば、最近この手の記事を書いてないし何ならインプットもしてない事を思い出し、今日雑にテストしてたらたまたまハマった面白い挙動があったのでまとめる。
久々に MySQL と格闘した。

前提条件

こんなテーブルを作っておく。

create table hoge(t DATE);
insert into hoge(t) values("2020-01-01");
mysql> desc hoge;
+-------+------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+------+------+-----+---------+-------+
| t     | date | YES  |     | NULL    |       |
+-------+------+------+-----+---------+-------+
1 row in set (0.01 sec)

mysql> select * from hoge;
+------------+
| t          |
+------------+
| 2020-01-01 |
+------------+
1 row in set (0.01 sec)

こうなればおっけー。

謎現象を再現する

8.0.15 以下

mysql> select * from hoge where t > "2020";     
+------------+                                                                                
| t          |                                                                                
+------------+                                                                                
| 2020-01-01 |                                                                                
+------------+                                                                                
1 row in set, 1 warning (0.00 sec)

mysql> show warnings;
+---------+------+------------------------------------------------------+
| Level   | Code | Message                                              |
+---------+------+------------------------------------------------------+
| Warning | 1292 | Incorrect date value: '2020' for column 't' at row 1 |
+---------+------+------------------------------------------------------+
1 row in set (0.00 sec)                                

これも個人的には??という感じだったが、まぁとりあえず放置。

8.0.16 以上

mysql> select * from hoge where t > "2020";
ERROR 1525 (HY000): Incorrect DATE value: '2020'

エラーになる。

原理

まず、MySQL 8.0.15 以下では文字列と DATE 型を比較する場合、以下の手順を踏む。
1. 文字列を DATE (or DATETIME) に変換して比較する
2. 1.の変換ができない場合は逆に DATE を文字列に変換して比較する


これが MySQL 8.0.16 以上では次のような手順に変更された。
1. 文字列を DATE (or DATETIME) に変換して比較する
2. 1.が失敗した段階でエラーとなる


これ、MySQL 8.0.16 以上を触っている人からすると普通にやべぇなって思うかもしれないが
MySQL 5.7.x から MySQL 8.0.16 以上にアップグレードしてからこれがわかるとまぁびっくりすることこの上ない。
しかも、ガチで文字列になっているみたいで DATE 型の値を文字列に直して評価したときと同じ結果になる。
断じて内部的に 2020 という文字列が "2020-01-01" みたいに修正されているわけではない。
ガチで DATE が文字列として評価されているみたい。

文献

MySQL 8.0.16 の Release Note に書いてあった。

When comparing DATE values with constant strings, MySQL first tries to convert the string to a DATE and then to perform the comparison. When the conversion failed, MySQL executed the comparison treating the DATE as a string, which could lead to unpredictable behavior. Now in such cases, if the conversion of the string to a DATE fails, the comparison fails with ER_WRONG_VALUE. (Bug #29025656)
(DATE値を定数文字列と比較する場合、MySQLは最初に文字列をDATEに変換してから比較を実行しようとします。変換に失敗した場合、MySQLはDATEを文字列として扱って比較を実行するため、予測できない動作が発生する可能性がありました。このような場合、文字列のDATEへの変換に失敗すると、ER_WRONG_VALUEで比較が失敗するようになりました。(バグ#29025656))

MySQL :: MySQL 8.0 Release Notes :: Changes in MySQL 8.0.16 (2019-04-25, General Availability)

いや、これ Bug 扱いかよとは思ったが一応そういうことらしい。

ちなみに 8.0.15 で Bug Report が出されていて、コメントがついていた。

Documented fix as follows in the MySQL 8.0.16 changelog:

When comparing DATE values with constant strings, MySQL first
tries to convert the string to a DATE and then to perform the
comparison. When the conversion failed, MySQL executed the
comparison treating the DATE as a string. Now in such cases, if
the conversion of the string to a DATE fails, the comparison
fails with ER_WRONG_VALUE.

Closed.

(一部抜粋)
( DATE値を定数文字列と比較する際に、MySQLが文字列をDATEに変換してから比較を実行しようとする。変換に失敗した場合、MySQL は DATE を文字列として扱い比較を実行していました。このような場合、文字列のDATEへの変換に失敗すると、ER_WRONG_VALUEで比較に失敗します。)

MySQL Bugs: #93513: Unexpected behaviour for date comparison with constant strings

おわりに

今日はこれに4時間溶かしました。

MySQL の docker コンテナを起動した時の warning, error を調べつくす

はじめに

ふと MySQL の docker コンテナのログを見たら、なにやら warning や error を吐いていたから原因を調べてみた。
基本的な設定は password 周りと port 33060, MYSQL_ROOT_PASSWORD のみ設定している

問題のログ

master_1  | 2020-05-24 14:14:47+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.18-1debian9 started.
master_1  | 2020-05-24 14:14:47+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
master_1  | 2020-05-24 14:14:47+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.18-1debian9 started.
master_1  | 2020-05-24 14:14:47+00:00 [Note] [Entrypoint]: Initializing database files
master_1  | 2020-05-24T14:14:47.489542Z 0 [Warning] [MY-011070] [Server] 'Disabling symbolic links using --skip-symbolic-links (or equivalent) is the default. Consider not using this option as it' is deprecated and will be removed in a future release.
master_1  | 2020-05-24T14:14:47.490218Z 0 [System] [MY-013169] [Server] /usr/sbin/mysqld (mysqld 8.0.18) initializing of server in progress as process 48
master_1  | 2020-05-24T14:14:51.079329Z 5 [Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.
master_1  | 2020-05-24 14:14:54+00:00 [Note] [Entrypoint]: Database files initialized
master_1  | 2020-05-24 14:14:54+00:00 [Note] [Entrypoint]: Starting temporary server
master_1  | 2020-05-24T14:14:54.738243Z 0 [Warning] [MY-011070] [Server] 'Disabling symbolic links using --skip-symbolic-links (or equivalent) is the default. Consider not using this option as it' is deprecated and will be removed in a future release.
master_1  | 2020-05-24T14:14:54.739005Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.18) starting as process 97
master_1  | 2020-05-24T14:14:55.555469Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
master_1  | 2020-05-24T14:14:55.566293Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
master_1  | 2020-05-24T14:14:55.608994Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.18'  socket: '/var/run/mysqld/mysqld.sock'  port: 0  MySQL Community Server - GPL.
master_1  | 2020-05-24 14:14:55+00:00 [Note] [Entrypoint]: Temporary server started.
master_1  | 2020-05-24T14:14:55.863681Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: '/var/run/mysqld/mysqlx.sock'
master_1  | Warning: Unable to load '/usr/share/zoneinfo/iso3166.tab' as time zone. Skipping it.
master_1  | Warning: Unable to load '/usr/share/zoneinfo/leap-seconds.list' as time zone. Skipping it.
master_1  | Warning: Unable to load '/usr/share/zoneinfo/zone.tab' as time zone. Skipping it.
master_1  | Warning: Unable to load '/usr/share/zoneinfo/zone1970.tab' as time zone. Skipping it.
master_1  | 2020-05-24 14:14:59+00:00 [Note] [Entrypoint]: Creating database sample
master_1  | 
master_1  | 2020-05-24 14:14:59+00:00 [Note] [Entrypoint]: Stopping temporary server
master_1  | 2020-05-24T14:14:59.745593Z 11 [System] [MY-013172] [Server] Received SHUTDOWN from user root. Shutting down mysqld (Version: 8.0.18).
master_1  | 2020-05-24T14:15:00.689149Z 0 [System] [MY-010910] [Server] /usr/sbin/mysqld: Shutdown complete (mysqld 8.0.18)  MySQL Community Server - GPL.
master_1  | 2020-05-24 14:15:00+00:00 [Note] [Entrypoint]: Temporary server stopped
master_1  | 
master_1  | 2020-05-24 14:15:00+00:00 [Note] [Entrypoint]: MySQL init process done. Ready for start up.
master_1  | 
master_1  | 2020-05-24T14:15:01.036210Z 0 [Warning] [MY-011070] [Server] 'Disabling symbolic links using --skip-symbolic-links (or equivalent) is the default. Consider not using this option as it' is deprecated and will be removed in a future release.
master_1  | 2020-05-24T14:15:01.036868Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.18) starting as process 1
master_1  | 2020-05-24T14:15:01.733035Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
master_1  | 2020-05-24T14:15:01.743247Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
master_1  | 2020-05-24T14:15:01.783752Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.18'  socket: '/var/run/mysqld/mysqld.sock'  port: 33060  MySQL Community Server - GPL.
master_1  | 2020-05-24T14:15:02.882639Z 0 [ERROR] [MY-011300] [Server] Plugin mysqlx reported: 'Setup of bind-address: '*' port: 33060 failed, `bind()` failed with error: Address already in use (98). Do you already have another mysqld server running with Mysqlx ?'
master_1  | 2020-05-24T14:15:02.882761Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: '/var/run/mysqld/mysqlx.sock' 

symbolic-links=0 が元のコンテナイメージ側にある Dockerfile で設定されているために発生するエラー。
おそらく MySQL 8.0.2 以降で非推奨になっている?
dev.mysql.com

あと、デフォルトで無効化されているため設定する必要がない。
dev.mysql.com

これはプルリクだした。マージされてないが LGTM が帰ってきている。
github.com


[Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.

起動時に datadir/mysql が存在しなければ、--initialize-insecure が無条件でつけられる。それで、このオプションをつけると必ず発生するのがこの Warning。
パスワードなしで root が生成されるが、その後にパスワードが設定されることが想定される場合はやっているみたい。
dev.mysql.com

実際 entrypoint.sh で --initialize-insecure 付きで実行された後に root アカウントに対してなにやらパスワードが設定されている部分を見つけた。
github.com

[Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.

MySQL 8 で auto_generate_certs が有効になっている場合(デフォルトで有効になっている) datadir に証明書がなければ自己署名証明書が生成される。
証明書が自己署名である場合にこのエラーは発生する。

[Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.

これは権限周りがあれしてあれになっている。
MySQL 8.0.18 の docker コンテナを割と最小構成で起動した時のログ。default_native_password, port=33060, MYSQL_ROOT_PASSWORD だけ設定している · GitHub

詳しくは yoku さんが解説していて pid ファイルそのものや pid ファイルを置いているディレクトリが o+w になっていると発生するエラーらしい。

Warning: Unable to load '/usr/share/zoneinfo/iso3166.tab' as time zone. Skipping it.

これはタイムゾーンのロードしている部分なのだが、 zoneinfo を全て見ているっぽくて timezone data 以外のファイルをロードしようとするとこの手のエラーを吐く。

[ERROR] [MY-011300] [Server] Plugin mysqlx reported: 'Setup of bind-address: '*' port: 33060 failed, `bind()` failed with error: Address already in use (98). Do you already have another mysqld server running with Mysqlx ?'

MySQL 側の port を 33060 にしているため mysqlx のものとかぶってしまっているために発生しているエラー。

おわりに

プルリク一個出せたので満足。

Go 再入門 ~基本的なデータ構造周り~

はじめに

どうも、最近自分と同い年のプロダクトに触れて死ぬほど苦しんだけんつです。
ここ数ヶ月ずっと MySQL の検証や DBMS の勉強をしてきて思ったのだが、コード書いてない。
読む機会は履いて捨てるほどあるが、コード書くことなさすぎてめちゃくちゃ書けなくなっていた。昨日書いてみてこんなに書けないかと驚いた。
なので、リハビリをかねてハイパー中途半端に知っているぐらいの Go の再入門やろうと思います。
未読だった Effective Go で「ここわかんねーわ」とか「お、これは学びだな」と、思ったところをやっていきます。

golang.org

今日はデータ構造周り。

Allocation with new()

Go にはメモリ割当を行うための組み込み関数として new と make がある。まずは new から。
new は単純に、ある型 T に対して new(T) と使用すると、メモリ領域を初期化せずに zero-value を持たせた上で確保しそのポインタを返すという挙動を取る。
例えば次のようなコードを実行する場合、実行結果は以下の通りになる。

package main

import "fmt"

type Person struct {
	Age  int
	Name string
}

func main() {
	p := new(Person)
	fmt.Printf("%+v\n", p)

	num := new(int)
	fmt.Println(*num)
}
❯ go run alloc_new.go
&{Age:0 Name:}
0

これが例えば、Effective Go にあるような sync.Mutex や bytes.Buffer にあるようなものだとこうなる。

package main

import (
	"fmt"
	"sync"
	"bytes"
)

type SyncedBuffer struct {
	Lock sync.Mutex
	Buffer bytes.Buffer
}

func main() {
	p := new(SyncedBuffer)
	fmt.Printf("%+v\n", p)
	
}
❯ go run alloc_new2.go
&{Lock:{state:0 sema:0} Buffer:{buf:[] off:0 lastRead:0}}

zero value によってすぐに利用できる構造体も存在する。
ちなみに var hoge T で変数を宣言した場合、それはポインタではないが new と同じように zero value をもつ。
初期化を伴わない*1と言われることが多い。

Allocation with make()

次は組み込み関数の make。make(T, args) という形を取るが、T に当てはまる型は slice, map, channel だけという特徴がある。
また new は返す値がポインタなのに対してこっちはポインタではない。またこちらは初期化を伴う。zero value と初期化の違いがわかりにくいので、中でもわかりやすい slice を例にする。
make を使って int を要素にもつスライスを作成するコードとその実行結果をあげる。

package main

import "fmt"

func main() {
	s := make([]int, 10, 100)
	fmt.Println(s)
	fmt.Printf("len(%d)\n", len(s))
	fmt.Printf("cap(%d)\n", cap(s))
}
❯ go run alloc_make.go
[0 0 0 0 0 0 0 0 0 0]
len(10)
cap(100)

実行結果からわかるように make を使用してスライスを作成した場合は array, len, cap *2の初期化を伴う。
これが new, make の違いとなる。

Arrays

配列はメモリの詳細なレイアウトを決める時に役立ち、割当を回避することができる場合もあるが基本的にはスライスのビルディングブロッックになっている。
また C の配列と比較して以下の違いを持っている。

  • 配列は値。配列に配列を渡すと全ての値がコピーされる。
  • 関数へ渡す時はポインタではなくコピーが渡される
  • サイズも含めて配列は一つの型として扱われる。[3]int, [2]int は別の型として扱われる。

Slices

Slice は Array のラッパーで連続するデータに対して、汎用的で強力なインターフェースを提供している。
行列のように明確な次元をもつ必要が無い限り、大抵は Slice を利用する。Slice は元になる配列への参照を持っていて、あるスライスを別のスライスに代入すると二つのスライスは単一の配列に対する参照を持つこととなっている。
これは関数の引数にする場合も同様に動作することに注意が必要。

例えば以下のコードを実行すると、次のような結果が得られる。

package main

import "fmt"

func main() {
	a := [10]int{1,2,3,4,5,6,7,8,9,10}
	s1 := a[:]
	s2 := a[2:]
	fmt.Println(a)
	fmt.Println(s1)
	fmt.Println(s2)

	s1 = s2
	s2[0] = 1
	fmt.Println(a)
	fmt.Println(s1)
	fmt.Println(s2)
}
❯ go run slice.go
[1 2 3 4 5 6 7 8 9 10]
[1 2 3 4 5 6 7 8 9 10]
[3 4 5 6 7 8 9 10]
[1 2 1 4 5 6 7 8 9 10]
[1 4 5 6 7 8 9 10]
[1 4 5 6 7 8 9 10]

おわりに

この調子で進めていきたい

*1:Go の変数は宣言だけだったとしても zero value が必ず入るためわかりにくいが、初期化とは区別されているみたい

*2:src/runtime/slice.go - The Go Programming Language

Database Internals を読み解く ~Introduction and Overview~

はじめに

ゴールデンウィークが外出自粛一択になってしまったので前から気になっていた Database Internals を買った。
なので、ぼちぼち読み始めつつ学んだことをメモ程度にまとめていきたい。

shop.oreilly.com

参考文献の構成

前半はストレージエンジン、後半は分散システムについての二部構成になっている。
ストレージエンジン部分は B-Tree Index やトランザクション処理とリカバリー、ファイルフォーマットなど、これぞストレージエンジン感のある部分が解説されている。
分散システム部分は分散トランザクションレプリケーション、分散システムにおける一貫性などの話が展開される。

愚直に最初から読んでいく。

Introduction and Overview

DBMS Architecture

コンポーネント間の区別などに違いがある場合もあるがDBMS の大まかなアーキテクチャは以下の図のようになっている。*1 *2
f:id:RabbitFoot141:20200504181338p:plain

DBMS は一般的にサーバ・クライアントモデルを取る。クライアントからのリクエストはトランスポートサブシステムで受け取り、下のレイヤーへと伝播していく。
サーバインスタンスはリクエストを受け取り次第、クエリをパーサーにかける。ここでは単なる構文解析の他にアクセスコントロールの確認を行う。これらの手順を全てパスした場合、オプティマイザによる最適化が行われる。ここでの最適化というのは冗長な部分やそもそもに不要な部分の除去、その後に行われるカーディナリティ*3をはじめとした統計情報を用いた効率的な実行方法の構築などを指す。
クライアントのリクエストによってわたってきたクエリが最適化までされた段階で実行計画が提供される。ここで提供される実行計画は結果が確実に返却される一連の流れであり、結果は同じだが効率が異なる複数の実行計画から最適なものが選択される。
その後はストレージエンジン内で複数のコンポーネントを跨いで実行される。ストレージエンジンには次の要素が含まれる。

  • Transaction Manager

Transaction Manager では DBMS 内で論理的整合性が崩れないようにトランザクションを管理する。

  • Lock Manager

Lock Manager はトランザクションが実行されている間、データベースオブジェクトが整合性などに違反しない状態を維持するために必要な同時実行制御上必要なロックを管理する。

ディスク上のデータ整理とアクセスを管理する。ここでのアクセスメソッドとはヒープファイルや B-Tree, LSM Trees などの選択を含む。

  • Buffer Pools

メモリ上のデータ管理、キャッシュする。リカバリに関するログなどもここで扱う場合がある。

On-Memory vs Disk-Based DBMS

DBMS はメモリ上にデータを持っておく場合*4と、ディスク上にデータを持っている場合がある。*5
オンメモリといっても、一次データをメモリ上に持っているだけで WAL のような機構を持っている場合はログをディスク上に吐く場合が多い。ディスクベースの DBMS はディスク上に一次データを持っているが、一時的なストレージとしてメモリ上にキャッシュしている。
オンメモリの利点としては比較的低いアクセスコスト、パフォーマンス、アクセス粒度の面でディスクベースの DBMS より優位な点がある。またその実装を主にメモリが対象になるためディスクベースのそれと比較してシンプルになりやすい。

OS の観点から見るのであれば、オンメモリであればメモリの確保、開放といった管理を抽象化できるという利点存在し、ディスクベースではデータのシリアライズ方式や参照方法、断片の再構成*6などを行う必要があるため複雑化しやすい。
メモリ上でデータを管理する場合、揮発することを考慮しなければならないことが大きな制限となるがこの点は不揮発性メモリの登場によって解消される可能性が出てきている。

Durability in On-memory DBMS

オンメモリでデータを扱う場合はその耐久性はどのように確保されるかが重要な観点となっている。
この問題は、多くの場合ディスク上にバックアップをとることで解決している。*7 また、実際にデータそのものをシリアライズして持っているのではなく WAL (Write-Ahead Log) のようなにシーケンシャルなログを持っている場合が多い。

Column Versus Row-Oriented DBMS

列指向か行指向かという話なので、RDB に関しての話。
それぞれ、行と列でデータ構造を分割して*8保存する。MySQL, PostgreSQL は行指向データベース。

Row Oriented Data Layout

このタイプの DBMS はデータをレコードか行として保持する。
表形式にすごく近い表現方法をもつデータレイアウトだが、表形式とは異なる。またこの形式の場合、レコードや行を一意に特定できるキー*9が存在するなら様々な面でうまく働く。
また行指向データレイアウトの大きな利点として行単位でデータにアクセスし、保存することが前提となるので空間局所性*10が向上する。これはディスクの1ブロック内*11 (()) にページがもつ全てのデータを格納できる場合、特に効果を発揮する。

Column Oriented Data Layout

列指向データベースは行を列に分けて保存する。あまり馴染みがないと思ったのは正しく、なぜならいわゆる Web 系のデファクトスタンダードである DBMS では採用されていないことが多いため。
この列指向データレイアウトを採用する場合どこに利点があるのかと、データ分析に利用する場合は行指向であれば不必要なデータも一旦読み込まれてしまう*12ことがあるが、列指向はその名の通り列で分かれているためパフォーマンスを出しやすい。
列で分割されていると普通に使う時辛くないかとおもうことがあるかもしれないが、それを解消するため列指向データレイアウトはデータの最小構成単位がタプルになっていることが多い。このタプルは仮想IDと列の値の組になっていることが多く一意に行に相当するものを探索することが可能となる。

Data file and Index File

DBMS の最も大事な目的はデータを管理し、迅速なアクセスを提供することにある。そのために、物理的なデータをどのように管理するかを考慮する必要がある。*13
実データはファイルに持っておくことがほとんどだが、フラットファイルではなく実装依存の固有形式なファイルフォーマットをとっていることが多い。これにはいくつかの理由がある。
まずはストレージの観点から、ファイル操作時のオーバヘッドを最小にしたいという理由がある。次にアクセス、正確には読み込みに関連して限りなく小さいステップでデータを読み込むため。最後に更新時になるべく変更回数を小さくするため。
このような理由を可能な限り満たすために Slotted Page といった形式が存在する。また削除については多くの DBMS では削除マーカーを置いて論理削除することで削除したように見せている。

DBMS はデータ操作、特に検索においてインデックスと呼ばれるものを補助的に使用している。インデックスはレコードのメタデータを持っているため、全てのレコードを探索することなく少ない回数でレコードを探索することができる。

Data Files

データファイルは index-organized table, heap-organized table, hash-organized table などを用いて実装することができる。
index organized table はデータ自身とそのインデックス情報を含み構成される。またそのレコード自体は順に配置されるため、実際にスキャンするときはその内容をシーケンシャルスキャンすればよい。
heap organized table は順序関係ないレコードの列挙で構成されるヒープファイルの集まりとなる。順序は関係ないが基本的にレコード順になっている。レコードが追加されてもファイルの再構成などを必要としないが、特に検索時にそれぞれのレコードに対するインデックスがないとフルスキャンするはめになる。
hash-organized table ではレコードそのものはキーのハッシュ値によってバケットに格納される。レコードは追加順かキー順でソートすることで検索が高速化できると言われている。

Index Files

インデックスは前述の通り、レコードを効率的に検索できるように構造化したものを指す。インデックスを構築するのに必要なデータはレコードを識別するキー*14か主キーになる。
インデックスには大きくわけて二つの種類がある。プライマリインデックスとセカンダリインデックス。MySQL で言えば主キーがあれば自動的に付与される*15ものがプライマリインデックスで、手動で後から貼ったインデックスをセカンダリインデックスという。
記憶がただしければ、セカンダリインデックスを用いて検索した場合、セカンダリインデックスのリーフノードの値はプライマリインデックスの値であり、それが判明した段階でプライマリインデックスで検索し最終的にレコードにたどり着くはず。
インデックスを含む場合、ファイルの構成方法を二つ存在する。 インデックスを含めたまま物理ファイルにするタイプと、インデックスは別ファイルに分割するタイプ。前者を Index organized table というらしい。
前者はプライマリインデックス、後者はセカンダリインデックスでよく使われるみたい。

最後に

ここで学んだこととしてあげたものはこの後詳細な解説がやってくるのでざっくりとまとめておいた。
次はインデックス、 B-Tree に関する話。

*1:分散 DB など分散システムを前提とする場合はコンポーネント間の関係はこの限りではない。これはあくまでも単一のサーバインスタンスとして DBMS を利用する場合。

*2:図はまとめていい部分や今回読まない部分を含んでいるため書籍の図と差分がある

*3:カラムの値の種類がレコードの数に対して多いか少ないか https://www.shift-the-oracle.com/words/cardinality.html

*4:Redis のようなオンメモリ KVS

*5:大体の RDBMS はディスク上に一次データを持っている。MySQL もこれ。

*6:slotted-page などで論理削除を用いない場合を考えるとわかりやすい

*7:Redis であれば AOF, RDB がまさにそれにあたる

*8:正確にはデータのシリアライズに関連する

*9:いわゆる主キー、Primary Key、PK と呼ばれるやつら

*10:キャッシュを考慮する場合に重要となる指標のひとつ。連続または近傍領域に対して比較的近い時間にアクセスがされればされるほど空間局所性があるという。ある特定の行の次の行にアクセスすることは冷静に考えてそれなりに存在する

*11:多くの場合は 4 KB ではあるが、DBMS によってはページサイズが 8 KB, 16 KB になっている場合もある。さらにページを 16 KB として、それを連続して配置させるスペースという概念で管理する場合もある。

*12:SELECT column FROM hoge みたいにすれば列だけ読み込めるじゃんと思うかもしれないが、内部的には必ずしも特定の列のみをストレージメディアから読み込んでいるとは限らない。

*13:オンメモリは考えない。あくまでも確定した実データをディスクに持っている場合の話。

*14:重複が認められているもの。ただし重複しすぎると大体インデックスが効かなくなる

*15:主キーがない場合はその代替に相当するもので構成される

MySQL 8.0.20 で導入された binlog transaction compression をハイパー雑に検証する

はじめに

どうも、共通新人研修がビジネス職よりで割とコードとか書いている暇がなかったけんつです。
今週からはエンジニアの研修で RFC と格闘することが強制されて息を吹き返してます。


MySQL 8.0.20 がリリースされて、めちゃくちゃ気になったのが binlog 圧縮。
他にも検証をしていたのだけど、なんか急に気になったので全てを放り投げて調べてみたくなった。
zstd というアルゴリズムを用いて binlog を可逆圧縮する機能が追加になったので、どれだけ圧縮されるのか調べてみた。
MySQL の運用に関わったことがなく、完全に趣味で追っているので binlog 関連の検証として正しい方法なのかはわかっていないので間違っているところがあったり、改善点があれば教えて欲しい。

前提環境と検証方法

github.com

ここの master ブランチにあるコンテナ群で検証した。
MySQL 8.0.19, zstd を有効にしたMySQL 8.0.20, zstd を無効にした MySQL 8.0.20 で特定の sql ファイルを実行して生成される binlog ファイルのサイズを比較する。
binlog 圧縮を有効にすることと、パスワードログインを可能にする設定以外は全てデフォルト。
圧縮レベルも指定できるが今回はデフォルトの 3 で行っている。

それぞれの設定は以下にある。
https://dev.mysql.com/doc/refman/8.0/en/replication-options-binary-log.html#sysvar_binlog_transaction_compression_level_zstd
https://dev.mysql.com/doc/refman/8.0/en/replication-options-binary-log.html#sysvar_binlog_transaction_compression

圧縮レベルは増加すればするほど、ストレージ領域とネットワーク帯域を節約できるが CPU リソースを多く使用してしまうという
いつものトレードオフが存在している。


リリースノートの文献

以下のリリースノートにある、引用部分を参照する。
dev.mysql.com

From MySQL 8.0.20, you can enable binary log transaction compression on a MySQL server instance. When binary log transaction compression is enabled, transaction payloads are compressed using the zstd algorithm, and then written to the server's binary log file as a single event (a Transaction_payload_event). Compressed transaction payloads remain in a compressed state while they are sent in the replication stream to replication slaves, other Group Replication group members, or clients such as mysqlbinlog. They are not decompressed by receiver threads, and are written to the relay log still in their compressed state. Binary log transaction compression therefore saves storage space both on the originator of the transaction and on the recipient (and for their backups), and saves network bandwidth when the transactions are sent between server instances.

You can enable binary log transaction compression on a MySQL server instance using the binlog_transaction_compression system variable, which defaults to OFF. You can also use the binlog_transaction_compression_level_zstd system variable to set the level for the zstd algorithm that is used for compression. This value determines the compression effort, from 1 (the lowest effort) to 22 (the highest effort).

ハイパー雑に和訳すると。

MySQL 8.0.20 からサーバインスタンスでバイナリトランザクションを圧縮することが可能になった。
圧縮する場合、トランザクションペイロードは zstd アルゴリズムを使用してバイナリログを圧縮する。
このときサーバはバイナリログファイルに1つのイベントとして書き込む。
スタンドアローン時やレプリケーション時を問わず圧縮された状態で送信され、受信時に解凍されない。
この特性によりトランザクションの送信、受信においてストレージ領域とネットワーク領域を節約する。

やってみた

MySQL 8.0.19, 8.0.20 の docker コンテナを起動して mysql client に入ってから以下の sql ファイルを実行する。

mysql> source sql/class.up.sql
mysql> source sql/hashjoin.up.sql

この状態で ./docker/mysql8019|mysql8020/data/ 以下の binlog ファイルのサイズを確認する。

// MySQL 8.0.19
-rw-r-----    1 hoge  hoge   3102158  4 29 03:01 binlog.000001
-rw-r-----    1 hoge  hoge    890060  4 29 03:03 binlog.000002
// MySQL 8.0.20
-rw-r-----    1 hoge  hoge    298346  4 29 03:01 binlog.000001
-rw-r-----    1 hoge  hoge    130275  4 29 03:02 binlog.000002
// MySQL 8.0.20 without zstd compression
-rw-r-----    1 hoge  hoge   3102159  4 29 03:55 binlog.000001
-rw-r-----    1 hoge  hoge    890061  4 29 03:56 binlog.000002

大体 binlog.000001 は 1/10 ~ 1/9 あたりのサイズになっているっぽい。
binlog ファイルの分割が発生した上で binlog.000001 のサイズが縮小しているのでおそらくそのぐらい圧縮されるとみても良いのか?というところ。

binlog.000002 に関しては 1/9 ほどになっている。

これだと怪しいのでみんな大好き sakila sample database で試してみた。

// MySQL 8.0.20
❯ ll ./docker/mysql8020/data | grep "binlog"
-rw-r-----    1 hoge  hoge    298170  4 29 10:57 binlog.000001
-rw-r-----    1 hoge  hoge       156  4 29 10:57 binlog.000002
-rw-r-----    1 hoge  hoge        32  4 29 10:57 binlog.index

// MySQL 8.0.19
~/mysqlProject/mysql-poc master*
❯ ll ./docker/mysql8019/data | grep "binlog"
-rw-r-----    1 hoge  hoge   3102158  4 29 10:57 binlog.000001
-rw-r-----    1 hoge  hoge       155  4 29 10:57 binlog.000002
-rw-r-----    1 hoge  hoge        32  4 29 10:57 binlog.index

この状態で以下のファイルを実行すると

mysql> source sakila-db/sakila-schema.sql;
mysql> source sakila-db/sakila-data.sql;
// MySQL 8.0.20
~/mysqlProject/mysql-poc master* 1m 59s
❯ ll ./docker/mysql8020/data | grep "binlog"
-rw-r-----    1 hoge  hoge    298170  4 29 10:57 binlog.000001
-rw-r-----    1 hoge  hoge    590183  4 29 11:01 binlog.000002
-rw-r-----    1 hoge  hoge        32  4 29 10:57 binlog.index

// MySQL 8.0.19
~/mysqlProject/mysql-poc master*
❯ ll ./docker/mysql8019/data | grep "binlog"
-rw-r-----    1 hoge  hoge   3102158  4 29 10:57 binlog.000001
-rw-r-----    1 hoge  hoge  1359983  4 29 11:01 binlog.000002
-rw-r-----    1 hoge  hoge        32  4 29 10:57 binlog.index

大体 1/2 程度、ファイルサイズが減少している。
もうちょっと binlog にいろいろ突っ込みたいので、sakila db を drop してもう一回やり直した場合、次のようになった。

~/mysqlProject/mysql-poc master* 24s
❯ ll ./docker/mysql8020/data | grep "binlog"
-rw-r-----    1 hoge  hoge    298170  4 29 10:57 binlog.000001
-rw-r-----    1 hoge  hoge   1180403  4 29 11:13 binlog.000002
-rw-r-----    1 hoge  hoge        32  4 29 10:57 binlog.index

~/mysqlProject/mysql-poc master*
❯ ll ./docker/mysql8019/data | grep "binlog"
-rw-r-----    1 hoge  hoge   3102158  4 29 10:57 binlog.000001
-rw-r-----    1 hoge  hoge   2719998  4 29 11:13 binlog.000002
-rw-r-----    1 hoge  hoge        32  4 29 10:57 binlog.index

1/2 ~ 1/3 あたりをフワフワしている。

perfomance_shema をみてみる

binary_log_transaction_compression_stats をみるとどの程度圧縮されたのか、圧縮適用外のトランザクションがどれぐらいあったかをみることができる。
MySQL :: MySQL 8.0 Reference Manual :: 26.12.11.11 The binary_log_transaction_compression_stats Table
みてみると、大体 58% ほど圧縮されているようなので大体ただしいのかなといったところ。

mysql> select * from binary_log_transaction_compression_stats\G;
*************************** 1. row ***************************
                            LOG_TYPE: BINARY
                    COMPRESSION_TYPE: ZSTD
                 TRANSACTION_COUNTER: 60
            COMPRESSED_BYTES_COUNTER: 2249088
          UNCOMPRESSED_BYTES_COUNTER: 5293704
              COMPRESSION_PERCENTAGE: 58
                FIRST_TRANSACTION_ID: ANONYMOUS
  FIRST_TRANSACTION_COMPRESSED_BYTES: 2185
FIRST_TRANSACTION_UNCOMPRESSED_BYTES: 4310
         FIRST_TRANSACTION_TIMESTAMP: 2020-04-29 02:32:49.437426
                 LAST_TRANSACTION_ID: ANONYMOUS
   LAST_TRANSACTION_COMPRESSED_BYTES: 193
 LAST_TRANSACTION_UNCOMPRESSED_BYTES: 212
          LAST_TRANSACTION_TIMESTAMP: 2020-04-29 02:36:34.471615
*************************** 2. row ***************************
                            LOG_TYPE: BINARY
                    COMPRESSION_TYPE: NONE
                 TRANSACTION_COUNTER: 151
            COMPRESSED_BYTES_COUNTER: 94042
          UNCOMPRESSED_BYTES_COUNTER: 94042
              COMPRESSION_PERCENTAGE: 0
                FIRST_TRANSACTION_ID: ANONYMOUS
  FIRST_TRANSACTION_COMPRESSED_BYTES: 105
FIRST_TRANSACTION_UNCOMPRESSED_BYTES: 105
         FIRST_TRANSACTION_TIMESTAMP: 2020-04-29 02:32:45.334943
                 LAST_TRANSACTION_ID: ANONYMOUS
   LAST_TRANSACTION_COMPRESSED_BYTES: 209
 LAST_TRANSACTION_UNCOMPRESSED_BYTES: 209
          LAST_TRANSACTION_TIMESTAMP: 2020-04-29 02:36:34.452032
2 rows in set (0.00 sec)
余談

圧縮レベルを最大にして同じことを行い、ファイルサイズを比較しても大体 1/2 ~ 1/3 で推移していて「?」となった。
その辺の設定をみてみると " from 1 (the lowest effort) to 22 (the highest effort). " とあるので、もしかすると圧縮レベル以外に
何か別の要因がないと圧縮率は向上しない?

一回だけ雑に SQL を叩きまくったら 1/7 程度に減少したケースがあったけど再現しなかったので余談としている。要検証。

さいごに

binlog ファイルが良い感じに圧縮されると見ていいのか自信がないが物理的な容量が減少していることはわかった。大体半分ぐらいになるっぽい。
計算リソースについては言及していないので、圧縮レベルとファイルサイズがどのようにそれらと関わってくるかがわかっていない。
あと圧縮レベルを上げても満足に圧縮されないケースがあったので、このへんも努力的な意味合いでのレベルなのかそれとも強制力があるかどうかみたいなところは
まだまだ追っていく必要がありそう。

2020/04/29 4:42

MySQL が死ぬぐらいの SQL (source sql/hashjoin.up.sql をもう一回実行すると死ぬ)をぶん投げると 1/5 程度になることが確認できたけど
その場合、バイナリログの圧縮にどのような影響がでるのか要検証

2020/04/29 11:43

sakila sample database を使った場合に binlog.000002 のファイルサイズがどれくらい減少しているのか検証したので追加している。

2020/04/29 12:08

パフォーマンススキーマで確認できることがわかったので追記。

MySQL 8.0.19 と docker-compose 環境下で Single-Primary InnoDB Cluster を構築する

はじめに

InnoDB Cluster がどういうものか知ってはいるが構築したことがなかったのでとりあえず構築してみた。
github.com
今回は MySQL Shell をホストマシンに置き、MySQL Server x 3, MySQL Router を docker コンテナで建てる。
MySQL Shell のコンテナを使っても良いが、手動で行いたいため今回はホストマシン上で操作する。またこの都合により、MySQL Shellが関連する docker コンテナには固定で IPv4 のアドレスを割り当てている。

環境

  • mysql 8.0.19
  • ubuntu 18.04
  • docker 19.03.5
  • docker-compose 1.23.0

参考資料

dev.mysql.com

InnoDB Cluster の構築

InnoDB Cluster とは

MySQL InnoDB Cluster とは MySQL Group Replication + MySQL Router + MySQL Shell で構築可能な高可用性ソリューションを指す。


Group Replication がベースとなっているため、構築時における要求や制限は Group Replication のものに左右される。*1
また基本的に InnoDB Cluster の基幹は Group Replication であるがそれを InnoDB Cluster として、構成し管理するための AdminAPI が MySQL Shell により提供されるものを利用する。これにより Group Replication を直接操作することなく InnoDB Cluster
を操作出来る。

要件

まず Group Replication を使用するため、サーバインスタンスも Group Replication と同様の要件を満たす必要がある。実際に要件を満たしているかどうかを AdminAPI の dba.checkInstanceConfiguration() で確認する。

ストレージエンジンに関しては InnoDB 以外の MyISAM 等を選択することが Group Replication では可能であるが InnoDB Cluster 構築時は InnoDB のみでメンバを構成する必要がある。またそれと同時に全てのインスタンスでパフォーマンススキーマを有効にする必要がある。

また、バージョン 8.0.17 以降では server_id を通常のレプリケーション構築時と同様に InnoDB Cluster 内で一意に設定する必要がある。

MySQL Shell の導入

手元に MySQL Shell がなかったので以下のドキュメントを参考に導入する。
dev.mysql.com

各サーバインスタンスの設定を修正する

MySQL Shell で dba.checkInstanceConfiguration() を実行することで確認できる。

 mysqlsh -u root -h 127.0.0.1 -p -P 33060
Please provide the password for 'root@127.0.0.1:33060': ****
MySQL Shell 8.0.19

Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its affiliates.
Other names may be trademarks of their respective owners.

Type '\help' or '\?' for help; '\quit' to exit.
Creating a session to 'root@127.0.0.1:33060'
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 13
Server version: 8.0.19 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.
 MySQL  127.0.0.1:33060 ssl  JS > dba.checkInstanceConfiguration();
Validating local MySQL instance listening at port 33060 for use in an InnoDB cluster...

This instance reports its own address as 7147fa7c00f5:33060
Clients and other cluster members will communicate with it through this address by default. If this is not correct, the report_host MySQL system variable should be changed.

Checking whether existing tables comply with Group Replication requirements...
No incompatible tables detected

Checking instance configuration...

NOTE: Some configuration options need to be fixed:
+--------------------------+---------------+----------------+--------------------------------------------------+
| Variable                 | Current Value | Required Value | Note                                             |
+--------------------------+---------------+----------------+--------------------------------------------------+
| binlog_checksum          | CRC32         | NONE           | Update the server variable                       |
| enforce_gtid_consistency | OFF           | ON             | Update read-only variable and restart the server |
| gtid_mode                | OFF           | ON             | Update read-only variable and restart the server |
+--------------------------+---------------+----------------+--------------------------------------------------+

Some variables need to be changed, but cannot be done dynamically on the server.
NOTE: Please use the dba.configureInstance() command to repair these issues.

{
    "config_errors": [
        {
            "action": "server_update", 
            "current": "CRC32", 
            "option": "binlog_checksum", 
            "required": "NONE"
        }, 
        {
            "action": "server_update+restart", 
            "current": "OFF", 
            "option": "enforce_gtid_consistency", 
            "required": "ON"
        }, 
        {
            "action": "server_update+restart", 
            "current": "OFF", 
            "option": "gtid_mode", 
            "required": "ON"
        }
    ], 
    "status": "error"
}

binlog_checksum, enforce_gtid_consistency, gtid_mode の設定を修正する必要があるみたい。

binlog_checksum

このオプションではバイナリログの各イベントにチェックサムを付与することが可能になる。デフォルトは CRC32。
マスタースレーブ環境下でスレーブに認識されない異なるチェックサムの形式を指定している場合にスレーブが自身の binlog_checksum を NONE に変更しレプリケーションを停止する。後方互換性において懸念がある場合は明示的に NONE を設定する。
https://dev.mysql.com/doc/refman/8.0/en/replication-options-binary-log.html#sysvar_binlog_checksum

enforce_gtid_consistency

GTID を使用したレプリケーションを行う場合にログを確実に記録できる構文のみ実行を許可することで強制的に GTID の一貫性を確保したい場合に設定する。ON, OFF 以外に WARN があり、その場合は一貫性に反することが許容されるが警告が出るようになる。
https://dev.mysql.com/doc/refman/8.0/en/replication-options-gtids.html#sysvar_enforce_gtid_consistency

gtid_mode

GTID ベースのログを有効にするかどうかの設定を行う。
https://dev.mysql.com/doc/refman/8.0/en/replication-options-gtids.html#sysvar_gtid_mode

InnoDB Cluster の構築

InnoDB Cluster は MySQL Shell から、AdminAPI を使いサーバインスタンスを操作することで構築可能となっている。
手順自体は簡単で

  1. dba.createCluster('hoge'); でクラスタを作成
  2. dba.addInstance(other_node_option) でノードを追加する

これだけとなっている。
ただここにたどり着くまでの設定などで docker を前提とする場合いくつかの罠が存在したり
公式ドキュメントにはないが Group Replication の設定を追加する必要がある。それは最後にまとめて記述する。

まずは InnoDB クラスタを作成する。

 MySQL  172.30.0.60:3306 ssl  JS > var cluster = dba.createCluster('singlePrimaryCluster');
A new InnoDB cluster will be created on instance '172.30.0.60:3306'.

Validating instance configuration at 172.30.0.60:3306...

This instance reports its own address as 172.30.0.60:3306

Instance configuration is suitable.
NOTE: Group Replication will communicate with other members using '172.30.0.60:33061'. Use the localAddress option to override.

Creating InnoDB cluster 'singlePrimaryCluster' on '172.30.0.60:3306'...

Adding Seed Instance...
Cluster successfully created. Use Cluster.addInstance() to add MySQL instances.
At least 3 instances are needed for the cluster to be able to withstand up to
one server failure.

createCluster は無事に完了したのでステータスを確認する。

 MySQL  172.30.0.60:3306 ssl  JS > cluster.status();
{
    "clusterName": "singlePrimaryCluster", 
    "defaultReplicaSet": {
        "name": "default", 
        "primary": "172.30.0.60:3306", 
        "ssl": "REQUIRED", 
        "status": "OK_NO_TOLERANCE", 
        "statusText": "Cluster is NOT tolerant to any failures.", 
        "topology": {
            "172.30.0.60:3306": {
                "address": "172.30.0.60:3306", 
                "mode": "R/W", 
                "readReplicas": {}, 
                "replicationLag": null, 
                "role": "HA", 
                "status": "ONLINE", 
                "version": "8.0.19"
            }
        }, 
        "topologyMode": "Single-Primary"
    }, 
    "groupInformationSourceMember": "172.30.0.60:3306"
}

大丈夫っぽい。

次にこのプライマリに対してノードを2つ追加する。

 MySQL  172.30.0.60:3306 ssl  JS > cluster.addInstance('root@172.30.0.68:3308')

WARNING: A GTID set check of the MySQL instance at '172.30.0.68:3308' determined that it contains transactions that do not originate from the cluster, which must be discarded before it can join the cluster.

172.30.0.68:3308 has the following errant GTIDs that do not exist in the cluster:
98f05a53-691a-11ea-9705-0242ac1e0044:1-9

WARNING: Discarding these extra GTID events can either be done manually or by completely overwriting the state of 172.30.0.68:3308 with a physical snapshot from an existing cluster member. To use this method by default, set the 'recoveryMethod' option to 'clone'.

Having extra GTID events is not expected, and it is recommended to investigate this further and ensure that the data can be removed prior to choosing the clone recovery method.

Please select a recovery method [C]lone/[A]bort (default Abort): C
NOTE: Group Replication will communicate with other members using '172.30.0.68:33081'. Use the localAddress option to override.

Validating instance configuration at 172.30.0.68:3308...

This instance reports its own address as 172.30.0.68:3308

Instance configuration is suitable.
A new instance will be added to the InnoDB cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.

Adding instance to the cluster...

Monitoring recovery process of the new cluster member. Press ^C to stop monitoring and let it continue in background.
Clone based state recovery is now in progress.

NOTE: A server restart is expected to happen as part of the clone process. If the
server does not support the RESTART command or does not come back after a
while, you may need to manually start it back.

* Waiting for clone to finish...
NOTE: 172.30.0.68:3308 is being cloned from 172.30.0.60:3306
** Stage DROP DATA: Completed
** Clone Transfer  
    FILE COPY  ############################################################  100%  Completed
    PAGE COPY  ############################################################  100%  Completed
    REDO COPY  ############################################################  100%  Completed
** Stage RECOVERY: \
NOTE: 172.30.0.68:3308 is shutting down...

* Waiting for server restart... ready
* 172.30.0.68:3308 has restarted, waiting for clone to finish...
* Clone process has finished: 68.01 MB transferred in about 1 second (~68.01 MB/s)

Incremental state recovery is now in progress.

* Waiting for distributed recovery to finish...
NOTE: '172.30.0.68:3308' is being recovered from '172.30.0.60:3306'
* Distributed recovery has finished

The instance '172.30.0.68:3308' was successfully added to the cluster.

 MySQL  172.30.0.60:3306 ssl  JS > cluster.addInstance('root@172.30.0.69:3309')

WARNING: A GTID set check of the MySQL instance at '172.30.0.69:3309' determined that it contains transactions that do not originate from the cluster, which must be discarded before it can join the cluster.

172.30.0.69:3309 has the following errant GTIDs that do not exist in the cluster:
99fe015f-691a-11ea-bc83-0242ac1e0045:1-9

WARNING: Discarding these extra GTID events can either be done manually or by completely overwriting the state of 172.30.0.69:3309 with a physical snapshot from an existing cluster member. To use this method by default, set the 'recoveryMethod' option to 'clone'.

Having extra GTID events is not expected, and it is recommended to investigate this further and ensure that the data can be removed prior to choosing the clone recovery method.

Please select a recovery method [C]lone/[A]bort (default Abort): C
NOTE: Group Replication will communicate with other members using '172.30.0.69:33091'. Use the localAddress option to override.

Validating instance configuration at 172.30.0.69:3309...

This instance reports its own address as 172.30.0.69:3309

Instance configuration is suitable.
A new instance will be added to the InnoDB cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.

Adding instance to the cluster...

Monitoring recovery process of the new cluster member. Press ^C to stop monitoring and let it continue in background.
Clone based state recovery is now in progress.

NOTE: A server restart is expected to happen as part of the clone process. If the
server does not support the RESTART command or does not come back after a
while, you may need to manually start it back.

* Waiting for clone to finish...
NOTE: 172.30.0.69:3309 is being cloned from 172.30.0.60:3306
** Stage DROP DATA: Completed
** Clone Transfer  
    FILE COPY  ############################################################  100%  Completed
    PAGE COPY  ############################################################  100%  Completed
    REDO COPY  ############################################################  100%  Completed
** Stage RECOVERY: \
NOTE: 172.30.0.69:3309 is shutting down...

* Waiting for server restart... ready
* 172.30.0.69:3309 has restarted, waiting for clone to finish...
* Clone process has finished: 68.01 MB transferred in about 1 second (~1.00 B/s)

Incremental state recovery is now in progress.

* Waiting for distributed recovery to finish...
NOTE: '172.30.0.69:3309' is being recovered from '172.30.0.60:3306'
* Distributed recovery has finished

The instance '172.30.0.69:3309' was successfully added to the cluster.

これで良い。 設定さえ問題なければ cluster.addInstance を実行するだけでノードは追加できる。

 MySQL  172.30.0.60:3306 ssl  JS > cluster.status();
{
    "clusterName": "singlePrimaryCluster", 
    "defaultReplicaSet": {
        "name": "default", 
        "primary": "172.30.0.60:3306", 
        "ssl": "REQUIRED", 
        "status": "OK", 
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.", 
        "topology": {
            "172.30.0.60:3306": {
                "address": "172.30.0.60:3306", 
                "mode": "R/W", 
                "readReplicas": {}, 
                "replicationLag": null, 
                "role": "HA", 
                "status": "ONLINE", 
                "version": "8.0.19"
            }, 
            "172.30.0.68:3308": {
                "address": "172.30.0.68:3308", 
                "mode": "R/O", 
                "readReplicas": {}, 
                "replicationLag": null, 
                "role": "HA", 
                "status": "ONLINE", 
                "version": "8.0.19"
            }, 
            "172.30.0.69:3309": {
                "address": "172.30.0.69:3309", 
                "mode": "R/O", 
                "readReplicas": {}, 
                "replicationLag": null, 
                "role": "HA", 
                "status": "ONLINE", 
                "version": "8.0.19"
            }
        }, 
        "topologyMode": "Single-Primary"
    }, 
    "groupInformationSourceMember": "172.30.0.60:3306"
}

ステータスにも反映されていることが確認できる。

ここまで出来たら次は、このクラスターへのアクセスに MySQL Router を挟む。

オプションの類

MySQL Router に関する色々をやるまえに、 ここまでに必要だった設定達をまとめる。
セカンダリとプライマリはほとんど同じ設定を使っているため、プライマリの設定を挙げて書く。

skip-name-resolve=ON
bind_address="172.30.0.60"
report-host="172.30.0.60"
loose-group_replication_local_address="172.30.0.60:3316"
loose-group_replication_local_seeds="172.30.0.60:3316"
loose-group_replication_start_on_boot=OFF

*** ネットワーク周り

ホスト名を使用しないで IP アドレスを使う。
ただし、今回のように docker コンテナとして存在する MySQL に対してホストマシンの MySQL Shell から操作する場合、createCluster 時に何故か名前解決しようとして詰まった。
これは report-host で IP アドレスを指定することで解決した。

group_replication 関連

start_on_boot は、サーバ起動時に自動的にグループレプリケーションを開始するかどうかというあれ。構築時は OFF にする必要があるらしい。
local_address はグループレプリケーションが使用するアドレスとポートを明記する。アドレスは大体サーバと同じだがポートは MySQL サーバが使用しているものとは別のものにする必要がある。

MySQL Router

MySQL Router はいい感じの Docker コンテナがあったのでそれを使う。

lrf141@lrf141-ThinkPad-X220:~/infraProject/mysql-innodb-cluster-dc$ docker exec -it mysql-innodb-cluster-dc_router_1_8c5f6a8ebeca bash
bash-4.2# mysqlrouter --bootstrap root@master1 --user=root
Please enter MySQL password for root: 
# Bootstrapping system MySQL Router instance...

- Creating account(s) (only those that are needed, if any)
- Verifying account (using it to run SQL queries that would be run by Router)
- Storing account in keyring
- Adjusting permissions of generated files
- Creating configuration /etc/mysqlrouter/mysqlrouter.conf

Existing configuration backed up to '/etc/mysqlrouter/mysqlrouter.conf.bak'

# MySQL Router configured for the InnoDB Cluster 'singlePrimaryCluster'

After this MySQL Router has been started with the generated configuration

    $ /etc/init.d/mysqlrouter restart
or
    $ mysqlrouter -c /etc/mysqlrouter/mysqlrouter.conf

the cluster 'singlePrimaryCluster' can be reached by connecting to:

## MySQL Classic protocol

- Read/Write Connections: localhost:6446
- Read/Only Connections:  localhost:6447

## MySQL X protocol

- Read/Write Connections: localhost:64460
- Read/Only Connections:  localhost:64470

これが表示されるならオッケーなはず。
MySQL Router を通して InnoDB Cluster を利用するなら、ここでは localhost:6446,6447 に対してリクエストを投げる。

終わりに

一応できたけども、InnoDB Cluster が正常に起動した状態で docker-compose down でコンテナを落としてから再起動すると
スタンドアロンモードでプライマリが起動し、セカンダリが自動で接続されない現象が起こってしまったのでどうにかしたい。