淘寶大秒系統設計詳解

一些數據:前端

你們還記得2013年的小米秒殺嗎?三款小米手機各11萬臺開賣,走的都是大秒系統,3分鐘後成爲雙十一第一家也是最快破億的旗艦店。通過日誌統計,前端系統雙11峯值有效請求約60w以上的QPS ,然後端cache的集羣峯值近2000w/s、單機也近30w/s,但到真正的寫時流量要小不少了,當時最高下單減庫存tps是紅米創造,達到1500/s。java

熱點隔離:程序員

秒殺系統設計的第一個原則就是將這種熱點數據隔離出來,不要讓1%的請求影響到另外的99%,隔離出來後也更方便對這1%的請求作針對性優化。針對秒殺咱們作了多個層次的隔離:redis

 

  • 業務隔離。把秒殺作成一種營銷活動,賣家要參加秒殺這種營銷活動須要單獨報名,從技術上來講,賣家報名後對咱們來講就是已知熱點,當真正開始時咱們能夠提早作好預熱。算法

  • 系統隔離。系統隔離更可能是運行時的隔離,能夠經過分組部署的方式和另外99%分開。秒殺還申請了單獨的域名,目的也是讓請求落到不一樣的集羣中。數據庫

  • 數據隔離。秒殺所調用的數據大部分都是熱數據,好比會啓用單獨cache集羣或MySQL數據庫來放熱點數據,目前也是不想0.01%的數據影響另外99.99%。apache

固然實現隔離頗有多辦法,如能夠按照用戶來區分,給不一樣用戶分配不一樣cookie,在接入層路由到不一樣服務接口中;還有在接入層能夠對URL的不一樣Path來設置限流策略等。服務層經過調用不一樣的服務接口;數據層能夠給數據打上特殊的標來區分。目的都是把已經識別出來的熱點和普通請求區分開來。後端

動靜分離:數組

前面介紹在系統層面上的原則是要作隔離,接下去就是要把熱點數據進行動靜分離,這也是解決大流量系統的一個重要原則。如何給系統作動靜分離的靜態化改造我之前寫過一篇《高訪問量系統的靜態化架構設計》詳細介紹了淘寶商品系統的靜態化設計思路,感興趣的能夠在《程序員》雜誌上找一下。咱們的大秒系統是從商品詳情繫統發展而來,因此自己已經實現了動靜分離,如圖1。瀏覽器

除此以外還有以下特色:

 

  • 把整個頁面Cache在用戶瀏覽器

  • 若是強制刷新整個頁面,也會請求到CDN

  • 實際有效請求只是「刷新搶寶」按鈕

這樣把90%的靜態數據緩存在用戶端或者CDN上,當真正秒殺時用戶只須要點擊特殊的按鈕「刷新搶寶」便可,而不須要刷新整個頁面,這樣只向服務端請求不多的有效數據,而不須要重複請求大量靜態數據。秒殺的動態數據和普通的詳情頁面的動態數據相比更少,性能也比普通的詳情提高3倍以上。因此「刷新搶寶」這種設計思路很好地解決了不刷新頁面就能請求到服務端最新的動態數據。

基於時間分片削峯

熟悉淘寶秒殺的都知道,初版的秒殺系統自己並無答題功能,後面才增長了秒殺答題,固然秒殺答題一個很重要的目的是爲了防止秒殺器,2011年秒殺很是火的時候,秒殺器也比較猖獗,而沒有達到全民參與和營銷的目的,因此增長的答題來限制秒殺器。增長答題後,下單的時間基本控制在2s後,秒殺器的下單比例也降低到5%如下。新的答題頁面如圖2。

 

其實增長答題還有一個重要的功能,就是把峯值的下單請求給拉長了,從之前的1s以內延長到2~10s左右,請求峯值基於時間分片了,這個時間的分片對服務端處理併發很是重要,會減輕很大壓力,另外因爲請求的前後,靠後的請求天然也沒有庫存了,也根本到不了最後的下單步驟,因此真正的併發寫就很是有限了。其實這種設計思路目前也很是廣泛,如支付寶的「咻一咻」已及微信的搖一搖。

 

除了在前端經過答題在用戶端進行流量削峯外,在服務端通常經過鎖或者隊列來控制瞬間請求。

數據分層校驗

對大流量系統的數據作分層校驗也是最重要的設計原則,所謂分層校驗就是對大量的請求作成「漏斗」式設計,如圖3所示:在不一樣層次儘量把無效的請求過濾,「漏斗」的最末端纔是有效的請求,要達到這個效果必須對數據作分層的校驗,下面是一些原則:

 

  • 先作數據的動靜分離

  • 將90%的數據緩存在客戶端瀏覽器

  • 將動態請求的讀數據Cache在Web端

  • 對讀數據不作強一致性校驗

  • 對寫數據進行基於時間的合理分片

  • 對寫請求作限流保護

  • 對寫數據進行強一致性校驗

秒殺系統正是按照這個原則設計的系統架構,如圖4所示。

把大量靜態不須要檢驗的數據放在離用戶最近的地方;在前端讀系統中檢驗一些基本信息,如用戶是否具備秒殺資格、商品狀態是否正常、用戶答題是否正確、秒殺是否已經結束等;在寫數據系統中再校驗一些如是不是非法請求,營銷等價物是否充足(淘金幣等),寫的數據一致性如檢查庫存是否還有等;最後在數據庫層保證數據最終準確性,如庫存不能減爲負數。

實時熱點發現:

其實秒殺系統本質是仍是一個數據讀的熱點問題,並且是最簡單一種,由於在文提到經過業務隔離,咱們已能提早識別出這些熱點數據,咱們能夠提早作一些保護,提早識別的熱點數據處理起來還相對簡單,好比分析歷史成交記錄發現哪些商品比較熱門,分析用戶的購物車記錄也能夠發現那些商品可能會比較好賣,這些都是能夠提早分析出來的熱點。比較困難的是那種咱們提早發現不了忽然成爲熱點的商品成爲熱點,這種就要經過實時熱點數據分析了,目前咱們設計能夠在3s內發現交易鏈路上的實時熱點數據,而後根據實時發現的熱點數據每一個系統作實時保護。 具體實現以下:

 

  • 構建一個異步的能夠收集交易鏈路上各個中間件產品如Tengine、Tair緩存、HSF等自己的統計的熱點key(Tengine和Tair緩存等中間件產品自己已經有熱點統計模塊)。

  • 創建一個熱點上報和能夠按照需求訂閱的熱點服務的下發規範,主要目的是經過交易鏈路上各個系統(詳情、購物車、交易、優惠、庫存、物流)訪問的時間差,把上游已經發現的熱點可以透傳給下游系統,提早作好保護。好比大促高峯期詳情繫統是最先知道的,在統計接入層上Tengine模塊統計的熱點URL。

  • 將上游的系統收集到熱點數據發送到熱點服務檯上,而後下游系統如交易系統就會知道哪些商品被頻繁調用,而後作熱點保護。如圖5所示。

重要的幾個:其中關鍵部分包括:

 

  • 這個熱點服務後臺抓取熱點數據日誌最好是異步的,一方面便於作到通用性,另外一方面不影響業務系統和中間件產品的主流程。

  • 熱點服務後臺、現有各個中間件和應用在作的沒有取代關係,每一箇中間件和應用還須要保護本身,熱點服務後臺提供一個收集熱點數據提供熱點訂閱服務的統一規範和工具,便於把各個系統熱點數據透明出來。

  • 熱點發現要作到實時(3s內)。

關鍵技術及優化點:

前面介紹了一些如何設計大流量讀系統中用到的原則,可是當這些手段都用了,仍是有大流量涌入該如何處理呢?秒殺系統要解決幾個關鍵問題。

 

Java處理大並發動態請求優化

 

其實Java和通用的Web服務器相比(Nginx或Apache)在處理大併發HTTP請求時要弱一點,因此通常咱們都會對大流量的Web系統作靜態化改造,讓大部分請求和數據直接在Nginx服務器或者Web代理服務器(Varnish、Squid等)上直接返回(能夠減小數據的序列化與反序列化),不要將請求落到Java層上,讓Java層只處理不多數據量的動態請求,固然針對這些請求也有一些優化手段能夠使用:

 

  • 直接使用Servlet處理請求。避免使用傳統的MVC框架也許能繞過一大堆複雜且用處不大的處理邏輯,節省個1ms時間,固然這個取決於你對MVC框架的依賴程度。

  • 直接輸出流數據。使用resp.getOutputStream()而不是resp.getWriter()能夠省掉一些不變字符數據編碼,也能提高性能;還有數據輸出時也推薦使用JSON而不是模板引擎(通常都是解釋執行)輸出頁面。

 

同一商品大併發讀問題

 

你會說這個問題很容易解決,無非放到Tair緩存裏面就行,集中式Tair緩存爲了保證命中率,通常都會採用一致性Hash,因此同一個key會落到一臺機器上,雖然咱們的Tair緩存機器單臺也能支撐30w/s的請求,可是像大秒這種級別的熱點商品還遠不夠,那如何完全解決這種單點瓶頸?答案是採用應用層的Localcache,即在秒殺系統的單機上緩存商品相關的數據,如何cache數據?也分動態和靜態:

 

  • 像商品中的標題和描述這些自己不變的會在秒殺開始以前全量推送到秒殺機器上並一直緩存直到秒殺結束。

  • 像庫存這種動態數據會採用被動失效的方式緩存必定時間(通常是數秒),失效後再去Tair緩存拉取最新的數據。

 

你可能會有疑問,像庫存這種頻繁更新數據一旦數據不一致會不會致使超賣?其實這就要用到咱們前面介紹的讀數據分層校驗原則了,讀的場景能夠容許必定的髒數據,由於這裏的誤判只會致使少許一些本來已經沒有庫存的下單請求誤認爲還有庫存而已,等到真正寫數據時再保證最終的一致性。這樣在數據的高可用性和一致性作平衡來解決這種高併發的數據讀取問題。

 

同一數據大併發更新問題

 

解決大併發讀問題採用Localcache和數據的分層校驗的方式,可是不管如何像減庫存這種大併發寫仍是避免不了,這也是秒殺這個場景下最核心的技術難題。

 

同一數據在數據庫裏確定是一行存儲(MySQL),因此會有大量的線程來競爭InnoDB行鎖,當併發度越高時等待的線程也會越多,TPS會降低RT會上升,數據庫的吞吐量會嚴重受到影響。說到這裏會出現一個問題,就是單個熱點商品會影響整個數據庫的性能,就會出現咱們不肯意看到的0.01%商品影響99.99%的商品,因此一個思路也是要遵循前面介紹第一個原則進行隔離,把熱點商品放到單獨的熱點庫中。可是無疑也會帶來維護的麻煩(要作熱點數據的動態遷移以及單獨的數據庫等)。

 

分離熱點商品到單獨的數據庫仍是沒有解決併發鎖的問題,要解決併發鎖有兩層辦法。

 

  • 應用層作排隊。按照商品維度設置隊列順序執行,這樣能減小同一臺機器對數據庫同一行記錄操做的併發度,同時也能控制單個商品佔用數據庫鏈接的數量,防止熱點商品佔用太多數據庫鏈接。

  • 數據庫層作排隊。應用層只能作到單機排隊,但應用機器數自己不少,這種排隊方式控制併發仍然有限,因此若是能在數據庫層作全局排隊是最理想的,淘寶的數據庫團隊開發了針對這種MySQL的InnoDB層上的patch,能夠作到數據庫層上對單行記錄作到併發排隊,如圖6所示。

你可能會問排隊和鎖競爭不要等待嗎?有啥區別?若是熟悉MySQL會知道,InnoDB內部的死鎖檢測以及MySQL Server和InnoDB的切換會比較耗性能,淘寶的MySQL核心團隊還作了不少其餘方面的優化,如COMMIT_ON_SUCCESS和ROLLBACK_ON_FAIL的patch,配合在SQL裏面加hint,在事務裏不須要等待應用層提交COMMIT而在數據執行完最後一條SQL後直接根據TARGET_AFFECT_ROW結果提交或回滾,能夠減小網絡的等待時間(平均約0.7ms)。據我所知,目前阿里MySQL團隊已將這些patch及提交給MySQL官方評審。

大促熱點問題思考:

以秒殺這個典型系統爲表明的熱點問題根據多年經驗我總結了些通用原則:隔離、動態分離、分層校驗,必須從整個全鏈路來考慮和優化每一個環節,除了優化系統提高性能,作好限流和保護也是必備的功課。

 

除去前面介紹的這些熱點問題外,淘系還有多種其餘數據熱點問題:

 

  • 數據訪問熱點,好比Detail中對某些熱點商品的訪問度很是高,即便是Tair緩存這種Cache自己也有瓶頸問題,一旦請求量達到單機極限也會存在熱點保護問題。有時看起來好像很容易解決,好比說作好限流就行,但你想一想一旦某個熱點觸發了一臺機器的限流閥值,那麼這臺機器Cache的數據都將無效,進而間接致使Cache被擊穿,請求落地應用層數據庫出現雪崩現象。這類問題須要與具體Cache產品結合纔能有比較好的解決方案,這裏提供一個通用的解決思路,就是在Cache的client端作本地Localcache,當發現熱點數據時直接Cache在client裏,而不要請求到Cache的Server。

  • 數據更新熱點,更新問題除了前面介紹的熱點隔離和排隊處理以外,還有些場景,如對商品的lastmodifytime字段更新會很是頻繁,在某些場景下這些多條SQL是能夠合併的,必定時間內只執行最後一條SQL就好了,能夠減小對數據庫的update操做。另外熱點商品的自動遷移,理論上也能夠在數據路由層來完成,利用前面介紹的熱點實時發現自動將熱點從普通庫裏遷移出來放到單獨的熱點庫中。

 

按照某種維度建的索引產生熱點數據,好比實時搜索中按照商品維度關聯評價數據,有些熱點商品的評價很是多,致使搜索系統按照商品ID建評價數據的索引時內存已經放不下,交易維度關聯訂單信息也一樣有這些問題。這類熱點數據須要作數據散列,再增長一個維度,把數據從新組織。

1、題目

1, 這是一個秒殺系統,即大量用戶搶有限的商品,先到先得

2, 用戶併發訪問流量很是大, 須要分佈式的機器集羣處理請求

3, 系統實現使用Java

2、模塊設計

1, 用戶請求分發模塊:使用Nginx或Apache將用戶的請求分發到不一樣的機器上。

2, 用戶請求預處理模塊:判斷商品是否是還有剩餘來決定是否是要處理該請求。

3, 用戶請求處理模塊:把經過預處理的請求封裝成事務提交給數據庫,並返回是否成功。

4, 數據庫接口模塊:該模塊是數據庫的惟一接口,負責與數據庫交互,提供RPC接口供查詢是否秒殺結束、剩餘數量等信息。

第一部分就很少說了,配置HTTP服務器便可,這裏主要談談後面的模塊。

用戶請求預處理模塊

通過HTTP服務器的分發後,單個服務器的負載相對低了一些,但總量依然可能很大,若是後臺商品已經被秒殺完畢,那麼直接給後來的請求返回秒殺失敗便可,沒必要再進一步發送事務了,示例代碼能夠以下所示:

package seckill;
import org.apache.http.HttpRequest;
/**
 * 預處理階段,把沒必要要的請求直接駁回,必要的請求添加到隊列中進入下一階段.
 */
public class PreProcessor {
	// 商品是否還有剩餘
	private static boolean reminds = true;
	private static void forbidden() {
		// Do something.
	}
	public static boolean checkReminds() {
		if (reminds) {
			// 遠程檢測是否還有剩餘,該RPC接口應由數據庫服務器提供,沒必要徹底嚴格檢查.
			if (!RPC.checkReminds()) {
				reminds = false;
			}
		}
		return reminds;
	}
	/**
	 * 每個HTTP請求都要通過該預處理.
	 */
	public static void preProcess(HttpRequest request) {
		if (checkReminds()) {
			// 一個併發的隊列
			RequestQueue.queue.add(request);
		} else {
			// 若是已經沒有商品了,則直接駁回請求便可.
			forbidden();
		}
	}
}

併發隊列的選擇

Java的併發包提供了三個經常使用的併發隊列實現,分別是:ConcurrentLinkedQueue 、 LinkedBlockingQueue 和 ArrayBlockingQueue。

ArrayBlockingQueue是初始容量固定的阻塞隊列,咱們能夠用來做爲數據庫模塊成功競拍的隊列,好比有10個商品,那麼咱們就設定一個10大小的數組隊列。

ConcurrentLinkedQueue使用的是CAS原語無鎖隊列實現,是一個異步隊列,入隊的速度很快,出隊進行了加鎖,性能稍慢。

LinkedBlockingQueue也是阻塞的隊列,入隊和出隊都用了加鎖,當隊空的時候線程會暫時阻塞。

因爲咱們的系統入隊需求要遠大於出隊需求,通常不會出現隊空的狀況,因此咱們能夠選擇ConcurrentLinkedQueue來做爲咱們的請求隊列實現:

package seckill;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.apache.http.HttpRequest;
public class RequestQueue {
    public static ConcurrentLinkedQueue<HttpRequest> queue =
            new ConcurrentLinkedQueue<HttpRequest>();
}

用戶請求模塊

package seckill;
import org.apache.http.HttpRequest;
public class Processor {
	/**
	 * 發送秒殺事務到數據庫隊列.
	 */
	public static void kill(BidInfo info) {
		DB.bids.add(info);
	}
	public static void process() {
		BidInfo info = new BidInfo(RequestQueue.queue.poll());
		if (info != null) {
			kill(info);
		}
	}
}
class BidInfo {
	BidInfo(HttpRequest request) {
		// Do something.
	}
}

數據庫模塊

數據庫主要是使用一個 ArrayBlockingQueue 來暫存有可能成功的用戶請求。

package seckill;
import java.util.concurrent.ArrayBlockingQueue;
/**
 * DB應該是數據庫的惟一接口.
 */
public class DB {
	public static int count = 10;
	public static ArrayBlockingQueue<BidInfo> bids = new ArrayBlockingQueue<BidInfo>(10);
	public static boolean checkReminds() {
		// TODO
		return true;
	}
	// 單線程操做
	public static void bid() {
		BidInfo info = bids.poll();
		while (count-- > 0) {
			// insert into table Bids values(item_id, user_id, bid_date, other)
			// select count(id) from Bids where item_id = ?
			// 若是數據庫商品數量大約總數,則標誌秒殺已完成,設置標誌位reminds = false.
			info = bids.poll();
		}
	}
}

3、結語

看起來大致這樣應該就能夠了,固然還有細節能夠優化,好比數據庫請求能夠都作成異步的等等。

 

現現在,春節搶紅包的活動已經逐漸變成你們過年的新風俗。親朋好友的相互饋贈,微信、微博、支付寶等各大平臺種類繁多的紅包讓你們收到手軟。雞年春節,鏈家也想給15萬的全國員工包個大紅包,因而咱們構建了一套旨在支撐10萬每秒請求峯值的搶紅包系統。經實踐證實,春節期間咱們成功的爲全部的小夥伴提供了高可靠的服務,紅包總髮放量近百萬,搶紅包的峯值流量達到3萬/秒,最快的一輪搶紅包活動3秒鐘全部紅包所有搶完,系統運行0故障。

紅包系統,相似於電商平臺的秒殺系統,本質上都是在一個很短的時間內面對巨大的請求流量,將有限的庫存商品分發出去,並完成交易操做。好比12306搶票,庫存的火車票是有限的,但瞬時的流量很是大,且都是在請求相同的資源,這裏面數據庫的併發讀寫衝突以及資源的鎖請求衝突很是嚴重。就咱們實現這樣一個紅包系統自己來講,面臨着以下的一些挑戰:

首先,到活動整點時刻,咱們有15萬員工在固定時間點同時涌入系統搶某輪紅包,瞬間的流量是很大的,而目前咱們整個鏈路上的系統和服務基礎設施,都沒有承受過如此高的吞吐量,要在短期內實現業務需求,在技術上的風險較大。

其次,公司是第一次開展這樣的活動,咱們很難預知你們參與活動的狀況,極端狀況下可能會出現某輪紅包沒搶完,須要合併到下輪接着發放。這就要求系統有一個動態的紅包發放策略和預算控制,其中涉及到的動態計算會是個較大的問題(這也是爲系統高吞吐服務),實際的系統實現中咱們採用了一些預處理機制。

最後,這個系統是爲了春節的慶祝活動而研發的定製系統,且只上線運行一次,這意味着咱們沒法積累經驗去對服務作持續的優化。而且相關的配套環境沒有通過實際運行檢驗,缺乏參考指標,系統的薄弱環節發現的難度大。因此必需要追求設計至簡,儘可能減小對環境的依賴(數據路徑越長,出問題的環節越多),而且實現高可伸縮性,須要盡一切努力保證可靠性,即便有某環節失誤,系統依然可以保障核心的用戶體驗正常。

系統設計

系統架構圖如圖所示。全部的靜態資源提早部署在了第三方的CDN服務上,系統的核心功能主要劃分到接入層和核心邏輯系統中,各自部署爲集羣模式而且獨立。接入層主要是對用戶身份鑑權和結果緩存,核心系統重點關注紅包的分發,紅色實線的模塊是核心邏輯,爲了保障其可靠性,咱們作了包括數據預處理、水平分庫、多級緩存、精簡RPC調用、過載保護等多項設計優化,而且在原生容器、MySQL等服務基礎設施上針對特殊的業務場景作了優化,後面將爲讀者一一道來。

紅包自己的信息經過預處理資源接口獲取。運行中用戶和紅包的映射關係動態生成。底層使用內部開發的DB中間件在MySQL數據庫集羣上作紅包發放結果持久化,以供異步支付紅包金額到用戶帳戶使用。整個系統的絕大部分模塊都有性能和保活監控。

優化方案

優化方案中最重要的目標是保障關鍵流程在應對大量請求時穩定運行,這須要很高的系統可用性。因此,業務流程和數據流程要儘可能精簡,減小容易出錯的環節。此外,緩存、DB、網絡、容器環境,任何一個部分都要假設可能會短時出現故障,要有處理預案。針對以上的目標難點,咱們總結了以下的實踐經驗。

1.數據預處理

紅包自己的屬性信息(金額,狀態,祝福語,發放策略),咱們結合活動預案要求,使用必定的算法提早生成好全部的信息,數據總的空間不是很大。爲了最大化提高性能,這些紅包數據,咱們事先存儲在數據庫中,而後在容器加載服務啓動時,直接加載到本地緩存中看成只讀數據。另外,咱們的員工信息,咱們也作了必定的裁剪,最基本的信息也和紅包數據同樣,預先生成,服務啓動時加載。

此外,咱們的活動頁面,有不少視頻和圖片資源,若是這麼多的用戶從咱們的網關實時訪問,極可能咱們的帶寬直接就被這些大流量的請求佔滿了,用戶體驗可想而知。最後這些靜態資源,咱們都部署在了CDN上,經過數據預熱的方式加速客戶端的訪問速度,網關的流量主要是來自於搶紅包期間的小數據請求。

2.精簡RPC調用

一般的服務請求流程,是在接入層訪問用戶中心進行用戶鑑權,而後轉發請求到後端服務,後端服務根據業務邏輯調用其餘上游服務,而且查詢數據庫資源,再更新服務/數據庫的數據。每一次RPC調用都會有額外的開銷,因此,好比上一點所說的預加載,使得系統在運行期間每一個節點都有全量的查詢數據可在本地訪問,搶紅包的核心流程就被簡化爲了生成紅包和人的映射關係,以及發放紅包的後續操做。再好比,咱們採用了異步拉的方式進行紅包發放到帳,用戶搶紅包的請求再也不通過發放這一步,只記錄關係,性能獲得進一步提高。

實際上有些作法的可伸縮性是極強的。例如紅包數據的預生成信息,在當時的場景下咱們是可以做爲本地內存緩存加速訪問的。當紅包數據量很大的時候,在每一個服務節點上使用本地數據庫,或者本地數據文件,甚至是本地Redis/MC緩存服務,都是能夠保證空間足夠的,而且還有額外的好處,越少的RPC,越少的服務抖動,只須要關注系統自己的健壯性便可,不須要考慮外部系統QoS。

3.搶紅包的併發請求處理

春節整點時刻,同一個紅包會被成千上萬的人同時請求,如何控制併發請求,確保紅包會且僅會被一個用戶搶到?

作法一,使用加鎖操做先佔有鎖資源,再佔有紅包。

能夠使用分佈式全局鎖的方式(各類分佈式鎖組件或者數據庫鎖),申請lock該紅包資源成功後再作後續操做。優勢是,不會出現髒數據問題,某一個時刻只有一個應用線程持有lock,紅包只會被至多一個用戶搶到,數據一致性有保障。缺點是,全部請求同一時刻都在搶紅包A,下一個時刻又都在搶紅包B,而且只有一個搶成功,其餘都失敗,效率很低。

作法二,單獨開發請求排隊調度模塊。

排隊模塊接收用戶的搶紅包請求,以FIFO模式保存下來,調度模塊負責FIFO隊列的動態調度,一旦有空閒資源,便從隊列頭部把用戶的訪問請求取出後交給真正提供服務的模塊處理。優勢是,具備中心節點的統一資源管理,對系統的可控性強,可深度定製。缺點是,全部請求流量都會有中心節點參與,效率必然會比分佈式無中心繫統低,而且,中心節點也很容易成爲整個系統的性能瓶頸。

作法三,巧用Redis特性,使其成爲分佈式序號生成器。(咱們最終採用的作法)。

前文已經提到,紅包系統所使用的紅包數據都是預先生成好的,咱們使用數字ID來標識,這個ID是全局惟一的,全部圍繞紅包的操做都使用這個ID做爲數據的關聯項。在實際的請求流量過來時,咱們採用了「分組」處理流量的方式,以下圖所示。

訪問請求被LB分發到每一個分組,一個分組包含若干臺應用容器、獨立的數據庫和Redis節點。Redis節點內存儲的是這個分組能夠分發的紅包ID號段,利用Redis單進程的自減數值特性實現分佈式紅包ID生成器,服務經過此獲取當前拆到的紅包。落地數據都持久化在獨立的數據庫中,至關因而作了水平分庫。某個分組內處理的請求,只會訪問分組內部的Redis和數據庫,和其餘分組隔離開。

分組的方式使得整個系統實現了高內聚,低耦合的原則,能將數據流量分而治之,提高了系統的可伸縮性,當面臨更大流量的需求時,經過線性擴容的方法,便可應對。而且當單個節點出現故障時,影響面可以控制在單個分組內部,系統也就具備了較好的隔離性。

4.系統容量評估,藉助數據優化,過載保護

因爲是首次開展活動,咱們缺少實際的運營數據,一切都是摸着石頭過河。因此從項目伊始,咱們便強調對系統各個層次的預估,既包括了活動參與人數、每一個功能feature用戶的高峯流量、後端請求的峯值、緩存系統請求峯值和數據庫讀寫請求峯值等,還包括了整個業務流程和服務基礎設施中潛在的薄弱環節。後者的難度更大由於很難量化。此前咱們連超大流量的全鏈路性能壓測工具都較缺少,因此仍是有不少實踐的困難的。

在這裏心裏真誠的感謝開源社區的力量,在咱們制定完系統的性能指標參考值後,藉助如wrk等優秀的開源工具,咱們在有限的資源裏實現了對整個系統的端到端全鏈路壓測。實測中,咱們的核心接口在單個容器上能夠達到20,000以上的QPS,整個服務集羣在110,000以上的QPS壓力下依然能穩定工做。

正是一次次的全鏈路壓測參考指標,幫助咱們瞭解了性能的基準,並以此作了代碼設計層面、容器層面、JVM層面、MySQL數據庫層面、緩存集羣層面的種種優化,極大的提高了系統的可用性。具體作法限於篇幅不在此贅述,有興趣的讀者歡迎交流。

此外,爲了確保線上有超預估流量時系統穩定,咱們作了過載保護。超過性能上限閾值的流量,系統會快速返回特定的頁面結果,將此部分流量清理掉,保障已經接受的有效流量能夠正常處理。

5.完善監控

系統在線上運行過程當中,咱們很須要對其實時的運行狀況獲取信息,以便可以對出現的問題進行排查定位,及時採起措施。因此咱們必須有一套有效的監控系統,可以幫咱們觀測到關鍵的指標。在實際的操做層面,咱們主要關注了以下指標:

服務接口的性能指標

藉助系統的請求日誌,觀測服務接口的QPS,接口總的實時響應時間。同時經過HTTP的狀態碼觀測服務的語義層面的可用性。

系統健康度

結合總的性能指標以及各個模塊應用層的性能日誌,包括模塊接口返回耗時,和應用層日誌的邏輯錯誤日誌等,判斷系統的健康度。

總體的網絡情況

儘可能觀測每一個點到點之間的網絡狀態,包括應用服務器的網卡流量、Redis節點、數據庫節點的流量,以及入口帶寬的佔用狀況。若是某條線路出現太高流量,即可及時採起擴容等措施緩解。

服務基礎設施

應用服務器的CPU、Memory、磁盤IO情況,緩存節點和數據庫的相應的數據,以及他們的鏈接數、鏈接時間、資源消耗檢測數據,及時的去發現資源不足的預警信息。

對於關鍵的數據指標,在超過預估時制定的閾值時,還須要監控系統可以實時的經過手機和郵件實時通知的方式讓相關人員知道。另外,咱們在系統中還作了若干邏輯開關,當某些資源出現問題而且自動降級和過載保護模塊失去效果時,咱們能夠根據情況直接人工介入,在服務不停機的前提早經過手動觸發邏輯開關改變系統邏輯,達到快速響應故障,讓服務儘快恢復穩定的目的。

6.服務降級

當服務器壓力劇增的時候,若是某些依賴的服務設施或者基礎組件超出了工做負荷能力,發生了故障,這時候極其須要根據當前的業務運行狀況對系統服務進行有策略的降級運行措施,使得核心的業務流程可以順利進行,而且減輕服務器資源的壓力,最好在壓力減少後還能自動恢復升級到原工做機制。

咱們在開發紅包系統時,考慮到原有IDC機房的解決方案對於彈性擴容和流量帶寬支持不太完美,選擇了使用AWS的公有云做爲服務基礎環境。對於第三方的服務,缺乏實踐經驗的把握,因而從開發到運維過程當中,咱們都保持了一種防護式的思考方式,包括數據庫、緩存節點故障,以及應用服務環境的崩潰、網絡抖動,咱們都認爲隨時可能出問題,都須要對應的自動替換降級策略,嚴重時甚至可經過手動觸發配置開關修改策略。固然,若是組件自身具備降級功能,能夠給上層業務節約不少成本資源,要本身實現所有環節的降級能力的確是一件比較耗費資源的事情,這也是一個公司技術慢慢積累的過程。

結束語

以上是咱們整個系統研發運維的一些體會。此次春節紅包活動,在資源有限的狀況下成功抵抗超乎日常的流量峯值壓力,對於技術而言是一次很大的挑戰,也是一件快樂的事情,讓咱們從中積累了不少實踐經驗。將來咱們將不斷努力,但願可以將部分轉化成較爲通用的技術,去更好的推進業務成功。真誠但願本文的分享可以對你們的技術工做有所幫助。

電商的秒殺和搶購,對咱們來講,都不是一個陌生的東西。然而,從技術的角度來講,這對於Web系統是一個巨大的考驗。當一個Web系統,在一秒鐘內收到數以萬計甚至更多請求時,系統的優化和穩定相當重要。此次咱們會關注秒殺和搶購的技術實現和優化,同時,從技術層面揭開,爲何咱們老是不容易搶到火車票的緣由?

1、大規模併發帶來的挑戰

在過去的工做中,我曾經面對過5w每秒的高併發秒殺功能,在這個過程當中,整個Web系統遇到了不少的問題和挑戰。若是Web系統不作針對性的優化,會垂手可得地陷入到異常狀態。咱們如今一塊兒來討論下,優化的思路和方法哈。

1. 請求接口的合理設計

一個秒殺或者搶購頁面,一般分爲2個部分,一個是靜態的HTML等內容,另外一個就是參與秒殺的Web後臺請求接口。

一般靜態HTML等內容,是經過CDN的部署,通常壓力不大,核心瓶頸實際上在後臺請求接口上。這個後端接口,必須可以支持高併發請求,同時,很是重要的一點,必須儘量「快」,在最短的時間裏返回用戶的請求結果。爲了實現儘量快這一點,接口的後端存儲使用內存級別的操做會更好一點。仍然直接面向MySQL之類的存儲是不合適的,若是有這種複雜業務的需求,都建議採用異步寫入。

固然,也有一些秒殺和搶購採用「滯後反饋」,就是說秒殺當下不知道結果,一段時間後才能夠從頁面中看到用戶是否秒殺成功。可是,這種屬於「偷懶」行爲,同時給用戶的體驗也很差,容易被用戶認爲是「暗箱操做」。

2. 高併發的挑戰:必定要「快」

咱們一般衡量一個Web系統的吞吐率的指標是QPS(Query Per Second,每秒處理請求數),解決每秒數萬次的高併發場景,這個指標很是關鍵。舉個例子,咱們假設處理一個業務請求平均響應時間爲100ms,同時,系統內有20臺Apache的Web服務器,配置MaxClients爲500個(表示Apache的最大鏈接數目)。

那麼,咱們的Web系統的理論峯值QPS爲(理想化的計算方式):

20*500/0.1 = 100000 (10萬QPS)

咦?咱們的系統彷佛很強大,1秒鐘能夠處理完10萬的請求,5w/s的秒殺彷佛是「紙老虎」哈。實際狀況,固然沒有這麼理想。在高併發的實際場景下,機器都處於高負載的狀態,在這個時候平均響應時間會被大大增長。

就Web服務器而言,Apache打開了越多的鏈接進程,CPU須要處理的上下文切換也越多,額外增長了CPU的消耗,而後就直接致使平均響應時間增長。所以上述的MaxClient數目,要根據CPU、內存等硬件因素綜合考慮,絕對不是越多越好。能夠經過Apache自帶的abench來測試一下,取一個合適的值。而後,咱們選擇內存操做級別的存儲的Redis,在高併發的狀態下,存儲的響應時間相當重要。網絡帶寬雖然也是一個因素,不過,這種請求數據包通常比較小,通常不多成爲請求的瓶頸。負載均衡成爲系統瓶頸的狀況比較少,在這裏不作討論哈。

那麼問題來了,假設咱們的系統,在5w/s的高併發狀態下,平均響應時間從100ms變爲250ms(實際狀況,甚至更多):

20*500/0.25 = 40000 (4萬QPS)

因而,咱們的系統剩下了4w的QPS,面對5w每秒的請求,中間相差了1w。

而後,這纔是真正的惡夢開始。舉個例子,高速路口,1秒鐘來5部車,每秒經過5部車,高速路口運做正常。忽然,這個路口1秒鐘只能經過4部車,車流量仍然依舊,結果一定出現大塞車。(5條車道突然變成4條車道的感受)

同理,某一個秒內,20*500個可用鏈接進程都在滿負荷工做中,卻仍然有1萬個新來請求,沒有鏈接進程可用,系統陷入到異常狀態也是預期以內。

其實在正常的非高併發的業務場景中,也有相似的狀況出現,某個業務請求接口出現問題,響應時間極慢,將整個Web請求響應時間拉得很長,逐漸將Web服務器的可用鏈接數佔滿,其餘正常的業務請求,無鏈接進程可用。

更可怕的問題是,是用戶的行爲特色,系統越是不可用,用戶的點擊越頻繁,惡性循環最終致使「雪崩」(其中一臺Web機器掛了,致使流量分散到其餘正常工做的機器上,再致使正常的機器也掛,而後惡性循環),將整個Web系統拖垮。

3. 重啓與過載保護

若是系統發生「雪崩」,貿然重啓服務,是沒法解決問題的。最多見的現象是,啓動起來後,馬上掛掉。這個時候,最好在入口層將流量拒絕,而後再將重啓。若是是redis/memcache這種服務也掛了,重啓的時候須要注意「預熱」,而且極可能須要比較長的時間。

秒殺和搶購的場景,流量每每是超乎咱們系統的準備和想象的。這個時候,過載保護是必要的。若是檢測到系統滿負載狀態,拒絕請求也是一種保護措施。在前端設置過濾是最簡單的方式,可是,這種作法是被用戶「千夫所指」的行爲。更合適一點的是,將過載保護設置在CGI入口層,快速將客戶的直接請求返回。

2、做弊的手段:進攻與防守

秒殺和搶購收到了「海量」的請求,實際上裏面的水分是很大的。很多用戶,爲了「搶「到商品,會使用「刷票工具」等類型的輔助工具,幫助他們發送儘量多的請求到服務器。還有一部分高級用戶,製做強大的自動請求腳本。這種作法的理由也很簡單,就是在參與秒殺和搶購的請求中,本身的請求數目佔比越多,成功的機率越高。

這些都是屬於「做弊的手段」,不過,有「進攻」就有「防守」,這是一場沒有硝煙的戰鬥哈。

1. 同一個帳號,一次性發出多個請求

部分用戶經過瀏覽器的插件或者其餘工具,在秒殺開始的時間裏,以本身的帳號,一次發送上百甚至更多的請求。實際上,這樣的用戶破壞了秒殺和搶購的公平性。

這種請求在某些沒有作數據安全處理的系統裏,也可能形成另一種破壞,致使某些判斷條件被繞過。例如一個簡單的領取邏輯,先判斷用戶是否有參與記錄,若是沒有則領取成功,最後寫入到參與記錄中。這是個很是簡單的邏輯,可是,在高併發的場景下,存在深深的漏洞。多個併發請求經過負載均衡服務器,分配到內網的多臺Web服務器,它們首先向存儲發送查詢請求,而後,在某個請求成功寫入參與記錄的時間差內,其餘的請求獲查詢到的結果都是「沒有參與記錄」。這裏,就存在邏輯判斷被繞過的風險。

 

應對方案:

在程序入口處,一個帳號只容許接受1個請求,其餘請求過濾。不只解決了同一個帳號,發送N個請求的問題,還保證了後續的邏輯流程的安全。實現方案,能夠經過Redis這種內存緩存服務,寫入一個標誌位(只容許1個請求寫成功,結合watch的樂觀鎖的特性),成功寫入的則能夠繼續參加。

或者,本身實現一個服務,將同一個帳號的請求放入一個隊列中,處理完一個,再處理下一個。

2. 多個帳號,一次性發送多個請求

不少公司的帳號註冊功能,在發展早期幾乎是沒有限制的,很容易就能夠註冊不少個帳號。所以,也致使了出現了一些特殊的工做室,經過編寫自動註冊腳本,積累了一大批「殭屍帳號」,數量龐大,幾萬甚至幾十萬的帳號不等,專門作各類刷的行爲(這就是微博中的「殭屍粉「的來源)。舉個例子,例如微博中有轉發抽獎的活動,若是咱們使用幾萬個「殭屍號」去混進去轉發,這樣就能夠大大提高咱們中獎的機率。

這種帳號,使用在秒殺和搶購裏,也是同一個道理。例如,iPhone官網的搶購,火車票黃牛黨。

應對方案:

這種場景,能夠經過檢測指定機器IP請求頻率就能夠解決,若是發現某個IP請求頻率很高,能夠給它彈出一個驗證碼或者直接禁止它的請求:

 

  1. 彈出驗證碼,最核心的追求,就是分辨出真實用戶。所以,你們可能常常發現,網站彈出的驗證碼,有些是「鬼神亂舞」的樣子,有時讓咱們根本沒法看清。他們這樣作的緣由,其實也是爲了讓驗證碼的圖片不被輕易識別,由於強大的「自動腳本」能夠經過圖片識別裏面的字符,而後讓腳本自動填寫驗證碼。實際上,有一些很是創新的驗證碼,效果會比較好,例如給你一個簡單問題讓你回答,或者讓你完成某些簡單操做(例如百度貼吧的驗證碼)。
  2. 直接禁止IP,其實是有些粗暴的,由於有些真實用戶的網絡場景剛好是同一出口IP的,可能會有「誤傷「。可是這一個作法簡單高效,根據實際場景使用能夠得到很好的效果。

 

3. 多個帳號,不一樣IP發送不一樣請求

所謂道高一尺,魔高一丈。有進攻,就會有防守,永不休止。這些「工做室」,發現你對單機IP請求頻率有控制以後,他們也針對這種場景,想出了他們的「新進攻方案」,就是不斷改變IP。

有同窗會好奇,這些隨機IP服務怎麼來的。有一些是某些機構本身佔據一批獨立IP,而後作成一個隨機代理IP的服務,有償提供給這些「工做室」使用。還有一些更爲黑暗一點的,就是經過木馬黑掉普通用戶的電腦,這個木馬也不破壞用戶電腦的正常運做,只作一件事情,就是轉發IP包,普通用戶的電腦被變成了IP代理出口。經過這種作法,黑客就拿到了大量的獨立IP,而後搭建爲隨機IP服務,就是爲了掙錢。

應對方案:

說實話,這種場景下的請求,和真實用戶的行爲,已經基本相同了,想作分辨很困難。再作進一步的限制很容易「誤傷「真實用戶,這個時候,一般只能經過設置業務門檻高來限制這種請求了,或者經過帳號行爲的」數據挖掘「來提早清理掉它們。

殭屍帳號也仍是有一些共同特徵的,例如帳號極可能屬於同一個號碼段甚至是連號的,活躍度不高,等級低,資料不全等等。根據這些特色,適當設置參與門檻,例如限制參與秒殺的帳號等級。經過這些業務手段,也是能夠過濾掉一些殭屍號。

4. 火車票的搶購

看到這裏,同窗們是否明白你爲何搶不到火車票?若是你只是老老實實地去搶票,真的很難。經過多帳號的方式,火車票的黃牛將不少車票的名額佔據,部分強大的黃牛,在處理驗證碼方面,更是「技高一籌「。

高級的黃牛刷票時,在識別驗證碼的時候使用真實的人,中間搭建一個展現驗證碼圖片的中轉軟件服務,真人瀏覽圖片並填寫下真實驗證碼,返回給中轉軟件。對於這種方式,驗證碼的保護限制做用被廢除了,目前也沒有很好的解決方案。

由於火車票是根據身份證明名制的,這裏還有一個火車票的轉讓操做方式。大體的操做方式,是先用買家的身份證開啓一個搶票工具,持續發送請求,黃牛帳號選擇退票,而後黃牛買家成功經過本身的身份證購票成功。當一列車箱沒有票了的時候,是沒有不少人盯着看的,何況黃牛們的搶票工具也很強大,即便讓咱們看見有退票,咱們也不必定能搶得過他們哈。

最終,黃牛順利將火車票轉移到買家的身份證下。

解決方案:

並無很好的解決方案,惟一能夠動心思的也許是對帳號數據進行「數據挖掘」,這些黃牛帳號也是有一些共同特徵的,例如常常搶票和退票,節假日異常活躍等等。將它們分析出來,再作進一步處理和甄別。

3、高併發下的數據安全

咱們知道在多線程寫入同一個文件的時候,會存現「線程安全」的問題(多個線程同時運行同一段代碼,若是每次運行結果和單線程運行的結果是同樣的,結果和預期相同,就是線程安全的)。若是是MySQL數據庫,能夠使用它自帶的鎖機制很好的解決問題,可是,在大規模併發的場景中,是不推薦使用MySQL的。秒殺和搶購的場景中,還有另一個問題,就是「超發」,若是在這方面控制不慎,會產生髮送過多的狀況。咱們也曾經據說過,某些電商搞搶購活動,買家成功拍下後,商家卻不認可訂單有效,拒絕發貨。這裏的問題,也許並不必定是商家奸詐,而是系統技術層面存在超發風險致使的。

1. 超發的緣由

假設某個搶購場景中,咱們一共只有100個商品,在最後一刻,咱們已經消耗了99個商品,僅剩最後一個。這個時候,系統發來多個併發請求,這批請求讀取到的商品餘量都是99個,而後都經過了這一個餘量判斷,最終致使超發。(同文章前面說的場景)

在上面的這個圖中,就致使了併發用戶B也「搶購成功」,多讓一我的得到了商品。這種場景,在高併發的狀況下很是容易出現。

2. 悲觀鎖思路

解決線程安全的思路不少,能夠從「悲觀鎖」的方向開始討論。

悲觀鎖,也就是在修改數據的時候,採用鎖定狀態,排斥外部請求的修改。遇到加鎖的狀態,就必須等待。

雖然上述的方案的確解決了線程安全的問題,可是,別忘記,咱們的場景是「高併發」。也就是說,會不少這樣的修改請求,每一個請求都須要等待「鎖」,某些線程可能永遠都沒有機會搶到這個「鎖」,這種請求就會死在那裏。同時,這種請求會不少,瞬間增大系統的平均響應時間,結果是可用鏈接數被耗盡,系統陷入異常。

3. FIFO隊列思路

那好,那麼咱們稍微修改一下上面的場景,咱們直接將請求放入隊列中的,採用FIFO(First Input First Output,先進先出),這樣的話,咱們就不會致使某些請求永遠獲取不到鎖。看到這裏,是否是有點強行將多線程變成單線程的感受哈。

而後,咱們如今解決了鎖的問題,所有請求採用「先進先出」的隊列方式來處理。那麼新的問題來了,高併發的場景下,由於請求不少,極可能一瞬間將隊列內存「撐爆」,而後系統又陷入到了異常狀態。或者設計一個極大的內存隊列,也是一種方案,可是,系統處理完一個隊列內請求的速度根本沒法和瘋狂涌入隊列中的數目相比。也就是說,隊列內的請求會越積累越多,最終Web系統平均響應時候仍是會大幅降低,系統仍是陷入異常。

4. 樂觀鎖思路

這個時候,咱們就能夠討論一下「樂觀鎖」的思路了。樂觀鎖,是相對於「悲觀鎖」採用更爲寬鬆的加鎖機制,大都是採用帶版本號(Version)更新。實現就是,這個數據全部請求都有資格去修改,但會得到一個該數據的版本號,只有版本號符合的才能更新成功,其餘的返回搶購失敗。這樣的話,咱們就不須要考慮隊列的問題,不過,它會增大CPU的計算開銷。可是,綜合來講,這是一個比較好的解決方案。

有不少軟件和服務都「樂觀鎖」功能的支持,例如Redis中的watch就是其中之一。經過這個實現,咱們保證了數據的安全。

4、小結

互聯網正在高速發展,使用互聯網服務的用戶越多,高併發的場景也變得愈來愈多。電商秒殺和搶購,是兩個比較典型的互聯網高併發場景。雖然咱們解決問題的具體技術方案可能千差萬別,可是遇到的挑戰倒是類似的,所以解決問題的思路也殊途同歸。

相關文章
相關標籤/搜索