京東毫秒級熱key探測框架設計與實踐,已完美支撐618大促

在擁有大量併發用戶的系統中,熱key一直以來都是一個不可避免的問題。或許是突然某些商品成了爆款,或許是海量用戶突然湧入某個店鋪,或許是秒殺時瞬間大量開啓的爬蟲用戶, 這些突發的無法預先感知的熱key都是系統潛在的巨大風險。

風險是什麼呢?主要是數據層,其次是服務層。

熱key對數據層的衝擊顯而易見,譬如數據存放在redis或者MySQL中,以redis爲例,那個未知的熱數據會按照hash規則被存在於某個redis分片上,平時使用時都從該分片獲取它的數據。由於redis性能還不錯,再加上集羣模式,每秒我們假設它能支撐20萬次讀取,這足以支持大部分的日常使用了。但是,以京東爲例的這些頭部互聯網公司,動輒某個爆品,會瞬間引入每秒上百萬甚至數百萬的請求,當然流量多數會在幾秒內就消失。但就是這短短的幾秒的熱key,就會瞬間造成其所在redis分片集羣癱瘓。原因也很簡單,redis作爲一個單線程的結構,所有的請求到來後都會去排隊,當請求量遠大於自身處理能力時,後面的請求會陷入等待、超時。由於該redis分片完全被這個key的請求給打滿,導致該分片上所有其他數據操作都無法繼續提供服務,也就是熱key不僅僅影響自己,還會影響和它合租的數據。很顯然,在這個極短的時間窗口內,我們是無法快速擴容10倍以上redis來支撐這個熱點的。雖然redis已經很優秀,但是它的內心是這樣的:

熱key對服務層的影響也不可小視,譬如你原本有1000臺Tomcat,每臺每秒能支撐1000QPS,假設數據層穩定、這樣服務層每秒能承接100萬個請求。但是由於某個爆品的出現、或者由於大促優惠活動,突發大批機器人以遠超正常用戶的速度發起極其密集的請求,這些機器人只需要很小的代價就能發出百倍於普通用戶的請求量,從而大幅擠佔正常用戶的資源。原本能承接100萬,現在來了150萬,其中50萬個是機器人請求,那麼就導致了至少1/3的正常用戶無法訪問,帶來較差的用戶體驗。

根據以上的場景,我們可以總結出來什麼是有危害的熱key。

什麼是熱key

1 MySQL等數據庫會被頻繁訪問的熱數據

     如爆款商品的skuId

2 redis的被密集訪問的key

     如爆款商品的各維度信息,skuId、shopId等等

3 機器人、爬蟲、刷子用戶

     如用戶的userId、uuid、ip等

4 某個接口地址

     如/sku/query

或者更精細維度的

5 用戶id+接口信息

     如userId + /sku/query,這代表某個用戶訪問某個接口的頻率

6 服務器id+接口信息

    如ip + /sku/query,這代表某臺服務器某個接口被訪問的頻率

7 用戶id+接口信息+具體商品

    如userId + /sku/query + skuId,這代表某個用戶訪問某個商品的頻率

以上我們都稱之爲有風險的key,注意,我們的熱key探測框架只關心key,其實就是一個字符串,隨意怎麼組合成這個字符串由使用者自己決定,所以該框架具備非常強的靈活性,可以完成熱數據探測、限流熔斷、統計等多種功能。

以往熱key問題怎麼解決

我們分別以redis的熱key、刷子用戶、限流等典型的場景來看。

redis熱key:

    這種以往的解決方式比較百花齊放,比較常見的有:

    1》上二級緩存,讀取到redis的key-value信息後,就直接寫入到jvm緩存一份,設置個過期時間,設置個淘汰策略譬如隊列滿時淘汰最先加入的。特點就是無腦緩存,不關心數據是不是熱點,緩存數據在應用集羣內無法達成一致性。

    2》改寫redis源碼加入熱點探測功能,有熱key時推送到jvm。問題主要是不通用,且有一定難度。

    3》改寫jedis、letture等redis客戶端的jar,通過本地計算來探測熱點key,是熱key的就本地緩存起來並通知集羣內其他機器。

    4》其他

刷子爬蟲用戶:

   常見的有:

   1》日常累積後,將這批黑名單通過配置中心推送到jvm內存。存在滯後無法實時感知的問題。

   2》通過本地累加,進行實時計算,單位時間內超過閾值的算刷子。如果服務器比較多,存在用戶請求被分散,本地計算達不到甄別刷子的問題。

   3》引入其他組件如redis,進行集中式累加計算,超過閾值的拉取到本地內存。問題就是需要頻繁讀寫redis,依舊存在redis的性能瓶頸問題。

限流:

    1》單機維度的接口限流多采用本地累加計數

    2》集羣維度的多采用第三方中間件,如sentinel

    3》網關層的,如Nginx+lua

綜上,我們會發現雖然它們都可以歸結到熱key這個領域內,但是並沒有一個統一的解決方案,我們更期望於有一個統一的框架,它能解決所有的對熱key有實時感知的場景,最好是無論是什麼key、是什麼維度,只要我拼接好這個字符串,把它交給框架去探測,設定好判定爲熱的閾值(如2秒該字符串出現20次),則毫秒時間內,該熱key就能進入到應用的jvm內存中,並且在整個服務集羣內保持一致性,要有都有,要刪全刪。

熱key進內存後的優勢

熱key問題歸根到底就是如何找到熱key,並將熱key放到jvm內存的問題。只要該key在內存裏,我們就能極快地來對它做邏輯,內存訪問和redis訪問的速度不在一個量級。

譬如刷子用戶,我們可以對其屏蔽、降級、限制訪問速度。熱接口,我們可以進行限流,返回默認值。redis的熱key,我們可以極大地提高訪問速度。

以redis訪問key爲例,我們可以很容易的計算出性能指標,譬如有1000臺服務器,某key所在的redis集羣能支撐20萬/s的訪問,那麼平均每臺機器每秒大概能訪問該key200次,超過的部分就會進入等待。由於redis的瓶頸,將極大地限制server的性能。

而如果該key是在本地內存中,讀取一個內存中的值,每秒多少個萬次都是很正常的,不存在任何數據層的瓶頸。當然,如果通過增加redis集羣規模的形式,也能提升數據的訪問上限,但問題是事先不知道熱key在哪裏,而全量增加redis的規模,帶來的成本提升又不可接受。

熱key探測的關鍵指標

1 實時性

    這個很容易理解,key往往是突發性瞬間就熱了,根本不給你再慢悠悠手工去配置中心添加熱key再推送到jvm的機會。它大部分時間不可預知,來得也非常迅速,可能某個商家上個活動,瞬間熱key就出現了。如果短時間內沒能進到內存,就有redis集羣被打爆的風險。

    所以熱key探測框架最重要的就是實時性,最好是某個key剛有熱的苗頭,在1秒內它就已經進到整個服務集羣的內存裏了,1秒後就不會再去密集訪問redis了。同理,對於刷子用戶也一樣,剛開始刷,1秒內我就把它給禁掉了。

2 準確性

    這個很重要,也容易實現,累加數量,做到不誤探,精準探測,保證探測出的熱key是完全符合用戶自己設定的閾值。

3 集羣一致性

    這個比較重要,尤其是某些帶刪除key的場景,要能做到刪key時整個集羣內的該key都會刪掉,以避免數據的錯誤。

4 高性能

    這個是核心之一,高性能帶來的就是低成本,做熱key探測目的就是爲了降低數據層的負載,提升應用層的性能,節省服務器資源。不然,大家直接去整體擴充redis集羣規模就好了。

    理論上,在不影響實時性的情況下,要完成實時熱key探測,所消耗的機器資源越少,那麼經濟價值就越大。

京東熱key探測框架架構設計

在經歷了多次被突發海量請求壓垮數據層服務的場景,並時刻面臨大量的爬蟲刷子機器人用戶的請求,我們根據既有經驗設計開發了一套通用輕量級熱key探測框架——JdHotkey。

它很輕量級,既不改redis源碼也不改redis的客戶端jar包,當然,它與redis沒一點關係,完全不依賴redis。它是一個獨立的系統,部署後,在server代碼裏引入jar,之後就像使用一個本地的HashMap一樣來使用它即可。

框架自身會完成一切,包括對待測key的上報,對熱key的推送,本地熱key的緩存,過期、淘汰策略等等。框架會告訴你,它是不是個熱key,其他的邏輯交給你自己去實現即可。

它有很強的實時性,默認情況下,500ms即可探測出待測key是否熱key,是熱key它就會進到jvm內存中。當然,我們也提供了更快頻率的設置方式,通常如果非極端場景,建議保持默認值就好,更高的頻率帶來了更大的資源消耗。

它有着強悍的性能表現,一臺8核8G的機器,在承擔該框架熱key探測計算任務時(即下面架構圖裏的worker服務),每秒可以處理來自於數千臺服務器發來的高達16萬個的待測key,8核單機吞吐量在16萬,16核機器每秒可達30萬以上探測量,當然前提是cpu很穩定。高性能代表了低成本,所以我們就可以僅僅採用10臺機器,即可完成每秒近300萬次的key探測任務,一旦找到了熱key,那該數據的訪問耗時就和redis不在一個數量級了。如果是加redis集羣呢?把QPS從20萬提升到200萬,我們又需要擴充多少臺服務器呢?

該框架主要由4個部分組成

1 etcd集羣

    etcd作爲一個高性能的配置中心,可以以極小的資源佔用,提供高效的監聽訂閱服務。主要用於存放規則配置,各worker的ip地址,以及探測出的熱key、手工添加的熱key等。

2 client端jar包

    就是在服務中添加的引用jar,引入後,就可以以便捷的方式去判斷某key是否熱key。同時,該jar完成了key上報、監聽etcd裏的rule變化、worker信息變化、熱key變化,對熱key進行本地caffeine緩存等。

3 worker端集羣

    worker端是一個獨立部署的Java程序,啓動後會連接etcd,並定期上報自己的ip信息,供client端獲取地址並進行長連接。之後,主要就是對各個client發來的待測key進行累加計算,當達到etcd裏設定的rule閾值後,將熱key推送到各個client。

4 dashboard控制檯

    控制檯是一個帶可視化界面的Java程序,也是連接到etcd,之後在控制檯設置各個APP的key規則,譬如2秒20次算熱。然後當worker探測出來熱key後,會將key發往etcd,dashboard也會監聽熱key信息,進行入庫保存記錄。同時,dashboard也可以手工添加、刪除熱key,供各個client端監聽。

綜上,可以看到該框架沒有依賴於任何定製化的組件,與redis更是毫無關係,核心就是靠netty連接,client端送出待測key,然後由各個worker完成分佈式計算,算出熱key後,就直接推送到client端,非常輕量級。

該框架工作流程

1 首先搭建etcd集羣

   etcd作爲全局共用的配置中心,將讓所有的client能讀取到完全一致的worker信息和rule信息。

2 啓動dashboard可視化界面

    在界面上添加各個APP的待測規則,如app1它包含兩個規則,一個是userId_開頭的key,如userId_abc,每2秒出現20次則算熱key,第二個是skuId_開頭的每1秒出現超過100次則算熱key。只有命中規則的key纔會被髮送到worker進行計算。

3 啓動worker集羣

    worker集羣可以配置APP級別的隔離,也可以不隔離,做了隔離後,這個app就只能使用這幾個worker,以避免其他APP在性能資源上產生競爭。worker啓動後,會從etcd讀取之前配置好的規則,並持續監聽規則的變化。

    然後,worker會定時上報自己的ip信息到etcd,如果一段時間沒有上報,etcd會將該worker信息刪掉。worker上報的ip供client進行長連接,各client以etcd裏該app能用的worker信息爲準進行長連接,並且會根據worker的數量將待測的key進行hash後平均分配到各個worker。

    之後,worker就開始接收並計算各個client發來的key,當某key達到規則裏設定的閾值後,將其推送到該APP全部客戶端jar,之後推送到etcd一份,供dashboard監聽記錄。

4 client端

    client端啓動後會連接etcd,獲取規則、獲取專屬的worker ip信息,之後持續監聽該信息。獲取到ip信息後,會通過netty建立和worker的長連接。

    client會啓動一個定時任務,每500ms(可設置)就批量發送一次待測key到對應的worker機器,發送規則是key的hashcode 對worker數量取餘,所以固定的key肯定會發送到同一個worker。這500ms內,就是本地蒐集累加待測key及其數量,到期就批量發出去即可。注意,已經熱了的key不會再次發送,除非本地該key緩存已過期。

    當worker探測出來熱key後,會推送過來,框架採用caffeine進行本地緩存,會根據當初設置的rule裏的過期時間進行本地過期設置。當然,如果在控制檯手工新增、刪除了熱key,client也會監聽到,並對本地caffeine進行增刪。這樣,各個熱key在整個client集羣內是保持一致性的。

    jar包對外提供了判斷是否是熱key的方法,如果是熱key,那麼你只需要關心自己的邏輯處理就好,是限流它、是降級它訪問的部分接口、還是給它返回value,都依賴於自己的邏輯處理,非常的靈活。

    注意,我們關注的只有key本身,也就是一個字符串而已,而不關心value,我們只探測key。那麼此時必然有一個疑問,如果是redis的熱key,框架告訴了我哪個是熱key,並沒有給我value啊。是的,框架提供了是否是熱key的方法,如果是redis熱key,就需要用戶自己去redis獲取value,然後調用框架的set方法,將value也set進去就好。如果不是熱key,那麼就走原來的邏輯即可。所以可以將框架當成一個具備熱key的HashMap但需要自己去維護value的值。

    綜上,該框架以非常輕量級的做法,實現了毫秒級熱key精準探測,和集羣規模一致性,適用於大量場景,任何對某些字符串有熱度匹配需求的場景都可以使用。

熱key探測框架性能表現

該key已經歷了多次大促壓測、極端場景壓測以及618大促線上使用,這期間修復了很多不常見、甚至有些匪夷所思的問題,之前也發表過相關問題總結文章。

這裏我們僅對它的性能表現進行簡單的闡述。

etcd端

etcd性能優異,官方宣稱秒級讀寫可達數萬,實際我們使用中僅僅是熱key的推送,以及其他少量信息的監聽讀寫,負載非常輕。數千級別的客戶端連接,平時秒級百來個的熱key誕生,cpu佔用率不超過5%,大部分時間在1%左右。

worker端

worker端是該框架最核心的一環,也是承載分佈式計算壓力最大的部分,需要根據秒級各client發來的key總量來進行資源分配。譬如每秒有100萬個key待測,那麼我們需要知道單個worker的處理能力,然後決定分配多少個worker機器來均分這些計算任務。

這一塊也是調優的核心地方,越高的qps,就是越低的成本。我簡單列舉一些之前的測試數據。

8核8G的worker單機場景負載,totalDealCount爲累計計算過的key數量(進行完累加、推送熱key到client等完畢後,數量+1),totalReceiveCount爲累計收到的key數量(剛收到尚未參與計算).expireCount爲收到時從客戶端發出到worker收到已經超過5秒,不參與計算的key數量。

以上每10秒打印一次,可以看到處理量每10秒大概是160萬次。

機器cpu佔有率達到70%左右,高峯地方多是gc導致,整體到這個壓力級別,我們認爲它已經不能再大幅加壓了。

換用16核16G機器後,同樣的數據量即10秒160萬不變,16核機器要輕鬆的多。

cpu佔有率在30%多,整體負載比較輕。

加大數據源後

10秒達到200萬時,cpu上升至40%多,說明還有繼續增加壓力的空間。後續經過極限壓力寫入,我們驗證了單機在30萬以上QPS情況下可穩定工作半小時以上,但CPU負載已很高,存在不確定性風險,這樣的性能表現足以應對大部分“突發”場景。

綜上,我們可以給出性能的簡單結論,使用8核的worker機器,單機每秒可處理每秒10萬級別的key探測計算和推送任務。使用16核的機器,可較爲輕鬆應對20萬每秒的處理任務。

用戶可以根據該性能標準,來分配相應的worker數量。譬如你的應用每秒有100萬個請求,你要探測的維度有userId、skuId兩個,那麼就需要自己去估算大概有多少個skuId和userId,假如100萬個請求分別來自於100萬個不同的用戶、每個用戶都訪問了不同的sku,那麼就是200萬的待測key。所以你需要10臺worker會比較穩妥。

該框架已在京東APP後臺上線使用,並經歷了多次大促壓測演練以及618大促,表現相當穩定,社區版也已在碼雲發佈(https://gitee.com/jd-platform-opensource/hotkey)。希望該框架能成爲所有熱key場景問題的通用解決方案,能爲各個有相關問題困擾的個人、公司提供一份助力。

相關問題可諮詢[email protected],[email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章