1. 程式人生 > >Java Web 高效能開發,第 2 部分: 前端的高效能

Java Web 高效能開發,第 2 部分: 前端的高效能

原文地址:http://www.ibm.com/developerworks/cn/java/j-lo-javawebhiperf2/

引言

在前端優化的第一部分中,主要講解了對靜態資源的一些優化措施,包括圖片壓縮、CSS Sprites 技術、GZIP 壓縮等。這一部分,本文將講解前端優化裡重要的 Flush 機制、動靜分離、HTTP 持久連線、HTTP 協議靈活應用、CDN 等。結合這些技術或思想,相信會使 Java Web 應用程式的效能更上一層樓。

Flush 機制的使用

實際上在 Web 技術中,Flush 機制並不新鮮,它的思想是無需等到網頁內容全部載入完畢,一次性寫回客戶端,而是可以部分逐次的返回。如果網頁很大的話,一次性寫回全部內容顯然是個不明智的選擇,因為這會造成網頁的長時間空白。Flush 機制允許開發人員將網頁的內容按文件流順序逐步返回給客戶端,這樣可以使得使用者知道我們的系統正在工作,只是等待的時間稍長而已,這樣使用者也會“心甘情願”的等下去。Flush 機制是一個經典的提高使用者體驗的方法,至今也一直在用。如果網頁很大,這個機制也是建議使用的。在 Java Web 技術中,實現 Flush 非常簡單,只要呼叫 HttpServletResponse.getWriter 輸出流的 flush 方法,就可以將已經完成載入的內容寫回給客戶端。

但是是否每個網頁都要使用該技術呢?筆者當然不這麼建議。將網頁內容載入完畢後再一次性返回客戶端也有它的好處。我們知道網路傳輸也有最大的傳輸單元,內容載入完畢後一次性輸出就可以最大程度的利用傳輸的頻寬,減少分塊,減少傳輸次數,也就是說實際上 Flush 機制會增加使用者等待時間、增加瀏覽器渲染時間,但是對於大網頁來說,降低這點效率來增強使用者體驗,是值得的。

動靜分離

所謂的動靜分離,就是將 Web 應用程式中靜態和動態的內容分別放在不同的 Web 伺服器上,有針對性的處理動態和靜態內容,從而達到效能的提升。本文基於 Java Web 來講解 Web 優化,而 Java Web 的主流伺服器軟體是 Tomcat。讓人遺憾的是,Tomcat 在併發和靜態資源處理的能力上較弱,這也是 Tomcat 為人詬病的地方。但是瑕不掩瑜,既然我們選擇了 Java Web,那麼就應該發揮我們程式設計師的頭腦去想方設法的提高效能。而動靜分離就是其中一種方法,既然 Tomcat 處理靜態資源的能力較弱,那就將靜態資源的處理任務交給適合的軟體,而讓 Tomcat 專注於處理 JSP/Servlet 的請求。

對於靜態資源處理的伺服器軟體,我們可以選擇 Nginx,它是一款俄羅斯人開發的軟體,似乎比 Apache 更加優秀。它支援高併發,對靜態資源處理的能力較強,這正是我們想要的不是嗎?事實上,動靜分離的方案很多,有人採用 Apache+Tomcat 的組合;也有人使用 Tomcat+Tomcat 的組合,不過兩個 Tomcat 分別被放置於不同的主機,不同的域名。其中 Apache+Tomcat 的方案與 Nginx 的方案原理上是一樣的,它們都是基於反向代理,相對於使用 Nginx 配置動靜分離,Apache 的配置就顯得略微複雜一些。在 Apache 裡,mod_proxy 模組負責反向代理的實現。其中核心配置內容如清單 1 所示,該配置屬於本人蔘與某專案的其中一部分。

清單 1. 動靜分離的 Apache 核心配置
 <Proxy balancer://proxy> 
        BalancerMember http://192.168.1.178:8080 loadfactor=1 
        BalancerMember http://192.168.1.145:8080 loadfactor=1 
 </Proxy> 
 NameVirtualHost *:80 
 <VirtualHost *:80> 
        ServerAdmin [email protected] 
        ServerName www.xuanli365.com 
        DocumentRoot /www 
        DirectoryIndex index.shtml 
        <Directory /www> 
                AllowOverride All 
                AddType text/html .shtml 
                AddType application/x-rar .rar 
                AddHandler server-parsed .shtml 
                Options +IncludesNOEXEC 
        </Directory> 
 RewriteEngine on 
 ProxyRequests Off
	ProxyPass /static/!
	ProxyPass / balancer://proxy/
	ProxyPassReverse / balancer://proxy/
	ProxyPreserveHost on
 </VirtualHost>

從 Apache 官方對 mod_proxy 模組的介紹,我們可以知道 ProxyPass 屬性可以將一個遠端伺服器對映到本地伺服器的 URL 空間中,也就是說這是一個地址對映功能。在清單 1 的配置中,當訪問的路徑不在 /static/ 下時(!表示非),就轉發給後端的伺服器(也就是 Tomcat);否則如果是 /static/ 路徑就訪問本機。例如,當訪問 www.xuanli365.com/static/css/index.css 時,實際處理請求的是 Apache 伺服器,而訪問 www.xuanli365.com/index.jsp,那麼 Apache 會將請求轉發到後端的 Tomcat 伺服器,實際訪問的頁面是 http:// 192.168.1.178( 或 145):8080/index.jsp,這就實現了動靜分離。在清單 1 的配置中實際也包含了簡單的負載均衡(loadfactor 因子)。

事實上,我們可以隨便開啟一個大型入口網站來看一下,我開啟的是騰訊網站,任意檢視其中兩張圖片的地址,我發現一個是:http://mat1.gtimg.com/www/iskin960/qqcomlogo.png,而另一個則是:http://img1.gtimg.com/v/pics/hv1/95/225/832/54158270.jpg。可見該網站存放圖片資源使用了多個的域名,我們再用 Linux 的 host 命令檢視兩個域名的 IP 地址,結果如圖 1 所示。

圖 1. 某網站的動靜分離
圖 1. 某網站的動靜分離

可以看到,通過檢視 IP 地址,我們發現這些圖片很可能存放在不同的主機上(為什麼是很可能?因為一個主機可以擁有多個 IP),而圖片內容和網頁的動態內容並不在同一 IP 下,也很可能是動靜分離。多個域名在前面也已經提到,可以增加瀏覽器的併發下載數,提高下載效率。

本文采用另一種策略對動靜分離進行演示,它的大致結構如圖 2 所示。

圖 2. 本文設計的動靜分離結構
圖 2. 本文設計的動靜分離結構

在本文中,我們將靜態資源放在 A 主機的一個目錄上,將動態程式放在 B 主機上,同時在 A 上安裝 Nginx 並且在 B 上安裝 Tomcat。配置 Nginx,當請求的是 html、jpg 等靜態資源時,就訪問 A 主機上的靜態資源目錄;當用戶提出動態資源的請求時,則將請求轉發到後端的 B 伺服器上,交由 Tomcat 處理,再由 Nginx 將結果返回給請求端。

提到這,可能有您會有疑問,動態請求要先訪問 A,A 轉發訪問 B,再由 B 返回結果給 A,A 最後又將結果返回給客戶端,這是不是有點多餘。初看的確多餘,但是這樣做至少有 2 點好處。第一,為負載均衡做準備,因為隨著系統的發展壯大,只用一臺 B 來處理動態請求顯然是是不夠的,要有 B1,B2 等等才行。那麼基於圖 2 的結構,就可以直接擴充套件 B1,B2,再修改 Nginx 的配置就可以實現 B1 和 B2 的負載均衡。第二,對於程式開發而言,這種結構的程式撰寫和單臺主機沒有區別。我們假設只用一臺 Tomcat 作為伺服器,那麼凡是靜態資源,如圖片、CSS 程式碼,就需要編寫類似這樣的訪問程式碼:<img src=”{address of A}/a.jpg”>,當靜態資源過多,需要擴展出其他的伺服器來安放靜態資源時,訪問這些資源就可能要編寫這樣的程式碼:<img src=”{address of C}/a.jpg”>、<img src=”{address of D}/a.jpg”>。可以看到,當伺服器進行變更或擴充套件時,程式碼也要隨之做出修改,對於程式開發和維護來說非常困難。而基於上面的結構,程式都只要 <img src=”a.jpg”>,無需關心具體放置資源的伺服器地址,因為具體的地址 Nginx 為幫您繫結和選擇。

按照圖 2 所示的架構圖,安裝好需要的軟體 Nginx 和 Tomcat。按照設想,對 Nginx 的配置檔案 nginx.conf 進行配置,其中與本文該部分相關的配置如清單 2 所示。

清單 2. 動靜分離的 Nginx 配置
 # 轉發的伺服器,upstream 為負載均衡做準備
 upstream tomcat_server{ 
        server 192.168.1.117:8080; 
 } 

 server { 
        listen       9090; 
        server_name  localhost; 
 index index.html index.htm index.jsp; 
        charset koi8-r; 

        # 靜態資源存放目錄
        root  /home/wq243221863/Desktop/ROOT; 

        access_log  logs/host.access.log  main; 

 # 動態請求的轉發
        location ~ .*.jsp$ { 
            proxy_pass http://tomcat_server; 
            proxy_set_header Host $host; 
        } 
        
 # 靜態請求直接讀取
 location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|css)$ { 
          expires      30d; 
 } 
……

清單 2 十分簡潔,其目的和我們預期的一樣,動態的請求(以 .jsp 結尾)發到 B(192.168.1.117:8080,即 tomcat_server)上,而靜態的請求(gif|jpg 等)則直接訪問定義的 root(/home/wq243221863/Desktop/ROOT)目錄。這個 root 目錄我直接將其放到 Linux 的桌面 ROOT 資料夾。

接下來在 Tomcat 中新建 Web 專案,很簡單,我們只為其新增一個 test.jsp 檔案,目錄結構如圖 3 所示。

圖 3. B 上的測試專案結構
圖 3. B 上的測試專案結構

而我們定義了一張測試用的靜態圖片,放置在 A 的桌面 ROOT/seperate 目錄下。結構如圖 4 所示

圖 4. A 上的靜態資原始檔夾結構
圖 4. A 上的靜態資原始檔夾結構

注意這裡的 separate 目錄名是與 B 的專案資料夾同名的

再檢視圖 3 中的 test.jsp 的原始碼。如清單 3 所示。

清單 3. test.jsp 原始碼
 <%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%> 
 <%@ page import="java.util.Date" %> 
 <%@ page import="java.text.SimpleDateFormat" %> 
 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/
 html4/loose.dtd"> 
 <html> 
 <head> 
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 
 <title>動靜分離的測試</title>
 </head> 
 <body> 
      <div>這是動態指令碼處理的結果</div><br>
    <% //這是一段測試的動態指令碼
    Date now=new Date(); 
    SimpleDateFormat f=new SimpleDateFormat("現在是"+"yyyy年MM月dd日E kk點mm分");
    %> 
    <%=f.format(now)%> 
    <br><br> 
    <div>這是靜態資源的請求結果</div><br><img alt="靜態資源" src="jquery.gif">
 </body> 
 </html>

清單 3 是一個非常簡單的 JSP 頁面,主要是使用 img 標籤來訪問 jquery.gif,我們知道 test.jsp 在 B 伺服器上,而 jquery.gif 在 A 伺服器上。用於訪問 jquery.gif 的程式碼裡不需要指定 A 的地址,而是直接使用相對路徑即可,就好像該圖片也在 B 上一樣,這就是本結構的一個優點了。我們在 A 上訪問 test.jsp 檔案。結果如圖 5 所示。

圖 5. test.jsp 的結果
圖 5. test.jsp 的結果

非常順利,完全按照我們的想法實現了動靜分離!

我們初步完成了動靜分離的配置,但是究竟動靜分離如何提高我們的程式效能我們還不得而知,我們將 Tomcat 伺服器也遷移到 A 伺服器上,同時將 jquery.gif 拷貝一份到 separate 專案目錄下,圖 3 的結構變為圖 6 所示。

圖 6. 拷貝 jquery.gif 的 separate 專案
圖 6. 拷貝 jquery.gif 的 separate 專案

我們將 Tomcat 的埠設定為 8080,Nginx 的埠依然是 9090。現在訪問 http://localhost:9090/separate/test.jsp(未使用動靜分離)和訪問 http://localhost:8080/separate/test.jsp(使用了動靜分離)的效果是一樣的了。只是 8080 埠的靜態資源由 Tomcat 處理,而 9090 則是由 Nginx 處理。我們使用 Apache 的 AB 壓力測試工具,對 http://localhost:8080/seperate/jquery.gif、http://localhost:9090/seperate/jquery.gif、http://localhost:8080/seperate/test.jsp、http://localhost:9090/seperate/test.jsp 分別進行壓力和吞吐率測試。

首先,對靜態資源(jquery.gif)的處理結果如清單 4 所示。

清單 4. 靜態資源的 AB 測試
測試指令碼:ab -c 100 -n 1000 http://localhost:{port}/seperate/jquery.gif 
 9090 埠,也就是 Nginx 的測試結果:
 Concurrency Level:      100 
 Time taken for tests:   0.441 seconds 
 Complete requests:      1000 
 Failed requests:        0 
 Write errors:           0 
 Total transferred:      4497000 bytes 
 HTML transferred:       4213000 bytes 
 Requests per second:    2267.92 [#/sec] (mean)
 Time per request:       44.093 [ms] (mean) 
 Time per request:       0.441 [ms] (mean, across all concurrent requests) 
 Transfer rate:          9959.82 [Kbytes/sec] received 

 8080 埠,也就是 Tomcat 的測試結果:
 Concurrency Level:      100 
 Time taken for tests:   1.869 seconds 
 Complete requests:      1000 
 Failed requests:        0 
 Write errors:           0 
 Total transferred:      4460000 bytes 
 HTML transferred:       4213000 bytes 
 Requests per second:    535.12 [#/sec] (mean)
 Time per request:       186.875 [ms] (mean) 
 Time per request:       1.869 [ms] (mean, across all concurrent requests) 
 Transfer rate:          2330.69 [Kbytes/sec] received

清單 4 的測試指令碼代表同時處理 100 個請求並下載 1000 次 jquery.gif 檔案,您可以只關注清單 4 的粗體部分(Requests per second 代表吞吐率),從內容上就可以看出 Nginx 實現動靜分離的優勢了,動靜分離每秒可以處理 2267 個請求,而不使用則只可以處理 535 個請求,由此可見動靜分離後效率的提升是顯著的。

您還會關心,動態請求的轉發,會導致動態指令碼的處理效率降低嗎?降低的話又降低多少呢?因此我再用 AB 工具對 test.jsp 進行測試,結果如清單 5 所示。

清單 5. 動態指令碼的 AB 測試
測試指令碼:ab -c 1000 -n 1000 http://localhost:{port}/seperate/test.jsp 
 9090 埠,也就是 Nginx 的測試結果:
 Concurrency Level:      100 
 Time taken for tests:   0.420 seconds 
 Complete requests:      1000 
 Failed requests:        0 
 Write errors:           0 
 Total transferred:      709000 bytes 
 HTML transferred:       469000 bytes 
 Requests per second:    2380.97 [#/sec] (mean)
 Time per request:       42.000 [ms] (mean) 
 Time per request:       0.420 [ms] (mean, across all concurrent requests) 
 Transfer rate:          1648.54 [Kbytes/sec] received 

 8080 埠,也就是 Tomcat 的測試結果:
 Concurrency Level:      100 
 Time taken for tests:   0.376 seconds 
 Complete requests:      1000 
 Failed requests:        0 
 Write errors:           0 
 Total transferred:      714000 bytes 
 HTML transferred:       469000 bytes 
 Requests per second:    2660.06 [#/sec] (mean)
 Time per request:       37.593 [ms] (mean) 
 Time per request:       0.376 [ms] (mean, across all concurrent requests) 
 Transfer rate:          1854.77 [Kbytes/sec] received

經過筆者的多次測試,得出了清單 5 的較為穩定的測試結果,可以看到在使用 Nginx 實現動靜分離以後,的確會造成吞吐率的下降,然而對於網站整體效能來說,靜態資源的高吞吐率,以及未來可以實現的負載均衡、可擴充套件、高可用性等,該犧牲我想也應該是值得的。

我想任何技術都是有利有弊,動靜分離也是一樣,選擇了動靜分離,就選擇了更為複雜的系統架構,維護起來在一定程度會更為複雜和困難,但是動靜分離也的確帶來了很大程度的效能提升,這也是很多系統架構師會選擇的一種解決方案。

HTTP 持久連線

持久連線(Keep-Alive)也叫做長連線,它是一種 TCP 的連線方式,連線會被瀏覽器和伺服器所快取,在下次連線同一伺服器時,快取的連線被重新使用。由於 HTTP 的無狀態性,人們也一直很清楚“一次性”的 HTTP 通訊。持久連線則減少了建立連線的開銷,提高了效能。HTTP/1.1 已經支援長連線,大部分瀏覽器和伺服器也提供了長連線的支援。

可以想象,要想發起長連線,伺服器和瀏覽器必須共同合作才可以。一方面瀏覽器要保持連線,另一方面伺服器也不會斷開連線。也就是說要想建立長連線,伺服器和瀏覽器需要進行協商,而如何協商就要靠偉大的 HTTP 協議了。它們協商的結構圖如圖 7 所示。

圖 7. 長連線協商
圖 7. 長連線協商

瀏覽器在請求的頭部新增 Connection:Keep-Alive,以此告訴伺服器“我支援長連線,你支援的話就和我建立長連線吧”,而倘若伺服器的確支援長連線,那麼就在響應頭部新增“Connection:Keep-Alive”,從而告訴瀏覽器“我的確也支援,那我們建立長連線吧”。伺服器還可以通過 Keep-Alive:timeout=10, max=100 的頭部告訴瀏覽器“我希望 10 秒算超時時間,最長不能超過 100 秒”。

在 Tomcat 裡是允許配置長連線的,配置 conf/server.xml 檔案,配置 Connector 節點,該節點負責控制瀏覽器與 Tomcat 的連線,其中與長連線直接相關的有兩個屬性,它們分別是:keepAliveTimeout,它表示在 Connector 關閉連線前,Connector 為另外一個請求 Keep Alive 所等待的微妙數,預設值和 connectionTimeout 一樣;另一個是 maxKeepAliveRequests,它表示 HTTP/1.0 Keep Alive 和 HTTP/1.1 Keep Alive / Pipeline 的最大請求數目,如果設定為 1,將會禁用掉 Keep Alive 和 Pipeline,如果設定為小於 0 的數,Keep Alive 的最大請求數將沒有限制。也就是說在 Tomcat 裡,預設長連線是開啟的,當我們想關閉長連線時,只要將 maxKeepAliveRequests 設定為 1 就可以。

毫不猶豫,首先將 maxKeepAliveRequests 設定為 20,keepAliveTimeout 為 10000,通過 Firefox 檢視請求頭部(這裡我們訪問上面提到的 test.jsp)。結果如圖 8 所示。

圖 8. 伺服器開啟長連線
圖 8. 伺服器開啟長連線

接下來,我們將 maxKeepAliveRequests 設定為 1,並且重啟伺服器,再次請求網頁後檢視的結果如圖 9 所示。

圖 9. 伺服器關閉長連線
圖 9. 伺服器關閉長連線

對比可以發現,Tomcat 關閉長連線後,在伺服器的請求響應中,明確標識了:Connection close, 它告訴瀏覽器伺服器並不支援長連線。那麼長連線究竟可以帶來怎麼樣的效能提升,我們用資料說話。我們依然使用 AB 工具,它可以使用一個 -k 的引數,模擬瀏覽器使用 HTTP 的 Keep-Alive 特性。我們對 http://localhost:8080/seperate/jquery.gif 進行測試。測試結果如清單 6 所示。

清單 6. AB 測試長連線
測試指令碼:ab – k -c 1000 -n 10000 http://localhost:8080/seperate/jquery.gif 


關閉長連線時:
 Concurrency Level:      1000 
 Time taken for tests:   5.067 seconds 
 Complete requests:      10000 
 Failed requests:        0 
 Write errors:           0 
 Keep-Alive requests:    0 
 Total transferred:      44600000 bytes 
 HTML transferred:       42130000 bytes 
 Requests per second:    1973.64 [#/sec] (mean)
 Time per request:       506.678 [ms] (mean) 
 Time per request:       0.507 [ms] (mean, across all concurrent requests) 
 Transfer rate:          8596.13 [Kbytes/sec] received 
 
 
開啟長連線時,maxKeepAliveRequests 設定為 50:
 Concurrency Level:      1000 
 Time taken for tests:   1.671 seconds 
 Complete requests:      10000 
 Failed requests:        0 
 Write errors:           0 
 Keep-Alive requests:    10000 
 Total transferred:      44650000 bytes 
 HTML transferred:       42130000 bytes 
 Requests per second:    5983.77 [#/sec] (mean)
 Time per request:       167.119 [ms] (mean) 
 Time per request:       0.167 [ms] (mean, across all concurrent requests) 
 Transfer rate:          26091.33 [Kbytes/sec] received

結果一定會讓您大為驚訝,使用長連線和不使用長連線的效能對比,對於 Tomcat 配置的 maxKeepAliveRequests 為 50 來說,竟然提升了將近 5 倍。可見伺服器預設開啟長連線是有原因的。

HTTP 協議的合理使用

很多程式設計師都將精力專注在了技術實現上,他們認為效能的高低完全取決於程式碼的實現,卻忽略了已經成型的某些規範、協議、工具。最典型的就是在 Web 開發上,部分開發人員沒有意識到 HTTP 協議的重要性,以及 HTTP 協議可以提供程式設計師另一條效能優化之路。通過簡單的在 JSP 的 request 物件中新增響應頭部,往往可以迅速提升程式效能,一切實現程式碼彷彿都成浮雲。本系列文章的宗旨也在於讓程式設計師編最少的程式碼,提升最大的效能。

本文提出一個這樣的需求,在文章前面部分提到的 test.jsp 中,它的一部分功能是顯示伺服器的當前時間。現在我們希望這個動態網頁允許被瀏覽器快取,這似乎有點不合理,但是在很多時候,雖然是動態網頁,但是卻只執行一次(比如有些人喜歡將網頁的主選單存入資料庫,那麼他肯定不希望每次載入選單都去讀資料庫)。瀏覽器快取帶來的效能提升已經眾人皆知了,而很多人卻並不知道瀏覽器的快取過期時間、快取刪除、什麼頁面可以快取等,都可以由我們程式設計師來控制,只要您熟悉 HTTP 協議,就可以輕鬆的控制瀏覽器。

我們訪問上面提及的 test.jsp。用 Firebug 檢視請求情況,發現每次請求都會重新到伺服器下載內容,這不難理解,因此 test.jsp 是動態內容,每次伺服器必須都執行後才可以返回結果 , 圖 10 是訪問當前的 test.jsp 的頭部情況。現在我們往 test.jsp 新增清單 7 的內容。

清單 7. 在 test.jsp 的首部新增的程式碼
 <% 
 SimpleDateFormat f2=new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss"); 
 String ims = request.getHeader("If-Modified-Since"); 
 if (ims != null) 
 { 
 try 
 { 
 Date dt = f2.parse(ims.substring(0, ims.length()-4)); 
 if (dt.after(new Date(2009, 1, 1))) 
 { 
 response.setStatus(304); 
 return; 
 } 
 } catch(Exception e) 
 { 

 } 
 } 
 response.setHeader("Last-Modified", f2.format(new Date(2010, 5, 5)) + " GMT"); 
 %>

上述程式碼的意圖是:伺服器獲得瀏覽器請求頭部中的 If-Modified-Since 時間,這個時間是瀏覽器詢問伺服器,它所請求的資源是否過期,如果沒過期就返回 304 狀態碼,告訴瀏覽器直接使用本地的快取就可以,

圖 10. 修改 test.jsp 前的訪問頭部情況
圖 10. 修改 test.jsp 前的訪問頭部情況

修改完 test.jsp 程式碼後,使用滑鼠啟用瀏覽器位址列,按下回車重新整理頁面。這次的結果如圖 11 所示。

圖 11. 修改 test.jsp 後的首次訪問
圖 11. 修改 test.jsp 後的首次訪問

可以看到圖 11 和圖 10 的請求報頭沒有區別,而在伺服器的響應中,圖 11 增加了 Last-Modified 頭部,這個頭部告訴瀏覽器可以將此頁面快取。

按下 F5(必須是 F5 重新整理),F5 會強制 Firefox 載入伺服器內容,並且發出 If-Modified-Since 頭部。得到的報頭結果如圖 12 所示 .

圖 12. 修改 test.jsp 後的再次訪問
圖 12. 修改 test.jsp 後的再次訪問

可以看到,圖 12 的底部已經提示所有內容都來自快取。瀏覽器的請求頭部多出了 If-Modified-Since,以此詢問伺服器從快取時間起,伺服器是否對資源進行了修改。伺服器判斷後發現沒有對此資源(test.jsp)修改,就返回 304 狀態碼,告訴瀏覽器可以使用快取。

我們在上面的實驗中,用到了 HTTP 協議的相關知識,其中涉及了 If-Modified-Since、Last-Modified、304 狀態碼等,事實上與快取相關的 HTTP 頭部還有許多,諸如過期設定的頭部等。熟悉了 HTTP 頭部,就如同學會了如何與使用者的瀏覽器交談,也可以利用協議提升您的程式效能。這也是本文為何一直強調 HTTP 協議的重要性。那麼對於 test.jsp 這個小網頁來說,基於快取的方案提升了多少效能呢?我們用 AB 給您答案。

AB 是個很強大的工具,他提供了 -H 引數,允許測試人員手動新增 HTTP 請求頭部,因此測試結果如清單 8 所示。

清單 8. AB 測試 HTTP 快取

測試指令碼:ab -c 1000 – n 10000 – H ‘ If-Modified-Since:Sun, 05 Jun 3910 00:00:00 GMT ’ http://localhost:8080/seperate/test.jsp

未修改 test.jsp 前 : 
 Document Path:          /seperate/test.jsp 
 Document Length:        362 bytes
 Concurrency Level:      1000 
 Time taken for tests:   10.467 seconds 
 Complete requests:      10000 
 Failed requests:        0 
 Write errors:           0 
 Total transferred:      6080000 bytes 
 HTML transferred:       3630000 bytes 
 Requests per second:    955.42 [#/sec] (mean)
 Time per request:       1046.665 [ms] (mean) 
 Time per request:       1.047 [ms] (mean, across all concurrent requests) 
 Transfer rate:          567.28 [Kbytes/sec] received

修改 test.jsp 後:
 Document Path:          /seperate/test.jsp 
 Document Length:        0 bytes
 Concurrency Level:      1000 
 Time taken for tests:   3.535 seconds 
 Complete requests:      10000 
 Failed requests:        0 
 Write errors:           0 
 Non-2xx responses:      10000 
 Total transferred:      1950000 bytes 
 HTML transferred:       0 bytes 
 Requests per second:    2829.20 [#/sec] (mean)
 Time per request:       353.457 [ms] (mean) 
 Time per request:       0.353 [ms] (mean, across all concurrent requests) 
 Transfer rate:          538.76 [Kbytes/sec] received

分別對比 Document Length、Requests per second 以及 Transfer rate 這三個指標。可以發現沒使用快取的 Document Length(下載內容的長度)是 362 位元組,而使用了快取的長度為 0。在吞吐率方面,使用快取是不使用快取的 3 倍左右。同時在傳輸率方面,快取的傳輸率比沒快取的小。這些都是用到了客戶端快取的緣故。

CDN 的使用

CDN 也是筆者最近才瞭解和接觸到的東西,耳中也是多次聽到 CDN 這個詞了,在淘寶的前端技術報告上、在一個好朋友的創新工場創業之路上,我都聽到了這個詞,因此我想至少有必要對此技術瞭解一下。所謂的 CDN,就是一種內容分發網路,它採用智慧路由和流量管理技術,及時發現能夠給訪問者提供最快響應的加速節點,並將訪問者的請求導向到該加速節點,由該加速節點提供內容服務。利用內容分發與複製機制,CDN 客戶不需要改動原來的網站結構,只需修改少量的 DNS 配置,就可以加速網路的響應速度。當用戶訪問了使用 CDN 服務的網站時,DNS 域名伺服器通過 CNAME 方式將最終域名請求重定向到 CDN 系統中的智慧 DNS 負載均衡系統。智慧 DNS 負載均衡系統通過一組預先定義好的策略(如內容型別、地理區域、網路負載狀況等),將當時能夠最快響應使用者的節點地址提供給使用者,使使用者可以得到快速的服務。同時,它還與分佈在不同地點的所有 CDN 節點保持通訊,蒐集各節點的健康狀態,確保不將使用者的請求分配到任何一個已經不可用的節點上。而我們的 CDN 還具有在網路擁塞和失效情況下,能擁有自適應調整路由的能力。

由於筆者對 CDN 沒有親身實踐,不便多加講解,但是各大網站都在一定程度使用到了 CDN,淘寶的前端技術演講中就提及了 CDN,可見 CDN 的威力不一般。

圖 12. 淘寶的 CDN 前端優化
圖 12. 淘寶的 CDN 前端優化

因此 CDN 也是不得不提的一項技術,國內有免費提供 CDN 服務的網站:http://www.webluker.com/,它需要您有備案的域名,感興趣的您可以去試試。