gallu’s blog

エンジニアでゲーマーで講師で占い師なおいちゃんのブログです。

DB handle周りのチューニング

某所というかまぁFacebookでなのですが。

接続数が多くて「too many connections」エラーが発生する場合。
PHPのプログラム(バッチファイル)側として何か対応すべきことや、
注意すべきことなどありますでしょうか?

という、大変に有意義で結構質問のありそうなあたりをいただいたので。
せっかくなので、Blogネタで書かせていただきました。


さて早速。
とりあえず直線的に状況を考えると、このエラーメッセージは「なんか接続数多すぎてさばけないヨ!」っていうMySQLからのクレームです。
悲鳴、って読み替えてもOK。


先に対象外である「PHP以外の角度からの」解法としては
・max_connections を増やして、もっと沢山接続できるようにする
って方法があります。
あんまり無茶な数値にすると「DBサーバが落ちる」とかいう悲劇に直結しますので、サーバのリソースをしっかり監視しつつ、丁寧に、時には大胆に、増やしてみてください。
デフォは100ですが、個人的には500くらいまでは普通に。人によっては「1000くらいまではいけるでしょ」とかって感じの数値感覚なので、そんな感じで調整をしてみるとよいでしょう。


さて、PHP…に直結するまえに「かぶる程度」の所を。
例えば、こんなケースがあります。
Apacheを「なんもチューニングせんと」使ってると、MaxClients ディレクティブのデフォは256になります(多分バージョンによって違うのかしらん? おいちゃんの記憶は些か古いので、最近のデフォを正確に把握したい場合、各人でご確認くださいませ)。
MaxClients ディレクティブってのは「Apacheが作る子プロセスの最大数」なので、つまり「同時に処理できるリクエスト数」と等しくなります。
httpのリクエストは実際には「HTMLやら画像やらCSSやらJSやら」色々あるので「全部PHP」って事はまぁないのですが。
なんかの偶然で「一気に150人アクセス」来たと仮定します。
そうすると、Apacheは150リクエストはさばけますが、その150PHPファイルが同時にDBアクセスをしようとすると、MySQLのmax_connectionsはデフォが100なんで、パンクします。


…なんていうケースは割とあるので。
Webサーバの数*MaxClients ディレクティブの値、あたりから「max_connectionsの必要数」は、計算して設定しておくと心やすいですね。


ここからもう少し「PHP」に入ってきます。
プログラム側で出来る事はあまり沢山はないのですが、とはいえポイントはいくつかあります。


まず「DBを触らないプログラムでは、DBにconnectしない」というのがあるのですが…実際の所、経験的に「99%以上のファイルがDBを触る」ケースがほとんどなので。
細かいプログラムテクニックやら設計テクニックでその辺をやってもいいのですが、今ひとつ「労力に見合うほどの旨みがない」かなぁ、と、個人的には思っています。
なので「やってみてもいいけどあんまり旨みはないかもしれない」って感じですね。


次に、pconnectなどの「持続的データベース接続」ですが、個人的にはあまりお勧めしません。
少なくともApache(で、MPM preforkの場合:PHPならそうですよねぇ)の場合、持続的データベース接続では「プロセス毎に接続ハンドルが紐尽く」ので。
上述の例の場合、MaxClients ディレクティブが256なら基本的には「MySQLのmax_connectionsも256(以上)」ってのがほぼ絶対に必須になります。
ただ、Apacheの子プロセスが「全部PHPと関わっているか」ってぇと、そうとも限らんので(画像やらcssやらjsやらHTMLやら、その辺のリクエストを捌いてる事も多々あるので)。
その辺を考えると、多少接続コストがかかっても「持続しない接続で、使い終わったらとっとと切る」ほうが、connection数がパンクしないんじゃないかなぁ、と思われます。


じゃぁ「nginxでphp-fpmの時は?」って話になるのですが。細かくいくと「pm.max_children の値次第」にはなるのですが。
こちらは間違いなく「PHPが動いているプロセス」なので、こちらの場合は「持続的データベース接続もありなんじゃないかなぁ」と思います。
ただ、php-fpmのプロセス数の最大とMySQLのconnection数はちゃんと確認しておきましょう。


この辺までが、割と「真っ当な」領域。
ぶっちゃけますとこの辺までで引っかかっている場合、よっぽどビジネスロジックにミスが無い限り「相応のアクセスがある = 相応の収益がある(はず)」なので、よいサーバにするなりサーバの台数を増やすなり、クラウドならよりよいインスタンスにすげ替えるなり、順当な方向で進めます。


以下、ちょっとまずい、でも「やりがちな」ケースについて。


接続の方法にもよるのですが、例えばPDOなんかですと「1プログラム内で3回呼んだら」1プログラムで3接続使います。
なので、当然ではあるのですが「1プログラムでは絶対に1接続を使い回しましょう」。これ、案外ミスりやすい所です。
プログラム的には、こーゆー時こそ「シングルトンパターン」をがっつり使うとよいですね。
MySQLだと

show status like 'Threads_connected';

というSQLで接続数確認できるんで、色々試してみましょう。


ちなみに試して分かったのですが、PDOのコンストラクタで第四引数に「array(PDO::ATTR_PERSISTENT => true)」をいれると、1プログラムでn回callしても、接続は1つでした。
多分「持続的データベース接続」のロジックに従って、同一プロセス内なので「ハンドルを使い回してる」んだろうなぁ、と予想されます。
ただ「何回接続してもハンドルを使い回す」目的でコレを使っても本末転倒なので、シングルトンパターンなりなんなり、ちゃんと「随所に関所( http://d.hatena.ne.jp/gallu/20080225/p1 )」するほうがよいと思われます。


次に割とあるのが「えらいこと重たいSQLが流れていて、DB connectionがつかまれっぱなし」なケース。
1つ2つならよいのですが(でもあんまりよくない)。大体そーゆーのって、じわりじわりと勢力を伸ばしてくるので、気付くと「connection全部取られてた orz」みたいなケースが発生します。
この場合は「重たいクエリはいねが〜(@ナマハゲ)」てな感じで探し出してみるとよいでしょう。
MySQL slow query」あたりのキーワードでググると、情報が山盛りです。


で…今回「バッチ」って記述があったので、バッチの可能性があるので、そのあたりを言及しませう。


一つ割とあるのが「ある時間に集中してバッチが走る」状態。
特にバッチは「数分とか数時間とか」長い子が多いので、startはずらしても「あるタイミングで重なってる」ケースがあります。


これに加えて「バッチの中で、ちゃんと1バッチ1DB handleが出来てるか?」というあたり。
httpアクセスは言っちゃうと「数秒(普通は1秒以内くらい)」なので、1アクセスで5connectionはったって「ばれない可能性」があるのですが。
バッチは「長い」ので、1バッチで2 connection以上つかまれると、痛さが違います。


また、バッチはどうしても「重たいクエリが流れやすい」ので、その辺も割と「connectionを握りやすい」ところなので。
slow queryも、しっかりチェックしておきましょう。


あとは、frontとの合わせ技。
んと…例えば「うちApacheでデフォ設定だから、MaxClients ディレクティブが256だからMySQLのmax_connectionsも256」ってやると、コケる可能性があります。
状況としては「Apacheから256アクセスきているタイミングでバッチが5本走った」ら? 5connection分、足りないでしょ?


なので、上述でも「MaxClients ディレクティブが256なら基本的には「MySQLのmax_connectionsも256(以上)」と、わざと「以上」って単語をつけましたが。
バッチの併走分もちゃんと考えて、余裕のある数値にしておきましょう。


なので、全体をまとめると。
・1プログラムでは「1connection」になるようにちゃんとプログラムを書く
・重たいクエリがないかチェックする
あたりがPHPプログラムに直結する所で、後は設定値とか「バッチの併走本数」とか、その辺をチェックするとよいのではないかなぁ、と思われます。


取り急ぎ書いたので思い出せるところまで。
また「あぁそういえばこんなのも」的なものがあったら、追記しますが、突っ込んでいただける方におかれましては突っ込んでいただければ幸いでございます。