ISUCON7 予選を通過した

ISUCON7 予選に†空中庭園†《ガーデンプレイス》として @ryot_a_rai@mozamimy と参加して2日目1位で通過することができた http://isucon.net/archives/50956331.html

リポジトリhttps://github.com/ryotarai/isucon7q

当日まで

例年 Ruby で参加していたけど、今年は発表された初期実装に Ruby が含まれていなかったこと (※後から追加された) もあり、@ryot_a_rai から Go で参加したいという話が出て Go を選択した。 僕と @mozamimy は Go はまぁまぁ書いたことはあるくらいの状態だったので、事前に一度 Go で練習したり pprof の使い方を教えてもらったりしていた。

Go は個人的にはあんまり好きになれなかった言語ではあるんだけど、ISUCON で使う上では普通に書けば普通に速くなるし、 制限時間があってプレッシャーが高い状況で typo とか型エラーとかのつまらないミスをコンパイル時に検出できるので、Ruby よりやりやすく感じた。 おかげでアプリケーションの改修中にほとんどバグを出さずに進めることができた。

当日

僕は主にアプリケーション側の改修を担当していて、MySQL の設定とか Redis をインストールして使える状態にするとか、deploy.sh を用意して git pull && make && systemctl restart && nginx のログローテートを自動化したりとか、そのへんは他の二人に任せていた。 以下、各サーバは isu1、isu2、isu3 と呼ぶことにする。初期状態で nginx とアプリが動いていたのが isu1 と isu2 で、MySQL が動いていたのが isu3。

とりあえずベンチマークを実行してアクセスログを見て GET /icons/:file_name が遅いことがわかり、コードを読むと MySQL に画像を入れてることがわかったので、社内 ISUCON でも見たな~と思いながらとりあえず Redis に入れることにした。 この時点では MySQL の負荷が高かったので Redis は isu2 に入れた。 これで速くはなったんだけどそれでもまだ icons が支配的で、 :file_name が画像の SHA1 値になっていてパスをキーにしてキャッシュできるので nginx の proxy_cache を使ってキャッシュを入れてみたもののほとんど改善せず。 CPU もメモリも余ってるのになんでこんなにスコアが出ないんだ? というあたりで public 側の帯域によるものだと気付いた。 isu1 と isu2 の両方をベンチマーク対象にしたらスコアは伸びたが、それでもリソースは余っていた。 これをなんとかするには画像を配信しないようにするしかなくて、いつかの ISUCON で見たように Cache-Control なのか?? となっていた。 レギュレーションに 304 のケースに関して明確に記述されていて、スコアが100分の1になってしまうものの帯域で詰まってる以上この先に進むには 304 を返せるようにするしかないと決めて試すことにした。 最初は Last-Modified と ETag をアプリケーション側で返すようにして tcpdump をしながら様子を見ていたけど If-Modified-Since や If-None-Match が icons に対して送られてこなくてうーん? と思いながら Cache-Control も返すようにしたら挙動が変わって 304 を返せるようになって一気にスコアが伸びた。 この時点で 16:15 くらいで 98840 点だった。

icons の壁を越えると MySQL や Redis がボトルネックになってきて、ここからは N+1 クエリを直すとか、Redis に移せるものは Redis に移すとか、select のカラムを絞るとか、いつもの作業になった。 MySQL のテーブル定義は変えてない気がする? そのへんは任せていたのでよくわかっていない。 微妙に JS や CSS のリクエストでエラーになっていたので、gzip_static on にしたり Cache-Control 系のヘッダを返すようにしたりしていた。 一通りやりきると MySQL や Redis が空いてきてアプリケーションの CPU 負荷がボトルネックになってきたので、Redis を isu2 から isu3 に移したりしていた。 isu1 と isu2 は nginx とアプリが動いていて、isu3 は public 側の帯域確保のために nginx が動いていてベンチマーカからのリクエストを isu1 と isu2 にプロキシしつつ、Redis と MySQL が動いている状態。

最後に Redis の負荷を分散させるために3台に Redis を入れてシャーディングするというのをやっていた。均等にばらけるのか不安だったけど id を3で割ったり icons のハッシュ値を適当に数値化して3で割ったりして接続先の Redis を切り替えるようにした。 MGET とか HMGET を使っている箇所が面倒だったので、そこは3台に同じクエリを投げてからその結果をマージするようなコードを書いた。 いま振り返るとちゃんと必要なキーだけ集めてクエリを投げるように直してもよかったな。 isu3 だけアプリが動いてなくて CPU 負荷が若干低かったので isu3 にウェイトを置くようにして比率を調整したりもしてみたが、あまり効果もなさそうだったので 1:1:2 の比率で決定にした。 このへんでコードフリーズということにして、pprof の削除やロギングの削除をやった。

ラスト1時間で再起動試験と最終スコアの確定をやった。レギュレーションでは最後のスコアが使われることになっており、今回のベンチマークは同じコードで実行しても一割くらいスコアにぶれがあることがわかっていたので、ここまでのベストスコアが59万点だったため58万点以上が出たらそれを最終スコアにしようという風に決めてベンチマークを実行した。 実際には再起動後のベンチマーク一発目で58万点が出たので、もう少し試したい気持ちもありつつも、最終的にはこのときのスコアのまま終えた。

感想

icons の 304 をさっさと試して最初の帯域の問題を早くクリアできたのが大きかったかなと思う。画像をファイルシステムに置いて nginx に任せるとかせずすべてアプリケーション側で扱うようにした結果、Last-Modified や ETag のずれではまるのも意図せず回避できていた。 アプリケーションの改修でバグを出さなかったのも調子がよくて、最後のシャーディングも一発でベンチマークが通って気持ちよかった (スコアはそこまで伸びなかったけど)。 予選から3台のサーバを使う問題で、ベンチマーク対象を複数指定できるという新鮮な設定で楽しめました。ベンチマークもエンキューしたらすぐに実行されるような環境で体験がよかった。 本選でもよいスコアを残したい。

present? と blank? が嫌い

params の中身のように入っているオブジェクトのクラスが事前に分からないものに対して空っぽい文字列の場合と存在しない場合を区別したくないときに限って blank? を使うのは分かるけど、 nil チェックをするために blank? を使ったり、配列が空かどうかをチェックしたいだけなのに blank? を使ったりすると、 blank? の挙動を正確に理解して nil と空配列を区別したくないから使っているのか、それとも nil がくるかどうか分からないので適当に防御的に blank? を使っているのか、 あるいは blank? しか知らないのかが読みとれずにめんどくさいと思うことがよくある。 かわりに empty? を使っていれば empty? を持っているオブジェクトは blank? を持っているオブジェクトより少ないので読み手に伝わる情報量が大きくなるし、 かわりに nil? を使っていればその変数や引数が nil かもしれないことが考慮されているということが伝わりやすい。 blank? を気軽に使う人たちは ' '.blank?' '.blank? の結果を知って使ってるのかも気になる。 これらを知った上で blank? が適切だと判断し blank? を使ってるならいいけど、そうは思えないようなコードをよく見る。

似たような理由でブロック無しの any? が嫌いで、 !ary.empty? のかわりに ary.any? と書く人たちがいるけど、[nil].any?[false].any? の結果を知った上でその挙動を意図して書いているのか、単に ! と empty? を書くのがめんどくさくてそうしているのかが読みとりにくい。

Rails アプリでオンラインでカラムの削除やリネームを行うには

前提知識

Rails アプリにおいて、テーブルの追加やカラムの追加は簡単なものの、カラムの削除やリネームは慎重に行う必要がある。たとえアプリからそのカラムを参照してないとしても、いきなりカラムを削除するとエラーになる可能性が大いにある。

というのも Rails にはスキーマキャッシュというものがあり、テーブルのカラム情報をモデルがキャッシュしているからだ。このキャッシュはたとえばいわゆる N+1 クエリ問題を避けるために includes (eager_load) するときに参照される。 SELECT 句で t0_r0 のような機械的に別名が振られるようなクエリを見たことがある Rails エンジニアは多いと思う。

機械的に全カラムを取得するためにスキーマキャッシュを利用しているため、このようなクエリが実行されてる中でカラムを削除したりリネームしたりすると、スキーマキャッシュをもとに並べらたカラムの一部が消えてしまうため、不正なクエリとなってエラーになってしまう。 このせいで Rails においてカラムの削除やリネームは面倒なものになっている。

Rails 5.x 以降のやりかた

Rails 5.0 からは ignored_columns というオプションが追加され、ここに追加されたカラムは Rails からは見えなくなる。 このオプションはまさに Rails のオンラインスキーマ変更のやりにくさを解消するために追加された。僕が Rails 5.0 で最も好きな改善の一つだ。

https://github.com/rails/rails/pull/21720

なのでカラム削除の手順としては、

  1. アプリケーションコード内でそのカラムを参照している箇所を直す (これはスキーマキャッシュとは無関係に当然やる必要がある)
  2. Model.ignored_columnsカラム名を追加してデプロイ (1と同時でもよい)
  3. 実際にカラムを削除またはリネーム
  4. Model.ignored_columns を元に戻してデプロイ

という流れになる。

Rails 4.x でのやりかた

ignored_columns は使えないが、DB からカラム情報を取得するところにモンキーパッチをあてて特定のカラムを隠すようにすればなんとかいける。 具体的には MySQL を使っている場合は mysql2 adapter のメソッドをフックする。

module ActiveRecordInvisibleColumn
  INVISIBLE_COLUMNS = {
    'foos' => ['bar'],
  }.freeze
  def columns(table_name)
    super.delete_if { |column| INVISIBLE_COLUMNS.fetch(table_name, []).include?(column.name) }
  end
end

ActiveSupport.on_load :active_record do
  ActiveRecord::ConnectionAdapters::Mysql2Adapter.prepend(ActiveRecordInvisibleColumn)
end

この方法は foos というテーブル名が (たとえ複数の DB を使っていても) グローバルに一意という前提を置いている点に注意する必要がある。 これで Rails 5.x の ignored_columns と同じような効果が得られるので、あとは同じ流れでカラムの削除を進めることができる。 過去に某巨大 Rails アプリで実際に成功したのでまぁたぶんだいたいいけると思う。

とはいえ Rails 5.0 以上にさっさとバージョンアップして ignored_columns を使ったほうがいいことは言うまでもない。

(ネタバレあり) 魔法少女リリカルなのは Reflection 初日感想

文章にまとめられなかったので箇条書きで。

  • まず劇場に入る直前に Detonation が 2018 年公開という掲示を見て戸惑う。そこに映ってるなのはの姿は Reflection と一緒だし、キャスト一覧を見ても変わってないし、つまり……? という気持ちで入場した
  • はやてが自分の足で走ってる姿にまず感動。立って歩いてる姿は 2nd A’s にもあったけど、こう普通に生活できてるんだな感が
  • 今までは海鳴市という架空の場所だったのに、急に東京とか新宿とかが出てきて驚いた。これ今までのなのはシリーズの中で結構大きな変化な気がするけど……?
  • 咄嗟にワイヤーで反撃するのがとてもなのはらしくてよかった。強い
  • キリエがなのはやフェイトの顔面を全力で殴ってて笑った。実質 Vivid Strike!
  • リインがキリエに抵抗しているのがかわいかったし、キリエもちゃんと手加減していて傷付けることが目的ではないことを表せていてよかった
  • なのはが今度こそ助けると決意している横顔をアリサが心配しているシーン…… ここが実になのはらしくアリサらしく、今回の映画の中で特に好きなシーンの一つ
  • 闇の書事件を回想するときに「2年前」と言われていて実際作中だとそうなんだけど、こっちは5年待ったよ!!という気持ちで見ていた
  • シャーリー出てくるの意外すぎた
  • マテリアルズの登場。紫天一家大好きなので事前情報で登場が分かった時点で喜んだし劇場アニメで動いている姿を見れて感謝……
  • なのは対シュテル、高火力の魔法のぶつかり合いは見ていて楽しい。そういえばこのときってレイハさん無しで戦ってた?
  • ユーノくんの結界強すぎる
  • フェイト対レヴィは、フェイトの最後の必殺技ですね…… バインドをかけて全力全開といいながら撃つ姿は 1st のなのはのそれでここも特に好きなシーン
  • リンディを母さんと呼ぶシーンはとても感動的なんだけど、バインドをかけられてるとはいえレヴィがすぐ近くにいることを思うとちょっと
  • はやて、基本後方支援型なので全体的に得意分野じゃない戦闘で苦戦したり変身シーンもカットされたりでやや不遇だった……
  • イリスが嘘を告白するシーン。全部が嘘ってのもちょっと無理があると感じたし Detonation で回収されてほしい
  • そしてユーリ登場。一応 BoA や GoD のストーリーを知ってる身としては特に設定が変わったように見えたけど、とはいえ Reflection の時点では情報が少なすぎてなんとも。個人的にはイリスが言ってる内容には一部間違いというか勘違いみたいなのが含まれていると思っているけど、どうなるんでしょうね
  • なのはが立ち向かおうとしたところで終わり。入場前に見た Detonation の掲示と合わせて、前後編か~~という気持ちになった
  • 最後の週変わりの映像特典、マテリアルズがメインでしゃべってるだけで嬉しいし、来週は初代リインフォースも出るっぽいので楽しみですね

(ネタバレあり) アイカツスターズ! イリュージョン Show Time 感想

アイカツスターズ! の VR ステージ「アイカツスターズ! イリュージョン Show Time」を何度か見てきた。 http://www.aikatsu.com/stars/event/aikatsustars_illusionshowtime/

A パターンと B パターンの2つがあって、両方とも見た。実際のところ A と B の差分はそこまで大きくはなくて、曲目と順序や MC パートは共通で一部の曲で歌うユニットやコーデが違うという程度。

VR ステージ

VR ステージというのを見るのは初めてで、少し前にプリキュアでもやってるのを知ってたけど、実際に見たのは今回が初めて。 メガネをつけずに楽しめる VR ステージがどんなものか期待と不安があったが、実際に見てみるとすごくよく出来ていて感動した。 フル CG によるライブはテレビアニメとかアーケードとかで見慣れているわけだけど、VR ステージだと立体感が全然違うというのもあるし、テレビアニメだとカメラワークによってメインだけが映されていたりするものが VR ステージだと本物のライブのときのように常に全体をみわたせてそれぞれのよさがあった。 とくに MC パートではキャラクターの背後に影ができていたり、レイが話している間に観客のほうをきょろきょろして落ち着きがないきららがいたり、本物感を感じられた。スポットライトがあたっていないときの挙動にそれぞれのキャラクターの個性が出ているのがよい。

途中にファッション Show Time という撮影 OK のコーナーがあるのでそこでの写真。暗闇の中なのでうまく撮るのがむずかしい…… なんとなく後ろに影があるのがわかると思う。

映像の出来は大満足だったんだけど、あえて不満点を上げるなら前後の移動がほとんど無いことだろうか。キャラクター同士が交差することはあっても、前後の移動が極端に小さいように感じた。技術的制約なのかもしれない。 キャラクターが前に出てくるのに合わせて背景等が動くことで前に出てきてる感は出ていたけど。

ライブ

基本的に曲は全部ショートバージョンなんだけど、みんな大好き episode Solo だけがフルバージョンで、フルだとわかった瞬間に自分含めて会場全体でどよめきがおこった体験がよかったですね…… 荒野の奇跡ではステンドグラスが割れるシーンが VR でも再現されていてよかったし、舞っていた羽がラストで空中で停止する演出もかっこよかった。停止するのはアニメ53話ともアーケードとも違うので独自だと思う。

A パターンでは One Step がアニメ31話の SKY-GIRL バージョンで、個人的にはゆずこしょうのユニットが好きではあるんだけどこれはこれでよい。 またアニマルカーニバルではゆずとあこのバージョンで始まった後、途中できららが入ってくるという楽しい演出があった。ゆずといいきららといい、スターズになってからポップの自由なアイドルが好きになった。 一方 B パターンではアニマルカーニバルがアニメ42話のゆずリリバージョンだった。ありがとうございます。One Step はゆずこしょうバージョン。 それ以外の A、B の差分だと、Miracle Force Magic でツバサのコーデが変わっていたり、A ではみつばちのキスがゆめロラで B ではアニメ44話のまひロラバージョンだったり。どちらが好きかが諸説ありそうだけど個人的には A がより好きです。

その他

ライブシーン以外にもニコニコしてしまう場面がいくつかあって、リリィのステージが終わったときに壇上に出てきて甘やかしていくゆずとか、ファッション Show Time での香澄姉妹の掛け合いとか、エルザとかと立っているときららの身長が低めな点が目立ったりとか。 それとエンディングで2人ずつペアで登場して締めの挨拶をしていくところは外せなくて、最初いきなりひめツバが手を繋いで登場したときにヒッと声が出たけど、その後ゆずリリ、よぞまひ、あこはる、ゆめロラと全員手を繋いで登場して感謝しかなかった。 カプ厨にやさしい。

分離式キーボード MiSTEL BAROCCO MD600 を使ってみた感想

https://www.amazon.co.jp/dp/B01KN6VEYG/ これを買って3週間ほど仕事とプライベートで使ってみた感想。 先に結論を書いておくと、現在は使わなくなってしまった。

きっかけ

Nintendo Switch を発売日に手に入れてゼルダの伝説 BotW を楽しんでいたところ、プロコンのほうがより正確に操作しやすいと感じつつも、左右に分かれた Joy-Con での操作がとても楽であることを実感し、分離式キーボードにも興味を持って MiSTEL BAROCCO MD600 を買ってしばらく使ってみることにした。

前提

RealforceThinkPad のワイヤレスキーボードをよく使っていて、単純にキーボードとしては Realforce が一番良いと思いつつも、ThinkPadトラックポイントとワイヤレス (Bluetooth) も便利だと思って使っている。 どちらも物理的には JIS 配列のキーボードであるものの、論理的には US 配列として使っている、いわゆる物理 JIS 論理 US の状態。 余った無変換キーとか変換キーとかを Esc や Shift にリマップして使っている。 https://github.com/eagletmt/dotfiles/blob/master/dot.Xmodmap

良かった点

楽な姿勢で使える

そもそもこれを期待して買ったわけだけど、実際に左右に分かれていることでより楽な姿勢で使うことができた。個人的にはこれだけでも結構でかい。左右のキーボードをつなぐ付属の USB ケーブルがやたら短いのが気になったけど……

悪かった点

右手でも左手でも押したいキーがあるのに押せない

自分は b と y のキーを普段は両方とも左手で押しつつもたまに右手で押したりする。 左右に分離しているキーボードである以上、右側にあれば常に右手、左側にあれば常に左手で押さなければならない。これが結構きつかった。 y キーを右手で押すように矯正するのはそれほど大変ではなかったけど、普段左手で押してる b キーも時折右手で押していることに気付いて、これらを完全に矯正しきるのはなかなか大変そうだと思った。

物理 US のキーボードである

前提でも書いたけど、普段は無変換キーや変換キーを Esc や Shift にリマップしている環境で生きていて、それらができない時点でストレスがあった。 物理 US の環境は仕事で触れる機会もあるので一時的に使う分には問題無いけど、ストレスなく常用できるまではいけなかった……

一部キーが Fn と同時押しである

矢印キーやバッククォート、チルダを押すのに Fn と同時押しで厳しい。物理 US であることにも関連するけど、自由にリマップできるとはいえそもそもキーの数が少なすぎる。

うるさい

メカニカルキーボードうるさい……

まとめ

分離式で、JIS 配列で、左右に y と b キーがある、Realforce がほしい。

Docker と nftables

Docker は iptables を前提として書かれており、nftables を使っている環境ではそのままだと動かない。 issue は既にあったんだけど nftables を使っている環境はまだまだ少なそうだし進まなそう https://github.com/moby/moby/issues/26824 。 とはいえ iptables でやってることは少ないので、ちょっと設定を足せば動かせる。

とりあえず dockerd に iptables を触らせないようにして、

# /etc/systemd/system/docker.service.d/override.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// --iptables=false

あとは nftables の設定で masquerade を足せばいい。nftables だと変数使えて便利ですね。

define docker_bridges = {"docker0", "br-0123456789ab"}

table inet filter {
  chain forward {
    type filter hook forward priority 0;
    policy drop;

    ct state {established, related} accept;
    ct state invalid drop;

    iifname $docker_bridges accept;
  }
}

table ip nat {
  chain prerouting {
    type nat hook prerouting priority 0;
  }

  chain postrouting {
    type nat hook postrouting priority 0;
    oifname != $docker_bridges masquerade;
  }
}

Docker の network を作成すると br-${network_id} というブリッジが作成されるので、Docker の network を作成するたびに手動で編集する必要があるのがだるいけど、まぁ network を増やすことはそんなに発生しないはず。