純規の暇人趣味ブログ

首を突っ込んで足を洗う

正しく圧縮、素早く送信、mod_deflateの設定方法

      2017/03/05    HimaJyun

このWeb鯖では、帯域の削減とページ表示を高速化する目的でgzip圧縮して転送しています。
ただ、「Apache deflate 設定」とかで調べると「ん?、それ必要なの?/間違ってない?」と思う様な設定例が沢山ヒットします。

「僕の設定が完璧だ!!」とは言いませんが、参考にして頂ければと思います。

圧縮して転送すると?

圧縮して転送するのはページの表示速度を向上する目的でもほぼ全方位からおススメされている方法です。
圧縮にCPUリソースを利用しますが、転送に掛かる時間が軽減される事を考えるとむしろプラスです。
(転送中はその負荷の有無を問わず転送完了までCPU(プロセス/スレッド)を専有します。)

この時代にmod_deflateが使えない鯖の方が少ないと思うので、出来る事ならばやっておいた方が良いでしょう。

良くある間違った例

以下の様な例が良くヒットしますが、間違ってますよ。

全部圧縮してる

<IfModule mod_deflate.c>
 <IfModule mod_filter.c>
  SetOutputFilter DEFLATE
  BrowserMatch ^Mozilla/4 gzip-only-text/html
  BrowserMatch ^Mozilla/4\.0[678] no-gzip
  BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
  AddOutputFilterByType DEFLATE なんとか/かんとか
  AddOutputFilterByType DEFLATE かくかく/しかじか
 </IfModule>
</IfModule>

「え?、これ俺じゃね?」、はいそうです、あなたです。
やめてください、(鯖が)死んでしまいます。

何が間違ってるかと言うと、「SetOutputFilter DEFLATE」←これ
これを設定すると全てのコンテンツが圧縮されます。

つまり、後ろにある「AddOutputFilterByType」は無駄!!
imageだろうがzipだろうがなんでもかんでも全部圧縮しちゃいます。

ただしこっちは正しい

以下のパターンはとても多いですね、このやり方は正しいです。

<IfModule mod_deflate.c>
 <IfModule mod_filter.c>
  SetOutputFilter DEFLATE
  BrowserMatch ^Mozilla/4 gzip-only-text/html
  BrowserMatch ^Mozilla/4\.0[678] no-gzip
  BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
  SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
 </IfModule>
</IfModule>

全部圧縮する設定にした後、画像を圧縮の対象外にしているので大丈夫です。

application/x-httpd-php……w;;

稀に圧縮の設定に「application/x-httpd-php」を含めている例を見掛けます。

しかし、コンテンツが圧縮のフィルタを通るのは「最終的な出力」に対してです。

すなわち、PHPなどで処理した結果に対してフィルタが適用されるのです。

そして、PHPはデフォルトで「text/html」として結果を出力します、「application/x-httpd-php」の指定は何の意味も持ちません。

AddOutputFilterByTypeは古い

先程の例でも上げた様に、MIMEを指定して圧縮するには「AddOutputFilterByType」を利用するのですが、この「AddOutputFilterByType」はApache2.1以降で非推奨(deprecated)になり、2.3.7からはcore機能から「mod_filter」の機能へと移行されました。

なので「mod_filter+AddOutputFilterByType」を使えば良いのですが、どうせmod_filterを使うのですから、「FilterProvider」を利用した方が良いと思います。

FilterDeclare Compression CONTENT_SET
FilterProvider Compression DEFLATE Content-Type $text/html
FilterProvider Compression DEFLATE Content-Type $mime/type
FilterChain Compression

と言った設定がヒットします。
ピュアな心で何も疑う事無く、心ウキウキニューヨークで設定して再起動してみると

AH00526: Syntax error on line 5 of /etc/apache2/mods-enabled/deflate.conf:
FilterProvider takes three arguments, filter-name provider-name match-expression
Action 'configtest' failed.

と言う訳で再度調べ直すとGitHubでそれっぽい記事を発見、

どうやら、2.2→2.4の過程で構文が変わった模様……

「FilterProvider COMPRESS DEFLATE resp=Content-Type $text/html」ではなく
「FilterProvider COMPRESS DEFLATE "%{CONTENT_TYPE} = 'text/html'"」

コッチの方が新しい設定方法なんだ!!

この鯖での設定例

このサーバでは以下の様に設定してみました。

<IfModule mod_deflate.c>
 DeflateCompressionLevel 1
 <IfModule mod_filter.c>
  FilterDeclare COMPRESS
  FilterProvider COMPRESS DEFLATE "%{CONTENT_TYPE} =~ m#^text\/#i"
  FilterProvider COMPRESS DEFLATE "%{CONTENT_TYPE} =~ m#^application\/(atom\+xml|javascript|json|rss\+xml|xml|xhtml\+xml)#i"
  FilterProvider COMPRESS DEFLATE "%{CONTENT_TYPE} =~ m#^image\/(svg\+xml|vnd\.microsoft\.icon)#i"
  FilterChain COMPRESS
  FilterProtocol COMPRESS DEFLATE change=yes;byteranges=no
# BrowserMatch ^Mozilla/4 gzip-only-text/html
# BrowserMatch ^Mozilla/4\.0[678] no-gzip
# BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
# Header append Vary Accept-Encoding env=!dont-vary
 </IfModule>
</IfModule>

コメントアウトの部分は必要ないですが、なぜ排除したのかも説明したいので記載しています。
1つずつ解説して行きますね。

圧縮レベルは1で充分

「DeflateCompressionLevel 1」←これは圧縮レベルを1で圧縮する事を意味します。
ちなみに、(環境にも依りますが)僕の場合はこの記載が無い時は6で圧縮されていました

圧縮レベルは1~9の範囲で、数字が大きいほど高圧縮です。
「高圧縮の方が良いでしょ?」と思われがちですが、圧縮レベルを上げると当然ながらCPUの負荷が増えます。

でもって、圧縮レベルを1~9にした場合でもそう大差ありません。
せいぜい数%ほど変わるかな?、くらいです。

実際に1~9まで試してみました。
計測対象はこのブログのトップページで、「33101 byte」です。

レベル 圧縮後 削減 圧縮率
1 10374 byte 22727 byte 68.6%
2 10102 byte 22999 byte 69.4%
3 9963 byte 23138 byte 69.9%
4 9459 byte 23642 byte 71.4%
5 9330 byte 23771 byte 71.8%
6 9272 byte 23829 byte 71.9%
7 9272 byte 23829 byte 71.9%
8 9261 byte 23840 byte 72.0%
9 9256 byte 23845 byte 72.0%

ご覧の通り、殆ど変わりませんね。
しかも、レベルが上がるほど圧縮率の伸びが悪くなって行ってます。

最低圧縮と最高圧縮の差も1118byteしかありません。
CPU負荷を上げて圧縮するくらいならば、画像の色数なり枚数なりを減らした方が良いです。

この様に、上げても殆ど無意味なため圧縮レベルは1、欲張ってもせいぜい2か3まででしょう。

圧縮するMIMEタイプを指定

当初は良くある例で上げたような全圧縮設定にしてから、拡張子指定で画像を圧縮の対象から除外する設定を利用していたのですが、PHPで画像を動的に生産するとその中身が何だろうが勝手に圧縮しやがりました。

それでは困ると言うか、二度手間なのでMIMEタイプ指定で圧縮する方法を選びました。

 
FilterDeclare COMPRESS
FilterProvider COMPRESS DEFLATE "%{CONTENT_TYPE} =~ m#^text\/#i"
FilterProvider COMPRESS DEFLATE "%{CONTENT_TYPE} =~ m#^application\/(atom\+xml|javascript|json|rss\+xml|xml|xhtml\+xml)#i"
FilterProvider COMPRESS DEFLATE "%{CONTENT_TYPE} =~ m#^image\/(svg\+xml|vnd\.microsoft\.icon)#i"
FilterChain COMPRESS
FilterProtocol COMPRESS DEFLATE change=yes;byteranges=no

元の記事では.icoが「image/x-icon」でしたが、この鯖では「image/vnd.microsoft.icon」なのでそれに合わせて書き換えました。
あと、見るからに正規表現なので一部の文字をエスケープしています。

また、「text\/(html|css|plain|xml)」」を「text\/」に変更しています。
こうする事で、MIMEタイプが「text\/」から始まる物(HTML,CSS……すなわち平文のコンテンツ)は全て圧縮対象になります。

元記事では「FilterProtocol COMPRESS DEFLATE change=yes;byteranges=no」と言った指定があるのですが、この指定を(最低でもchangeは)入れないと静的コンテンツなのにETagやキャッシュが効かなかったりします。
せっかく304でキャッシュが有効なのに効かないからと毎回gzipして転送していたのでは本末転倒ですからね。

拡張子指定で除外する方法とMIME指定、どちらを使うかは完全に好みなのでお好きな方を採用して下さい、MIMEに依存しない分、拡張子指定で除外する方が楽です。

一部の画像は圧縮出来る

「画像だから圧縮しない」と言う考え方は短絡的で好ましくありません。
画像でも圧縮出来る物もあるので良く考えてから設定しましょう。

SVG、確かに画像ですが中身はXMLです、圧縮できます。
ICO、確かに画像ですが、圧縮すると1/10くらいのサイズになります。

今回は含んでいませんが、bmp画像は基本的に無圧縮なので圧縮出来ます。

GZIPの解凍に問題を抱えるブラウザは無視

BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html

これはgzipの解凍に問題を抱えるブラウザは圧縮せずに転送する設定ですが、こんな平安時代に和歌を詠みあげてた様なブラウザを未だに使っている人間は居ません。(人間以外なら(悪質なbotとかなら)居るかもね。)

仮に居たとしても、CSS3やHTML5が主流、HTTP/1.1が大前提でVirtualHostを使う昨今のWebサイトではもはや表示する事すら碌に出来ないでしょう。

個人的には、これらの設定は省いてしまって構わないと思います。
設定してたって無駄なマッチングが発生するだけですしね。

Varyは自動的に付与される?

「Header append Vary Accept-Encoding env=!dont-vary」

これは、プロキシなどに対してgzipが解凍出来ないブラウザに圧縮されたキャッシュを送信しない様にするための設定ですが、きょうびgzip解凍出来ないブラウザの方が少ないのでは?

さて、「Varyヘッダ」は自動的に付与されるとどこかで見かけたのでtelnetで試してみました
まずはgzip対応ブラウザ

GET / HTTP/1.1
Host: jyn.jp
Accept-Encoding: gzip, deflate

HTTP/1.1 200 OK
Server: Apache
Vary: Accept-Encoding
Content-Encoding: gzip
Transfer-Encoding: chunked
Content-Type: text/html

うむ、「Vary: Accept-Encoding」が追加されてますね。

次に、gzip非対応ブラウザ

GET / HTTP/1.1
Host: jyn.jp

HTTP/1.1 200 OK
Server: Apache
Vary: Accept-Encoding
Transfer-Encoding: chunked
Content-Type: text/html

あ……あれ?、どっち道出力されている?
気になったので大手サイトをいくつか調べてみました。

  • Google、Twitter->要求ヘッダの「Accept-Encoding」の有無を問わず「Vary」ヘッダ無し
  • Yahoo!、Facebook->要求ヘッダの「Accept-Encoding」の有無を問わず「Vary: Accept-Encoding」
  • Amazon->要求ヘッダの「Accept-Encoding」の有無を問わず「Vary: Accept-Encoding,User-Agent」

どのサイトもユーザ側の要求ヘッダの「Accept-Encoding」の有無を問わず同じ値を返してきます。
ちなみにですが、Apacheの公式サイトもあろうがなかろうがヘッダ無しでした。

つまり、設定しなくても良いと思います。

.htaccessでの注意点

僕はサーバのアドミンです、ルートです、スーパーユーザです、confを直接触れます。
しかし、世の中には共有サーバなるモノがあって、.htaccessで設定している方も居るかも知れないので、注意点をいくつか

  1. DeflateCompressionLevelは.htaccessでは使えない
    文字通りです、DeflateCompressionLevelを含んだ.htaccessを有効にすると500 Internal Server Errorになります。
  2. CGI版PHPにはdeflate圧縮できない?
    CGI版PHPの出力を.htaccessで設定したdeflateで圧縮する様に設定しても圧縮が効きません。
    その場合はphp.iniのzlib.output_compressionをOnにすると良いでしょう。
    (.confに直接記載している場合は圧縮が効きます、.htaccessの場合だけ効きません)

設定が終わったら必ず確認を、

「設定終わった、はい終了!!」は愚か者のする事です。
設定が終わったら必ず意図した通りに動くか確認しましょう。

以下の様なツールを使えば簡単に確認できます。
https://www.port80software.com/support/p80tools

一応、確認しながら記事を書きましたが、何か間違ってたら教えて下さいね。

 - サーバ運営