ISUCON6 4位でした

会社の同僚の@wata_dev@osadake212とISUCON6本戦に出場して4位でした。チームメンバー全員普段アプリケーション書いてるエンジニアでインフラ寄りのメンバーがいなくて複数台構成の本戦はきついだろうなと思ってたので、4位という結果はかなり健闘したほうだと思うけどやはり悔しい・・。

本戦での役割的にはざっくり

  • @hokaccha: 方針たて
  • @wata_dev: インフラ
  • @osadake212: アプリケーション

という感じでした。

どんなアプリケーションだったか

  • リアルタイムお絵かきアプリ
  • Reactでサーバーサイドレンダリング
  • 裏にAPIサーバー
  • SSEで変更をpush
  • 全部Docker

序盤

itamaeでMySQL, Nginx, Ruby, Redisあたりのレシピを作っといて複数サーバーでも必要なミドルウェアといい感じの設定をすぐに入れられるようにしといたんだけどまさかのDockerだったのでそのまま使えなくてパニくったのが序盤のハイライト。

Docker全くわからないという感じではなかったものの、メンバー全員そこまで熟練しているわけではなく、最初の構成の確認とかローカルに開発環境作ったりするのに手間がかかって初動が遅れた。

結局昼前ぐらいに@wata_devの提案でDockerをやめようと決断して全てのコンポーネントを脱Dockerしてベンチ通ったのが13時ぐらいだった。このあたりはほとんど@wata_devにやってもらった。これで事前に仕込んどいたitamae使えるようになって僕歓喜

終端をNginxで受けて静的ファイルをNginxで返すぐらいまではやって多少スコアあがったぐらいだった。ここまでアプリケーションには全く手を入れられず。

中盤

とりあえずサーバー5台をフルで使うための分散戦略を考え始めた。全サーバーにReact、RubyMySQLを同じ構成で立てて、room idごとに分散させつつtokenとかroomなどの共通で使うデータだけ一箇所に集めて、他はそれぞれのサーバーにたってるMySQL見るよにすればよさそうという案でアプリケーション側の実装とインフラが側の設定を始める。

これは最終的にはうまくいかなくて、結局トップページのroom一覧をつくるのにstrokeのデータも中央にないとダメで、failもでまくっていたので一旦諦めてMySQLは一箇所に変更した。完全に方針ミスった。

とりあえずアプリケーションの分散はできたのでこの構成でアプリケーションの高速化に取りかかることに。が、時間が足りなすぎた・・。この時点で16時ぐらいだったかな・・。

終盤

とりあえず分散させただけで高速化ほとんどやれてないので、アプリケーション側を改善に入る。ReactがSSRしている/img/:idが重いことはわかっていたのでそこをキャッシュしようということになった。

JS側でキャッシュしてRubyは更新のときにキャッシュをpurgeする作戦で、僕がJS側、Ruby側を@osadake212にやってもらって割とさくっと実装できたが、僕がタイポしまくりで本番を壊しまくってたのが終盤のハイライト。

その実装が入って20000点を超えた。その後色々やりたいことはあったけど時間内でできそうなことがなかったので、ログ切ったり再起動チェックしてフィニッシュした。結果failせずに4位だったのはよかった。

その他の感想

  • 序盤に、httpsってことはHTTP/2話せそうだねーという話はしていたが最後のほう完全に忘れていた
  • Node.js/Reactあたりは自分の得意分野だったのにもかかわらずそこを最適化するまでに至らなかった。一番の悔しいポイント・・
  • SSEのstreamの接続が数秒で切れるようになってる実装の意味がわからなくてだいぶそっちに頭もっていかれた
  • 途中の方針完全に失敗してごめんなさいという気持ち
  • itamae最高だった

謝辞

@wata_devは予選で一番難しい正規表現の改善をお願いした結果、最後までバグがとれなくて、つらい思いをさせてしまったんですが、本戦ではインフラの構築から細かいところまで色々とやってもらって本当に助かりました。

@osadake212には雑にアプリケーションの実装方針伝えたらさくっと実装してくれて助かりました。逆に実装早くて手持ち無沙汰にさせてしまってた感すらありました。

よいチームメートとでれて楽しかったです。

また、出題チームは本当にいい問題ありがとうございました。運営チームも毎年よい大会をありがとうございます。来年もがんばります。

sqlite3でカラム定義の変更

sqlite3だとalter table change columnみたいのがないらしいのでnot nullとかdefault valueを変更するのどうすればいいんだろうと思ってrailsがどうしてるか見てみた。

class CreateTodos < ActiveRecord::Migration[5.0]
  def change
    create_table :todos do |t|
      t.string :text
      t.boolean :completed

      t.timestamps
    end
  end
end
class UpdateTodos < ActiveRecord::Migration[5.0]
  def change
    change_column :todos, :text, :string, null: false
  end
end

こんな感じのmigrationを実行してみるとログはこうなった。

   (0.1ms)  begin transaction
   (0.2ms)  CREATE TEMPORARY TABLE "atodos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "text" varchar NOT NULL, "completed" boolean, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL)
   (0.4ms)  INSERT INTO "atodos" ("id","text","completed","created_at","updated_at")
                     SELECT "id","text","completed","created_at","updated_at" FROM "todos"
   (0.6ms)  DROP TABLE "todos"
   (0.1ms)  CREATE TABLE "todos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "text" varchar NOT NULL, "completed" boolean, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL)
   (0.1ms)  INSERT INTO "todos" ("id","text","completed","created_at","updated_at")
                     SELECT "id","text","completed","created_at","updated_at" FROM "atodos"
   (0.1ms)  DROP TABLE "atodos"
  SQL (0.1ms)  INSERT INTO "schema_migrations" ("version") VALUES (?)  [["version", "20161013104634"]]
   (0.7ms)  commit transaction

一時テーブル作ってinsert selectでデータ退避させて作り直してるらしい。なるほど・・。

ISUCON6 予選の記録

ISUCON6にしましまスペシャルというチーム名で会社の同僚と参加して最終スコア147,028で予選通過できました。言語はRubyです。コードはここに公開してます。

https://github.com/hokaccha/isucon2016_qualifying

以下やったこととかのメモ。

10時-11時

下準備を整える

  • どういうアプリケーションか確認
  • コードをざっと読む
  • サーバーのスペックとか動いてるプロセスを確認
  • ベンチ流してみてリクエストの傾向を把握する
    • nginxのログから集計してブラウザから見れるような雑なやつを用意しといた

11時-12時

作戦をたてる

  • とりあえず//keywordが遅いのでそこを改善することにする
  • htmlifyの改善、isutarの統合、インフラ・ミドルウェア周りの設定の3つに作業を分けてそれぞれ取り掛かる
  • isupamもどうにかしたほうがいいかと思ったけど、計測結果を見るにそこまで遅くないのでこの時点で改善を捨てた(結果この判断はよかった)
  • とりあえずruby実装に変えてベンチ流したらスコア 0になってここからしばらく0だった

12-15時

  • ローカルで開発できる環境作ったりdeploy script書いて開発環境を整える
  • nginxでstatic file返す
  • unix domain socketを使う
  • isutarを統合してstarを全部redisに載せる
  • userは最初に全部引いてきてメモリに載せる
  • unicorn の worker 数増やす
  • などなど、htmlifyの改善以外は(isupamを除いて)ほぼほぼやり終えた
  • しかしスコア300点だった(0からやや進んで喜んでた)

15-16時

  • htmlifyの置換処理をいい感じにする最初の実装がマージされる
  • 正しくリンクが作られずベンチがfailしたのでrevertする
  • htmlifyの高速化が入ってもどのみち結果はキャッシュしといたほうがよさそうなのでキャッシュを実装してみる
    • htmlifyの結果をredisにキャッシュしといてPOSTでinsert/updateが走ったときに変更が必要なキャッシュだけ消す実装にした
    • キャッシュの実装が入った結果スコア2万を超える

この実装が今回秘孔をついたらしい。

16-17時

  • htmlifyの高速化のバグがなかなか取れなくてこのままでは間に合わない可能性がありそうということに気づき始める
  • これまでhtmlifyの高速化を信じて全く手をいれてなかったので、間に合わなかったことを考えて最低限keywordsぐらいはキャッシュするようにした
  • この実装で6万ぐらいまでいった気がする(記憶が曖昧)

17-18時

  • 初期データのdescription -> htmlの変換を予めやってDBに保存しといた上で/initializeでredisに全部乗っけるようにした
    • その結果スコア15万超える
  • htmlifyの高速化ロジックはバグが取れずにマージを断念
  • ログ切ったりunicornのworker数調整してベンチマークガチャ回して15万弱でfinishした

感想

  • htmlifyのロジック変更が間に合わない場合のことを考えた実装に取り掛かる判断が早めにできたのがよかった
  • アプリケーションよりのエンジニア3人で望んだけどなんとかなる問題でよかった
  • 途中がんばって色々やってもスコアが0ではりついて動かなかったのはつらかった
  • 本戦がんばります

Railsのscopeとclass method

http://api.rubyonrails.org/classes/ActiveRecord/Scoping/Named/ClassMethods.html#method-i-scope

  • ARのscopeはclass methodとだいたい同じ
  • scopeはnilfalseを返した時にallを返すのでメソッドチェインをブロックしない
  • 必ずActiveRecord::Relationを返す場合はscopeのほうがよさそう

Babel 6.xでErrorとかArrayをextendsしたときの挙動がおかしい

class FooError extends Error {}
console.log(new FooError() instanceof FooError); //=> false
console.log(new FooError() instanceof Error); //=> true

class FooArray extends Array {}
console.log(new FooArray() instanceof FooArray); //=> false
console.log(new FooArray() instanceof Array); //=> true

サポートしてないらしい。
https://phabricator.babeljs.io/T3083

GraphQLでNonNullなList

  1. フィールド自身がNonNull
  2. 要素がNonNull
  3. その両方

があって

let QueryType = new GraphQLObjectType({
  name: 'Query',
  fields: {
    list1: {
      type: new GraphQLNonNull(new GraphQLList(GraphQLString)),
      resolve: () => arr1,
    },
    list2: {
      type: new GraphQLList(new GraphQLNonNull(GraphQLString)),
      resolve: () => arr2,
    },
    list3: {
      type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))),
      resolve: () => arr3,
    },
  },
});

GraphQLの型で表現するとこうなる。

type Query {
  list1: [String]!
  list2: [String!]
  list3: [String!]!
}

違いはこんな感じ

  • list1
    • null : NG
    • ['foo', null] : OK
    • [] : OK
  • list2
    • null : OK
    • ['foo', null] : NG
    • [] : OK
  • list3
    • null : NG
    • ['foo', null] : NG
    • [] : OK

module_function

moduleに普通にメソッドを定義するとインスタンスメソッドとして定義される。

module Foo
  def foo
  end

  puts instance_methods.include?(:foo)         #=> true
  puts private_instance_methods.include?(:foo) #=> false
  puts singleton_methods.include?(:foo)        #=> false
end

classにincludeして使う。

class Bar
  include Foo
end

Bar.new.foo

module_functionを使うと、instance_methodsから消えてprivate_instance_methodssingleton_methodsに追加される。

module Foo
  module_function
  def foo
  end

  puts instance_methods.include?(:foo)         #=> false
  puts private_instance_methods.include?(:foo) #=> true
  puts singleton_methods.include?(:foo)        #=> true
end

モジュールから直接呼んだり

Foo.foo

includeしたclassでレシーバーなしでアクセスできるようになる。

class Bar
  include Foo

  def hoge
    foo
  end
end

ただしpublicなインスタンスメソッドにはならない。

Bar.new.foo #=> NoMethodError

includeする使い方をしないのであればsingleton_methodだけ定義するだけでもいい。

module Foo
  def self.foo
  end

  puts instance_methods.include?(:foo)         #=> false
  puts private_instance_methods.include?(:foo) #=> true
  puts singleton_methods.include?(:foo)        #=> false
end
Foo.foo