Speeding up your web site

網站的回應速度快慢一直是攸關於使用者會不會停留下來繼續瀏覽最主要的關鍵。最近工作上也面臨到同樣的問題,網站回應速度不佳,客戶抱怨導致客群流失。除了提昇硬體容量跟頻寬這種暴力辦法之外(上級長官已經指示了,沒多餘預算可做容量提升),最後還是得回頭來看看系統面與 AP 面是否還有調整的空間。

Best Practices for Speeding Up Your Web Site 是 Yahoo 做效能調校時整理出來的 14 項 Rule,涵蓋了系統面與網頁程式開發時需注意的事項。各個 Rule 的細節這兒就不提了,有興趣的可自行至上述網址研讀即可。我僅針對 Rule 3 “Add an Expires Header” 拿來套用在 Broso Web 的設定,套用的結果的確效果顯著。

Broso 上的應用不外乎是部落格與相簿,所以網站上大部分都是靜態的圖檔、JavaScript、CSS。在沒有設定 Expires Header 之前,即使靜態圖檔內容並未改變,Browser 仍然會發個 Request 來 Broso Web 問問圖檔是否有更新。由於圖檔並未更新,於是 Apache 便會回覆 HTTP Result Code 304 (Not Modified),因此 Browser 便繼續使用 local cache 上的原圖檔來顯示。下面的資料是從 Fiddler 所節錄出來的:

當網頁中的小圖檔眾多時,Browser 所發出詢問圖檔是否更新的 Request 以及 Web Server 所回覆的 304 (Not Modified) Response,將形成嚴重的 overhead。如果確定大部分的靜態資料都是很久才會做更新,便可以針對這類型的資料設定 Expires Header。

HTTP 協定中 Expires 這個 header tag,是由 Web Server 回覆給 Browser。舉例如下:


HTTP/1.1 200 OK
Date: Sun, 25 Nov 2007 15:58:58 GMT
Server: Apache/2.0.61 (Unix) mod_watch/4.3
Last-Modified: Sat, 17 Nov 2007 04:16:21 GMT
ETag: “1e6f13-216e-2d712f40”
Accept-Ranges: bytes
Content-Length: 8558
Cache-Control: max-age=259200
Expires: Wed, 28 Nov 2007 15:58:58 GMT
Content-Type: image/jpeg

Expires 欄位指示這個靜態圖檔一直到 2007/11/28 15:58:58 以後才會過期,因此 Browser 在這個時間之前都不會再發出任何 Request 到 Web Server 來詢問圖檔是否更新,直接使用 local cache 中的圖檔來顯示,所以所有的 304 (Not Modified) Response 都可以省了,便可減輕 Web Server loading 與頻寬,Browser 的顯示速度也會更迅速。

在 Apache 2.0 要啟用 Expires Header,需使用 mod_expires module。預設設定此模組並不會啟用,需重新編譯,並透過 configure 設定需加入此模組,Apache2 才會具有處理 Expires Header 的能力。

重新編譯 Apache2 的方法可以參考先前的文章 – Apache Basic / Digest Realm Authentication,這次我重新編譯 Apache 時使用的 configure 參數如下:

[root@broso httpd-2.0.61]# ./configure –prefix=/usr/local/apache2 –enable-auth-digest –enable-deflate –enable-expires

編譯完畢後,apache2 就具有處理 Expires Header 的能力了,此時便可進行 httpd.conf 的設定了(mod_expires 的詳細語法可參考 Apache Module mod_expires),針對需要設定 Expires Header 的文件種類與預計過期的時間進行設定。


# mod_expire 針對下列 MIME type 設定 Expires header
ExpiresActive On
ExpiresByType image/jpeg “access plus 3 days”
ExpiresByType image/gif “access plus 3 days”
ExpiresByType image/png “access plus 5 minutes”

ExpiresByType image/x-icon “access plus 4 weeks”
ExpiresByType application/x-javascript “access plus 2 weeks”
ExpiresByType text/css “access plus 2 weeks”
ExpiresByType application/x-shockwave-flash “access plus 4 weeks”

設定完畢,重新啟動 Apache2 即可。

再用 Fiddler 來驗證一下 Expires Header 有沒有生效。首先記得先清除 Browser cache,再瀏覽相簿首頁。隨便找一個圖檔來看看 Expires Header 是否已經出現?

再次瀏覽相簿首頁,看看第二次 Browser 是否還會發出 Request 來詢問圖檔是否更新?

敗 Expires 之賜,Browser 已經知道圖檔尚未過期,因此便只會發出 jsp 的 Request 了。

Apache Digest Authentication – IE Workaround.

昨天設定好 Digest Realm Authentication 之後,測試過 IE7 / Firefox 2,看起來都沒什麼問題。結果今天到公司用 IE6 看的時候,竟然發現怎麼會有 400 Bad Request 錯誤..

Bad Request

查了一下子,才發現原來這是 IE 的 bug,原定身份檢核完畢後,任何連線 IE 都應該會自動帶上 Authorization 的 Header.

Authorization: Digest username=”cychien”, realm=”AWSTATS”, qop=”auth”, algorithm=”MD5″, uri=”/awstats/awstats.pl/”, nonce=”UBoBaJM3BAA=192135920c05c0d0ce0b89e69fbef6144b46ccad”, nc=00000002, cnonce=”a592556fde6ae1e828f360be6081d636″, response=”53580f5e52dd07e62ffa594865aeb1e2″

但碰到有 query string 的 URL 時,IE 就突槌了,摘錄 HTTP Header 資料如下就一切明瞭了

GET /awstats/awstats.pl/awstats.pl?framename=mainleft HTTP/1.1
….
….省略
Cookie: __utma=200320509.1007104159.1151628725.1187006067.1187006101.242; __utmz=200320509.1182908533.228.4.utmccn=(organic)|utmcsr=google|utmctr=%e9%9a%a8%e6%83%b3%e6%84%8f%e8%aa%8c|utmcmd=organic; phpbb2mysql_data=a%3A2%3A%7Bs%3A11%3A%22autologinid%22%3Bs%3A0%3A%22%22%3Bs%3A6%3A%22userid%22%3Bi%3A-1%3B%7D; control-commentControl=false; control-pluginControl=false; control-miscControl=false; control-mediaCastControl=false; rfolder-root=true; rfolder-??????=true; rfolder-??L???=true; __utmb=200320509; __utmc=200320509; JSESSIONID=GQGGcQPrYCQhzvCpyVKxJBGwmfNpy1cs2wXfy1bv1sgST9vSYyL1!-857858892

Authorization: Digest username=”cychien”, realm=”AWSTATS”, qop=”auth”, algorithm=”MD5″, uri=”/awstats/awstats.pl/awstats.pl?framename=mainleft“, nonce=”UBoBaJM3BAA=192135920c05c0d0ce0b89e69fbef6144b46ccad”, nc=00000003, cnonce=”8b98b9b1072e4c812443386f8363c07c”, response=”6d5c775889d9e631aed40cfafd19ce9a”

問題就出在 Authorization 的 uri 參數中,IE 的 bug 註定不會將 query string 附上,所以Apache 會認為與先前的 uri 不同,所以回覆 Bad Request.

針對微軟的老大心態,死就是不改他的 bug,所幸 Apache 從 2.0.51 之後也推出一組解決方案,藉以解決 IE query string 問題.

只要在 httpd.conf 中加上

BrowserMatch “MSIE” AuthDigestEnableQueryStringHack=On

只有針對 IE 瀏覽器,才會忽略 Authorization uri 參數的錯誤。經過測試果然有效。特別記錄一下..

Apache Basic / Digest Realm Authentication

最近想針對 Broso 特定系統統計功能做身份檢核的動作,於是稍微研究了一下針對 HTTP 協定中有關於身份驗證的機制。
HTTP 協定針對身份驗證分兩種模式,Basic Realm / Digest Realm。Basic Realm 僅將帳號密碼用標準 BASE64 來做編碼後,上送給 Web Server 來驗證,所以有心人其實可以透過 Sniffer 之類的軟體來進行側錄,再用 BASE64 解碼即可得知帳號密碼。

Digest Realm 則是為了解決上述 Basic Realm 的缺點,採用一般 Unix 儲存/驗證密碼的方式,每次瀏覽器要求連線,Web Server 會回覆一組隨機產生的 nonce value。瀏覽器則將帳號/密碼/nonce/GET(or POST)以及 URI 等參數,依指定的 hash function 做成訊息摘要( Digest),再將 Digest 上傳到 WebServer 驗證,如此一來上送的 Digest 每次皆會不同,藉以避免被有心人士所側錄。

以上可參考 RFC 2617 – HTTP Authentication: Basic and Digest Access Authentication

Apache 已經實作上述兩種機制,mod_auth 模組僅支援 Basic Realm。而 mod_auth_digest 則是支援 Digest Realm。不過如果 Apache 是抓預先編譯好的的 binary,或是編譯時未特別指定,那麼預設是不包含 mod_auth_digest 模組。

步驟一:重新編譯 APACHE.

由於我先前的 Apache 2.0.55 並未特別指明要啟用 mod_auth_digest,於是我重新下載 Apache 2.0.59 回來重新編譯。
執行 configure 指令如下

./configure –enable-auth-digest –prefix=<APACHE_INSTALL_DIR>

如此編譯時就會自動將 mod_auth_digest 加入。接著作 make, make install 就完成了。

步驟二:產生 Digest 帳號密碼檔

接下來先產生一組帳號密碼設定檔,每組帳號密碼都需針對特定的 realm 獨立設定。需使用 <APACHE_INSTALL_DIR>/bin/htdigest 指令來產生密碼檔。
指令格式

<APACHE_INSTALL_DIR>/bin/htdigest -c <path>/<digest_password_file_name> <realm name> <user id>

-c 代表要產生一個新的密碼檔,如果檔名已經存在,就會被覆蓋。所以如果想繼續增加新的帳號密碼,就無須加上 -c 即可
後面接上密碼檔的路徑與檔名,以及要檢核密碼的 “領域(realm)” (意指瀏覽器 popup 視窗,提示輸入帳號密碼時,上面顯示的名稱),最後是帳號名稱
之後 htdigest 會詢問兩次密碼後就完成建立密碼檔。

例如:我要針對 /mymrtg 與 /awstats 兩個目錄進行身份檢核,就需針對這兩個領域來建立帳號密碼

htdigest -c <somewhere>/digestpasswd “MYMRTG” <user_id>
htdigest <somewhere>/digestpasswd “AWSTATS” <user_id>

屆時瀏覽器提示帳號密碼輸入時,就會顯示目前要您輸入的領域是 MYMRTG 或是 AWSTATS

步驟三:調整 httpd.conf

針對每個要作身份檢核的的領域作設定

<Directory “<somewhere>/mymrtg”>
   AuthType Digest → 指定使用 Digest Realm Authentication
   AuthName “MYMRTG” → 指定領域名稱
   AuthDigestFile <somewhere>/<digest_password_file_name> → 指定帳號密碼檔路徑與檔名
   Require user <allowed user id> → 這裡指定允許使用的 user id

   …
   …
</Directory>

設定完畢重起 Apache 即可

圖片盜連進階版

續上篇 防賊妙招!如何防止圖片被小偷盜連?

後來發現部分圖片是我自己發佈在一些我常去的討論區,或者是從股溝大神搜尋結果中連結過來的,以及直接在網址列打入網址時,Referer 會是空白。所以特別把上面這些情形給開放了。

設定調整如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 防止人家盜連圖片, 擋 gif, jpg,png
SetEnvIfNoCase Referer "^http://broso\.twbbs\.org/" local_ref=1
SetEnvIfNoCase Referer "^http://www\.mobile01\.com/" external_ref=1
SetEnvIfNoCase Referer "^http://www\.play01\.com/" external_ref=1
SetEnvIfNoCase Referer "^http://www\.google\.com/" external_ref=1
SetEnvIfNoCase Referer ^$ blank_ref=1

<FilesMatch "\.([Gg][Ii][Ff]|[Jj][Pp][Gg]|[Pp][Nn][Gg] )$">
  Order deny,allow
  Deny from all
  Allow from env=local_ref
  Allow from env=external_ref
  Allow from env=blank_ref
  ErrorDocument 403 /thief.giff
</FilesMatch>

另外,還加了一個小圖,如果盜連圖片的人網頁上就會出現小偷的圖示 😛
小偷

嘿嘿..再繼續盜連沒關係])
圖片盜連

防賊妙招!如何防止圖片被小偷盜連?

這兩天把 AWSTATS 給裝起來了,順道把 Apache Access Log 格式改成 Combined 格式,一併將 Referer 與瀏覽器/OS的類型一併記錄在Access Log 中。
開始紀錄了以後才發現,原來有人盜連我文章上的圖片。

220.132.143.85 – – [30/Jul/2007:00:02:46 +0800] “GET /blojsom/resources/default/20050911_azureus/step3.JPG HTTP/1.1” 403 357 “http://www.btpig.com/html/200702/649999.html” “Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.8.1.5) Gecko/20070713 Firefox/2.0.0.5”

按照 Referer 的網址連過去,果然又是大陸的網站。難怪先前沒事在看 Access Log 時,常常看到怎麼會只有圖片而沒有文章的的點擊紀錄。這下子得來想想怎麼防這些小偷。

股溝大神真是我的好朋友,有什麼問題問他,包準給你一個滿意的答覆。查了一下怎麼擋盜連圖片,大抵上都是說要透過 HTTP Header 上的 Referer 來達到阻擋的效果。

所謂 Referer 是指,瀏覽器在進行 HTTP Request 時,會自動將上一個 URL 放在 Referer 中。於是乎,如果是正常連結過來圖片,Referer 必然是從 broso.twbbs.org 所連結過來的。所以只要 Referer 不是 broso.twbbs.org,那就不用客氣了,殺無赦!!

於是調整了一下我的 Apache httpd.conf,增加參數如下:

1
2
3
4
5
6
7

# 防止人家盜連圖片, 擋 gif, jpg,png
SetEnvIfNoCase Referer "^http://broso\.twbbs\.org/" local_ref=1
<FilesMatch "\.([Gg][Ii][Ff]|[Jj][Pp][Gg]|[Pp][Nn][Gg] )$">
  Order deny,allow
  Deny from all
  Allow from env=local_ref
</FilesMatch>

稍微解釋一下

SetEnvIfNoCase 這一行,指的是任何 Referer 開頭是 http://broso.twbbs.org/ 都設定環境變數 local_ref 為 1
接著用 FilesMatch Tag,針對任何 gif/jpg/png 結尾的檔案進行檢查,有設定環境變數 local_ref 的要求才允許讀取

改完後,重起以後就可以了…