我們一起聊聊如何設計一個秒殺系統?
動靜分離
對頁面進行徹底的動靜分離,使得用戶秒殺時不需要刷新整個頁面,借此把頁面刷新的數據降到最少。
用戶看到的數據可以分為:靜態數據 和 動態數據。
簡單來說,"動態數據"和"靜態數據"的主要區別就是看頁面中輸出的數據是否和URL、瀏覽者、時間、地域相關,以及是否含有Cookie等私密數據。
比如說:
- 很多媒體類的網站,某一篇文章的內容不管是你訪問還是我訪問,它都是一樣的。所以它就是一個典型的靜態數據,但是它是個動態頁面。
- 我們如果現在訪問淘寶的首頁,每個人看到的頁面可能都是不一樣的,淘寶首頁中包含了很多根據訪問者特征推薦的信息,而這些個性化的數據就可以理解為動態數據了。
這里再強調一下,我們所說的靜態數據,不能僅僅理解為傳統意義上完全存在磁盤上的HTML頁面,它也可能是經過Java系統產生的頁面,但是它輸出的頁面本身不包含上面所說的那些因素。
也就是所謂"動態"還是"靜態",并不是說數據本身是否動靜,而是數據中是否含有和訪問者相關的個性化數據。
靜態化改造
靜態化改造就是要直接緩存 HTTP 連接。
相較于普通的數據緩存而言,你肯定還聽過系統的靜態化改造。靜態化改造是直接緩存 HTTP 連接而不是僅僅緩存數據,如下圖所示,Web 代理服務器根據請求 URL,直接取出對應的 HTTP 響應頭和響應體然后直接返回,這個響應過程簡單得連 HTTP 協議都不用重新組裝,甚至連 HTTP 請求頭也不需要解析。
圖片
商詳上靜態化
高并發時候,商詳頁面是最先受到沖擊的,通過商詳靜態化,可以幫助服務器擋掉99.9%流量。
分類舉例:商品圖片、商品詳細描述等,所有用戶看到的內容都是一樣的,這一類數據就可以上靜態化。
會員折扣、優惠券等信息具備個體差異性,就需要放在動態接接口中,根據入參信息實時查詢。
我們從以下 5 個方面來分離出動態內容:
- URL 唯一化:商品詳情系統天然地就可以做到 URL 唯一化,比如每個商品都由 ID 來標識,那么 http://item.xxx.com/item.htm?id=xxxx 就可以作為唯一的 URL 標識。為啥要 URL 唯一呢?前面說了我們是要緩存整個 HTTP 連接,那么以什么作為 Key 呢?就以 URL 作為緩存的 Key,例如以 id=xxx 這個格式進行區分。
- 分離瀏覽者相關的因素:瀏覽者相關的因素包括是否已登錄,以及登錄身份等,這些相關因素我們可以單獨拆分出來,通過動態請求來獲取。
- 分離時間因素:服務端輸出的時間也通過動態請求獲取。
- 異步化地域因素:詳情頁面上與地域相關的因素做成異步方式獲取,當然你也可以通過動態請求方式獲取,只是這里通過異步獲取更合適。
- 去掉 Cookie:服務端輸出的頁面包含的 Cookie 可以通過代碼軟件來刪除,如 Web 服務器 Varnish 可以通過 unset req.http.cookie 命令去掉 Cookie。注意,這里說的去掉 Cookie 并不是用戶端收到的頁面就不含 Cookie 了,而是說,在緩存的靜態數據中不含有 Cookie。
分離出動態內容之后,如何組織這些內容頁就變得非常關鍵了。
動態內容的處理通常有兩種方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案。
- ESI 方案(或者 SSI):即在 Web 代理服務器上做動態內容請求,并將請求插入到靜態頁面中,當用戶拿到頁面時已經是一個完整的頁面了。這種方式對服務端性能有些影響,但是用戶體驗較好。
- CSI 方案。即單獨發起一個異步 JavaScript 請求,以向服務端獲取動態內容。這種方式服務端性能更佳,但是用戶端頁面可能會延時,體驗稍差。
CDN
網站應用,靜態資源占流量的多數。系統做了動靜分離之后,就可以把靜態資源通過CDN加速。
這樣,靜態資源的請求大部分通過就近部署的CDN服務器提供服務,用戶的延遲也會有明顯的提升。網站服務器專注于服務動態流量,帶寬壓力會小很多。
動靜分離,部署時靜態資源要給一個單獨域名,這個域名是個CNAME,CNAME映射到CDN服務廠商提供的DNS服務器,CDN DNS服務器會根據請求的IP地址所在區域和資源內容,返回就近的CDN緩存服務器ip,后續用戶對這個DNS的請求都會轉到這個IP上來。
Tips:CNAME 簡單來講就是給域名起了個別名。
CDN 工作流程大致如下:
圖片
靜態資源上 CDN 存在以下幾個問題:
- 失效問題。前面我們也有提到過緩存時效的問題,不知道你有沒有理解,我再來解釋一下。談到靜態數據時,我說過一個關鍵詞叫“相對不變”,它的言外之意是“可能會變化”。比如一篇文章,現在不變,但如果你發現個錯別字,是不是就會變化了?如果你的緩存時效很長,那用戶端在很長一段時間內看到的都是錯的。所以,這個方案中也是,我們需要保證 CDN 可以在秒級時間內,讓分布在全國各地的 Cache 同時失效,這對 CDN 的失效系統要求很高。
失效需要一個失效系統來實現,一般有主動失效和被動失效。
主動失效需要監控數據庫數據的變化然后轉成消息來發送失效消息,這個實現比較復雜,阿里有個系統叫metaq,可以網上參考下。
被動失效就是只緩存固定時間,然后到期后自動失效
- 命中率問題。Cache 最重要的一個衡量指標就是“高命中率”,不然 Cache 的存在就失去了意義。同樣,如果將數據全部放到全國的 CDN 上,必然導致 Cache 分散,而 Cache 分散又會導致訪問請求命中同一個 Cache 的可能性降低,那么命中率就成為一個問題。
- 發布更新問題。如果一個業務系統每周都有日常業務需要發布,那么發布系統必須足夠簡潔高效,而且你還要考慮有問題時快速回滾和排查問題的簡便性。
部署方式如下圖所示:
圖片
你可能會問,存儲在瀏覽器或 CDN 上,有多大區別?我的回答是:區別很大!因為在 CDN 上,我們可以做主動失效,而在用戶的瀏覽器里就更不可控,如果用戶不主動刷新的話,你很難主動地把消息推送給用戶的瀏覽器。
秒殺場景 CDN 應用
比如,1 元賣 iPhone,100 臺,于是來了一百萬人搶購。
我們把技術挑戰放在一邊,先從用戶或是產品的角度來看一下,秒殺的流程是什么樣的。
- 首先,你需要一個秒殺的 landing page,在這個秒殺頁上有一個倒計時的按鈕。
- 一旦這個倒計時的時間到了,按鈕就被點亮,讓你可以點擊按鈕下單。
- 一般來說下單時需要你填寫一個校驗碼,以防止是機器來搶。
從技術上來說,這個倒計時按鈕上的時間和按鈕可以被點擊的時間是需要后臺服務器來校準的,這意味著:
- 前端頁面要不斷地向后端來請求,開沒開始,開沒開始……
- 每次詢問的時候,后端都會給前端一個時間,以校準前端的時間。
- 一旦后端服務器表示 OK 可以開始,后端服務會返回一個 URL。
- 這個 URL 會被安置在那個按鈕上,就可以點擊了。
- 點擊后,如果搶到了庫存,就進入支付頁面,如果沒有則返回秒殺已結束。
很明顯,要讓 100 萬用戶能夠在同一時間打開一個頁面,這個時候,我們就需要用到 CDN 了。數據中心肯定是扛不住的,所以,我們要引入 CDN。
在 CDN 上,這 100 萬個用戶就會被幾十個甚至上百個 CDN 的邊緣結點給分擔了,于是就能夠扛得住。然后,我們還需要在這些 CDN 結點上做點小文章。
一方面,我們需要把小服務部署到 CDN 結點上去,這樣,當前端頁面來問開沒開始時,這個小服務除了告訴前端開沒開始外,它還可以統計下有多少人在線。每個小服務會把當前在線等待秒殺的人數每隔一段時間就回傳給我們的數據中心,于是我們就知道全網總共在線的人數有多少。
假設,我們知道有大約 100 萬的人在線等著搶,那么,在我們快要開始的時候,由數據中心向各個部署在 CDN 結點上的小服務上傳遞一個概率值,比如說是 0.02%。
于是,當秒殺開始的時候,這 100 萬用戶都在點下單按鈕,首先他們請求到的是 CDN 上的這些服務,這些小服務按照 0.02% 的量把用戶放到后面的數據中心,也就是 1 萬個人放過去兩個,剩下的 9998 個都直接返回秒殺已結束。于是,100 萬用戶被放過了 0.02% 的用戶,也就是 200 個左右,而這 200 個人在數據中心搶那 100 個 iPhone,也就是 200 TPS,這個并發量怎么都應該能扛住了。
熱點緩存
熱點數據亦分 靜態熱點 和 動態熱點。
所謂"靜態熱點數據",就是能夠提前預測的熱點數據。
例如,我們可以通過賣家報名的方式提前篩選出來,通過報名系統對這些熱點商品進行打標。另外,我們還可以通過大數據分析來提前發現熱點商品,比如我們分析歷史成交記錄、用戶的購物車記錄,來發現哪些商品可能更熱門、更好賣,這些都是可以提前分析出來的熱點。
所謂"動態熱點數據",就是不能被提前預測到的,系統在運行過程中臨時產生的熱點。例如,賣家在抖音上做了廣告,然后商品一下就火了,導致它在短時間內被大量購買。
靜態熱點比較好處理,所以秒級內自動發現熱點商品就成為了熱點緩存的關鍵。
動態熱點發現
這里我給出一個動態熱點發現系統的具體實現:
- 構建一個異步的系統,它可以收集交易鏈路上各個環節中的中間件產品的熱點Key,如Nginx、緩存、RPC服務框架等這些中間件(一些中間件產品本身已經有熱點統計模塊)。
- 建立一個熱點上報和可以按照需求訂閱的熱點服務的下發規范,主要目的是通過交易鏈路上各個系統(包括詳情、購物車、交易、優惠、庫存、物流等)訪問的時間差把上游已經發現的熱點透傳給下游系統,提前做好保護。比如,,對于大促高峰期,詳情系統是最早知道的,在統一接入層上 Nginx 模塊統計的熱點URL。熱點的統計可以很簡單的對訪問的商品進行訪問計數,然后排序。還有就是用通常的隊列的淘汰算法如 LRU 等都可以實現。
- 將上游系統收集的熱點數據發送到熱點服務臺,然后下游系統(如交易系統)就會知道哪些商品會被頻繁調用,然后做熱點保護。
這里我給出了一個圖,其中用戶訪問商品時經過的路徑有很多,我們主要是依賴前面的導購頁面(包括首頁、搜索頁面、商品詳情、購物車等)提前識別哪些商品的訪問量高,通過這些系統中的中間件來收集熱點數據,并記錄到日志中。
圖片
我們通過部署在每臺機器上的Agent把日志匯總到聚合和分析集群中,然后把符合一定規則的熱點數據,通過訂閱分發系統再推送到相應的系統中。你可以是把熱點數據填充到Cache中,或者直接推送到應用服務器的內存中,還可以對這些數據進行攔截,總之下游系統可以訂閱這些數據,然后根據自己的需求決定如何處理這些數據。
熱點發現要做到接近實時(3s內完成熱點數據的發現),因為只有做到接近實時,動態發現才有意義,才能實時地對下游系統提供保護。
對于緩存系統來講,緩存命中率是最重要的指標,甚至都沒有之一。時間拉的越長,不確定性越多,緩存命中率必然越低。比如如果10s內才發送熱點就沒意義了,因為10s內用戶可以進行的操作太多了。時間越長,不可控元素越多,熱點緩存命中率越低。
可以參考,京東開源的熱點探測 Hot Key。
可以考慮建立實時熱點發現系統。
具體步驟如下:
- 接入Nginx將請求轉發給應用Nginx。
- 應用Nginx首先該取本地緩存。如果命中,則直接返回,不命中會讀取分布式緩存、回源到Tomcat進行處理。
- 應用Nginx會將請求上報給實時熱點發現系統,如使用UDP直接上報請求,或者將請求寫到本地 kafka,或者使用 flume 訂閱本地Nginx日志。上報給實時熱點發現系統后,它將進行熱點統計(可以考慮storm實時計算)。
- 根據設置的閾值將熱點數據推送到應用Nginx本地緩存。
熱點限制
限制更多的是一種保護機制,限制的辦法也有很多,例如對被訪問商品的 ID 做一致性 Hash,然后根據 Hash 做分桶,每個分桶設置一個處理隊列,這樣可以把熱點商品限制在一個請求隊列里,防止因某些熱點商品占用太多的服務器資源,而使其他請求始終得不到服務器的處理資源。
多級緩存
使用Java堆內存來存儲緩存對象。使用堆緩存的好處是不需要序列化/反序列化,是最快的緩存。缺點也很明顯,當緩存的數據量很大時,GC(垃圾回收)暫停時間會變長,存儲容量受限于堆空間大小。
一般通過軟引用/弱引用來存儲緩存對象,即當堆內存不足時,可以強制回收這部分內存釋放堆內存空間。一般使用堆緩存存儲較熱的數據。可以使用Caffeine Cache實現。
現在應用最多的是多級緩存方案,就好比 CPU 也有 L1,L2,L3。
Nginx緩存 → 分布式Redis緩存(可以使用Lua腳本直接在Nginx里讀取Redis)→堆內存。
整體流程如下:
- 接入Nginx將請求負載均衡到應用Nginx,此處常用的負載均衡算法是輪詢或者一致性哈希。輪詢可以使服務器的請求更加均衡,而一致性哈希可以提升應用Nginx的緩存命中率。
- 應用Nginx讀取本地緩存(本地緩存可以使用LuaShared Dict、Nginx Proxy Cache(磁盤/內存)、LocalRedis實現)。如果本地緩存命中,則直接返回,使用應用Nginx本地緩存可以提升整體的吞吐量,降低后端壓力,尤其應對熱點問題非常有效。
- 如果Nginx本地緩存沒命中,則會讀取相應的分布式緩存(如Redis緩存,還可以考慮使用主從架構來提升性能和吞吐量),如果分布式緩存命中中,則直接返回相應數據(并回寫到Nginx本地緩存)。
- 如果分布式緩存也沒有命中,則會回源到Tomcat集群,在回源到Tomcat集群時,也可以使用輪詢和一致性哈希作為負載均衡算法。
- 在Tomcat應用中,首先讀取本地堆緩存。如果有,則直接返回(并會寫到主Redis集群)。
- 作為可選部分,如果步驟4沒有命中,則可以再嘗試一次讀主Redis集群操作,目的是防止當從集群有問題時的流量沖擊。
- 如果所有緩存都沒有命中,則只能查詢DB或相關服務獲取相關數據并返回。
- 步驟7返回的數據異步寫到主Redis集群,此處可能有多個Tomcat實例同時寫。
流量削峰
秒殺答題
添加秒殺答題。有以下兩個目的:
- 第一個目的是防止部分買家使用秒殺器在參加秒殺時作弊。
- 第二個目的其實就是延緩請求,起到對請求流量進行削峰的作用,從而讓系統能夠更好地支持瞬時的流量高峰。這個重要的功能就是把峰值的下單請求拉長,從以前的 1s 之內延長到 2s~10s。這樣一來,請求峰值基于時間分片了。
限流
請求排隊
- 應用層做排隊。按照商品維度設置隊列順序執行,這樣能減少同一臺機器對數據庫同一行記錄進行操作的并發度,同時也能控制單個商品占用數據庫連接的數量,防止熱點商品占用太多的數據庫連接。
- 數據庫層做排隊。應用層只能做到單機的排隊,但是應用機器數本身很多,這種排隊方式控制并發的能力仍然有限,所以如果能在數據庫層做全局排隊是最理想的。阿里的數據庫團隊開發了針對這種 MySQL 的 InnoDB 層上的補丁程序(patch),可以在數據庫層上對單行記錄做到并發排隊。
你可能有疑問了,排隊和鎖競爭不都是要等待嗎,有啥區別?
如果熟悉 MySQL 的話,你會知道 InnoDB 內部的死鎖檢測,以及 MySQL Server 和 InnoDB 的切換會比較消耗性能。
對于分布式限流,目前遇到的場景是業務上的限流,而不是流量入口的限流。流量入口限流應該在接入層完成,而接入層筆者一般使用 Nginx。業務的限流一般用Redis + Lua腳本。
庫存扣減
千萬不要超賣,這是大前提。超賣直接導致的就是資損。
庫存扣減方式
在正常的電商平臺購物場景中,用戶的實際購買過程一般分為兩步:下單和付款。你想買一臺 iPhone 手機,在商品頁面點了“立即購買”按鈕,核對信息之后點擊“提交訂單”,這一步稱為下單操作。下單之后,你只有真正完成付款操作才能算真正購買,也就是俗話說的“落袋為安”。
- 下單減庫存,即當買家下單后,在商品的總庫存中減去買家購買數量。下單減庫存是最簡單的減庫存方式,也是控制最精確的一種,下單時直接通過數據庫的事務機制控制商品庫存,這樣一定不會出現超賣的情況。但是你要知道,有些人下完單可能并不會付款。
下單減庫存有多種方式保證不超賣:一種是在應用程序中通過事務來判斷,即保證減后庫存不能為負數,否則就回滾;另一種辦法是直接設置數據庫的字段數據為無符號整數,這樣減后庫存字段值小于零時會直接執行 SQL 語句來報錯;再有一種就是使用 CASE WHEN 判斷語句,例如這樣的 SQL 語句:
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
- 付款減庫存,即買家下單后,并不立即減庫存,而是等到有用戶付款后才真正減庫存,否則庫存一直保留給其他買家。但因為付款時才減庫存,如果并發比較高,有可能出現買家下單后付不了款的情況,因為可能商品已經被其他人買走了。
- 預扣庫存,這種方式相對復雜一些,買家下單后,庫存為其保留一定的時間(如10分鐘),超過這個時間,庫存將會自動釋放,釋放后其他買家就可以繼續購買。在買家付款前,系統會校驗該訂單的庫存是否還有保留:如果沒有保留,則再次嘗試預扣;如果庫存不 足(也就是預扣失?。﹦t不允許繼續付款;如果預扣成功,則完成付款并實際地減去庫存。
先說第一種,"下單減庫存",可能導致惡意下單。
正常情況下,買家下單后付款的概率會很高,所以不會有太大問題。但是有一種場景例外,就是當賣家參加某個活動時,此時活動的有效時間是商品的黃金售賣時間,如果有競爭對手通過惡意下單的方式將該賣家的商品全部下單(雇幾個人下單將你的商品全都鎖了),讓這款商品的庫存減為零,那么這款商品就不能正常售賣了。要知道,這些惡意下單的人是不會真正付款的,這正是"下單減庫存"方式的不足之處。
既然,從而影響賣家的商品銷售,那么有沒有辦法解決呢?你可能會想,采用"付款減庫存"的方式是不是就可以了?的確可以。但是,"付款減庫存"又會導致另外一個問題:庫存超賣。
假如有 100 件商品,就可能出現 300 人下單成功的情況,因為下單時不會減庫存,所以也就可能出現下單成功數遠遠超過真正庫存數的情況,這尤其會發生在做活動的熱門商品上。這樣一來,就會導致很多買家下單成功但是付不了款,買家的購物體驗自然比較差。
超賣情況可以區別對待:對普通的商品下單數量超過庫存數量的情況,可以通過補貨來解決;但是有些賣家完全不允許庫存為負數的情況,那只能在買家付款時提示庫存不足。
預扣庫存方案確實可以在一定程度上緩解上面的問題。但沒有徹底解決,比如針對惡意下單這種情況,雖然把有效的付款時間設置為 10 分鐘,但是惡意買家完全可以在 10 分鐘后再次下單,或者采用一次下單很多件的方式把庫存減完。針對這種情況,解決辦法還是要結合安全和反作弊的措施來制止。
例如,給經常下單不付款的買家進行識別打標(可以在被打標的買家下單時不減庫存)、給某些類目設置最大購買件數(例如,參加活動的商品一人最多只能買 3 件),以及對重復下單不付款的操作進行次數限制等。
更新操作轉化為插入操作
方案的核心思路:將庫存扣減異步化,庫存扣減流程調整為下單時只記錄扣減明細(DB記錄插入),異步進行真正庫存扣減(更新)。
大量請求對同一數據行的的競爭更新,會導致數據庫的性能急劇下降,甚至發生數據庫分片的連接被熱點單商品扣減。
前置校驗庫存,從db更換為redis,庫存扣減操作,從更新操作,直接修改為插入操作(性能角度,插入鎖比更新鎖的性能高)
熱點發現系統(中間件)會通過消息隊列的方式通知應用,應用對庫存進行熱點打標。一但庫存不再是熱點(熱點失效),則會進行庫存熱點重置。
庫存分段
將商品庫存分開放,分而治之。例如,原來的秒殺商品的id為10001,庫存為1000件,在Redis中的存儲為(10001, 1000),我們將原有的庫存分割為5份,則每份的庫存為200件,此時,我們在Redia中存儲的信息為(10001_0, 200),(10001_1, 200),(10001_2, 200),(10001_3, 200),(10001_4, 200)。將key分散到redis的不同槽位中,這就能夠提升Redis處理請求的性能和并發量。
隔離
單個熱點商品會影響整個數據庫的性能,導致0.01%的商品影響99.99%的商品的售賣,這是我們不愿意看到的情況。一個解決思路是遵循前面介紹的原則進行隔離,把熱點商品放到單獨的熱點庫中。但是這無疑會帶來維護上的麻煩,比如要做熱點數據的動態遷移以及單獨的數據庫等。
線程隔離
線程隔離主要是指線程池隔離,在實際使用時,我們會把請求分類,然后交給不同的線程池處理。當一種業務的請求處理發生問題時,不會將故障擴散到其他線程池,從而保證其他服務可用。
圖片
隨著對系統可用性的要求,會進行多機房部署,每個機房的服務都有自己的服務分組,本機房的服務應該只調用本機房服務,不進行跨機房調用。其中,一個機房服務發生問題時,可以通過DNS/負載均衡將請求全部切到另一個機房,或者考慮服務能自動重試其他機房的服務,從而提升系統可用性。
圖片
核心業務以及非核心業務可以放在不同的線程池。
可以使用Hystrix來實現線程池隔離。
降級
所謂“降級”,就是當系統的容量達到一定程度時,是為了保證核心服務的穩定而犧牲非核心服務的做法。
降級方案可以這樣設計:當秒殺流量達到 5w/s 時,把成交記錄的獲取從展示 20 條降級到只展示 5 條。“從 20 改到 5”這個操作由一個開關來實現,也就是設置一個能夠從開關系統動態獲取的系統參數。
降級無疑是在系統性能和用戶體驗之間選擇了前者,降級后肯定會影響一部分用戶的體驗,例如在雙 11 零點時,如果優惠券系統扛不住,可能會臨時降級商品詳情的優惠信息展示,把有限的系統資源用在保障交易系統正確展示優惠信息上,即保障用戶真正下單時的價格是正確的。所以降級的核心目標是犧牲次要的功能和用戶體驗來保證核心業務流程的穩定,是一個不得已而為之的舉措。
拒絕服務
如果限流還不能解決問題,最后一招就是直接拒絕服務了。
當系統負載達到一定閾值時,例如 CPU 使用率達到 90% 或者系統 load 值達到 2*CPU 核數時,系統直接拒絕所有請求,這種方式是最暴力但也最有效的系統保護方式。
在最前端的 Nginx 上設置過載保護,當機器負載達到某個值時直接拒絕 HTTP 請求并返回 503 錯誤碼,在 Java 層同樣也可以設計過載保護。
負載均衡
在項目的架構中,我們一般會同時部署 LVS 和 Nginx 來做 HTTP 應用服務的負載均衡。也就是說,在入口處部署 LVS,將流量分發到多個 Nginx 服務器上,再由 Nginx 服務器分發到應用服務器上。
為什么這么做呢?
主要和 LVS 和 Nginx 的特點有關,LVS 是在網絡棧的四層做請求包的轉發,請求包轉發之后,由客戶端和后端服務直接建立連接,后續的響應包不會再經過 LVS 服務器,所以相比 Nginx,性能會更高,也能夠承擔更高的并發。
可 LVS 缺陷是工作在四層,而請求的URL是七層的概念,不能針對URL做更細致地請求分發,而且LVS也沒有提供探測后端服務是否存活的機制;而Nginx雖然比LVS的性能差很多,但也可以承擔每秒幾萬次的請求,并且它在配置上更加靈活,還可以感知后端服務是否出現問題。
因此,LVS適合在入口處,承擔大流量的請求分發,而Nginx要部在業務服務器之前做更細維度的請求分發。
我給你的建議是,如果你的QPS在十萬以內,那么可以考慮不引入 LVS 而直接使用 Nginx 作為唯一的負載均衡服務器,這樣少維護一個組件,也會減少系統的維護成本。
但對于Nginx來說,我們要如何保證配置的服務節點是可用的呢?
這就要感謝淘寶開源的 Nginx 模塊 nginx_upstream_check_moduule 了,這個模塊可以讓 Nginx 定期地探測后端服務的一個指定的接口,然后根據返回的狀態碼,來判斷服務是否還存活。當探測不存活的次數達到一定閾值時,就自動將這個后端服務從負載均衡服務器中摘除。
它的配置樣例如下:
upstream server {
server 192.168.1.1:8080;
server 192.168.1.2:8080;
check interval=3000 rise=2 fall=5 timeout=1000 type=http default_down=true
check_http_send "GET /health_check HTTP/1.0\r\n\n\n\n"; //檢測URL
check_http_expect_alivehttp_2xx; //檢測返回狀態碼為 200 時認為檢測成功
}
不過這兩個負載均衡服務適用于普通的Web服務,對于微服務多架構來說,它們是不合適的。因為微服務架構中的服務節點存儲在注冊中心里,使用 LVS 就很難和注冊中心交互,獲取全量的服務節點列表。
另外,一般微服務架構中,使用的是RPC協議而不是HTTP協議,所以Nginx也不能滿足要求。
所以,我們會使用另一類的負載均衡服務,客戶端負載均衡服務,也就是把負載均衡的服務內嵌在RPC客戶端中。
DNS負載均衡
當我們的應用單實例不能支撐用戶請求時,此時就需要擴容,從一臺服務器擴容到兩臺、幾十臺、幾百臺。
然而,用戶訪問時是通過如 http://www.jd.com 的方式訪問,在請求時,瀏覽器首先會查詢DNS服務器獲取對應的IP,然后通過此 IP 訪問對應的服務。
因此,一種方式是 www.jd.com 域名映射多個IP,但是,存在一個最簡單的問題,假設某臺服務器重啟或者出現故障,DNS 會有一定的緩存時間,故障后切換時間長,而且沒有對后端服務進行心跳檢查和失敗重試的機制。
Nginx負載均衡
對于一般應用來說,有Nginx就可以了。但Nginx一般用于七層負載均衡,其吞吐量是有一定限制的。為了提升整體吞吐量,會在 DNS 和 Nginx之間引入接入層,如使用LVS(軟件負載均衡器)、F5(硬負載均衡器)可以做四層負載均衡,即首先 DNS解析到LVS/F5,然后LVS/F5轉發給Nginx,再由Nginx轉發給后端Real Server。
圖片
對于一般業務開發人員來說,我們只需要關心到Nginx層面就夠了,LVS/F5一般由系統/運維工程師來維護。Nginx目前提供了HTTP (ngx_http_upstream_module)七層負載均衡,而1.9.0版本也開始支持TCP(ngx_stream_upstream_module)四層負載均衡。
一致性hash算法最好在 lua腳本里指定。
Nginx商業版還提供了 least_time,即基于最小平均響應時間進行負載均衡。
Nginx的服務檢查是惰性的,Nginx只有當有訪問時后,才發起對后端節點探測。如果本次請求中,節點正好出現故障,Nginx依然將請求轉交給故障的節點,然后再轉交給健康的節點處理。所以不會影響到這次請求的正常進行。但是會影響效率,因為多了一次轉發,而且自帶模塊無法做到預警。
- Nginx服務器是服務端的負載均衡,而分布式服務實現是客戶端的負載均衡。
- Nginx是集中式的負載均衡,分布式服務是消費者內部線程實現的負載均衡。
數據異構
比如對于訂單庫,當對其分庫分表后,如果想按照商家維度或者按照用戶維度進行查詢,那么是非常困難的,因此可以通過異構數據庫來解決這個問題。可以采用下圖的架構。
圖片
異構數據主要存儲數據之間的關系,然后通過查詢源庫查詢實際數據。不過,有時可以通過數據冗余存儲來減少源庫查詢量或者提升查詢性能。
針對這類場景問題,最常用的是采用“異構索引表”的方式解決,即采用異步機制將原表的每一次創建或更新,都換另一個維度保存一份完整的數據表或索引表,拿空間換時間。
也就是應用在插入或更新一條訂單ID為分庫分表鍵的訂單數據時,也會再保存一份按照買家ID為分庫分表鍵的訂單索引數據,其結果就是同一買家的所有訂單索引表都保存在同一數據庫中,這就是給訂單創建了異構索引表。