カレーの恩返し

おいしいのでオススメ。

sorbet は production で使えるのか [2020/03/29 時点]

sorbet は少し前に話題になっていた Ruby の type annotaion gem。

sorbet.org

以下のようなコードを書くと静的型チェック、動的型チェックの両方をやってくれる。

# typed: true
require 'sorbet-runtime'

class A
  extend T::Sig

  sig {params(x: Integer).returns(String)}
  def bar(x)
    x.to_s
  end
end

def main
  A.new.barr(91)   # error: Typo!
  A.new.bar("91")  # error: Type mismatch!
end

上記コードのsrb ts実行結果

editor.rb:12: Method barr does not exist on A https://srb.help/7003
    12 |  A.new.barr(91)   # error: Typo!
          ^^^^^^^^^^^^^^
  Autocorrect: Use `-a` to autocorrect
    editor.rb:12: Replace with bar
    12 |  A.new.barr(91)   # error: Typo!
                ^^^^


editor.rb:13: Expected Integer but found String("91") for argument x https://srb.help/7002
    13 |  A.new.bar("91")  # error: Type mismatch!
          ^^^^^^^^^^^^^^^
    editor.rb:5: Method A#bar has specified x as Integer
     5 |  sig {params(x: Integer).returns(String)}
                      ^
  Got String("91") originating from:
    editor.rb:13:
    13 |  A.new.bar("91")  # error: Type mismatch!
                    ^^^^
Errors: 2

sorbet は前から気になっていたけど社内で運用できる状態になっているのかどうか気になったので土日で調べてみた。

気になっていたこと

  • sorbet-typed に更新があったら各リポジトリに更新を反映した PR を自動で作ることは可能なのか
  • 社内 gem 用の sorbet-typed は作れるのか
  • CI に組み込んで型チェックにコケたら PR 上で自動で指摘させることは可能か
  • 型の coverage のようなものを計測することはできるか
  • メタプロで生成されたメソッドの型定義はどうなるのか
  • 周辺環境はどのくらい整備されているのか
    • エディタとの integrate, sorbet の型定義を何かから自動生成する何か

sorbet-typed に更新があったら自身のリポジトリに更新を反映した PR を自動で作ることは可能なのか

sorbet には TypeScript の DefinitelyTyped のような型定義の中央管理システムがあって、gem に型定義を追加する場合はここに PR を投げる形になっている。
GitHub - sorbet/sorbet-typed: A central repository for sharing type definitions for Ruby gems

また、sorbet は型定義ファイルを commit する方針を取っている1ので利用している gem の型定義が更新されると自身のリポジトリsrb rbi updateを実行し、明示的に更新する必要がある。

型定義をいい感じに更新する PR を投げてくれる script がどこかに転がっていればいいなと思ったけど、パッと調べたところ見当たらなかった。作る必要がありそう。

travis cronjob とか使えばいい感じにできるんじゃないかと思っている。しらんけど

docs.travis-ci.com

結論:自作すれば可能

社内 gem 用の sorbet-typed は作れるのか

上に書いたように sorbet は sorbet-typed を使って型定義を共有する仕組みを作っている。 ただ、これは public なので private gem の型定義をここに書いちゃうと全世界に公開することになってしまう。 それは困る。
社内用の sorbet-typed を作り、それを差し込めるような仕組みがあれば private gem でも社内で利用することはできるはず。

sorbet, sorbet-typed の issue や sorbet Slack を眺めたりしたけど private gem に対する言及はどこにもされていなかった。 その辺りのソースコードも眺めてみたけど sorbet-typed の URL がハードコードされていた。 サポートされていない可能性が高い?

じゃあ Stripe はどうしているんだという話はあるし、他で問題になっていないわけがないので何を根本的に勘違いしている可能性がある

結論:本家に PR 投げれば可能?

CI に組み込んで型チェックにコケたら PR 上で自動で指摘させることは可能か

PR 上でコメントをつけてくれるサービスはいくつがあるが、社内では reviewdog を使うことが多い。
reivewdog は sorbet 未対応だったので PR を出した。数日中に使えるようになるはず。

追記:出していた PR が merge された!

github.com

他のサービスが sorbet に対応しているかどうかはちゃんと調べてない

結論:reviewdog を使えば可能

型の coverage のようなものを計測することはできるか

ここのサポートは手厚く、かなり細かい情報まで取得することができる。
Tracking Adoption with Metrics · Sorbet

しかも、導入フェーズに合わせてどの metrics に着目すべきかまで書いてくれているのでとても分かりやすい。
https://sorbet.org/docs/metrics#suggestions-for-driving-adoption

取得できる metrics 一覧

% srb tc --metrics-file=metrics.json
% cat metrics.json | jq ".metrics[].name" | sort
"ruby_typer.unknown..error.max"
"ruby_typer.unknown..error.min"
"ruby_typer.unknown..error.p25"
"ruby_typer.unknown..error.p50"
"ruby_typer.unknown..error.p75"
"ruby_typer.unknown..error.p90"
"ruby_typer.unknown..error.total"
"ruby_typer.unknown..release.build_scm_commit_count"
"ruby_typer.unknown..release.build_timestamp"
"ruby_typer.unknown..run.utilization.context_switch.involuntary"
"ruby_typer.unknown..run.utilization.context_switch.voluntary"
"ruby_typer.unknown..run.utilization.inblock"
"ruby_typer.unknown..run.utilization.major_faults"
"ruby_typer.unknown..run.utilization.max_rss"
"ruby_typer.unknown..run.utilization.minor_faults"
"ruby_typer.unknown..run.utilization.oublock"
"ruby_typer.unknown..run.utilization.system_time.us"
"ruby_typer.unknown..run.utilization.user_time.us"
"ruby_typer.unknown..types.input.bytes"
"ruby_typer.unknown..types.input.classes.total"
"ruby_typer.unknown..types.input.files"
"ruby_typer.unknown..types.input.files.sigil.autogenerated"
"ruby_typer.unknown..types.input.files.sigil.false"
"ruby_typer.unknown..types.input.files.sigil.ignore"
"ruby_typer.unknown..types.input.files.sigil.strict"
"ruby_typer.unknown..types.input.files.sigil.strong"
"ruby_typer.unknown..types.input.files.sigil.true"
"ruby_typer.unknown..types.input.methods.total"
"ruby_typer.unknown..types.input.methods.typechecked"
"ruby_typer.unknown..types.input.sends.total"
"ruby_typer.unknown..types.input.sends.typed"
"ruby_typer.unknown..types.sig.count"

metrics.json を人間が見やすい形式にしてくれる gem も見つけた。これを各 PR に comment する bot 作るととても良さそう。

github.com

bundle exec srb tc --metrics-file /tmp/sorbet_metrics.json
# No errors! Great job.
bundle exec sorbet_progress /tmp/sorbet_metrics.json
# Sorbet Progress

# Progress for sig coverage
# total_signatures  7528
# total_methods     183447
# total_classes     112433

# Progress for file coverage
# sigil_ignore      12      0.20 %
# sigil_false       5466        91.60 %
# sigil_true        460     7.71 %
# sigil_strict      12      0.20 %
# sigil_strong      17      0.28 %
# ---------------------------------------
# Total:        5967    100%
# Keep up the good work 👍

結論:可能

メタプロで生成されたメソッドの型定義はどうなるのか

experimental な機能として、メタプロで定義したメソッドに対して型定義を行うための plugin を書くことができる。

Metaprogramming plugins · Sorbet

# metaprogramming.rb
# typed: true

class Metaprogramming
  def self.macro(name)
    define_method("#{name}_time") { name }
  end

  macro(:bed)
  macro(:fun)
end

hello = Metaprogramming.new
hello.bed_time # error: Method `bed_time` does not exist on Metaprogramming
hello.fun_time # error: Method `fun_time` does not exist on Metaprogramming

上記のような macro を定義する DSL があった場合、以下のような plugin を書くことで sorbet にメソッドが定義されていたかのような挙動をさせることができる。

# macro_plugin.rb

# Sorbet calls this plugin with command line arguments similar to the following:
# ruby --class Metaprogramming --method macro --source macro(:bed)
# we only care about the source here, so we use ARGV[5]
source = ARGV[5]
/macro\(:(.*)\)/.match(source) do |match_data|
  puts "def #{match_data[1]}_time; end"
end
# Note that Sorbet treats plugin output as rbi files

Caveats

Sorbet decides which plugin to call using method names only. This might be a problem if different metaprogramming methods use the same name, have similar usages, but behave differently. To illustrate, using the configuration above, the following calls macro_plugin.rb twice but results in an error.
https://sorbet.org/docs/metaprogramming-plugins#caveats

気をつけないといけないこととして、同じ interface の DSL を作ると見分けるのが難しくなることが挙げられていた。
plugin には string のクラス名が渡ってくるので klass_str.constantize.method(method_name).owner で定義元を確かめることはできるが、クラスを load する必要があるのでsrb tcがかなり遅くなるはず。悩ましい。。。

Rails はメタプロの宝庫なのでどうなるんだろうと思っていたら、ちゃんとプロジェクトが進んでいた。
GitHub - chanzuckerberg/sorbet-rails: A set of tools to make the Sorbet typechecker work with Ruby on Rails seamlessly.

結論:基本的には可能

周辺環境はどのくらい整備されているのか

エディタとの integrate

Language Server Protocolに準拠しているので様々なエディタと互換性はあるらしいが、vscode plugin のようなものは見当たらなかった。 自分で設定して Language Server を立ち上げれば使えるのかも。

結論:可能(と言っている)

sorbet の型定義を何かから自動生成する何か

色々あった。

ちょっと性質は違うけど sorbet 用の rubocop も見つけた。
GitHub - Shopify/rubocop-sorbet

まとめ

今からすぐに使える感じではなかったけど、社内 gem の sorbet-typed 問題さえ解決すれば手の届く範囲にあるなと思った。 前向きに検討したい。


  1. その方針を取った理由は https://sorbet.org/docs/rbi#a-note-about-vendoring-rbis に書いてある

grpc_required_annotator gem つくった

rubygems.org

TL;DR

  • 会社で grpc ruby を使っていて、同じような実装何回もやってんなと思ったので共通化して gem にした
  • request message を required チェックを簡単にできる DSL を提供してくれる
  • いつも書いていた required をチェックするだけの冗長なテストがすごく短くなって生産性上がった

本編

最近会社で grpc ruby を使った backend を書いていて、「同じような実装何回もやってんな」と思うことがあった。 それは request message の required な field に対して、空だったときに GRPC::InvalidArgument を返すやつ。

class SampleService < SamplePb::Sample::Service
  def foo(request, call):
    raise GRPC::InvalidArgument.new("`num` is required") if req.num == 0  # これ
    raise GRPC::InvalidArgument.new("`str` is required") if req.str == "" # これも
    raise GRPC::InvalidArgument.new("`repeated_fields` is required") if req.repeated_fields.empty? # 嫌になってくる
    FooResponse.new(num: req.num, str: req.str, repeated_fields: req.repeated_fields)
  end
end

これに対応するテストを書くのもめんどくさい。

describe SampleService do
  describe "#foo" do
    subject {  described_class.new.foo(request) }

    context "when num is empty" do
      let(:request) {
        SamplePb::Sample::FooRequest.new(
          num: nil,
          str: "str",
          repeated_fields: ["a"]
        )
      }

      it "raises GRPC::InvalidArgument" do
        expect { subject }.to raise_error(GRPC::InvalidArgument)
      end
    end

    context "when str is empty" do
      ...
      it "raises GRPC::InvalidArgument" do
        expect { subject }.to raise_error(GRPC::InvalidArgument)
      end
    end

    context "when repeated_fields is empty" do
      ...
      it "raises GRPC::InvalidArgument" do
        expect { subject }.to raise_error(GRPC::InvalidArgument)
      end
    end
  end
end

shared_exaple を使えばそれなりに共通化はできるけど request のクラスは各 rpc ごとで異なるので微妙に使いにくい。

というわけでシュッと required な field を宣言できて、パッとテストを書けるいい感じの gem を作ろうと思った。
できたのがこちら

github.com

この gem に入っている GrpcRequiredAnnotator module を service の実装クラスに include することで required メソッドが使えるようになる。

class SampleService < SamplePb::Sample::Service
  include GrpcRequiredAnnotator

  required :num, :str, :repeated_fields
  def foo(request, call):
    # raise GRPC::InvalidArgument.new("`num` is required") if req.num == 0  # これ
    # raise GRPC::InvalidArgument.new("`str` is required") if req.str == "" # これも
    # raise GRPC::InvalidArgument.new("`repeated_fields` is required") if req.repeated_fields.empty? # 嫌になってくる
    FooResponse.new(num: req.num, str: req.str, repeated_fields: req.repeated_fields)
  end
end

こんな感じで各 rpc に対応するメソッドのすぐ上で required にしたい field を symbol で宣言すると実行前に validation してくれるようになる。便利。
各 rpc に対応する required な field を取得できるメソッドも生やしているのでテストもかなり行数が減った。

RSpec.describe SampleService do
  describe "#foo" do
    describe "required" do
      it "num, str and repeated_fields are required" do
        expect(described_class.required_fields(:foo)).to eq [:num, :str, :repeated_fields]
      end
    end
  end
end

結構便利だと思うので grpc ruby 書いてる人はぜひ使ってみてください。

method_addedで added された method の挙動を override して再定義してたりとか、その再定義を module の extend で実現しようとしたら grpc interceptor の治安の悪い仕様1によって壊れてしまった話は元気があればまた書きます。


  1. grpc interceptor は intercept した method の method インスタンスを参照できる

ModelとConcernに記述したcallbackの実行順

結論

Concern に記述された callback が先に実行される。
既存の callback を Model もしくは Concern に移すときは気をつける必要がある。


以下確認コード

class Hoge < ApplicationRecord
  include HogeConcern

  before_validation -> { puts "before_validation in Class" }
end

class HogeConcern
  extend ActiveSupport::Concern

  included do
    before_validation -> { puts "before_validation in Concern" }
  end
end
[2] pry(main)> Hoge.create
   (0.5ms)  BEGIN
before_validation in Concern
before_validation in Class
...

マイクロサービス他社事例

マイクロサービスの事例を調べた。 多少まとめた方がいいんだろうけど、一覧があるだけでも一定の価値はあると思ったので載せておく。


その他

更新系 Web API response 再考

更新系の Web API response をどうすればいいのか毎回悩むので良い感じのデザインパターンをパッと調べたけど見当たらなかったので友達と議論した。

どんな場合はどんな response を返すべき、という結論には至らなかったけど一通りのパターンは洗い出せたと思うのでメモしておく。

TL;DR

  • response bodyを返さないパターン
  • 更新したリソースの id だけ返すパターン
  • 更新したリソース全体を返すパターン
    • 更新したリソースだけを返すパターン
    • 関連する情報を含む更新したリソースだけを返すパターン
  • 更新したリソースのうちクライアント側が必要な情報だけを返すパターン
    • 柔軟なinterfaceである程度endpointを共通化するパターン
    • 固定のinterfaceで必要な情報のユースケース分のendpointを作るパターン

response bodyを返さないパターン

204 を返すパターン。 protocol buffer だと google.protobuf.Empty を返すパターン。

リソースの新規作成など、response として property を返さなくてもクライアント側でリソースの状態を知る術がある場合に使う印象がある。

更新したリソースの id だけ返すパターン

response body を返さないパターンと使う場面は大きく変わらない気がしている。

しかし、クライアント側が更新したリソースの id を知る術がない(request で複数のパラメータによってリソースを一意に特定するといった id 以外の情報で指定している場合)がクライアントが id を key として状態を管理しているため id を知りたい、といったケースでは id だけ返すパターンの有用性が出てくる。

更新したリソース全体を返すパターン

これは backend 側の目線で返すリソースを決めるパターンで大別すると2つに分類できる。

  • 更新したリソースだけを返すパターン
  • 関連する情報を含む更新したリソースだけを返すパターン

どっちで返すかはパフォーマンス次第という印象がある。

更新したリソースのうちクライアント側が必要な情報だけを返すパターン

これは frontend 側の目線で返すリソースを決めるパターンで実装方針は2つに分類できる。

  • 柔軟なinterfaceである程度endpointを共通化するパターン
  • 固定のinterfaceで必要な情報のユースケース分のendpointを作るパターン

柔軟なinterfaceである程度endpointを共通化するパターン

サーバに対してクライアントが複数存在しユースケースがたくさんある場合、こっちの方が手早く開発できる印象がある。 GraphQL, Protocol Buffer の Field Mask, Rails active-model-serializer の fields パラメータなどはこれに該当する。

固定のinterfaceで必要な情報のユースケース分のendpointを作るパターン

サーバとクライアントが 1:1 対応しているような比較的小規模なアプリケーションではこっちの方が手早く開発できる印象がある。

Thanks to

DB共有しているRailsアプリケーションのテストを書くときに気をつけること

これで2時間くらい潰れたので供養のために書いておく。

TL;DR

DatabaseCleanerのstrategyがtransactionになってると、もう一方のRailsアプリケーションに反映されないから気をつけよう

遭遇したこと

2つのRailsアプリケーションで1つのDBを共有しているサービスで以下のようなテストを書いていた。

  1. Rails 2 がレコードを作成する
  2. Rails 2 がRails1に対して作成したレコードのidを渡しAPI callをする
  3. Rails 1 が受け取ったidのレコードを取得し、中身を書き換える
  4. Rails 2 で中身が書き換わったことを確かめる
# めちゃくちゃ雑な図解

             Rails 1                Rails 2
                |                     |
                |                 a = A.create!
                |                     |
          A.find(a.id)                |
      a.update!(foo: "bar")           |
                |                     |
                |          expect(a.foo).to eq "bar"
                |                     |

実行すると、A.find(a.id)がResourceNotFoundを返してテストが落ちていた。
テストコードを何回見直してもおかしい箇所は見当たらず、悲しい気持ちになっていたら database_cleanerが悪いことをしている気がしたので確認してみると、

RSpec.configure do |config|
  ...
  condig.before do |_|
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.start
  end
  ...
end

database_cleanerのstrategyがtransactionに設定されていた。
なので変更してもDB自体にはcommitされていなかったためにRails 1から作成したレコードは見えていなかった。

よく考えると当たり前なんだけど、問題に直面するとなかなか気づかなかったりするのでメモメモ。

新卒ISUCONで優勝しました

新卒ISUCON(SHISUCON)とは

弊社ではwebアプリケーション開発の基礎となる知識を実践的に吸収するため、新人研修の一環として新卒メンバー+CTOでISUCONを行うことが恒例になっています。
このISUCONをSHISUCON(SHinsotsu ga Iikanjini Speed Up Contest)と呼んでいます。(他の人は新卒ISUCONと呼んでいます)

ルールは以下の2点を除けば本来のISUCONと同じです。

  • 競技時間が11:00~17:00と本番より2時間短い
  • 13:00~13:00, 15:00~15:30の計2回、ISUCON上級者のメンターがアドバイスをしにきてくれる

↓去年、一昨年のSHISUCONの内容

www.wantedly.com

www.wantedly.com

SHISUCONの意義についてはこれがわかりやすい

speakerdeck.com

今回のSHISUCONではYahoo!Y!SUCONを利用しました。
Y!SUCONは1台のサーバで解くことが想定されている問題ですが、今回は3台のサーバ(c4.large)が与えられました。

新卒ISUCONで優勝しました

新卒ペア*3 + CTOおひとりさまの4チームで戦い、自分は @spring1018 とチームを組みました。(新卒ではないけど別業種からの転職&本人の強い参加希望があったので新卒枠としての参加になったらしい?)

自分達のチームは24,550点でフィニッシュしCTOをおさえて優勝することができました。 自分達は結局サーバを1台しか使っていないのでY!SUCONと同じ状況でのscoreとなっています。
今まで行われていたSHISUCONではCTOが毎回優勝していたのでCTOから優勝の座を奪えてよかった✌️

f:id:euglena1215:20190512003038p:plain
青いのが自分のチーム

f:id:euglena1215:20190512005606p:plain
負けて体調が悪化するCTOの図

戦略

まず、自分も@spring1018もインフラエンジニアではないため制限時間内でのmiddlewareの完璧なチューニングは難しいだろうと考えました。 そこで 「競技時間中はインフラのことをなるべく考えずにアプリケーションに集中する、そのために80%のクオリティのチューニングはコピペで終わるような秘伝のタレを作っておく」 と話し合って決めました。

事前準備

過去問を解く

@spring1018は初ISUCONだったということもあり、問題形式に慣れることを目的にISUCON7予選を本選出場ラインのscoreが出るまで解きました。
(ネットワーク帯域の再現はしていないので正確ではない & 本選出場のscore届くまでに2日かかった)

自分はISUCONは参加したことがあったのですが、アプリケーションしか触ってこなかったのでインフラ周りの知識が皆無でした。
そのため個人的にISUCON6予選、ISHOCON2*1を解いてインフラ周りのハマりポイントに一通りハマっておくよう心がけました。

wikiに諸々の情報をまとめる

Home · wantedly/shisucon2019-teppei-haruki Wiki · GitHub

競技開始前からリポジトリを作っておき、wikiを見ればほとんども問題が解決することを目指し充実させました。
普通に便利なのでもうちょっとちゃんとまとめてブログの記事にしたい.

作業フェーズをリマインダーに設定しておく

f:id:euglena1215:20190512181402p:plain:h300
こんな感じ

人間は焦ってしまうと予定を忘れる傾向にあるので複数台使うかどうかの最終判断/再起動試験チェックなど忘れたら困るやつは予めリマインダーに設定しておきました。 これで時間のことを気にせず安心して改善に取り組めました。 作業フェーズもwikiに書いていた。

本番(時系列)

自分のやったことを時系列で書いていく、ペアの@spring1018がやったことは↓に記事のリンクが貼られるはず

spring1018.hatenablog.com

10:50ごろ 競技開始
真っ先にbenchを走らせて初期scoreを確認、確か1500点くらいだった気がする

10:58 ~/をgit管理下に

11:17 nginx, mysql, sysctlをgit管理下に移動

11:28 kataribe, myprofilerが動く状態になる
@spring1018がDB schemaの調査を終わらせてくれていたのでkataribe, myprofilerを見つつbenchを回した。
kataribeはこれ、myprofilerはこれ
kataribeから「この時点では静的ファイルもアプリケーションを介して配信していたのでnginxで配信した方がいいよね、ついでに304つけちゃおうか、ついでにnginxの秘伝のタレ流し込んじゃおうか」と判断。
myprofilerから「tweets系が明らかに遅い、一旦created_atにindex貼ってお茶を濁そう」という判断をした。(確かあんまりscore変わらなかったはず)

11:50 nginxの秘伝のタレを流し込む hiden nginx by euglena1215 · Pull Request #6 · wantedly/shisucon2019-teppei-haruki · GitHub
scoreはほぼ変化なかったけどkataribeががらっと変わって GET /が遅すぎるということがわかった
ISUCON序盤のはずなのに全然CPUを使ってなかったのでおもむろにunicornのworkerを1→2に増やしたら1500→1700になってDBのCPUがボトルネックであることがわかるようになった。

12:00すぎ アプリケーションの概要をきちんと把握する
ようやくボトルネックの箇所がわかるようになってきたので二人でアプリケーションの概要の整理をした

  • isuwitterとisutomoが2つに分かれている(DBも別)
  • tweetsのオーダーが10万なので重そう、GET /ではフォロワーのツイートだけでいいのに全員のツイートを取得している。無駄っぽい。
  • GET /でusers.nameを取得するためにN+1が発生している、直したい

12:30ごろ DBを1つにくっつける
初期実装ではisuwitterとisutomoの2つにDBが分かれていたのをisuwitter DBにfriendsテーブルをもってくることによって1つにくっつけた。
理由は以下の通り。

  • 今後2つのサービス(isuwitterとisutomo)をくっつける可能性がある、そうなったときには2つのDBとのconnectionを張る必要がでてきて2倍のconnectionが必要になる
  • 後半にテーブルの移し替えを行うとindexを貼り忘れるといった可能性がでてくる、なら何も手をつけていない今のうちにやっちゃえばよさそう

結局http通信による遅延がボトルネックになるところまでチューニングが進まなかったのでほとんど意味はなかった気がする。

15:03 GET /でフォロワーの最新50件のツイートだけを取得するようにする 1700→3800 where friend ids by euglena1215 · Pull Request #14 · wantedly/shisucon2019-teppei-haruki · GitHub
この修正に2時間かけてしまい、申し訳ねえ申し訳ねえと呻きながら修正したら3800に点数が上がってホッとした。

kataribeを見るとこんな感じにGET /が若干早くなってGET /hashtagGET /searchと同じくらいになった。
なので次はGET /のN+1改善とdef searchで実行しているクエリを絞るのをやっていこうという話になった。

↓は申し訳ねえ申し訳ねえと呻きながらbinding.pryしているときに送られてきた自分の部署のリーダーからのDM。絶対に許さない。

f:id:euglena1215:20190513011018p:plain
自分のscoreが伸びないことを喜ぶチームリーダーの図, 一番下が自分のチーム

15:28 GET /のN+1改善 3800→3941 global user hash by euglena1215 · Pull Request #16 · wantedly/shisucon2019-teppei-haruki · GitHub
全然伸びなくて :thinking_face: という感じだった。SHISUCON終了後にキャッシュをインスタンス変数からThread.currentに持ち替えたら50000→59000とガッと上がったので最初からThread.currentで保存するようにしていたらもっと伸びていたかもしれない。

15:32 def searchで実行しているクエリの取得数を絞る by @spring1018 3941→13311 change get_all_tweets by spring1018 · Pull Request #17 · wantedly/shisucon2019-teppei-haruki · GitHub
これが効いてかなり点数が伸びた。
kataribeを見るとこんな感じにまたGET /ボトルネックが戻ってきた。

この辺りから複数台構成を意識し始めて@spring1018に2,3台目のセットアップをお願いした気がする。

16:00 GET /でisutomoにアクセスしていたのを直接実行するように変更した 13311→14865 join GET / by euglena1215 · Pull Request #11 · wantedly/shisucon2019-teppei-haruki · GitHub

この辺りで@spring1018が2台目のサーバの/etcを全消去するというアクシデントが発生し、2台目は復旧不能になってめちゃくちゃ面白かった。
一応3台目もセットアップするようお願いしたけど、さすがに時間が間に合わず最後まで1台のサーバで動かすことになった。

16:15 tweets tableにuser_id, created_atの複合indexを貼った 14865→23914 myprofilerでスロークエリを確認するとSELECT * FROM tweets WHERE user_id = ? ORDER BY created_at DESCが一番多かったのでindexを貼るとすごい点数が上がった。わーい。

16:36 erbをerubisに変更 23914→24951 add erubis by euglena1215 · Pull Request #20 · wantedly/shisucon2019-teppei-haruki · GitHub 定番のやつ

この辺で「もうこれ以上触ると壊れそうだし何もしない方がいいよね*2」ということで片付けて撤収した。

その後結果発表で優勝したと聞いてよかったねーとなった。

感想

今まで参加していたISUCONではインフラ周りを全く触っていなかったのでとても良い勉強になってよかった。
本番のISUCONも本選出場できるよう頑張りたいです。


延長戦

競技終了後、雑談をしているとその間に他のチームが47000点を叩き出していて悔しかったのでもう少し頑張ることにした。

最終的に65000点で終了した

*1:本来はSHISUCONではこの問題を使う予定だったけど自分が解いていることを知って急遽SHISUCON2日前に問題変更したらしい。ごめんなさい

*2:と言いつつも競技終了5分前に「とはいえSELECT *くらいは消せるのでは?」と修正してしまいエラーがめちゃくちゃ出て死ぬかと思った。触っちゃダメ絶対