
前提聲明
本篇内容完全是筆者自己對技術分析和總結沉澱,由于筆者技術和能力有限,如果有不對的地方,還望大家多多見諒和包涵,并且多多指正留言,謝謝。
秒殺系統-情報背景
相信大家都接觸過新浪微網誌、淘寶、京東等等這些通路量較為巨大的平台以及網站,針對于“高流量”、“高并發”來講,更是我們【技術開發者】都要面臨的的一個很難的“包袱”難題。哎,看來如果要在這行混下去,即使你可能沒有接觸高并發場景,也要自己創造“高并發”進行迎難而上,因為隻有這樣子我們才可以更進一步啊!
秒殺系統-情報介紹
對于今天我們要介紹的内容就屬于高并發的一個最極端的場景之一:“秒殺”,這個名詞一般會在“大促”的時候出現,當然也會在某些平台活動上出現,那麼肯定會有小夥伴會說,秒殺系統要注意哪些問題啊!為啥會比較難呢,難在哪裡啊!
秒殺系統- 特點分析
- 瞬時劇增:在某一個時刻開始進入流量(很少會有熱身以及緩慢增長機制),秒殺時大量使用者會在同一時間,搶購同一商品,網站瞬時流量激增。
- 僧多粥少:商品的庫存是有限的,秒殺請求下的訂單數量會遠遠大于庫存數量,隻有少部分使用者能夠秒殺成功。
- 資源鎖定:秒殺業務流程比較簡單,一般就是下訂單減庫存。庫存就是使用者争奪的“資源”,實際被消費的“資源”不能超過計劃要售出的“資源”,也就是不能被“超賣”。
秒殺系統-難度分析
它的難度就在于要完成一個“60-100 分”的秒殺系統,那麼它必須要要至少兼顧以下這三個方面,才算合格,這三個“惡魔”分别叫“服務可用性”、“資料一緻性”和“快速響應性”,有點“苛刻”!
在我們現在的場景下,很難再去考慮一個非分布式系統的架構了。(分布式架構)相信大家都知道 CAP 理論吧!沒事不知道也沒關系,可見内容:
CAP 理論又稱 CAP 定理,它說的是在一個分布式系統中,服務(資料)層面的一緻性(Consistency)、服務自身的可用性(Availability)、網絡不同節點分區容錯性(Partition tolerance)。
A 和 C 相信大家從字面上都可以了解了,這裡要聲明一下比較陌生的 P:它代表如果要保證不同的節點即使在網絡出現問題的時候仍能夠通路到資料,那麼最直接的辦法就是備援指派節點,否則一切都是空談,是以作為一個分布式系統而言,無法忽略 P,我們可以了解它就是 A 和 C 的基礎。
CAP 體系總結
- 隻保證 AC 就是一個單體應用,根本不是分布式。意義當然有,在分布式出現之前都是這麼搭系統。倘若這個系統的節點之一挂了,不會發生腦裂而是整個系統直接宕掉。
- 進一步說如果網絡中存在的節點越多,分區容忍性越高,但要複制更新的資料就越多,一緻性就越難保證。
- 為了保證一緻性,更新所有節點資料所需要的時間就越長,可用性就會降低。
服務的可用性
服務可用性,是在于高并發流量的沖擊下,仍然可以保持服務的可用性并且還要保證一直可以輸出對外界的服務能力,不會造成當機以及資源損壞,即使在記憶體和網絡甚至硬體資源有限的情況下,也不會被擊垮“死亡”。
資料的一緻性
都知道,我們開發的程式以及現在多數的伺服器,比如資料庫,他們在處理資料的時候,很有可能會存在多個線程同時在修改同一行資料或者同一塊記憶體,在 Java 角度而言本身也會存在不一緻的問題,而在程式和中間件的角度而言,也是一樣,會出現同一時刻在資料修改順序的亂序化,以及資料的紊亂,造成資料的重複操作,造成與我們預期的設想不同。
- 除非你可以實作串行化,一條一條處理,不讓它們同一時刻就行修改或者操作資料,這個是最本質且最安全的辦法,但是也是最影響性能的辦法。(悲觀鎖、同步隊列)。
- 此外還有一種辦法就是,時時刻刻在原子層級,也就是最接近底層的計算機修改資料的時候,或者在所有節點之間建立一個應用層級的中間彙總幹路點(redis 或者 database 的主幹點),上面加入寫屏障和讀屏障,在修改之前,在進行一次校驗判斷,如果資料與預期不同,就不進行修改。這就是著名的樂觀鎖!
服務快速響應性
一般來講這個屬于使用者體驗,一個較為合格的秒殺系統,是不應該讓使用者漫長的等待最好盡可能快速回報結果。要做成快速響應,就不需要是異步傳回,直接快速響應。此外還需要盡快幫助使用者計算資料,直接傳回。
秒殺系統-架構設計
我們将秒殺架構進行一下劃分,大體分為三個層級進行分析:由外到内進行分析,分别是:應用層、服務層、資料通路層。
應用層架構設計
動靜分離+CDN 技術
動靜分離分析
- 場景分析:在秒殺活動開啟之前,使用者一般都會嘗試不斷的重新整理浏覽器頁面(俗稱 F5)以保證不會錯過秒殺活動的商品。
- 按照常用的網站應用架構:
- 我們假設,如果這些無用的請求,頻繁的沖擊我們的背景伺服器,比如說經過:Web 伺服器(LVS、Nginx 等)->應用伺服器(tomcat 或者 Jetty 等)、連接配接資料庫(MySQL),者無疑會對後端服務以及伺服器造成非常大的壓力。
- 解決方案:重新設計秒殺商品頁面,不使用網站原來的商品詳細頁面,頁面内容靜态化,減少/隔絕無用的請求經過後端服務。
CDN 技術分析
增加網絡帶寬
網站的靜态頁面資料大小 100K,那麼需要的網絡和伺服器帶寬是 2G(100K×10000),這些網絡帶寬是因為秒殺活動新增的,超過網站平時使用的帶寬。
即使将動态業務轉換為靜态化頁面,但是秒殺活動會非常劇烈的增加的網絡帶寬的消耗,同時并不會減輕前端網站伺服器的壓力,是以如果可以的話,需要再進一步将秒殺商品頁面緩存在 CDN,而不在是單純的我們的前端 Nginx 伺服器層面,是以需要和 CDN 服務商臨時租借新增的出口帶寬。
阻斷緩存頁面
在頁面中加入一個 JavaScript 檔案引用(進行傳遞随機号+狀态位),該 JavaScript 檔案
中包含秒殺開始标志為否;
- 當秒殺開始的時候生成一個新的 JavaScript 檔案(檔案名保持不變,隻是内容不一樣),更新秒殺開始标志為是,加入下單頁面的 URL 及随機數參數(這個随機數隻會産生一個,即所有人看到的 URL 都是同一個,伺服器端可以用 redis 這種分布式緩存伺服器來儲存随機數),并被使用者浏覽器加載,控制秒殺商品頁面的展示。
- 這個 JavaScript 檔案的加載可以加上随機版本号(例如 xx.js?v=32353823),這樣就不會被浏覽器、CDN 和反向代理伺服器緩存。
- 這個 JavaScript 檔案非常小,即使每次浏覽器重新整理都通路 JavaScript 檔案伺服器也不會對伺服器叢集和網絡帶寬造成太大壓力。
根據 ID 限制頻率
為了控制公平性原則,由于黃牛或者一些黑客達人,會采用”高科技“,比如說,采用爬蟲腳本操作,瘋狂的去重新整理頁面。為了防止一些人的破壞以及公平分散,是以采用同一個标準去控制 UID(使用者 ID)去通路頻率資訊,當超過每個人所需要達到的頻率門檻值,就要進行限制互動視窗内能夠通路重新整理的資料量!
例如:可以用 Redis 給每個使用者做通路統計,根據使用者的 ID 和商品的辨別雙方面進行對使用者對某一個商品的通路頻率控制,超過通路頻率後,就會将他的請求暫時性熔斷。
負載均衡
秒殺系統必然是一個叢集系統,在硬體不提升的情況下利用 nginx 做負載均衡也是不錯的選擇。
負載均衡(Load Balance)是叢集技術(Cluster)的一種應用,可以将工作任務分攤到多個處理單元,進而提高并發處理能力,有利于提升中大型網站的性能。需要使用服務叢集和水準擴充,讓“高峰”請求分流到不同的伺服器進行處理。
http 協定負載均衡
根據使用者的 http 請求的 DNAT 計算出一個真實的 web 伺服器位址,并将該 web 伺服器位址寫入 http 重定向響應中傳回給浏覽器,由浏覽器重新進行通路。該方式比較簡單,但性能較差。
DNS 解析負載均衡
DNS 伺服器上配置多個域名對應 IP 的記錄。該方式直接将負載均衡的工作交給了 DNS,為網站管理維護省掉了很多麻煩,通路速度快,有效改善性能。
反向代理負載均衡
反向代理伺服器在提供負載均衡功能的同時,管理着一組 web 伺服器,根據負載均衡算法将請求的浏覽器通路轉發到不同的 web 伺服器處理,處理結果經過反向伺服器傳回給浏覽器。
網絡層 IP 負載均衡
網絡層通過修改目标位址進行負載均衡,該方式在響應請求時速度較反向伺服器負載均衡要快,但是,當請求資料較大(大型視訊或檔案)時,速度反應就會變慢。
MAC 層負載均衡
資料鍊路層修改 Mac 位址進行負載均衡,負載均衡伺服器的 IP 和它所管理的 web 服務群的虛拟 IP 一緻。它不需要負載均衡伺服器進行位址的轉換,但是對負載均衡伺服器的網卡帶寬要求較高。
硬體負載均衡
F5 的全稱是 F5-BIG-IP-GTM,硬體負載均衡裝置,其并發能力達到。該方式能夠實作多鍊路的負載均衡和備援,可以接入多條 ISP 鍊路,在鍊路之間實作負載均衡和高可用。
服務層架構設計
降速機制
即使我們擴充再多的應用服務,使用再多應用伺服器,部署再多的負載均衡器,都會遇到支撐不住海量請求的時候。
排隊處理
排隊處理就像我們日常買東西排隊一樣,将請求放入隊列中的,采用 FIFO(First Input First Output,先進先出),這樣的話,我們就不會導緻某些請求永遠擷取不到鎖。看到這裡,有一些将多線程處理方式變成單線程處理機制,會大大影響資料的效率和性能!
阻塞隊列
- ArrayBlockingQueue 是初始容量固定的阻塞隊列。
- ConcurrentLinkedQueue 使用的是 CAS 原語無鎖隊列實作,是一個異步隊列,入隊的速度很快,出隊進行了加鎖,性能稍慢。
- LinkedBlockingQueue 也是阻塞的隊列,入隊和出隊都用了加鎖,當隊空的時候線程會暫時阻塞。
分批放行
在同步排隊的基礎上,可以再加入一個分批放行執行機制,我們可以考慮達到預定門檻值以後,在進行相關的執行後端服務,這樣子可以提高一定的性能以及減少後端請求的次數和壓力,如下圖所示:
利用緩存和隊列技術減輕應用處理的壓力,通過異步請求的方式做到最終一緻性。
限流機制
漏桶算法
漏桶算法思路很簡單,水(請求)先進入到漏桶裡,漏桶以一定的速度出水,當水流入速度過大會直接溢出,可以看出漏桶算法能強行限制資料的傳輸速率。
- 設定漏桶流出速度及漏桶的總容量,在請求到達時判斷目前漏桶容量是否已滿,不滿則可将請求存入桶中,否則抛棄請求。
- 采用一個線程以設定的速率取出請求進行處理。
算法弊端
- 隻能以特定速率處理請求,如果速率設定太小則會浪費性能資源,設定太大則會造成資源不足。
- 無論輸入速率如何波動,均不會展現在服務端,即使資源有空餘,對于突發請求也無法及時處理,故對有突發請求處理需求時,不宜選擇該方法。
令牌桶算法
令牌桶算法的原理是系統會以一個恒定的速度往桶裡放入令牌,而如果請求需要被處理,則需要先從桶裡擷取一個令牌,當桶裡沒有令牌可取時,則拒絕服務。
實作原理
設定令牌桶中添加令牌的速率,并且設定桶中最大可存儲的令牌,當請求到達時,向桶中請求令牌(根據應用需求,可能為 1 個或多個),若令牌數量滿足要求,則删除對應數量的令牌并通過目前請求,若桶中令牌數不足則觸發限流規則。
為解決固定視窗計數帶來的周期切換處流量突發問題,可以使用滑動視窗計數。滑動視窗計算本質上也是固定視窗計數,差別在于将計數周期進行細化。
滑動視窗
滑動視窗計數法與固定視窗計數法相比較,除了計數周期 T 及周期内最大通路(調用)數 N 兩個參數,增加一個參數 M,用于設定周期 T 内的滑動視窗數。
資料通路層
由于要承受高并發,mysql 在高并發情況下的性能下降尤其嚴重。
資料更新點
需要針對于資料更新點進行控制!
悲觀鎖
可以從“悲觀鎖”的方向
- 悲觀鎖,也就是在修改資料的時候,采用鎖定狀态,排斥外部請求的修改。遇到加鎖的狀态,就必須等待。
- 雖然它解決了線程安全的問題,但是“高并發”場景下,會很多這樣的修改請求,每個請求都需要等待“鎖”,某些線程可能永遠都沒有機會搶到這個“鎖”,這種請求就會死在那裡。
- 這種請求會很多,瞬間增大系統的平均響應時間,結果是可用連接配接數被耗盡,系統陷入異常。
樂觀鎖
- 樂觀鎖,是相對于“悲觀鎖”采用更為寬松的加鎖機制,大都是采用帶版本号(Version)更新/通過狀态化進行更新操作機制。
- 所有請求都有資格去修改,但會獲得一個該資料的版本号,隻有版本号符合的才能更新成功,其他的傳回搶購失敗。
- 不需要考慮隊列的問題,不過,它會增大 CPU 的計算開銷。但是在沖突較小的時候,這是一個比較好的解決方案。