ISUCON7 予選通過した!

スギャブロエックス(id:sugyan, id:kazeburo, id:gfx) で予選に出場して2日目2位でした。去年は予選敗退だったので2年ぶりの本戦出場です。

バランスの良い良問で大変楽しかったです。ISUCON運営チームに於かれましては大変おつかれさまでした&ありがとうございました。

isucon.net

スギャブロエックスのスコア推移:

f:id:gfx:20171023000645p:plain

repo: https://github.com/gfx/isucon7-qualify

言語

採用言語はnodejsでした。もともとはPerl界隈で知り合った3人ですが、最近だれも現役でPerlを書いておらず、go, ruby あたりでやるか〜みたいな話を最初していました。途中でnodejs実装が追加されることになったので、nodejsにしたい!と希望してこうなりました。

と驚かれる言語なんですが、 ぼくは 最近一番得意だし、速度的にもRubyよりも数倍速いのでgolangの次くらいに有利なはず、ということでnodejsにしたかったのでした。他の二人的には負荷が大きかったと思うけど、それが不利にならないくらいサポートは出来ていたと思う…!

前半

isubataというチャットサービス…という顔をして画像配信がボトルネックという感じでした。帯域が足りない…これは…ISUCON4で見たやつ…!

ISUCON4 本選の解説と講評 : ISUCON公式Blog

このへんはだいたいkazeburoさんとsugyanがいろいろやって解決してだいたい10万点突破するくらい。

ぼくはこのへんひたすらコードリーディングしてたりプチプチN+1を潰したりしていました。

後半

画像配信がそれほど問題ではなくなり、ウェブアプリの問題が大きくなってきたのでアプリケーションエンジニアとしてようやく活躍できるようになってとにかくアプリの変更を色々やってみるフェーズに。ひたすら試行錯誤です。

よくみるとnodejs実装は地味に無駄なクエリを発行してたりするのでとにかく潰す。難しいところは相談しながら、簡単なところはエイヤでやっていく感じに。このへんはISUCON1に出場したころは全然できなかったなーと感慨深い思いでした。

やったこと:

https://github.com/gfx/isucon7-qualify/commits?author=gfx

なんどかrevertしたりしてますね。

普段TypeScriptで補完バリバリの環境に慣れているせいか、変数のtypoが目立って苦しい感じでした。ローカルで動かせればそんなことには…と思うけどローカル環境のちゃんとしたセットアップはそれなりに大変なので判断がむずかしい…。しかしeslintのセットアップくらいすればよかった。

その他雑多な感想

  • はじめて組むメンバーだったけど、作業をいいかんじに分担できて、作業が重複したりコンフリクトしたりみたいなことがなくて効率よくできた
    • kazeburoさんがインフラ中心に全体をみて、ぼくとsugyanがアプリ側をみるという感じ
  • ボトルネックが「帯域(HTTP cache)」「クエリ」「CPU」それぞれ良い感じに設定してあって総合力が試される感じだった
  • WAFの置き換えまではやる余力がなかったので本戦までには選択肢として検討できるようにはしたい
    • nodejs実装でつかわれていたexpressは全然速いWAFではないので
  • 事前gzip圧縮をzopfliで…とかも余力があればやってよかったかも
  • redis, memcached, オンメモリキャッシュなどもまったくやらず
    • サーバー構成的にやる余地がなかったというのもあるけど、もっと時間があればオンメモリで1秒キャッシュみたいなのは試したかった気がする
  • サーバー三台でいいかんじにリクエストを受けるのは結局いい感じのアイデアを出せなくてちょっと悔しい
    • 全体構成はkazeburoさんの案でした
  • alp めっちゃいい
    • 今回だと alp -r --sum -f access_log.6.txt --aggregates '/history/\d+,/channel/\d+,/profile/\w+,/icons/\S+' とかで良い感じにまとめられたので最高

チームメイトのエントリ

compression-webpack-plugin + zopfli でgzip圧縮ファイルを用意する

sprocketsを使っているアセットは半ば自動的にgzip圧縮版ファイルが用意されるのでそれをnginxのgzip_staticなどでサーブすればいいわけですが、JSのビルドをwebpck化したときにそういえばgzipされたファイルを用意しなくなったなと。それでもまあ、nginxが圧縮はしてくれますが、nginx自身が行うon-the-flyよりも事前に時間をかけて圧縮するほうが圧縮効率はいいので、やらない手はありません。

というわけでcompression-webpack-pluginです。これはデフォルトだとgzip圧縮しますが、圧縮ルーチンをカスタマイズできるのでnode-zopfli *1を使うこともできます。こんな感じに:

const CompressionPlugin = require("compression-webpack-plugin");
const zopfli = require('node-zopfli');

// ...

webpack.plugins.push(
    // ...

    new CompressionPlugin({
      test: /\.js$/,
      algorithm: (content, options, fn) => {
        zopfli.gzip(content, options, fn);
      },
    }),
    
    // ...
);

これを自動でやってくれる zopfli-webpack-plugin というのもありますが、compression-webpack-pluginとメンテナが同じなわりにあまりメンテされてない様子なのでcwpにしました。

ちなみに元サイズ909kbのJSファイルは次のようになりました:

  • nginx on-the-fly: 331kb
  • zlib: 281kb
  • zopfli: 271kb

zopfliはわりと効果ありますね。なお node-zopfli は環境によってはインストールが難しいため、 WebAssemblyをNodeJS Native Addonの配布形式として使う - Islands in the byte stream で紹介したwasm版の @gfx/zopfli を使うほうが運用は楽かもしれません。

gzipよりも更に高い圧縮率をほこるbrotliもやりたいですが、nginxの設定をしないといけないのでまた後ほど。

cf. Brotli を用いた静的コンテンツ配信最適化と Accept-Encoding: br について | blog.jxck.io

*1:zopfliはzlibよりも高い圧縮率をほこるgzip実装です。

GraphQLの型を補完する Date, BigInt, Any を提供する graphql_types gem を書いた

github.com

クライアントサイドでもdecoderがないとただの文字列だったりオブジェクトだったりしますが、ひとまずRuby側だけでも。

Anyなんて使う機会あるの?って感じもしますが、「なんらかのオブジェクトの構造をもっているはずだが、クライアントサイドはその詳細を知る必要がない」ケースってあると思うんですよね。そういうときに使う想定です。多用するものではないと思いますが。

開発者向けにMarkdown JPというコミュニティを作ってみました

gitter.im

  • Markdown自体の仕様については、CommonMarkに期待しているので commonmark.org でよい
  • CommonMarkに収まらない拡張を日本語で議論できる場所がほしい
    • ルビや数式など
  • サービス間で(ある程度)互換性があることはMarkdownの大きな価値なので、その議論ができるといいと思っている
    • ビジネス的には競合だったりするもあると思うが、そこで差別化してもユーザーにとってメリットはないと思うので

という感じです。

8月にMarkdown Nightをやったら思いのほか反響があったので、それをうけてコミュニティの必要性を感じたというのもあります。

GraphiQL が "Mode graphql failed to advance stream." というエラーで起動しなったときのワークアラウンド

GraphiQL(グラフィクル)ってのはGraphQL(グラフキューエル)のAPI consoleです。GraphQL IDEと呼ばれることもあります。

github.com

(現行バージョン: v0.11.5, バグ確認バージョン: v0.11.2)

こいつには、どうも変なクエリを食わせると二度と起動しなくなることがあるという問題があります。このときdev consoleには "Uncaught Error: Mode graphql failed to advance stream." というメッセージがでてます。

LocalStorageへ保存しているのがダメなクエリだと思われるのですが、データを消してもすぐGraphiQL自体がLocalStorageに書き込んでしまってダメな状態を復活させてしまいどうしたものかという感じでした。

とりいそぎ「GraphiQLを閉じて、同じドメインの別ページでLocalStorageを消す」という方法がうまく回復できるっぽいのでそれでしのいでいます。


少し調べたところ、codemirror-graphqlかCodeMirrorのバグっぽいなと当たりをつけ、codemirror-graphqlで問題を再現できたので起票しました。

https://github.com/graphql/codemirror-graphql/issues/236

しかし、この起票した問題はテストが無限ループっぽい感じになって終了しなくなるというもので、例外が出るわけではないので違う問題かもしれません。

yarnpkgのenginesのバージョンチェックを無効にする方法

yarnpkg v1.0から、 package.jsonengines sectionのバージョンチェックが厳密になりました。これにより新しいnodejsやyarnpkgを試すのが面倒になります。

これにメリットを感じない場合は無効化しましょう。

具体的には、 ~/.yarnrcignore-engines true を書き足すと、 --ignore-engines が指定されたのと同じになりバージョンチェックが無視されます。

echo "ignore-engines true" >> ~/.yarnrc

Rubyの型定義ファイルを中央repoにしないほうがいい理由

あるいは私がDefinitelyTyped (DT) が失敗だと思っている理由、です。

あたりが話の発端です。


DTについては以前いまいちイケてない理由を書いたことがあります。

TypeScriptのDefinitelyTypedは「ダメでもともと、うまく使えればラッキー」くらいの距離感がよい - Islands in the byte stream

この時の話を一言でまとめると「ライブラリの作者ではない第三者がメンテしていることが多く、基本的に品質が低い」のがよくないというものでした。

それ以外にもいろいろ欠点があります。

また、 あらゆるライブラリの型情報をあつめる という野心的な試みなのはいいものの、そのせいでrepositoryをcloneするのに時間がかかり、パッチを作るのも大変です。

DTの場合はバージョニングも微妙で、型定義ファイルのバージョンはmajor versionだけオリジナルと一致させてteenyなどを自動的にインクリメントさせる方式をとっています。このため、ライブラリの特定バージョンを表す型定義ファイルというものは存在しません。このへんは本質的な欠点というよりは、DTのポリシーがイケてないという話かもしれませんが。

というわけで、いま私が関わっているプロジェクトではDTをあまり使っていません。DTは、基本的には使わないほうがいいと考えています。


追記:

TSはJavaScriptそのものではないのでいろいろ難しい面はあります。JS互換の静的型AltJSだけでもTypeScriptとflowtypeがあるし。

TypeScriptでPromise.prototype.finallyを使う

github.com

Promise.prototype.finally が stage-3 になって、 ES polyfill集である core-js にも v2.5.0 で追加されたので、babel-runtime などを使っている場合はcore-jsのバージョンを上げるだけで finally を使えるようになってます。

しかし、TypeScriptの場合は型情報が必要なので、core-jsのアップデートだけでなく、次のnpm moduleを使って型情報の追加が必要になります。

www.npmjs.com

TypeScript compiler (確認したのはv2.5.2) 的には、package.jsonに追加(して当然npm installなりyarnpkg installなりする)だけで利用可能になるようです。

ただし、RubyMine (v2017.2.3) はそれだけだと解釈できないようで、エディタ上は型エラーになります。vscodeだと問題ないので、TSでちょっと変わったことをするならやはりvscodeのほうがいいですね。

RubyMineでも Invalidate Cache をしたらfinallyが見えるようになりました。よかった。

CommonMark v0.28 のイマイチなところ

作業メモとして。なお、 CommonMark ≒ GitHub Flavored Markdown くらいの感覚で書いてます(実際にはGFMはCommonMark + いくつかの独自拡張)。

膠着語分かち書きしない言語におけるスペースで区切られないトークンとインライン記法について

膠着語というのは、日本語や中国語のように単語と単語の間にスペースをいれないタイプの言語です。 膠着語は分かち書きしない言語のことではありませんでした…。素直に「分かち書きしない言語」というのがよさそうです。

CommonMarkは分かち書きしなくてもそれなりに動くように定義されていますが、エッジケースではまだ思った通りに動かないということがあるようです。

ひとつ仕様のバグっぽいのをみつけたので起票しましたが、これ系の話は中国語のコンテキストで議論されてるっぽいので見守り。

Non-spaced tokens with emphasis seems not parsed as expected · Issue #488 · commonmark/CommonMark

脚注記法

CommonMarkには(ついでにGFMにも)脚注記法は存在しません。存在しないこと自体はまあいいのですが、PandocやQiitaなど多数のmarkdownサービスで採用されている脚注記法(以下pandoc’s footnotes)が、CommonMarkのLink reference definitionという記法と一部かぶっていて意味がまったく違うため、CommonMarkとの互換性をとるとpandoc’s footnotesを実装することが不可能であるという問題があります。

これはCommonMarkの仕様を変えるかpandoc’s footnotesを諦めるかの二択なので困ったものです。

これも起票済み。

Footnote extensions vs Link Reference Definition - Extensions - CommonMark Discussion

参考スレッド:

見出し記号(#)のあとにスペースが必要な件

CommonMark以前の仕様が未定義だったGFMでは #foo で見出しになっていたのが、CommonMark化後のGFMで見出しにならないのはだいたいこのせいです。これはおそらく、GitHubで多用するissue記法(#123)と明確に区別するためにスペースを必要にしたのだと思います。GitHubはCommonMark仕様策定に噛んでますからね。

これは受け入れるしかないのですが、影響範囲がそれなりにあるのでどうしたものかなという感じです。

Androidライブラリのためのmaskaradeというorgを作った

The Maskarade project · GitHub

最近イマイチAndroidの活動ができてないんですが、Androidライブラリのメンテを諦めたわけではなくて、たとえばOrmaとかはまだやりたいことがいくつかあるのでやるつもりはあります。一方で、ちゃんと新しいメンテナがいたほうがいいなーというプロジェクトもあって、とりあえず私のメンテする気のあるなしに関わらずユーザーがいそうなAndroidライブラリを全部まとめて maskarade(マスカレード)というorganizationに移すことにしました。

何故これが必要なのかというと、新しくオーナーシップをもったメンテナを受け入れられる体制にするためにorgが必要だからです。GitHubは個人アカウントにあるリポジトリのオーナーシップをコラボレータに渡すことはできないんですよね。なので、オーナーシップを誰かに渡したいときは (1) org管理にする (2) 新しいオーナーにrepoを移譲する、という選択肢しかないのです。

ここでオーナーシップにこだわるのは、そもそも誰かをOSSのメンテナに任命するにあたってはちゃんとオーナーシップ(リポジトリのオーナーシップとリリース権)を渡さないと引き継ぎがうまくいかないという経験則があるからです。逆に言えば、オリジナル作者がメンテしなくなったときに、コントリビュータにコミット権だけ渡してオーナーシップを渡さないと、結局そのコミッターは「お客さん」以上の関わり方ができず、そのOSSは死にます。OSSにユーザーがいなくなって死ぬのであればそれは寿命がきたということですが、ユーザーがいるのにオーナーシップが不明瞭なためメンテされずに死ぬのは大変残念です。

OSS の引き継ぎ方 - builderscon tokyo 2017 でも「引き継ぎをするときはリリース権もちゃんと引き継ぐこと」みたいな話をしていましたね。

というわけで、maskaradeのリポジトリは全てメンテナを名乗り出ればオーナーシップ(リポジトリのowner権限とbintrayのリリース権限)を渡します。私の直接の知り合いであればそのまま、そうでない場合でもそのまえに2~3のテスト(pull-requestをおくってレビューするというプロセスを経験する)をしたら渡します。なお、Androidライブラリをmaskaradeにtransferしたいとうい申し出も受け入れます(私はメンテはしませんが!)。

TODO: これを英語でも書いて maskarade/README に置く