同僚の退職にともなって、彼がしていたサーバの管理を引き継ぎ早半年。

未だ完璧初級者のおいらがこんなに泥縄で業務やってていいんだろうかと思いつつも、後任が入るないしは外注するという話もないし殆ど首っ引きでいろいろ試しているところ。最近は少し糸口がつかめてきたし、プログラムの対応含めて徐々に冗長性を確保できるような構造になりつつあるので、今の2サーバ体制(APサーバとDBサーバ)でどこまでアクセスを捌けるか!というチキンレースみたいな状況からはそのうち抜け出せるんじゃないかと期待。あくまで期待。とりあえず近日中に静的ファイルの分散およびAPサーバのI/O対策を目的としてリバースプロキシを導入できる…はず。まだ、Symfonyのプラグインに関する問題が解決できてないけど。

リバースプロクシの内側で Location: ヘッダーの http/https を書き換えたい - 音ログのヒント
業務を引き継いで一番最初にやったことはといえば、サーバの状況を技術者以外の人間に見えるようにすること。これはサーバがいかに苦しいかを示すことで、「重いじゃねーかこの野郎!」という社内の非難を和らげるためでもあるんだけれども、まぁ実際にはお手軽な方法でヘルスチェックできた方が僕も楽って言う話。

実際、コマンド叩けば状況なんてすぐにつかめるんだけど、数字の羅列って意外にわかりづらいんでね…データの蓄積もないしね。今はFlashで全てグラフ化できてる(Cronで収集→SQLiteに格納→グラフ閲覧時にXML書き出し→Flashで描画)ので、やばそうなとき、やばそうな点がある程度見てわかるようになった。

関連

【メモ】サーバ負荷計測まとめ(結局、sysstatインストールした)



その後、熱心にやったことは、ページキャッシュを全てmemcachedに載せること。それまでは全てSmartyのキャッシュ機能を使っていたんだけど、サイト全体をそれで運用するにはI/O面での負担が大きすぎるように感じられたのでそれは止め、memcachedを使うことにした。この辺は多分PEAR::Cacheとか使っても同じことだと思う。SmartyのキャッシュがI/O的に負担が大きいという単純な話では無しに、限度ってもんがあるっていうことかと。

途中でmemcachedの設定ミスで容量が64MBしか確保できていないことが発覚し驚愕したけれども、きちんと設定し直して現在は8GB中3GBを割り当てて動作してる。3GBという数字の妥当性については結構ギリギリの微妙なところだけれども、前述のグラフの推移を参考に、平均的なメモリ使用量(memcachedを使うようになってメモリの使用量の推移が安定した)、ピーク時のメモリの増え方などを参考に、SWAPしないギリギリを見極めてるつもり。もしSWAPするようなら、安定動作していた2GBとの間で設定を考える。



で、最近熱心にやってるのは、ページキャッシュをmemcachedへ載せるのを止めること。その前の取り組みと矛盾しているのだけど、サーバ数が限られている状態で無数に作られ得る動的ページのページキャッシュをmemcachedに載せ続けるのは無理があった。ていうか2GBではとても足りなくて、むちゃくちゃ漏れまくってた。

代わりにしたのはキャッシュの粒度を見直して、もっと細かい単位でキャッシュすること。サイトの内容がいわゆる「カタログページ」で、ページ数が無限に増えたとしても表示される商品の数には上限がある(=キャッシュの最大値が割り出せる)。ということで、memcachedに載せるキャッシュは商品単位で行うこととし、ページ表示に関する負荷はクエリを徹底的に単純化することで減らす方向に(ただし、検索などクエリの単純化が難しいページは暫定的にページキャッシュを使用している)。結果としてDBへのアクセスが増えることになって、その辺のバランスが難しいところではあるのだけれども、極端に重いクエリを投げさえしなければ今のところは影響は少ないと思われ。

あと、情報が1対1で更新頻度が極めて少ないようなデータについても、全てmemcachedに載せている。ただあまりに更新がないので、むしろmemcachedっていうか永続化を考えてTokyo Tyrantとかに移行しても良いような気がしてる。メモリの節約にもなるし。

またmemcachedを利用しているのは商品データだけではないので、商品データに関するキャッシュだけ別サーバないしは別プロセスに分散させることも考えてたり。負荷軽減というよりかは管理の問題、影響を分けるという意味合いなのだけれども。



そういえばこの間発見した重要なこととしては、SQLiteの更新クエリが思いの外ボトルネックになっていた。あるページが更新されるごとにそのページの中の一部情報を格納していたのだけど、その更新に3秒とか掛かるときがある(クエリは単純なUPDATE。挿入する要素もINT2つしかない。インデックスは当然付いてる)。重いのは知ってたけどまさか秒単位で時間が掛かってたとは知らなかった。

更新中はDBがロックされるわけなので、そのデータを読み出しているページでも同様の遅延が発生する。プログラムの性質的に、トランザクションでどうにかなる問題でもなかった(1プログラムに付き1更新クエリしか発行されない)ので、大人しくSQLiteを諦めてMySQLに移した。もうちょっとなにがしか工夫すれば上手くいったのかも知れないけれども…やはりSQLiteは更新しつつ参照するような用途には向いていないのかも知れない。更新と参照のタイミングを分けられれば、参照は高速なので便利なんだけれども(その用途では使用している)。



そういうわけでいろいろ試みてはいるものの、実は、ピーク日のピーク時にはとてもアクセスできるような状況ではないってのが現状。技術者として情けない限りだけど、現状すぐに改善できるような状況にないので謝りつつ前進していくしかない。目標値は現在の3倍当たりに置いているので、それを捌くためのシステムを構築するのが現在の最も優先的な仕事かなと思う。なんとかサーバ増設の予算も出せるような状況になってきたっぽいしね。できればあと3台くらい欲しいなぁ…