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

moshisora.memo

もくもくと。

gulp-revのリビジョン付のファイル名を任意の形式にフォーマットするgulpプラグインを書きました

javascript gulp gulp-plugin

はじめに

ブサウザにファイルのキャッシュを捨てさせる目的でファイル名をリビジョン付きのものにする、ということは多々あるかと思います。 gulp でそれを行うためにgulp-revがよく使われていますね。
gulp-rev はファイルの内容をhashにしたものをリビジョンとして [ファイル名]-[revision].[拡張子] の形式で付与します。

今回は、やんごとなき事情により、 gulp-rev で生成されるリビジョン付のファイル名のフォーマットを任意の形式に変更したい、という状況になったので、 gulp-rev と連携して任意のフォーマットでリビジョンを付与できるプラグインを書きました。

www.npmjs.com

使い方

npmでインストールできます。

$ npm install --save-dev gulp-rev-format-re

以下のように、gulp-revの後に実行します。

const rev = require('gulp-rev');
const format = require('gulp-rev-format-re');

gulp.task('default', () => {
    gulp.src('./src/style.css')
        .pipe(rev())
        .pipe(format('[% rev %].[% filename %]'))
        .pipe(gulp.dest('./dist'))
        .pipe(rev.manifest())
        .pipe(gulp.dest('./dist'));
});

gulp-rev-format-re は、引数に与えた文字列の下記のパラメータを、それぞれオリジナルのファイル名とgulp-revの出力したリビジョンに置き換えます。拡張子は自動で最後に付与されます。

  • [% rev %] : gulp-revで付与されたリビジョン
  • [% filename %] : オリジナルのファイル名

上記の例では、出力されるファイル名はxxxxxxxx.style.cssのようにフォーマットされます。
前後や間に任意の文字列を挿入することもできます。

format('head-[% filename %]-prefix-[% rev %]-suffix')

とすると、head-style-prefix-xxxxxxxx-suffix.cssのようにフォーマットされます。

おわり

中身は大したものではない&やんごとなき事情で書いたのであまり利用ケースが思いつかない気はしますが、何かお役に立てることがあれば幸いです。
類似のプラグインとして、gulp-rev-formatがあるので、[ファイル名][prefix][revision][suffix].[拡張子]の順番を保ったまま任意のprefix suffix を追加したい場合はそちらを使うとよいのではないかと思います。

関連リンク

ISUCON6にmorimoto組で出場して準優勝でした

ISUCON

ISUCON6に出場しました

ISUCON6本戦@渋谷ヒカリエ LINE株式会社 f:id:moshisora:20161022090704j:plain:w480

ISUCON6に「morimoto組」で出場して、準優勝できました。メンバーは、

という布陣でした。言語は自分とボンゴレ氏がPerlの方が使い慣れている関係でPerlを選びました。

スコア推移

ラスト1時間は他のチームのスコアが非表示ですがこんな推移でした。一台でチューニングしてから最後に複数台構成になったので結果発表ではダークホース感があったかもしれません…。

f:id:moshisora:20161024182157p:plain

Time PASS/FAIL Score 変更
11:02:53 PASS 942 Perl初期スコア
11:21:28 PASS 1159
11:58:31 PASS 1364
12:31:15 FAIL 253
12:36:34 PASS 1616
12:39:18 PASS 2051 nginx導入
12:43:40 PASS 3175 perlのプロセス数を200に(していたらしい)
12:46:41 PASS 3543
12:49:26 FAIL 12
12:53:18 PASS 2816
13:03:36 PASS 3314
13:35:14 FAIL 251
13:47:46 FAIL 251
14:07:14 PASS 3211 pointsをstrokesのカラムにjsonで保存するように
14:09:41 FAIL 0
14:10:56 FAIL 0
14:11:25 FAIL 0
14:14:25 PASS 3470
14:15:21 FAIL 0
14:16:38 FAIL 0
14:20:25 FAIL 0
14:24:03 PASS 4697 /img/* で返すSVGPerlレンダリング
14:27:05 FAIL 252
14:29:53 PASS 4781
15:17:25 FAIL 106
15:21:32 PASS 7412 /img/* をRedisにCache
15:31:52 PASS 6040
16:09:04 FAIL 253
16:10:24 FAIL 253
16:12:32 PASS 8020
16:54:56 FAIL 253
16:58:52 FAIL 253
17:10:31 PASS 9339 /api/stream/rooms をRedis pub/subとGo実装に置き換え
17:41:33 PASS 15241 2台構成
17:43:36 PASS 30714 5台構成
17:45:12 PASS 29301 isu02再起動試験
17:48:35 PASS 36067 isu01にnginx,MySQL,Redis、その他でPerl,Go,Reactの5台構成

タイムライン

10:00~12:00

開始間も無くしてAzureへのデプロイがうまくいかなかったり、アプリを読んだり触ったり、Perl実装を反映してベンチをかけるまでに1時間ほどかかりました。その後手元でperlのサーバを動かしたりと準備をしているといつの間にかお昼頃に。

12:00~15:30

pointsをstrokesのカラムにjsonで保存

points は一点ごとにレコードになっていて、必ず strokes と一対で、かつ更新もなさそうだったので、これは strokes と一緒にしてしまっていいよね、ということで strokes テーブルにカラムを追加して、そこにjsonで入れることに。同時に、初期データもjsonに変換するスクリプトを書いて strokes のカラムに保持、としたもののこの時点ではあまり効果は無し。

/img/* で返すSVGPerlレンダリング

SVGの生成はReact側からperlAPI経由で pointsjsonを取得して、React側で表示するSVGを生成していたので、これをperl側で生成するようにする実装が、残り2名で points の変更を入れている間にfujiwaraさんにより導入。

/img/* をredisにcache

stroke が新たに追加されるまで、 /img/* の結果は更新する必要がないので、これをredisにcache。新たに stroke が追加されたときにキャッシュを破棄するように。これで結構伸びて7412。

15:30~17:10

この時間が苦しかった…。

/api/stream/rooms をRedis pub/subとGo実装に置き換え

200プロセスでゴリ押していた stream がやはり辛いという話になり、この部分の実装をGoに置き換えることに。Perl側から新しいstrokeの投稿があったときにstrokeをpointsと一緒にpublish、Go側でsubscribeしてクライアントに配信するように変更。この変更でPerlでは500msごとに新しいstrokeをまとめて送信していたものが、新しいstrokeが来るたびに送信できるように。一旦は完成したかと思いきや、ベンチを走らせると 必要なwatch_countが送信されていません 旨のエラーメッセージでベンチが通らず、しばらくこれの解決に食われましたが、ボンゴレ氏が、「これ、publishされなくても500msしたら返さないといけなくないですか?」と閃いてなんとかベンチ通過。パフォーマンスも改善して9339。

新しいstroke投稿があったときにすぐ送信できる実装自体はあまりスコアに影響がないらしかったですが、触ってみるとレスポンス早くなっていいアプリになった感じはありました。余談です。

strokes countもredisにcache

毎回 stroke を全部拾ってきて scalar で個数だけ拾っている部分があった

$room->{stroke_count} = scalar @{ get_strokes($self->dbh, $room->{id}, 0) };

ので、これもredisに

my $stroke_count = $redis->get('stroke_count'.$room->{id});
unless ( $stroke_count ) {
    $stroke_count = scalar @{ get_strokes($self->dbh, $room->{id}, 0) };
    $redis->set('stroke_count'.$room->{id}, $stroke_count);
}
$room->{stroke_count} = $stroke_count;

stroke 投稿時に incr: $redis->incr('stroke_count'.$room->{id});

というものは書いてはいたものの、時間が差し迫っていたのでmergeしないまま複数台構成作業へ。

17:10~18:00

コードはフリーズして、複数台構成と再起動試験。
fujiwaraさんにお願いしてあとは自分たちは祈るだけという状態に。
最終的には isu01 でnginx, MySQL, Redisを動かしてそれ以外でPerl, Go, Reactをのせました(競技後に教えてもらいました)。

isu02 の再起動試験をしたのち、最終的な構成にしてスコアは36067。最後に isu01 を再起動はしたものの、これ以降スコアが伸びることもなさそうだったのと、時間が終了5分前くらいだったこともあり、これでスコアfixしましょうということに。ベンチは実行しないまま、各自 isu01 につないでちゃんとレスポンスが返ってることだけ確認してそっとコンソールとブラウザを閉じました。

結果

最終スコア 36067 で準優勝でした。

f:id:moshisora:20161022190409j:plain:w320

反省はやはり、コードくらいは2人でがんばるぞ!!で、fujiwaraさんに下回りをお願いするのが理想形だったと思うのですが、結局結構コードも書いてもらってしまったことと、Go慣れしてなくて選択肢も最初から絞り気味だったことでしょうか。お題を聞いた時点でPerlつらいしGoよさそうという話はあったのですが。golang書こう…

結果は嬉しいものでしたが、ブログを書きながら振り返ると、肝心なところは全部先輩頼りでしたし、16時を過ぎたあたりからは何やれば良さそうかも分からなくなってきて、個人的にはやはり全然戦えてないなーという感想で、悔しくも感じた初参戦でした。悔しさ胸に勉強せねば…

出題・運営の皆様、予選も本戦も、楽しい問題と競技を本当にありがとうございました。また協賛の皆様、楽しい懇親会をありがとうございました!
そしてチームの皆様、本当にありがとうございました!すごい体験をさせていただきました。嬉しくも悔しくもありましたがこれを糧にまた頑張ります。

最後にこちらを!

ISUCON6にmorimoto組で参加してきました

ISUCON

ISUCON6に参加しました

ISUCON6 、日曜日に参加しました。前々から出てみたいなーと思ってはいたものの、面子をゆるぼしてみたものの集まらず…と見送りかなーと思っていたところ、社内メンバーで組めることになって自分でも驚きの布陣で参戦することに。ありがとうございました!

  • morimoto組
    • fujiwara(組長。いわずもがな)
    • moshisora(私。新卒2年目。初参戦)
    • ボンゴレ(新卒1年目。初参戦)

足回りや方針はfujiwaraさんに引っ張っていただきつつ、残る2名で実装がんばるぞい、という感じで挑みました。実装言語はPerlでやりました。

事前準備

  • slackとかgithubリポジトリ準備
  • 過去問とpixivさんの社内ISUCONの問題をやってみる
  • Azureさわってみる
  • (項目は作ってみたものの特別なことはしてなかった…)

当日タイムライン

当日のスコア遷移をメモしてなかったので朧げですが、slackとgithubのログを掘り返しつつ、こんなことをしましたメモです。アプリ側しかみてないので覚えていることをさらっと。

9:00

  • 集合&作業場設営
    全員起床成功。
    会議室を確保してなかなかよい環境でできました。

10:00〜11:00

  • Azureにdeploy、chefを回して環境準備。30分くらいかかった。
  • dumpとかbackupとかしておく。
  • (たぶん)ミドルウェア類設定を反映。
  • アプリケーションをさわってなんとなく把握。

11:00〜12:00

  • ログとか計測とか。
  • コード読む。
  • isutarをisudaに統合。
  • ローカルで動かないと辛いですねーと環境を作り始める。

12:00〜15:00

スコア伸びるまで結構つらい時間でした。

  • とりあえずindex alter table entry add index updated_at(updated_at);
  • トラップを踏んだのか何なのか、新卒陣がPerlのローカル環境を動かせずに断念。以後「動け!!!🙏」と念を込めてプルリクすることに。ちょっと凹んだ。
  • 明らかに htmlify が重いので、とりあえず Regexp::Trie を導入するもあまりスコアは伸びず。
  • 毎回生成してるから重いのでは…ということで entry のidを覚えておいて更新があったときだけ再生成するように変更。

(この時点で最大スコア20000程度。いまいち伸びず内心結構焦ってました…。)

  • 正規表現をRedisに入れるようにして、 htmlify正規表現を作るのはやめて、 entry がpostされる時に再生成するように変更。が、同時リクエストでRedisの値の整合性が取れずベンチがこける。
  • post entry用のプロセスを立ててnginxから対象のパスは専用のそちらの1プロセスでさばくように変更。この変更で15時前頃にスコア80000くらい。一気に上位に食い込んでやる気が…!

  • この頃に /SELECT COUNT(*) FROM entry していたところもRedisにのせましたが、スコアは若干上がった程度でした。

このあたりから、プロセスを生かしたまま2度目のベンチを回すとスコア120000くらい出ることに気付きながらも原因についてはこの時点ではよくわからず。
同時に、 "上町村" に "川上町" からのリンクがありません (GET /keyword/川上町) というメッセージが出始め、「なんでしょうねこれ」とはなりながらも、運営へ質問したところ Status: PASS ならスコアは有効ですとの回答を得られたのでとりあえず無視。ぬか喜びしていた時間帯。

15:00〜17:30

スパムチェッカーがCPUを食っていてしばらくワード等ログに出して見てはみたものの、NG判定されているワードも規則が見えなかったのでここのテコ入れは断念。
あまり突き刺さる変更はできなかったものの地道にスコア更新。細かいものも色々。

  • /entries のクエリを自己結合に。
  • /stars をRedisにキャッシュ。
  • initializestars を全部Redisに載せて get_multi するように変更。
  • uri_for が遅いらしい&絶対パスにしなくてもベンチ通る。ので全部消す。(たぶんほとんど効いてない)
  • user_nameもsessionにいれて set_name のクエリ削除
  • とかとか

ここまでで初回ベンチスコア90000くらい、何度か回すと最大スコア170000くらい(まだ謎でした)。

17:30〜18:00

post entry用に立てたプロセスをrestartするとスコアが90000程度に戻ってそうだと発覚。どうやら Trie オブジェクトが生きたままになっていて、1度目のベンチで追加したキーワードが2度目のベンチでも効いていた模様でした。ここでようやく謎が解けた。30分前。
(なので、ないはずのリンクが作られていた模様。川上町の謎が解けた…)

ならば、ということで、一度ベンチを走らせて追加されるデータをテキストに落として、 initialize 時に正規表現に追加しておくという実装がfujiwaraさんによって高速挿入されてコード変更は終了。完全に未来予知作戦なので本質的ではない&データセットが変わると全く効かないので、複雑な気持ちですねーという話をしながらではありつつも、これで140000点を突破(17:50頃)。
その後再起動してベンチキューに滑り込み、終了3秒前にベンチ実行開始。稼働状況を見る感じ大丈夫かなあ…という雰囲気でしたが、再起動後のスコアを確認できないまま競技時間終了…。

ギリギリもギリギリだった…

予選突破できた

不安な感じで競技終了時間を迎えてしまったので反省会中もdkdkしていましたが、最終スコア143,366で無事通過できました。とはいえ最後の裏技的なやつが効いてなかったら9万点いくかいかないかくらいだったので、結構やばかったんじゃないかなと…。

  • Perlのコード書く以外やってない。要復習。
  • htmlifyもっとなんとかできた気もする…。
  • 手元でPerl動かそう…(重要)
  • 普段やってないことをやると結構単純なことで詰む。全部一瞬で拾ってくれる大先輩心強すぎる…。ありがとうございました。(自分でできるようにならないと…)
  • (普段仕事でJSばかりなためか、JSのコード書いてボンゴレ氏に「それJSですよ」って突っ込まれた。手が…手が…)

大会の製作・運営のみなさま、ありがとうございました!初参加でしたが楽しかったです。ベンチマークも数分で実行されて快適でした!

そしてチームの皆様、本当にありがとうございました!ものすごく連れて行ってもらった感が否めないですが、何はともあれ予選突破できました。突破されたみなさま、本戦でお会いしましょう!!本戦は勉強じゃない、戦争だぞ!!との激励もいただきまして、一ヶ月頑張ります。がんばります…

さいごは反省会の様子です。弊社golang実装組がなかなかつらそうでした。正規表現

リンク

sfujiwara.hatenablog.com