最近ローカル環境でMySQL 5.7 + Railsの開発をしていると、たまにMysql2::Error: Lost connection to MySQL server at 'reading initial communication packet'
というエラーが出て困っていた。これについては、mac osx におけるファイルディスクリプタの上限 | ++頭道++を見ると、table_open_cache=400
と設定することで起きなくなるとされている。
ただ、いまいちその原理が分かってなかったので軽く調査してみた。この辺りについては自分は詳しくないので、正確性の保証はできない。
なぜLost connection to MySQL serverのエラーが起きるのか
tail -1000 /opt/homebrew/var/mysql/$(hostname).err | grep Warning
というコマンドを実行してみると、次のエラーが出ていたため、やはりFile Descriptorの問題のようだ。
2022-12-12T02:00:32.340845Z 0 [Warning] File Descriptor 1072 exceeded FD_SETSIZE=1024
FD_SETSIZEは固定で1024*1となっていて、これを超えるほどtable_open_cache用のファイルが開かれていると、次のコネクションが貼れなくなる。それにより、Mysql2::Error: Lost connection to MySQL server at 'reading initial communication packet'
というエラーにつながるようだ。
MySQLのデフォルトではtable_open_cacheは2000となっていたため、コネクションをずっと受け付けていてしばらくすると1024以上のFile Descriptorを開いてしまい、するとコネクション自体も繋げなくなる状態になっていたらしい。
この辺りの話は https://dev.mysql.com/doc/refman/5.6/ja/table-cache.html にも書かれている。
オペレーティングシステムで、table_open_cache の設定に示されたオープンファイルディスクリプタの数を処理できることを確認してください。table_open_cache の設定が大きすぎると、MySQL がファイルディスクリプタを使い果たして接続を拒否し、クエリーの実行に失敗して、信頼性が大幅に低下します。
table_open_cache=400でどうして直るのか
上の通り、FD_SETSIZE以上のtable_open_cacheが設定されていると、どこかのタイミングでFD_SETSIZEを超えたファイル数が開かれるので、コネクションすら繋がらないという状況になってしまう。一方、table_open_cacheはパフォーマンスのためのキャッシュなため、減らしたとしてもローカル開発においては基本問題ない。
そこでFD_SETSIZE以下になるようにtable_open_cacheを制限すれば、ひとまずコネクションが繋がらないという問題は起こらなくなるようだ。
疑問点: Macのulimitの-nの値は256なので、そちら側でエラーになったりしないのだろうか
これを調べていて思ったのは、Macのulimitの値は256なので、そちら側の方でエラーにならないだろうかという疑問だった。
$ ulimit -a -t: cpu time (seconds) unlimited -f: file size (blocks) unlimited -d: data seg size (kbytes) unlimited -s: stack size (kbytes) 8176 -c: core file size (blocks) 0 -v: address space (kbytes) unlimited -l: locked-in-memory size (kbytes) unlimited -u: processes 5333 -n: file descriptors 256
以下のようなRubyコードを書いて試すと、確かにたくさんファイルを開いて256あたりまで行くと、Too many open filesで死ぬ様子が観察できる。248くらいで死んでいる理由は、STDOUTなど別のfdも開いているからであろう。
files = [] (1..(2**10)).each do |i| puts i files << File.open("#{i}", 'w') sleep 0.1 end
245 246 247 248 ~/junk/2022-12-12-224052.rb:4:in `initialize': Too many open files @ rb_sysopen - 248 (Errno::EMFILE)
少し調べてみると、ulimitはプロセスレベルで変更することができるらしい。そのためMySQLのプロセスではこの値が変更されていそうだと推測した。
https://dev.mysql.com/doc/refman/5.6/ja/server-options.html#option_mysqld_open-files-limitに書かれているように、open_files_limitの値を変更するとsetrlimitによって利用できるFile Descriptorの数が変更される。デフォルトのopen_files_limitは5000となっていてFD_SETSIZEよりも大きい。そのため、ulimit側では制限に引っかからず、FD_SETSIZE側の制限に引っかかっていたようだ。
実際にMySQLのプロセスでulimitの値が何になっているか確認したくなったため、https://hacknote.jp/archives/2173/ を参考にしつつprocfsから確認しようとした。しかし、Mac OSXはprocfsでなかったため、確認方法がさっとわからず、ひとまず調べるのをやめた。何か良いやり方があれば知りたい。
ここまでで、ulimit側でエラーにならず、FD_SETSIZE側でエラーになっていた理由がわかった。
MySQL 8.0では解決されているっぽい
https://bugs.mysql.com/bug.php?id=79125 のスレを見ていると、最後の方に以下のようなコメントがある。
We fixed this in 8.0 by introducing the use of kqueue. This build on another change in the code, which will make the backport to 5.7 non-trivial and inherently pose some risk. So we are reluctant to backport to a GA version.
FD_SETSIZEはselectシステムコールの制限なため、kqueueを使ったfd監視に変えることで制限がなくなったようだ。
まとめ
今回は「ローカル開発環境でMySQL5.7を使っているときにtable_open_cacheを少なくすると、なぜLost connection to MySQL serverが起きなくなるか」という疑問が沸いたので、少しだけ深掘りをしてみた。最初に言った通り、この辺りについては自分は詳しくないので、正確性の保証はできないが、何かの参考になれば。
参考資料
- mac osx におけるファイルディスクリプタの上限 | ++頭道++
- https://qiita.com/fujinochan/items/2337ce48a998cf67966b#comment-dced009fa3d659c288bb
- https://dev.mysql.com/doc/refman/5.6/ja/table-cache.html
- https://dev.mysql.com/doc/refman/5.6/ja/server-options.html#option_mysqld_open-files-limit
- https://bugs.mysql.com/bug.php?id=79125