時計を壊せ

駆け出してからそこそこ経ったWebプログラマーの雑記

YAPC::Kyoto 2020~2023の思い出

3年前、2020年の2月にこんな記事を書きました。

blog.perlassociation.org

まだその脅威の全貌が明らかにならないままに刻一刻とタイムリミットは迫り、 YAPC::Kyoto 2020のために積み上げてきたものとそれをその状況下で実施することのリスクを直視して、頭を抱えました。

自分たちの本業は当然ながらイベントの運営ではありません。 その一方で、イベントの開催には様々な準備が必要になってくるため、それなりの労力と時間がかかります。 それを誰もが持て余しているわけがないように、スタッフも暇だからそれをやっているわけではないのです。

では、それをどこから捻出するかというと、趣味や大切な家族との時間を削ったり、睡眠時間と体力を削ったり、業務調整をして同僚に助けてもらうなど、人によってその程度の差はあれど全く何も犠牲にしていないという人は少ないでしょう。 そういった、掛け替えのない時間をもらうことによってYAPC::Kyoto 2020の準備は進められてきたことを、自分はよく理解していました。

イベントは一点物です。 イベントの準備のほとんどは「その日」のためにしか使えないものであり、つまりイベントを中止するという判断はそのために準備したもののほとんどを無意味なものにしてしまうことを意味します。 例を挙げると、イベントの内容は会場や日程に大きく左右されます。必ずしも同じスペースが同じ条件で借りられるとは限りませんし、どこでどのようなことをやろうとしていたかという構想も簡単に崩れてしまいます。ゲストとして呼ぶ人々の都合がつくかもわかりません。

和気あいあいと、こんなことが出来たら面白そうだ、こんなことができるとみんな嬉しいのではないか、という具合で盛り上がっていた運営メンバーの様子を思い浮かべながら、 中止を考えざるを得ないこの変化にどのように対処するか、中止以外になにか打つ手はないか、現状でベストな打ち手はなにか、といったことをひたすら考えていました。

そして、2時間近い話し合いをオンラインで行って結論を出し、以下のようなブログエントリを書きました。

blog.perlassociation.org

あえて「開催見送り」「延期」という言葉を使い、「中止」とは書きませんでした。

イベントの延期は、それを言うこと自体は簡単ですが、それを実際に行うことは簡単ではありません。 その準備のほとんどが無駄になったことにもう一度前向きに取り組むためには意思が必要です。 そして、延期を提案するということは「いままでと同じくらいの労力と時間をもう一度ください」と言うことに等しいので、延期したいということへの共感が必要です。

しかしながら、イベントのスタッフをやった報酬は、イベントを開催してそこに参加したり関わった方々からの感謝、それだけです。 そして、これは開催することで初めてそれを得ることができます。まだそれがない状況で、延期としましょうと言ったところでそれは本当にできるのでしょうか? 最初はよくても、燃え尽きる、折れる、といった形容をされるような状態になってしまっても不思議ではないと思います。

とりあえず言っておけば良いという意見もあるかもしれませんが、もし期待値を高めてそれを裏切るという行為になってしまうと、信頼を毀損してしまいます。 自分自身のことであればそれでもよいかもしれません(よくはない)が、YAPCという世界中で何代にも渡って主宰が交代してきたカンファレンスは自分だけが築き上げたものではない、これまでYAPCをつくりあげてきた人々から受け継いだ大切な財産です。

「延期」と言うからにはたとえ誰も手伝ってくれなくてももう一度やる覚悟を持つ、そういう気持ちが自分にとっては必要でした。 YAPC::Kyoto 2020のメンバーは延期に対して前向きに向き合ってくれて、延期と決めた話し合いでも「絶対やるぞ!」となって終わるくらいに前向きでいてくれたので、それに背中を押してもらったことであまり迷わずに延期と言うことができました。

そうなれば、コロナ禍でイベントがやりづらくなっても、なんとかYAPCを開催するための土台を維持したいとなります。 コミュニティは生き物であり、集まるきっかけが失われると死んでしまいます。

オンラインでそれをどうするか、悩んだ末にJapan.pm 2021、YAPC::Japan::Online 2022というイベントをやりました。 このあたり葛藤とそこからどうしたかはイベント振り返り Meetup #2というイベントでも話したとおりです。

speakerdeck.com

そして、結果的にはこのようにYAPC::Kyoto 2023として復活を果たしました。 いろいろと不安を書いていましたが、実際はどうだったか。

なんと、YAPC::Kyoto 2020を手伝ってくれていたほとんどのスタッフが引き続き手伝ってくれました。そうでない方も3年のあいだに状況が変わって手伝えなくなってしまった、当日の予定が合わなくなってしまった。といった方ばかりで、自分の知る限りでは100%のメンバーが延期開催の実現に引き続き前向きでいてくれました。 先に書いたような不安は幸いにも杞憂に終わり、id:azumakuniyuki さん id:papix さんを中心に多くのメンバーが高いモチベーションでまた新しいカンファレンスを作ることに尽力してくれたのです。

結果的に、(ブランクが空いたことなどを加味しても)残念ながらトラブルは少なくはありませんでしたが、 それを踏まえても「とても良かった」と参加した人々に言ってもらえるような、多くの人の心に残るカンファレンスとなったのではないかと思います。 ぼくも、とても良いカンファレンスになったのではないかと思います。

この彼らの前向きさとこの結果に力をもらって、自分たちにとって掛け替えのない場をこれからも続けるために、自分にできることを引き続きやっていきたいと僕も心を新たにすることができました。 そんなわけで、YAPC::Kyoto 2023を作り上げてくれたメンバーに僕は感謝をするばかりです。本当にありがとうございました。


いろいろと思い出して感慨深いものがありました。という話でした。

YAPC::Kyoto 2023ではぼくはスタッフ業として溢れたタスクを拾ったり、あるいは #ぶつかり稽古 のリバイバルイベントの司会進行兼実況解説役やLTもやりましたが、その話はまた別で書こうかと思います。

UNIVERGE IX2105で宅内ネットワークを構築した

とある分譲マンションを購入して入居したところ、棟一括契約のインターネット回線以外は引けないようで、それを使わざるを得なかった。 宅内への配線は1000BASE-Tが1本あり、宅内に設置されたスイッチングハブを通じて各部屋へ配線されている。 ネットワーク的には少なくとも他住居のネットワークへルーティングされることはないようだった。 速度も実測で下り600~700Mbpsが出ていて概ね問題がない。ということで普通に使うぶんにはほとんど不満はなかった。

唯一心配なのがマンション側のネットワーク構成がブラックボックスであることで、まあおそらくそんなに問題ない構成にはしているのだろうとは思いつつコントロールの範囲外で安全性が保たれていることにはちょっとだけ不安がある。 あと、NASを使うにあたってDHCPでNASのMACアドレスに対してIPを固定アサインしたい(でないと使いづらい)のだが、ルーターは家の外なので権限があろうはずもなく、まあIPが変わる機会もそうそうないはずではあるのだがこれもまたちょっと不安。 ということで、明確にネットワーク的に宅内と宅外を分離しつつ外からのパケットを一定ブロックしたい。ということで家の中の最前段にルーターを設置したくなった。

なお、それまではどうしていたのかというと、光回線やCATVなどの終端装置にAterm WX3000HPを置いていた。 しかし、引っ越しに伴って家のレイアウトが変わったことでアクセスポイントを置きたい位置と配線の来る位置が変わったので、これは単なるアクセスポイント兼スイッチングハブにしてルーターは別途調達しようという考えになった。

社内で雑に相談したところ、中古のUNIVERGEがよさそうというオススメをもらい、中古で型落ちなら値段も手頃だったのでそれでやってみることにした。 ということで、UNIVERGE IX2105を購入したのでした。ついでにネットワークのお勉強もできる。

構成

超ざっくり

WAN(※)--->[IX2105]--->[WX3000HP]

※便宜上、WANとしているが、実態はたぶんマンション共用部に構成させたネットワークになっていそう

やったこと

まず、コンソールケーブルを持っていなかったので初期設定に困った。 いまどきRS-232Cのコネクタなどそうそう載っていないわけで、うちの場合は家にMac系しかなかったのでUSB-Cからなんとかする必要があった。

そこで買ったのがこちらです。

RS-232Cのコネクタを経由せずに内部的にコンソール用のRJ45に合わせてよしなにしてくれて便利。 コネクタの仕様はよく知らないけど様々なルーターに対応しているようだし、業界標準的な仕様があるのでしょう。知らんけど。

ドライバはここから入手した。

ftdichip.com

家にM1(arm)のMacしかないなか、macOS向けにはx86_64のドライバしかなくて、しかもダウンロードしてインストールしようとしたらエラーが出て焦ったけど、 よくよく読んだらApplicationフォルダに入れて実行しろということらしく、ほんまかいなと思いつつ言われるがままに入れて実行したらあっさり成功して問題なく使えた。 なんかmacOSによってよしなになっているのでしょうたぶん。

あとはマニュアルを見ながらよしなに設定。 ぐぐって出てきた設定みても結局なぜそういう設定になるのかが分からないので、ちゃんとマニュアルを読んで基礎的な操作方法や概念を理解してから設定するほうが早かった。

構成的にPPPoEとかもする必要もなくいので普通に設定をしていく。

WAN側のインターフェースはDHCPでIPを受け取りつつ、Proxy DNSとNAPTを有効化したり、 パケットフィルターやキャッシュを設定したり、NetMeisterに登録してファームウェア・アップデートをかけたりなど。

NTPはホスト名で指定できないのがちょっと微妙で悩ましかった。

よし

まあ、とりあえず良い感じになったと思うので、しばらくこれで運用してみる。 速度も元のスイッチングハブでやってたときと同等の速度がでているし、パケットフィルタもいれたことでちゃんとネットワーク的にも分離したうえで必要のないアクセスをある程度は排除できていそうで当初の目的が達成できてよかった。

Google Cloud Workflowのエミュレータを作り始めた

github.com

Google Cloud Workflowのエミュレータを作り始めた。 標準関数などもだいたいひとまずの実装が終わってきて、ある程度動くようになっている。

Cloud Workflowを検証・導入する機会があって、便利な一方でエミュレータが提供されておらずローカルでの動作確認に困ったのもあり、いっそ自作してみるかという気分になった。 あと、一度は処理系っぽいものを作ってみたかったというのもあって、それもそこそこ大きなモチベーションだった。

Cloud WorkflowはYAMLかJSONで手続きを記述するとそれをクラウド上で動かせるというもので、 Cloud Functionなどとの違いとしては制約が多く複雑なことがやりづらい(できなくはない)のでオーケストレーター的な役割などに集中させやすいこと、 また冗長性とフォールトトレランスに力がいれられておりゾーン障害などが起きても実行の継続が保証されたりリトライが簡単に記述できるあたりが良いと思う。

詳しくはドキュメントを見るとよさそう:

cloud.google.com

どうつくったかというと、GoだとCIからバイナリを落としてくるだけで動く世界観になってインストールが楽そう && 慣れているのでGoで作ることにした。

Cloud Workflowの構文はYAMLとJSONで書けるのだが、お馴染みの github.com/goccy/go-yaml が実は YAMLToJSON というインターフェースが提供されており、さらに一定のパターンは持つものの決定的に構造が定まるわけではないので一部を json.RawMessage にしてその親の構造に応じて遅延評価するような実装ができると楽そうだと踏んでそういう感じで実装していった。

JSONのデータ構造を解釈して、それの実行順序を組み、そのとおりに実行していく簡単なインタプリタを基本としている。

ほか、式や変数を扱えたり for によってスコープが作れたり、関数呼び出しが式のなかでも行えたり、演算子に優先順位があったり動的型付けながら型の概念もあったりなど、諸々をうまく解決していく必要があってなかなか大変だったし勉強になった。このあたりは機会があればどこかで話したい。

というわけで、OAuth2を使って(Google Cloud APIではなくGoogle CalendarなどのAPIである)Google APIを叩けないなど一部困った問題はあるが、 おおまか動くっぽい感じになってきたので、Cloud Workflowを使っている皆様におかれましては、ぜひ試してみてほしいです。(まだまだテスト不足でバグってるところも多いかとは思いますが。。)

ISUCON12予選に1人で挑み惨敗を喫しました

FAILでスコア0でフィニッシュです。ありがとうございました……。

経緯

近頃、チームで仕事するの難しいなーと感じていたところ、気が狂い、いっそのこと俺一人で全部やりてーとなって、血迷いました。難しさから逃げるな。

やったこと

最初に動いているコードはGoでした。

Goのまま行ってもよかったのですが、せっかく好きな言語であるところのPerlの初期実装が用意されているわけだし、Perlを使うことでチームメイトが手を出しづらくなるわけでもないし、RDBを使った開発経験*1としては一番長いPerlで挑もうということでPerlに切り替えます。 なにより、Perlで勝てたらカッコいいじゃんと思っていました。負けたが……。

兎にも角にも、まず計測。

ベンチを回し、事前に用意したAnsibleによってセットアップしたNetdataでOSリソースの消費バランスを見つつ、アクセスログをalpで集計しました。 ISUCON11予選と同様にセキュリティグループの変更は制限されていそうだったので、こんな具合でnginx経由でNetdataにアクセスできるようにしておきます。

        location /_netdata/isucon12q-1/ {
            access_log off;
            rewrite /_netdata/isucon12q-1/(.*) /$1 break;
            proxy_pass http://127.0.0.1:19999;
        }

        location /_netdata/isucon12q-2/ {
            access_log off;
            rewrite /_netdata/isucon12q-2/(.*) /$1 break;
            proxy_pass http://192.168.0.12:19999;
        }

        location /_netdata/isucon12q-3/ {
            access_log off;
            rewrite /_netdata/isucon12q-3/(.*) /$1 break;
            proxy_pass http://192.168.0.13:19999;
        }

https://github.com/karupanerura/isucon12-qualifier/blob/main/ansible/files/config/nginx.conf#L113-L129

オープニングムービーの通り、ランキングを提供するエンドポイントが圧倒的な処理時間を占めています。また、I/O waitが大変に高い値を示していることが分かりました。 (本当はtopも見る手筈だったけどうっかり抜けていた。。)

では、と、実際にコードを読み始めてみるのですが、SQLiteだったので少々面食らいます。コード量も多く、焦りを感じます。

コード量も多く一人なのもあってつい焦ってしまい、I/O waitの原因は都度openしているSQLiteを通じた、ディスクへのアクセス過多だろうと早合点してしまいました。 本来はちゃんと計測して切り分けをするべきです。

スキーマを見ると明らかにIndexが効いていないのでとりあえずIndexを貼ってベンチを流したところ、ほとんどスコアが変わりません。

実は、このとき、実はスキーマの定義ファイルだけを変更しただけで初期データに対してIndexを貼るのを忘れており、またしても早合点していました。 これでは新しく作られたテナントに対してしかIndexが効かず、実際にはID=1のテナントがかなりヘビーに使われていたため、ほとんど効果がないのは当然です。 この結果をすぐに疑うべきでしたが、ここでSQLiteのままチューニングすることに対する懐疑心が強くなります。

また、SQLiteのままでは、普段使っているMySQL向けのツールが使えません。 Profilerも用意されていましたが、初見のプロファイリングのログをうまく集計/解析して頑張ってSQLiteのままチューニングしきれる自信はありませんでした。

さらに、SQLiteからMySQLへの移行を補助する簡易なスクリプトも用意されているのを発見したので、MySQLへ頑張って移行するぞという気持ちが強くなります。

そして、MySQLへの移行を決意しました。すでに判断ミスが多い……。

テナントDBのMySQL移行

SQLiteのDBをMySQLに移行するにあたって、それぞれを別のデータベースとして作ってしまうのがよかろうという発想に自然となりました。データベース名は isucon_tenant_%d のようにしました。

そして、IDは数値なので、IDの2の余剰で2台に分散させることができそうです。実際にシャーディングするかはさておき、ベンチマーカーで負荷が高くなっていった際に新しくヘビーに使われるテナントが発生するシナリオはありえなくはなさそうですし、そういったテナントを隔離するためにもDBを選択できる実装にしておくのはよさそうな気がしたのでついでにやってしまうことにしました。

幸い、SQLiteのファイルがテナント毎に別れていためにそのようなシャーディングの実装は簡単にできそうでした。

どうせMySQLに移行するなら同時にスキーマも最適化してしまうことにします。 幸い、今回は1人チームなので他のひとの作業をブロックすることはありません。

SQLiteからMySQLへの移行はそこそこ時間がかかりそうだったので、シャーディングすることを考えたときに同居する可能性があるAdmin DBをどうするか見ていきます。

Admin DBのRedis移行

Admin DBをどうするかを考えるためにスキーマと実装を追ってみると、 id_generatorvisit_history がつらそうなことに気づきます。

いずれもハードに利用されるテーブルであり、特に visit_history はランキングを提供するエンドポイントにアクセスする度にINSERTされているのでボトルネックになりやすいかなり邪魔げです。しかし、実際には visit_history は.finished_at 以前の最終アクセス日時しか求められていないようでした。ランキングを提供するエンドポイントで finished_at 以前の期間だけ更新するようにすればRedisのSET型で十分そうです。これもついでにやってしまうことにしました。

id_generator もRedisでINCR/INCRBYを使って代替できそうです。UUIDやULIDに変えることも考えましたが、現状のID発番は連番となっておりそれをわざわざ16進数に変換して扱っていたため、連番であることや外形的に16進数表記であることに意味がある可能性があると考えてそれはやめておきました。なお、いずれも問題なかったそうです。残念。

これをやりきれば、Admin DBはテナントの定義だけになり、かなりシンプルになりそうです。負荷はほとんどなくせるでしょう。

ついでにガッと移行してしまうことにします。

テナントDBの最適化

SQLiteからMySQLへの移行がなかなか終わらないので、テナントDBの最適化も先回りして考え始めます。

ランキング情報を作るための player_score はCSV経由でインポートされます。 flockでロックを取ってアイソレーションが行われていますが、DELETEする際にtenant_idcompetition_idで絞り込む際に適切にロックを取れるはずです。トランザクションにまとめることでこのflockは外せるでしょう。

また、N+1クエリとなっているクエリを読み解くと、各player_id毎に最後の行のスコアを採用してそれをscore順に並び替えていることがわかります。 他の箇所でも最後の行以外は要求されていなさそうでした。CSVからインポートする際に各ユーザーごとに最後の行だけを採用すればよさそうです。(後述しますが、この考えは実際には少し間違っていました。)

ここで、初期データを修正する必要性に気付きますが、修正は窓関数を使えばSQL一発で出来るので他のALTERと一緒にやってしまえばよさそうです。(このときに、先のINDEXの適用が実はできてなかったと気づければ引き返せたのだけど、気付かず。)

SELECT
   id, tenant_id, competition_id, player_id, score, row_num, created_at, updated_at
FROM (
    SELECT
        id, tenant_id, competition_id, player_id, score, row_num, created_at, updated_at,
        ROW_NUMBER() OVER (PARTITION BY tenant_id, competition_id, player_id ORDER BY row_num DESC) AS `rank`
    FROM player_score) a
WHERE a.rank = 1;

実際には、idは使っている箇所がないのでDROPできますし、row_num も使うことはないので残す必要はありません。(後述しますが、この考えは実際には少し間違っていました。)

さらに、IDが16進数表記の文字列になっているわけで、単調増加なぶんB+Treeの挿入効率は悪くはないのですが、無駄にインデックスサイズが大きいのが気になります。 (ボトルネックではないので)放っておけばいいものの、ついでなのでbigintにすることにしました。変換はMySQLのCONV関数で一発です。

……などを織り込んだものがこちらです:

-- competition
CREATE TABLE `competition_new` (
  `id` bigint NOT NULL,
  `tenant_id` bigint NOT NULL,
  `title` text NOT NULL,
  `finished_at` bigint DEFAULT NULL,
  `created_at` bigint NOT NULL,
  `updated_at` bigint NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO competition_new (id, tenant_id, title, finished_at, created_at, updated_at) SELECT CONV(id,16,10) AS id, tenant_id, title, finished_at, created_at, updated_at FROM competition;
RENAME TABLE competition TO competition_old, competition_new TO competition;
DROP TABLE competition_old;

-- player
CREATE TABLE `player_new` (
  `id` bigint NOT NULL,
  `tenant_id` bigint NOT NULL,
  `display_name` text NOT NULL,
  `is_disqualified` tinyint(1) NOT NULL,
  `created_at` bigint NOT NULL,
  `updated_at` bigint NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO player_new (id, tenant_id, display_name, is_disqualified, created_at, updated_at) SELECT CONV(id,16,10) AS id, tenant_id, display_name, is_disqualified, created_at, updated_at FROM player;
RENAME TABLE player TO player_old, player_new TO player;
DROP TABLE player_old;

--- player_score

CREATE TABLE `player_score_new` (
  `tenant_id` bigint NOT NULL,
  `player_id` bigint NOT NULL,
  `competition_id` bigint NOT NULL,
  `score` bigint NOT NULL,
  `created_at` bigint NOT NULL,
  `updated_at` bigint NOT NULL,
  PRIMARY KEY (`tenant_id`, `player_id`, `competition_id`),
  INDEX `ranking_idx` (`tenant_id`, `competition_id`, `score`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO player_score_new (tenant_id, competition_id, player_id, score, created_at, updated_at) SELECT tenant_id, CONV(competition_id,16,10) AS competition_id, CONV(player_id,16,10) AS player_id, score, created_at, updated_at FROM (SELECT id, tenant_id, competition_id, player_id, score, created_at, updated_at, ROW_NUMBER() OVER (PARTITION BY tenant_id, competition_id, player_id ORDER BY row_num DESC) AS `rank` FROM player_score) a WHERE a.rank = 1;
RENAME TABLE player_score TO player_score_old, player_score_new TO player_score;
DROP TABLE player_score_old;

https://github.com/karupanerura/isucon12-qualifier/blob/main/sql/pre_convert.sql

お気づきでしょうか。せっかくtalentごとにDB分けてるのにうっかりtenant_idを抜くのを忘れています。 ちょっともったいないですが些細なことなのでそのまま先に進みます。

このあたりを組み終わった頃に様子をみてみると、なかなかSQLiteからMySQLへの移行スクリプトが終わらないことに気付きます。 SHOW FULL PROCESSLIST を打って進捗を確認してみると、どうやらすごい量のplayer_scoreを持っているようです。

こりゃ大変だということで並列でインポートすることを試みます。 seq 1 100 | xargs -P10 -I{} bash -c './sql/sqlite3-to-sql ../initial_data/{}.db | mysql -uisucon isuports_tenant_{}' みたいな具合で並列で入れることにします。 この時点では、ろくに見積もりも取らずに、どうせ実装時間は取られるのだし並行で移行を進めておけば時間の無駄にはならなかろうと高をくくっていました。見積もりくらい取るべきでした。

実装デバッグ祭り

このあたりの実装をいったん終えたのが15:21頃だったようです。

github.com

新しいスキーマに合わせていざベンチ実行!コケたー! というわけで修正まつりです。

様々な問題、考慮漏れが次々と見つかります。visit_historyのRedis移行に対するPOST /initializeへの対応漏れや、同様に初期化におけるテナントDBのDROP DATABASE漏れ。 さらには、IDのURL上での16進数表記との相互変換漏れや、数年ぶりに使うSQL::Makerの使い方のミスなど、実に様々な問題を発見しますが一向にFAILは解消されず、刻一刻と時間が過ぎていきます。

最後まで残った問題が 大会内のランキング取得: ページングなし,上限100件 GET /api/player/competition/9fa52466/ranking 大会のランキングの1位のプレイヤーが違います (want: 9fa524cb, got: 9fa5252f) tenant:et-vrj-1659142684 role:player playerID:9fa524cb competitionID:9fa52466 rankAfter: というものです。 新しく作成されたテナントのもので、WebUIで確認しようにもログインセッションの発行の仕方が分からず(事前にちゃんと確認しておけばよかった……)、バグの原因が分からぬままモンキーデバッグを繰り返し、タイムアップを迎え、敗北が確定しました。

感想戦

なお、ベンチが公開されたので追試しつつデバッグを進めたところ、ベンチマーカーのレスポンスのログからすべてのプレイヤーが同一のスコアであった場合のランキングが正しくなかった事がわかりました。

修正内容としては以下です。

https://github.com/karupanerura/isucon12-qualifier/compare/main...study#diff-500630450a78877efc40cad8ce8ee6addd162a6bbe0ccf2bc9cd8cfaeae750ecR2

row_num をDROPしていたのを残すようにして、 INDEX ranking_idx (competition_id, score DESC, row_num) のインデックスを貼ることでクエリ一発でいい感じに取れるようにしています。

結果はこんな感じでした。

ほか、tenant_idをDROPしたり、 ORDER BY created_at 向けにインデックスを貼ったり細かいことをしているのでその影響も多少はあるかもしれませんが、デバッグ用にアプリケーションのAccessLogを有効にしているのも入っているので影響はほぼトントンでしょう。

もしこれが17時くらいにできていれば、複数台構成にしたりログを切ったりすることで3万点台は確実に狙えただろうと思うと悔しい限りです。

KPT

  • KEEP
  • PROBLEM
    • 計測のツメが甘い
      • 焦ったのもあるがボトルネックとなっているプロセスすらちゃんと特定せずに手を出したのはダメだった
      • 効果が思ったほど出なかった打ち手はその原因をちゃんと追求するべき
    • 計測と改善のサイクルがちゃんと回せていない
      • "改善"をデカくしすぎた
      • ついでに変えるのをやりすぎ
        • 先回りが良い方向に働いた部分もあったはずだけど確実性を欠いている
        • 結果的に博打っぽい打ち手になってしまった
        • 効果がそんなに見込めない打ち手に時間を使ってしまった(e.g. IDの16進数→10進数変換)
    • 一気にたくさん変えすぎた
      • 変更を一気に入れすぎてデバッグのサイクルを素早く回せなかった
    • 動作確認環境として他のサーバーを活かせなかった
      • MySQLへの移行とかは遊んでるサーバーでやっておいて、その間にSQLiteのままできる改善を進めておけばよかった
      • これができていれば、ULIDにシュッと変えてみて試しにベンチ流してみる。といったことを気軽に試せたはず
  • TRY
    • 焦らずちゃんと計測する
      • 問題の原因をちゃんと明らかにしてから次へ進む
    • ボトルネック以外には手を出さない
      • 特に、IDの16進数→10進数変換とかやる必要なかった。コードの変更箇所多すぎて時間的なコスパ悪い打ち手だった
      • 本当に手軽にできそうかちゃんと考えてから取り組む
    • 動作確認環境として他のサーバーを活かす
      • DBのオペレーションとか、ベンチ回しやすいところではやらないほうが他の作業をブロックしないのでよい

感想

解いていて面白かったです!ありがとうございました!次こそは……。

*1:GoでのWebアプリケーションの開発経験の大半はGoogle CloudでCloud Firestore(Datastore mode)を使うことが殆どでRDBの出番がなかった。

GCSへのアクセスをIdentity Aware Proxyで制限したいのでProxyを作った

storybookを共有したいなど、GCSに静的ファイルを配置しつつもそれを限定したメンバーだけに見せたいような用途ではアクセスを簡単かつ確実に制限するために、その制限にIdentity Aware Proxyを使いたくなることがあります。 しかし、Identity Aware ProxyはBackend Serviceに紐付けることはできますが、Backend Bucketに紐付けることはできません。Cloud Armerも同様です。 そこで、Backend ServiceとしてGCSのコンテンツを配信できるように、Proxy Serverを作りました。

github.com

ghcr.io/karupanerura/gcsproxy:v0.0.1 にビルド済のイメージを置いてあるので、 これをそのまま(予め自身のProjectのArtifact Registryにコピーを配置した上で)Cloud Runにdeployすることで、 GCSを参照するProxyとして動かすことができます。

GCS_PROXY_BUCKET 環境変数でGCSのバケット名を指定すればとりあえず動きます。

ほか、URLのパスプレフィクスをどうにかしたい場合、たとえば /static/ 以下をCloud Load BalancerからルーティングしてGCSのルートを参照させたい場合は GCS_PROXY_PATH_PREFIX/static/ と設定すればよいようになっていたり、 /index.html を参照させたかったら GCS_PROXY_INDEX_FILEindex.html と設定すればよくなっていたりなど、細かい機能が少しだけ実装されています。

あと(複数のRangeは扱えないけれど)Range Requestに対応していたり、gzip圧縮済のコンテンツをAccept-Encodingに応じてうまく扱えたり、HEADリクエストを解釈できたりなど、地味な機能がいくつか付いています。

よかったら使ってください。こんなの使わなくてもこれでいけるよ的な情報もお待ちしています。

PerlNavigatorがすごい

年々とelispのメンテが雑になってきて、ついにはemacsclientがemacs serverにうまく接続できなくなってしまい、とはいえ普通にスタンドアロンで立ち上げると動くのでログも取れずに原因究明が難しく、もはやこのままでは引退も近いかと思われたので、悪あがきでVSCodeに手を出してみることにした。

Perl Mongerの端くれとして、まずはPerlが書ける環境を整えようと、とりあえず最近ちょっと話題になっていたPerlNavigatorをVSCodeと共にインストールしてみた。

github.com

ところがこいつがすごい。

シンタックスハイライトをいいかんじにやってくれるのはもちろんのこと、emacsではperldoc -lmした結果に飛べるelispを仕込んでおいた(たぶんid:sugyanさんあたりのelispから拝借したきがする)のを使っていたが、PerlNavigatorはinclude pathの設定を入れたらいい感じにロードしてジャンプできる。 asdf でPerlを入れてもいい感じにつかってくれているようにみえる。

さらに、flycheckみたいな感じでエラーをわかりやすいところに出してくれし、auto-complete相当の雑な補完もよしなにやってくれる。

それも、大した設定もせずにこれだけのことがあっさりとできてしまったのがすごい。以下が入れた設定。

{
    "perlnavigator": {
        "includePaths": [
            "lib",
            "local/lib/perl5"
        ]
    }
}

これだけです。 lib.pm 置き場、 local/lib/perl5 はCartonで入れたやつを読むためのパスという感じ。 実際はarchごとにXSをビルドしたものが入るパスを lib pragmaのように突っ込めばより良い感じになるんだろうけどめんどくさくてまだやっていない。

設定したのはこれだけだけど、VSCodeに不慣れでもちょっとコードを書くにはそんなに困らない環境があっさり出来上がってしまったので驚いた。Amon2+Anikiで作ったWebアプリがいい感じに書ける。

実はむかしもVSCodeに移行しようとしたことがあったんだけど、そのときはセットアップしようとしたものの色々とどうすればいいのかわからず、だるくなってemacsに戻ってしまったが、これならVSCodeでもいいかなというところまであっさり来れたのでPerlNavigatorはすごい。