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

たごもりすメモ

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

2016年を振り返ってみる

2016年のblogエントリを眺めてみたら13本、めっちゃすくなかった……。

年の前半くらいで済むかなと思ってたFluentd v0.14のコード書くのにとにかく1年中かかりっきりになっていて、ほとんどそれだけしかやらなかった。コードはめちゃめちゃ書いた*1からアウトプットが減ったとは言いたくないし、実際大きな成果だったとは思うが、もうちょっとこうバランスの取れた生活ができないものか。ううむ。

Fluentd v0.14

とにかくこれのコードを書いていた。

tagomoris.hatenablog.com

しかしblogエントリはこれ1本なんだな。とにかくひと段落つくまではドキュメントも後回しの勢いでやってたら年が終わってしもうた。ちょうど年末にひと段落と言えるところまで到達したので、年明けに残りタスクを片付けつつドキュメントを書いて、でひと区切りになると思う。
戯れに fluent/fluentd リポジトリで2016年の自分の変更を数えてみたところ 703 commits +46918, -19050 だったそうな。まだmasterにマージされてないものは入ってないけど。いやー、テスト書きまくったから多いな。*2

その他コード関係

小ネタばっかり。 msgpack-inspect というツールを作って便利!!! と思ったものの、完成した頃には自分の用は既に済んでいて、そのあと一度も使ったことがないというね……。*3

tagomoris.hatenablog.com
tagomoris.hatenablog.com

mrubyと格闘したのはちょっと面白かった。来年もいくらか使いそう。

しゃべってきた

あれこれ。海外のカンファレンスは一度だけかな。国内で英語でしゃべったものはあるけど。

仕事で他所の会社の人とかと英語で会話する機会が増えて、海外のカンファレンスでしゃべらないと! みたいな意欲が薄れちゃった感じはする。もうちょっと分散処理関連のカンファレンスにちゃんと行きたいなあ。
あと(主にTDの顧客関係で)呼ばれて他社の社内勉強会でしゃべったことが実は数度ある。毎回話の内容は変えてるんだけど、これは果たして喜んでもらえているのだろうか……。まあ、毎回それなりに(自分が)興味があることをしゃべっているので、いいかなあ。いいかな?

ISUCON6

負けた。人は負ける。

#ISUCON 6 予選を戦ってなんとか通過した - たごもりすメモ
ISUCON6決勝を戦って敗北した - たごもりすメモ

Webアプリケーションの開発・運用から離れて長いし、今年なんかTDの運用すら完全ノータッチでミドルウェア開発しかやってなかったからなあ、というのは言い訳です。ううう。来年はもうちょっとHTTPの気持ちを考えてコードを書く機会が増えるはずなので、もうちょっと、こう、こう。

その他

テキトーな放言ネタは今年はこのひとつだけ書いた。

tagomoris.hatenablog.com

いろんな人がいろんな元ネタを想像して読んでいたらしいが、残念、全部ハズレです。少なくとも自分のところに聞こえてきた限りは。

来年

仕事のしかたもその他も、今年とはだいぶ方向を変えたいとは思っているので、またあれこれ挑戦することになりそうです。退屈しないなあ。
次の1年もよろしくお願いします。

*1:しかも全部OSS

*2:なお現在 fluentd/lib/**/*.rb は19008行、test/**/*.rb は30785行らしい

*3:frsyukiが知らない人にこういうのがあるよって紹介してるのはなんか見掛けた

YAPC::Hokkaido 2016 に行ってきた

いやあ、雪がすごかった……。

YAPC::Hokkaido 2016 SAPPORO

今年からちょっと趣を変えて復活したYAPCが札幌で行われるとのことで、最近Perl書いてないけど話は面白そうだから旅行ついでに行くかーという感じで。今回は発表もなしで、単純に参加者として楽しんできた。

f:id:tagomoris:20161215160229j:plain

カンファレンス

色々面白い話があったけど、個人的にはちょっと仕事でこれからやるので気になっていたWeb API関連のトークふたつがすごくよかった。

YAPC::Hokkaidoで「PerlでAPIを作る時に僕達が考えたこと」というトークをしました - Masteries
YAPC::Hokkaido 2016 - iOS/AndroidアプリにおけるAPIサーバーのいろは

どちらも実践的な経験談の話で、ついやってしまいそうな失敗となぜそれが失敗なのかという理由がきちんと述べられているなど、非常に学ぶべきところの多いトークだった。それもあってか(?) xaicron さんのトークがベストトークとして選ばれてたけど、自分はどっちかというと papix さんのトークのほうが好みだったなあ。単に問題意識がどこにあったかだけかもしれないけど。

f:id:tagomoris:20161215160320j:plain

とりあえず "Web API: The Good Parts" は買ったぞ!

f:id:tagomoris:20161215160347j:plain

全体的にはトークを聞いたり聞かずにコードをいじりながら雑談してたりスープカレー食べたり。自分のトークがないとずっとお気楽にいられて大変よい。

f:id:tagomoris:20161215160442j:plain

懇親会など

前夜祭と懇親会は普通にパーティーでわいわい。東京から大勢行ってたので雰囲気が渋谷な場所が各所に。いいことか悪いことかは知らんが気楽に楽しめてよい。

f:id:tagomoris:20161215160715j:plain
f:id:tagomoris:20161215160756j:plain
f:id:tagomoris:20161215160853j:plain
f:id:tagomoris:20161215160920j:plain

ホテルをすすきのにとってる人が多いのもあってか、前夜祭や懇親会のあとはそのままみんなで移動してあれこれ飲み食い。金曜土曜だったけど、入れた店はだいたいおいしくて最高な感じ。札幌ほんとうにいい街だよなー。さいこうだよなー。

f:id:tagomoris:20161215161002j:plain
f:id:tagomoris:20161215161020j:plain

すすきの中心に良い感じのクラフトビアバーも見付けたので次に行くときも安心。

f:id:tagomoris:20161215161105j:plain

なんか12月としては二十ウン年ぶりの大雪だったとかで、めちゃめちゃ降ってた。往路の金曜日でも降雪の影響でLCCとかは欠航もあったらしい。ANAでもちょっと遅れて着いて、雪のなか頑張って前夜祭会場まで行った。

f:id:tagomoris:20161215161143j:plain
f:id:tagomoris:20161215161209j:plain

カンファレンス当日の大雪はもう本当にひどくて、一瞬で地下鉄で行くのを諦めてタクシーで会場まで。それでもタクシーどこで降りたらいいか分からないくらい雪だらけで景色がよくわからんことになってたし、何十センチも積もってるし、ギャーというところ。お昼食べに会場の隣のショッピングセンターに行っただけで精一杯だった。

f:id:tagomoris:20161215161258j:plain

札幌中心街でもあちこちで雪が吹き溜ったり妙な形の圧雪があったりアイスバーンがあったり。いやあ、雪を堪能したなあ。

f:id:tagomoris:20161215161342j:plain
f:id:tagomoris:20161215161348j:plain
f:id:tagomoris:20161215161612j:plain
f:id:tagomoris:20161215161630j:plain

帰る予定の日曜日もやっぱり大雪。朝から欠航が相次いでいるのはわかっていたが自分の便は午後で、午後から天候が回復するっぽい予報だったし、実際昼過ぎくらいからは大丈夫そうだったので安心してた……ら、夕方にいきなり崩れたっぽくて欠航。人生で初めてフライトが欠航の憂き目にあった。

f:id:tagomoris:20161215161416j:plain

その日のうちの便や翌日朝の便はすべて満席で、あっさり翌日月曜日の午後に……。札幌にもう一泊する羽目になってしまった。まあこういう旅もいいさ。ちょっと余計に観光する時間ができた。*1

まとめ

f:id:tagomoris:20161215161709j:plain
f:id:tagomoris:20161215161725j:plain

札幌は最高でした。どう最高だったかというと、飲み食いしたものを順に並べるとご理解いただけると思います。
味噌ラーメン、クラフトビアバー、前夜祭、うまい居酒屋、スープカレー、懇親会、カニやイクラ丼(白米抜き)、クラフトビアバー、ジンギスカン、寿司と日本酒、日本酒飲みまくり居酒屋、カニウニイクラ海鮮丼およびカニ汁、スープカレー

f:id:tagomoris:20161215161749j:plain

*1:というか最初は日曜にちょっと見て回るつもりだったんだけど雪で不可能だったのが、多少取り返せたとも言える

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表現で出力