從一個到一百萬個用戶 - 淺談系統擴展的方法
阿恆
編寫網頁應用是一個進入門檻低的行業,只要隨便翻一、兩本書便可以學得有關的知識,架設一個基本的網站也很便宜,或許你早已有了自己的網站,甚至不只一個呢。隨著網站的知名度上升,訪客人數和數據流量也逐漸上升,真是值得高興。但是漸漸你發覺網站的反應速度變得緩慢,系統逐漸負荷不來,很多人開發人員這時開始手足無措,應該如何擴展系統呢?通常這些問題會落到一些擁有花哨頭銜例如系統架構師、高級架構師等等的人手中,唉,這些問題實在太難了,即使想一想它有多難也令你頭疼不已。
其實對這個問題有粗略的認識,對開發人員的職業前途不無好處,也使你在開發過程要進行一些取捨決定時,考慮得更全面。以下是 Joël Perras 一篇有關系統擴展策略的介紹。
用錢解決問題
開始的時候你可能覺得親手維護各個不同的伺服器和所需的系統很酷,也很有趣,不過蜜月期很快過去,告訴你一個鐵一般的事實:你每花一分鐘擺弄你的伺服器,便少一分鐘改善你的應用程式,使你的用戶失去更直接得益的機會。
通常情況下最簡單的(有時是最符合成本效益)的解決方案是用錢解決問題,近年隨著 platform as a service (PaaS) 越來越普遍,一些如 Heroku、 Engine Yard 等服務供應商,甚至是對應特定編程語言的 Gondor.io 等等,使大部分中、小型開發公司無須雇用全職的系統維護人員。
從小開始
你很可能像世上 99% 的網站一樣,開始的時候只有一台主機,網站伺服器、應用程式、數據庫通通都在裏面,你甚至可能只是從某種形式的共享伺服器上開始。這沒有甚麼對與錯、大與小的問題,絕大部分人都經歷過這個階段。
過了一段時間,單一台主機不能跟上應用程序的需求,例如你的數據庫變得太大,你的系統受制於記憶體的數量或數據流量的限制(視乎你的應用程序),或者你不能夠實現預定的反應時間或吞吐量的目標。這些問題可能會使你十分緊張,不過這類規模上的問題基本上都是好的問題,大多數應用程序達到幾百個用戶便開始遇到這些問題。
這時最合適的策略是盡量不要變更你的程序、系統和應用程序的架構,在大多數情況下,你只需要換一部更強大的主機。如果你使用一些虛擬硬體服務,例如 Linode 或 Amazon Web Services,那麼你要做的就是相關的控制台網頁上點擊幾下,然後等幾分鐘的停機時間便可以了。停機聽起來很嚇人,其實只要處理得當,一般用戶是可以接受的。
透過虛擬硬體供應商,只要你小心選擇各種記憶體、處理器和硬盤配置,便可以加強伺服器的效能,這稱為垂直擴展 (vertical scaling),這個非常簡單的方法可以給你好一段高枕無休的日子。
必要的複雜性
垂直擴展所帶來的效益始終有極限,如果你的用戶群的持續增長,接下來的步驟是由根據系統的組件和服務,把架構拆散。
最明顯的選擇是把網站伺服器和數據庫分家,這樣做會使架構變得複雜,內在的網絡負荷也會增加,不過,你卻可以根據各個系統的效能特點和要求,獨立地進行擴展。
一旦數據庫和網站伺服器分了家,下一階段是提高這個組合的可用性。能夠為更多用戶提供服務固然值得高興,但如果你的網站無論任何原因而出現故障,便不會再有人關心你的平均反應時間或吞吐量,所以最重要是你的應用程序要無時無刻都在運作。
首先你對現時的系統架構進行「單一失效點」分析 (single point of failure),你可以嘗試以下的簡單方法:如果任何特定的伺服器變得不可用,應用程序是否能繼續運作呢?不能的話,你已經找到了一個單一失效點。
我們目前的架構有兩個非常明顯而分離的單一失效點:數據庫伺服器和網站伺服器。任何一個失效或離線,我們的應用程序便不可能正常運行。
讀到這裏,若果你不熟悉無共享 (shared-nothing) 架構的概念,請到網絡上搜尋一下,弄明白後再繼續看下去。
現在的情況下,最容易處理的單一失效點是網站伺服器,只要你不存儲任何用戶的狀態資訊在網站伺服器上 (把 session 資訊儲存在檔案中是其中一個典型的毛病),添加額外的網站伺服器便可以解決問題。
下一個你要解決的單一失效點是 DNS A record,它負責把你的域名 (例如 example.com) 翻譯為 IP 地址。(為了簡單起見,這裏假設你沒有管理自己的 DNS,若非如此,你可能不需要下面的解決方案。)
一種解決方案是一個支援 HTTP 的負載平衡器 (load balancer),例如 HAProxy、nginx、Amazon Web Services Elastic Load Balancer,或許多其他類似的服務。一個正確配置的負載平衡器可以讓你根據一組規則分配送進伺服器的請求,從而消除了單一失效點,你可以隨時添加或刪除網站伺服器,只需要確保有足夠的活躍的伺服器,讓你的應用程序能夠及時處理用戶的請求。
現在,你的網站伺服器不再是單一失效點,即使有一台或多台網站伺服器失效,只要剩餘的伺服器足以應付正常的效能需求,我們的應用程序將繼續服務用戶。
下一個明顯的的單一失效點是你的數據庫,這個問題比較棘手。如果你足夠聰明,也負擔得起的話,你可以把它轉為別人的問題,一些如 Amazon’s Relational Database Service 的服務可以令你無須再為管理自己的關連式數據庫 (relational database) 而頭疼。如果你願意冒險和嘗試新事物,有大量非關連式數據庫技術如 MongoDB、Riak 和 CouchDB,為了提供更高的可用性,它們進行了各種權衡。
小東西大分別
在瀏覽器打開你最喜歡的網頁應用程序,然後打開網絡流量檢查器控制台 (或你的瀏覽器上相似的功能),你很可能發現有幾十個 (甚至是數百個) 索取外部樣式表、JavaScript、圖像和字體的要求,甚至可能有些在載入頁面後啟動的 Ajax 請求。
假設所有這些文件都已被正確地縮小、壓縮,並使用所有可以用的著的 HTTP 標頭來緩存,但是網站伺服器仍然需要回應每一個請求,這可以是一個不能忽視的負擔。一個非常簡單而有效的解決方案是將這些東西放在其他地方,這可以是一組專門提供靜態內容的伺服器,也可以是一些外部服務,例如 Amazon S3。這個方法有一些細節需要注意,但在大部分的情況下它的確是一個不錯的主意,你可以把伺服器省下來的效能投入到更有用的地方。
再進一步
你的任務還遠遠沒有完成。現在你可能有一個堅實而可用性甚高的平台來運行你的網頁應用,但是工作永遠沒有完,下面是一些值得考慮的事項:
- 如果用戶可以搜索網站上的內容,強烈建議你把儲存資料和處理搜尋的工作移到一個單獨的伺服器,有興趣的話可以查看一下 Solr 和 Elasticsearch。
- 把需要很多資源產生 (也稱為「昂貴」) 的頁面緩存起來有時是必要的惡魔,也是一柄雙刃劍,在你使用 Memcached 或 Redis 之前,應該先熟習一下讀取和寫入高速緩存策略。
- 當遇上一些昂貴卻不能緩存的頁面,例如 Twitter 或 Facebook 上的好友列表或其他來自第三方的資訊,最好的解決方法是異步作業隊列,任何不需要立即被計算或處理的工作,都應該被移動到一個異步作業隊列。網上有不少很好的方案供你選擇,例如 Celery、Amazon Simple Queue Service、RabbitMQ,甚至 Redis 和 MongoDB。
還沒走到終點
對於一個現代網頁應用來說,這篇文章所述的只是極少的一部分,有很多東西被省略掉,有很多細節沒有解說清楚,還有更多過分簡化了的內容,但是請不要怕!網頁應用比桌面應用優勝之一,是你可以逐小逐小改善它的功能和效能,期間對用戶全然沒有影響,本文所述的策略不必一次全部實施,一步一步來就好了。
你對這篇文章有甚麼意見?你有沒有相關的經驗想分享?歡迎留言。