読者です 読者をやめる 読者になる 読者になる

考える×つくる×動かす

主に技術系のことを書いていきます。

ISUCON2014予選に参加してみた

チーム名:「/^o^\マッミョサーン」
最終スコア:26890
チームメイトはid:kazuphと@kokonoka

詳細はid:kazuphが書いていてくれるので割愛します。


ISUCON2014予選でがんばってきた - 僕の車輪の再発明


9月の頭に昨年の過去問を1回4時間くらいやって傾向と対策を練っていました。

基本的なスタンス

・nginxやmysqlのログを見て遅い部分のボトルネックを解消
・インデックスは貼る前と貼った後のEXPLAINをみながら貼っていく
・DBの構造とかを変更
・ロジックで減らせるクエリは極限まで減らす
ミドルウェアに手を入れたりは最後までしない
・最後にキャッシュ出来る部分はキャッシュする


そのおかげで変なエラーが出てベンチマークとか動かない(´・ω・`)とかなくて
基本的には成功法で攻めることができたはず。

やったこと

・全体のロジックを確認
・3回以上ID,PASSを間違えたipアドレスをブロックするipsテーブルを追加
・10回以上ID,PASSを間違えたユーザーをブロックするunlock_usersを追加
・初期で上テーブルにINSERTする初期スクリプトを作成
・usersに最新ログインの日時+IPアドレスを加えた
・login_logを使わなくした
mysqlの設定ファイルを秘伝のタレを利用した
・自分たちで新たに加えたテーブルにインデックスを貼った
・WORLOADが増えるとTCPでTIME_WAITが発生してPORTが専有されてしまうのを解決した

ハマったところ

・login_logのロジックを確認しつつテーブル分割するかキャッシュを使うか決めきれなかった
→これで午前のほぼ時間を費やしてしまった。。
・お昼ごはんのメニュー(結局寿司食べたく出前とった)
・/loginのエラー時にリダイレクトされるのを減らすためにnginxで静的ページを返したかった
MySQLのバージョンアップ(最後で5.6系に変えようと別インスタンスでテストしたけどエラー出て動かなかった)

反省点

・もっとはじめにベンチ前にく初期投入データを見ておくべきだった(これがランダムだと思ってた)
・全体ロジックを確認してUSERSをキャッシュ化してしまうべきだった
・パスワードのHASH処理を始めに計算にしてDBに入れておくべきだった
・nginxでCookieの値を持ってこれるそしてその値を見て静的ページを返せばよかった
・土曜朝じゃなくて金曜朝に海外から帰ってきて万全の状態で挑むべきだった(言い訳ですがw)


今回基本的にはid:kazuphがほとんどコード書いてくれてた。
自分ももっとさくっと書けるようにならないとと反省。


1年強振りのブログ記事でした。
詳細とMySQLの秘伝のタレに関しては別エントリ書きます。

mroongaで「ERROR 1005 (HY000): already used name was assigned:」の対処

MySQL Mroonga

MroongaでTokenizerをMecabからTokenBigramSplitSymbolAlphaDigitに変更する際に
インデックスのDropでエラーが出てしまい強制的に中止した。

これがまずかったのか、
その全文検索のインデックスが存在していないにもかかわらず、
再度インデックスを貼り直そうとした際に、

ERROR 1005 (HY000): already used name was assigned:

というエラーが発生するようになってしまった。

groonga /var/lib/mysql/database.mrn
>table_list

でMroongaのtable_list情報を確認した所しっかりと残っていた。

http://groonga.org/ja/docs/reference/commands/table_remove.html
の情報を参考に

>table_remove index_name 

でMroongaのtable_list情報を削除したら無事インデックスを貼り直すことができました。


以下バージョン情報です。

mysql Ver 14.14 Distrib 5.6.11
groonga(mroonga) 3.0.3

人生で大事なことは大体自転車から学んだ。

自転車

・一歩ずつ前へ前へペダルを漕ぐだけで大概の場所に自分の力で行ける
・長くてきつい上り坂は永遠には続かない
・楽な下り坂も永遠には続かない
・頂上から見る景色は物凄い綺麗だ
・下り坂に入ったときにブレーキを掛けながら下りないと転んで怪我することもある
・距離は短いけど急な坂もあれば距離が長くゆるやかな道もある
・毎日のメンテナンスが安全やパンクを防いでくれる
・自転車本体の良し悪しはあるけど結局は自分の体力次第
・長距離の自転車に乗る場合はペース配分を考えないと途中でバテてしまう
・食べ物や水はある程度ストックしておかないと大変なことになる
・いくら遠回りしたって足を止めないで漕ぎ続ければ目的地には到着できる

社内のISUCONに参加しました。

PHP

昨日のことですが社内のISUCONに参加しました。

ISUCON2に関しては以下のブログに書いてあります
http://blog.livedoor.jp/techblog/archives/67728751.html
http://github.com/tagomoris/isucon2

今回はPHPを選択しました。
だって一番慣れてるしー
ホントだったらRubyでやりたいんだけどねー

実を言うと自分は業務がちょっと忙しくて
ソースを完全に見たのが当日の昼という状況でした。

まず基本的な設計指針

PHP不要モジュールの削除
→ほぼ必須モジュールのみだったので手を加えず

Apache不要モジュールの削除
→Auth系とかProxy系はOFF

・不要なサーバーのサービスのKill
→chkconfigで確認してPostfixとかOFF

CSS,JS,画像ファイルの最小化
→JSはGoogleから引っ張ってきて画像は圧縮してBASE64エンコで内部に埋め込み

・Memcacheの導入
→別途記載(1)

・DBテーブルカラムの最適化
→別途記載(2)

Apache等各種ログファイル出力のOFF
httpd.confですべてOFF

・Keep Aliveの設定
→一回のリクエスト数が少ないので重くなりOFFにした


まず初期状態でのベンチスコアは110チケット前後
WebよりもDBのLoadAverageがいっぱいな感じ。

Memcacheの導入(1)

スロークエリーが出ていた場所は

SELECT stock.seat_id, variation.name AS v_name, ticket.name AS t_name, artist.name AS a_name
FROM stock
JOIN variation ON stock.variation_id = variation.id
JOIN ticket ON variation.ticket_id = ticket.id
JOIN artist ON ticket.artist_id = artist.id
WHERE order_id IS NOT NULL
ORDER BY order_id DESC LIMIT 10

最近購入されたチケットの10件を取得して表示する場所。

これは様々なページで呼ばれていたのでMemcacheに入れた。
チケットが売れた時に10件を超えていた場合はarray_popし古いのを1件削除して
array_unshiftで直近で売れたチケット情報を加えることにした。

Memcacheの導入(2)

他の部分でスロークエリーが出ていた場所は

SELECT COUNT(*) FROM variation
INNER JOIN stock ON stock.variation_id = variation.id
WHERE variation.ticket_id = :ticket_id AND stock.order_id IS NULL

残りのチケットの枚数を取得する場所。

はじめの枚数をMemcacheに入れて、
チケットが売れた時にそのvariationの枚数を-1する処理に変更。

ここで400前後のスコアまで改善した。

APCの導入+サーバーのチューンナップ

APCを入れたらスコアが一気に700前後まで改善した。
KeepAliveはスコアが落ちたのでOFFにした。

DBテーブルカラムの最適化

アーティストとチケットと開催場所をDBではなくソースに直接配列として記述した。
これらの値は基本固定だったので配列として処理した。

以下やらなかったこと

・nginxの導入
→時間が足りなかった
Mysqlのエンジン変更
Mysqlサーバはボトルネックになってなかったから
フレームワークの変更
→時間が足りなかった
Mysqlのテーブルカラムの構造変更
(stockのseat_idをvarchar255から2個にカラム分割してintに変換しindexを貼り直す)
Mysqlサーバはボトルネックになってなかったから

以下反省点

・事前に時間を作ってちゃんとソースとサーバーを見ておくべきだった
・テストのソースをしっかりと確認してどこをテストしてるのかを確認すべきだった
・Webサーバっていう大まかな負荷を見るだけじゃなくて、
どの画面がボトルネックになっているのかっていうのをしっかりと把握すべきだった
・実際の業務じゃないので新しい技術に積極的にチャレンジしてもよかった

最後に

チームメンバーのお陰でなんと優勝することができました。
最小の処理でなかなかのパフォーマンスを引き出すことができ良かったかなと思います。
運営の皆様お疲れ様でした。次回ありましたら是非また参加したいです。

賞金5000円分の商品券もらったんで飲みにでも行きましょう(笑)

CakePHPでBlogを作る

PHP CakePHP

某社の課題でBlogを作る。

基本設計編

まず基本となる言語環境選択

PHP(一番得意)
Perl(はてななら)
RubyRails使うなら)

今回は単純に一番得意なPHPにて開発することにした。

次にデータベースの選択

Mysql(ローカルにインストール済)
PostgreSQL(ローカルに未インストール)
Sqlite(環境が変わってもファイルなんで手軽)

ローカル環境なので何も考えずにMysqlで作ることにした。

次にフレームワークの選択

・ZendFramework(ちょっと規模が小さい)
CakePHP(お手軽)
Ethna(昔使ってた)

ZendFrameworkだと1時間かからずに作成できるけど、
自分フレームワークも含んでいるためかなり規模が大きくなって、
Zend内部と自分フレームワークと2重の説明をしないといけなくて煩雑になりそう。
お気軽っていう意味でCakePHPを選択した。
ただCakePHPは1.3系までしか使ったことがないのと、
最後に使ったのが数年前なのでちょっとブランクあり。


ローカル環境編

ホストの設定

hostsファイルにローカルアクセスできるように以下を記述する。

sudo vi /etc/hosts 
127.0.0.1       blog.localhost
Apacheの設定

バーチャルホストの設定する。

sudo vi /opt/local/apache2/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
    DocumentRoot "/Users/kazuya/home/blog/app/webroot/"
    ServerName blog.localhost
</VirtualHost>
サーバー再起動
sudo apachectl restart
データベース作成

本当ならユーザーも作成すべきだけどローカル環境なのでrootで全てやっちゃう。

mysql -u root -p   
# (パスワード入力)
mysql > CREATE DATABASE  `blog` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;
CakePHPのインストール

http://cakephp.jp/ より最新版をゲットして/Users/kazuya/home/blogに設置する。

CakePHPの設定

セキュリティーの値を変更

app/Config/core.php以下の値を変更する。

'Security.cipherSeed'
'Security.salt'
データベースアクセスの設定

app/Config/databese.phpを作成する。
ファイル内容はこんな感じ。(パスワードとユーザー名は適宜変えてね)
またMacportsでインストールしたMysqlだとソケットを指定しないと動かないので注意する。

	public $default = array(
		'datasource' => 'Database/Mysql',
		'persistent' => false,
		'host' => 'localhost',
		'login' => 'root',
		'password' => 'XXXXXXXXX',
		'database' => 'blog',
		'prefix' => '',
		'encoding' => 'utf8',
	    'unix_socket' => '/opt/local/var/run/mysql5/mysqld.sock'
	);
書き込みの権限を付与

キャッシュとかセッションを保存する一時フォルダーに書き込み権限をつける。

chmod -Rf 0777 app/tmp


http://blog.localhost/ にアクセスして問題なく動作していることを確認。
f:id:kazupyong:20121104231442j:plain


アプリケーション設計

データベース設計

データベースはERMasterで作った。
Blogで最低限記事とタグ付けが出来ればいいとの課題なので、
記事とタグと関連付けテーブル。あと認証を使いたいのでユーザーテーブルを作成する。

ER図はこんな感じ。
f:id:kazupyong:20121104232233p:plain

ERMasterでSQLをエクスポートしてコマンドラインからテーブルを作成する。

> mysql -u root -p blog < blog.sql 
Bakeでモデル、コントローラー、ビューを作成する

CakePHPにはBakeっていうインタラクティブなコマンドラインツールがあるんだけど、
2.2で久しぶりに使ってみたらかなり使えるツールになってた。

php -f ./app/Console/cake.php bake
---------------------------------------------------------------
Interactive Bake Shell
---------------------------------------------------------------
[D]atabase Configuration
[M]odel
[V]iew
[C]ontroller
[P]roject
[F]ixture
[T]est case
[Q]uit
What would you like to Bake? (D/M/V/C/P/F/T/Q) 

と表示されるのでMを選択してModelを作成する。

Use Database Config: (default/test) 
[default] > 
Possible Models based on your current database:
1. ArticleTag
2. Article
3. Tag
4. User

と4テーブル分のモデルをチュートリアルに従って作成する。
バリデーターとかも事細かにそれぞれのフィールドごとに指定できる。

ここで大切なのは
ArticleとTagはArticleTagにhasAndBelongsToManyであり、
ArticleTagはArticleとTagにbelongsToしていると関連付けしておく。

ControllerとViewも同様にして作成する。


アプリケーション実装

ルーティング処理

articlesのindexがblog.localhostにアクセスした際にデフォルトの表示になるように
app/Config/routes.phpに以下を記述する。

	Router::connect('/', array('controller' => 'articles', 'action' => 'index'));

またコントローラーのadmin_の関数を有効化する為に
app/Config/core.phpに以下を記述

	Configure::write('Routing.prefixes', array('admin'));
ページング処理

ArticlesControllerにページング処理の設定。
記事の公開日時順に並び替えたいのとデフォルトで5件表示に変更。

クラス変数に以下を追記

public $paginate = array(
        'Article' => array(
        'maxLimit' => 5,
        'order' => array('published' => 'desc'),
));

あとはタグIDが指定されている場合にそのタグの記事だけが表示されるように、
ArticlesControllerのページング処理に絞込みの条件を指定。

function index ()内に以下を追記した。

	if(isset($this->params['named']['tag'])){
	    $cond = "ArticleTag.tag_id = {$this->params['named']['tag']} and ";
	    $this->paginate['Article']['joins'] = array(array('type' => 'LEFT', 'alias' => 'ArticleTag', 'table' => 'article_tags','conditions' => 'Article.id = ArticleTag.article_id'));
	}
ログイン処理

LoginControllerとAppControllerにadminのログイン処理を記述。
今回はAuthコンポーネントを使って処理する。

AppControllerにコンポーネントの有効化を記述することにより、
基本すべてのコントローラーがAppControllerを継承しているので全てに反映される。

クラス変数に以下を追記

    public $components = array(
            'Session',
            'Auth' => array(
                    'loginRedirect' => Array('controller'  => 'admin', 'action' => 'articles'),
                    'logoutRedirect' => Array('controller' => 'login', 'action' => 'index'),
                    'loginAction' => Array('controller' => 'login', 'action' => 'index')),
            );

ただ、Blogのトップと記事詳細はログインしてなくてもアクセスできるように、
AppControllerのbeforeFilterに以下を記述。

    function beforeFilter() {
        $this->Auth->authError = __('Login Error');
        $this->Auth->allow('article','index');
        $this->Auth->allow('article','view');
    }
日本語化対応

デフォルトでエラーメッセージやデータベースのカラムとかは全て英語表示。
一応日本語化しておく。
コマンドで全ファイル内の文字出力関数__()内の翻訳ファイルを作る。

php -f ./app/Console/cake.php i18n 

これでapp/Locale/以下にdefault.potファイルが出来る。

mkdir -R app/Locale/jpn/LC_MESSAGES/
cp app/Locale/default.pot app/Locale/jpn/LC_MESSAGES/default.po

日本語の翻訳メッセージを入力しておく。
デフォルトで英語環境で使っているので日本語のFirefoxをインストールして確認した。



こんな感じで最低限ひと通りの機能は完成。


ソースコードは以下で。
https://github.com/kazupyong/Blog