WordPressのデータベース接続エラーを接続オプションで回避した話
つい昨夜の話なのですが、このブログを置いているサーバー※でMySQLデータベースへのデータベース接続確立エラーが発生し、急遽対処したので忘れないうちに備忘録を兼ねて投稿しておきます。
データベース接続確立エラーとはなんぞや
このブログページ(WordPress)を開いたときに、そのものズバリ、「データベース接続確立エラー」と太字でデカデカと表示される現象です。
プログラマー的に言うとMySQLへの接続処理を行ったのにサーバーからエラーを返されたときのメッセージ。
じゃあ具体的になぜデータベース接続エラーになっているのか?というのはログ等を調べても良いのですが、そんなことをせずとも、一般的にはphpMyAdmin等のツールを入れているでしょうし、それで手動接続してみれば具体的なエラーメッセージが表示されるハズです。
ぼくの場合は自前で書いたPHPプログラムもあったので、PDOで接続した際に「Too many connections」というエラーメッセージが返ってきていることがわかりました。
Too many connectionsならほっといても治る
Too many connectionsはMySQLへの接続数が多すぎるよ、キャパオーバーだよ、というお知らせです。
つまり、乱暴な言い方ですが、ほっとけば、そのうち、治る。
実際、最初は10分くらいほっといたら治りました。
…………………………が、その後、数時間経ってから再発し、合計で3回、最後のときは50分以上Too many connectionsが続いたので、これはあかんと思い、対処方法を真面目に考えることにしました。
消極的なToo many connectionsへの対処
まずは思いついたところから、下記のアクセスについてMySQLへの接続をしないようにしました。
- botからのアクセス時
- 不審なURLでのアクセス時
- USER AGENTが入っていないクライアントからのアクセス時
これら3つはWordPressとは直接関係ない話なので、WordPressのみでデータベース接続エラーが出て困っている人はこの項目自体スルーしてください。
ぼくは自前のPHPスクリプトでアクセス解析処理をしているため、WordPressと一緒に別の自作プログラムが動作しており、それが高負荷を引き起こしているのではないか、とまず考えたわけです。
このブログだけでも月に9万pv、1日あたり約3,000pv、少なくとも1分に2ページは開かれている計算です。聞きかじりの知識なので、鵜呑みにしてはいけませんが、PHPからMySQLへの接続は明示的に閉じたとしても60秒間は接続が維持される(場合がある)と聞きます。
であるならば、ページを開くたびにMySQLへの接続と切断が実行されるこのページでは最低でも1分間に2回は接続の被りが発生しているわけです。
もちろん綺麗に1分に2ページずつ開かれるというわけではなく、あくまで平均値なので、場合によっては1分間に20ページ以上同時に開かれるケースもあるでしょう。
更に言うとぼくはこのサーバーで当ブログのみを運営しているわけではなく、複数サイト運営しているため、この数字が数倍、いや場合によっては10数倍に膨れ上がります。
………………………うーん、そうして冷静に考えるとMySQLへの同時接続数は数百単位でもいずれ足りなくなりそうですね。
事実、不審なURLでのアクセス(いわゆるSQLインジェクションを狙ったような不正アクセス)をブロックすることで、自前のアクセス解析プログラムの負荷は軽減されましたが、それでもToo many connectionsのエラーはなくなりませんでした。
積極的なToo many connectionsへの対処
そもそもですね、MySQLから切断しても60秒間接続が残る、という仕様が本当なら対応不可能じゃないですか?
だって、MySQLの最大同時接続数を無尽蔵に増やすことなんて出来ませんし、レンタルサーバーを使っている以上、こちらでその設定を変更することも出来ません。
ちなみにColorfulboxでぼくが借りているサーバーではMySQLの最大同時接続数は501でした。更にオマケ情報として、mixhost(の一番安いプラン)では245でした。どちらもphpMyAdminから確認することができます。
一見十分な数に見えますが、接続が60秒間残る(可能性がある)という話を聞いた後だと心もとない数字に見えませんかね。1分間に500アクセス………はなかなか厳しいにしても、1秒に1回、1分で60回くらいアクセスされることはあるし、それが複数サイトあれば500を超える瞬間すらありそうです。
これは現実的な話ですが、昨夜はどこぞのTwitterでフォロワー数の多い方がうちのブログのリンクを貼ってくださったようで、その影響から一時的にアクセスが増大→データベース接続確立エラーというコンボになりました。
(ログからアクセス元を辿っていって判明)
じゃあどうするのかって話ですが、同じサイトの同じデータベースへ接続しているのだから接続を使いまわせば良いのでは?と思うのです。むしろMySQLがそのくらい勝手にやるものだと解釈していました。
しかし、目を皿のようにしてPHPのPDOのマニュアルを読んでいるとこのような記述が。データベースサーバーへの持続的な接続による恩恵をこうむる web アプリケーションは多いでしょう。持続的な接続は、スクリプトが 終了しても閉じられずにキャッシュされ、他のスクリプトが同じ内容の 接続を要求してきた際にそれが再利用されます。持続的接続の キャッシュにより、スクリプトがデータベースを使用するたびに 新しい接続を確立するオーバーヘッドを避けることができます。 それにより、結果として web アプリケーションを高速化できるように なります。
例4 持続的な接続
<?php
$dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass, array(
PDO::ATTR_PERSISTENT => true
));
?>
おぅふ………知らなかった…………。PDOで接続を使いまわす(≒持続的接続)にはオプションで ATTR_PERSISTENT を指定しなきゃいけなかったんだ…。
WordPressはPDOを使っていない
自作のPHPスクリプトはPDOを使ってMySQLへ接続しているため、先述の例のとおり、
<?php
$dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass, array(
PDO::ATTR_PERSISTENT => true
));
?>
という書き方に変更するだけで完了しました。
しかも効果てきめん(?) この変更をした直後からToo many connectionsが発生しなくなったのです。
よーし、じゃあWordPressのほうにも PDO::ATTR_PERSISTENT オプション付けるぞぉ~、と張り切ってソースを開いたのですが、PDOの記述が見当たりません。
WordPressのデータベース接続部分のコードは下記のとおり。
if ( WP_DEBUG ) {
mysqli_real_connect( $this->dbh, $host, $this->dbuser, $this->dbpassword, null, $port, $socket, $client_flags );
} else {
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
@mysqli_real_connect( $this->dbh, $host, $this->dbuser, $this->dbpassword, null, $port, $socket, $client_flags );
}
※このソースは /wp-includes/wp-db.php にあります。
…え、WordPressはPDOを使っていない…だと…っ!
PHP5.5くらいの頃にmysql関数が非推奨となり、PHP7ではとうとう削除されたので絶滅したと思っていたのですが、mysqliという後継(プリペアドステートエントやオブジェクト指向に対応)が追加されていたのですねー。mysql関数やめるなら移行先は当然PDOやろ、と思っていたからノーチェックでした。
まいったなぁ。mysqliにPERSISTENTのオプションなんてあるのかなー?とまたもやPHPマニュアルとにらめっこしたところ、ありました。ありましたよ。
mysqli 拡張モジュールでの持続的接続
持続的接続とは、クライアントプロセスとデータベースとの間の接続を 何度も作っては破棄するかわりにクライアントプロセス側で再利用しようというものです。 これにより、必要なときに毎回新規接続を作成するというオーバーヘッドが軽減でき、 未使用の接続はキャッシュされて再利用できるようになります。
他の mysql 用拡張モジュールとは異なり、mysqli には持続的接続のオープン専用の関数はありません。 持続的な接続をオープンするには、接続時にホスト名の前に p: をつけなければなりません。
正確にはオプションとして付けるのではなく、ホスト名の頭に「p:」を付けるという、なんとも不思議な指定方法ですが、これがPersistent Connection(持続的接続)となるようです。
if ( WP_DEBUG ) {
mysqli_real_connect( $this->dbh, 'p:'.$host, $this->dbuser, $this->dbpassword, null, $port, $socket, $client_flags );
} else {
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
@mysqli_real_connect( $this->dbh, 'p:'.$host, $this->dbuser, $this->dbpassword, null, $port, $socket, $client_flags );
}
ということで、このとおり、'p:'をホスト名の前に付けることで持続的接続に対応することができました。先述したとおり、このソースはWordPressのwp-includesディレクトリの下のwp-db.phpです。
まとめ
- WordPressで「データベース接続確立エラー」が出た場合はMySQLへの接続でエラーが発生している。
- この場合、ログを確認するか、phpMyAdmin等で手動接続し、より詳細なエラーメッセージを確認すると良い。
- エラーが「Too many connections」だった場合は単にアクセス数が多いというだけの可能性が高い。
- PDOを使ったPHPがあるならATTR_PERSISTENTオプションで接続を使いまわすことで対応。
- WordPressはPDOを使っていないため、wp-db.php内のmysqli_real_connectの接続先ホスト名の前に p: を付けることで対応できる。
相変わらずですが、無駄に長くなってしまいました。
とりあえず、昨夜 Too many connections のエラーが発生しはじめてからまだ20時間くらいなので、これで絶対!とは言えませんが、現時点では問題なく動作しているようです。
ぼくのことだから、すぐに記事として投稿しておかないと絶対そのまま忘れると思ったので早めに備忘録にしてみました。
この記事がどこかの誰かの参考になれば幸いです。
ところで、Colorfulboxやmixhostは本当に軽快なサーバーで、なぜ何年もバリューサーバーで我慢していたのか、アホらしくなるほどWordPressが軽やかになりました。
安価で軽量なホスティングサービスを探しているなら候補のひとつとして検討すると良いよ!