たごもりすメモ

コードとかその他の話とか。

RubyConf Taiwanにいってきた & しゃべってきた

RubyConf Taiwan 2016
https://2016.rubyconf.tw/#Satoshi Tagomori

Proposalを出したら通ったので台湾まで行って参加してきた。3泊4日。旅費は勤務先の Treasure Data に出してもらいました。
日本から近いし時差もほとんどないし、台北市内は交通の便がよくて治安もよくて食事もおいしい。旅行先にはいいよなあ。

f:id:tagomoris:20161206124256j:plain

しゃべってきた

内容は、ミドルウェア*1Rubyで書くときに気をつけるべき tips の紹介、という感じ。Linux/Mac/Windows間でスレッドスケジューラの癖とかが違うから、どれかでうっかり通るようなテストを書いても他所ではコケるからね、気をつけてテスト書いて全部でCIを走らせようねとか、JSONを例にライブラリは速さの違いもあるけど、その一方でAPIとして使いやすいか、安定したサーバが書きやすいかみたいな違いもあるよとか、スレッド作るときはこういうことに気をつけないと黙って死んでるみたいな話がいろいろ出ちゃうよとか、そういう感じ。

自分の中では安定したコードを書くためということで整合性は取れてる話題なんだけど、聞く側からするとちょっと散漫なトークになっちゃってたかもなあ、という気はする。聴衆からの反応はあんまり良くなかった……。オーガナイザーの一人からはすごい良かったよ! と言ってもらえたので、よかったということにする。

英語でプレゼンテーションすること自体はもうあんま困らないんだけど、いまだにちょっと尺の調整のしかたが分からないところがあって、事前に2〜3度通してぶつぶつやりながら調整してた。日本語だと「速くしゃべってなんとか収める」という秘技が使える*2んだけど、英語だとそうもいかないしな。

いってきた

f:id:tagomoris:20161206124722j:plain
f:id:tagomoris:20161206124728j:plain

RubyConf Taiwan、話題としてはいつものRubyConf的なやつが多いということで、トークを聞いたり、会場のそのへんでだらだらコード書いてたりしていた。
台湾のRubyユーザの中心はやっぱりRailsな人なのかな、という感じで、やっぱりRails関連のセッションが人気があったように思う。HTTP/2 の話とか mruby-cli の話とかを聞いてたけど、現地の人にはあんまり人気なさそうだった。
あと現地の人はやっぱり中国語でやりとりするよなあ普通、という感じ……だけど中国語→英語の同時通訳があった、すごい。

参加者は日本からの人も多くて、最近自分にあんまり英語で誰かとしゃべる経験を積まないと! みたいなのも無くなっちゃってるので、おおむね日本語(とちょっと英語)でわいわいやってしまっていた……。向上心がなくなるといかんな。

観光とか

行きで空港に到着するなり中華電信のデータ通信使い放題SIMを買った。5日でNTD300、1200円弱。安い。便利。MRT(地下鉄)の車内でも使えて日本と変わらない感じ。
MRTも EasyCard (Suicaみたいなやつ)が NTD100 のデポジットで買えて、あとはけっこう長く乗っても NTD40 あるいはもっと安い金額しかかからなかったり、大変便利。数分おきに休みなく走ってるし車内も綺麗。安全。
タクシーも使ったし安い*3んだけど、ドライバーは基本的に英語が通じないし、人によっては電話しながら運転してたりでちょっと怖い。道路の渋滞も時間帯によってはなかなかきびしい。しかも一度は「道を間違えた」的なことを言ってたんだと思うけど、目的地まで半分行ってないくらいのところで降ろされたりした*4し。タクシー以外の選択肢がある場合は自分は使いたくないなあ。公共交通機関最高です。

f:id:tagomoris:20161206125301j:plain
f:id:tagomoris:20161206125304j:plain

あまり時間がとれなくて、観光的なのは到着したカンファレンス前日に数時間と、あとはカンファレンス2日目(最終日)の夜、あと翌朝(帰国日)にほんのちょっと、というくらい。
台北101に登ったりその周辺の繁華街をうろうろして牛肉麺食べたり。食べ物がおいしいのは正義。夜中の市街地でも身の危険を感じないのもほんとにいい。周囲の人もみんなスマホ片手に歩いてるし。

f:id:tagomoris:20161206125457j:plain
f:id:tagomoris:20161206125459j:plain

2年前に行ったとき、最近は台北でもクラフトビールを作るところが増えてきて、みたいな話を聞いていたので、今回クラフトビアバー行って地元のビール飲めたのはよかった。台湾ビールもおいしいけど、たまにはホップの効いたのを飲みたくなるじゃないですかー。スタンダードな味のビールがいろいろあってたいへん美味しかった……んだけど、烏龍茶風味? の Tea Ale「立冬」だけはちょっと自分にはきびしかった。これは味の好みの問題。

f:id:tagomoris:20161206125734j:plain
f:id:tagomoris:20161206125737j:plain

2年前に市内のけっこう色々な観光地を案内してもらって行っていたので、あとは故宮博物院に行きたかったんだけど、ちょっと時間がなくて断念。また行く機会もあるだろうということで無理はしなかった。
全体的に、超楽しいカンファレンス旅行でした。また行きたいな。

*1:に代表されるような long running process

*2:真似はお薦めできません

*3:30分走ってもNTD500かからなかったり

*4:料金はいらないから、とのことだった

専門用語を並べてしゃべる専門家は許せない、という人は愚かである、という話

ちょっと最近腹に据えかねる記事がネットで散見されるので敢えてアレなタイトルで、よろしくおねがいします。
なおこの記事は、自分はソフトウェアエンジニアリングの専門家であるので、そのような領域を大雑把に想定して書かれております。が、たぶん他の専門領域においても似たような状況なのではないかと推察しております。

専門用語ばかり使って会話するような人は本当のプロではない

という言説を最近ちょくちょく見ますね。曰く、普通の人に説明できないようではダメだ。曰く、普通の人でも重要性が理解できないように話せないということは、実際にはお前のやっていることは重要ではないのだ。曰く、専門用語ばかりで会話するようでは実際の能力はわからない、専門用語などわからなくても本当に能力がある人にはあるのだ。

んなわけねーだろ。

専門家というのは、非専門家には扱えない問題を扱う専門家だから専門家として働けていて、それなりの待遇をもらっているわけです。その領域には非常に多くの前提を要求する問題というのが数多くあるわけです。
で、専門的な、難しい問題を解こうと業務上の努力をしている場合、こういった前提をいちいち確認しながら仕事してたら話が進まないわけですね。専門家どうしでコミュニケーションをとるとき、難しい問題を解くためには当然知っているはずの知識について面倒な前提の確認をすっとばすためのものが「専門用語」と言われるものなんですよ。その共通となる下地の知識無しでは、その土台の上にある問題と格闘できるわけがないんですね。

もちろん、何らかの専門的な問題と格闘するとき、その問題が何であるのかという説明ができないようでは困ります。説明ができないということは、つまり、その問題そのものに対する理解が足りてない可能性がある、ということだからです。
逆に言うと、本当に専門家なのであれば、その人が格闘している問題について世界で最も詳しいのはその人自身である可能性もあります。つまり他の人には何かしらの方法で説明できない限り、その研究あるいは技術の有用性を理解させることができないかもしれないのです。だからきちんと教育を受けた人は、自分以外の(ある程度バックグラウンドを共有している)人に対して説明する能力を有しています*1

しかしそんな説明を日常の仕事のレベルでやれるわけがありません。そんなことをやっていたらオーバーヘッドが大きすぎて効率が極端に低下するからです。だから、ある業務領域の専門家として働いている以上は知っていてほしいはずの専門用語を知らないということは、単に能力が無いか、体系化されていないだけで能力としては持っているかもしれないものの非常に低い効率で働いているか、どちらかだということになるのです。

もちろんある領域の専門用語を知らないからといって地頭が悪いということを意味するわけではありませんし、何かの用語を知らなくても実際には頭がいい人というのもいるでしょうが、たぶんその頭のよさは無駄に使われてるんじゃないですかねえ。

とはいえ専門外の人でも理解できる必要があるのではないか

そのとおりです。

例えばコンピュータシステムにおいて何か問題が起きたとき、その根本的原因は何だったのか、どのように対応してどのような結果になったのか、どのようにすれば再発が回避できるのか、などなどは、例えば経営者やサポートスタッフや営業やその他多くの人も知る必要がある、というケースなどがあるでしょう。

そのような時に専門外の人にきちんと説明できる、というのは、それはそれでひとつの能力です。そしてそれは、全ての「専門家」が持っているべき能力というものではありません。なぜなら、それを専門にする人達がいるからです。企業の部署単位で言えば各々の部署のマネージャがそれに当たるでしょうし、もっと大きい単位で言えば例えばNASAの広報官とか、あれ超絶の専門家ですよね。各種の問題を一般向けの書籍にして出版しているような人もこの種の専門家と言えるでしょう。

各部署のマネージャやNASAの広報官は、べつに専門的な問題の解決について各々の専門家より高い能力を持っているわけではありません。場合によっては各々の専門家が使っている専門用語の数割しか理解できないこともあるでしょう。
しかし彼らがなぜその位置にいるかと言えば、必要なときに必要なだけ各々の専門家に対して聞きとりを行い、話を咀嚼し、そして専門外の人が理解できるようさらに分解して話す、という技能を持っているからです。これはこれで技術を必要とするもので、個別の専門家が備えているようなものではないし、個別の専門家が備えられるものでもありません。

そうは言っても例外ケースとかあるんじゃないの

もちろん、あります。例えばスタートアップ企業で一人目の、あるいはごく初期数人の技術者の一人として働く場合などがそうです。
この場合はそもそもチームが各方面の専門家一人ずつ、全部で数人、ということになるため、もちろん非専門家の人に説明するための人、などという贅沢は言っていられません。自分で全部やる必要があります。

こういったケースはおそらく他にもあるでしょうが、そもそもチームが大きくなったらその人はCTOなり何らかのリーダー的なポジションを期待されることとなるでしょうから、どちらにせよ、ある程度以上は非専門家に伝える技能を暗黙に期待されていると思ってよいでしょう。

結論

以上をまとめると、専門用語ばかり使う専門家は駄目だ、という言説はふたつの重要な点を無視していると言えます。

ひとつ、専門用語というものは業務を高効率で遂行する際のコミュニケーションには必須のもので、必要に応じて使っている用語の意味の確認などを挟む必要はあるものの、むしろこれを使わないというのは単純に業務効率を下げるだけであるということ。
ひとつ、専門的な話題を一般向けにわかるように伝えるというのはそれだけでひとつの専門的な技術であり、この技術は誰でも持てるようなものではない、ということ。

専門用語ばかり使う専門家は駄目だという人は、この非常に明確なふたつの事実が見えていない、ということだと自分は考えています。

*1:研究者の場合はこれが論文になります

ISUCON6決勝を戦って敗北した

ぼーっとしてたら1週間が経過してしまった。先日のエントリのとおりISUCON6決勝に通ったので戦ってきた。チームはもちろん引き続き @joker1007 と @tnmt との3人、チームJingisukan。そして負けた。6位。

f:id:tagomoris:20161022085352j:plain

やったことは色々あったし、それ以上やれることも、やっていてうまく結果に出しきれなかったことも色々あった。後悔することもあるけど、出題内容の中で自分(たち)がよく知らず最適化しきれなかったこともあったので、順位はともかく勝てなかったのはしょうがなかったなあ、という感じ。
おおざっぱに経緯を記しておこうと思う。

あれこれ

事前準備は特になし。空のリポジトリを用意したくらい。

当日は余裕をもってチームで集まったので、雑談したりうろうろしながらリラックス。競技開始後、出題内容見てウヘーってなってた。
とりあえずデプロイしてみるも、10コア制限にひっかかってうまくいかず。参加者チャットに同じ目にあってるチームがいっぱいあってちょっと安心する。
しかし Microsoft Azure のコンソール、かっこいいのはいいけどサイズ変更不可な部分で画面を大きく占有されて、MacBookAir 11inch の縦に狭い画面では本当に必要な部分がほんのわずかにしか表示されない、みたいになってつらい。

コア数変更が発表されて再度デプロイ、今度は成功。5台に鍵を配ってログインできることを確認したりしつつ、初期ベンチを回す。そこからRuby実装に切り替え、nginx reverse proxyを置いてログ取得、など。並行してコードを読んだり、手元のMacで環境を起動するための docker-compose 設定ファイルを準備してもらったり、何も設定されてなかった isu02 から isu05 まででもアプリが起動するようにセットアップしてもらったり。

  • 10:32 4048 (PHP実装初期スコア)
  • 10:41 3012 (Ruby実装初期スコア)

この間、11:30くらいに一度手を止めてその後の戦略について話しあう。とりあえず10コアあるけど react.js のサーバサイドレンダリングをやっているプロセスと本プロセス(Rubyアプリケーションサーバ)の負荷のバランスがよくわからず、とりあえずで以下のような戦略を決める。

  • node + ruby をセットで公平に複数台でバランシング
  • isu01 だけは nginx + MySQL を docker 外で起動して使う、アプリサーバはこいつは除外する?
  • Redisなんかの必要があればそれも isu01 で
  • 10コアあるので、負荷によっては isu01 に6コア集めて isu02 〜 isu05 は1コアだけにする?*1
  • コードに N+1 問題的なものがいろいろあるので、それは潰す

このくらい。
f:id:tagomoris:20161024111546j:plain

この時点で react.js アプリの詳細をあんまり深く読み込んでいなかった + react が処理するパスと /api 以下の違いをきちんと追っていなかった + reverse proxyでのキャッシュ戦略についてもっとちゃんと考えて準備をしておくべきだった …… みたいな後悔は今にして思うとある。

  • 12:53 2454 (nginx reverse proxy追加)
  • 13:26 2608 (isu02, LTSV access log追加)
  • 13:31 2509 (MySQLをDocker外部の常駐プロセス化)
  • 13:51 1341 (strokes -> points の N+1 クエリをJOINで1クエリ化、失敗)((全pointsに変化のないstrokeデータをくっつけちゃって、クエリ結果のデータ総量が増大したためだと思う。revertした)))
  • 14:09 1289 (アプリケーションサーバのpuma化)*2
  • 14:27 767 (nginx - nodeプロセス間の通信を https から http に)

このあたり、明らかに効くはずの変更をしてるのにスコアがどんどん下がってて、おいおいどういうことだという話に。FAILしてなければとりあえずOKという感じで進んでたけどベンチマークが大量にエラーを報告しているのを見てなかった。ロジックがおかしいわけではなく、レスポンスタイムアウトなどが大量に報告されてて、あれーとなる。
とりあえずそのへんの対処をしよう……という感じで調整的なことをあれこれやりはじめる。主にはMySQLのコネクション。

  • 14:51 5712 (MySQLへの接続の connection pool 化)
  • 15:03 6338 (strokes -> points N+1 to 2)
  • 15:27 6115 (react 2プロセス化)
  • 15:33 5974 (JSONリアライザをOjに)

ここまではすべて1ノードでの実行。複数ノード化すれば高速化するはずの構成をずっとひっぱってきていた。ある時点からじょーかーさんに複数ノード化の作業をお願いしていて、ここでマージして初めて試す。これでいっきに上位に行けるかなー、と思っていたら、思ったほどではなかった……。自分はこの間、コンフリクトが怖くて大きめの変更を試せず、うまく行くような行かないような微妙な変更ばっか試してた気がする。

  • 15:49 11667 (5ノード化)
  • 15:54 11760 (roomの人数カウントを COUNT を使うSQLに)
  • 16:00 15743 (nginx worker_connections 4096)
  • 16:07 14605 (nginx worker_process 2 nofile 20480)

複数ノード化してみたらMySQLに負荷がだいぶ来るものの、これ以上クエリの改善はどうにも難しそうだぞ、というのがこのあたりではっきりしだす。時間がなくなってきた割にいまいち上位に行ききれなくて焦ってきたあたり。やっぱりキャッシュをちゃんと考えないとダメかーという感じになるが、reverse proxyでのキャッシュ戦略を最初にちゃんと考えてなかったツケがきて、ここで考えるのに時間を使ってしまった。
このとき考えたアイデアは以下のみっつ(実装の案としては4つ)。

  1. RubyアプリケーションからMySQLへのクエリを減らすべく、とにかくあれこれRedisにキャッシュ
  2. Reactのレンダリング結果キャッシュ(案1): nginx でmruby/luaで頑張る?
  3. Reactのレンダリング結果キャッシュ(案2): stroke登録時にrubyからreactにリクエスト発行 → レスポンスをnginxのキャッシュとして登録して次からはノータイムで nginx から返す
  4. /api 以下のレスポンスはReverse proxyからRubyアプリケーションに直接流す

ここで一番最後の項目、やっちゃえばすぐにできる内容だったんだからやればよかったのに、なんとなく後回しにして結局やらなかった。これはもう、ものすごく後悔している……。
RubyアプリケーションでとにかくRedisにキャッシュするのはじょーかーさんにお願いした。最終的にはこれがこの後2時間で唯一の前進になった。

  • 17:10 18102 (Redis導入 stroke/pointキャッシュ)
  • 17:27 18989 (roomキャッシュ)
  • 17:37 18059 (再起動試験)

Reactのレンダリング結果のキャッシュについては、nginxで頑張る案については正直初手からの準備が足りなさすぎて、この時刻から試すには厳しすぎた。このため案2を採用してRubyのコードからReactにリクエスト投げる → nginxはWebDAVを設定、レンダリング結果をPUTしたらnginxはtry_filesでキャッシュをノータイムで返す、という方針をざっと立てる。nginxでのWebDAV設定をつねさまにお願いして自分はRubyのコードの変更をガッと。
が、このときnginxの設定がうまくいかず時間を浪費 + nginxをdocker-compose化しておかなかったため手元で試せない構成になってしまい、自分のコードもあれこれエンバグしてた。最終的には stroke 登録時にやることが多過ぎたせいで(?) POST リクエストに対するタイムアウトが多くなってしまい、加点要素にしきれなかったと思う。再起動試験後にようやくデプロイ可能状態になって変更を試したんだけど、スコアはほぼ変動なしだったように思う。ベンチマークが報告するエラーは増えていた。

この時点でフィニッシュ。17時前に見えていたスコアから上位は5〜8万点くらいで戦うチームばかりになると思っていたので大幅なスコアアップを目指した手ばかりを打って、あまりうまくいかないものが多かった、という結果になってしまった。

結果

フタを開けてみたら上位があまり伸びておらず、6位。しかも3位までが近くて、えー、もっと堅実な手(/apiの処理とかプロセス/スレッド数調整とか)をちゃんと打っておけば3位にいけたのに! という感じだった……。。。

f:id:tagomoris:20161022192047j:plain

しかし懇親会などであれこれ話したところ、自分たちではきちんと打てていない手がいくつもあった。たぶん影響が大きいものは以下のふたつ。どちらも優勝チームが打っていた手。

  • Reactプロセスのバックエンド化
  • SVGファイル生成とキャッシュ

一度全リクエストをNginxの背後のアプリケーション(優勝チームならGolang、我々ならRuby)で受け、Reactでのレンダリングが必要なものについてだけReactのプロセスにプロキシする。これによりレスポンスを自前アプリで簡単にキャッシュ制御することが可能。
これは正直思い付かなかった、が、思い付いてもよかったなあという内容。/apiのルーティング変更をやらなかったのもそうだけど、Webアプリケーションの構成に対する発想が貧弱になっている気がする。うううう。

SVGファイル生成とキャッシュについては、SVGファイルの中身は実際にはテキストで*3、かつstroke追加時には前に生成されていたものに1行追加するだけで済むので、cache invalidation + 再生成しなくても1回のI/Oで更新後のSVGファイルを生成できる、という話。
正直、まじかー、と思った。SVGファイルというものについての理解が欠けてた。
他のチーム(準優勝チームなど)ではReactのプロセスの外でSVG生成をやってたところもあるらしい、が、この更新時に1行追加というのはファイルフォーマットに対する理解あってこそできることで、なるほどなあ、という感じ。負けた。

そのほか懇親会で出題チームと話したところでは、今回のベンチマーカーは時間が経つごとにどんどん並列度が上がっていくというものだったらしい。なるほど、そりゃリクエスト/レスポンスのタイムアウトが減らないわけだ。たぶんこれにひっかかって最終的なベンチマーカー並列度を充分に上げられていないチームばかりだったのではと思う。*4
あとどこかで思い切って頭おかしいくらいアプリケーションのプロセス数とかを増やしてればガツンとスコアが上がったかもなあ、とか、まあ思うところはいろいろある、が、手を動かしきれなかったなあ。

雑感

なんにしろ面白い問題でした。出題チームのみなさん、お疲れさまでした&ありがとうございました。1日みっちり楽しませてもらいました!

そして、ついにISUCONで敗北してしまったので、ISUCON1からの無敗伝説保持者は誰もいなくなってしまった。これもイベントが成熟してきた証だと思う。ううう……。。。くやしい。
しかし思えば、自分ももう4年くらいマトモに大規模Webアプリケーションをやってないので、負けるのも無理からぬことだよなあ、という気がする。現場にいない人間が勝てるような競技ではない、というのは自然だと思う。去年の出題は頑張った。……という感じでこの1週間は自分を慰めている。

みんなでウィッシュリストをクリックしよう! あと出題側の話をするということで、以下のようなイベントをやるらしいです。自分もパネルでなんか言う。たぶん。

おかれさまでした! また来年! お疲れさま会をジンギスカン屋でやろう! > @joker1007 @tnmt

*1:実現可能性については考えてなかった

*2:並列度の調整をまったくしてない

*3:ここまでは知ってた

*4:このへんのロジックは聞いてみたところちょっとおかしくない? という感じのもので、あれこれ議論してた。講評を待ちたい。

CRuby/JRubyで実行可能かつmrubyでビルド可能なコードを書く

msgpack-inspectを作った話に書いたが、このツールはエントリにも書いたとおり rubygems.org に公開されていて CRuby や JRuby でインストール・実行可能である。その一方でバイナリをダウンロードするだけで使えると便利だよねってことで、mrubyでクロスコンパイルしてリリース版が置いてある。

これは実はそこまで簡単ではなくて、Rubyの機能のうちmrubyでもサポートされている文法や組込みライブラリの範囲しか使えないのはもちろん、たとえば外部のライブラリに依存する機能*1などは mruby でクロスコンパイルしようとすると地獄を見ることなどもある。
そんな事情もあって、今回クロスコンパイルしたリリースに成功するまで、けっこうな手間をかけた。ここにそのへんをざらっと書いておこうと思う。

やったこと

mruby-cliを使う

基本的にmrubyでビルドするための準備はほぼmruby-cli頼り。クロスコンパイル用のDocker imageなども準備されてるので簡単に使える。mruby-cli -s NAME でプロジェクトツリーを生成するだけ。

ただし(特にCRubyとしての動作との共存を考えると)現在リリースされている v0.0.4 ではいくつか問題があった。ので、パッチを書いて pull-request を送り、自分はそのパッチ済みのものをビルドしなおして使っていた。今ではそのへんの pull-request は全部マージされているので、単に clone してビルド(docker-compose run release)すれば使えるバイナリが手に入ると思う。

なおCRubyとの共存を考えなくても、現在の master:HEAD には release とかの便利タスクが入っているので、mruby-cliを使うなら自分でビルドしなおして使うといいと思う。

注意点として bundle gem は name-part という名前を name/part というディレクトリに展開するが mruby-cli は name-part というディレクトリを作ってしまう。手作業で修正すればいい話だが、ビルド設定とかに不整合が残ると面倒なことになるので注意すること。

bundle gemの出力を統合する

mruby-cli だけだと mruby でビルドするだけのファイルしか作られないので、そこに普通のgemとしてリリースするためのファイルを生成して統合する。どうやるかというと別ディレクトリで bundle gem NAME してファイルツリーを生成し、以下のディレクトリおよびファイルを mruby-cli で生成したディレクトリツリーに持ってくるだけ。ほとんどのものは競合しない。

  • Gemfile
  • README.md
  • exe/
  • lib/
  • NAME.gemspec

もちろん bundle gem した結果に mruby-cli -s の内容を統合してもいいけど、mruby側のほうがビルドまわりで必要なものが多くてミスるとハマる*2ので、この順番のほうがいいと思う。

唯一衝突するのが Rakefile で、これを mruby 関連のタスクと rubygems まわりのタスクを衝突しないようマージしてやる必要がある。自分が作ったものはこちらにある。

msgpack-inspect/Rakefile at master · tagomoris/msgpack-inspect · GitHub

何をやるかというとつまり docker-compose で実行されているときは mruby 関連タスクを優先し、そうでない場合には rubygems まわりのタスクを有効化する、というだけ。特に release というタスクがカブるので実行時に有効にする方はどちらかを決めている。

# https://github.com/tagomoris/msgpack-inspect/blob/master/Rakefile#L103-L128
if is_in_a_docker_container?
  load File.join(File.expand_path(File.dirname(__FILE__)), "mrbgem.rake")

  current_gem = MRuby::Gem.current
  app_version = MRuby::Gem.current.version
  APP_VERSION = (app_version.nil? || app_version.empty?) ? "unknown" : app_version

  task default: :compile
else
  Rake::Task['release'].clear # to clear release tasks to create mruby binary
  require "bundler/gem_tasks"
  require 'rake/testtask'
  # require 'rake/clean'

  Rake::TestTask.new(:test) do |t|
    # To run test for only one file (or file path pattern)
    #  $ bundle exec rake test TEST=test/test_specified_path.rb
    #  $ bundle exec rake test TEST=test/test_*.rb
    t.libs << "test"
    t.test_files = Dir["test/**/test_*.rb"].sort
    t.verbose = true
    t.warning = true
    t.ruby_opts = ["-Eascii-8bit:ascii-8bit"]
  end
  task default: [:test, :release]
end
実行ファイルのエントリポイントを分ける

実行可能コマンドに仕立てるにあたり、入口の部分だけはちょっと分岐が必要。rubygems側は spec.bindir に書いたディレクトリに実行可能rubyスクリプトとして置いておく必要があるし、mruby-cliの場合は __main__ というメソッドで定義してやる必要がある。

このため、おおむね以下のような構成にした。

  • CRuby側
  • exe/msgpack-inspect: 実行ファイル
    • lib/msgpack/inspect/command.rb をrequireして実行する(だけ)
  • lib/msgpack/inspect/command.rb
    • 実行時に必要なファイルをすべてrequireする
    • コマンドラインオプションの解析
    • メインの実行コードを呼ぶ
  • mruby側
  • mrblib/msgpack-inspect.rb

この「メインの実行コードを呼ぶ」以降はすべて完全に同一のファイルを使っている。

ファイルをsymlinkで共有

「以降はすべて完全に同一のファイルを使っている」だが、これは mrblib 以下のファイルツリーを、エントリポイントにあたる1ファイルを除き、すべて lib 以下のファイルツリー(の必要なファイル)への symbolic link として作成することで実現している。コピーとかは邪悪なのでしない。Pure Rubyで書かれているならこれで問題ない。

問題ぽいものはひとつだけあって、普通にテスト等を実行すると Rakefile の構造上、かならず NAME.gemspec と mrbgem.rake の両方がロードされてしまう。デフォルトでこれらのファイルはそれぞれ lib/NAME/version.rb と mrblib/NAME/version.rb を読むようになっているが、これらは実際には同じファイルであって、このソフトウェアのバージョンを示す定数 Name::VERSION が2度定義されてしまう(警告が出る)。
これはもう面倒だったので mrbgem.rake 側の参照を lib/NAME/version.rb に変えてしまって対応した、ことにした。

requireをエントリポイントに並べる

mrubyはデフォルトでは mrblib 以下のものをすべて読み込みリンクした状態となるようビルドする*3。このためあるコードが他のファイルに書かれている定義を参照する場合でも require などは必要ない。し、できない。mruby-require というものはあるが、……あれ? これはなんでやめておいたんだっけ。忘れた。

まあ mruby-require を使っていない場合には require とコードに書かれていると動かない。ただしCRuby/JRubyではもちろん必要なファイルはrequireされなければならないので、次善の策としてCRuby/JRuby側でのみ読まれるファイル*4に必要なすべてのファイルをrequireするように書いてしまった。

テストはCRubyだけ

mruby-cli にもテスト記述の支援のための機構はあるようで bintest というディレクトリがある。が、コマンド全体に対しての end-to-end テストのみという感じなので unit test の粒度のテストを書きたいときにちょっときびしい。

なので、とりあえずとして CRuby で動かすのを前提として普通に unit test を書いた。これは rake test すればCRubyで普通に走る。CRubyとmrubyの互換性が保たれている限りは、これでも問題ないだろう。むしろ最終的に mruby にコンパイルするんであっても unit test が書けて便利。

CRubyとmrubyにおける挙動の分岐

msgpack-inspect には外部のRubyスクリプトを読んでそこに定義してある処理を実行する、という機能が存在する。またこの機能は外部の msgpack.gem を必要とするためmrubyでは実行できない。
これをどうやって実現するか……正確にはこの機能を実現しているコードをそのままmrubyで動かすか、ということをちょっと考える必要があった。

しかしまあ、そんなに難しくはない。単に外部スクリプトが読まれていたら存在するはずの定数を確認してみて、それを条件に分岐するだけ。

MSGPACK_LOADED = MessagePack.const_defined?('Unpacker')

def foobar(value)
  @value = MSGPACK_LOADED ? MessagePack.unpack(value) : nil
end

上記の内容だけだったらメソッド定義の内容そのものを変更する(ifの内側にdefを書く)とかの選択肢もあるけど、実際のコードではメソッド全体で分岐しているわけではない(共通の処理もけっこうある)ので、まあいいか、とコード内で if で分岐してしまった。

このコードは正確にはランタイム(CRubyとmruby)で分岐しているわけではないけど、逆に言うと、サポートされている機能で分岐したいならこれで充分、ということ。

ハマったこと

順調にはいかなくて、ハマったこともいろいろ。

キーワード引数が使えない

なつかしの def foo(a, b, opts={}) みたいなコードをひさしぶりに書いた。

使えるメソッドがわからない

API documentはあるんだけど、なんかちょいちょい抜けてることがある。あと普通のRubyのつもりで書いてたらうっかりサポートされてなかったりとか。無いなら無いで mruby-pack みたいなmrbgemがあったりでそっちを使えばいいんだけど、本当に無いの? みたいなのを確認できないのはちょっと困る。

で、今となっては、最終的にはコードを毎回見てる。mruby-cliを使っていると必ずmrubyのコードツリーがリポジトリの mruby/ 以下に展開されてるのでコードを読みにいく手間が少なくて便利。
ドキュメントに足りないものがあれば pull-req すればいいんだけど、YARDがまったくわからなくて……どうなってるのこれ。

mruby-yamlのビルド失敗

最初はYAML出力をCRubyの添付モジュールで済ませようかと思ってたんだけど、これはもちろん mruby ではサポートされていなくて、しょうがないからmruby-yamlを使おうとしてみたら mruby-cli のクロスコンパイルがぜんぜんうまくいかない。しばらく試行錯誤してたんだけど、あまりにも不毛だったんで諦めた。

で、どうしたかというと、YAMLのフォーマッタくらい自分で書けばいいじゃん! Pure Rubyで書けば mruby でも問題なく使えるし! (ピコーン!)

で、書いた。ついでにJSONのフォーマッタも書いた。ガッとな。

実はこのトラブルのせいだけでもなくて、他にもエラーハンドリング向上のためにストリームフォーマッタ*5が欲しかったので、どっちにしろそのうち書く必要はあったのだった。

mruby側で実行時にRubyで記述されている定義が見付からない

これはmrubyでのビルドに成功した後でバイナリを実行したら起きた現象で、mrblib以下に(symlinkが)置いてある .rb ファイルに定義されてるクラスが見付からねーぞ、というもの。なんでやねん。

あれこれ試行錯誤したけど mrbgem.rake でビルド対象(?)のファイルを明示的に指定するようにしたらうまくいくようになった。具体的には rbfiles の指定を足す(デフォルトでは存在しない)。

spec = MRuby::Gem::Specification.new('msgpack-inspect') do |spec|
  spec.rbfiles = [
    "mrblib/msgpack/inspect/version.rb",
    "mrblib/msgpack/inspect/node.rb",
    "mrblib/msgpack/inspect/streamer.rb",
    "mrblib/msgpack/inspect/inspector.rb",
    "mrblib/msgpack/inspect.rb",
    "mrblib/msgpack-inspect.rb",
  ]
  spec.bins    = ['msgpack-inspect']
  spec.add_dependency 'mruby-io', mgem: 'mruby-io'
  spec.add_dependency 'mruby-pack', mgem: 'mruby-pack'
  spec.add_dependency 'mruby-print', core: 'mruby-print'
  spec.add_dependency 'mruby-mtest', mgem: 'mruby-mtest'
end

このとき最初は mrblib 以下のファイルを適当に glob で展開しようとしたんだけど、そうすると今度は undefined method __main__ とか言われてビルドに失敗する。 __main__ は mruby-cli のお約束として実行時の入口になるメソッドで tools 以下に置かれた .c のコードから呼ばれる。
ちゃんと書いてあるのに見付からないとかなんでだ!!! と思ったんだけど、どうもこれは rbfiles の最後のファイル(この例では mrblib/msgpack-inspect.rb")に無いといけないようだ。理由はちゃんと調べてなくてわからない……。ということで glob を使わずに明示的にファイルのリストを並べることにした。

mrubyでの文字コードの扱い

mrubyはM17N(多言語文字コード)をサポートしていない。ので、Stringオブジェクトはかならずどれかの文字コードということになって、これはmrubyのビルド時に決定される。具体的には MRB_UTF8_STRING が定義されていれば utf8 に、そうでなければバイナリになる。あと、これに伴ってエンコーディング操作関連のメソッドも String クラスに存在しない。

これはなかなか困ったことで、エンコーディング操作は普通にCRubyのコードだとあちこちに埋まってるので、そのたびごとに実装ごとの呼び出し切り替えみたいなのをするのもちょっと面倒くさい。
……ので、えいやっとそのへんを誤魔化すコードを mruby 向けのエントリポイントになるファイルに追加してしまった。

class String
  # It's only for mruby... Encoding of String are defined by MRB_UTF8_STRING (or undef it) on build time.
  # Default is disabled, and this tool is built under that configuration.
  def force_encoding(encoding)
    self
  end
  def b
    self
  end
end

Rubyだからこういうこともやってしまえる。べんり(?)。

まとめ

ちょいちょいハマる部分があったが、なんとかやっつけられた。

CRubyとmrubyの両方で動かせる状態になると、とりあえず試すならバイナリをダウンロードして実行、複雑な機能を使いたい場合にはrubygem版を入れて他のスクリプトと連携、みたいなことを選択的にできるようになるのでだいぶ便利だと思う。みんなもぜひ真似して他のハマりポイントを潰して回ろう。

(なお上述のことをだいたいやってくれる ruby-cli というツールを作ろうかと思ったのだが、この名前は既にrubygems.orgで取られていたのでやる気を失った。)

*1:CRubyのYAMLサポートはたとえば libyaml に依存する

*2:自分は tools/ 以下を持っていきわすれて一度盛大にハマった

*3:ように自分は理解している、が、理解が不十分な気はしている。後述。

*4:この場合は lib/msgpack/inspect/command.rb

*5:オブジェクトを生成するはしから、終端されていなくてもとにかく出力していけるフォーマッタ

msgpack-inspect を作った

MessagePackJSONぽいけどバイナリでデータサイズが小さく抑えられ、またシリアライズ/デシリアライズが比較的高速であるとして広く使われておるところであります。なんか #linedevday にいるせいで口調がおかしいな。

が、バイナリなせいでデータを作ったあとその内容が正しいかどうか確認するのがいまいち面倒くさく、いちいちunpackするスクリプトを書いて中身を見る必要がある。JSONみたいに目で見て思った通りの表現になっているかどうかを判別するのは普通の人間にはなかなか難しい*1
このため開発時にデータがバグってるのかコードがバグってるのかが分かりづらく、MessagePackを利用したアプリケーションおよびサービスを開発する上で問題になっていた。

ので、MessagePackのバイナリデータを食わせると内容を分かりやすくダンプしてくれるツールを作った! のです!

msgpack-inspect

github.com

Rubyで書いてあるがmrubyでクロスコンパイルしてあり、releaseページから各プラットフォーム向けのバイナリがダウンロードできる、ので、ダウンロードして展開すればすぐ使える。あとrubygems.orgにもリリースしてあって、CRuby/JRubyがあればその上に gem install msgpack-inspect でインストールもできる。

これはどういうものかというと、msgpackのバイナリが書かれているファイルを引数にして起動すると、以下のように(デフォルトではyamlで)各バイナリオブジェクトの format(型情報)、ヘッダ部の16進ダンプ、データ部の16進ダンプ、(存在する場合は)長さやtypeなどを表示し、その上でunpackした結果のデータも表示してくれる。

$ msgpack-inspect msgpack-example.bin 
---
- format: "false"
  header: "0xc2"
  data: "0xc2"
  value: false
- format: "true"
  header: "0xc3"
  data: "0xc3"
  value: true
- format: "nil"
  header: "0xc0"
  data: "0xc0"
  value: null
- format: "fixint"
  header: "0x01"
  data: "0x01"
  value: 1
- format: "uint8"
  header: "0xcc"
  data: "0xff"
  value: 255
- format: "fixarray"
  header: "0x93"
  length: 3
  children:
    - format: "fixstr"
      header: "0xa1"
      length: 1
      data: "0x61"
      value: "a"
    - format: "fixstr"
      header: "0xa1"
      length: 1
      data: "0x62"
      value: "b"
    - format: "fixstr"
      header: "0xa1"
      length: 1
      data: "0x63"
      value: "c"
- format: "fixmap"
  header: "0x83"
  length: 3
  children:
    - key:
        format: "fixstr"
        header: "0xa4"
        length: 4
        data: "0x686f6765"
        value: "hoge"
      value:
        format: "fixint"
        header: "0x6f"
        data: "0x6f"
        value: 111
    - key:
        format: "fixstr"
        header: "0xa3"
        length: 3
        data: "0x706f73"
        value: "pos"
      value:
        format: "fixarray"
        header: "0x93"
        length: 3
        children:
          - format: "fixstr"
            header: "0xa1"
            length: 1
            data: "0x78"
            value: "x"
          - format: "fixstr"
            header: "0xa1"
            length: 1
            data: "0x79"
            value: "y"
          - format: "fixstr"
            header: "0xa1"
            length: 1
            data: "0x7a"
            value: "z"
    - key:
        format: "fixstr"
        header: "0xa4"
        length: 4
        data: "0x7a65726f"
        value: "zero"
      value:
        format: "fixarray"
        header: "0x90"
        length: 0
        children: []

もちろん map や array などは子要素も再帰的に解析してダンプしてくれるという超絶便利なもので、これがあれば開発が進みまくることは間違いない。
なおダンプの出力フォーマットはオプションで指定でき、現状では yaml, json および jsonl *2 をサポートしていたり、引数としてハイフンを与えることで標準入力からの読み込みができたりする。

$ msgpack-inspect -h
Usage: msgpack-inspect [options] FILE"

Options:
  -f, --format FORMAT   Output format of inspection result (yaml/json/jsonl) [default: yaml]
  -r, --require LIB     (Not supported in binary executable)
  -h, --help            Show this message
  -v, --version         Show version of this software

CRuby/JRuby 実装に限り、オブジェクトのunpack時に ext type 定義が書かれているファイルを読み込んでそれを使うこともできる。MessagePack::DefaultFactory に register_type しておく必要があるけど。これで ext type を使っているデータ表現でもテストはばっちり!

ということで、大変便利なものを作った気がします。みなさんぜひどうぞ。

*1:……が、じつは訓練されるとだんだん分かるようになってくる、んだけども

*2:トップレベルオブジェクトひとつごとに1行のJSON表現で出力

#ISUCON 6 予選を戦ってなんとか通過した

今年のISUCONはどうするかなあ、出ないのも寂しいけど同僚や元同僚と出るのもなー……とか考えてたんだけど飲みにいった時に声かけてたらうまい感じのチームが結成できたので @joker1007 @tnmt との3人でチームJingisukan*1として出ることにしました。やった!

事前には一度だけ集まって、Azureをちょっとさわってみたり全体の流れを確認したり、とか。集まってのちゃんとした練習は結局やらなかった。個人的にはalpの使い方とかを事前に学習したくらいかな。

予選参加は2日目にしたんだけど、事前に2日目参加メンバーをidobataで見てゲエッとか思ってた……が、真の恐怖は1日目のトップ3通過チームリストを見たときに訪れたのであった。これシングルプr(ry

予選経過

開始前にちゃんと集まってわいわい。

開始後しばらく(1時間くらい)は事前の打合せ通り、インスタンスの立ち上げ、SSHログイン準備からのコードのgit共有、実際に自分でアクセスしてアプリを確認、アクセスログを変更してPerlRubyでそれぞれ初期ベンチ走行、といったあたりを実施して、それから手が空き次第コードを読んで全員でアプリケーションの中身を把握したり。

  • Perl初期スコア: 3628
  • Ruby初期スコア: 0

このとき isupam とかいう謎のプログラムがあって、うわっなんだこれという警戒心が先に立ち、考え無しに手元環境でのアプリケーション動作を諦めてしまった。なんとなく話しあってAzureのインスタンスでだけやるかーということにしたんだけど、結論だけ言うとこいつは失敗だった。マージ待ちや他の人の作業待ちみたいな時間が増えちゃってたいへん良くなかった。

それはともかくアプリの改修内容について議論。当初話した内容は以下の通り(だったと思う)。

  • static fileはnginxで返す
  • isutarのisudaへの統合、マイクロサービス的なやつ撲滅
  • / における keyword の N+1 の抹殺
  • keywordまわりをいくらかRedis化する
  • starも完全にRedis化する? 付けたユーザ名が入ってるんだよなあ
  • その後 htmlify が遅かったらなんか考える
  • その後 isupam が遅かったらなんか考える

元のアプリが遅過ぎたのと、(後から明確になったことだけど)ログインとかのどう見ても問題なさそうな処理でも大量に遅いレスポンスが出てて、なんだこりゃってなってこの時点ではあまり明確な方針が立てられていなかった。htmlifyが遅いだろうことは見ればわかるけど、というかそこしか明確に問題点になりそうな部分がなくて、本当か? みたいに疑ってたのもある。

まあとにかく確実に必要でやれることをやろう、みたいな感じでお昼前にある程度やっつけた。自分がisutar統合で盛大にハマって時間をロスト。じょーかーさんに助けてもらった。最初はあれこれやってみてもスコアがふた桁とかでウケてたが、まあちょいちょい直した結果無事上がってきたので安心したところでお昼。

  • keyword listのRedis化 + isutar統合 + static fileのnginx配信: 5006

お昼食べたあと、早目に疑念を潰しとこうと思って isupam へのリクエストにかかるレスポンスタイムを全部計測してみた、ら、特にワザと時間を稼いでいるとかいうこともなさそうだったので、あまり考えないことにした。これは正解だったように思う。検討しないといけない要素を順次減らしていくのは大事。
そんなあたりでじょーかーさんがガリガリと修正を入れる。

  • N+1抹殺: 3332、4143
  • Puma化: 5261

ログインまわりとかの軽い処理にやたら時間がかかっていたのは、本当に何か時間がかかる原因があるのか、それともhtmlifyとかの遅い処理にプロセスが占有されたせいで待たされてるだけなのかの区別が付かずちょっと混乱してたんだけど、Puma化により無事そのへんの処理の request time が下がっていったので、待たされてるだけだっためでたしめでたし、となった。今回ちゃんとそのへんは毎回アクセスログを確認しながらやっていけてたのは良かった。alp便利。

自分は htmlify の最適化にとりかかっていた。まずキーワードの置換処理を 2 pass でやっていたのを 1 pass に修正。キーワードにHTMLタグに関連する文字が含まれてたらこの処理は 2 pass じゃないといけないんだけど < も > も無いことは確認したので、まあいいじゃろ、と。
結論から言うと間違いだった……。シングルクォートを含むキーワードが「Kissin' Christmas (クリスマスだからじゃない) 」と「TV's HIGH 」のふたつあることが後から判明したのであった。見付けたときにクリスマスだからじゃない、じゃねー!とか半ギレになってた。しかしこういう確認とかでDBを叩くのとか、もう常様べったりだったので本当に助かった。ISUCONにはインフラまわり担当のスペシャリストがまじ欠かせない。

で、htmlify を 1 pass にしてみてもほとんどスコアは変わらず5000前後のまま。明らかに正規表現マッチが遅いのはわかってるんだけど、Perlにあった正規表現コンパイルモジュールとか Trie とかのが使いたい!と思って rubygems とかを漁ってみるんだけど見付からず。うう、Perlのエコシステムの強力さよ……。。。
他に String#gsub でマッチ対象に固定文字列リストを渡したりできないかなーとか見たり、あれこれもやってたけど、有効そうな手段が見付からず。

このまま悶々と時間が過ぎていって、15時半くらいでようやく htmlify 結果をキャッシュしないとどうにもならん、と決断。
今回のアプリケーションはキーワードが追加されると既存文書がすべて影響を受けるという性質がある上にレンダリング済み文書の生成コストが極めて重いので、正直、内容を把握したときからシングルプロセスアプリケーション向きだなーとは思ってた。思ってたけど、マルチプロセスでもそこに踏み込まないことにはどうにもならん、とようやく覚悟が固まった感じになる。正直、この決断が15時半というのは遅かった。

決断ができたのは cache invalidation にかかるコストが実は思ったより安かったというのもある。転置インデックスとか作りたくないよー、と悶々考えながらふとやってみた LIKE 全文検索が 0.1 秒で返ってくるじゃありませんか。おやおや。データが全部メモリに載ってると競技が変わるなあ。

UPDATE entry SET rendered = NULL WHERE description LIKE '%#{keyword}%'

これだけで cache invalidation できるじゃん! 便利!
データ量小さいからおもむろに entry テーブルで ALTER TABLE して rendered カラムを追加、htmlifyまわりをやるコードあたりをくくり出して適当なスクリプトにまとめて、id 7101までの全件に対してガッと実行した。ちょっとかかったけど10分くらいで完了。同時に keyword 追加時には htmlify を実行して rendered にストア、既存 keyword への description アップデートなら同じく htmlify して更新、description更新も無いなら rendered は触らない、という感じで。
あとは参照時、keywordに対して rendered が存在していればそれを返す、rendered が存在しなければ htmlify してストアした上で返す、あたりの変更をガツッとやって突っ込んだ。これが16時ちょうどだったらしい。

  • pre-rendered: 53872

上位陣が数万点とってるのに俺達は5000点……みたいに果てしなくダウナーな気分だったところ、いきなりスコアが10倍になってチーム全体の雰囲気が超改善した。pre-renderedなコンテンツを返すようにしたことでボトルネックが一気に解消、これまでの変更がようやく全部有効になったんだと思う。戦える、俺達も戦えるぞ!みたいな。

ただしここでエラーが大量に出ていて、なんだこりゃ、と思っていたらキーワードのマッチ順序に気を遣っていなかったことがわかった。エラーメッセージの意味がわかんなくて唸ってたんだけど、実際にレンダリングされてたページを見てみたら割とすぐわかった。実際の動作を確認するの、大事だなあ。
キーワードをぜんぶ最長マッチに直して、事前レンダリングのもやりなおして(ちょっと時間がかかるのがキツい)、えいやっと。

  • keyword match longer first: 108373

ほーらほらほら上位に来たぞ!!! みたいにテンション上がる。16時半。
しかしここでシングルクォートを含むキーワードでエラーが出ているのを確認。ベンチマークがPASSになってるんだけど、果たしてこれがどんだけの減点要素になってるのかが分からなくて、直すべきかどうかで悩む。個人的には、こういうエラーがどのくらいの減点要素になるのかちゃんとレギュレーションに書いてなかったのは出題のミスだと思う。(まあ難しいよね……。)

さて残りの時間でどうするか、ということでまたアクセスログを見たら / へのリクエストが予想以上に多く、当初はキーワード登録された後に必ず invalidation 対象になるので意味がないかと思ってたんだけど、やったほうが良さそうだね、という結果だった。そこはじょーかーさんにお任せしてやってもらい、他に何か無いかなあ……と探して回る。結局自分はずっと htmlify の中の正規表現をどうにかする方法がないか探してた気がする*2。思わぬボトルネックが隠れてないか、とかは継続的に常様に slow query log を見てもらったり。安心のクオリティ。

じょーかーさんが / のキャッシュを実装し終えたのが17時半過ぎ、これはギリギリかー? と思いながらベンチを回してみたら12万点くらい出る*3ものの、エラーが大量に並んでる。5万→10万のときの経験からして、ここでエラーを全部無くせば20万点くらいには行くんじゃね? などとテンションが上がってギリギリまでエラーを無くすためにbugfixとかやってた。
しかしあれこれ改善しても変わらず、「既に表示されたページが表示されています(GET / )」とかの意味不明なエラーが消えない。えぇー、インデックスページってキーワードが追加されたら既存キーワードは次のページにズレ込んだりするから、pagenation進めていったときに表示済みのやつが出たりするのは普通なんじゃないのー? とか不満をぶいぶい言わせてた。

この付近、スコアの改善どころかベンチを回すたびにスコアが大幅に下がったりすることもあって、うーんこれはダメか、みたいな感じでいったんはトップページのキャッシュ戦略を放棄、masterブランチに戻して再起動チェックしておしまいにしようか、10万点くらい出るはずだったし、ということになった。systemd上での各ソフトウェアの自動起動等は安心のtnmtクオリティで一発で問題なし。さあベンチ回しておしまいにしよう!

  • master branchに戻して再起動後: 約6万点

えええええええええええ、なんでだよ、ちょっと待ってよおい、みたいな感じ。さっきまで10万点前後だったスコアが急降下して、予選まあ通るんじゃないの、という空気から予選落ち確定に転落。おいおいおい。
このままじゃ正直マズいだろ、ちょっと待てよ、と緊急で相談、こうなったらもう今からトップページキャッシュ有効のコードにイチかバチか切り替えてチャレンジしよう、ということでブランチを切り替え、ベンチマークをエンキューして走行を待ってたら18時タイムアップした。走ってるのはなんとなくわかるけどスコアが採用されるかどうかはタイミング的にギリギリだし、結果の確認もできないしで、あぁー終わったー予選落ちだー、とがっくりして終わった……。。。





その後

完全に自棄酒モードになった我々はPCの前に居るとコードを読みながら悪い酒を飲んでしまう、ということで雨の中を近所の築地へ。日曜なのを忘れててだいぶ店も閉まってたんだけど、運よくナイスな店がやってて入れそうだったので店の前でしばらく待機。
……してたら、なんと発表された予選通過リストに我々の名前が! やったー!

築地の店の前でハイタッチしてた。スコアを見ると最後に走行したベンチマークが有効スコアとして認められたっぽい。結局最終スコアが10万点なので予選中の最高スコアに及んでいなくて、そういう意味では結局謎のスコア低下は残り、なんだったんだという感じはする。一方で最後の走行が有効扱いになるとはあまり思っていなくて、幸運に拾われた面もあるかもしれない。

まあとにかく通ったぞ、ということで。予選は通りゃいいんだよ通りゃ。勝負は決勝だ!



反省などあれこれ

とにかく @joker1007 さんの実装力が高くて、Rubyのコード改善はお願いしまくり。あと今回自分はMySQLやらsystemdやらは全く見てなくて、そこやら全部再起動便利スクリプトやらの足回りは @tnmt さんに完全に丸投げしてた。この2人とチーム組めてほんとに良かった、助かった、という結果だった。
最終的なアプリケーションコードも全体の設計やオペレーションも、今回の予選も完全に複数ノード構成にそのまま持っていける内容になっていた。ので、今回のチームも*4決勝でこそ実力を発揮できるチームだと思ってる。来月もがんばりたい。

自分については、もうこれまでRedisから逃げ続けてきたツケがついに、という感じ。来月までにRedis使ったWebアプリケーションを一度書いてみよう…… orz ということで反省しきり。そういえば俺のWebアプリケーション実装力はこの2〜3年は改善ゼロなのであった。もうちょっとちゃんとしよう。

今回のアプリケーションとベンチマークについては、htmlifyを理想的に高速化できるかどうかがまずひとつで、これはイチから実装して突破するか、ナイスな機能・モジュールのある言語を使うか、ということに尽きた感じ。Rubyまわりだとそのまま使えるものが何もなかったので少し厳しかった。まーC拡張をゴリゴリ書けなかった自分が悪いといえば、そのとおりかもしれぬ。
あとは生成したレンダリング済みcacheの共有や invalidation をやるのにシングルプロセスだと超絶やりやすいよなーってことで、これもまあ思うところが無くはない……が、予選はそういう傾向のことが多いので、分かっていたことではある。あるけど、マルチプロセスでも通用する構成でも今までは予選通過は固かったところ、今回はそのハードルがだいぶ上がっていた気はした。

キャッシュ戦略については、正直ベンチマークツールが出したエラーがどういう理由を示しているのかが分からなくて、本当に不整合が出ているのはどういうケースなのかを特定し切れなかった部分がある。
あとはコンテンツが正常にレンダリングされていないのにベンチは PASS になっている状態で、そのままスループットを上げる方向の判断をしちゃった方がじつは高スコアが狙えるんじゃないの、という……自分たちはエラーを消す方向に思考を向けて足を止めちゃったけど、あのままガンガンスループットを上げればいいんだったらそれなりの手はまだいくつもあったので、どうだったのかなあ、というのは気になる。

しかしコンテンツに関して出ているエラーがどういう減点要素になっているのかがレギュレーションのどこにも書いてなかったので、どう出たらいいのかが判断つかなかったんだよねー、ということで、これは決勝では改善されるといいなあ、というところだった。

全体を見ると、とにかく pre-rendered なコンテンツを準備する & キャッシュを頑張る、という方針への決断が遅かったのは最大の反省点。次はもっと決断を早目にしていきたい。あと手元環境は作ること、かな。本番ノード上でデバッグとかしてちゃ時間の無駄だ。

まとめ

出題者&運営の方々はお疲れさまでした! 決勝もよろしく! 決勝頑張るぞ!

*1:チーム名は優勝したら賞金で食べに行きたいものでなんかつけよう、という話をして決めた。綴りはどうなんだと思ったけど英語版WikipediaJingisukanって書いてあった!

*2:しかも徒労に終わった

*3:正確なスコアをメモり忘れてた……

*4:生ハム原木に引き続き

RubyKaigi 2016 に行ってきた & しゃべってきた

今年は京都で開催されたRubyKaigi、トーク募集に応募してたものが1件採択されたので、行ってしゃべってきた。

RubyKaigi 2016

京都ですよ京都。いいよねー。前日入りして飲みにいったり、最終日も泊まりにして飲みにいったりしていた。たのしい。カンファレンスとかで日本各地に行きたいなあ。


しゃべってきた

採択されたトークはこちら。

Modern Black Mages Fighting in the Real World - RubyKaigi 2016

内容的には実はヤパチーでしゃべったやつのマイナーアップデートだった*1んだけど、時間がこちらの方がちょっと長いので、もうひとつ実用例を足したりしていた。

Fluentd v0.14 におけるプラグイン互換性対応 『現代の"リアル"黒魔術師』登場 #RubyKaigi #RubyKaigiB - Togetterまとめ

おかげさまで会場は超満員、反応を見てもけっこう楽しんでもらえたようで、ひと安心というところ。内容としてはRubyメタプログラミングにおける基本的な操作(include, extend, singleton_class および prepend)を説明しながら、それを武器に現実の問題をどう片付けたかという感じ。これをやるべきかというと普段のお仕事では絶対にやるべきではないテクニックの塊なんだけど、しかしそれでも何をやってでも問題を解決しないといけないケースというのはごく稀にあって、そういう時のための知識としてはよいのではないんでしょうか。Module.new して define_method して extend するとextendされたオブジェクトのメソッドが呼ばれたときに define_method 実行時のローカル変数がいじれて便利だよね!!!!

行ってきた

RubyKaigiはなんといってもRubyコミッタをはじめ言語実装そのもの(や、その近い周辺)をやっている人達が多くて、サービス開発というよりはソフトウェア開発という視点にフォーカスしているように見えるのがなかなか毎年面白いなーと思いながら眺めている。CRuby以外にJRubyとかErRuby(Ruby on Erlang/OTP)みたいなのも出てくるしねえ。
聞いた中だと圧巻は @nalsh さんのクロージングキーノート、なんだけど同僚なのであれこれ知っている話もあるので印象としては @shyouhei さんの最適化の話かなあ。しかしRuby3x3はやっぱりそれをどう計測するかっていうのが今のところ抜けてて、これが実は来年に向けていちばん面白いことができるトピックなんじゃないかという気もする。

個人的には自分のお仕事に関係する点でもあって @_ko1 さんの並行性制御の話(Guildの話)が楽しみだったんだけど、1日目は体調崩して午後は帰って寝てたから、聞けなかった。1日目のAfter Partyも出られなかった。残念。

あとはなんか普段と違うふらふらした気分のままコードを書いたりもしていた。これはあとで別エントリに。

観光してた

京都、中学生のころに修学旅行で行ったのと、あとごく短期の滞在で行ったことが1回か2回あるだけで、あんまりちゃんと観光して回ったことがなかった。ので、泊まった近所の祇園とか四条とかを存分にふらふらできたのは満足。
あとカンファレンス翌日にせっかくなので御所と二条城だけ見てきた。

御所、いい場所だなあ。昔の情報で入るのはかなり厳しいというのを見てたんだけど、いま調べてみると行けば(手荷物検査くらいはあったけど)普通に入れる状態で、どこもかしこも良さそうな光景だった。あと周囲も含めた面積が広大すぎて、権力者というやつは……! みたいな感じがする。
次の二条城も、もう、京都のど真ん中にこんなの作っちゃってー、という感じだったけど石垣というか城塞はいいよね。

伏見稲荷に行く時間がとれなかったのが残念。まあ、人生またいつか行く機会もあるだろう。

まとめ

もっとカンファレンスを東京以外でやってくれるとあちこち行けて人生が楽しくなる。京都楽しかった!

*1:もうひとつ完全新作もCFPに出してたんだけど、こっちが通っちゃったので……