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