天天看點

深入淺出Symfony2 - 如何提高網站響應速度

簡介

Symfony2是一個基于PHP語言的Web開發架構,有着開發速度快、性能高等特點。但Symfony2的學習曲線也比較陡峭,沒有經驗的初學者往往需要一些練習才能掌握其特性。相對其他架構,Symfony2比較吸引人的特點有:

  1. 支援DI(Dependency Injection,依賴注入)和IoC(Inversion of control)。
  2. 高性能。
  3. 擴充性強。
  4. 文檔成熟、擁有成熟的社群支援。

本文通過對一個基于Symfony2架構所開發的網站頁面進行逐漸優化,最終實作頁面加載速度的提高的例子,向讀者介紹Symfony2架構的一些核心功能和特點。通過閱讀本文,你可以通過一些具體的例子了解Symfony2架構的優秀特性和技術特點,進而體會到使用Symfony2架構可以為網站開發帶來的各種優勢。

适合人群

  • 本文适用于希望提高PHP語言的開發技術,或者對Symfony2架構有興趣的讀者。
  • 本文也适用于系統架構師和各類技術決策者。

1.Symfony2的運作環境的設定

在我所示範的項目中,已經包含了一個頁面,通過輸入這個位址來打開它:http://your.host.com/appdev.php/testpage_1。出現的頁面如下圖所示:

深入淺出Symfony2 - 如何提高網站響應速度

我們打開浏覽器自帶的調試功能,然後重新整理頁面:

深入淺出Symfony2 - 如何提高網站響應速度

可以看到,該頁面充斥着大量的js/css/圖檔檔案,而整個頁面的加載速度竟然達到了9.6秒。

而如果打開這個頁面:http://your.host.com/app.php/testpage1,出現的頁面如下圖所示:

深入淺出Symfony2 - 如何提高網站響應速度

我們發現頁面的加載速度變成了4秒,同時衆多js和css檔案被各自合并成為了兩個單獨的檔案(圖中紅框的部分)。

造成上面兩個頁面打開速度截然不同的原因在于:如果通過不同的入口檔案(app.php和appdev.php)進入頁面,Symfony2會根據入口檔案的不同,切換到不同的運作環境。比如在預設配置中:通過app.php通路的頁面,就是生産環境,而通過appdev.php通路的頁面,則是開發環境。Symfony2根據運作環境的不同,運作程式時的配置也會不同。比如細心的讀者可能會發現,開發環境中頁面的下方多了一條像是工具欄一樣的東西(這是Symfony2特有的開發調試欄)。環境的不同會影響Symfony2程式運作的各個環節,以下列舉了一些比較重要的不同配置下的差異處:

功能 開發環境 生産環境
----- ----- -----
開發調試欄 會出現 不會出現
日志記錄 記錄詳細的程式執行資訊 隻在程式出現錯誤的時候記錄
css/js合并 不會

是以可以看出,css/js檔案合并其實是Symfony2自動根據環境不同所開啟或關閉的一個自帶功能罷了,這個功能在Symfony2中叫做Assets管理,當然我們也可以通過控制入口檔案來實作開啟或者關閉其他更多的功能。

通過Symfony2的環境配置功能開啟或關閉各種自帶功能就像在文本裡改一個參數那麼簡單,而每個不同的環境又有一套獨立的環境配置。Symfony2提供了大量的參數供使用者友善的配置各種功能,通過對不同環境下的各個功能進行配置,可以很友善的設定出一套适合你自己的工作/生産環境。

接下來讓我們看看Assets管理子產品還能為我們做什麼。

2.深入Assets管理

通過對上述頁面的分析,我們發現雖然js和css檔案合并了,但各自的檔案内容卻沒有經過壓縮,兩個檔案的大小分别是437k和310k,這顯然是一個不太合理的數字。但我們可以通過簡單的配置,讓Assets管理子產品幫我們在合并檔案的同時對内容也進行壓縮。

例如我們選擇使用uglifyjs2對js進行壓縮,用yuicompressor對css進行壓縮。在這些軟體已經安裝完畢的情況下,隻需要修改app/config.yml的以下幾行:

assetic:
    debug:          "%kernel.debug%"
    use_controller: false
    bundles:        ['ScourgenHFS2Demo1Bundle']
    java: /usr/bin/java
    filters:
        cssrewrite: ~
        uglifyjs2:
            compress: true
            mangle: true
            bin: /opt/local/bin/uglifyjs
        yui_css:
            jar: /usr/share/yuicompressor-2.4.7.jar
      

然後在layout模闆中引入js/css的地方分别增加一個過濾器

'@ScourgenHFS2Demo1Bundle/Resources/public/css/public_home.css'
'@ScourgenHFS2Demo1Bundle/Resources/public/css/inner_city_line.css'
filter='?yui_css'
%}
<link rel="stylesheet" type="text/css" media="screen" href="{{ asset_url }}" target="_blank" rel="external nofollow"  />

...

'@ScourgenHFS2Demo1Bundle/Resources/public/js/common/title.js'
filter='?uglifyjs2'
%}         
      

我們再執行一下生成Assets的指令:

% php app/console assetic:dump  --env=prod                                                                                                                                  
Dumping all prod assets.
Debug mode is off.
03:14:06 [file+] /Users/scourgen/Desktop/InfoQ/
optimize_performance_of_pages_with_symfony2/HeadFirstSymfony2-Demo1/app/../web/css/2ff013f.css
03:14:14 [file+] /Users/scourgen/Desktop/InfoQ/
      

然後再打開剛才生産環境下的頁面,這時會發現剛才的兩個css和js檔案的大小已經變成了271k和232k,檔案内容也已經都變成了經過uglifyjs2和yuicompressor壓縮之後的内容。雖然兩個檔案大小依然很大,但如果考慮到它們在經過gzip壓縮後的檔案大小隻有86k和39k,也應該算是在合理範圍之内了。

當然在實際開發中,我們經常會碰到雖然服務端的js/css檔案内容修改了,但用戶端卻保留着舊版本的緩存,導緻頁面樣式和js功能出現問題的情況,而為了解決這個問題,同樣可以通過修改配置實作:

#app/config.yml
#将framework的templating改成如下的樣子:
…
templating:
    assets_version: 1
    assets_version_format: %%s?%%s
    engines:
        - twig
    assets_base_urls:
        http:
            - http://server1.dev
            - http://server2.dev
...
      

然後為css合并檔案指定一個檔案名:

'@ScourgenHFS2Demo1Bundle/Resources/public/css/public_home.css'
'@ScourgenHFS2Demo1Bundle/Resources/public/css/inner_city_line.css'
filter='?yui_css'
output='css/a.css'
%}
      

我們再重新整理一下頁面,看看發生了什麼。

這時剛才兩個js和css的URL分别變成了:

  • http://server1.dev/css/a.css?1
  • http://server2.dev/js/9ad140b.js?1

雖然js和css檔案的url後面都帶上了一個變量(也就是上面所定義的assets_version),而由于URL的不同,用戶端将會重新下載下傳這兩個檔案以避免從緩存中讀取舊的版本。但為什麼這兩個檔案的位址會變成serverx.dev呢?

其實這是Assets管理子產品的另外一個功能:将它所管理的檔案路徑變成絕對位址(也就是增加了上面配置檔案中的http://server1.dev和http://server2.dev兩個域名)。而且在配置了多個域名的情況下,哪個檔案名比對哪個域名是固定的,不會随機顯示造成帶寬浪費,而這其實是由它的一套算法實作的。

通過這樣的修改,我們也得到了兩個益處:

  1. 當頁面的js/css/圖檔檔案很多時,由于這些URL的域名都不一樣,可以讓浏覽器在同一時間并發下載下傳更多的檔案,進而加快頁面打開的時間(參考:浏覽器并發連接配接數)。
  2. 由于這些URL的域名和網頁所在的域名不一樣,是以HTTP頭裡不會攜帶cookie資訊,能夠減少網絡帶寬,進而實作cookie-free domain。

類似的情況也有很多,在開發中為了實作最佳實踐我們往往需要絞盡腦汁,但如果使用Symfony2作為開發架構則會使問題變得非常簡單,甚至簡單到根本不用寫代碼,隻需要更改幾行配置就能實作。

話說回來,在經過這些調整之後,前端的載入速度看起來已經不錯了。那麼Symfony2是否也可以很友善的調試和優化後端代碼?答案是肯定的。接下來我們看一下如何使用Symfony2調試和優化程式的性能。

3.調試和開發工具介紹

調試和優化程式的最基本前提就是:你得知道你的程式在幹什麼。這句話雖然說得輕巧,但在許多架構面前卻很難做到。這些架構在各種高新技術的封裝下,代碼和程式邏輯也變得十分複雜和臃腫,想要得知你所使用的架構背後到底做了哪些具體的事情、或者想擷取程式運作的堆棧和調用資訊、以及MySQL/NoSQL/MessageQueue……這些服務的調用和執行情況等等,都是不太容易的。而在沒有這些資訊支援的情況下,想去對後端程式做調試和優化幾乎是不可能的。

而擷取這些資訊以便進行開發和優化對于Symfony2來說卻非常容易,原因有三個:

  1. Symfony2有一個調試工具欄,能夠在每一個頁面打開時,在頁面的最下方顯示該頁的程式調試資訊,甚至能夠從中直接找出導緻頁面響應速度緩慢的瓶頸。Symfony2自帶的調試工具欄非常強大,下面選一個比較酷的功能給大家示範一下:
深入淺出Symfony2 - 如何提高網站響應速度

上圖是一個顯示程式執行順序以及耗時的界面,通過這個界面,開發者可以很直覺的看到程式的詳細執行流程和順序。通過這個界面,也可以看到每個步驟所占用的時間,進而發現影響程式執行速度的瓶頸,進而有針對性的進行優化。比如上圖所示的程式執行流程裡,一眼就能看出一共有三個地方執行的時間比較久(青色的條比較長)。

  1. 高效且合理的架構設計使得Symfony2架構内的每個子產品都不互相耦合,每個子產品都有自己的職責,可以單獨為其執行測試用例而不依托其他子產品。每個子產品也都會像一個獨立的軟體一樣有其自己的版本釋出周期,有的子產品甚至擁有獨立的維護團隊。這樣的設計讓Symfony2取得了複雜性和擴充性之間的平衡,也完成了架構上的解耦,進而直接在架構層面降低了整個架構的複雜度。是以對于開發者來說,不論是開發新功能,還是優化現有程式,都會覺得非常友善和高效。
  2. 日志記錄功能把架構在執行時的全過程都完完全全記錄在了日志中,你唯一需要擔心的就是及時清理日志以免磁盤空間占用過大。下圖是日志的一部分,可以看到日志所記錄的資訊是非常詳細的,讓人不禁聯想起了一些重量級Java架構的日志的輸出。
深入淺出Symfony2 - 如何提高網站響應速度

是以綜上三點所述,Symfony2對于調試和優化是非常友好的,利用其自帶功能和設計可以很友善的進行調試和優化。

Symfony2本身已經做得非常出色了,那麼在Symfony2之外呢?

雖然大家在國内可能不太聽得到Symfony2這個名字,但在國際市場上Symfony2可是非常出名的。許多IDE軟體開發商都支援Symfony2(比較出名的有PHPStorm、NetBeans和Eclipse with PDT),通過這些軟體對于Symfony2的支援,能夠讓開發者更加友善快捷的進行開發工作。

而對于開源軟體開發者,以及一些大網站的技術團隊而言,他們也圍繞Symfony2架構做了很多工作,也開源了許多他們自己的子產品。在Symfony2的托管平台Github上,Symfony2項目的fork量和star量分别為6428和2174(截止2013年4月中旬),位居所有PHP項目的前列。而在最近釋出的對2.2版本的開發統計顯示,共有2035次送出以及711次申請合并,平均每天11次送出和4次申請合并。如此頻繁的代碼變更速度也能證明Symfony2以及相關社群的活躍度。

有如此強大的開發工具和社群支援,不管開發者在碰到什麼問題時,基本上都可以迎刃而解。當然就算碰到了比較複雜的難題,也可以在Symfony2的社群或IRC頻道中詢問其他開發者。當然也可以來找我,我的聯系方式在本文最下面。

4. 如何使用緩存

對于優化程式性能來說,一般會有三個方向:

  1. 優化代碼文法
  2. 優化業務邏輯
  3. 優化架構及系統

代碼文法的優化并非本文的主題,而業務邏輯則又是由具體的網站功能所決定的,并不會有什麼放之四海皆準的辦法,是以本節主要介紹的是如何更加高效的使用Symfony2架構所附帶的功能來提高網站的響應速度。

下面介紹一下Symfony2中最重要的優化功能之一 - 頁面緩存功能。

對于一個頁面來說,經常會有多個程式塊來負責分别處理不同的頁面部分,比如上文我們所展示的這個頁面中,可能會有一塊程式來處理頁面上方的黑色導覽列:顯示所有的公交資訊及判斷使用者所在地點;一塊程式來處理頁面的中間部分:根據使用者是否登入顯示不同的内容;一塊程式來處理頁面下方的線路詳情資訊;而這個頁面的最終内容其實就是頁面樣式模闆加上這三個程式輸出後的結果。如果希望加快頁面速度,最好的辦法就是加快這三個程式的輸出結果,甚至将結果緩存起來以便今後直接調用。

那我們看看如何對頁面進行緩存,下面我将通過一個例子來向讀者展示如何做到這點。

使用頁面片段緩存,我們先要在包含其他頁面片段的代碼上增加一個standalone參數:

{% render url('layout_top', {}) with {}, {'standalone': true} %}

然後在這個處理頁面片段的方法上配置緩存資訊。

/**
 * @Route("/esi/getTop", name="layout_top")
 * @Cache(public=true,expires="+1 hour", smaxage=3600, maxage=3600)
 */
      

可以看到通過配置,我們為這個頁面片段定義了一小時的過期時間,定義緩存的參數和HTTP頭資訊中控制緩存的參數是相同的(public,expires,maxage等)。

細心的讀者會發現兩個問題:

  1. 緩存控制的參數為什麼是寫在注釋裡的?
  2. 為什麼這些參數和HTTP頭資訊中負責控制緩存的參數相同?這是一個巧合還是有意為之?

其實通過回答這兩個問題,讀者可以了解Symfony2架構的另外兩個重要功能:

  1. 支援使用注釋(Annotation)來對程式進行配置,改變其實作邏輯。
  2. 使用ESI規範作為頁面緩存的标準,緩存參數相容HTTP頭資訊中的緩存參數。

Annotation這個單詞是“注釋”的意思,在程式設計開發領域特指一種程式設計語言能夠通過注釋來改變程式的運作邏輯。對于熟悉其他語言的讀者來說,Annotation其實并不陌生,比如在Java裡就有各種Annotation,@Override和@GuardedBy等大家也都比較熟悉。Annotation對于開發者來說能夠大大的簡化程式的複雜度:把複雜的程式邏輯抽象成為參數配置。但是對于PHP來說,PHP語言其實是不支援Annotation這個功能的,于是Symfony2在其架構内部實作了Annotation:第一次執行程式時,Symfony2會自動分析處理源檔案,并将結果緩存在檔案系統中,下次程式再被執行時,Symfony2會自動執行上次生成的檔案,進而避免每次都對源檔案的注釋進行分析,而整個過程對開發者來說是完全透明的(這也能解釋為什麼有些頁面第一次打開會比較慢,但以後就會很快)。

Symfony2架構中大量使用了Annotation:從緩存的定義到路由的配置,甚至到表結構的定義,處處都使用了Annotation功能。你甚至可以根據規範編寫自己的Annotation。是以在使用Symfony2開發程式時,複雜的邏輯會變成一行行清晰的注釋,程式的流程控制将變得非常簡單。

而對于頁面緩存功能來說,Symfony2有一個子產品實作了相容ESI協定的反向代理功能,進而允許開發者使用HTTP協定來控制頁面緩存以及設定過期時間等,是以在網站規模變大的時候,開發者也可以平滑地将自帶的反向代理子產品更新成專門的反向代理服務(例如使用Varnish),進而提升網站整體性能。

對于不太熟悉ESI的讀者,我在這裡稍微做一下解釋:

ESI是通過代理服務對頁面片段進行通路的一種協定,比如你的HTML代碼中有一段ESI:

<esi:include src="/top" max-age="45"/>
      

那麼當ESI服務軟體(例如Varnish)擷取到包含這行代碼的HTML之後,會立即向"/top"這個URL做一個額外的HTTP請求,同時把這個請求的HTML傳回資料填充在這個頁面裡。假設"/top"的傳回資料是一段A标簽,那麼上文的這段HTML代碼在顯示給使用者的時候,就變成了:

<a href="http://for_example.com" target="_blank" rel="external nofollow" >for_example.com</a>
      

當然ESI的功能遠不止那麼簡單,我們在此主要用到的是它的頁面片段緩存功能。

5.資料庫操作

作為一個成熟的Web開發架構,對資料庫操作的支援自然是重中之重,Symfony2對于這塊自然也不例外。Symfony2預設使用Doctrine2作為其ORM的實作,通過Doctrine2,使用者可以像操作一個類一樣去操作資料庫,進而提高開發效率。ORM這個概念讀者應該都不陌生,在其他語言裡也有各種實作,但在使用ORM上我們經常遇到這樣的挑戰,即如何權衡資料庫性能和開發效率之間的平衡:

  • 直接寫SQL語句來操作資料庫當然效率最高,可是開發速度慢,動一個字段甚至會對整個項目都需要進行重構。
  • 使用ORM則可以加快開發速度,但往往由于ORM的封裝和限制,所生成出來的SQL無法保證運作效率。

那Doctrine2又是如何解決這些問題的呢?我們通過一個比較有特色的例子來感受一下:

$user=$this->getDoctrine()->getRepository('ScourgenHFS2Demo1Bundle:User')->find(2)
echo $user->getName();
      

在上面這段代碼中,第一行雖然看起來是向User表去查詢ID為2的使用者,但其實還沒有任何SQL語句在資料庫上執行,$user就是一個上文所提到的代理對象。而當在第二行中,當這個對象的屬性第一次被調用的時候,真實的SQL語句才會被傳遞到伺服器,并且将結果集中的name字段傳回給echo函數。

造成以上現象的原因是Doctrine2支援延遲加載功能:當程式執行對資料庫的操作時,比如擷取一條條資料,傳回的對象并不是一個真實的來自資料庫的結果集,而是一個代理對象(Proxy Object),而隻有當資料被真正調用的時候,這個代理對象才會去資料庫裡進行查找,并傳回真實的資料。

是以使用延遲加載的好處是:ORM會根據真正需要的内容去擷取相應的資料。

想象一下如果你的表結構非常複雜,而且前端頁面經常改變,在這種情況下一般都需要對SQL操作進行一定的修改和重構才能滿足不斷變化的需求,而且很難保持性能的最優化。而如果此時有延遲加載功能,就能夠保證不管頁面如何變化,資料庫操作相關的代碼都可以在不需要修改的前提下,一直生成最優化的SQL語句去擷取那些真正被使用到的資料。是以延遲加載才能夠一方面顯著的加快開發速度,一方面優化頁面性能。

當然除此之外Doctrine2的功能還有很多,包括批量處理、對象生命周期管理、表結構自動維護等等,Doctrine2也可以作為一個單獨的類庫被Symfony2之外的程式所使用。但在這裡由于主題和篇幅的限制,我不做過多的介紹,如果讀者們對此感興趣的話,希望以後能夠有機會單獨寫文介紹。

6.總結

本文向大家展示了Symfony2的一些很酷的功能,從Symfony2的環境設定,到Assets管理,到開發調試欄的介紹,再到緩存優化的分析……讀者可以通過上面的幾個例子感受到使用Symfony2作為架構進行開發并不是一件很複雜的事情:改幾行配置,甚至都不需要寫代碼,就可以使用業界的各種“最佳實踐”來解決問題。就讓架構做應該做的事情吧,分工明确才能讓開發者能夠安心的把注意力集中在自己項目的業務邏輯上,進而提高項目的速度和品質。

而在國内,由于架構輩出,甚至有幾年開發經驗的工程師甚至都會自己做一個架構,社群和業界也缺少一個公認的答案和方向:工程師都在打一槍換一個地方,而各大公司和團隊都在重複制造各種不同樣子的輪子,整個業界的風氣也變得異常浮躁,以至于許多人索性放棄了對完美的追求,直接承認了他們做的事情就是“quick and dirty“的。

我并不指望能夠改變你的想法,但如果你也有一些相同的感悟,那麼請嘗試一下Symfony2,它将幫助你重新找回信心。

繼續閱讀