nginx+tomcat動靜分離結構

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

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

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

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

提到這,可能有您會有疑問,動態請求要先訪問 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 所示。java

清單 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 文件夾。jquery

接下來在 Tomcat 中新建 Web 項目,很簡單,咱們只爲其添加一個 test.jsp 文件,目錄結構如圖 3 所示。nginx

圖 3. B 上的測試項目結構

圖 3. B 上的測試項目結構

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

圖 4. A 上的靜態資源文件夾結構

圖 4. A 上的靜態資源文件夾結構

注意這裏的 separate 目錄名是與 B 的項目文件夾同名的web

再查看圖 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 應用程序的性能。

相關文章
相關標籤/搜索