拼團(tuán)活動(dòng)遇黑產(chǎn)?搭進(jìn)去了8臺(tái)服務(wù)器...
作者:浪漫先生
來(lái)源:uejin.im/post/6854573218322513933
# 前言
最近,因?yàn)樵黾恿艘恍╋L(fēng)控措施,導(dǎo)致新人拼團(tuán)訂單接口的QPS、TPS下降了約5%~10%,這還了得!
首先,快速解釋一下【新人拼團(tuán)】活動(dòng):
業(yè)務(wù)簡(jiǎn)介:顧名思義,新人拼團(tuán)是由新用戶發(fā)起的拼團(tuán),如果拼團(tuán)成功,系統(tǒng)會(huì)自動(dòng)獎(jiǎng)勵(lì)新用戶一張滿15.1元減15的平臺(tái)優(yōu)惠券。這相當(dāng)于是無(wú)門檻優(yōu)惠了。每個(gè)用戶僅有一次機(jī)會(huì)。新人拼團(tuán)活動(dòng)的最大目的主要是為了拉新。
新用戶判斷標(biāo)準(zhǔn):是否有支付成功的訂單 ? 不是新用戶 : 是新用戶。
當(dāng)前問(wèn)題:由于像這種優(yōu)惠力度較大的活動(dòng)很容易被羊毛黨、黑產(chǎn)盯上。因此,我們完善了訂單風(fēng)控系統(tǒng),讓黑產(chǎn)無(wú)處遁形!然而由于需要同步調(diào)用風(fēng)控系統(tǒng),導(dǎo)致整個(gè)下單接口的的QPS、TPS的指標(biāo)皆有下降,從性能的角度來(lái)看,【新人拼團(tuán)下單接口】無(wú)法滿足性能指標(biāo)要求。因此CTO指名點(diǎn)姓讓我?guī)ь^沖鋒……沖?。?/span>
# 問(wèn)題分析
風(fēng)控系統(tǒng)的判斷一般分為兩種:在線同步分析和離線異步分析。在實(shí)際業(yè)務(wù)中,這兩者都是必要的。在線同步分析可以在下單入口處就攔截掉風(fēng)險(xiǎn),而離線異步分析可以提供更加全面的風(fēng)險(xiǎn)判斷基礎(chǔ)數(shù)據(jù)和風(fēng)險(xiǎn)監(jiān)控能力。
最近我們對(duì)在線同步這塊的風(fēng)控規(guī)則進(jìn)行了加強(qiáng)和優(yōu)化,導(dǎo)致整個(gè)新人拼團(tuán)下單接口的執(zhí)行鏈路更長(zhǎng),從而導(dǎo)致TPS和QPS這兩個(gè)關(guān)鍵指標(biāo)下降。
# 解決思路
要提升性能,最簡(jiǎn)單粗暴的方法是加服務(wù)器!然而,無(wú)腦加服務(wù)器無(wú)法展示出一個(gè)出色的程序員的能力。CTO說(shuō)了,要加服務(wù)器可以,買服務(wù)器的錢從我工資里面扣……
在測(cè)試環(huán)境中,我們簡(jiǎn)單的通過(guò)使用StopWatch來(lái)簡(jiǎn)單分析,偽代碼如下:
(rollbackFor = Exception.class)public CollageOrderResponseVO colleageOrder(CollageOrderRequestVO request) { StopWatch stopWatch = new StopWatch(); stopWatch.start("調(diào)用風(fēng)控系統(tǒng)接口"); // 調(diào)用風(fēng)控系統(tǒng)接口, http調(diào)用方式 stopWatch.stop(); stopWatch.start("獲取拼團(tuán)活動(dòng)信息"); // // 獲取拼團(tuán)活動(dòng)基本信息. 查詢緩存 stopWatch.stop(); stopWatch.start("獲取用戶基本信息"); // 獲取用戶基本信息。http調(diào)用用戶服務(wù) stopWatch.stop(); stopWatch.start("判斷是否是新用戶"); // 判斷是否是新用戶。查詢訂單數(shù)據(jù)庫(kù) stopWatch.stop(); stopWatch.start("生成訂單并入庫(kù)"); // 生成訂單并入庫(kù) stopWatch.stop(); // 打印task報(bào)告 stopWatch.prettyPrint(); // 發(fā)布訂單創(chuàng)建成功事件并構(gòu)建響應(yīng)數(shù)據(jù) return new CollageOrderResponseVO();}
執(zhí)行結(jié)果如下:
StopWatch '新人拼團(tuán)訂單StopWatch': running time = 1195896800 ns---------------------------------------------ns % Task name---------------------------------------------014385000 021% 調(diào)用風(fēng)控系統(tǒng)接口010481800 010% 獲取拼團(tuán)活動(dòng)信息013989200 015% 獲取用戶基本信息028314600 030% 判斷是否是新用戶028726200 024% 生成訂單并入庫(kù)
在測(cè)試環(huán)境整個(gè)接口的執(zhí)行時(shí)間在1.2s左右。其中最耗時(shí)的步驟是【判斷是否是新用戶】邏輯。這是我們重點(diǎn)優(yōu)化的地方(實(shí)際上,也只能針對(duì)這點(diǎn)進(jìn)行優(yōu)化,因?yàn)槠渌襟E邏輯基本上無(wú)優(yōu)化空間了)。
# 確定方案
在這個(gè)接口中,【判斷是否是新用戶】的標(biāo)準(zhǔn)是是用戶是否有支付成功的訂單。因此開發(fā)人員想當(dāng)然的根據(jù)用戶ID去訂單數(shù)據(jù)庫(kù)中查詢。我們的訂單主庫(kù)的配置如下:
這配置還算豪華吧。然而隨著業(yè)務(wù)的積累,訂單主庫(kù)的數(shù)據(jù)早就突破了千萬(wàn)級(jí)別了,雖然會(huì)定時(shí)遷移數(shù)據(jù),然而訂單量突破千萬(wàn)大關(guān)的周期越來(lái)越短……(分庫(kù)分表方案是時(shí)候提上議程了,此次場(chǎng)景暫不討論分庫(kù)分表的內(nèi)容)而用戶ID雖然是索引,但畢竟不是唯一索引。因此查詢效率相比于其他邏輯要更耗時(shí)。
通過(guò)簡(jiǎn)單分析可以知道,其實(shí)只需要知道這個(gè)用戶是否有支付成功的訂單,至于支付成功了幾單我們并不關(guān)心。因此此場(chǎng)景顯然適合使用redis的bitmap數(shù)據(jù)結(jié)構(gòu)來(lái)解決。在支付成功方法的邏輯中,我們簡(jiǎn)單加一行代碼來(lái)設(shè)置bitmap:
// 說(shuō)明:key表示用戶是否存在支付成功的訂單標(biāo)記// userId是long類型String key = "order:f:paysucc"; redisTemplate.opsForValue().setBit(key,?userId, true);
通過(guò)這一番改造,在下單時(shí)【判斷是否是新用戶】的核心代碼就不需要查庫(kù)了,而是改為:
Boolean paySuccFlag = redisTemplate.opsForValue().getBit(key, userId);if (paySuccFlag != null && paySuccFlag) { // 不是新用戶,業(yè)務(wù)異常}
修改之后,在測(cè)試環(huán)境的測(cè)試結(jié)果如下:
StopWatch '新人拼團(tuán)訂單StopWatch': running time = 82207200 ns---------------------------------------------ns % Task name---------------------------------------------014113100 017% 調(diào)用風(fēng)控系統(tǒng)接口010193800 012% 獲取拼團(tuán)活動(dòng)信息013965900 017% 獲取用戶基本信息014532800 018% 判斷是否是新用戶029401600??036%??生成訂單并入庫(kù)
測(cè)試環(huán)境下單時(shí)間變成了0.82s,主要性能損耗在生成訂單入庫(kù)步驟,這里涉及到事務(wù)和數(shù)據(jù)庫(kù)插入數(shù)據(jù),因此是合理的。接口響應(yīng)時(shí)長(zhǎng)縮短了31%!相比生產(chǎn)環(huán)境的性能效果更明顯……接著舞!
# 晴天霹靂
這次的優(yōu)化效果十分明顯,想著CTO該給我加點(diǎn)績(jī)效了吧,不然我工資要被扣完了呀~
一邊這樣想著,一邊準(zhǔn)備生產(chǎn)環(huán)境灰度發(fā)布。發(fā)完版之后,準(zhǔn)備來(lái)個(gè)葛優(yōu)躺好好休息一下,等著測(cè)試妹子驗(yàn)證完就下班走人。然而在我躺下不到1分鐘的時(shí)間,測(cè)試妹子過(guò)來(lái)緊張的跟我說(shuō):“接口報(bào)錯(cuò)了,你快看看!”What?
當(dāng)我打開日志一看,立馬傻眼了。報(bào)錯(cuò)日志如下:
ERR bit offset is not an integer or out of range : at io.lettuce.core.ExceptionFactory.createExecutionException(ExceptionFactory.java:135) ~[lettuce-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]at io.lettuce.core.ExceptionFactory.createExecutionException(ExceptionFactory.java:108) ~[lettuce-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]at io.lettuce.core.protocol.AsyncCommand.completeResult(AsyncCommand.java:120) ~[lettuce-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]at io.lettuce.core.protocol.AsyncCommand.complete(AsyncCommand.java:111) ~[lettuce-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]at io.lettuce.core.protocol.CommandHandler.complete(CommandHandler.java:654) ~[lettuce-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]at io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:614) ~[lettuce-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]…………
bit offset is not an integer or out of range。這個(gè)錯(cuò)誤提示已經(jīng)很明顯:我們的offset參數(shù)out of range。為什么會(huì)這樣呢?我不禁開始思索起來(lái):redis bitmap的底層數(shù)據(jù)結(jié)構(gòu)實(shí)際上是string類型,redis對(duì)于string類型有最大值限制不得超過(guò)512M,即2^32次方byte…………我靠?。?!
# 恍然大悟
由于測(cè)試環(huán)境歷史原因,userId的長(zhǎng)度都是8位的,最大值99999999,假設(shè)offset就取這個(gè)最大值。那么在bitmap中,bitarray=999999999=2^29byte。因此setbit沒有報(bào)錯(cuò)。
而生產(chǎn)環(huán)境的userId,經(jīng)過(guò)排查發(fā)現(xiàn)用戶中心生成ID的規(guī)則變了,導(dǎo)致以前很老的用戶的id長(zhǎng)度是8位的,新注冊(cè)的用戶id都是18位的。以測(cè)試妹子的賬號(hào)id為例:652024209997893632 = 2^59byte,這顯然超出了redis的最大值要求。不報(bào)錯(cuò)才怪!
緊急回退版本,灰度發(fā)布失敗~還好,CTO念我不知道以前的這些業(yè)務(wù)規(guī)則,放了我一馬~該死,還想著加績(jī)效,沒有扣績(jī)效就是萬(wàn)幸的了!
本次事件暴露出幾個(gè)非常值得注意的問(wèn)題,值得反思:
-
懂技術(shù)體系,還要懂業(yè)務(wù)體系
對(duì)于bitmap的使用,我們是非常熟悉的,對(duì)于多數(shù)高級(jí)開發(fā)人員而言,他們的技術(shù)水平也不差,但是因?yàn)椴煌瑯I(yè)務(wù)體系的變遷而無(wú)法評(píng)估出精準(zhǔn)的影響范圍,導(dǎo)致無(wú)形的安全隱患。本次事件就是因?yàn)闆]有了解到用戶中心的ID規(guī)則變化以及為什么要變化從而導(dǎo)致問(wèn)題發(fā)生。 -
預(yù)生產(chǎn)環(huán)境的必要性和重要性
導(dǎo)致本次問(wèn)題的另一個(gè)原因,就是因?yàn)闆]有預(yù)生產(chǎn)環(huán)境,導(dǎo)致無(wú)法真正模擬生產(chǎn)環(huán)境的真實(shí)場(chǎng)景,如果能有預(yù)生產(chǎn)環(huán)境,那么至少可以擁有生產(chǎn)環(huán)境的基礎(chǔ)數(shù)據(jù):用戶數(shù)據(jù)、活動(dòng)數(shù)據(jù)等。很大程度上能夠提前暴露問(wèn)題并解決。從而提升正式環(huán)境發(fā)版的效率和質(zhì)量。 -
敬畏心
要知道,對(duì)于一個(gè)大型的項(xiàng)目而言,任何一行代碼其背后都有其存在的價(jià)值:正所謂存在即合理。別人不會(huì)無(wú)緣無(wú)故這樣寫。如果你覺得不合理,那么需要通過(guò)充分的調(diào)研和了解,確定每一個(gè)參數(shù)背后的意義和設(shè)計(jì)變更等。以盡可能降低犯錯(cuò)的幾率。
# 后記
通過(guò)此次事件,本來(lái)想著優(yōu)化能夠提升接口效率,從而不需要加服務(wù)器。這下好了,不僅生產(chǎn)環(huán)境要加1臺(tái)服務(wù)器以臨時(shí)解決性能指標(biāo)不達(dá)標(biāo)的問(wèn)題,還要另外加7臺(tái)服務(wù)器用于預(yù)生產(chǎn)環(huán)境的搭建!因?yàn)閎itmap,搭進(jìn)去了8臺(tái)服務(wù)器。痛并值得。接著奏樂,接著舞~~~
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!