科技創業在許多人的印象中講究快捷、靈活和精簡的工作環境和開發模式;因此在思考科技新創的需求時,我們常常忽略了創業很重要的一環:Scale-up 以及最佳化。科技新創公司的創辦人以及團隊一般來說較其他產業年輕,不管是產品設計、專案管理還是工程,新創公司團隊(尤其是第一次創業的團隊)都偏資淺,十之八九都沒有開發高流量、高負載產品的經驗。
沒有最佳化的經驗,那碰到網站、App 流量大增怎麼辦?
通常有點規模的新創公司會請所謂的 DevOps 工程師 (Development Operations Engineer),而 DevOps 的工作就是在於配置和建置公司的軟體架構。而在大型公司中,DevOps 通常又會再將資料庫管理、作業系統管理、程式碼管理等職務再細分。
別慌,其實網路軟體架構最佳化基本功並不難上手,讓我們來聊聊。
下藥前先把脈
網頁(或 App)慢原因有很多種:有可能是多媒體內容體積太大、可能是外部 API 回應時間過長、可能是資料庫搜尋時間太長,當然也有可能是網頁伺服器超載無法支撐流量。
因此在做任何動作前,最好是先為你的軟體架構把把脈,看到底瓶頸是在客戶端還是在伺服端。以下小弟會先討論各種前端和後端的把脈工具,然後再根據每種工具偵測的問題討論最佳化技巧。
首先,如果你的軟體架構是以網頁為主,第一步我會建議你先試試用 Google PageSpeed Insights 去偵測你的網站使用的前端資源的載入速度和效率。
Google PageSpeed 偵測的前端載入效率問題大致上可以分為幾類:
如果這些基本問題修正後,網頁的載入速度應該會有基本的提升。
接著,第二步小弟會建議個人看官使用 Chrome 瀏覽器的除錯 Console(註:開啟 Chrome 以後按下 F12 鍵即可開啟)來測量資源的載入速度與效率。
相較起 PageSpeed,Chrome Console 偵測的問題是屬於比較全面性的。前者專注於瀏覽器載入資源的最佳化,後者則是可幫助我們將網頁的整體載入速度(資料庫、伺服器端 Script、網路傳輸、瀏覽器載入)視覺化。
上圖是 Youtube 首頁的載入圖,我們可以清楚看到資源的載入順序、載入方式(序列還是平行)、資源的載入耗時以及整體載入時間。這些資訊可以幫助我們了解如何將資源分散至不同的伺服器或合併成為單一檔案來增進載入效率。
第三步,我們可以透過載量測試(Load Testing)來探討網路和伺服器端的瓶頸。通常載量測試會告訴我們目前的軟體架構整體而言可以服務多少使用者。筆者個人喜歡 Blitz.io。由於載量測試的原理就是透過許多用戶端同時嘗試連接伺服主機(其實跟 DDOS攻擊的原理相同),這種測試方法除了通知我們伺服主機是否能夠服務使用者外,並沒有辦法告訴我們到底瓶頸是出在路由器、資料庫、網頁伺服器還是其他資源主機。
因此,第四步我們可以透過如 NewRelic 的伺服器監管服務來測量我們的網頁伺服器、資料庫主機等的回應速度和 CPU、記憶體、硬碟使用狀況。
接下來,小弟會將問題分為前端和後端兩大塊進行最佳化技巧的討論。
前端(瀏覽器端)的最佳化技巧
在思考網頁載入的時候,我們要先思考網頁的載入順序以及網路連線的開關成本。
當你在瀏覽器中輸入網址,瀏覽器下載完網頁原始碼後會逐一下載網頁中使用的圖片、樣式檔(CSS檔)、Script 檔、影片等。由於現代網頁使用的外部資源通常不下數十個檔案,若逐一下載一定會花上不少時間,若能夠把資源用平行的方式下載,將能夠節省很多時間。
由於瀏覽器和伺服器連線需要時間和多次傳輸對話才能夠達成(註:有興趣者可以閱讀 TCP 的連線方式),因此檔案在同一伺服器上,瀏覽器會維持根伺服器的連線,等到所有同伺服器上的檔案都下載完畢後才會關閉連線。
了解了瀏覽器和網路連線的以上基本概念,我們可以討論 PageSpeed 幫我們抓出的一些問題。
圖片最佳化(圖片整合)
網頁上的圖片除了大小以外,就是類型:PNG 是不失真壓縮格式,JPEG 是失真壓縮格式。因此,圖片最佳化前置作業就是將相片等可容忍失真的圖片用 JPEG 壓縮來減少檔案大小。
圖片最佳化的概念其實跟前面提到的瀏覽器與網路連線概念息息相關。瀏覽器下載多個圖檔若不是開啟多個連線不然就是需要維持連線以完成多個下載作業。若你將你使用的圖片(如按鈕圖片)全部合併成為單一圖檔,如下:
然後利用 CSS 的位移(offset)來顯示個別圖檔,那瀏覽器只需要下載一個檔案就可以完成載入圖片,可以有效地縮小載入序列,有時甚至可以將所有圖片的載入時間縮減數倍。
這種最佳化技巧稱為圖片合併(Image Spriting)。
網域名切割(平行化)
先前提到的圖片合併技巧可以有效減少圖片的數量,但這樣不是讓下載的圖片體積變大嗎?那如果合併之後還是有數個圖檔,有沒有辦法繼續最佳化?
當然有!有種作法就是利用網域名切割(Domain Sharding)來增進平行載入的效果。
假設你有十個大小相當的圖檔,在目前的情況各需要五十微秒才能完成載入。而跟伺服器連線需要一百微秒。那若圖檔都在同一伺服器上(假設伺服器限制客戶端一次只能開通一條線),那你總共需要六百微秒才能夠完成下載所有圖檔。若你將十個圖檔合併成兩個各需兩百微秒下載的圖檔,那在同一伺服器上你只需要五百微秒就可以載入,你成功地減少了一百微秒的載入時間,也就是增加了圖片 100/600 = 0.16666… = 16.66% 的載入效率。
有趣的地方在這邊:若你的伺服器名稱為 testserver.com,而你去設定一新網域名 testserver2.com 或是設定 CNAME web1.testserver.com,即使這些網域名通通都指向同一台伺服器,瀏覽器還是會將他們認作不同的伺服端而各自開啟新連線。
因此,若你的兩個合併後的圖檔其中一個被搬到第二個網域名,現在瀏覽器會開啟兩條不同的連線,故能夠(理論上)平行下載兩個合併圖檔,因此下載的時間被縮短到 100 + 200 = 300微秒而已。你因此改善了載入效率 300 / 600 = 0.5 = 50%!
而在網域名上動手腳的作法,可以用在任何檔案上來增加載入速度。
但講到這邊值得注意的一點:開啟連線"理論上"可以將載入時間砍半,但是開啟新連線也意味著瀏覽器必須吃更多的 CPU 時間和記憶體,因此實際的效益往往不如預期。通常網域名切割的做法在分成兩、三個網域名後效果就會打折扣。
故此:究竟是要將小檔案用網域名切割平行化,還要要將檔案合併起來減少連線數量?這是沒有標準答案的。你必須視你網站的檔案大小和連線情況去使用不同最佳化技巧來達到最大效益。
Javascript 與 CSS 最小化
檔案最小化(Minification)的作法可能很多人都已知道,因此小弟在此簡單複習以免贅述。現在不管你是用甚麼開發環境,都有很多現成的minifier可以使用。
Javascript 和 CSS 最小化的原理很簡單,假設你有以下 Javascript 程式碼:
var num1 = 5;
var num2 = 10;
var num3 = num1 + num2;
alert(num3);
以上總共62字元,其功能上與以下程式碼是相同的:
var a=5;var b=10;var c=a+b;alert(c);
但是第二段程式碼卻只有36字元,也就是減少了將近42%的大小!
因此,若利用 minifier 去把開發中的程式碼轉成最小化的 Javascript 和 CSS 後,可以大大減少網頁資源載入所需的時間。
合併 Javascript 與 CSS
合併程式碼的概念跟合併圖檔一樣,只是為了減少連線和檔案的數量。合併程式碼通常用動態的方式在執行時期將已經最小化的檔案合併起來,然後加入伺服端的快取。
打個比方:若你的網站上有 a, b, c, d 四個已經最小化的 Javascript 檔案,第一張網頁使用 a, b,第二張網頁使用 c, d,第三網頁使用 a,b,c,第四網頁使用 b,d等。因為使用的組合不一樣,你沒有辦法事先將所有的檔案合併起來,否則會造成許多網頁浪費頻寬和 CPU 時間載入額外的資源。
故此,你可以使用一伺服器端的 script 去動態將檔案組合起來,比如說:
testdomain.com/combine=a,b
testdomain.com/combine=c,d
testdomain.com/combine=a,b,c
testdomain.com/combine=b,d
這支 script 會將組合過的檔案放進快取,如此能夠進一步減少下載的檔案數量。
順帶一提,使用快取時記得要設定版本管理,才不會造成更新程式碼後伺服器還繼續使用舊的合併程式碼的窘境。
內容傳輸壓縮
你如果用 Chrome Console 去分析一網頁,在 Network 頁下選取網頁本身,再選取 Header,即可了解該網頁所在之伺服器是否有開啟內容壓縮功能,請見下圖:
內容壓縮原理跟電腦上的檔案壓縮一樣,基本上就是瀏覽器若連上伺服器時在標頭中加入 gzip 壓縮的選項,那伺服器就會先將網頁檔壓縮後再傳到客戶端以節省頻寬。
若 Chrome Console 沒有顯示 gzip 字樣,你可以參考這篇文章來在伺服器端開啟壓縮功能。
在網頁上你可以用:
<meta http-equiv=”Cache-Control” content=”no-store” />
來關閉快取功能,小弟我會建議你設定快取時間以避免重複下載鮮少更新的內容。請參考這篇文章來設定瀏覽器快取。
後端(伺服器端)的最佳化技巧
在最佳化瀏覽器載入和網路傳輸效率後,當使用者不斷增加,最後伺服端還是會發生超載的情形。
一般而言,大部分的新創公司一開始的軟體架構都不會太複雜,大致上應該分為:負載平衡管理、網頁伺服器(Web Server)和資料儲存三大塊。而絕大部分的時候,使用者連接伺服端都會先碰上負載平衡器(Load Balancer),而平衡器會根據背後的伺服器的CPU、網路流量或其他資源使用量來決定要由哪一台伺服器來服務該使用者,以達到平衡流量的目的。
可惜的是,雖然大部分小公司的軟體架構中有多台網頁伺服器在負載平衡器後面執行各種程式碼,但是存取資料卻還是連接到單一資料庫主機。因此將資料庫(與資料儲存媒介)最佳化對新創公司來講常常比增加程式執行效率更重要。
選取資料庫
相較起第一次達康泡沫前後,今天的新創公司可選擇的資料庫多了很多種。過去資料庫多以 SQL 為主,但自從 MongoDB、CouchDB、Cassandra 等 NoSQL(就是"不是 SQL"的意思)資料庫在2010後成熟且大流行,資料儲存的選擇成了一非常重要的議題。
SQL 由於是採取樹狀式目錄查詢,當資料量一大,增加、查詢、合併資料等作業耗時都會成比例增加。
而 NoSQL 呢?目前 NoSQL 資料庫大致上分為以下幾種:
- Key-Value Store,字典式查詢資料庫,理論上可保證用鑰碼即時查詢數值
- Document Store,文件式資料庫,理論上可以即時調閱文件(但一般來說不能查詢、分析或合併文件內容)
- Graph,圖式資料庫(節點、鍵結),理論上可以即時尋找互相連結的節點
(其他 NoSQL 資料庫類型可參考這裡)
一般而言,SQL 的資料格式和檢索方式雖然較 NoSQL 更有彈性,但資料量大時儲存和檢索的彈性反而成了效能瓶頸。NoSQL 基本上就是將儲存和檢索功能精簡,使檢索效能(理論上)不受資料量影響。
SQL 資料目錄(Indexing)
由於 SQL 還是資料儲存主流,在此小弟還是針對SQL最佳化稍微講解一下。
從資料結構的角度來看,絕大多數的 SQL 資料庫都是以 B-Tree 的方式儲存。B-tree 簡單來說是二元樹的概化版本,其好處就是可以相對快速(註:logN 時間內)地增加、檢索和刪除資料,也可以快速進行所有結果的排序。相對於一般的 Hash Table,B-tree 的功能更彈性,但是同時也犧牲了一部份的效能(註:Hash table 理論上能在固定時間內完成增加、刪除、檢索而不受資料量影響,但 Hash Table 無法快速進行排序)。
由此可見,B-tree 要達到相對快速地檢索,其中資料必須以使用者搜尋的欄位來組織搜尋用的樹狀圖,B-tree 用來整理資料的欄位就是所謂的 Clustered Index。但今天假設你的使用者資料中可能有電子郵件、有姓名、有 Twitter 帳號等資料,總不可能只用單一欄位尋找使用者吧?
為了不讓不是 Clustered Index 的欄位變成線性查詢(註:檢索效率與資料量成反比),你必須適時根據資料庫中用來檢索的欄位增加所謂的 Non-clustered Index 來大幅提升檢索效率。由於 Non-clustered Index 的結構並非資料的真實結構,每增加一Non-clustered Index 都會使用更多的儲存空間,也會增加新資料的作業時間。意思就是不是每個欄位都加上 Index 就萬事 OK 了啦!要適可而止。
好在的是,許多 SQL 資料庫(尤其是企業用資料庫)都有所謂的查詢效率分析工具(Query Performance Analyzer) ,只要你把你的 SQL 查詢語法全部打進去,該工具就會告訴你,你現在資料庫有那些檢索用欄位沒有對應的 Index。
完成加入 Non-clustered Index 後,你的資料庫已經完成了第一步最佳化。
用硬碟陣列提升資料庫效率
有自己組裝過電腦或是做過 IT 的朋友可能對磁碟陣列(RAID)不陌生。然而,由於雲端太方便,很多人可能都忘記了雲端的硬碟服務是虛擬化的,其讀寫效能很不穩定。因此不管你今天是要架 SQL 還是 NoSQL 資料庫,為了確保資料庫的讀寫速率,你可以使用RAID 10(1+0)來合併多個雲端硬碟來提升和穩定讀寫速率。RAID 1(理論上)硬碟數量和讀寫效能是呈正比的。
SQL 資料庫檔和 Log 檔分開放
SQL 資料庫最佳化一小撇步就是當 SQL 內的資料更新時,SQL 除了修改資料庫檔外,還會(理論上同時)修改 Log 檔。因此,若將資料庫檔和 Log 檔放在兩個不同的硬碟,即可提升資料庫的寫入速度。
SQL 資料庫複製(Replication)與鏡象(Mirroring)
除卻一些 NewSQL 和平台雲 SQL 資料庫(如微軟的 SQL Azure)能夠完全採取分散式的方式建置外,一般來說 SQL 資料庫的讀寫還是以伺服器主機為單位。這意味著當單一主機的流量過大(而你已經無法再增加更多處理器和記憶體時),你必須要讓多台主機能夠同時服務使用者。與網頁伺服器一樣,多台資料庫主機也能夠放置於負載平衡器後來分攤資料流量。
一般來說 SQL 有兩種方式能夠將資料讀取分攤給多台主機:複製與鏡象。
複製(Replication)基本上就是把一台資料庫主機的內容發布給多台備用資料庫主機,而透過負載平衡後,使用者可以從任何一台資料庫主機上讀取資料。
鏡象(Mirroring)嚴格上來講並不是用來分攤資料量,而是將資料庫主機上的資料備份到第二台主機上,以便第一台主機當機時上線接手。
由於複製會創造多個資料庫主機實體,此技巧有助於分攤流量,但是卻無法對資料庫內的資料進行分散管理。因此,當資料庫體積過大時,各主機實體仍可能出現效能問題。
另外值得注意的一點就是使用複製和鏡象時,主機實體之間同步多少都會有些許延遲。故此,兩技巧並不適合分散寫入流量,而只適用於分散讀取流量。
SQL資料庫切割(Database Sharding)
先前提到了當資料庫的讀取流量太大可以使用多個資料庫主機來分攤流量,但若今天資料庫體積過大,各自的主機實體還是會出現效能問題。
此時,我們可以利用資料庫切割(Sharding)的技巧將資料庫切割成為數個小資料庫。資料庫切割的概念就是將資料表依照欄位數值去分割,比如說一欄位含有數字1~20,我們可能將該欄位含有1~5、6~10、11~15和16~20的資料列分割成為四個不同的小資料表,這樣一來對使用者來說雖然還是一完整的資料表,其中的資料(理論上最佳狀況)已分割成四個體積只有原先四分之一的資料表。
此資料庫最佳化技巧除了有助於減少單一資料庫主機的資料量,更可以減少單一資料庫主機主機和資料區塊所分配到的讀寫流量。但是,資料庫分割由於必須將多個資料庫主機的資料表串聯起來,這代表資料表檢索的延遲時間增長。除此外,由於一資料庫分為多台主機管理,若其中一台主機當機離線,就會造成資料庫功能受損。
因此,究竟資料庫主機應該要複製還是要切割(有些 SQL 資料庫支援切割後再複製或鏡象),都要看資料庫其中的資料量、讀流量、寫流量等因素,才能依照使用需求找到最合適的最佳化組合。
效能監控
最後,最重要的最佳化工作就是要長期監控所有伺服器和瀏覽器端的效能,才能在問題發生前(或問題發生時)在最快的時間反應。
說到監控伺服器,小弟個人偏愛 New Relic:
如上圖所示,New Relic 可以幫助你分析使用者進入網頁時所耗費的時間之間到底多少是花在瀏覽器端的文件處理、圖像顯示,又有多少時間花在網路傳輸和伺服器端的應用程式執行。除此外還可以分析資料庫各檢索語法的效能。相較起很多他牌的監控平台,New Relic 在資訊整合上可圈可點,很適合新創團隊。
結語
前面提到這麼多最佳化技巧,其實最佳化工作有如科學實驗,必須根據自己公司的軟體架構和使用者習慣進行測量、測試後再檢討,並沒有絕對的"必勝秘笈"。比如說影音串流公司的瓶頸可能是網路頻寬和內容伺服器、語音辨識公司的瓶頸可能是 CPU,而數位行銷公司可能是資料庫的效能和多樣化。
在對於網路軟體架構最佳化技巧有了基本的認識後,多數新創公司應該能夠在不擴編人事的情況下解決大部分的效能問題。未來可等到公司業務成長可負擔專職的 DevOps 和資料庫工程師後再投入更多資源和時間去深入研究。