Note

3年後の自分のために書いています

ISUCON 過去問やってみた話

社内勉強会のコピペ。


結論

ISUCON の過去問やって自分の無力さを思い知った話です

:@daido1976: @daido1976

  • 2018/04〜 :feed: Feedforce Inc.
  • パフォーマンスチューニングできるようになりたい

ISUCON とは

Iikanjini Speed Up Contest

See. https://www.google.com/search?q=isucon+%E3%81%A8%E3%81%AF

アプリケーションの仕様は変えずに Web アプリケーションのパフォーマンスをチューニングするコンテスト

パフォーマンスチューニング?

サーバ側なら、

  • n+1 クエリの解決
  • DB にインデックス貼る
  • キャッシュとか???

知識としてはふんわり知ってる。実際にやれと言われて一人でできるのか?またそのチューニングの効果を実際に体験したことはあるか?

今回のルール

リポジトリ

https://github.com/daido1976/isucon7-qualify

環境構築

  • Docker で一発のはずなのに、ベンチマークが動かず 1 日ハマる
  • やたらとベンチマークのコードを読むも分からず
  • 結論、DB に初期データが入ってなかった :innocent:

初回ベンチ

スコア 4000 前後

チューニング開始まで

  • 自分の書いたコードに差し替えて開発できるようになるまでに半日ハマる(https://github.com/isucon/isucon7-qualify をクローンするような Dockerfile になってる)
  • ほとんど Docker 力のなさによるもの

Ruby アップグレード

スコア変わらず

Ruby 2.4.2 -> 2.6.6(2.7.2 まで上げると bundler のバージョンでエラーが出たのでやめた、多分 Dockerfile の中で bundler をインストールしなおせば解決するやつ)

nginx に Cache-Control public; を追加

:github: Add Cache-Control public to nginx.conf

ここからがスコアを上げるための施策(チューニング開始)

スコア変わらず

これってローカル Docker で動かすベンチマーク(Go の HTTP クライアント)でも効くのか?CDN なら物理的な近さで早いの分かるけどローカルなら意味ない?

DB にバイナリで突っ込まれている icon 画像をファイルに書き出して nginx で配信するようにした

スコア4946 まで上がった、でも安定しない、これもローカルではあんまり効かないとか? そういえば画像をバイナリで DB に突っ込むのやめましょうってよくいうけどなんで?容量を取られるの分かるけど、ストレージも RDB でもバイナリの読み書きするなら速度的には変わらないのでは???

n+1 の解決(GET /message

:github: Resolve N+1 in GET /message

GET /fetch の sleep 消した

:github: Remove sleep in GET /fetch

スコア 18056、ここでグッと上がった。

n+1 の解決(GET /history/:channel_id

スコア 19357、GET /message とほぼ同じ。無論共通化などしている余裕はない。

:github: Resolve N+1 in GET /history/:channel_id

message テーブルの channel_id にインデックス追加

スコア 37783、グッと上がった。何度かやるとスコアは激しく(1 万点ぐらい)上下する、原因が分からない…

:github: message テーブルにインデックス貼る

結果

ここまでやって 100 位ぐらい

See. https://isucon.net/archives/50961437.html

次回やりたいこと

  • ボトルネックを見つけるのが一番重要なので次回はそこから(プロファイリングツールなど使って)
  • isucon9 が良問らしいのでやりたい
  • Go でやる(参加者のほとんどが Go なので情報が多いし、サンプル実装も良いらしい)

関連記事

参考になったブログ

Rails の API モードで CSRF 対策機能を実装する

半年前に会社の技術ブログの下書きに雑に書いてて公開しどきを忘れたやつ、もったいないのでこちらで公開。


RailsAPI モードはデフォルトでは CSRF 対策機能が有効になってないので、クッキーによるセッション機能を使うと脆弱性を生む可能性がある。

Rails実装済みの CSRF 対策機能を API モードで使う方法を紹介します。

やること

RailsCSRF 対策機能を有効にする

  • ActionController::RequestForgeryProtection をインクルード
  • protect_from_forgery を有効にしたいコントローラに追加

クライアントに CSRF トークンを返す

セッション生成直後に { authenticity_token: form_authenticity_token } をクエリストリング or レスポンスとして渡す(#form_authenticity_token メソッドで rails が生成した CSRF トークンが取れる、ちなみにセッション内の CSRF トークンは session[:_csrf_token] で取れるが、どちらかは暗号化もしくはハッシュ化されてるので値は違うものになる)

クライアント側で POST する際に CSRF トークンを渡す

フォームから POST する場合

<input type="hidden" id="authenticity_token" name="authenticity_token"></input> を仕込んで js で getElementById とかして先ほどクエリストリング or レスポンスで渡された authenticity_token を value に突っ込む

Ajax や fetch の場合

ちなみに rails の ujs を使わずに ajax リクエスト送る時は X-CSRF-Token というヘッダに csrt-token を自前で含めないといけない

ujs では ここ でやってる。

クロスオリジンの検証を無効にする

このままだと request.base_uri と request.origin が違う(クロスオリジンになってる)と InvalidToken のエラーになってしまうので、 config.action_controller.forgery_protection_origin_check = false にする(これは Rails 4 までデフォルト false だったが Rails 5 以降は true になってる)

CSRF を有効にしたセッションのテストをする

ポイントは以下。

  • セッションを作成するエンドポイントを叩く
  • csrf の authenticity_token を params で渡す(これしないと session 取れない)InvalidToken が raise しなかったのが気になる
  • テスト環境の config.cache_store を null_store -> memory_store に(null_store は本当に cache_store を使わないことを意味する)
  • 👆 の cache の後片付けも忘れない

sinatraとrailsとrfcとmdnと徳丸本から学ぶセッションとクッキーと認証とCSRFとXSS

半年前の下書き、めっちゃ途中だけど埋もれるのも嫌なので公開。(あとから追記していく)


元々 csrf についての記事を書こうと思っていたが、csrf について調べるうちセッションとクッキーのことが気になり、rails のセッションとクッキーの実装方法が気になり、フォーム送信と Ajax リクエストって具体的にどう違うんだっけと気になり、Ajax リクエストと fetch って一緒だっけ?違うっけ?と気になり、セッションベースとトークンベースで認証やる場合のメリット・デメリットが気になり、今に至る。

web の技術は全て連関しているのだと思う、これを機に連関する部分を全て理解したい。

セッションとは

ステートレスである HTTP で状態を保持するための仕組み。

クッキーとは

セッションを実現するための仕組み。 サーバ側で Set-cookie して、ブラウザで保持、Cookie はブラウザからサーバ側に常に送信される。

サーバ側ではこの cookie に入った session id もしくは 暗号化された session 情報そのもの(user id や password など?)によってユーザを識別する。

session id のみの場合はなんらかの cache storage(メモリやredisやdynamodb)に session id を key に session 情報を保存しておく。

※ session 情報ってどこまで?passwordは入れてもOK?csrfトークンも入れる?

セッション & クッキー の sinatra での実装

enable :sessions

👆 で session が有効になる。

enable の定義は ここ、中で .set してるだけ。

詳細見てないけど .set されることで デフォルトでは false の sessions が true になるんだと思う。

いろいろあってデフォルトでは session_store として Rack::Session::Cookie が使われてる ことが分かる。

rack の session 周りのコードは https://github.com/rack/rack/tree/master/lib/rack/session にある。

#set_cookie とか定義されてる。ここでヘッダの Set-Cookie に値突っ込んでる。

セッション & クッキーの rails での実装

サーバのどこでキャッシュしてるかとかも書く、キャッシュストレージの違いとか

結論 sinatra と同じく Rack::Session が使われていた。

キャッシュストレージが Memcached 前提だけど以下は参考になる。

Rails の session を完全に理解した

フォーム送信と Ajax リクエストの違い

Ajax リクエストと fetch って一緒だっけ?違うっけ?

社内 esa に書いた

CORS

POST で content-type application/json の場合はプリフライトリクエストが飛ぶ。(その時はサーバ側で header とか method の allow が必要)

See. https://github.com/daido1976/sinatra-vanilla-spa/pull/4

jwt での認証方法

セッションベースとトークンベースで認証やる場合のメリット・デメリット

MPA と SPA の認証方法の違いも書く

jwt トークンはブラウザのどこに保存するのが良いか

そして csrfxss

vagrant ssh したときに裏側で何が起こっているか

結論

以下と同義。

$ ssh vagrant@127.0.0.1 -p 2222 -i ~/vagrant/ubuntu-xx/.vagrant/machines/default/virtualbox/private_key

$ vagrant ssh --debug すると最後の方に INFO ssh: Invoking SSH: ... から続くログが出るのでそれを確認すれば分かる。

補足

vagrant のコードリーディングもちょっとした。

上記のログを出してるのは以下。この lib/vagrant/util/ssh.rbssh 接続の実装の詳細が書かれてるファイルだろう。

https://github.com/hashicorp/vagrant/blob/v2.2.10/lib/vagrant/util/ssh.rb#L225

👆 の Util::SSH.exec が以下で呼ばれてる。

https://github.com/hashicorp/vagrant/blob/v2.2.10/lib/vagrant/action/builtin/ssh_run.rb#L73

👆 の SSHRun クラスを呼んでるのは以下。

https://github.com/hashicorp/vagrant/blob/v2.2.10/plugins/commands/ssh/command.rb#L54-L57

Go + VSCode でサブディレクトリに go.mod を置くと `could not import ... (no package for import ...)` になる

以下のようなやつ。

f:id:daido1976:20200917154706p:plain

環境

原因

gopls("go.useLanguageServer": true で有効)を使いながら、go.mod をサブディレクトリに置いて、プロジェクトのルートディレクトリから VSCode を開いてたのが原因だった。

解決策

gopls を使いたい場合

go.mod の置いてあるディレクトリまで移動してそこで VSCode を開く。

gopls を使わなくてもよい場合

そもそも "go.useLanguageServer": false にして gopls を使わないようにするという手もあった。(その場合は goimports などの command line tools が使われる、むしろ現在はこっちがデフォルト)

gopls は Go modules を正しく使用するプロジェクトのみを想定しているようだ。(公式の README にちゃんと書いてあった…)

https://github.com/golang/vscode-go#language-server

gopls is recommended for projects that use Go modules.

rails stats コマンドで app 以下のディレクリを全て計測できるようにする

$ rails stats コマンドはデフォルトでは以下のディレクトリしか計測しないため、サービスクラスを app/services 以下に置いているような場合、その分は計測されない。

app/controllers
app/helpers
app/jobs
app/models
app/mailers
app/mailboxes
app/channels
app/assets/javascripts
app/javascript
lib/
app/apis
+ test 以下のディレクトリ

See. rails/statistics.rake at 3d428777b05701ca7211a1be723cb1ee0e094bd9 · rails/rails · GitHub

ちなみに上記のようにデフォルトではテストコードの計測は test 以下のディレクトリを対象にしているが、rspec-rails が計測ディレクリ名を test -> specs に上書きしているので、specs 以下のテストコードが計測されるようになっている。

See. rspec-rails/rspec.rake at 13a8a01cc07c93db97b5f1cd57229527441cd626 · rspec/rspec-rails · GitHub

👆 の rspec-rails の上書きを参考にしながら以下のようなコードをプロジェクトの Rakefile に追加してやれば、app 以下のディレクトリ(view 以外)全て計測できるようになる。

# Rakefile
# ...

task stats: "custom:statsetup"

namespace :custom do
  task :statsetup do
    # Push target directories to STATS_DIRECTORIES
    app_dirs = Dir.children('./app').map { |child| "app/#{child}" }
    stats_types = Hash[app_dirs.map { |dir| [dir.split('/').last, dir] }]
    stats_types.each do |type, dir|
      next if type == 'views'
      name = type.capitalize
      ::STATS_DIRECTORIES << [name, dir] unless ::STATS_DIRECTORIES.map(&:first).include?(name)
    end
  end
end

その他

前述のように rspec-rails が計測ディレクリ名を上書きしているため、もし rspec-rails を Gemfile の group :test に置いていると、$ rails stats コマンド実行時にこの上書きがされず、テストコードの計測ができないので group :development, :test などに置いてやる必要がある。

参考

Hello, Computer Science 〜前編(ハードウェア編)〜

社内勉強会で発表した内容のコピペ。


今日のアジェンダ

  • 前置き(5分)
  • コンピュータの動く仕組みについて〜ハードウェア編〜(各5分ずつ)
    1. ブール論理
    2. ブール算術
    3. 順序回路
    4. 機械語
    5. コンピュータアーキテクチャ
  • 終わりに(3分)

今年の目標

「骨太なプログラマになる」

我々が日々開発しているアプリケーションは全てコンピュータの上で動いているので、まずは「コンピュータの動く仕組み」を知らなければならないと思った。

コンピュータシステムの理論と実装

一言でいうとコンピュータ作ってみようぜって本です。 本には仕様とヒントしか載ってなくて写経できないのが良いです。

コンピュータを理解するための最善の方法はゼロからコンピュータを作ることです。(裏表紙より)

See. 「コンピュータシステムの理論と実装」をやりきりました - Qiita

実は 1 年半前からずっと積んでました…。 英語版 は無料で読めます。

今日話す部分

本当は最後までやり切ってから発表したかったのですが、2ヶ月前から準備したにも関わらずバーチャルマシンの途中までしか実装が終わらなかったので、今日は1~5章のハードウェアの部分を中心に話します。

理解度2割ぐらいなので、間違ってたらドンドンご指摘ください。

1. ブール論理 〜Boolean Logic〜

  • あらゆるデジタル機器は回路からできている
  • 回路を論理的に表現するためのブール代数ではブール値(true/false, yes/no, 1/0, on/off などで表せる値、2値ともいう)を扱う
  • 物理的にはトランジスタ(スイッチ素子)によって回路のオンオフが切り替えられる
  • スイッチング技術によってコンピュータ開発者は物理的な要素に囚われず論理の世界(抽象化された世界)でハードウェアを扱うことができる
    • 現在はスイッチングに電気を使うのが一般的だが、磁石、光、バイオ、水圧、空気圧などオンオフが切り替えられるものであればなんでも良い
  • HDL(Hardware Description Language)は回路を設計するための言語
  • 論理ゲートは論理演算を行う回路のこと、HDL で表現できる

Nand ゲートだけ与えられた状態から、Not, Or, And, Xor, Mux, Dmux などのコンピュータの基礎となる論理ゲートを実装せよ

:github: https://github.com/daido1976/nand2tetris/pull/1

2. ブール算術 〜Boolean Arithmetic〜

  • 2進数とは 01 のやつ
  • 2進数の加算は最下位ビットから順に足していき、キャリービット(桁上がりビット)と次の桁の和を足す、最後のキャリービットが1であればオーバーフローとして桁が一つ多くなる
  • 符号付き2進数は +- を表現するやつ、4ビットなら -8~7 まで表現できる(符号なしの場合は0~15まで)
  • 符号付き2進数を表現するためには2の補数を表現するのが一般的
    • 正の数の最上位ビットは 0 で負の数の最上位ビットは 1
    • x から -x を得るには x のビットを全て反転させその結果にの最下位ビットに1を足せば良い
  • ALU は CPU の中心的役割を担う回路で、CPU でおこわなれる算術演算(四則演算)と論理演算は ALU で行われる

1 章で作成した論理ゲートを使って加算器(Adder)、インクリメンタ、ALU(算術論理演算器)を実装せよ(計算をできるようにせよ)

:github: https://github.com/daido1976/nand2tetris/pull/2

ALU マジムズイ :sob:

3. 順序回路 〜Sequential Logic〜

  • 1, 2 章で実装した回路は全て 組み合わせ回路 で、入力の組み合わせだけで出力が一意に決まるため計算はできるが状態を保つことができない
  • 過去の内部状態と現在の入力とで出力が決まる 順序回路 によって、状態を保持することができる
  • 記憶とは時間に依存する行為で、「 に記憶したものを 思い返す」という行為が本質である(なので回路上でも時間の経過を表現しなくてはならない)
  • フリップフロップ(data flip-flop, DFF) はクロック入力を絶えず受け取り、一つ前の入力値を出力する
  • レジスタ) とはデータを格納、呼び出しする記憶装置(本章だけでなく後ほど出てくるプロセッサ内のレジスタなど、結構いろんな文脈で出てくる)
  • メモリは複数のレジスタを積み重ねて実装される、どのレジスタへの読み書きなのかを表現するためにアドレスを用いる
  • プログラムカウンタとは次に実行すべき命令が格納されているメモリ上のアドレスを保存しているレジスタ

与えられた DFF と今まで実装した回路を使って、レジスタ、メモリ、プログラムカウンタを実装せよ(状態を保持できるようにせよ)

:github: https://github.com/daido1976/nand2tetris/pull/3

4. 機械語 〜Machine Language〜

  • 機械語(バイナリ)は 01 で表現するやつ、機械語を人間にも読みやすくしたのがアセンブリアセンブリ機械語は 1 対 1 で対応している)
    • コンピュータは機械語しか解釈できない
  • 機械語は仕様に決められた形式に従い、プロセッサ(CPU)とレジスタを用いてメモリを操作する
  • 一般的にアセンブリは算術演算、論理演算、メモリアクセス、分岐命令用のコマンドを持つ
    • ADD とか LOAD とか JNG とか(ここら辺のコマンド名は各コンピュータのアセンブリ言語の仕様に依存する)
  • Hack 機械語の仕様について(軽くで良さそう)

アセンブリで乗算(掛け算)プログラムと入出力操作プログラムを作ってみよう(アセンブリ機械語を理解せよ)

:github: https://github.com/daido1976/nand2tetris/pull/4

5. コンピュータアーキテクチャ 〜Computer Architecture〜

  • 我々の周りにある機械と比べて、デジタルコンピュータだけが持つ最も際立つ特徴は多様性である(同じハードウェアであっても様々なアプリケーションを起動することができる)
  • プログラム内蔵(Stored Program)方式 とはプログラムによる 命令データ と同じようにメモリに置いて実行するというもの
    • コンピュータサイエンスにおける最も深遠な発明の一つで、プログラム内蔵方式によりコンピュータは多様性を手に入れた(1930年より前にはハードウェアによる物理的な結線によるロジックの実装も存在していた)
    • ここ本文では超熱い文章なんですが、箇条書きにすると魅力が伝わりづらい
  • コンピュータのアーキテクチャとして超有名な ノイマン型アーキテクチャ ではプログラム内蔵方式をベースにしている(ていうかノイマン型以外のコンピュータアーキテクチャって何かあるんだろうか :thinking_face_google: )
  • ノイマンアーキテクチャでは CPU を中心としてメモリを操作し、入力デバイス(キーボードなど)からデータを受け取り出力デバイス(スクリーンなど)へデータを送信する
  • ノイマンアーキテクチャのメモリには データ命令 の 2 種類の情報が保存される
  • CPU の役割は現在読み込まれている 命令 を実行することで ALU、レジスタ、制御ユニットを用いて行う
    • レジスタは CPU の中に物理的に存在するためアクセスが高速、それに比べるとメモリへのアクセスには時間を要する
    • レジスタにはデータレジスタ(簡易・高速版メモリ的な)、アドレスレジスタ(メモリアクセス用)、プログラムカウンタレジスタなどがある
  • I/O(入出力)デバイス(キーボードやスクリーンなど)はメモリマップド I/O として扱う
    • 各 I/O デバイス専用のメモリ領域を確保し、I/O デバイスの入出力と対応させることで実現している(キーボードの A を押すと、メモリの 24566のアドレス(例)1 が書き込まれるなど)
    • メモリマップド I/O により、I/O デバイスを CPU にとって通常のメモリに見せかけることができる(これにより CPU の設計を I/O デバイスと完全に独立して行える!)
    • ここも本文は熱い文章だけど伝わらない…

メモリ(スクリーン、キーボード含む全体)、CPU、コンピュータを実装せよ(機械語を理解するコンピュータを構築せよ)

:github: https://github.com/daido1976/nand2tetris/pull/5

終わりに

  • どんな高水準言語で書かれたプログラムも最終的には 0 と 1 の機械語に変換されて実行される、というのを実感できた(まだ途中だけど)
  • プロセッサ(CPU)、レジスタ、メモリが何者なのか、どういう動作原理なのかがふんわり分かった
  • ソフトウェア編のアセンブラ、バーチャルマシン、コンパイラの実装には好きな言語を使って良いとのことだったので、Go で実装してみてます