sorbet は production で使えるのか [2020/03/29 時点]
sorbet は少し前に話題になっていた Ruby の type annotaion gem。
以下のようなコードを書くと静的型チェック、動的型チェックの両方をやってくれる。
# 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 とか使えばいい感じにできるんじゃないかと思っている。しらんけど
結論:自作すれば可能
社内 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 された!
他のサービスが sorbet に対応しているかどうかはちゃんと調べてない
結論:reviewdog を使えば可能
型の coverage のようなものを計測することはできるか
ここのサポートは手厚く、かなり細かい情報まで取得することができる。
Tracking Adoption with Metrics · Sorbet
しかも、導入フェーズに合わせてどの metrics に着目すべきかまで書いてくれているのでとても分かりやすい。
https://sorbet.org/docs/metrics#suggestions-for-driving-adoption
% 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 作るととても良さそう。
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 の型定義を何かから自動生成する何か
色々あった。
- proto file から rbi ファイルを生成してくれるやつ
GitHub - coinbase/protoc-gen-rbi: Protobuf compiler plugin that generates Sorbet .rbi "Ruby Interface" files. - YARD to rbi
GitHub - AaronC81/sord: Generate Sorbet RBIs from YARD documentation - rbi を spec file から tracepoint を使って自動生成するやつ、黒魔術が過ぎる
GitHub - camertron/gelauto: Automatically annotate your code with Sorbet type definitions.
ちょっと性質は違うけど sorbet 用の rubocop も見つけた。
GitHub - Shopify/rubocop-sorbet
まとめ
今からすぐに使える感じではなかったけど、社内 gem の sorbet-typed 問題さえ解決すれば手の届く範囲にあるなと思った。 前向きに検討したい。
-
その方針を取った理由は https://sorbet.org/docs/rbi#a-note-about-vendoring-rbis に書いてある↩
grpc_required_annotator gem つくった
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 を作ろうと思った。
できたのがこちら
この 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によって壊れてしまった話は元気があればまた書きます。
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 だけ返すパターン
- 更新したリソース全体を返すパターン
- 更新したリソースだけを返すパターン
- 関連する情報を含む更新したリソースだけを返すパターン
- 更新したリソースのうちクライアント側が必要な情報だけを返すパターン
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を共通化するパターン
サーバに対してクライアントが複数存在しユースケースがたくさんある場合、こっちの方が手早く開発できる印象がある。 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を共有しているサービスで以下のようなテストを書いていた。
- Rails 2 がレコードを作成する
- Rails 2 がRails1に対して作成したレコードのidを渡しAPI callをする
- Rails 1 が受け取ったidのレコードを取得し、中身を書き換える
- 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の内容
SHISUCONの意義についてはこれがわかりやすい
今回の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から優勝の座を奪えてよかった✌️
戦略
まず、自分も@spring1018もインフラエンジニアではないため制限時間内でのmiddlewareの完璧なチューニングは難しいだろうと考えました。 そこで 「競技時間中はインフラのことをなるべく考えずにアプリケーションに集中する、そのために80%のクオリティのチューニングはコピペで終わるような秘伝のタレを作っておく」 と話し合って決めました。
事前準備
過去問を解く
@spring1018は初ISUCONだったということもあり、問題形式に慣れることを目的にISUCON7予選を本選出場ラインのscoreが出るまで解きました。
(ネットワーク帯域の再現はしていないので正確ではない & 本選出場のscore届くまでに2日かかった)
自分はISUCONは参加したことがあったのですが、アプリケーションしか触ってこなかったのでインフラ周りの知識が皆無でした。
そのため個人的にISUCON6予選、ISHOCON2*1を解いてインフラ周りのハマりポイントに一通りハマっておくよう心がけました。
wikiに諸々の情報をまとめる
Home · wantedly/shisucon2019-teppei-haruki Wiki · GitHub
競技開始前からリポジトリを作っておき、wikiを見ればほとんども問題が解決することを目指し充実させました。
普通に便利なのでもうちょっとちゃんとまとめてブログの記事にしたい.
作業フェーズをリマインダーに設定しておく
人間は焦ってしまうと予定を忘れる傾向にあるので複数台使うかどうかの最終判断/再起動試験チェックなど忘れたら困るやつは予めリマインダーに設定しておきました。 これで時間のことを気にせず安心して改善に取り組めました。 作業フェーズもwikiに書いていた。
本番(時系列)
自分のやったことを時系列で書いていく、ペアの@spring1018がやったことは↓に記事のリンクが貼られるはず
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 /hashtag
とGET /search
と同じくらいになった。
なので次はGET /
のN+1改善とdef search
で実行しているクエリを絞るのをやっていこうという話になった。
↓は申し訳ねえ申し訳ねえと呻きながらbinding.pry
しているときに送られてきた自分の部署のリーダーからのDM。絶対に許さない。
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点を叩き出していて悔しかったのでもう少し頑張ることにした。
- きちんと3台使う 24951→30044(DB1台+App1台)→37984(DB1台→App2台)
more server by euglena1215 · Pull Request #23 · wantedly/shisucon2019-teppei-haruki · GitHub GET /
のN+1改善のときに作ったuserのキャッシュを他のところでも使うよう変更した 37984→41251
Use user_id_to_name by euglena1215 · Pull Request #21 · wantedly/shisucon2019-teppei-haruki · GitHub- いろいろ修正してみた 41251→47076
Awesome refact by euglena1215 · Pull Request #25 · wantedly/shisucon2019-teppei-haruki · GitHub - mysqlの秘伝のタレを入れた 47076→50669
mysql hiden inject by euglena1215 · Pull Request #19 · wantedly/shisucon2019-teppei-haruki · GitHub - unicornのworker_processを増やした 変化なし
moremore worker process by euglena1215 · Pull Request #26 · wantedly/shisucon2019-teppei-haruki · GitHub - unicornからpumaにした 変化なし
Use puma by euglena1215 · Pull Request #29 · wantedly/shisucon2019-teppei-haruki · GitHub - userのキャッシュをインスタンス変数から
Thread.current
に持ち替えた 50669→59748
use thread current by euglena1215 · Pull Request #33 · wantedly/shisucon2019-teppei-haruki · GitHub - 事前にhtmlifyするようにした 59748→61137
Pre htmlify by euglena1215 · Pull Request #34 · wantedly/shisucon2019-teppei-haruki · GitHub - nginxのkeepaliveを設定 61137→65473
add keepalive by euglena1215 · Pull Request #24 · wantedly/shisucon2019-teppei-haruki · GitHub
最終的に65000点で終了した