Write and Run

it's a simple way, but the only way.

函館に行ってきた

別に隠す理由があるわけではないし、X を見ればどこに行っていたかなんて丸わかりなのだけれど、下記のようなツイ……ポストを見かけたので敢えて伏せて(?)書いてみるかと思って書いてみます。対戦よろしくお願いします。

私と名前を出さないあのカンファレンスとの関係

恥ずかしながら、あのカンファレンスに参加したのは今回が初めてでした。もちろん存在自体はもはや思い出せないくらい昔から知っていたのだけれど、開催情報をずっと見落とし続けており、開催当日に Twitter(当時)に流れてきた楽しそうな写真を、みんな美味そうなもの食いやがって許せんなんで誰も教えてくれなかったんだと指くわえて見て悔しい思いをしてきました。

では今回はなぜ参加できたのかというと、これは今回のベストスピーカー*1でもある id:tomo_ari (osyoyu) のおかげによるところが大きいです。彼とは同じチームで毎年 ISUCON に出場しており、どうしても Ruby で優勝するぞという合理性のない目標を掲げて一緒に闘っている仲です。そして Ruby で勝つためには正しくパフォーマンス計測ができなきゃいかんということで、彼は pf2 という Ruby のプロファイラを開発しています。

ところが我々の ISUCON チームは仲が悪い[要出典]ことで知られており、直前の練習と本番以外で顔を合わせることはほとんどありません。そのため pf2 の開発状況はチームメイト間であっても共有されておらず、果たしてその年の大会で使えるのか、そのプロファイラで分析して何がわかったのかなどは長らく謎に包まれたままでした。

そんな中、ぼちぼち今年こそはなんとしても勝ちたい*2と思った私は、pf2 はどうなっているんだ、Ruby は遅いのか、100万円は獲れるのか、というようなことを問いただすべく彼のポストを漁りました。するとちょうど例のカンファレンスでそれに関する発表をするとの予告があり、かくしてようやく、私はチケット販売期間中に例のカンファレンスの存在を知ることができたわけです。チームへの情報共有を疎かにするのは悪いことばかりではないという学びがありますね。

私とはこだて未来大学

私がはこだて未来大学を訪れるのはこれが2度目です。1度目は6年ほど前、私がまだ前職にいたときでした。はこだて未来大学の学生を対象に集中講義(のようなもの。単位は出ません)をやっていいよと言われたので、当時まだマイナー言語枠だった Rust のハンズオンをやるべく訪れたのでした。C言語の講義の内容を初めて理解できたとか、低レイヤおもしろそうだとかといった反応をいただけてホクホクで帰ったような記憶がありますが、だいぶ昔のことなので都合の良い記憶が捏造されているかもしれません。

函館名物ラッキーピエロのチャイニーズチキンバーガーを初めて食べたのもそのときです。丸一日の講義ですから学生含めてお昼ご飯の用意が必要だったのですが、休日なので食堂などは使えません。困って未来大の学生さんに相談したところ、それならと教えてもらったのがラッピでした。というかラッピ以外ありえんだろみたいなテンションで言われたような気がします。当日も、お昼ご飯はラッピ?とかいうの用意しました、という案内に多くの学生が喜んでおり、当時ラッピの偉大さを微塵も知らなかった私はそんな大げさなと思っていたのですが、食べてみて確信しました。たしかに美味い。それ以来、年一くらいで食べに行っている気がします。

なんか未来大というか半分くらいラッピの話ですが、はこだて未来大学という地に再び遊びに行けるのはとても嬉しかったです。

スポンサーブースを回る

思い起こすと、今までいろいろなカンファレンスに参加してきましたが、セッションを聞くだけでスポンサーブースは回らないということがほとんどだったように思います。毎回登壇者として行くので、スライド作成に追われてて余裕がないとか、ロビーに突っ立ってると絡まれるのでそれで時間が溶けるとか、そういうことになりがちでした。

今回は友人の発表を聞くために行ったのでとても余裕がありました。スライド作成に追われないカンファレンス参加がこれほど気楽なものだとは思いませんでした。気楽さを体験して初めて、登壇者って大変なんだなと思いました。登壇者には懇親会では発表の感想を伝えたりして労ってあげましょう。登壇者のみなさんは猛烈なプレッシャーを乗り越えて懇親会に到達しています。

それはともかく、時間がたくさんあったので、今回はスポンサーブースをじっくり回ることにしました。そもそも、参加の目的の半分は「外の風にあたる」ことでした。pf2 の発表を聞くのももちろん大事でしたが、前日にスライド作成を手伝ったりでほとんど内容を知ってしまったのでそちらばかりに意識を向け過ぎる必要もなくなっていたのです。

私は、今はいわゆるウェブ業界*3から離れ、人工衛星スタートアップで働いています。そのため、今のウェブ業界にどういう企業があるのか、どういうビジネスをしているのかという事情にかなり疎くなっていました。業界は違えど、ソフトウェアエンジニアの活躍場所としては並列です。つまり、自分の未来の転職先である可能性があると同時に、今人材を取り合うライバルでもあるわけです。知らないわけにはいかないのです。

やや打算的な書き方になってしまいましたが、まぁそんなに深くは考えていなくて、私の狭い視野と拙い想像力では、ウェブ技術は EC サイトと UGC サービスしか作れないように感じてしまう(失礼)ので、他のみなさんは一体どんなビジネスに技術を使っているのかに興味があったということです。

今回は、私の知らない会社がたくさんブースを出展していました。私にとってとても嬉しいことでした。とりあえず社名を知らないブースに行って会社紹介の紙ペラをもらい、会社の概要や創業のきっかけ、お金を払ってくれるお客さんはどんな人・会社なのか、エンジニアリングのおもしろさはどこか、などを聞いてまわりました。私の全然知らないドメインでビジネスチャンスを見つけ、そこの課題を技術で解決している人たちの話を聞くのはとても新鮮な体験でした。ブースを回るごとに新しいことを教えてもらいました。事業ドメインのおもしろさだけを語る勉強会があったら行きたいかもしれません。

概ね各社共通しているなと思ったのは、そのドメインに詳しい人が創業しているということです。そんなの当たり前だ、と思うかもしれませんが、テックカンパニーはドメイン知識だけでは成立せず技術も必要です。しかし IT 技術はどんな分野とも隣接しているわけではありません。昔から距離の近かった金融や EC などはともかく、ヘルステックや不動産といった領域にも IT 技術が深く応用されるようになったのは、IT 技術がより広い範囲の人々に行き渡った結果だと思います。その結果、あるドメインエキスパートがたまたま IT 技術を持ち合わせているという確率が高まり、おもしろいビジネスが発生しやすくなっているのだと感じます。

あこがれとふるさと

私が参加に失敗しつづけていたからというのもありますが、このカンファレンスやこのカンファレンスに登壇している人々は以前から私のあこがれでした。私自身は Perl を書きませんが、Perl を書いている・書いていた先輩たちこそ、20年前、私がプログラミングを始めたときに憧れたハッカーの姿なのです。記号だらけのよくわからないコードを書き、伝わらないジョークを飛ばし、そういう仲間と酒を飲む、それが私にはクールでした。

いくらかの参加者とはそれぞれ別の場所で会ったことがありました。そのせいか、このカンファレンスに参加したのは今回が初だったのに、すごく懐かしい感じがしました。そうか、会に参加こそしていなかったけれど、いつの間にか自分はこの文化の中にいたんだな、そう思ったのでした。

初めて来るのにふるさと、そんな不思議な体験でした。

関係ない画像

*1:おめでとうございます

*2:毎年思っています

*3:というのが今日日まともな括りとして有効かどうかわかりませんが

OBS Studioの配信画面レイアウトのベストプラクティス

OBS Studio は現代の配信者にとって必須なソフトウェアです。かつては XSplit などのお世話になったこともありましたが、とくに YouTube Live や Twitch で活動している現代のアマチュア配信者のほとんどは OBS Studio を用いていると思います。かくいう私もそのような配信者のうちの1人で、Rust で Z80 を動かしたりリードソロモンを教わってゼロから実装したりする配信をしています(隙あらば宣伝): #ch789 / KOBA789 - YouTube

そんな OBS Studio ですが、配信画面のレイアウトを整えるのは至難の業であることが知られています。GUI の操作が難しいからでもありますが、レイアウトを司る「変換」という機能の仕様が難解なのが主因だと思います。日本語訳がわかりづらいこともその難解さに拍車をかけています。たとえば「変換」は "Trasnform" の訳なんですが、これはさすがに伝わらんと思います。他のアプリケーションに倣うと「変形」とかでしょうか。

この記事では「変換」の詳細な仕様については説明しません。言葉で説明したところでそれぞれの機能がプリミティブすぎて結局どのように使えばいいのかわからないからです。かわりにこの記事では、具体的にどのような設定にすべきかを紹介します。

基本の設定

シーンにソースを追加したらまずは「変換の編集」を開いてください。いきなりマウスで大きさを変えると不幸が始まります。うっかり大きさを変えた場合は「変換をリセット」で戻してから以下の設定をしてください。

以下の設定をした後ならマウスで動かしても大丈夫です。わかってきたら基本以外の設定をいろいろ試すとよいですが、まずは下記の設定から始めるのが安全だと思います。

「変換の編集」の原則

位置

そのままでよい。
座標を直接打ち込んで調整したいときはここに入力する。

回転

0.00° にする。
回転が入るとややこしくなるのでこの記事では扱わない。

大きさ

絶対に変えない。
大きさを直接打ち込んで調整したいときは後述の「バウンディングボックスのサイズ」を変更する。

位置揃え

「左上」でよい。
回転したいときは他の値にするがややこしくなるのでこの記事では扱わない。

バウンディングボックスの種類(超大事)

「境界の内側に合わせる」に設定する。この設定が一番大事。
この設定だと縦横比を歪められないが、縦横比を歪めたいケースはほとんどないのでこの記事では扱わない。うっかり縦横比が歪んでダサくなるほうがよっぽど不幸である。

バウンディングボックス内の配置

「中央」でよい。
それ以外でも悪いことは起きない。それぞれの効果が気になる人は試してみよう。

バウンディングボックスのサイズ

そのままでよい。
大きさを直接打ち込んで調整したいときはここに入力する。

バウンディングボックスにクロップ

この記事通りの設定では ON でも OFF でも同じ。
ややこしくなるのでこの記事では詳細を扱わない。

クロップ

よくわからなければ全部 0 でいい。入力データの端に映したくない部分や余計な部分がある場合にここで削れる。詳細は割愛するけどお好みで。

細かいことが気になる人向け

いきなり画面上でマウスを使って大きさを変えてはいけない理由

バウンディングボックスの種類」が「境界なし」のままマウスでサイズを変えると、「バウンディングボックスのサイズ」ではなく「大きさ」が変わるからです。

「大きさ」を変えてはならない理由

これを理解するには、シーン上のソースの大きさを OBS がどのように計算しているかを知らねばなりません。

バウンディングボックスの種類」が「境界なし」のときのシーン上の大きさは次の式で計算しています(クロップなどを無視しています)。

width = scaleX * sourceWidth
height = scaleY * sourceHeight

ここで、sourceWidth, sourceHeight はそれぞれ入力データの横幅と縦幅です。たとえばキャプチャカードデバイスが 1920x1080 で映像を取り込んでいれば、これらはそれぞれ sourceWidth = 1920, sourceHeight = 1080 となります。左辺の width, height はそれぞれ実際にシーン上で表示される大きさの縦幅と横幅です。

シーン上で表示される大きさは scaleX, scaleY という係数が乗されて決まるので、シーン上に表示される大きさを直接的に決めることはできません。

では「変換の編集」の「大きさ」を変更するとなにが起きているのでしょうか。実は width, height が変更されるかわりに、scaleX, scaleY が変更されます。つまり、狙った width, height になるように scaleX, scaleY が逆算されて保存されるのです。

これは入力データの大きさが変化したときに意図しない挙動を生み出します。例えば、シーンに背景画像を置いてそれをあとで差し替えるといったケースを考えてみます。

まず、シーンに「画像」ソースを追加し、背景画像を置きます。この画像ファイルの解像度は 3840x2160(4K) だったとします。つまり sourceWidth = 3840, sourceHeight = 2160 です。

しかし画像が大きすぎてシーンからはみ出してしまうので、あなたはマウスで大きさを調整してシーンサイズ 1920x1080(Full HD) ぴったりになるようにします。このとき、「バウンディングボックスの種類」は「境界なし」のままなので、内部パラメータ scaleX, scaleY が変更され、それぞれ 0.5 になります。

次にあなたはやはり別の背景画像に差し替えようと思い立ち、ソースのプロパティを変更して別の画像ファイルを読み込みます。この画像ファイルの解像度は 1920x1080(Full HD) だったとします。つまり sourceWidth = 1920, sourceHeight = 1080 に変わったわけです。

するとどうでしょう。シーン全体を覆うように配置されていた「画像」ソースは半分のサイズに縮んでしまい、960x540 の大きさになってしまいます。sourceWidth, sourceHeight が変わってしまったために、シーン上の大きさである width, height の値もそれに引きずられて変わってしまったのです。入力データの大きさが変わっても内部パラメータ scaleX, scaleY は調整されないので、入力データを変えるとシーン上の大きさが変わってしまうことがあるのです。

同様の現象はキャプチャカード等でゲームの画面を取り込んでいるときにも発生することがあります。ゲームが出力する画面解像度が変化すると、それに影響されてシーンのレイアウトが崩れてしまうのです。

COVID-19になった

2019年って何年前だよ。

今更ながら COVID-19 になりました。なってましたというか。

1/15くらいからやんわりと体調が変で1/18の夜に喉が痛くなり、1/19の夜に発熱。そのままするすると40℃まで上がり、布団から出れば寒くて震えが止まらない状態で、トイレに行くのも覚悟がいるといった具合。

かぜ薬は飲んだものの、朝になってもロクに熱は下がらず、1/20の夜も同様に40℃。食欲はあったし嗅覚異常とかもなかったのでご飯だけはめちゃくちゃ食べました。

1/21は40℃まで行かないにせよ38℃は切らないみたいな熱が継続。しかしながらこの辺りからじわじわと喉が痛くなってきました。

1/22になると喉が爆裂痛くなり、一方で熱は38℃弱くらいまで下がりました。喉の痛みと同時に咳が出るようになって体力と精神力が削られ始めました。抗原検査をしたのがこの日。

咳でロクに眠れないまま1/23になり、熱は微熱まで下がったものの寝不足と疲れでヘロヘロに。熱下がってやめてた風邪薬を改めて飲んだらこれがなかなか喉に効き、一筋の光に。

というわけでまだまだ調子悪くて回復しきってませんが、忘れる前にメモ。

相槌すらも喋れないレベルで喉痛いのはほんとにしんどいのでなんとかなってほしい。

総括2023

年末。 KOBA789 です。

本年もさまざまなことがありました。思い出したり反省したり、やんわりと近況報告したりするために書き残しておこうと思います。

年初

2023年はロケットの打ち上げから始まりました。私が管制システムの一部を担当した衛星が1月3日の夜中に打ち上がり、初の人工衛星運用を体験し、我々の至らなさあるいはコンピュータの活躍の余地について解像度を高める経験となりました。 仕事なので詳細は書けない。

また、友人に煽られて新車の購入を決意。煽られた翌週にディーラーに行き、即日サインしました。 買ったのはスズキ・スイフトスポーツ。よくも悪くも私に似合ってるのではないでしょうか。車のチョイスは残酷なほど当人の趣味が出ます。 このタイミングで車を買ったのは、欲しかったからだけではありません。仕事ばかりで遊んでおらず貯金が貯まっていたので、このままだとまたすぐ会社を辞めそうだなと思ったからです。車を現金で買えば一気に貯金は減るし、走りたくなって無駄遣いするはずなので好都合です。労働意欲を維持するために欲しい車を買うのは有効です。

古い方の車は必ず北海道に連れて行くと決めていたんですが、新車を衝動買いしたので時間的猶予がなく季節は真冬。詰んだかと思いきや、水戸のタイヤ屋さんがスタッドレスのレンタルをやっていたのでそこで借りて走って行きました。

仕事では採用活動に奔走していた……ような気がします。 思い返すと、今チームで共に仕事している同僚にはこの時期に入ってもらった人が多く、一年前の心細さをもはや思い出せないほどに毎日助けられています。

趣味の方だと、YouTube を放置して旅行三昧でした。5月の連休には新しい車で九州の南端、佐多岬まで自走で行きました。はじめ、MT 車を買うことに多少の不安があったのですが、3泊4日で3300kmも走ると左半身が意識の外でシフトチェンジをしてくれるようになり、実質 AT です。

また鹿児島旅行の翌々日には富士24耐にも行きました。テント張って観戦してたのですが、寝ても覚めてもエンジンの爆音が響いているというのはそれだけでおもしろいです。目で追いやすい速度で走る道路公団リバリーのフィットが最後まで楽しませてくれました。 現地とはいえ広いサーキット全体のことは見渡せないので、観戦は YouTube Live を観ながらです。セルラー回線でやると安定しないしギガが爆発してしまうしでいいことないので、Starlink を持ち込みました。いやー、快適。テクノロジーってすごい。

この季節も例に漏れず、いろいろありました。 私が作っていたものの一部が OSS になり、社外の人と仕事の話をしやすくなった……というのが一番嬉しかった出来事かもしれません。

夏はやっぱり北海道でしょ、ということで(一般人が簡単に到達できる)日本最北端に行きました。札幌の北の石狩あたりから稚内に向けての海岸線沿いには、オロロンラインという景色の綺麗な最高の道路があります。しかしオロロンライン周辺には宿町が少なく、ホテルというと留萌あたりにしか期待できません。 というわけで、人生初のキャンプをしました。先述の富士24耐で一応練習していたとはいえ、知らん土地でひとりでテント張って寝るというのはなかなか緊張します。ランタン忘れて真っ暗だし。 オロロンライン沿いに点在するそれぞれの町には、道の駅とキャンプ場と温泉がセットで存在しがちです。しかもキャンプ場は無料だったりします。車でのアクセスもよくて便利です。 1泊目のサイトが快適すぎて、2泊目にナメてかかったら、1泊目の撤収時にコオロギをテントに巻き込んでいたようで、いきなりペルソナノングラータとの戦いになりました。

手軽に行ける南端と北端に到達したので、ここらで旅行欲はおさまり、今年の秋の行楽シーズンはちょっとした日帰り旅行程度になりました。

仕事はボチボチというか、相変わらず忙しい日々が続きますねという感じでした。年始の打ち上げで燃え尽き気味だったけど、この頃チャレンジングな仕事がまた始まって、少しモチベーションが回復したかな。

プライベートの方では7,8年くらいお付き合いしていた相手と別れるなどのイベントがあり、仕事が一切できないくらいに精神はグラグラでした。相手に迷惑がかかるので詳細は書きませんが、大きな衝突があったとかではないので、双方を知っている各位に関してはあまり気にせず接してもらえると幸いです。 落ち込んでいる時期に支えてくれた古い友人各位には本当に感謝しています。持つべきものはよき友人です。

ここまで来ると最近の出来事です。 ISUCON は散々な結果でしたね。チームの力を引き出せなかったなぁという反省があります。1人だけ気張っていても勝てないというのが、チームで挑む ISUCON の難しいところです。それでも、チームメイトはいた方が楽しいのでチームで出場しています。ひとりだったら勝てるというわけでもありませんが。

年末には特番と称する長時間配信もやりました。年末特番という言葉の響きになんとも言えぬ憧れがあったり、RiJ のように風物詩化してる企画が好きだったり、年末年始って暇な時間多いよねという下心だったりで、年末の長時間配信は毎年やりたいと思っています。昨年末のように仕事の繁忙期が重なってしまうと難しいんですけどね。

企画の趣旨としては、Z80 という有名な CPU を(私が)知りたい、みんなにも知って欲しい、素朴な CPU を通じて低レイヤーに興味を持って欲しい、非定型な試行錯誤の記録を動画として残しておきたい(これはいつも)、みたいな感じでした。 長時間かつゴールデンタイムを外した配信にも関わらず、想像の倍くらいの方に興味を持ってもらえて大成功でした。 ちなみに「知って欲しい」「興味を持って欲しい」というのはつまり、同志を増やしたいということです。話の通じる友人は多いほど楽しいので、いつもそういうモチベーションで企画を考えています。私の企画が誰かにインスピレーションを与え、何かに挑戦するきっかけになれば嬉しいです。

来年に向けて

2024年の目標はすでに決めており、すでに実現可能性の検証も進めているところです。しかし、ここに書くのは恥ずかしいので、秘密にしておきます。

2024年の KOBA789 にもご期待ください。

非公式ISUCON練習プラットフォームISUNARABEを公開した

あなたの予想に反して、この記事の大半はタイトルとおおよそ関係のない感想文である。感情の発露に任せて書いているのであって、読ませるために書いているわけではないため、読みづらくても耐えて欲しい。

表題のとおり、非公式 ISUCON 練習プラットフォーム「ISUNARABE」をリリースした。
これは、ISUCON の素振り*1をより簡単にすることを目指して開発されたシステムで、近年の ISUCON 予選のポータルと似たような機能を持っている。
具体的には、過去問環境の CloudFormation テンプレートの自動生成・チーム編成・ベンチマーク実行・ベンチマーク結果確認などである。自動生成された CloudFormation テンプレートでスタックを作成すると、GitHub から取得したチームメイトの公開鍵が焼かれた状態のインスタンスが上がってくる、というあたりが重要なポイントだと個人的には思っている。公開鍵をチームメイトから収集して数台のサーバーに撒くというのは結構ダルい作業である。

こだわりポイントという意味だと、ベンチマークの実行と結果確認の UX にはちょっとしたこだわりがある。
私はかなりせっかちな性格である。となりに階段があるなら、エレベーターを待つよりも階段を上ることを選ぶ。結果的にエレベーターの方が早かったとしても、何も手を下せずボタンを押して待つことしかできないという時間が嫌いなのだ。鉄の箱が扉の前に到着するまでの間、人間が自由を奪われているのだ。許せるわけがない。
したがって、ベンチマーカーの実行は即時に開始され、ベンチマーカーのログや結果は「直ちに、遅滞なく」UI に反映されなければならない。
Pull 型でポーリングをするアーキテクチャは負荷分散という点において優れているのは知っている。それが理由かはわからないが、AWS のサービスは極端にストリーミング API を避けている。なんでもかんでもポーリングである。まぁこれは課金単位のわかりやすさのためもあるかもしれない。すべて邪推である。 さて、本物の ISUCON ではベンチマーカーは共有資源である。他チームの利用で埋まっている場合には即座に実行開始できない。これは仕方がない。また参加するチームの数は数百のオーダーであり、競技の参加者は千を超える。さらには、100万円という賞金(よりもプライド)が懸かった真剣勝負であるため、競技時間中のシステムダウンは可能な限り避けたい。つまるところ、ベンチマーク実行リクエストはキューイングされるし、ベンチマーカーのログを厳密にストリーミングするのは難しい。
しかしながら、ISUNARABE は練習プラットフォームであり、安定性よりも快適性が優先される。なぜなら、快適でなければ人はどんどん素振りをしなくなるからである。したがって、ベンチマークの実行リクエストは即座に受理され、ログは完全にストリーミングされる。ベンチマーカーは CloudFormation テンプレートに含まれており、チームに1台ずつ用意される。共有リソースではないのでキューイングは不要である。

この体験を実現するアーキテクチャは、意外にも自明ではない。最もナイーブには、各ユーザーのベンチマーカーが HTTP API を露出させ、ポータルのフロントエンドがその API を呼び出す方法である。ストリーミングが必要なため、これは WebSocket になるかもしれない。
残念ながらこのアイデアはいくらかの問題がある。どうやってベンチマーカーの Public IP を発見するのか、HTTPS で通信するなら証明書はどのように発行するのか、そもそもベンチマーカーのポートを開放して安全なのか、認証はどうするのか、等である。
そこで私は Reverse HTTP Transport のようなプロキシがあったらよいのではないかと考えた。私が管理するサーバーにプロキシを立てて、プロキシとベンチマーカーの間にトンネルを掘り、ユーザーはそのプロキシ経由でベンチマーカーにリクエストを送るという方針である。
この場合、プロキシで認証と認可を行うチャンスがあり、またベンチマーカー側からプロキシ側に向けてセッションを開始するようにすればポート開放も要らず、サービス検出も達成できる。
Reverse HTTP Transport を真面目に実装してもよかったのだが、証明書の管理が面倒ということと、私のサーバーは Cloudflare の裏側にあり TLS が貫通しない(クライアント証明書による認証ができない)ということから、アイデアだけを拝借して独自のプロトコルを作ることにした。

独自に開発したトンネリングプロトコルの基本的なアイデアは以下の通りである。

  • HTTP/1.1 (HTTPS)の Connection: Upgrade を用いて、ベンチマーカーからプロキシに向かってコネクションを張る
  • HTTP/1.1 なので、Authorization ヘッダに含めたトークンを用いて認証・認可をする
  • 開いたコネクションの上には HTTP/2 のリクエストを流す
    • リクエストの向きは プロキシ→ベンチマーカー

ポイントは、生の TCP ではなく HTTP/1.1 を用いることで、認証情報を乗せるチャンスを得ることと、その上のプロトコルを HTTP/2 とすることで、1本のコネクション上に複数のリクエストを多重化することである。
ここまでくればあとは gRPC のリクエストをこのトンネルの中に通すだけである。gRPC の Server Streaming を使えばログをストリームするくらいはなんの工夫もなく達成できる。

かくして私が納得できる品質の ISUNARABE はリリースにこぎ着けたわけである。趣味で作ったウェブサービスを一般公開するという行為は大変久しぶりであり、ヘタすると10年ぶりとかそういうレベルである。

サービスをひとりで作り、またそれを公開するというのは私にとって特別な意味がある。これは、ワールドワイドウェブが、なんらかの力に独占されているわけではないという事実を噛みしめ、技術者ひとりの持つ力の潜在的な大きさを再確認するための行いである。メモ帳で <CENTER> を書き、DynDNS で半固定 IP に無料のドメイン名を割り当てていた頃と、ウェブは本質的にはなんにも変わっちゃいないんだと感じることで、ある種の全能感を得られる。

ウェブサービスを作ろうとすると、あらゆる難しいことが襲いかかってくる。AWS やら Docker やらといった技術的なこともそうであるが、法律的な部分や、炎上したらどうするのか、攻撃されたらどうするのかといった本当に考えたくないことが多すぎる。法律やセキュリティの知識が不要であるとは言わないが、無知が責められる光景を日常的に目の当たりにしすぎているが故に、過度に萎縮しているのではないかと感じることがある。

個人開発者にはビッグテックのような優秀な法務部もなければ、インシデントレスポンスチームもない。SNS で盛り上がってしまっているユーザーとうまくコミュニケーションをとってくれるカスタマーサポートもいない。でもそれらがなければウェブサービスを公開してはならないのかといえば、そうではないと思う。

私はウェブがこれほど堅苦しくなる以前にだいぶ自由にやらせてもらった側であり、それによって今のキャリアがある。言ってしまえばイタズラを繰り返していたらいつのまにか職をもらえていたという状況である。自身がイタズラ坊主出身であるからこそ、スーツの奴らが嫌いである。結局ギークとスーツという古典的な二項対立かよと思われるかもしれないが、そうではない。ギーク出身であっても、若いギークが生まれる土壌を維持しないやつは全員ダサいスーツであるということだ。大人の事情とかはどうでもいいのである。イタズラ坊主のイタズラを邪魔するスーツだけは絶対に許せないのだ。

この文章にオチはないので、必然的に綺麗にはまとまらないのだが、ひとりでサービスを開発して公開するという行いは、ウェブが未だにイタズラ可能な土地であるかを確かめることであり、ウェブがイタズラ可能でなくなりそうなときは立ち上がらねばならない。さもなくばスーツである。

*1:ISUCON の練習のことをこう呼ぶ

リモートマシンから手元に向かってコマンドを実行できるやつを作った

KOBA789 です。寒い日が続きますね。こうもあまりに寒いとアイスを食べたくなるものです。昨日の私はその衝動に抗えず、コンビニでソフトクリーム(チョコ味とのミックス)を買ってきて食べました。余計に寒くなったのでもう二度とやりません。今はおでんが食べたいです。よろしくお願いします。

リモートから手元に向かってコマンドを実行したい

さて、寒いとアイスが食べたくなるように、リモートマシンに SSH でログインしていると手元でコマンドを実行したくなるものです。せっかくリモート接続してるのにね。人って不思議です。

たとえば、SSH 先の Linux マシンで code って打ったら手元の MacBook AirVS Code が起動してほしいわけです。VS Code の Integrated Terminal 内ならできますけど、そもそも VS Code のウィンドウが1枚も開いていないときには使えない技です。

Alacritty で SSH して、cd でプロジェクトのディレクトリまで潜って、さぁやるぞと思って code . と叩くと zsh: command not found: code の響きあり。がーんだな……出鼻をくじかれた。ただでさえ寒くてお布団から出たくない*1のにこれでは仕事する気になるわけがありません。エンジニアリングでなんとかしましょう。MacBook AirLinux サーバーを併用するなんていうトリッキーな開発環境でなければ困らない問題ですが、困っているのでなんとかするしかありません。

さらにいうと、KOBA789 は AWS の認証情報管理に aws-vault というツールを使っています。これはアクセスキーやセッショントークンを OS のセキュアなキーチェーンに保存してくれて Credential Provider の一種である Process credentials として振る舞えるというスグレモノです。要はキーチェーンに保存しておいたクレデンシャルを AWS CLIAWS SDK から透過的に使えるというわけです。

さて話が脱線しているようにも見えますがそうでもないのでお付き合いください。この aws-vault で使うキーチェーンが問題です。macOS であれば標準の Keychain Access.app 一択ですが、Linux ではいろいろな選択肢があり困ります。しかもどれもデスクトップ環境のない(= headless な)環境だと使いづらいです。ここはせっかくなので SSH 先の Linux マシンでも macOS のキーチェーンを使いたいですよね。あれ、これってもしかして VS Code の問題と同じ手法で解決できるのでは?

SSH Agent Protocol

そうと決まれば次はどうやって実現するかが問題です。SSH と関係ないサイドチャネルの通信でどうにかするのはセキュリティの観点からナシでしょう。せっかく Secure SHell のセッションがあるのですからこれを使うべきです。

では逆向きに SSH セッションを張るというのはどうでしょうか。リモートの Linuxssh temoto-machine するというイメージです。悪くはないように見えますが、リモートマシンから手元のマシンへの接続性が常にあるとは限りません。頻繁に持ち歩き、時には信用できないネットワークにも接続するかもしれない MacBook AirSSH のポートを開けておくというのはナンセンスでしょう。これもボツです。

というわけで見出しに書いた手法 SSH Agent Protocol目的外利用します。

SSH Agent Protocol とはその名の通り ssh-agent で使われているプロトコルで、ForwardAgent yes すると SSH 先のリモートマシンでも手元のマシンにしかない秘密鍵が使えるようになったりするアレです。元々そういう目的のプロトコルなので、伝送路はセキュアであると考えていいでしょう。

肝心のプロトコルの仕様については以下の IETF のページにあります。微妙に曖昧というか細かいところの詰めが甘いような気もしますが、そこは実地でパケットキャプチャしたりしてなんとかすればよいのです。

draft-miller-ssh-agent-04

プロトコルの大枠だけざっくり解説しておくと、フレームの長さに続いてフレームのボディーが流れてくるシンプルなフレーミングを用いて、1リクエスト=1フレームに対して1レスポンス=1フレームを返すというだけのシンプルな仕様です。レスポンスはリクエストと同じ順で返さねばなりません。

この SSH Agent Protocol には拡張機能のための仕様があり、message type = SSH_AGENTC_EXTENSION(27) としたメッセージの中身に拡張機能の識別子と任意のデータを詰めて送っていいことになっています。任意のデータを送れるので、つまりなんでもアリです。ここにコマンド名や引数を付けて送信し、stdout や stderr を返してもらえば目的は達成できそうです。

というわけで実装

論理できた*2のであとは yaru-dake です。日曜日を潰して実装しました。

あ、この記事は Rust Advent Calendar 2022 2枚目 day5 の記事です。なので Rust で実装しました。本当は発表の場がなくて困っていたところに Advent Calendar の空き枠を見つけただけですが。

github.com

まず agent(手元)側で次のように起動して SSH します。

$ echo $SSH_AUTH_SOCK
/home/user/.ssh/agent
$ ssh-rev agent --ssh-rev-sock /path/to/rev-sock
$ export SSH_AUTH_SOCK=/path/to/rev-sock
$ ssh remote-host

そして client(リモート)側で次のようにしてコマンドを実行します。

$ ssh-rev exec -- hostname
temoto-machine

きっと手元のマシンで実行した結果が得られるはずです。得られなかったらバグってます。残念でしたね。

Agent から Client へは任意のタイミングで送信できない問題

しくみとしては前述のとおりです。が、実は意外とテクい部分があったのでご紹介します。

SSH Agent Protocol は Client 主導のリクエスト・レスポンスモデルです。つまり、Agent の好きなタイミングで Client にメッセージを Push することは許されません。HTTP と同じセマンティクスだと思ってもらえれば納得しやすいでしょう。

しかしその通信モデルでは stdout や stderr の配送で困ります。それらは任意のタイミングでバイト列が発生しますが、これを Client に伝える術がありません。まぁぶっちゃけコネクションは張りっぱなしなのでプロトコルを無視すれば送れてしまうんですが、とりあえず足掻いてみましょう。

Client から Agent に送れるリクエストの種類のひとつとして、WATCH というものを定義しました。これを送信すると、Agent は

  1. 子プロセスの stdout から出力が発生
  2. 子プロセスの stderr から出力が発生
  3. 子プロセスが死んだ(exited)

のいずれかのイベントが起きるまで、レスポンスを返さずに黙り込みます。昔のウェブで使われた Comet と呼ばれるテクニックと同じですね*3

一見うまく動作しそうなこの設計にもまだ問題があります。Client は任意のタイミングで stdin のバイト列を書き込みたいのです。先述のとおり、SSH Agent Protocol ではレスポンスの順番を入れ替えてはいけません。WATCH のレスポンスが返ってくるまでは stdin の書き込みが成功したかどうかわかりません。

そこで、WATCH は stdin の書き込みリクエストでキャンセルできることにしました。WATCH のレスポンス待ち中に stdin の書き込みリクエストを投げると、先行する WATCH に対応するレスポンスは Cancelled が返るようにしました。

さあこれで解決、となればよかったのですが、これでもまだまだ問題があります。次のようなケースを考えます。

$ dd if=/dev/zero of=/dev/stdout bs=1M count=100 | ssh-rev exec -- cat > /dev/null

大量のデータを手元に送りつけ、cat で折り返してリモートに送り返すというシナリオです。一見うまく動きそうに見えますが実は途中で詰まります。さぁなぜでしょうか。この原因究明は読者の課題とします。

……とするとブーイングが飛んできそうなのでちゃんと解説すると、このような stdin への書き込みが頻発するようなシナリオでは WATCH リクエスト、つまり stdout の読み出しリクエストがすぐにキャンセルされ、stdin の書き込みが圧倒的に有利になります。その結果、stdout を読み出すチャンスがないまま cat のバッファが埋まり、やがて stdin への書き込みが永久に完了しなくなるのです。わかりましたか、読者?

ちなみにこれをうまく解決するプロトコルは実装しておらず、未解決です。もし使う人がいたら気をつけてください。

まとめ

SSH Agent Protocol の仕様に違反した実装しても誰も困らんのではないか……

おまけ

SSH 先から手元の VS Code を起動する設定

以下のようなシェルスクリプトreverse-code命名して PATH の通ったところに置きます。REMOTE_HOSTSSH 先のホスト名に置き換えてください。

#!/usr/bin/env bash

ssh-rev exec -- code --remote ssh-remote+REMOTE_HOST "$(readlink -f $1)"

そして、.zshrc に以下のようなコード片を入れています。Integrated Terminal 内でだけ使える code コマンドを殺さないようにしているわけです。

if ! command -v code &> /dev/null; then
  alias code=reverse-code
fi

SSH 先から手元の aws-vault を使う設定

~/.aws/config にこんな感じで書けばいいと思います。PROFILE_NAME, TEMOTO_PROFILE_NAME は置き換えてください。

[profile PROFILE_NAME]
credential_process = ssh-rev exec -- aws-vault exec --no-session --json TEMOTO_PROFILE_NAME

*1:KOBA789 の寝室にはエアコンがありません

*2:机上では成立することが確認できたこと

*3:†リアルタイムウェブ†

ISUCON12予選にRubyで出場して8位で予選通過した(ソレイユ)

霧矢あおい(KOBA789)です。

タイトルのとおり、ISUCON12予選に Ruby で出場して8位で予選通過しました。最終スコアは34635(ベストと等しい)です。

本戦出場は ISUCON9 ぶり2回目です。

チーム「ソレイユ」

今年は私の多忙もあってチーム解散の危機だったんですが(チームメイトにはご迷惑をおかけしました)、無事いつものチームで出場することができました。

霧矢あおい「無事、チーム『 ソレイユ』の一員になれました。ありがとうございました!」
ひとりチームからソレイユへの移籍に際して運営さんにお手伝いいただき、無事移籍を成功させたときの様子。

というわけでメンバーは以下:

アイカツ劇場版公開おめでとうございます。忙しくてまだ観られていません。

たたかいのきろく

うちのチームは例によって Git をほぼ使っていないので正確な記録はないのですが、今回は私が ToDo リストを書きながらマネージャーっぽいことをしていたのでそれをベースに書きます。

  • 10:00 競技開始
    • KOBA789 & s4ichi: レギュレーション読み合わせ開始
    • osyoyu: 競技用サーバーに入って開発環境構築開始
  • 10:20 頃
    • KOBA789 & s4ichi: とりあえずブラウザでアプリケーションを触ってメンタルモデルを構築
  • 10:30 頃
    • KOBA789 & s4ichi: とりあえずコードを上から下まで流し読み。雰囲気を掴む
    • KOBA789: 「SQLite3 がいつもの戦い方を阻害するので MySQL への移行を視野に入れた方がいい」
  • 10:50
    • osyoyu: 開発環境構築完了
    • all: KOBA789 から osyoyu へ、課題のアプリケーションのメンタルモデルを共有
    • osyoyu: 「いつものツール使うのに Docker 邪魔なので降ろした方がいい」
    • osyoyu & s4ichi: Docker 降ろし開始
    • KOBA789: DB へのアクセスパターンの洗い出しを開始
  • 12:00
    • KOBA789: DB へのアクセスパターンの洗い出しを完了。MySQL への移行が現実的な規模であることを確信
    • osyoyu & s4ichi: Docker 降ろし完了。いつものツールで開発・分析が可能に
    • KOBA789: 「移行を見据え、すべての SQL に名前をつけてメソッドに切り出したい。移行しなくても書き換えに役立つ」
    • all: 手分けして SQL 呼び出しのメソッド切り出しを開始
  • 12:30
    • all: メソッド切り出しが完了
    • s4ichi: 切り出したメソッドを使うように変更を開始
    • KOBA789: ranking のアルゴリズムの読解を開始(味集中カウンターへ)
    • osyoyu: visit_history の Redis 化を開始
  • 12:45
    • KOBA789: ranking アルゴリズムの読解が完了。player_score はプレイヤーひとりあたり最新の1行だけを保持すればいいことに気づく
    • KOBA789: player_score のデータ構造の最適化と MySQL への移行を同時に開始(player_last_score テーブルを MySQL に作る)
  • 13:19
    • KOBA789: player_last_score 対応のアプリのコード書き換えが完了
    • KOBA789: player_last_score の初期データ作成スクリプトの開発開始
  • 13:50
    • KOBA789: player_last_score の初期データが完成
    • osyoyu: visit_history の Redis 化がバグり散らしたので断念
    • osyoyu & s4ichi: player & competition の MySQL 移行を開始
    • KOBA789: MySQL 移行完了後の一手を考えるため、プロファイリングとタスク整理の旅に出る
    • なおこのへんまでスコア変化なし
  • 15:40
    • osyoyu & s4ichi: MySQL 移行完了
    • s4ichi: 移行完了を確かめるため、SQLite 周りのメソッドを noop に書き換え開始
    • osyoyu: いつもの分析ツールが使えるようになったため、MySQL にインデックスを貼ってスロークエリを潰し始める
    • KOBA789: player_score CSV の入稿で1件も保存されずに不整合でペナルティを食らう原因を調査開始
  • 16:18
    • KOBA789: ペナルティの原因は DELETE & INSERT によるデッドロックトランザクションが ROLLBACK してるからと判明。バルクインサートにして解決を試みる
    • s4ichi: 地道に N+1 を潰し始める
    • osyoyu: 完全に MySQL に移行できたため、満を持して複数台構成へ
    • このへんでスコアが一気に跳ねて25000くらいに
  • 17:00 頃
    • s4ichi: 「player の INSERT で dispense_id が遅い」
    • KOBA789: dispense_id を Redis INCR 化
    • osyoyu & KOBA789: visit_history の効率化に再挑戦開始
  • 17:40 頃
    • osyoyu & KOBA789: visit_history がまたもやバグり散らしたため断念
    • s4ichi: デバッグログを無効化
    • デバッグログ切っただけで1割くらいスコア伸びた
  • 17:50 頃
    • s4ichi: YJIT 有効化
    • YJIT は意味なかった(MySQL がパツパツで Ruby が遊んでいたため)
  • 18:00 競技終了
    • 17:00 から本質的な改善はなく、デバッグログを切ったことによるスコア向上だけでフィニッシュ

リモートソレイユと味集中カウンター

うちのチームはいつも1つの物理空間に3人で集まって戦うスタイルだったのですが、今年は初のリモートソレイユでした。

物理空間の大きな特徴として、ブロードキャストが容易であるという点があります。たとえば同じ空間にいる場合、その場で声を出せばなんの工夫がなくても他の2人に伝わるでしょう。

一方で、互いが遠隔地にいる状況ではそうではありません。音声はなんらかの通信手段を用いて意識的にブロードキャストしなければ伝わりません。これは物理空間と比較して(直感的には)不便な点です。

しかし私はこの特性を逆手に取り、従来あったコミュニケーションの課題を解決することにしました。

物理空間では「ブロードキャストが容易である」と述べましたが、裏を返すとそれは、受け取る情報のフィルタリングが難しいということでもあります。

私達のチームでは、しばしばペアプログラミングが発生します。難しい変更でバグが出たときなど、ひとりではパニックや視野狭窄に陥ってしまいがちなシチュエーションでは、ペアプログラミングはひとりよりも冷静に問題に対処できるため、2倍の人的リソースを投入してもなお結果として効率的なことがあるためです。

ペアプログラミングをすると必然的に声によるコミュニケーションの量が増えます。そしてこの声は、物理空間では必ずブロードキャストになります。ペアでないもう一人にも声は到達します。他人が悩んでいる声というのはとても気になるものです。特に私は気になってしまうタイプです。

たいていは2人だけで解決できますし、たとえ3人の知恵を同時に投入した方が解決が多少早かったとしても、3倍の人的リソースを投入するのが合理的な課題というのはそう多くありません。多くの場合、ただの野次馬根性で首を突っ込み、貴重な時間を浪費するだけになります。それくらいなら、チームメイトを信頼して背中を任せ、もうひとりは別の問題の解決に向けて深い思考をすべきでしょう。

やや遠回りな説明になってしまいましたがまとめましょう。従来あったコミュニケーションの課題とは、物理空間ではどうしても声が気になってしまい(主に私)、ひとつの問題に余計に多くの人数で取り組むことになって効率が悪くなることがある、ということです。

今回、私達のチームではコミュニケーションツールとして Discord を選びました。そして、上述の問題を解決するため、ひとり専用のボイスチャンネル「味集中カウンター」を用意しました。

チームのDiscordサーバーには、ひとりしか入れないボイスチャンネル「味集中カウンター」がある

他のチャンネル名が意味不明なのは見なかったことにしてください。語ると長い経緯はありますが、深い意味はありません。

ひとりで集中したいとはいえ、すぐに声をかけられないのでは不便です。時間の限られた ISUCON という競技では、メンション送ったけど反応がないというようなことがあると貴重な時間を無駄にします。

さて、Discord には他のツールにない機能として、あるボイスチャンネルにいるユーザーを強制的に別のボイスチャンネルに移動させるという機能があります。味集中カウンターはこの機能を使ったアイデアです。

まず、集中したい人は味集中カウンターに入ります。そして黙々と作業をします。もし、他のメンバーが一蘭を食ってる人に用があるときは、先述のボイスチャンネル間の強制移動機能で「再起動試験(あっさり)」チャンネルに呼び戻します。するとすぐに声をかけて相談等ができます。相談が済めば、麺が伸びないうちにまたカウンターに戻ります。

こうして、「集中したいときは他人の声をシャットアウトできるが、用があるときはすぐに声をかけられる」という仕組みが完成しました。

本戦に向けて

まずは Ruby を本戦に連れて行けて嬉しいです。やはり本戦に出るからには目指すは優勝です。Ruby にもう一度てっぺんの景色を見せてあげられるまで諦められません。

おまけ

感想戦配信のアーカイブがあるのでよかったら観てってください。はじめて凸待ち配信をしたのですが、たくさんの方とお話しできて楽しかったです。

youtu.be