ISUCON14 振り返り

2024-12-08 に開催された ISUCON14 に参加した記録です。
次回以降に向けてやったことなど振り返っていきます。

去年の記録↓

notfounds.hatenablog.com

tl;dr

特に何もできず惨敗でした

ベスト: 8533 最終: 2,705

メンバー

去年と同じ。ただしメンバー2は当日事情があり欠席となった。

役割としては、自分がインフラ周りをメインで見つつ飛び道具的な改善に取り組み、もう一人のメンバーにアプリケーション周りの改善を進めてもらう。

タイムライン

初動(~10:20)

  • メンバー1
    • ブラウザから動作確認できるように設定
    • ER 図の作成
  • @NotFounds
    • SSH でサーバーに入り Ruby 実装に変え初回ベンチ
      • → 動かないので焦る
      • → リーダーボード見るとみんな0点なので安心する
    • Ansible で tool のインストール
    • GitHub にバックアップ
    • 初回ベンチ通る 1010 点
    • alp/slp のために Nginx/MySQL の設定を流す。ついでに NewRelic も入れた(Infrastructure Logs のみ利用)→ 699

全員でマニュアル読み(~11:00)

  • マニュアルを読み用語・概念確認
    • 今年は SSE かあ。そこまで手が付けられるといいねーとかいう話をする
    • Idempotent-Key とかは後でやれそうだねーみたいな感じ
    • 点数計算
      • 評価とか直接関係ないのか?
        • → その後の利用に影響がある
      • オーナーの売り上げとか直接関係ないのか
        • → 椅子の追加とかに影響がある

ぱっとできる改善フェーズ(11:00~13:00)

  • メンバー1
    • Index 貼り
      • alp/slp が改善するも MySQL の負荷は高い
  • @NotFounds
    • 少しでも DB 負荷を下げたい & 呼び出し回数が多かったので AccessToken から User/Chair を引く処理をインメモリキャッシュ → 効かない
    • ActionLog
    • MySQL の負荷が高いので3台目に移す → 3703 → なんか不安定になって revert

一進一退フェーズ(13:00~16:00)

  • メンバー1
    • /api/chair/notification、/api/app/notification のクエリ改善
    • /api/chair/coordinate で INSERT → SELECT みたいな処理を改善
  • @NotFounds
    • 距離計算を事前にすることで /api/owner/chairs の計算改善
    • Payment に重複リクエストを送らない → 効いてるのか分からん
    • マッチングレートを 0.5 から 0.4 → 変化なし。むしろ悪化?
    • Nginx/Puma 周りの秘伝のたれ対応 → 変化なし
    • クーポン適用周りの最適化
    • MySQL を別サーバーに移行リベンジ → 一瞬8533まで上がるもベンチの度に下がる 挙句、クリティカルエラー連発で心が折れそうになる

気づきフェーズ(16:00~)

ベンチ結果を見てユーザーの評価が一向に上がらない(すべて不満100%)なことに気づく → マッチングじゃね?となり改善を行う

  • メンバー1
    • /api/app/rides の N+1 改善
    • マッチングでの椅子の空状態の判定処理改善 → 椅子
  • @NotFounds
    • マッチングで speed が速いやつを割り当てる → なんかデッドロック状態になってエラーになった マッチングで rand * speed でソートして割り当てる → 評価の椅子の速さのところが改善した(気がする)
    • MySQL の問題は一向に解決せず、時間が解ける

最終調整(17:00~)

  • メンバー1
    • マッチング修正
    • ログを切る作業の準備
  • @NotFounds
    • 最後の賭けで複数台構成(Nginx/App+App+MySQL)に挑むも /api/initialize でエラー
    • Nginx/App + MySQL の原因調査

最終的に問題が解決せず、原因特定のためにこれまでのほとんどの修正を revert したうえで一台構成で終了

KPTA

Keep

  • Ansible で同時にサーバーを操作したのはよかった
    • 正直 Ansible にこだわりはなかったが NewRelic など重いのを入れるのは楽そうだとおもった
    • しっかり書いておけば timeout とかも簡単にできるのはよいと思った
    • ファイル操作と標準出力を取ってくるのはやりづらい...
  • 試行錯誤のために2週間ほどほぼ毎日 AWS で環境作って壊すをやったので、これでも大分障害耐性がついた
  • スコアには影響しなかったが、alp/slp計測→改善→alp/slpが改善されていることの確認はできていてよかった
  • 事前に用意していた ActionLogger (ユーザーの行動分析補助)や InMemoryCache は一応生かせて良かった

Problem

  • デプロイ・インフラ回り
    • Ansible の実行が思ったより遅い(Gather Fact がつらい)
    • Ansible を使ったことによりデプロイ処理が俗人化してしまった気がする(メンバーが使いやすいようにするところまでできなかった)
    • デプロイスクリプトはかゆいところに手が届かず、結果として複数台構成の失敗につながった可能性がある 三台それぞれに対して Nginx/MySQL/Application の ON/OFF まで管理できなかった
    • デプロイ→(ベンチ)→ログ解析はコマンド化していたので楽ではあったが、自分がブロックしてしまったのでよくない また、地味にログの投稿が面倒で時間が取られた
    • alp/slp の diff を使ったが、main との差分ではなく、ひとつ前のベンチの結果との比較になったので正確な比較ができなかった(featureブランチ同士の比較になったりした)
    • MySQL の slow query の threshold がうまく決められず SlowQuery の解析ができないときがあった(ログが吐かれすぎる)
    • どのくらい Query が投げられているのか計測できておらず、MySQL の負荷の原因が突き止められなかった
    • 複数台構成は練習不足を感じた
  • 実装まわり
    • Ruby で雑に非同期処理する方法を作れていない(Sidekiqの導入は負担が大きいので検討しなかった)
      • Go だと goroutine 投げて先にレスポンス返したり、Node でも await せずにレスポンス返すみたいなことができる
      • 練習で Thread 投げっぱなしにしてみたりしたけどうまく動かなかった
        • なんか書き方がよくなかっただけな気もする
      • SQL でできる処理なら TRIGGER 使うのがよい?
    • 細かいミス(SQL の SyntaxError や Rubynil 参照)などは競技耐性不足を感じる
    • 型のセットアップは行ったがエラーが多すぎて通すところまでできなかった
      • TypeProf 使いたくなった
    • (去年は Go だったけど)今年は Ruby の方が慣れているからという理由で選んだが、SQL と戯れる時間が長くてどっちでもいいのでは?となった
  • その他
    • 試行錯誤が足りない
      • 仕様で出てくるパラメータの調整などは全然できなかった
      • 手元で修正→デプロイ→ベンチ→エラー→ログ調査だと改善が遅いのでどうにかしたい
    • 点数に直結するコアな部分にたどり着くのが遅い
      • レギュレーション理解
    • エラーが出たときにちゃんと対策しておらず、どこでエラーが出たのかわからなくなり全部を戻す羽目になった
    • 事前に用意していた Ruby 関連の静的解析や型周りのセットアップスクリプトが1ファイルにしか対応していなくて使えなかった

Try

  • ログの投稿は自動化したい。もしくはダッシュボードにしたい
  • ログはコミットグラフに紐づけられるようにしたい
  • 1コマンドでデプロイ→ベンチ→計測までできるのが理想...
  • システムのメトリクスを見ながらの改善も大事だがベンチの結果とマニュアルをよく読む
  • 仕様に出てくるようなパラメータは極端な値を試してみて挙動を確認する
  • データ不整合が出てきた場合ちゃんとロックされているか/トランザクションが張られているかを確認する

Action

  • クリティカルエラーの原因は明らかにしておきたいので後日振り返りとしてバグ修正をやる

irbでRSpecのmatcher を使えるようにする

tl;dr

irb で以下の2行を実行するだけ。

require 'rspec/expectations'
include RSpec::Matchers

$ rails c
Loading development environment (Rails 7.1.5.1)
irb(main):001> require 'rspec/expectations'
irb(main):002> include RSpec::Matchers
irb(main):003>
irb(main):004> expect(1).to eq(1)
=> true

背景

コードレビュー中など、ふとした時に RSpec の matcher を試したくなることがある。
しかしわざわざ新しくファイルを作ってテストを実行するのは面倒。

そんなときは irbRSpec の matcher を利用すると手軽に検証できる。

RubyKaigi 2024 に参加しました

5月15日~17日に沖縄で開催された RubyKaigi 2024 に参加してきました。
これまで RubyKaigi はオンラインでちょくちょく参加していましたが、リアルで参加したのは`初めてでした。チケット代から交通費・宿泊費まで全て負担してくれた会社に感謝です。

0日目

会社の代休が余っていたので前日(5/14)に沖縄に移動し前泊しました。Pre-checkin が19時までだったのですが、17時半頃に空港についたため急いで会場に向かいました。

ゆいレールに乗ったときくらいからエンジニアっぽい人々をチラホラ見かけるようになり、県庁前駅で降りてからは5~10人くらいが小雨が降るなか会場を目指して歩いていたと思います。

会場はすでに多くの人で賑わっており、入口のパネルでの記念撮影をする人やスポンサーブースを設営する人、久しぶりの再開に話を弾ませている人、そしてネームタグをケースにいれるの苦戦する人(自分)など様々でした。

RubyKaigi が同窓会みたいになっているのは現地参加の醍醐味ですね。自分の知り合いも何人か来るという情報を聞いていましたが、このときは誰にも会わなかったのでそそくさとホテルへ向かいました。

RubyKaigi 2024 というロゴと沖縄の文化や食べ物をモチーフにした図形などが配置された図
会場入口にあった RubyKaigi のパネル

ホテルでチェックインを済ませたときには19時を過ぎていました。この時間になると沖縄といえど日は落ち、雨がパラパラ降っていました。

移動で疲れていましたがせっかくなら沖縄料理を食べたいなと思い、国際通りに向かいました。バスで Suica が使えなかったときは焦りましたが、お守りとして持っていた千円札のお陰で無賃乗車せずにすみました。

向かったお店はここ 飯ト寿 小やじ maps.app.goo.gl

オリオンビールで乾杯し、刺し身やサラダ、メンチカツを食べました。刺し身は臭みがまったくなく美味しかったです。

会社の同僚に連絡したところ、一緒に飲もうということになったので牧志公設市場の居酒屋で飲みました。
実は職場から RubyKaigi に参加しているメンバーはチームが異なり、これまでほとんど面識がありませんでした。
こうやってイベントがないと、なかなか会うことも難しいのでいい機会となりました。


ここからはざっくりと参加したセッションをピックアップして振り返っていこうと思います。

1日目

Writing Weird Code

資料: https://drive.google.com/file/d/1Dkx15u_5UAGoFqJHCeAuj2FXS-z_U7EE/view

Animated Quine というわけのわからないもの(褒めてる)を初っ端から見せつけられ、RubyKaigi 始まったなぁという感想でした。
一見すると使い道がなさそうなトリック(失礼)が散りばめられているのですが、字句解析や構文解析の穴をつくためのテストコードとしてこういった技術が使われるというのは興味深かったです。

アニメーションつけるのは難しそうですが、普通の Quine は試したいなと思います。

The grand strategy of Ruby Parser

資料: https://speakerdeck.com/yui_knk/the-grand-strategy-of-ruby-parser

お昼ご飯が長引き、途中参加でしました。
パーサージェネレータである Lrama に関する話で、そもそもなぜ必要なのか?から今後の展望までが熱く語られました。
Prism は触ったことがあったのですが、Lrama に関しては初めて知りました。しかし、その必要性や Universal Parser の構想などについてざっくり理解できたと思います。

An adventure of Happy Eyeballs

資料: https://speakerdeck.com/coe401_/an-adventure-of-happy-eyeballs

Socket ライブラリに RFC8305: Happy Eyeballs v2 で提案されているアルゴリズムを実装したという話でした。
実装する上での複雑な状態との戦い方やブロッキングなどの対処として IO.selectIO.pipe を使った実装は参考になりました。

ネットワーク周りの実装するときのテストって大変そうだなって思ったのですが、デモのように簡易サーバー立ち上げるのが一般的なんですかね...?

Official Party

特に説明はないです。海を眺めながらのバーベキューは最高でした。(0日目の天気じゃなくてよかった〜)

バーベキューグリルで焼かれている肉・ソーセージ・野菜など
一日目の Official Party のバーベーキューの様子

2日目

Does Ruby Parser dream of highly expressive grammar?

資料: https://speakerdeck.com/ydah/does-ruby-parser-dream-of-highly-expressive-grammar

Lrama の文法を定義する構文に関する話でした。
Lrama では新しい構文を定義して DSL を使って実装したことによって複雑な文法を抽象化できるようになったということだと理解しました。

パラメタライズや頻出文法のライブラリ化、条件分岐などがあるとのことですが Ruby 以外のパーサーも作ろうと思えば作れるという認識であっているんですかね...?

Embedding it into Ruby code

資料: https://speakerdeck.com/soutaro/embedding-it-into-ruby-code

YARD のように書いたコメントから RBS を生成する rbs-inline gem を作っているという話でした。 YARD との共存?として説明などを書きたいときにどうなっていくのかが気になりますね。 まだ RBS 導入できていないのでこれから導入するときは使ってみたいです。

あと、Live demo のときに snippet を使っていて真似したいなと思いました。

Getting along with YAML comments with Psych

資料: https://speakerdeck.com/qnighy/getting-along-with-yaml-comments-with-psych

Psych という YAML パーサーを拡張してコメントを扱えるようにしたという話でした。(スピーカーは前職の同僚)
以前 YAML の歴史について教えてもらい、実はライブラリ存在はその時に聞いていました。

しかし、発表後半で触れていたライブラリの拡張ライブラリを作る際にどこまでを責務とするか?のような設計の線引の話はあまり聞いていなかったので面白かったです。(以前も聞いてたらごめんなさい..)

RuboCop: LSP and Prism

資料: https://speakerdeck.com/koic/rubocop-lsp-and-prism

RuboCop に LSP を統合した話でした。この話を聞くまで RuboCop が LSP の機能を持っていること知らなかったです。
静的解析→リアルタイムな解析でエラートレラントの必要性を理解できました。

Shopify の Ruby LSP も LSP と linter/formatter を提供しているので、今後どちらが使われていくのかは気になりますね。

Good first issues of TypeProf

資料: https://speakerdeck.com/mame/good-first-issues-of-typeprof

TypeProf の紹介とコントリビューションしてねという話でした。型注釈無しで無理矢理型を推論するのはパワフルですごい... 特にテストに関する考え方が印象的でした。編集過程をテストするため、そのようなテストが書きやすい DSLを作りシナリオテストを実装しているのが面白かったです。

実は TypeProf 自体は以前の発表で知っていたのですが何もできていなかったので、これを期に手を動かしたいなと思います。

夜ご飯

この日はアフターイベントに参加できなかったので一人で飲みに行きました。行ったお店は県庁近くの くもざき

maps.app.goo.gl

気さくな店長が色々とオススメの泡盛を教えてくれました。大きくてジューシーな焼売と沖縄野菜が美味しかったです。

3日目

YJIT Makes Rails 1.7x Faster

資料: https://speakerdeck.com/k0kubun/rubykaigi-2024

YJIT のパフォーマンス改善の話でした。タイトルつけた時点では 1.7 倍の高速化が、発表のタイミングでは 1.8 倍になったとのことで笑いが起きました(すごい)。
Ruby のコードはまず Ruby VM 命令に変換され、それが JIT コードに変換されて動作するようです。(JIT でない部分はインタプリタ) ただしすべてのコードが YJIT に変換されるわけではないようで、今回の対応でサポート範囲が増えたことによりパフォーマンスが改善したようです。
他にも VM のスタックの値をメモリーに書き込んでいたものがレジスタに置き換わるなど様々な高速化が行われているようです。

個人的に最近低レイヤに興味が出てきたのでこのあたりの発表が聴けてよかったです。また Ruby のバージョンを上げるだけでどんどん速くなってくれるので嬉しいですね。

Porting mruby/c for the SNES (Super Famicom)

資料: https://speakerdeck.com/gedorinku/c-for-the-snes-super-famicom-rubykaigi-2024

スーパーファミコンに MRuby を移植し、ゲームを動かす話でした。(スピーカーは前職の同期)
以前から話を聞いていましたが、画面更新のタイミングが決められていることやデバッグの難しさ、パフォーマンス問題など改めて発表で聞いて面白かったです。

この取り組みを聞いて自分も低レイヤに興味を持ち、CPU エミュレータを書きはじめたりしているので感謝です。

感想

今回はじめて RubyKaigi に現地参加してみて、現地参加の良さを実感しました。
登壇者や他の参加者との交流ができることはもちろん、会場の熱気や一体感みたいなものは実際に同じ空間にいないと感じることはできないなと強く思いました。

また、オンラインだと本当に気になるセッションしか見ないことが多いです。しかし、現地にいるとせっかくだし除いてみるかという感じでふらっと聴きに行くことも多く、幅を広げられる(知らないことを知れる)という点で結構よかったなと思います。

発表全体に関する所感としては、Ruby のパーサー周りや開発支援系の話が多かった印象です(自分が意識して聴きに行ったのもある)。
他にも YJIT、並列・並行処理などパフォーマンス関連のトピックも多かったです。この辺は実際にアプリケーションを運用する上で直に恩恵を受けられるので今後が楽しみなところですね。

これは僕だけかもしれないですが、JSConf や Go Conference に比べて言語本体の実装やランタイムの話が多いなという印象をもちました。RubyKaigi は 「Ruby にこんな機能を実装しました」「xxx のこの実装はこうなっています」みたいな内部の話が聴けます。これは自分にとってはなかなか新鮮な感じがして楽しかったです。

一方で反省点としては事前の予習が足りなかったなと感じています。前の話にも重なるのですが、Ruby のコア周りの話が多いため予備知識がないと置いてきぼりになってしまうなと... 直近の Ruby 周りの動向を知っているともっと RubyKaigi を楽しむことができ、吸収できることも多かっただろうなと思いました。

一通り話を聞いてみて、RubyKaigi の内容を今すぐ実務に取り入れるイメージはあまりついていないです。しかし、今後はより積極的にコミュニティ貢献するぞというモチベーションになったので参加してよかったなと思っています。

さっそく自分の周りでも RubyKaigi で紹介された gem に対してパッチを送ったり、それに便乗して(?)僕もパッチを投げたりしていて KaigiEffect を実感しています。

github.com

自分が今後注視していきたいところは以下の4つです。来年の RubyKaigi では話についていけるようにしっかりとキャッチアップしていきたいと思います。(あわよくば登壇したい...!

  • Prism と Lrama の関係
  • Ruby 自体
    • 文法?機能のフリーズ周りの動向
    • まだ使ったことない機能周り(Fiberとか)
  • 開発ツール周り全般
    • 型の支援ツールなども出てきているが複数あるので今後どうなっていくのか
  • 気になった gem とか

そういえば沖縄に行ったのは今回が3度目なのですが、いちばん沖縄料理を堪能できました。沖縄野菜やお刺身が美味しく、オリオンビール泡盛を飲みすぎてしまったので次の健康診断が心配です。
一つだけ心残りがあるとすれば〆のステーキを食べる元気がなかったので、次は〆ステーキ食べるために沖縄に行こうと思います! RubyKaigi お疲れ様でした!


編集履歴

2024-05-29: The grand strategy of Ruby Parser の資料のリンクが間違っていたため修正いたしました。ご指摘いただきありがとうございました。
2024-05-30: Getting along with YAML comments with Psych のセクションがまるっと抜けてたので追記しました。

jqとfzfで雑にJSONを探索する

tl;dr

タイトルにあるとおりだが jq と fzf で JSON を探索する Shell Script を書いた。
具体的にはある JSON ファイルを渡したら JSON Path が一覧され、fzf を使って JSON Path をインタラクティブに絞り込みながら値を preview するというもの。
百聞は一見にしかずというので gif を貼っておく。

動作している様子

背景

同僚が「フィールド名はわかっているけどわざわざ JSON Path を指定するのが面倒」という事を言っていたのでなんか簡単に JSON Path の抽出/絞り込みできないかなと思った。

本体

gist.github.com

使い方

自分は .zshrc に貼ってあるので適当に shell を開いて jpath <JSON file> のようにして使う。

おわり

ちなみに同僚が本当に求めていたものは jq '.. | .<field name>? | values' で十分だった。

引用: https://pmac-agpc.ca/project-management-tree-swing-story

RubyでStringのサブクラスをprotobufに渡せるようにした話

毎年機会を見つけては OSS に Contribute したいなと思っているのですが、今年もなかなか思うようには貢献できませんでした。
今回は protocolbuffers/protobuf に投げた Pull Request について紹介します。

TL;DL

  • Protocol Buffers の Ruby 実装として Google が公式に実装している protocolbuffers/protobuf がある
  • Protocol Buffers の string フィールドに対して Ruby の String を渡すことができるが String のサブクラスは受つけず、ランタイムエラーが出てしまっていた
  • String のサブクラスも受け取るように提案した PR が受け入れられた 🎉

protocolbuffers/protobuf とは

前提として Protocl Buffers とは Google が提案・利用しているデータシリアライゼーションフォーマットで、ILD を提供しています。
主に gRPC による通信で利用されています。protocolbuffers/protobufGoogle によって実装された公式ライブラリであり、2023年12月現在で C++ / C# / Dart / Go / Java / Kotlin / Objective-C / PHP / Python / Ruby 向けのライブラリを提供しています。 このライブラリを使うことで各言語で任意のオブジェクトを Protocol Buffers の形式にシリアライズすることができます。

今回は社内のマイクロサービス間通信をおこなっている箇所で Rails の gRPC Server を実装したところ、後述する問題に遭遇しました。

何が起きていたか

Protocol Buffers に定義されているスカラデータ型 は15種類あります。 具体的には符号付/符号なし整数型と浮動小数点型、そして文字列型、ブーリアン型、バイト型です。

今回 Ruby の protobuf ライブラリの文字列型(string*1 ) のフィールドに対して RailsActiveSupport::SafeBuffer を渡したところ以下のようなエラーが出てしまいました。

Google::Protobuf::TypeError: Invalid argument for string field 'foo_field' (given ActiveSupport::SafeBuffer).

どうあるべきか

ActiveSupport::SafeBufferXSS 対策として提供されているクラスで、HTML として出力する際に安全であるとマークされた文字列を表すためのクラスです。
実装を見てみるとわかるのですが String を継承したサブクラスとなっています。

module ActiveSupport # :nodoc:
  class SafeBuffer < String
    # ...
  end
end

github.com

String のサブクラスが string フィールドに渡せないのはリスコフの置換原則に反しているため修正するのが適切だと考えました。

修正

Ruby の protobuf ライブラリは FFI を用いて実装されており、本体はC言語で書かれています。今回の修正対象もC言語でしたが、運よく適切な関数 `rb_obj_is_kind_of を見つけることができ無事実装できました。
困りポイントとしては fork したリポジトリ上でうまく CI を動かすことができず、本家に Pull Request を投げて実行してもらうまでは CI 通るか確認ができなかったのが大変でした。

github.com その後 Reviewer と何度かやり取りがあり、Pull Request 上は Closed になっていますが 34908e2 で無事取り込まれました 🎉

感想

今回の内容とあまり関係ないのですが、今までは Pull Request の Description やコメントでのやり取りなど英語でのコミュニケーションに課題を感じていてあまり積極的になれませんで。
しかし ChatGPT をはじめとする AI を使い、AI に一度レビューしてもらうことである程度自信をもってやり取りすることができるようになったと感じました。
これを機に2024年はもっと積極的に OSS 活動していきたいです。

*1:この記事中では String と表記した場合は Ruby の文字列クラス、string と表記した場合は Protocol Buffers の文字列型とします

ISUCON13 振り返り

2023-11-25 に開催された ISUCON13 に参加した記録です。
次回以降に向けてやったことなど振り返っていきます。

メンバー

4月頃大学の編入同期に声かけたら快諾してくれた。

  • メンバー1 ISUCON 初参加。普段は Ruby on Rails を使ってバックエンド開発をしている。
  • メンバー2 ISUCON 初参加。普段はセキュリティエンジニア。Web 開発はほとんど未経験。
  • 僕 ISUCON は2年ぶり2回目。普段は Web エンジニア。

今回はメンバー1・2にアプリケーションをメインで見てもらい、自分はインフラいじりつつ重そうなところを見ていく方針。
普通だったらメンバーが使える言語の最大公約数を取るけどノリと勢いで Go になった。

練習

10月中盤と本番直前の祝日にそれぞれ1日ずつ使って行った。
環境はそれぞれ AWS に用意した。

1回目

ISUCON12 予選を利用。
目的としては本番の流れをざっくりと理解するためで、あとは index 貼る練習とかをちょこちょこやった。
あとはデプロイ周りの script とかを整理していた(が、本番のときとの環境祭に耐えられなかったので実際は使わなかった。カナシイ)

2回目

private-isu を利用。
6時間くらい何も見ずに触った後に ISUCON 本 を読んで振り返り。
画像の切り出しなどはここで触れてよかった。あと、1ヶ月ぶりの練習で割と色々忘れてたので直前にやって良かったねってなった。

当日

メンバー1の家に集合。寒かったのでコタツでぬくぬく。
開始までの間に空の repository 作ったり、コマンドメモを見返したりしていた。

10:00

最初の役割分担

  • メンバー1 マニュアル確認。アプリケーションの動作確認。スロークエリの設定
  • メンバー2 マニュアル確認。アプリケーションの動作確認。アイコン切り出し
  • 僕 実装の確認。git の設定。alp 導入。デプロイスクリプト設定

設定後に走らせた初回ベンチは 3,653 で10分くらいで初期設定できた。

11:00

ログ眺めながら方針を立て、以下のタスクに取り組んだ。

  • MySQL のスロークエリのトップがプリペアドステートメントだったのでおもむろに止める
  • SQL 眺めながら index を貼っていく
  • アイコンの切り出し

着実にボトルネックは移り、DB の負荷も減ってきたが対してスコアは伸びない 4,398

12:00

アイコンの切り出しには成功していたのでアイコンハッシュ周りに取り組み始める。

index がうまく貼れていなかった場所を修正したりして 6,211。 さらに index が貼れないか試行錯誤する。

その後少し伸びて 7,639

このとき、ちゃんと計測せずに write の負荷が高くなるのを懸念して慎重にやりすぎて時間を使いすぎた気がするので反省。

13:00 ~ 17:00

なぜか家の Network が落ちたのでお昼休憩を取ることにする。一旦休み。
買い出しから帰ってきたら復旧していたので再開するもベンチが止まる。一旦休み。
2台目のサーバーで開発できるように設定。

明らかに悪そうな INNER JOIN を取ったりするけどそんな伸びない。
statistics 周りの N+1 の解消に取り組み始める。他にも NG ワード判定をアプリ側で行うように修正し、やっと 10,261

17:00 ~ 18:00

ここに来て MySQL の restart に失敗するようになる。
怖くなったので手を付けていなかった3台目のサーバーのセットアップを行い、AWS から VM の再起動を行う。

ログを止めたりしつつ、ベストスコア 11,892 で終了。

個人の振り返り

K

  • チームの Discord 鯖 に GPT-4 の bot を用意しておいたが良かった
    雑に設定方法を聞いたり、SQL からの ER 図生成などに役立った
    ChatGPT が生成した reference を sqldiagram に投げた図
  • 物理で集まったのでコミュニケーションコストが低かった
    直接画面見ながらペアプロできるのは良い
  • 練習でやったことが活かせていた
    画像の切り出し周りは練習でやってなかったらあの短時間でできなかったと思う
  • 細かいけど git commit --allow-empty -m "Score: xxxx" ってしておくのは良かった
    壊したときに戻す場所が見えやすいし、振り返りに便利
    空コミットでスコアを記録すると便利
  • git sync が便利 notfounds.hatenablog.com

P

  • bot を雑に作りすぎてコードを全部投げられなかったので改善の余地がありそう
    そもそもコードの修正などは外部のツールやエディタ上で完結させたほうが良いかも
  • index を慎重に貼りすぎた
    SQLbot になげて index 貼ってもらうくらいやっても良かったかも
  • デプロイ周りがあまり自動化できていなかった&属人化していた
  • インフラ周りに弱すぎる
    練習で複数台構成の練習をサボっていたので本番でやる勇気がなかった
    今回スコアを稼ぐ上で重要な DNS 周り何も分からなくて手を付けられなかった
    MySQL が再起動しなくなったときなどに復旧できなかった。複数台構成にするなら致命的な気がする
  • 瞬発力がない
    言語への理解&慣れ。ISUCON 頻出問題への対応速度
  • キャッシュを効率的に使えなかった
    どこをキャッシュして良いのか?そこをキャッシュすると効くのかが見極められていなかった

T

  • 最初の環境構築はなるべく自動化したいので Ansible などを勉強する
    ログの投稿も手動だったので webhook とか使うようにしたい
  • 各メンバーがデプロイできるようにする
  • 練習で複数台構成を試す
  • 数をこなしてたくさんの施策に取り組めるようにする
  • レギュレーションやアプリケーションの書き込み箇所をちゃんと把握する

TypeScript で TextDecoder と TypedArray の取り扱いではまった

TypeScript と書いているが、JavaScript にも当てはまる内容になっているはず。

tl;dr

  • utf-8 のバイト列(TypedArray) → string への変換で TextDecoder を使った
  • Uint8Array をそのまま TextDecoder.decode() に投げたら生成された文字列がいまいちおかしい
  • Uint8Array.prototype.subarary() を使うとコピーせずに部分配列を作れる

おさらい

いきなり TypedArray とか言われてもピンとこないと思うので関連するオブジェクト周りについておさらいしておく。

ArrayBuffer

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
固定長の生のバイナリデータを扱うためのオブジェクト。他の言語でいうバイト配列。
しかし直接操作することはできないため、後述する DataView や TypedArray を使う必要がある。

DataView

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/DataView
ArrayBuffer を操作するためのインターフェースオブジェクト(ビュー)。ArrayBuffer は生のバイナリデータであるため操作する際は環境毎のエンディアンを考慮する必要があるが DataView を使うとエンディアンに依存せず読み書きできる。

TypedArray

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
ArrayBuffer を配列形式で扱えるようにするためのインターフェースオブジェクト(ビュー)の総称。
あくまで総称なので TypedArray という名前のオブジェクト自体は存在しない。
TypedArray には Uint8ArrayInt32Array などが存在する。繰り返しになるが、これはあくまで View であり、実体は ArrayBuffer である。

TextDecoder

https://developer.mozilla.org/ja/docs/Web/API/TextDecoder Web API の一つで、UTF-8EUC-JP などのデコーダーを提供する。
TypedArray を受け取り、デコードした結果を文字列として返す。
対となる API として TextEncoder がある。

はまったこと

以下のようなことをしていた。

// 適当に buffer を確保
const buf = new Uint8Array(32);

// 実際はここで Stream からデータを読み込んでいたが、ここでは TextEncoder の書き込みとしておく
// e.g. await s.read(buf);
new TextEncoder().encodeInto("Hello, World!", buf);

// byte列 → string 変換
const decoded = new TextDecoder().decode(buf);

console.log(decoded);                                     // "Hello, World!"
console.log(decoded.replace("Hello, World!", ""));        // ""
console.log(decoded.replace("Hello, World!", "").length); // 19 <-- !?

byte 列の操作に慣れている人からしたら当たり前かもしれないが TextDecoder は特定の byte 列からゼロ値までをいい感じに decode してくれるのではない。
与えられた byte 列をすべて decode しようとする。

decoded を表示した際に "Hello, World!" と表示されたが、実際には見えていないだけで Null 文字が28個続いているのである。

どのように対応したか

const buf = new Uint8Array(32);

// byte 列操作系のメソッドは大体読み込み・書き込みした byte 数を返してくれる
// e.g. const n = await s.read(buf);
const { written: n } = new TextEncoder().encodeInto("Hello, World!", buf);

const decoded = new TextDecoder().decode(buf.subarray(0, n));

console.log(decoded);                                     // "Hello, World!"
console.log(decoded.replace("Hello, World!", ""));        // ""
console.log(decoded.replace("Hello, World!", "").length); // 0 <-- 👍

TypedArray には TypedArray.prototype.subarray(begin[, end]) があり、メモリのコピーなしに同じ型の新しい TypedArray を返してくれる。
メモリのコピーが発生しないということは、物理メモリ的は元の TypedArray が参照している ArrayBuffer の実体と同じであるということなので、ここで生成された TypedArray への変更は元の TypedArray にも影響をあたえる。逆もまた然りである。

const base = new Uint8Array([4, 2]);
const copied = base.subarray(0);

copied[0] = 0;
console.log(base.toString());   // 0,2
console.log(copied.toString()); // 0.2

配列のコピー・複製を行う場合は TypedArray.from(source[, mapFn[, thisArg]]) を使うと良い。

const base = new Uint8Array([4, 2]);
const copied = Uint8Array.from(base);

copied[0] = 0;
console.log(base.toString());   // 4,2
console.log(copied.toString()); // 0.2

まとめ

TextDecoder に TypedArray を渡すときは subarray を用いて適切な範囲指定の TypedArray 作ってそれを渡してあげたほうが事故がなくて良いと思う。

余談

ちなみになぜこの問題にあたったのかと言うと TypeScript で TCP Server を立てて独自プロトコルの parse をしていたためである。
はじめは通信周りのバグか何かだろうと思い tcpdump を使って binary packet を眺めていたのだがてんで見当違いであった。