高可用可伸縮架構實用經驗談
移動互聯網、云計算和大數據的成熟和發展,讓更多的好想法得以在很短的時間內實現為產品。此時,如果用戶需求抓得準,用戶數量將很可能獲得爆發式增長,而不需要像以往一樣需要精心運營幾年的時間。然而用戶數量的快速增長(尤其是短時間內的爆發式增長),通常會讓應用開發者有些吃不消,不得不面臨一些嚴峻的技術挑戰:如何避免因為單臺機器當機導致服務不可用;如何避免在服務容量不足時,用戶體驗下降,等等。在系統構建之初就采用高可用和可伸縮架構,將能有效避免這些問題。
如何構建高可用和可伸縮架構呢?七牛云存儲首席架構師李道兵在3月22的「開發者最佳實踐日」第十期沙龍活動上給出了自己的想法。他結合自己多年的實踐經驗,針對一些不太復雜的業務場景,從入口層、業務層、緩存層和數據庫層四個層面細致講述了如何構建高可用和可伸縮系統。希望大家讀完這篇文章,能覺得高可用和可伸縮不是一個高不可攀的東西,投入不高的成本就能在項目早期把高可用和可伸縮納入架構設計之中。
如何實現高可用
入口層
入口層,通常指Nginx和Apache等層面的東西,負責應用(不管是Web應用還是移動應用)的服務入口。我們通常會將服務定位在一個IP,如果這個IP對應的服務器當機了,那么用戶的訪問肯定會中斷。此時,可以用keepalived來實現入口層的高可用。例如,機器A 的IP是 1.2.3.4,機器 B 的 IP 是 1.2.3.5, 那么再申請一個 IP 1.2.3.6(稱為⼼跳IP), 平時綁定在機器A上,如果A當機,IP會自動綁定在機器B上;如果B當機,IP會自動綁定在機器A上。對于這種形式,我們將DNS綁定到心跳IP上,即可實現入口層的高可用。
但這個方案有一點小問題。第一,它的切換可能會有一到兩秒的中斷,也就是說,如果不是要求到非常嚴格的毫秒級就不會有問題。第二,對入口的機器會有些浪費,因為買了兩臺機器的入口,可能就只有一臺機器用上。對一些長連接的應用可能會導致服務中斷,這時候就需要客戶端做配合做一些重新創建連接的工作。簡單來說,對于比較普通的業務來說,這個方案就能解決一部分問題。
這里要注意,keepalived在使用上會有一些限制。
- 兩臺機器必須在同一個網段,不是在同一個網段,沒有辦法實現互相搶IP。
- 內網服務也可以做心跳,但需要注意的是,以前為了安全我們會把內網服務綁定在內網IP上,避免出現安全問題。但為了使用keepalived,必須監聽在所有IP上(如果監聽在心跳IP上,那么機器沒有持有該IP時,服務無法啟動),簡單的方案是啟用 iptables, 避免內網服務被外網訪問。
- 服務器利用率下降,這時可以考慮做混合部署來改善這一點。
比較常見的一個錯誤是,如果有兩臺機器,兩個公網IP,DNS上把域名同時定位到兩個IP,就覺得已經做了高可用了。這完全不是高可用,因為如果一臺機器當機,那么就有一半左右的用戶無法訪問。
除了keepalive,lvs也能用來解決入口層的高可用問題。不過,與keepalived相比,lvs會更復雜一些,門檻也會高一些。
業務層
業務層通常是由PHP、Java、Python、Go等寫的邏輯代碼構成的,需要依賴于后臺數據庫及一些緩存層面的東西。如何實現業務層的高可用呢?最核心的就是,業務層不要有狀態,將狀態分散到緩存層和數據庫。目前大家通常喜歡將以下幾種數據放入業務層。
第一個是session,即用戶登錄相關的數據,但好的做法是將session放在數據庫里,或者一個比較穩定的緩存系統中。
第二個是緩存,在訪問數據庫時,如果一個查詢很慢,就希望將這些結果暫時放到進程里,下次再做查詢時就不用再訪問數據庫了。這種做法帶來的問題是,當業務層服務器不只一臺時,數據很難做到一致,從緩存拿到的數據就可能是錯誤的。。
一個簡單的原則就是業務層不要有狀態。在業務層沒有狀態時,一臺業務層服務器當掉了之后,Nginx/Apache會自動將所有的請求打到另外一臺業務層的服務器上。由于沒有狀態,兩臺服務器沒有任何差異,所以用戶完全感受不到。如果把session放在業務層里面的話,那么面臨的問題是,這個用戶以前是登錄在一臺機器上的,這個進程死掉后,用戶就會被登出了。
友情提醒:有一段時間比較流行cookie session,就是將session中的數據加密之后放在客戶的cookie里,然后下發到客戶端,這樣也能做到與服務端完全無狀態。但這里面有很多坑,如果能繞過這些坑就可以這樣使用。第一個坑是怎么保證加密的密鑰不泄露,一旦泄露就意味著攻擊者可以偽造任何人的身份。第二個坑是重放攻擊,如何避免別人通過保存 cookie 去不停地嘗試的驗證碼,當然也還有其他一些攻擊手段。如果沒有好辦法解決這兩方面的問題,那么cookie session盡量慎用。最好是將session放在一個性能比較好的數據庫中。如果數據庫性能不行,那么將session放在緩存中也比放在cookie里要好一點。
緩存層
非常簡單的架構里是沒有緩存這個概念的。但在訪問量上來之后,MySQL之類的數據庫扛不住了,比如在SATA盤里跑MySQL,QPS到達200、300甚至500時,MySQL的性能會大幅下降,這時就可以考慮用緩存層來擋住絕大部分服務請求,提升系統整體的容量。
緩存層做高可用一個簡單的方法就是,將緩存層分得細一點兒。比如說,緩存層就一臺機器的話,那么這臺機器當了以后,所有應用層的壓力就會往數據庫里壓,數據庫扛不住的話,整個網站(或應用)就會隨之當掉。而如果緩存層分在四臺機器上的話,每臺只有四分之一,這臺機器當掉了以后,也只有總訪問量的四分之一會壓在數據庫上面,數據庫能扛住的話,網站就能很穩定地等到緩存層重新起來。在實踐中,四分之一顯然是不夠的,我們會將它分得更細,以保證單臺緩存當機后數據庫還能撐得住即可。在中小規模下,緩存層和業務層可以混合部署,這樣可以節省機器。
數據庫層
在數據庫層面實現高可用,通常是在軟件層面來做。例如,MySQL有主從模式(Master-Slave),還有主主模式(Master-Master)都能滿足需求。MongoDB也有ReplicaSet的概念,基本都能滿足大家的需求。
總之,要想實現高可用,需要做到這幾點:入口層做心跳,業務層服務器無狀態,緩存層減小粒度,數據庫做一個主從模式。對于這種模式來講,我們做的高可用不需要太多服務器,這些東西都可以同時部署在兩臺服務器上。這時,兩臺服務器就能滿足早期的高可用需求了。任何一臺服務器當機用戶完全無感知。
如何實現可伸縮
入口層
在入口層實現伸縮性,可以通過直接水平擴機器,然后DNS加IP來實現。但需要注意,盡管一個域名解析到幾十個IP沒有問題,但是很多瀏覽器客戶端只會使用前幾個IP,部分域名供應商對此有優化(如每次返回的IP順序隨機),但這個優化效果不穩定。
推薦的做法是使用少量的Nginx機器作為入口,業務服務器隱藏在內網(HTTP類型的業務這種方式居多)。另外,也可以把所有IP下發到客戶端,然后在客戶端做一些調度(特別是非HTTP型的業務,如游戲、直播)。
業務層
業務層的伸縮性如何實現?與做高可用時的解決方案一樣,要實現業務層的伸縮性,保證無狀態是很好的手段。此外,加機器繼續水平部署即可。
緩存層
比較麻煩的是緩存層的伸縮性,最簡單粗暴的方式是什么呢?趁著半夜量比較低的時候,把整個緩存層全部下線,然后上線新的緩存層。新的緩存層啟動起來之后,再等這些緩存慢慢預熱。當然這里一個要求,你的數據庫能抗住低估期的請求量。如果扛不住呢?取決于緩存類型,下面我們先可以將緩存的類型區分一下。
- 強一致性緩存:無法接受從緩存拿到錯誤的數據 (比如用戶余額,或者會被下游繼續緩存這種情形)
- 弱一致性緩存:能接受在一段時間內從緩存拿到錯誤的數據 (比如微博的轉發數)。
- 不變型緩存:緩存key對應的value不會變更 (比如從SHA1推出來的密碼, 或者其他復雜公式的計算結果)。
那什么緩存類型伸縮性比較好呢?弱一致性和不變型緩存的擴容很方便,用一致性Hash即可;強一致性情況稍微復雜一些,稍后再講。使用一致性Hash,而不用簡單Hash的原因是緩存的失效率。如果緩存從9臺擴容到10臺,簡單Hash 情況下90%的緩存會馬上失效,而如果使用一致性Hash情況,只有10%的緩存會失效。
那么,強一致性緩存會有什么問題?第一個問題是,緩存客戶端的配置更新時間會有微小的差異,在這個時間窗內有可能會拿到過期的數據。第二個問題是,如果擴容之后再裁撤節點,會拿到臟數據。比如 a 這個key之前在機器1,擴容后在機器2,數據更新了,但裁撤節點后key回到機器1,這時候就會拿到臟數據。
要解決問題2比較簡單,要么保持永不減少節點,要么節點調整間隔大于數據的有效時間。問題1可以用如下的步驟來解決:
- 兩套hash配置都更新到客戶端,但仍然使用舊配置;
- 逐個客戶端改為只有兩套hash結果一致的情況下會使用緩存,其余情況從數據庫讀,但寫入緩存;
- 逐個客戶端通知使用新配置。
Memcache 設計得比較早,導致在伸縮性高可用方面的考慮得不太周到。Redis 在這方面有不少改進,特別是 @ngaut 團隊基于 redis 開發了 codis 這個軟件,一次性地解決了緩存層的絕大部分問題。推薦大家考察一下。
數據庫
在數據庫層面實現伸縮,方法很多,文檔也很多,此處不做過多贅述。大致方法為:水平拆分、垂直拆分和定期滾動。
總之,我們可以在入口層、業務層面、緩存層和數據庫層四個層面,使用剛才介紹的方法和技術實現系統高可用和可伸縮性。具體為:在入口層用心跳來做到高可用,用平行部署來伸縮;在業務層做到服務無狀態;在緩存層,可以減小一些粒度,以方便實現高可用,使用一致性Hash將有助于實現緩存層的伸縮性;數據庫層的主從模式能解決高可用問題,拆分和滾動能解決可伸縮問題。
本文中分享的這些技巧和方法,主要想幫助不太復雜的業務場景或者中小型應用快速搭建起高可用可伸縮的系統。關于如何構建高可用和可伸縮系統還有很多更為細節的點和實踐經驗值得探討,望以后能與大家做更充分的交流。