高可用可伸縮架構實用經驗談

李道兵發表於2015-05-24

移動網際網路、雲端計算和大資料的成熟和發展,讓更多的好想法得以在很短的時間內實現為產品。此時,如果使用者需求抓得準,使用者數量將很可能獲得爆發式增長,而不需要像以往一樣需要精心運營幾年的時間。然而使用者數量的快速增長(尤其是短時間內的爆發式增長),通常會讓應用開發者有些吃不消,不得不面臨一些嚴峻的技術挑戰:如何避免因為單臺機器當機導致服務不可用;如何避免在服務容量不足時,使用者體驗下降,等等。在系統構建之初就採用高可用和可伸縮架構,將能有效避免這些問題。

如何構建高可用和可伸縮架構呢?七牛雲端儲存首席架構師李道兵在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可以用如下的步驟來解決:

  1. 兩套hash配置都更新到客戶端,但仍然使用舊配置;
  2. 逐個客戶端改為只有兩套hash結果一致的情況下會使用快取,其餘情況從資料庫讀,但寫入快取;
  3. 逐個客戶端通知使用新配置。

Memcache 設計得比較早,導致在伸縮性高可用方面的考慮得不太周到。Redis 在這方面有不少改進,特別是 @ngaut 團隊基於 redis 開發了 codis 這個軟體,一次性地解決了快取層的絕大部分問題。推薦大家考察一下。

資料庫

在資料庫層面實現伸縮,方法很多,文件也很多,此處不做過多贅述。大致方法為:水平拆分、垂直拆分和定期滾動。

總之,我們可以在入口層、業務層面、快取層和資料庫層四個層面,使用剛才介紹的方法和技術實現系統高可用和可伸縮性。具體為:在入口層用心跳來做到高可用,用平行部署來伸縮;在業務層做到服務無狀態;在快取層,可以減小一些粒度,以方便實現高可用,使用一致性Hash將有助於實現快取層的伸縮性;資料庫層的主從模式能解決高可用問題,拆分和滾動能解決可伸縮問題。

本文中分享的這些技巧和方法,主要想幫助不太複雜的業務場景或者中小型應用快速搭建起高可用可伸縮的系統。關於如何構建高可用和可伸縮系統還有很多更為細節的點和實踐經驗值得探討,望以後能與大家做更充分的交流。

相關文章