本文采用另一種策略對動靜分離進行演示,它的大致結構如圖 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 上的測試項目結構
而我們定義了一張測試用的靜態圖片,放置在 A 的桌面 ROOT/seperate 目錄下。結構如圖 4 所示
圖 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 的結果
非常順利,完全按照我們的想法實現了動靜分離!
我們初步完成了動靜分離的配置,但是究竟動靜分離如何提高我們的程序性能我們還不得而知,我們將 Tomcat 服務器也遷移到 A 服務器上,同時將 jquery.gif 拷貝一份到 separate 項目目錄下,圖 3 的結構變為圖 6 所示。
圖 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. 長連接協商
瀏覽器在請求的頭部添加 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. 服務器打開長連接
接下來,我們將 maxKeepAliveRequests 設置為 1,并且重啟服務器,再次請求網頁后查看的結果如圖 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 前的訪問頭部情況
修改完 test.jsp 代碼后,使用鼠標激活瀏覽器地址欄,按下回車刷新頁面。這次的結果如圖 11 所示。
圖 11. 修改 test.jsp 后的首次訪問
可以看到圖 11 和圖 10 的請求報頭沒有區別,而在服務器的響應中,圖 11 增加了 Last-Modified 頭部,這個頭部告訴瀏覽器可以將此頁面緩存。
按下 F5(必須是 F5 刷新),F5 會強制 Firefox 加載服務器內容,并且發出 If-Modified-Since 頭部。得到的報頭結果如圖 12 所示 .
圖 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 前端優化
因此 CDN 也是不得不提的一項技術,國內有免費提供 CDN 服務的網站: http://www.webluker.com/ ,它需要您有備案的域名,感興趣的您可以去試試。
小結
本文總結了 HTTP 長連接、動靜分離、HTTP 協議等等,在您需要的時候,可以查看本文的內容,相信按照本文的方法,可以輔助您進行前端的高性能優化。筆者將繼續寫后續的部分,包括數據庫的優化、負載均衡、反向代理等。由于筆者水平有限,如有錯誤,請聯系我批評指正。
接下來在第三部分文章中,我將介紹服務器端緩存、靜態化與偽靜態化、分布式緩存等,并且將它們應用到 Java Web 的開發中。使用這些技術可以幫助提高 Java Web 應用程序的性能。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
