目次
はじめに
表題の件です。
MySQL(MariaDB)を使った開発を行っていましたが、そこで原因不明のバグに遭遇してしまい、すごく困りました。
具体的にはMacOSで開発したコードをCircleCIで動かそうとしたところ、テストでコケてしまう、という状況でした。
CircleCIに送っていた開発途中のコードでは問題なかったのですが、途中で大幅な仕様変更が入ったためまとめてコードを書いたところ、急に通らなくなってしまいました。
何度送っても手元(MacOS)ではテストに通るため、CircleCI側のキャッシュか何かだろう、と思っていましたが、いくら待っても収まらないため原因究明を行いました。
Dockerでの開発
今回の件はDockerを使って開発を行っていました。
MacOS上で自作のDocker(といっても公式のDockerイメージベース)イメージを作って、それで開発をしていました。
Docker上でPHP、MariaDBなどを動かすdocker-compose
を作って、そこでまとめて動かしていました。
また最近では、CircleCI上でもDockerがほぼ再現されて動くようになりました。そのため開発で使っているDockerとCircleCIで検証させたいDockerを一致させることができるようになりました。
今回の開発ではこのdocker-composeファイルをもとにCircleCI用のコンフィグを作って動かしていました。
(ところでこのCircleCI上で動くDockerですが、とても動作が早く、あまり制約が無く、実際に使うにあたってとても便利です。すごく助かっています!)
当初は良かったのですが、途中の大きな仕様変更の際、テストが動かなくなってしまっていました。
「まぁ手元でも修正途中まで動いていなかったし、CircleCIは素晴らしいキャッシュ機構があって、それが効きすぎちゃってるからテストにこけちゃうんだろう。そのうち直るだろうし、修正は早く取り込まないとだめだし放おっておこう」と思っていました。
ですがいつまでたっても解決される見込みがありません。
その後のコミットでもテストは失敗しつづけてしまって、本来チェックするべき箇所がチェックされなくなってしまいました。
原因の発見
というわけで渋々ながら詳しい原因究明を行うことになりました。
CircleCIではビルド時にSSH接続を有効にして、接続して試験することができるモードがあります。
TravisCIとちがってCircleCIのすごく大きな利点だと思っていますが、今回もこれが役に立ちました。
SSHで接続してログを確認してみました。今回はCakePHP3で開発していたので、失敗したときのエラーログが logs/cli-error.log
に格納されていました。
下記がログを確認したときのコマンドです。
$ ./vendor/phpunit || tac logs/cli-error.log | head -n 50
するとSQLのエラーがでていることが確認できました。
2017-10-30 21:38:50 Error: [Cake\Database\Exception] SQLSTATE[42S02]: Base table or view not found: 1146 Table 'db.Kitchens' doesn't exist
どうやらここがエラーのようです。
table.Kitchens
というテーブルにアクセスできない、ということですが、なぜだかわかりません。
試しにmysqlコマンドで繋いでみました。
$ mysql -h127.0.0.1 -uroot -pexample db -e 'select * from db.Kitchens;'
ERROR 1146 (42S02) at line 1: Table 'db.Kitchens' doesn't exist
エラーになります。
テーブル名を小文字にしてみました。
$ mysql -h127.0.0.1 -uroot -pexample db -e 'select * from db.kitchens;'
通りました!(゚∀゚)
原因はテーブル名が大文字になってしまっていたことでした。
CakePHP
今回の開発ではCakePHP3を使っていました。
CakePHP3を使うとDB廻りにあまり気を払うことがなく、PHPのコードを書くことが出来ます。
ですがフレームワークで補足してくれているSQL文についてわからなくなってしまいます。
今回は大型の修正があった際にテーブル定義の大文字小文字が混濁してしまったことにより、CakePHP3から発行されるテーブル名が大文字なったことが原因でした。
具体的にはテーブルクラスのsetTable()
で定義されるテーブル名が大文字になってしまっていたことが原因でした。
$this->setTable('Kitchens');
この箇所を小文字に直すことで解決出来ました。
もちろんですが、「こういう問題があるからCakePHP3使うな!」とかではなくて、たまたま今回はこういった問題にあたっただけで、CakePHPは便利なフレームワークで、書いていて楽しくてすごく好きです。
lower_case_table_namesが効かない
MySQLではテーブル名が大文字で発行された際、小文字に変換する機構があります。
lower_case_table_names
という名前の変数を1、2に設定することで強制変更や変更しない、といった設定を切り替えることができます。
1: テーブル名はディスク上に小文字で格納され、名前比較では大文字と小文字は区別されません。MySQL では、保存およびルックアップ時にすべてのテーブル名が小文字に変換されます。この動作はデータベース名やテーブルエイリアスにも適用されます。
これまでMySQLを使ってきてこの設定を行ったことはありませんでした。
変数を設定する注意点として下記のように書かれていました。
lower_case_table_names=1 を全システムで使用してください。これの主な欠点は、SHOW TABLES または SHOW DATABASES を使用したときに、元の大文字または小文字で名前が表示されないことです。
Unix 上では lower_case_table_names=0 を、Windows 上では lower_case_table_names=2 を使用してください。これでデータベース名とテーブル名の大文字と小文字の区別が保持されます。この欠点は、ユーザーのステートメントが、Windows 上で正しい大文字または小文字でデータベース名およびテーブル名を常に参照していることを確認する必要があることです。大文字と小文字が区別される Unix にステートメントを転送する場合、大文字と小文字が正しくなければこのステートメントは機能しません。
この設定は、Windows、Linux、MacOSでファイルシステムの挙動が違うことを吸収するために行われるようですね。
ですが小文字になってしまうというデメリットもあるそうで、基本はOSに合わせて0、2に設定するのことです。なるほど。
実は今回はホストOSがMacOSだったため、大文字小文字を区別しないHFS+が原因だったようです。
DockerをMacOSで動かしていて、データ永続化のためにMySQLのデータファイルをホスト側にバインドしていたため、Docker上のMySQLのイメージがMacOS側のファイルシステムに依存してしまいました。
MySQLではテーブルの実体をファイルに保存しています。(設定によりけり)
そのテーブルの実体にアクセスする際に小文字でも大文字でもアクセスできてしまいました。
Docker上のMySQLのOSはDebianでしたが、大文字、小文字ともにアクセスできてしまいました。
今まで「Dockerだったら環境を再現できて便利!冪等性最高!」と思っていましたが、なるほどこういう問題もあるんですね。
準仮想化(ではないですが)だと起こってしまう問題ですね。
他の環境(HomeBrew, Vagrant)でも起こる?
今回はDockerで起きた問題でしたが、同様の問題はHomeBrewで入れたMySQLでも起こると思います。
Vagrantの場合にも起こる…、かもしれないですが検証していないです。でも運用方法によってはおこらないかもしれないですね。