huguma’s blog (仮)

IT技術関連中心の備忘録

Apacheサーバのgzip圧縮最適化: 1. 確認方法

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

はじめに

先日天気データマップのWebサイト再立ち上げ作業を行いました。従来はHerokuなどのクラウドサービスから公開していましたが、今回はApacheレンタルサーバを利用しました。

自分には昔懐かしいApacheサーバなのですぐ設定できるだろうとたかをくくっていたら、gzip圧縮の最適化で結構苦労しました。そこで自分への復習も兼ねて解説記事を書いてみようと思います。

HTTPプロトコルの圧縮エンコーディング

ブラウザからWebページへのアクセスにはHTTPプロトコルを用いますが、HTTPには圧縮エンコーディングを用いたアクセス最適化機能があります。ここでその基本メカニズムを確認しておきます。

(参考) HTTPの勉強用、とてもよい解説です。

とほほのWWW入門 - HTTP入門 http://www.tohoho-web.com/ex/http.htm

HTTPプロトコルではまずクライアント(ブラウザ)からサーバに文書やデータを要求するメッセージを送信し、サーバは要求メッセージを読み応答メッセージと要求された文書やデータを返します。

この際データをサーバ側で文書やデータをgzipなどで可逆圧縮処理して返す機能をサポートしています。特にHTMLなどのテキスト文書は1/4~1/5程度に圧縮でき、通信データを大幅に削減しアクセスを高速化できます。

HTTPプロトコルではこれを次のように処理します。まずブラウザは要求メッセージの先頭(リクエストヘッダ)部分に次のような一行を追加します。

Accept-Encoding: gzip, deflate

これは「このブラウザはgzipとdeflateの圧縮に対応しています」という意味で、現行ブラウザはほぼ全てこのようなヘッダ情報を付けてサーバにデータを要求しています。

これに対し、gzip圧縮に対応しているサーバは応答メッセージの先頭部(レスポンスヘッダ)に次の一行を加えます。

Content-Encoding: gzip

これはもちろん「応答はgzip圧縮して返します」という意味で、ヘッダの後にメッセージボディとしてgzip圧縮されたデータを返信します。これをブラウザが認識しボディをgzip展開して表示します。

圧縮エンコーディングを用いるかどうかはファイルタイプによって切り替えます。基本的に圧縮が効果的なタイプだけgzipを有効にし、すでに圧縮処理されているタイプは無駄な処理になるのを防止するため無効にします

確認方法

ブラウザがアクセスしたファイルに対して圧縮エンコーディングが用いられているかどうかはブラウザを使って調べることができます。ここではgoogle chromeデベロッパツールを使う場合を説明します。

他のブラウザは省略しますが、今のブラウザには大体このような開発者用ツールがあります(FirefoxFireBugなど)。

まず調べたいサイトにアクセスし、デベロッパツールを起動します(右上メニューから「その他のツール - デベロッパツール」またはCtrl+Shift+I)。表示されたら「Network」タブを選択します。

f:id:huguma:20151126194338p:plain

この状態で再読み込みボタンを押すとWebページでアクセスしている全ファイルの詳細情報を表示します。左端の列のファイルリストから調べたいファイルをクリックすると、そのファイルのHTTPアクセスに関する詳細情報を表示します。

f:id:huguma:20151126194433p:plain

「Headers」タブを選択すると送受信のヘッダ情報を表示します。

f:id:huguma:20151126194501p:plain

確認事項は次の3点です。

  1. ステータスコード
    • 200 OK は正常に応答し、ボディ(データ)を返したことを表します。
    • 再読み込みでは 304 Not Modified を返すことがあります。これは「前回アクセスしているのでその時のキャッシュを使ってね」という意味でボディは空です。この場合は再読み込みボタンを押し続け「ハード再読み込み」すれば200で応答します。
  2. リクエストヘッダにAccept-Encodingがあり、gzipが含まれていること
    • HTMLでは必ず設定されますが、別のファイルタイプでは設定されない可能性もあります。
  3. レスポンスヘッダにContent-Encoding: gzipがあるかどうか
    • これが存在していればgzip圧縮されています

curlを用いた確認方法

ブラウザではなくコマンドラインツールでも確認できます。wgetcurl(cURL)の2つが有名ですが、開発時確認用にはcurlが適しています。

curl-vオプションを使うと送受信時のリクエスト/レスポンスヘッダを確認できます。curlはボディをコンソール出力するため、必要に応じてリダイレクトを併用します。curlはデフォルトではAccept-Encodingを付けずにサーバにアクセスします。

$ curl -v http://example.com/index.html [> index.html]

-Hオプションを使えば必要なリクエストヘッダを追加できます。

$ curl -v -H "Accept-Encoding: gzip" http://example.com/index.html [> index.html]

続く ⇒ Apacheサーバのgzip圧縮最適化: 2. 設定方法

天気データマップ再開のお知らせ

昨年3月より天気データマップというWebアプリケーションを公開しています。昨年末より機器故障のためデータ更新を停止していましたが、このたび環境をもういちど整備して再開します。

従来はGitHub pagesより公開していましたが、今回はレンタルサーバを利用して公開します。新サイト(URL)は次の通りです。

http://higuma.boo.jp/tenki-data-map/

f:id:huguma:20151120125449p:plain

昨年まではサーバ容量の制約から全データの一部(2011年以降)しか公開できませんでしたが、今回は完全版です。気象庁の1882年から現在までの全気象情報を見ることができるようになりました。

アプリケーション(ソフトウエア)自体は今までと同じです(変更はありません)。操作方法等のドキュメントは今まで通りGitHubで公開しています。

https://github.com/higuma/tenki-data-map

今後は管理人(私)の時間が許す範囲で、原則として毎日データ更新作業を行います。昼までには昨日のデータが見られるようになると思います(通常は10-11時頃)。

都合により時間が取れない日もありますのでどうかご了承ください。

まだ旧Googleマップを使いたいのに...どうしたらいい?

試験公開時から「使いにくい」と批判の多かった(?)新バージョンのGoogle Mapですが、先日(2015/4)新バージョンへ完全切り替えとなりました。

新版は確かに機能豊富です(その分重くなったのはまあ我慢します)。しかし余計なポップアップがぽこぽこ操作を妨げるのが我慢できません(いらいらがもう限界...)。みんな言ってる事ですがGoogleさんどうにかならないんでしょうか?

でも昨日(2015/4/28)までは旧バージョンを利用する設定が使えたので不便はありませんでした(次を参照)。この?option=classicを使えば旧マップを利用できました。

http://d.hatena.ne.jp/casualstartup/20140825/google_maps_classic

しかし本日(4/29)からこれも使えなくなり、現在は?option=classicを指定すると新バージョンのライトモードに設定(リダイレクト)されるようになりました。

ライトモードの操作感は旧マップと比較してもそこそこ悪くないと思います。しかし(現時点の明らかな欠点として)縮尺スケールがないため距離感が分かりません。ただし今はリリース直後なので今後順次改良されるとは思います。

そこで自分に最低限必要なGoogleマップの機能を利用するための簡単なHTMLを作りました。

http://higuma.github.io/google-maps-plain-map/

これは単独ファイルで動作するスタティックWebアプリケーションです。上記リンクにアクセスしても動きますが、それよりhtmlをソースとして(index.html等の名前で)ローカル保存し、それをブラウザに読ませた方がより軽快に起動します。

(参考) GitHubリポジトリのURLは次の通りです。

https://github.com/higuma/google-maps-plain-map

機能はあえて最小限にしています。新マップのような豪華な機能はありませんが、その分さくさく動きます。

  • 基本機能(拡大・縮小や地図/衛星写真の切り替え等)はもちろんOK
  • ストリートビューも使える
  • Photo Sphereも表示可能(この点に関しては旧classic版より便利)
  • 右下に縮尺スケールを付けた(ちょっと小さいけど我慢してね...)
  • 旧classic版にあった写真表示機能などはなし

Google Maps JavaScript APIを使っています。腕に覚えのある人はぜひJavaScriptソースに手を加えて改良してみて下さい(コードはほんの数行です)。次のリファレンスを参照。

https://developers.google.com/maps/documentation/javascript/reference#Map

XMLHTTPRequestによるバイナリデータ取得(モダンブラウザ専用)

現在Web Audio APIを利用したアプリケーションを書いていますが、オーディオサンプルをロードするためにXMLHTTPRequest(以下XHR)を使って波形データをロードする処理が必要になったのでreminderをここに残しておきます。

XHRでテキストデータを受信する場合はjQueryjQuery.ajaxを使うのが簡単で互換性も確保できます。しかしバイナリデータの場合はXHRを直接使う必要があります。

jQueryは今のところバイナリデータを扱うような作りにはなっていません(jquery ajax バイナリデータなどと検索してみて下さい)。

バイナリファイルの受信方法は次のMozillaドキュメントに解説があります。

https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Handling_binary_data

Mozillaドキュメントの多くは和訳もあり、上記URLのen-USjaに変えれば和訳ドキュメントを見ることができます。しかし今この文章を書いている時点ではまだ作業中で半分も日本語になってない状態です。

設定方法は2種類あります。

  • .overrideMimeType("text/plain; charset=x-user-defined") (旧式)
  • .responseType = 'arraybuffer' (新式)

今回はWeb Audio APIを使うことが前提なので新方式を使います。パターンは次のようになります(Mozillaドキュメントより抜粋)。

var oReq = new XMLHttpRequest();
oReq.onload = function(e) {
  var arraybuffer = oReq.response; // not responseText
  /* ... */
}
oReq.open("GET", url, true);
oReq.responseType = "arraybuffer";
oReq.send();

全体の流れを確認します。

  • まずXHRオブジェクトを生成
  • 送信前に設定(順不同)
    • .onloadに応答時のハンドラを設定(詳しくは後で...)
    • .open(メソッド, URL, true) で相手先を設定
      • ちなみに最後がfalseだと結果が返るまでブロック(やってはいけない!)
    • .responseTypeを'arraybuffer'に設定(今回はこれが一番のポイント)
  • .send()で送信

responseType属性(送信用)とresponse属性(応答用)のリファレンスは次の通り。

今回必要な部分だけ簡単にまとめます。

  • 送信前に.responseTypeを'arraybuffer'にする
    • 応答時は.responseにArrayBufferオブジェクトを返す
    • 失敗時(404など)でも空のArrayBufferを返すことに注意

http://www.w3.org/TR/XMLHttpRequest/#arraybuffer-response-entity-body

応答処理用ハンドラはonloadに設定します。

  • .onload = function (event) { ... }

loadイベントの他にloadstart(開始時)、progress(処理中)、error(エラー発生)などがあります。全イベントの一覧は次の通りです。

http://www.w3.org/TR/XMLHttpRequest/#event-handlers

errorはサーバ応答がない場合などの処理用で、例えば404(not found)などは(サーバが応答しているので)loadで処理することに注意して下さい。

引数のeventはProgressEventで、progressイベント処理中に進行状況を取得するのに用います(それ以外にはあまり使い道なし)。

https://dvcs.w3.org/hg/progress/raw-file/tip/Overview.html#progressevent

  • .lengthComputable HTTPレスポンスヘッダにContent-Lengthがあればtrue
  • .loaded 実際に転送したサイズ
  • .total Content-Lengthがあればそのサイズ、なければ0

受信時の情報取得はXHRオブジェクトから行います。event.targetにXHRオブジェクトが渡されるのでそれを使っても同じです。

oReq.onload = function(event) {
  xhr = event.target;           // 外側のoReqと同じオブジェクト
  if (xhr.status == 200) {      // 成功時
    var arraybuffer = xhr.response;     // 受信したバイナリデータ
    // 以下成功時の処理(略)
  } else {
    // エラー処理の例(404の場合は"Cannot load (Not Found): URL"と表示)
    alert("Cannot load (" + xhr.statusText + "): " + xhr.responseURL);
}

GitHubの各種サイズ上限とファイル削除の方法

GitHubはかなり大きなファイルやプロジェクトも受け付けますが上限はあります。

GitHub Help - What is my disk quota?

GitHub Help - Working with large files

まとめると次の通りです。

  • リポジトリサイズには物理的な上限は設定していない
    • ただし1GB以下に収めるようにという要請(お願い?)がある
    • それを超えると注意(要請)メールが送られる場合あり
  • その中の単一ファイルには100MBまでという物理的上限がある
    • 50Mを超えるファイルがあると警告を出す
    • 100Mを超えるファイルは受け付けない

GitHubリポジトリサイズの確認方法

この「リポジトリサイズ」はローカルリポジトリのディスク消費量ではなく、たぶんGitHub側が実際に使用しているリソース消費量のことです。

GitHub内部の話なので詳細は分かりませんが、この解釈が一番自然だと思います。

これはGitHub APIを使って取得できます。方法は簡単で、次のURLにアクセスすればリポジトリ情報を取得できます(ブラウザを使うのが一番簡単です)。

  • https://api.github.com/repos/ユーザ名/リポジトリ名

リポジトリ情報はJSON形式で返されます。リソース使用量は"size"プロパティで、単位はkbyteです。

{
  ...
  "size": 3504,
  ...
}

この値が”1048576`(1024 x 1024)を超えたら要対策ということになります。

ただし総サイズが1Gを超えても注意メールが来るのは少し先の事でしょう。私は今まで何回か1Gを超えた事がありますが、割と早く気付いて削減対応したところ何も来ませんでした。

gitからファイルを完全消去するには

gitのリポジトリ履歴からファイルを完全に削除しなければならない状況が時々発生します。GitHubを利用している場合でいくつか例を上げてみます。

  • サイズが1Gを超えた(かなりの猶予期間があるはず、落ち着いて対応すればOK)
  • 単一ファイルが100Mを超えた(これは厳密に拒否されてしまう)
  • パスワードや恥ずかしい写真(?)を間違えて一緒にコミットした(すぐ抹殺せねば...)

無理せず作り直すのが簡単

もしそのリポジトリを自分ひとりで管理しており、作り直してもどこからも文句が来ないのであればそれが一番簡単です。

GitHub側のリポジトリは削除して同じ名前で作り直してもいいですが、それよりpush時に--forceを指定すればリモート側の履歴を無視して強制的に送信・書き換えができます(作り直しの場合は--forceを指定しないとエラーになります)。

$ git push origin --force --all

GitHub側は削除して作り直すとそれまでの履歴は全て消去されますが、この方法では過去にいつ何回commitしたという記録は残り、ユーザページのContributionに表示されます。

リポジトリを残したまま消すのはやや高度

大事なリポジトリなので今までの履歴は残しておきたい(またはそうしないと他人に迷惑がかかる)場合の救済手段は次に書かれています。

GitHub Help - Remove sensitive data

ここの"Purging a file from your repository's history"にgit filter-branchコマンドを使った9ステップの方法があります。

もし間違えてもやり直しの効かない危険作業だということを忘れずに。最低限丸ごとバックアップするかgit cloneしてから作業して下さい。

リポジトリ履歴からファイルを強制的に取り除くにはgit filter-branchを使います。

$ git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch ファイル名' --prune-empty --tag-name-filter cat -- --all

各オプションの意味は次の通りです。

  • --force 強制的に実行
  • --index-filter 'コマンド' コマンドを実行(以下は削除の例)
    • --index-filter 'git rm --cached --ignore-unmatch ファイル' ファイルを全commitから削除
    • --index-filter 'git rm -r --cached --ignore-unmatch ディレクトリ' ディレクトリごと全commitから削除
  • --prune-empty コマンドを実行(削除)した結果空になるcommitを削除
  • --tag-name-filter cat -- --all 該当範囲を全ブランチとtagに設定

メインテナンス用特殊コマンドなのでかなり複雑です。詳しくはリファレンスを参照してください(でも正直言って私はほとんど読んでません...)。

http://git-scm.com/docs/git-filter-branch

結果をよーく確認して、OKならremote側の履歴から消すために強制全pushします。

$ git push origin --force --all

リリースタグを使っている場合はもうひとつ次も必要です。

$ git push origin --force --tags

複数で共同作業している場合はこの後さらにgit rebaseを使った「共有歴史の洗浄」作業が必要になるでしょう(これがドキュメントのstep 8)。しかし私にはこの経験はありませんので文献だけ示しておきます。

http://git-scm.com/book/ja/Git-%E3%81%AE%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81%E6%A9%9F%E8%83%BD-%E3%83%AA%E3%83%99%E3%83%BC%E3%82%B9

削除してもすぐには反映されない

実は削除しても実際の更新作業は即時には行われません。gitはそれ自体が高度なファイルシステムで、操作手順は一時的にキャッシュされ後でバックグラウンドジョブが処理しています。

詳しくは次を参照。ただしそんなに深く理解する必要はないと思います。

http://git-scm.com/book/ja/Git%E3%81%AE%E5%86%85%E5%81%B4-%E3%83%91%E3%83%83%E3%82%AF%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB

ローカルリポジトリに関しては次のコマンドでバックグラウンドジョブを強制実行して完全消去する手段があります。

$ git gc --prune=now

でもこちらからコマンドを送ってGitHub側にこれを行わせる方法はどうもないようです。しばらくすればGitHubが処理してくれるはずです。

私は実際にこの削除作業を行ったことがありますが、APIが返すsizeは数日間変わりませんでした。こればかりはGitHubを信じて待つしかないようです。

GitHub pages(リポジトリ別Webページ)の設定(基礎編)

GitHubでは自分が作成したリポジトリに対応したWebページ(GitHub pages)を作成して公開できます。はじめに基本事項を確認しておきます。

  • リポジトリに対してひとつ(一式)
  • スタティックページのみ
  • 取得できるURLはhttps://ユーザ名.github.io/リポジトリ名
  • サイズの上限は1GB(目安)
    • ただし実際の条件は何だか複雑です(詳しくはこの後の応用編で説明する予定)

この他にユーザ単位のページ作成機能もあります。詳しくはGitHub pagesホームをご覧下さい。

GitHub project pagesで実際に作った例を一つ紹介します(先日自作した暇つぶし)。

http://higuma.github.io/bouncing-balls/

作成方法は大きく分けて2種類あります。

  • Automatic page generatorを使う
  • 手動で作成する(今回説明するのはこちら)
    • スタティックHTMLページなら何でも自由に作ることができます

ただしGitHubなので昔のレンタルサーバみたいにFTPでアップロードすればOKという訳にはいかず、gitの基礎知識が必要になります。方法は次に書いてあります。

GitHug Help - Creating Project Pages manually

必要な説明は全てここに書いてあるのですが、手順の各ステップがどういう意味なのかちゃんと理解して作業しないと痛い目に会います(実際痛かったです...)。ここでは自分への備忘録を兼ねてできるだけ親切に説明します。

全体の仕組み

はじめにどういうメカニズムで機能しているかを簡単に説明しておきます。GitHub側の仕組みはいたってシンプルなものです。

  • リポジトリ(以下repo)にgh-pagesというブランチがあるかチェック
  • もしあればそのブランチ全体をGitHub pagesとして認識する
  • gh-pagesブランチのpushを検出しhttps://ユーザ.github.io/リポジトリにデプロイ

GitHubは内部処理を公開していないため以上はあくまで推測ですが、この程度の察しを付けておけばその後作業する時に戸惑うことはないと思います。

GitHubにgh-pagesブランチがpushされることによりこの作業が行われ、デプロイが完了するとページを閲覧できるようになります。

  • デプロイ作業のため少し時間がかかります(通常1-2分、最大15分)
  • もしデプロイに失敗するとエラーメッセージが書かれたメールが送られてきます

失敗することはほぼないと思います。しかし私は数日前に無茶なことをしてこの珍しいメールを受信しました(これも後で応用編で説明する予定です)。

作業前に確認

それでは実際の作業の説明に入りますが、作業を始める前にローカルrepoとGitHubのリモートrepoの同期が取れていることを確認しておきます。

以下初歩的な内容は(初心者の皆さんへ)と前置きしますが、「初心者」とはちょっと前の私自身そのものです。勉強し始めの頃自分がつまづいた点を少し紹介しておきたいと思います。

  • masterブランチがcommit完了した状態になっていること(重要)
  • それをgithubにpush済であること(これも重要)

(初心者の皆さんへ)これがなぜ重要かというと、後でファイルの全消去という(恐怖の?)作業があるからです。でもcommitさえしておけば後でいくらでもcheckoutで復元できます(何も怖くありません)。

なおGitHub helpではこれを確実に保証するため、はじめにgit cloneで作業用のコピーを作っておくようにと書いています。

$ git clone https://github.com/user/repository.git

しかしこれはおすすめしません(全てを理解した上でやるならいいですが...)。最初の頃私がはまった事例を紹介します。

  • 言われた通りcloneしたrepoでgh-pageブランチを作成
    • この先も書いてある通り操作してGitHub pagesサイトを構築(ここまではよい)
  • だがコピーにはローカルrepoで.gitignoreで除外したファイルは含まれていない
  • そこでついmaster用repoとcloneしたgh-pages用repoの2つで並行作業してしまう
    • これが致命的な誤り!
  • その後master側に変更が生じた時に問題が発生し始める
    • 最初の数回はまあ何とかなる
    • しかし何回も繰り返しているとだんだん収拾つかなくなる
  • 最後は制御不能に陥る(そもそも2つを別々に管理するのが間違い)

gh-pagesブランチの作成

それではgh-pagesブランチを作成しますが、ここでは--orphanという(普段は使わない)オプションがポイントです。

$ git checkout --orphan gh-pages

通常(git checkout -bで作成する場合)はmasterから派生した子の枝として作成しますが、orphanブランチは親のいない独立した枝になります。ここから先はmasterとgh-pagesは全く関係がなくなり、ブランチを切り替えて作業することになります。

GitHubページを作成する

次はgh-pagesの中身を設定してGitHubページを構築します。初期状態はmasterブランチと同じ内容が残っているがまだcommitしていないという状態です。分かりやすく(強制的に)全削除してから作業を開始する場合は次の通りです。

$ git rm -rf .

(初心者の皆さんへ)ここはgitに「ファイルが削除された」と認識させるため必ずgit rmを使って下さい(単にrmでは認識しません)。なおgh-pagesから消去したファイルはmaster側にちゃんと保存されています(後で復元方法も説明します)。

もしmasterブランチの中にgh-pagesで使用するファイルが含まれている場合は全消去はせず、git mvで適宜ファイルを移動した方が効率的です(例えばpublic/ディレクトリにスタティックHTML一式がある場合はgit mv public/* .)。

今回は機能確認用に簡単なテスト用メッセージ表示だけ作っておきます。

$ echo 'GitHub Pages test' > index.html

commit + push (+ deploy)

完成したらcommitし、最後にgh-pagesをpushすれば後はGitHub側がdeployしてくれます。なおgit pushのときに(masterではなく)gh-pagesを指定することに注意して下さい。

$ git add .
$ git commit -m 'first pages commit'
$ git push origin gh-pages

後は(念のため1-2分待ってから)ユーザ名.github.io/リポジトリ名にアクセスすればページが表示されます。

masterとgh-pagesの両ブランチには依存関係は全くありません(mergeとかをする意味はありません)。意味のある操作は片方からもう片方に切り替えることだけです。

$ git checkout master
$ git checkout gh-pages

(初心者の皆さんへ)書きかけ状態でこれをやろうとするとgitに怒られます。慌てずに作業用にいったんcommitしてから行えばgitは一瞬のうちに内容を入れ替えてくれます。