huguma’s blog (仮)

IT技術関連中心の備忘録

Apacheサーバのgzip圧縮最適化: 3. 事前圧縮による最適化

  1. 確認方法
  2. 設定方法
  3. 事前圧縮による最適化
  4. 補足: ファイルのgzip圧縮(初級編)

はじめに

前回説明したmod_deflateを使う方法ではリクエストがある毎にサーバ側でgzip圧縮処理を行いますが、これはサーバ側CPUの負荷を発生します。そこでファイルを事前圧縮しておき、それを直接返すようにすればサーバ負荷を大幅に改善できます。

ファイルのgzip圧縮

まず圧縮効果があるタイプのファイルをgzip圧縮し、元ファイルと同じディレクトリに拡張子.gzで保存します。方法は2通りあります。

  • 元ファイル(例:index.html)を残し圧縮ファイル(index.html.gz)を生成
  • 元ファイルを消去して圧縮ファイルに変換する

具体的な方法は 補足: ファイルのgzip圧縮(初級編) をご覧下さい。

元ファイルを残すかどうか

今のブラウザは全てgzip圧縮対応ですから、実はブラウザからアクセスする場合は元ファイルは使われません。元ファイルが必要なのはcurlなどのコマンドツールRuby/Pythonなどのプログラムコードからアクセスするようなケースだけです。

さらに元ファイルを消去してしまい、非圧縮ファイルを要求された場合はサーバ側で圧縮ファイルを展開処理して返すように設定することも可能です。この方法はファイル数が増えず、さらにディスク容量を大幅に削減する大きなメリットがあります。

非圧縮ファイルを要求された場合に展開処理を行うためわずかなサーバ負荷が発生します。しかし展開処理は圧縮よりはるかにサーバ負荷が少なく、さらに非圧縮で返す要求自体が今ではほとんどありません。

以下両方の場合の設定方法を説明します。バージョンはApache 2.4を対象とします。

元ファイルを残す場合

最小限に単純化した文例を使って説明します。ここではhtml, js, cssの3つの拡張子だけを対象としていますが、きちんと理解すれば自分でいくらでも拡張できます。

RewriteEngine on

RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteCond %{REQUEST_URI} \.(?:html|js|css)$
RewriteCond %{REQUEST_FILENAME}.gz -s
RewriteRule .* %{REQUEST_URI}.gz [L]

<FilesMatch "\.html\.gz$">
  ForceType text/html
</FilesMatch>
<FilesMatch "\.js\.gz$">
  ForceType text/javascript
</FilesMatch>
<FilesMatch "\.css\.gz$">
  ForceType text/css
</FilesMatch>

全体の流れを示します。Apacheサーバは次のように応答します。

  • 次の条件を全て満たす場合だけ対応(それ以外は通常処理)
    • リクエストヘッダがgzip圧縮を受け付ける
    • .gz生成時に指定した拡張子にマッチする
    • 事前圧縮した.gzファイルが存在する
  • 以上を全て満たす場合は次の通り応答
    • URLの末尾に.gzを追加(bodyとして事前圧縮した.gzファイルを返す)
    • ファイルタイプを強制的に.gzなしのタイプに設定する

Apacheではこれをmod_rewriteモジュールを利用して設定します。

http://httpd.apache.org/docs/current/ja/mod/mod_rewrite.html

mod_rewriteを有効にする

初期状態ではmod_rewriteの機能はoffになっています。まずこれを次の一行でonにします。

http://httpd.apache.org/docs/current/ja/mod/mod_rewrite.html#rewriteengine

RewriteEngine on

書き換え条件の設定

次にレスポンスヘッダ書き換え処理を行う条件をRewriteCondディレクティブで設定しますが、書式はとても難解です。全部読んでいるとそれだけで日が暮れそうなので要点を順番に説明します。

http://httpd.apache.org/docs/current/ja/mod/mod_rewrite.html#rewritecond

まず一行の書式は次の通りで、TestStringの部分が条件を設定する対象、CondPatternがマッチさせる条件(通常は正規表現)です。複数RewriteCond行はand結合し、全て満たす場合が書き換え対象となります。

RewriteCond TestString CondPattern

今回はTestStringとしてリファレンスに書かれているServer-variablesを設定します。これらは%{NAME_OF_VARIABLE}の形式で記述します。リファレンスにはNAME_OF_VARIABLEの一覧(四角で囲まれた部分)がありますが、これだけではありません。

リファレンスのずっと先の部分(箇条書きの4)に%{HTTP:header}という書式があり、headerにはリクエストヘッダの任意の項目を指定できます。これを最初に用い、Accept-Encodingの値にgzipが含まれているか判定します。

RewriteCond %{HTTP:Accept-Encoding} gzip

次のREQUEST_URIはURLから(もしあれば)クエリ部分を除いたものです。これで拡張子を判定します。

RewriteCond %{REQUEST_URI} \.(?:html|js|css)$

(参考) クエリはQUERY_STRINGで取得できます。

次のREQUEST_FILENAMEはURLに対応するローカル側ファイルシステムのフルパスで、これに.gzを付けて事前圧縮したファイルが存在するかチェックします。判定は(正規表現は使えないので)-sフラグ(通常ファイルなら真)を使います。

RewriteCond %{REQUEST_FILENAME}.gz -s

書き換え処理の設定

以上の条件を全て満たす場合のレスポンスヘッダ書き換え処理をRewriteRuleディレクティブで設定します(こちらもリファレンスは複雑怪奇です)。

http://httpd.apache.org/docs/current/ja/mod/mod_rewrite.html#rewriterule

書式は次の通りです。

RewriteRule Pattern Substitution [flags]

各項の意味は次の通りで、ここでのURL-pathはURLからスキームとホスト名を除いたものです(http://example.com/path/file.htmlの場合は/path/file.html)。

  • Pattern: URL-pathに対する正規表現パターン
  • Substitution: URL-pathを置換する文字列
  • flags(optional): 各種設定と制御(後述)

今回は次のように設定します(意味は後で説明します)。

RewriteRule .* %{REQUEST_URI}.gz [L]

まず適用条件はすでにRewriteCondで全て処理済みですから、 Patternには.*(全てにマッチ)を指定します。

次のSubstitutionでURLをどう置換するかを記述します。ここではRewriteCondで用いたServer-variablesの書式%{REQUEST_URI}でURLを参照し、その最後に.gzを追加します。

最後のフラグは[L]のように角括弧の中に書きます。[L][last]の略で、置換処理をこれで終了することを意味します。

複数の置換処理を組み合わせることも可能です。デフォルト(フラグなしは)次のルールを(無条件で)評価します。条件のand結合は[C](chain)です。最後に[L]を指定して終了を認識させます。

ファイルタイプの書き換え

以上で圧縮ファイルが存在する場合はパスに.gzを追加して事前圧縮ファイルを返すところまではできました。しかしこのままではファイルタイプ(Content-Type)が.gz(application/gzipまたはapplication/x-gzip)に設定されます。

最後にファイルタイプを書き換える処理が必要です。まずFilesMatchディレクティブで書き換えが必要なファイル拡張子を検出します。

http://httpd.apache.org/docs/current/ja/mod/core.html#filesmatch

そして検出した拡張子別にファイルタイプをForceTypeで強制的に付け替えます。

http://httpd.apache.org/docs/current/ja/mod/core.html#forcetype

以上まとめると次のようになります(htmlの場合)。拡張子別にファイルタイプが異なりますから、他の拡張子も同じ要領で記述して対応します。

<FilesMatch "\.html\.gz$">
  ForceType text/html
</FilesMatch>

<If ...><Elseif ...><Else ...>を利用してひとつにまとめることも可能ですが、かえって分かりにくくなると思います。

元ファイルを残す場合は以上です。次は残さない場合の設定方法を説明します。

元ファイルを残さない場合

Apacheにはコンテントネゴシエーションのサポート機能があり、これを利用して非圧縮コンテンツを返すよう要求された場合にgzip圧縮ファイルを展開して返すことができます。

コンテントネゴシエーションとはリクエストヘッダを読み、それに最も適したコンテンツを提供する機能です。具体的にはAccept-から始まるヘッダ行の値を読み、そこから応答するページ(ファイル)を選択することができます。

http://httpd.apache.org/docs/current/ja/content-negotiation.html

(典型例として)Accept-Languageの値から応答する言語を切り替える場合はindex.html.enindex.html.jaのように拡張子の末尾に言語コードを付けたファイルを用意し、ヘッダの値により応答するファイルを選択します。

http://httpd.apache.org/docs/current/ja/content-negotiation.html#naming

このバリエーションとしてAccept-Encodingの判定機能があり、gzipを含む場合は拡張子の末尾が.gzのファイルを返すように設定できます。さらに非圧縮コンテンツを要求された場合は.gzファイルを探し、それを自動展開して返します。

設定例は次の通りです。

Options +MultiViews
MultiviewsMatch Any
FilterDeclare gzip CONTENT_SET
FilterProvider gzip inflate "%{req:Accept-Encoding} !~ /gzip/"
FilterChain gzip

<FilesMatch "\.html\.gz$">
  ForceType text/html
</FilesMatch>
<FilesMatch "\.txt\.gz$">
  ForceType text/html
</FilesMatch>
# 以下、他のファイルタイプも同様(略)

設定方法はtype-mapファイル(httpd.conf専用)とMultiViewsオプションの2通りありますが、ディレクトリ別の設定や.htaccessに記述する場合はMultiViewsを用います。MultiViewsは初期状態では無効に設定されているので、次で有効にします。

http://httpd.apache.org/docs/current/ja/mod/core.html#options

Options +MultiViews

次にMultiViewsを適用する拡張子を設定します。ここで設定しているAnyは無条件に全て受け付ける事を表します。

http://httpd.apache.org/docs/current/ja/mod/mod_mime.html#multiviewsmatch

MultiviewsMatch Any

設定にはmod_filterを用います。次のリファレンスのExamplesの中にEmulating mod_gzip with mod_deflateという今回の目的そのものの文例があります。

http://httpd.apache.org/docs/current/ja/mod/mod_filter.html

mod_gzipはバージョン1.X時代のモジュール名です(現在は代わりにmod_rewriteを用います)。

今回はこれをそのまま頂くことにします(詳細調査は略)。これでAccept-Encoding: gzipが設定されていない場合だけinflate処理が実行されます。

http://httpd.apache.org/docs/current/ja/mod/mod_filter.html#examples

FilterDeclare gzip CONTENT_SET
FilterProvider gzip inflate "%{req:Accept-Encoding} !~ /gzip/"
FilterChain gzip

なおFilterProviderの書式にはバリエーションがあります。次は関数のreqを使った例です(動作確認済み)。先頭の!とマッチ部の=~の組み合わせで「マッチしなければ」という条件になりますが、この短縮形が上で用いている!~です。

FilterProvider gzip inflate "! req('Accept-Encoding') =~ /gzip/"

(参考) 関数一覧 http://httpd.apache.org/docs/current/ja/expr.html#functions

(次も参考) Apache 2.2以前は書式が異なります(ただし私は動作確認していません)。

http://stackoverflow.com/questions/26895894/get-apache-to-auto-decompress-a-file-but-only-if-needed

FilterProvider gzip INFLATE req=Accept-Encoding !$gzip

リファレンスの記述は以上ですが、この状態で動作確認するとやはり圧縮対応時のContent-Typeが全てgzipの設定になります(元ファイルを残す場合の動作と同様)。そこでForceTypeで拡張子別にファイルタイプを設定します(以下は.htmlの設定例)。

<FilesMatch "\.html\.gz$">
  ForceType text/html
</FilesMatch>

運用例

この方法で天気データマップのサイト設定を行いました。サーバ内部の設定は(バックアップの意味も兼ねて)次のURLで公開しています。

実サイトの.htaccesshtaccess_exampleというファイル名にしています。またdata/hourly/内のファイルはサイズ、ファイル数とも膨大なので.tar.gzアーカイブで保存しています(バックアップ用)。

まとめ

Apacheをこんなに詳しく勉強したのは始めての経験でしたが、書式が非常に難解でかなり苦労しました。

私は(多少のサイト管理はできますが)基本的にはプログラマーで、どちらかと言えばAPI仕様を作る側に近い人間です。大昔に作られたソフトのAPIを長年機能拡張すると最後はここまで複雑になってしまうのかというのを思い知りました。

これは仕方ない事だと思います。あまりにもメジャーなソフトなので後方互換性を考慮した仕様変更しかできません。そのような足し算の変更が積み重なると必然的にこうなるのはよく分かります。

過去のしがらみがない後発サーバはもっとシンプルです。特にNginxは専用設計されたgzip最適化機能があり、設定方法も簡単です。次の解説を読めばそれだけで十分理解できます。

http://qiita.com/cubicdaiya/items/2763ba2240476ab1d9dd

しかしApacheは長年に渡る抜群の実績があります。まだまだ今後も使われるでしょうし、今でもApache.htaccessでがんばっているサイト管理者は多いと思います。本解説が皆さんのお役に立てたら幸いです。