我在幾期薅羊毛活動(dòng)中學(xué)到了什么~
前言
為什么突然想寫(xiě)一篇總結(jié)了呢,其實(shí)也是被虐的。今年 3 月份初期,我們商城接了一個(gè) XX 銀行的一分購(gòu)活動(dòng)(說(shuō)白點(diǎn)就是薅羊毛),那時(shí)候是活動(dòng)第一期,未曾想到活動(dòng)入口開(kāi)放時(shí),流量能直接將 cpu 沖至 100%,導(dǎo)致服務(wù)短暫的 502 了。期間采取了緊急方案到活動(dòng)結(jié)束,但未曾想到還有活動(dòng)二期,以及上周剛上線的活動(dòng)三期。想著最近這段時(shí)間也做了一些事情,還有遇到的一些坑點(diǎn),趁此機(jī)會(huì),就不偷懶記錄一下吧。
活動(dòng)一期到三期具體做了些什么
技術(shù)背景&瓶頸
項(xiàng)目是基于 Vue+SSR 架構(gòu)的,且沒(méi)有做緩存處理,沒(méi)做緩存的主要原因第一個(gè)是原本應(yīng)用 tps 比較低,改造動(dòng)力不強(qiáng),并且頁(yè)面渲染結(jié)果中包含了用戶數(shù)據(jù)以及服務(wù)端時(shí)間,沒(méi)法在不經(jīng)過(guò)改造的情況下直接上緩存。所以當(dāng)一期活動(dòng)大流量沖擊時(shí),高并發(fā)情況下很容易將 cpu 打至 100%。
一期在未知情況下,服務(wù)直接扛不住了,當(dāng)時(shí)為了活動(dòng)能正常進(jìn)行,首要方案就是先加機(jī)器扛住部分壓力,緊接著就是加緩存,目前有兩種緩存方案,緩存頁(yè)面或緩存組件,但由于我們的需要緩存的商品詳情頁(yè)組件涉及到動(dòng)態(tài)信息,可維護(hù)性太差,心智成本高,最終選擇了前者。我們整理了一下商詳頁(yè)有關(guān)動(dòng)態(tài)變化的信息數(shù)據(jù)(與時(shí)間/用戶相關(guān)等類(lèi)型的數(shù)據(jù)),在活動(dòng)期間,緊急屏蔽了部分不影響功能的動(dòng)態(tài)內(nèi)容,然后頁(yè)面上 CDN。
活動(dòng)結(jié)束后,我們做了下復(fù)盤(pán),要像應(yīng)用要能保障大流量情況下穩(wěn)定運(yùn)行,性能優(yōu)化處理是避免不了的了。為此我們做了以下四大方案:
1.對(duì)數(shù)據(jù)做動(dòng)靜分離: 我們可以將數(shù)據(jù)分類(lèi)成動(dòng)靜兩類(lèi),靜態(tài)數(shù)據(jù)即是一段時(shí)間內(nèi),不隨時(shí)間/用戶改變,動(dòng)態(tài)數(shù)據(jù)則是相反的,經(jīng)常變動(dòng),與時(shí)間有關(guān),有用戶相關(guān)等類(lèi)型的數(shù)據(jù)都可以歸類(lèi)為動(dòng)態(tài)數(shù)據(jù)。原頁(yè)面無(wú)法上緩存的最大阻礙就是,就是在 node 渲染模板時(shí),會(huì)默認(rèn)獲取用戶數(shù)據(jù),或是在 asyncData 中調(diào)用用戶相關(guān)的接口;此外,還會(huì)設(shè)置服務(wù)端時(shí)間等動(dòng)態(tài)數(shù)據(jù)。所以思路就是將靜態(tài)數(shù)據(jù)放在 node 獲取,將動(dòng)態(tài)數(shù)據(jù)放到客戶端(瀏覽器讀取 asyncData、mounted 等瀏覽器生命周期里)獲取保證服務(wù)端的清潔。
2.頁(yè)面接入 CDN: 經(jīng)過(guò)動(dòng)靜態(tài)分離的改造后,已經(jīng)可以將其路徑加入 cdn。但需要注意路徑上 query 等參數(shù)是否會(huì)影響渲染,如果影響,需要把邏輯搬到客戶端,同時(shí)需要注意一下過(guò)期時(shí)間(如 10 分鐘)是否會(huì)對(duì)業(yè)務(wù)產(chǎn)生影響
3.應(yīng)用緩存: 如果在比較糟糕的情況下,cdn 失效了導(dǎo)致回源率上升,應(yīng)用本身還是需要做好準(zhǔn)備。這就要根據(jù)項(xiàng)目需要去選擇內(nèi)存緩存/redis 緩存。
4.自動(dòng)降級(jí): 在極端的情況下,前面的緩存都沒(méi)擋住流量,就需要終極方案:降級(jí)渲染。所謂降級(jí)渲染,就是不進(jìn)入路由,直接將空模板返回,完全交給瀏覽器去做渲染。這樣做的最大好處就是完全避免了 node 壓力,將一個(gè) SSR 應(yīng)用變成了靜態(tài)應(yīng)用。缺點(diǎn)也是顯而易見(jiàn)的,用戶看到的是空模板,需要等待初始化。那如何自動(dòng)降級(jí)呢,可以通過(guò)定時(shí)器時(shí)時(shí)檢測(cè) cpu、負(fù)載等壓力,來(lái)確定當(dāng)前機(jī)器的負(fù)載,從而決定是否降級(jí);也可以通過(guò) url 的 query 上是否攜帶特定標(biāo)識(shí),顯式地決定是否降級(jí)。
對(duì)項(xiàng)目方案做了以上性能優(yōu)化接下來(lái)就是壓測(cè),也算是順利上線了。🤪
二期活動(dòng)沒(méi)過(guò)多久又來(lái)了,不過(guò)如我們預(yù)期,項(xiàng)目很穩(wěn)定地扛住了壓力,期間也增加了流量接口,并加友好提示等優(yōu)化。但其中一個(gè)痛點(diǎn)是需要針對(duì)幾個(gè)特殊商品去做個(gè)文案處理,這幾個(gè)文案非接口返回,也是臨時(shí)性的一些醒目提示,沒(méi)必要放在詳情頁(yè)接口中返回。由于時(shí)間也很緊急,我們不確定后面還有沒(méi)有這種特定的文案需求(和具體的頁(yè)面以及特定的區(qū)域關(guān)聯(lián)),決定還是暫時(shí)寫(xiě)一些 low code:針對(duì)特定的活動(dòng)商品 id,臨時(shí)添加文案,活動(dòng)下線之后,把文案去除。這樣做的風(fēng)險(xiǎn)性是有的,畢竟代碼是臨時(shí)性的,需要上下線,并且有時(shí)間延遲。但好在活動(dòng)結(jié)束時(shí)是周末,最后一天流量訪問(wèn)并不大,給了相對(duì)應(yīng)的引導(dǎo)文案以及售后處理,評(píng)估下來(lái)影響不大,尚可接受。
以下圖片商品詳情頁(yè)和商品購(gòu)買(mǎi)頁(yè)需要加的特定文案:
薅羊毛活動(dòng)是真香現(xiàn)場(chǎng)嗎~~6 月底產(chǎn)品就和我打了個(gè)招呼,說(shuō) XX 活動(dòng)又要有三期了,但整體方案依舊和二期一樣不變。我內(nèi)心:還來(lái)???(小聲說(shuō)句打工人太苦了),由于最終時(shí)間沒(méi)定下來(lái),也有了二期的教訓(xùn)之后,和后端同學(xué)也一起商量了一下,把活動(dòng)商品往配置化方向考慮,放在我們配置后臺(tái)中文案模塊且是可行的。針對(duì)商品詳情頁(yè),考慮到不破壞動(dòng)靜分離,先確定下配置化接口返回的數(shù)據(jù)是靜態(tài)的,可以放在服務(wù)端獲取。以下具體三期做的事情:
- 將參與活動(dòng)商品的文案做成配置化,從配置接口獲取,去除 low code
- 整理大流量活動(dòng)頁(yè)(例如商詳頁(yè))的接口,放在客戶端的接口需要做限流,接口達(dá)到一定的 tps 后,返回 429 狀態(tài),前端要做容錯(cuò)處理,頁(yè)面功能正常訪問(wèn),屏蔽限流接口錯(cuò)誤。
- 針對(duì)購(gòu)買(mǎi)限流接口,需要給 busy 提示(活動(dòng)太火爆了,請(qǐng)稍后再試)
- // 統(tǒng)一在 getResponseErrorInterceptor 處針對(duì) 429 狀態(tài)做處理
- export const getResponseErrorInterceptor = ({ errorCallback }) => (error) => {
- if (!isClient) {
- ...
- } else {
- // 429 Code 服務(wù)報(bào)錯(cuò)需要支持不彈出錯(cuò)誤提示
- if (+error.response.status === 429) {
- // 針對(duì)限流接口,且需要 busy 提示時(shí)增加 needBusyMsg 屬性
- errorCallback(error.config.needBusyMsg ? '活動(dòng)太火爆了,請(qǐng)稍后再試' : null);
- } else {
- ...
- );
- }
- }
- return throwError(error);
- };
結(jié)束了上周一周忙碌的壓測(cè)和測(cè)試,三期終于上線了。👏👏👏👏
想了解更多 Vue SSR 性能優(yōu)化方案可以移步到這里: Vue SSR 性能優(yōu)化實(shí)踐
實(shí)際過(guò)程中遇到的一些 Coding Question
1.本地項(xiàng)目(vue-SSR 架構(gòu))里,一個(gè)動(dòng)態(tài)接口放在服務(wù)端獲取時(shí),有一段代碼很 easy,一個(gè)是否是會(huì)員的標(biāo)識(shí)去開(kāi)通會(huì)員按鈕的顯隱,代碼如下(代碼有簡(jiǎn)化):
- <redirect
- v-if="!isVip"
- :link="link"
- type="h5"
- >
- 開(kāi)通會(huì)員<ui-icon name="arrow-right" />
- </redirect>
本地中雖然運(yùn)行正常,但是會(huì)有如下警告:vue.esm.js:6428 Mismatching childNodes vs. VNodes: NodeList(2) [div, a.DetailVip-right.Redirect] (2) [VNode, VNode]
[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside
, or missing . Bailing hydration and performing full client-side render.
但更新到測(cè)試環(huán)境中,頁(yè)面會(huì)失效,點(diǎn)擊失效等。會(huì)報(bào)有如下錯(cuò)誤:Failed to execute 'appendChild' on 'Node': This node type does not support this method
分析
Vue SSR 指南在客戶端激活里剛好說(shuō)到了一些需要注意的坑,使用「SSR + 客戶端混合」時(shí),需要了解的一件事是,瀏覽器可能會(huì)更改的一些特殊的 HTML 結(jié)構(gòu)。例如 table 中漏寫(xiě)<tbody>,像以下幾種情況也會(huì)導(dǎo)致導(dǎo)致服務(wù)端返回的靜態(tài) HTML 和瀏覽器端渲染的內(nèi)容不一致:
- 無(wú)效的HTML(例如:<p><p>Text</p></p>)
- 服務(wù)器與客戶端的不同狀態(tài)
- 例如日期、時(shí)間戳和隨機(jī)化等不確定變量的影響
- 第三方腳本影響到了組件的渲染
- 需要身份驗(yàn)證相關(guān)時(shí)
當(dāng)然確定原因之后對(duì)癥下藥,總結(jié)有幾種辦法可以解決此問(wèn)題:
- 檢查相關(guān)代碼,確保 HTML 有效
- 最簡(jiǎn)單粗暴的一個(gè)方法就是:用v-show去代替v-if,要知道構(gòu)建時(shí)生成的HTML是無(wú)狀態(tài)的,應(yīng)用程序中與身份驗(yàn)證相關(guān)的所有部分應(yīng)該只在客戶端呈現(xiàn),具體可以 diff 下獲取數(shù)據(jù)以及在服務(wù)器/客戶端呈現(xiàn)的內(nèi)容,解決服務(wù)器和客戶端之間的狀態(tài)不一致
- 面對(duì)第三方腳本這類(lèi)的,可以通過(guò)將組件包裝在標(biāo)簽中來(lái)避免在服務(wù)器端渲染組件
- .....(歡迎補(bǔ)充)
針對(duì)此類(lèi)問(wèn)題,還可以看看這篇文章:https://blog.lichter.io/posts/vue-hydration-error/
2: 同樣的 h5 頁(yè)面,在瀏覽器中打開(kāi)配置生效,而在公眾號(hào)&小程序中打開(kāi)卻失效了?
三期的時(shí)候,我們把活動(dòng)商品 id 和對(duì)應(yīng)文案做成了配置化處理。配置方式如下:
獲取商品配置內(nèi)容經(jīng)過(guò) JSON.stringify()之后,毋庸置疑會(huì)得到如下字符串:
- 455164527672033280|龍支付 立減 10 元|滿 40 立減 10(僅限 XX 卡)#\\n623841656577658880|龍支付 立減 10 元(僅限 XX 卡)|滿 40 立減 10(僅限 XX 卡)#\\n350947143063699456|龍支付測(cè)試 立減 10 元(僅限 XX 卡)|滿 40 立減 10 測(cè)試(僅限 XX 卡)#
在詳情頁(yè)獲取所有的商品 id 列表信息,我們用的#做區(qū)分,寫(xiě)了一個(gè)簡(jiǎn)單的正則如下:
- activityItems() {
- return this.getFieldValue('activity_item')?.split('#\\n');
- },
但在公眾號(hào)里面打開(kāi)我們的 h5 鏈接,會(huì)將#自動(dòng)轉(zhuǎn)義成\,內(nèi)容會(huì)變成:
- 455164527672033280|龍支付 立減 10 元(僅限 XX 卡)|滿 40 立減 10(僅限 XX 卡)\\\n623841656577658880|龍支付 立減 10 元(僅限 XX 卡)|滿 40 立減 10(僅限 XX 卡)\\\n350947143063699456|龍支付測(cè)試 立減 10 元(僅限 XX 卡)|滿 40 立減 10 測(cè)試(僅限 XX 卡)\
啊,這,,,不是吧??(發(fā)現(xiàn)時(shí)內(nèi)心幾乎是崩潰的)😩 解決方式立馬把#字符換成不被轉(zhuǎn)移的字符;。
另外在小程序中打開(kāi)失效是因?yàn)檠佑枚诘姆桨福?dāng)時(shí)做了限制判斷,只需要在主站和主 app 中打開(kāi)有效,小程序設(shè)有自己?jiǎn)为?dú)的 appid,三期活動(dòng)有多方入口,把該限制放開(kāi)即可。
總結(jié)
薅羊毛參與了三期,也是積累了一些經(jīng)驗(yàn),踩了一些坑吧,想著太久沒(méi)寫(xiě)了該記錄一下了,先總結(jié)到這里,還有忘記的再補(bǔ)充~
本文轉(zhuǎn)載自微信公眾號(hào)「微醫(yī)大前端技術(shù)」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系微醫(yī)大前端技術(shù)公眾號(hào)。