京東毫秒級熱key探測框架設計與實踐,已實戰於618大促

少说点话發表於2024-07-04

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

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

熱key對資料層的衝擊顯而易見,譬如資料存放在redis或者MySQL中,以redis為例,那個未知的熱資料會按照hash規則被存在於某個redis分片上,平時使用時都從該分片獲取它的資料。由於redis效能還不錯,再加上叢集模式,每秒我們假設它能支撐20萬次讀取,這足以支援大部分的日常使用了。但是,以京東為例的這些頭部網際網路公司,動輒某個爆品,會瞬間引入每秒上百萬甚至數百萬的請求,當然流量多數會在幾秒內就消失。但就是這短短的幾秒的熱key,就會瞬間造成其所在redis分片叢集癱瘓。原因也很簡單,redis作為一個單執行緒的結構,所有的請求到來後都會去排隊,當請求量遠大於自身處理能力時,後面的請求會陷入等待、超時。由於該redis分片完全被這個key的請求給打滿,導致該分片上所有其他資料操作都無法繼續提供服務,也就是熱key不僅僅影響自己,還會影響和它合租的資料。很顯然,在這個極短的時間視窗內,我們是無法快速擴容10倍以上redis來支撐這個熱點的。雖然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快取一份,設定個過期時間,設定個淘汰策略譬如佇列滿時淘汰最先加入的。或者使用guava cache或caffeine cache進行單機本地快取,整體命中率偏低。

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萬,我們又需要擴充多少臺伺服器呢?

圖片

01該框架主要由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次算熱key。然後當worker探測出來熱key後,會將key發往etcd,dashboard也會監聽熱key資訊,進行入庫儲存記錄。同時,dashboard也可以手工新增、刪除熱key,供各個client端監聽。

圖片

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

02該框架工作流程

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場景問題的通用解決方案,能為各個有相關問題困擾的個人、公司提供一份助力。

圖片

原文:京東毫秒級熱key探測框架設計與實踐

相關文章