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

簡介: Web 發展的速度讓許多人歎爲觀止,層出不窮的組件、技術,只需要合理的組合、恰當的設置,就可以讓 Web 程序性能不斷飛躍。Web 的思想是通用的,它們也可以運用到 Java Web。這一系列的文章,將從各個角度,包括前端高性能、反向代理、數據庫高性能、負載均衡等等,以 Java Web 爲背景進行講述,同時用實際的工具、實際的數據來對比被優化前後的 Java Web 程序。第一部分已經講解了部分前端優化,該部分是前端性能優化的其他內容,包括 HTTP 協議的利用、動靜分離等等。合理利用這些技術將使 Web 技術更加高效。

引言

在前端優化的第一部分中,主要講解了對靜態資源的一些優化措施,包括圖片壓縮、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/,它需要您有備案的域名,感興趣的您可以去試試。

小結

本文總結了 HTTP 長連接、動靜分離、HTTP 協議等等,在您需要的時候,可以查看本文的內容,相信按照本文的方法,可以輔助您進行前端的高性能優化。筆者將繼續寫後續的部分,包括數據庫的優化、負載均衡、反向代理等。由於筆者水平有限,如有錯誤,請聯繫我批評指正。

接下來在第三部分文章中,我將介紹服務器端緩存、靜態化與僞靜態化、分佈式緩存等,並且將它們應用到 Java Web 的開發中。使用這些技術可以幫助提高 Java Web 應用程序的性能。

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章