如何為從1到10萬用戶的應用程序,設計不同的擴展方案?
對于創業公司來說,有用戶注冊是好事情,但是當用戶從零擴展到成千上萬之后,Web 應用程序又該如何支持呢?
通常來說,這種情況的解決方案要么是來自突然爆發的緊急事件,要么是系統出現瓶頸進行升級改造。雖然方式不同,但是我們也發現了,一個邊緣項目發展成高度可擴展項目,其升級方案是有一些普適的“公式”可以套用,本文以 Graminsta 為例,為大家介紹當用戶從 1 位發展到 10 萬,應用程序如何擴展?
1. 1 位用戶:1 臺機器
無論是網站還是移動應用,應用程序幾乎都包括這三個關鍵組件:API、數據庫和客戶端,其中數據庫用來存儲持久數據,API 服務于數據及與其有關的請求,而客戶端負責將數據呈現給用戶。
在現代應用程序開發中,客戶端往往會被視為一個獨立于 API 的實體,這樣一來就可以更輕松地擴展應用程序了。
當剛開始構建應用程序時,可以讓這三個組件都運行在一個服務器上,類似于我們的開發環境,一位工程師在同一臺計算機上運行數據庫、API 和客戶端。
當然,理論上我們可以把它部署到云上的單個 DigitalOcean Droplet 或 AWS EC2 實例上,如下所示:
但是,當我們的用戶未來不止 1 個的時候,其實剛開始就應該考慮是否要將數據層拆分出來。
2. 10 個用戶:拆分數據層
拆分數據層,并將其作為一個類似于 Amazon 的 RDS 或 Digital Ocean 的托管數據庫的托管服務。這樣做的話,雖然成本會比在一臺機器上或 EC2 實例上自托管高一些,但是我們可以獲得很多現成且方便的東西,例如多區域冗余、只讀副本、自動備份等等。
Graminsta 現在的系統如下所示:
3. 100 個用戶:拆分客戶端
當網站流量變得穩定之后,就到了拆分客戶端的時候了。
需要注意的是,拆分實體是構建可擴展應用程序的關鍵所在。當系統中的某一部分獲得了更多流量,那么就應該把它拆分出來,根據其自身的特定流量模式來處理服務的擴展。這也是我會把客戶端和 API 看作是相互獨立的組件的原因,這樣,我們就可以輕松為多平臺構建產品,例如 web、移動 web、iOS、Android、桌面應用、第三方服務等,它們都是使用相同 API 的客戶端。
現在,Graminsta 的系統如下所示:
4. 1000 個用戶:負載均衡器
當新用戶越來越多,如果只有一個 API 實例可能滿意滿足所有的流量,這時我們需要更多的計算能力。
這時,負載均衡器該上場了,我們在 API 前面添加一個負載均衡器,它會把流量路由到該服務的一個實例上,我們就可以進行水平擴展(通過添加更多運行相同代碼的服務器來增加可以處理的請求數量)。
我們在 web 端和 API 前面添加了一個獨立的負載均衡器,這意味著我們擁有了多個運行 API 和 web 客戶端代碼的實例。該負載均衡器會把請求路由到任何一個流量最小的實例上。并且,我們還可以從中得到冗余,當一個實例宕機(過載或崩潰)時,其他實例還可以繼續運行,響應傳入的請求,而不是整個系統宕機。
負載均衡器還支持自動擴展,在流量高峰時可以增加實例的數量,當流量低谷時,減少實例數量。借助負載均衡器,API 層實際上可以無限擴展,如果請求增加,我們只需要不斷增加實例就可以了。
編者注:到目前為止,我們擁有的產品和 PaaS 公司(如 Heroku 或 AWS 的 Elastic Beanstalk)提供的開箱即用產品非常類似。Heroku 把數據庫托管在單獨的主機上,用自動擴展來管理負載均衡器,并允許我們把 API 和 web 客戶端分開托管。對于早期初創企業來說,使用 Heroku 等服務來做項目是一個不錯的選擇,所有必需的、基本的東西都是開箱即用。
5. 10000 個用戶:CDN
對于 Graminsta 來說,處理和上傳圖像為服務器帶來了很大的負擔。所以,Graminsta 選擇了使用云存儲服務來托管靜態內容,例如圖像、視頻等(AWS 的 S3 或 Digital Ocean 的 Spaces),而 API 應該避免圖像處理和圖像等業務。
另外,使用云存儲服務,我們還可以使用 CDN,可以在遍布全球不同的數據中心自動緩存圖像。我們的主數據中心可能托管在
我們從云存儲服務得到的另一樣東西是 CDN(在 AWS,這是一個被稱為 Cloudfront 的插件,但是很多云存儲服務都以開箱即用的方式提供它)。CDN 將在遍布全球不同的數據中心自動緩存我們的圖像。
雖然我們的主數據中心可能托管在俄亥俄州,如果有人在日本對圖像發出了請求,那么云供應商就會進行復制,將其存儲在位于日本的數據中心,下一個請求該圖像的日本用戶就會很快收到圖像。
6. 10 萬個用戶:擴展數據層
負載均衡器在環境中添加了 10 個 API 實例,使得 API 的 CPU 和內存消耗都很低,CDN 幫助我們解決了世界各地圖像請求的問題。但是現在,我們有一個問題需要解決,那就是請求延遲。
通過研究,我們發現數據庫 CPU 的消耗占比達到了 80%-90%,因此擴展數據層成為了當務之急。數據層的擴展是一件很棘手的事情,雖然對于服務無狀態請求的 API 服務器來說,只需要添加更多實例即可,但是對于大多數數據庫系統來說,卻不是這樣。
緩存
要從數據庫獲得更多信息的最簡單方法之一是給系統引入一個新的組件:緩存層。實現緩存最常用的方法是使用內存中的鍵值存儲(如 Redis 或 Memcached),且大多數云廠商都會提供數據庫服務的托管版本。
當該服務正在進行對數據庫相同信息的大量重復調用時,就是緩存大顯身手的時候了。當我們訪問數據庫一次時,緩存就會保存信息,之后再進行相同請求時,就不必再訪問數據庫了。
例如,如果有人想在 Graminsta 中訪問 Mavid Mobrick 的個人資料頁面時,我們把從數據庫中得到的結果,緩存在 Redis 中關鍵字 user:id 下,到期時間為 30 秒。之后,每當有人訪問 Mavid Mobrick 的個人資料時,我們會首先查看 Redis,如果存在相關資料,那就直接從 Redis 提供數據。
大多數緩存服務的另一個優點是,與數據庫相比,更容易擴展。Redis 有個內建的 Redis 集群(Redis Cluster)模式,用的是跟負載均衡器類似的方式,可以把我們的 Redis 緩存分布到多臺機器上 。
所有高度擴展的應用程序幾乎都充分利用了緩存的優勢,緩存是構建快速 API 不可或缺的部分,可以提供更好的查詢和更高效的代碼,如果沒有緩存,我們可能很難擴展到數百萬用戶的規模。
只讀副本
由于對數據庫的訪問相當多,因此我們需要在數據庫管理系統來添加只讀副本。借助上面提到的托管服務,只需要點擊一下就可以完成。只讀副本將和主數據庫保持一致,并且能夠用于 SELECT 語句。
7. 未來展望
隨著應用的不斷擴展,我們會把重點放在拆分獨立擴展的服務。例如,如果我們使用了 websockets,那么會把 websockets 處理代碼抽取出來,放在新的實例上,同時安裝負載均衡器。該負載均衡器可以根據 websocket 連接打開或關閉的數量來上下擴展,與我們收到的 HTTP 請求數量無關。
如果未來還會遇到數據層的限制,我們就會對數據庫進行分區和分片。
我們會使用 New Relic 或 Datadog 等服務安裝監控程序,并通過監控程序發現比較慢的請求,改進它。同時,隨著擴展的不斷進行,我們希望能夠發現更多的瓶頸并解決它。