Closed16

【Webパフォーマンスチューニング】抑えておきたい高速化手法メモ

まきぞうまきぞう

外部コマンドではなく、ライブラリを使用する

外部コマンドを使用すると、アプリケーションとは別のプロセスが起動され、起動コストやプロセスがメモリを消費してしまうので、避けるべき。

また、OSコマンドインジェクション脆弱性というセキュリティ上の欠陥を生み出してしまう可能性があるので、やめた方が良い。

まきぞうまきぞう

シェルを起動してコマンドを実行するってどういうこと?
そもそもシェルとは?

シェルっていうのはプログラム。
どんなプログラムなのかというと、OSと対話するためのインターフェースを提供してくれるプログラム。
CUIとGUIの2種類があって、それぞれ例として

  • CUI
    • Bash
    • Zsh
    • Fish
    • Power Shell
    • Command Prompt
  • GUI
    • Finder
    • エクスプローラー

がある。
finderもシェルだったんだ、驚き。

まきぞうまきぞう

じゃあ、うちらが毎日触っていたターミナルアプリとかwarpとかもシェルっていこうこと?

厳密には違う。
ターミナルは、ユーザーがシェルと対話するためのウィンドウを提供するプログラム。
テキスト入力や出力を行うための画面を提供している。ユーザーがコマンドを実行し、シェルからの出力を見ることを可能にしてくれる。

まきぞうまきぞう

じゃあ、ターミナル以外からシェルのコマンドを実行する方法はあるの?

ある。

  • スクリプトファイルを書いて、そのファイルを実行する
  • GUI(Finderとか)からファイル操作
  • プログラムからコマンドを実行
  • CI/CDパイプラインのワークフローにコマンドを設定
まきぞうまきぞう

ちなみに、Go言語に標準で搭載されているexec.Commandはデフォルトではシェルを経由せずにコマンドを実行できる。
これすごい。。。!

ちなみに、パイプやリダイレクトを使いたい場合、シェルを経由してコマンドを実行することもできる

package main

import (
    "fmt"
    "os/exec"
)

func main() {
    // シェル経由でコマンドを実行
    cmd := exec.Command("sh", "-c", "ls -l | grep go")

    // コマンドの標準出力を取得
    output, err := cmd.Output()
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    // 結果を表示
    fmt.Println(string(output))
}
まきぞうまきぞう

プログラムからの外部コマンド呼び出し時、OSへのシステムコール的には2つの命令が実行されている。

  • fork
  • exec

fork

元々OSにはプロセスを作る機能は提供されていなく、既存のプロセスから子プロセスを複製するforkという機能によってプロセスを作っている。

exec

実行するバイナリをメモリにロードし、プロセスのメモリの内容を書き換えてプログラム(コマンド)を実行する。

execコマンドのみを実行すると、そのプロセスのメモリが書き換えられてしまい、プログラム上の後続の処理を実行するためのプロセスがなくなってしまう。
そのため、forkして子プロセス上でexecを実行することで指定した外部コマンドを実行しつつプログラムの後続処理を実行することができる。

まきぞうまきぞう

Goのexec.Command関数はforkした後に子プロセスでexecシステムコールをシェルを経由せずに直接呼び出す。

まきぞうまきぞう

そもそもシェルを経由せずにコマンドを実行できるというのはどういう意味なんだい?
lsとかpwdのようなコマンドはシェルを経由せずとも実行できるんだとよ?

それは、lsやpwdがシェルの内部コマンドではなく、実行可能なプログラムであるからだ。
確認してみるとわかるはず。
lsコマンドとかpwdコマンドなんかは、直接実行可能なバイナリが用意されているよ。

$ ls /bin
[		cp		dd		expr		launchctl	mkdir		pwd		sh		tcsh		zsh
bash		csh		df		hostname	link		mv		realpath	sleep		test
cat		dash		echo		kill		ln		pax		rm		stty		unlink
chmod		date		ed		ksh		ls		ps		rmdir		sync		wait4path

一方、パイプとかリダイレクト(|とか>とか)はシェルが提供する内部コマンドだから、シェルを経由しないと実行できない。

まきぞうまきぞう

exec.Command関数は渡した文字列をそのまま文字列(*とか渡してもシェルを経由しないので展開されない)として解釈するので、OSコマンドインジェクションによって値が入れられても命令を解釈しないので大丈夫という理屈か。

まきぞうまきぞう

開発用の設定で冗長なログを出力しない

開発環境ではログを大量に出力することが多いかもだが、多い分ファイル書き込みなどが実行されるため、パフォーマンスに影響が出る可能性がある。

ISUCON11では本番環境のログの出力設定がデバッグモードになっていたりしたので、デバッグモードを無効にしたり、ログレベルを変更する必要があったんやな。

まきぞうまきぞう

静的ファイルの配信をリバースプロキシから直接行う

バックエンドサーバーを介さず、nginxから直接画像などの静的ファイルを配信すれば、パフォーマンスは大きく向上する可能性あり。

nginxのtry_filesという設定にファイルパスをセットすることで、順番にチェックしていき、ファイルがあればファイルの内容をレスポンスとして返し、ファイルがなければアプリケーションサーバーにリクエストを送ってくれる。
https://nginx.org/en/docs/http/ngx_http_core_module.html#try_files

まきぞうまきぞう

try_filesってhtmlとかも静的ファイルだからいけるのか。

  • 404ページ
  • メンテナンス中のページ
    そんなページのhtmlを返すって使い方もいけるのか!
まきぞうまきぞう

ヘッダを使った静的ファイルのキャッシュ

更新頻度が低く、同じコンテンツを何回も参照することがあるコンテンツは、Cache-Controlヘッダなるものを使って無駄な通信が発生しないWebサービスを構築できるらしい。

サーバーで配信しているファイルがブラウザなどのクライアントが保持しているコンテンツと同一のものかを判定するために「HTTP条件付きリクエスト」というものが存在しているらしい。

HTTP条件付きリクエストの動作

  1. 初回、もしくはキャッシュが存在しない場合、リクエストは通常通り送る。
    2. レスポンスとして、Last-Modified, ETagのどちらかもしくは両方が返ってくるので、ブラウザがその値を保存しておく
  2. キャッシュが期限切れをした後のリクエストでは、リクエストのヘッダーとしてIf-Modified-Since, If-None-Matchヘッダーを付与する
    4. If-Modified-Sinceヘッダーには保存しておいたLast-Modified, If-None-Matchヘッダーに保存しておいたETagヘッダーの内容をそれぞれ付与する
    5. コンテンツに変化がなければ、レスポンスとしてbodyは空、ステータスコード304 NOT MODIFIEDを返す
    6. コンテンツに変化があれば、レスポンスとして新しいコンテンツデータと更新されたLast-Modified, ETagヘッダーをそれぞれ返す

ここにおけるキャッシュをそれぞれのシステムがいつまで有効かを記述できるHTTPヘッダーがCache-Controlヘッダー。
例) Cache-Control: max-age=86400というヘッダーを返せば、86400s=24h=1dキャッシュが有効になる。

静的ファイルは基本的にコンテンツに変化がないので、1年以上の数値を指定しても特に問題はない。

まきぞうまきぞう

頻度が低かったとしても、Cache-Controlヘッダーでキャッシュを使用しているファイルが変更されたら、どうやって反映されれば良いの?

  • ファイル名を変える
  • クエリ文字列を用いて古いファイルが利用されないようにする
まきぞうまきぞう

クエリ文字列を用いて古いファイルが利用されないようにする

これってどういうこと?

ブラウザはクエリ文字列を含めたURLが一致しているかどうかでキャッシュを使えるか判定している。
クエリ文字列を使うことで、クライアント側のキャッシュが意図せず使われることを避けることができる。

つまり、クエリ文字列が変われば、キャッシュを無効化できるということ。
これを利用して、ブラウザから送られるURLのクエリ文字列をコンテンツ変更後に変えることで、キャッシュを無効化できる。

まきぞうまきぞう

CDN上でレスポンスをキャッシュする

HTTPレスポンス自体をキャッシュする方法もある。

CDNとは?

日本のデータセンター内からコンテンツを配信しているとする。
そのコンテンツの配信を突然大量に配信する必要が出たとします。
データセンターでは契約している接続帯域以上の通信をすることは基本的にできませんし、突然増やすことも難しいことがほとんど。

日本国外からのアクセスを考えてみる。
日本のデータセンターから国外への配信は、必ず海底ケーブルを通る。海底ケーブルを通れば、物理的な距離もあれば、国によっては海底ケーブルの陸上げ局の帯域が狭く、途中でパケットがロスしてしまい、パケットの再送が必要になることもある。
日本国外からのアクセスを考慮すれば、日本のデータセンターからコンテンツを配信するだけでは安定性、パフォーマンス面どちらも期待することは難しい。

CDN事業者は世界的に高品質なネットワークを保有しており、クライアントから近くにあるエッジサーバーも保有している。CDNを利用することでクライアントはまず近くにあるエッジサーバーと通信をし、その後はCDN事業者が保有する安定したネットワークを通ってコンテンツにアクセスできる。

このスクラップは2ヶ月前にクローズされました