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