だるろぐ

だるいぶろぐです

3000req / sec と戦う

  • ざっくり概要
    • ピークで3000req / sec
    • 毎分コンテンツ更新要求
    • コンテンツ更新の際は他所からデータをapi経由で受け取る
    • コンテンツ更新にはTheSchwartzを使用

なコンテンツを色々してきたログ。
尚、ここに書く技術は大半が周囲のギークな方々にサポートしてもらったもので、僕自身が何かしたわけではない。残念すぎる。

構成

internet -> www(squid -> apache) -> app(memcached -> app) -> db
  • フロントエンド

wwwサーバがapachesquidを動かしている。apacheがリクエストを受け、squidのキャッシュが有ればそれを返し、無ければバックエンドのappサーバへproxy。

  • バックエンド

appサーバがmemcachedとアプリを動かしている。


それぞれ冗長化してるけど、リクエスト数の割に台数は少ない。
技術があれば台数なんて少なくて済むと学習。

apache

  • 何はともあれkeep alive off

画像配信サーバでもない限り、これはoffが望ましい。

  • mod_deflate でコンテンツ圧縮

転送量を抑える。

  • メリット
    • 転送量が減る
  • デメリット
    • サーバのcpu負荷が上がる

今回はメリットの方が勝ったので導入。一気に減った。

squid

そもそもは各wwwのsquid同士がsiblingしていたが、これをやめた。効率悪かった。
siblingしたら、自分以外のサーバのキャッシュも使えるけど、いちいち通信するのが難。
localhostsquidしか見ないようにした。

app

ギークに色々教わりつつ、ロジック変えたり、memcachedに突っ込む個所を増やしたり。

nginx

あかんapacheでは捌き切れん。nginxならどうだ。
というわけでapacheやめてnginx導入。

400 bad request

nginxのログに 400 bad request なログが延々溜まる。
ぐぐってこちらを発見。
http://limilic.com/entry/930zlf7u95d876yg
パッチを適用。上記のurlはnginxのバージョンが古く、現在のheadに当てるには

 if ( error != NGX_HTTP_CLIENT_CLOSED_REQUEST_LINE ) {

 if ( rc != NGX_HTTP_CLIENT_CLOSED_REQUEST_LINE ) {

に。パッチ適用したところ、直る。

重複enqueue

TheSchwartzにenqueueする際に、今まで同じコンテンツの更新要求を重複して投げてしまっていたので、やめる。

  • memcachedを使用
    1. enqueue前にmemcachedにunique keyをaddする
    2. expireは適当に5分とか。5分もありゃ処理されてんだろ、と。
    3. 次のenqueue時にaddが失敗したら、既にenqueueされてるものと見なし、enqueue中止。

これで大体よかったんだけど、もっとちゃんとする。

  • TheSchwartz自体にそういう機能あるじゃん
    1. enqueue時に uniqkey ってカラムにunique keyを指定

これだけ。


ついでに。
TheSchwartzの仕様では、enqueueする際には select ... limit OFFSET NUMBER なクエリを発行する。
とても無駄です。
enqueue減らすのは大事。

更新処理

これはこちら側の話ではないけれど。
これまで更新要求の処理は、

  • 他所にリクエストを投げる
  • 待つ。待つ。
  • jsonで結果を受け取る

だった。で、このリクエストも思い。そんだけの処理をしているので(当然キャッシュ機能はある)
相談したところ、何とコールバック形式のapiを用意してくれた。凄い凄い。
それに合わせてこちらも非同期でコンテンツ更新を行うように。

LWPからFurl

apiを叩くときはLWPを使っていた。
駄目だもうLWPじゃ遅い!となったのでしれっとFurlに。
改めて、どんだけ過酷なんだこのアプリは…と思い知る。

worker調整

Parallel::Prefork を使ってworkerを作っていた。で、

  • max_workers をとても減らした
  • termするまでworkする回数をとても増やした
    • apacheでいう MaxRequestsPerChild

つまり一度生成したプロセスは非常に多い回数jobをこなしてから死ぬ。
あまり頻繁にforkしてプロセス再生成してたら負荷になるし、今回はそんな必要もないので。
今回のように延々とjobが溜まるような場合は極力forkを避けるべし。

thundering herd対策

普段は安定してるのにたまに急激に負荷が上がる。色々コード見たり考えたりした結果


俺「フロントのキャッシュが切れて、バックに行って、キャッシュ作ってフロントに返してキャッシュされる前に、同じ様にバックにリクエスト沢山行ってんじゃね?」


多分これ。そしてこれを thundering herd と言う(ことを教わった)
で。

  • バックエンド(memcached)のキャッシュ時間 > フロントエンド(squid)のキャッシュ時間 にした
  • バックエンドのbacklogを減らした
    • チューニングでは増やすことの多い値だけど、今回のケースでは多いと死ぬ
  • やばいときは割り切ってリクエスト絞ってフロントのキャッシュを生成するのを最優先する
    • そうでもしないと続々同じようにバックに行くリクエストが増える。落ちっぱなしより、ちょっと落として元に戻す方がずっといい

squid

あるデプロイの後からsquidのメモリ使用量がおかしい。設定値以上に使ってる。
具体的には cache_mem で指定した量より、topで見て計算したsquidのメモリ使用量の方が多い。
どんどん増える。最終的にはfreeが50M切った。ギリギリだった。

原因多分これ。
http://squid-web-proxy-cache.1019090.n4.nabble.com/Bug-2973-Memory-leak-when-handling-pathless-http-requests-td2276286.html
というわけでギークな方がさくっとパッチ当ててrpm作ってアクセスの少ない時間帯に入れ替えという神対応をやってのけた。
ジェバンニと呼んでいいですか。

204 No Content

で、そもそも何でsquidがそうなったのかというと。原因となったデプロイで行った変更が、

  • ある非同期なリクエストをFurlで行った際のレスポンスを 204 にして、contentは空文字列を返すようにした

だった。どうやらsquidさんがこれをキャッシュしてしまった様子。
200にして小さなcontent返すように変更。

mysql 5.5

リクエストの度にログをDBに非同期で取っていて、そのデータは用が済んだらdeleteしている。のだが。
deleteが遅い!遅すぎる!別に1日分溜めて早朝に一気にdeleteでもいいんだけどもっとさくっとしれっとやりたい!
そうだpartitioningしてpartitionごと消せば早いんじゃね!(ピコーン)
5.1でもできるけどどうせなら5.5使おうぜ人柱的な意味で!
すいません動機としては新しいもの使ってみたかったのが大きいです。

partitioning

http://nippondanji.blogspot.com/2010/12/mysql-55.html とか参考にしつつRANGE COLUMNSでパーティション作成。
今月分作成、試しに来月分作成…何かエラー。

  • 原因: partition pmax values less than (maxvalue) 的なのを作っていたから

partitionの範囲が固定ならともかく、日毎のログみたいに際限なく増えていくならこのmaxvalueは作ってはいけない。


そして最初に作成するときは普通にcreate文でいいけど追加するときは ALTER TABLE table_name ADD PARTITION で。

utf8mb4

mysql 5.5では utf8mb4 とか使えるそうなのでせっかくだから使っとこうか。別に困ってはいないけど。

準同期レプリケーション / Semi-Synchronous

というのも使えるそうなので使ってみた。

  • master
 INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
 SET GLOBAL rpl_semi_sync_master_enabled = 1;
  • slave
 INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
 SET GLOBAL rpl_semi_sync_slave_enabled = 1;

が。
「それは多少遅くなるから、今回みたいに沢山アクセスがあってしかもすぐに応答を返さなきゃいけない場合には向かないと思うよ、課金とかなら向いてるかなあ」
すいませんメリットばっかり見てデメリット考えてすらいませんでした。
というわけで無効に。

  • master
SET GLOBAL rpl_semi_sync_master_enabled = 0;
  • slave
SET GLOBAL rpl_semi_sync_slave_enabled = 0;


そうそう5.5用のmy.cnf書いたのもジェバンニさんです。


この辺から、このサイト運用に直接は関わらないけど教わったり学んだこと。

クエリパラメータにバイナリデータ

を送りつける攻撃手法があるそうで。
どうなるかというと、もしサーバ管理者がroot権限で tail -f してたら任意のコマンドを実行され以下略。
怖い怖い。

静的コンテンツのキャッシュ

サイト上に置いたcssやらjsやらをクライアントにクライアント側のキャッシュを無視して強制的に更新させたいら、サーバ側で適当に hoge.css?123 とか、適当なクエリをつければいい。
が、「このタグ貼って使ってね!」な場合にはこれが使えない。
もしそういうことをするなら、cssやらjsを変更する場合は、互換性を維持する事。
世界中のクライアントからキャッシュが消えたと判断できたなら維持しなくてもいい。
つまりもしそういうことするならちゃんと考えましょうねという話。
今回ちょっとjsファイルの仕様を変えたけど、1週間くらいは、キャッシュされたjsが使われたログがあった。案外長いものです。

swapが溜まる

偉い人が手を付けたサーバでは発生しなかったswapが、僕があれこれいじったサーバでは発生する。
どう見ても僕が原因です本当に申し訳ありません。
とはいえ、そこまで気にする程でもないらしい。

vmstat 1 を実行して、si, so (swap in/out)が多く出てなければ大丈夫かと
http://memo.officebrook.net/20080418.html これを 0 にするというチューニングもありますが..

と。別に目の敵にするほどではないそうな。
swapをクリアしたいなら

swapoff -a && swapon -a


で。関係有るか分からんけど、もしかして原因は /dev/shm を使っていたから?
でかめのarchiveを操作するのにディスク上でやりたくねーなーと思って、ここを使ってそのままにしていた。
試しに使ったファイル全部rmったらswapが6Mに減少。
いやだってtmpfs使ったらはえーんだもの…

damontools

これまでdaemontoolsを使うときはこうしていた。

 exec setuidgid hirafoo \
   start_server --port $port -- \
   plackup -a server.psgi -s Starman --port $port --workers 1 2>&1

が。

それだとサーバ落とさずにworker数変えられなくね
start_serverにはshell scriptを渡して、その中で設定べた書きにするといいよ

と教わった。一つ賢くなった。

start_server

書き換える最中に、start_serverの使い方をちゃんと学ぼう としたけど早速躓く。
俺「他の人のstart_serverの使い方見てみたんですけど、引数の中の -- の意味が start_server のmanにもソースにも Server::Starter のperldocにもソースにも載ってないの!」
 「それ Getopt::Long の機能だよ」
すみませんでした。惜しかったね俺。

関係ないけどORM

どうも人は皆DBIに回帰していくらしい。

  • 俺のここ数年の変遷
    • DBI?へーこれ使うんだー
    • ORM?おお楽だなこれ
    • DBICは重いし黒いから色々軽いの試してみるか
    • 駄目だ重い…結局DBIのハンドラ取ってきて使いまくってる…
    • もうDBIでいいや

mysql

俺自身に5.5の運用実績も知識も無いのに「5.5に変えたい!」なんていう無茶が実にあっさり通ることに感謝しつつ、さっそく躓いてる俺。
移行の手順は

現DB(M) -> 現DB(S) -> 新DB(M) -> 新DB(S)

レプリケーションし、アプリは現Mをmasterとし、それ以外をslaveとする。どっかのタイミングでアプリを一旦停止、様子見て新DBしか見ないようにする、と。
で、新DBは投入前は色々いじることができた。素敵なのは新DBにalter投げてもレプリケーションが壊れない事。
あんなindexやこんなalterを投げることができる!!!いや別にそれまでだって早朝に投げればまぁいけたけど。
で。mysqldを色々再起動するたびに気付いたのが、新DBMでstop slaveしてからmysqldを停止しても、起動したら勝手にレプリケーションが走る。
いや別にいいっちゃいいのだけど。本番投入したらまず再起動しないだろうし、したとしてもその頃には現DBなんて無くなってるから。
とはいえ気持ち悪いので調べるも頭が足りず分からなかったので助けて。

skip-slave-start とか reset slave とか
でもstop slaveしたならmysqld再起動しても勝手に走らないと思うんだけどなあ

ええと…

[あとでやる」


こんなところか。
もう一度言うと実際にあれこれ教えてくれたり助言してくれるのが周囲のギークな方々で、俺一人じゃどれも出来てない。
そんなこんなで楽しく過ごしている。