商城首頁卡爆了?。?!怎么辦?
前言
最近我們的商城系統(tǒng)出現(xiàn)了一個(gè)線上問題,用戶訪問商城首頁的時(shí)候要差不多20秒,才返回?cái)?shù)據(jù),可以說卡爆了。
到底怎么回事呢?
1.案發(fā)現(xiàn)場(chǎng)
上周四晚上,我們有一個(gè)正常的迭代版本按照預(yù)期的時(shí)候上線。
本次迭代,我所涉及的功能,很快上線,并且測(cè)試通過了。
但沒法下班,因?yàn)轫?xiàng)目組其他同事,還有線上問題在緊急處理。
我過去了解了一下情況,用戶訪問商城首頁的時(shí)候響應(yīng)太慢了,要20秒才返回,有用戶投訴過來了。
進(jìn)一步了解之后發(fā)現(xiàn),造成這個(gè)問題的根本原因是redis服務(wù)器掛了。
為什么會(huì)掛呢?
是因?yàn)橐淮涡酝鵵edis中存儲(chǔ)的數(shù)據(jù)太多了,導(dǎo)致內(nèi)存不足。
這個(gè)商城系統(tǒng)部署到了阿里云上,當(dāng)時(shí)購(gòu)買了1G的內(nèi)存空間。
但由于這次上線,有個(gè)新功能,需要在商城首頁上,按不同的地區(qū),推薦不同的商品。商品還要按不同的分類做區(qū)分。
原本商品只有幾十萬其實(shí)不多,但是按地區(qū)和分類做區(qū)分之后,保存的數(shù)據(jù)量乘以了幾百倍,一下子占用了大量的內(nèi)存。
redis掛了為什么會(huì)導(dǎo)致首頁慢呢?
答:因?yàn)榇a中有業(yè)務(wù)邏輯,如果從redis中沒有獲取到數(shù)據(jù),或者訪問redis失敗了,會(huì)從數(shù)據(jù)庫中獲取。雖說當(dāng)時(shí)是晚上,用戶并發(fā)量不大,但是直接訪問數(shù)據(jù)庫,響應(yīng)時(shí)間一下子下降了很多。
圖片
2.如何快速解決問題?
目前的這套方案,先從redis中獲取數(shù)據(jù),如果失敗了,再?gòu)臄?shù)據(jù)庫中獲取。
現(xiàn)在的問題是:redis內(nèi)存不足,臨時(shí)解決問題,只能加內(nèi)存資源了。
因?yàn)榧觾?nèi)存是最快的,直接加到了4G。如果要改代碼,這個(gè)功能今天晚上可能沒法上線,之前購(gòu)買的1G的資源確實(shí)有點(diǎn)小。
在阿里云上redis加了內(nèi)存之后,這個(gè)問題很快解決了,首頁訪問速度一下子提升。
但這不是問題的本質(zhì)。
3.復(fù)盤
第二天,我們開始復(fù)盤問題。
發(fā)現(xiàn)之前的方案有點(diǎn)問題:
- 這次新增的推薦商品功能,保存到redis的數(shù)據(jù)量太大了,把有些為null值的字段,或者前端用不到的字段也保存到redis中了,數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)不合理。
- redis出現(xiàn)問題之后的兜底方案有點(diǎn)問題,如果redis掛了,就直接訪問了數(shù)據(jù)庫,導(dǎo)致了用戶訪問慢的問題。如果是白天用戶并發(fā)量上來,可能會(huì)直接導(dǎo)致數(shù)據(jù)庫掛掉。
那么,如何優(yōu)化呢?
4.如何優(yōu)化?
數(shù)據(jù)結(jié)構(gòu)不合理的問題,可以通過調(diào)整數(shù)據(jù)結(jié)構(gòu)解決,非常容易。
但如果redis掛了該如何處理呢?
4.1 頁面靜態(tài)化
其實(shí)對(duì)于商城首頁,最好的方案是做頁面靜態(tài)化處理。
但由于目前商城的用戶并發(fā)量,還不算很大,而且如果改成頁面靜態(tài)化,前后端的改動(dòng)都太大了。
因此,這個(gè)方案最先被我們否定了。
4.2 加本地緩存
為了防止后面再次出現(xiàn)商城首頁訪問慢的問題,可以在應(yīng)用服務(wù)增加本地緩存。
這樣不管redis以后能否正常運(yùn)行,都不影響商城首頁的功能。
但需要考慮一個(gè)事情:應(yīng)用服務(wù)的內(nèi)存是否夠用?
顯然如果將所有推薦的商品數(shù)據(jù),都保存到應(yīng)用服務(wù)的本地內(nèi)存中,同樣可能會(huì)導(dǎo)致應(yīng)用服務(wù)的內(nèi)存不足的問題。
因此,直接加本地內(nèi)存是不行的。
4.3 改成MongoDB
使用MongoDB替代Redis保存數(shù)據(jù)。
Redis:數(shù)據(jù)全部存在內(nèi)存,定期寫入磁盤,當(dāng)內(nèi)存不夠時(shí),可以選擇指定的 LRU 算法刪除數(shù)據(jù)。
MongoDB:數(shù)據(jù)存在內(nèi)存,由 linux系統(tǒng) mmap 實(shí)現(xiàn),當(dāng)內(nèi)存不夠時(shí),只將熱點(diǎn)數(shù)據(jù)放入內(nèi)存,其他數(shù)據(jù)存在磁盤。
顯然MongoDB更適合保存大批量的結(jié)構(gòu)化的文檔數(shù)據(jù)。
由于我們之前在做其他功能時(shí),使用過MongoDB,它的性能也是挺不錯(cuò)的。
但如果直接改成從MongoDB中獲取數(shù)據(jù),商城首頁的訪問速度可能會(huì)有所下降。
4.4 本地緩存 + MongoDB
上面說到過的加本地緩存,和使用MongoDB都有各自的優(yōu)缺點(diǎn)。
為什么不把兩種方案結(jié)合一下呢?
在本地緩存中保存熱點(diǎn)數(shù)據(jù),每隔5分鐘更新一次。
圖片
用戶的請(qǐng)求過來,先從本地緩存中獲取推薦商品數(shù)據(jù),如果有則直接返回。
如果沒有,則從MongoDB獲取數(shù)據(jù)。
這樣可以解決性能的問題,也可以解決保存大量的數(shù)據(jù)。
5.兜底方案
上面的說的本地緩存 + MongoDB,基本可以解決redis掛了的問題。
但如果MongoDB掛了該怎么辦呢?
這就需要有一套更好的兜底方案。
5.1 使用Apollo配置
如果MongoDB掛了,則直接返回Apollo配置中默認(rèn)數(shù)據(jù),默認(rèn)是北京市東城區(qū)的推薦商品數(shù)據(jù)。
該配置由于在Apollo中,我們可以根據(jù)實(shí)際情況動(dòng)態(tài)調(diào)整。
我們都知道Apollo可以配置成集群模式,是高可用的,一般不容易掛掉。
但它有一個(gè)硬傷,就是如果數(shù)據(jù)并更了,需要人手動(dòng)調(diào)整數(shù)據(jù)。
沒法保證數(shù)據(jù)的實(shí)時(shí)性。
5.2 再?gòu)臄?shù)據(jù)庫訪問數(shù)據(jù)
如果從MongoDB中獲取數(shù)據(jù)失敗了,則直接從數(shù)據(jù)庫中獲取數(shù)據(jù)。
該方案從業(yè)務(wù)的角度來說,確實(shí)沒有問題。
但萬一真的出現(xiàn)這種情況,同樣會(huì)出現(xiàn)商城首頁訪問很慢的問題。
5.3 再?gòu)膔edis訪問數(shù)據(jù)
如果從MongoDB中獲取數(shù)據(jù)失敗了,則直接從redis中獲取數(shù)據(jù)。
Redis中只保留熱點(diǎn)商品數(shù)據(jù)。
這也是一種方案,不過要維護(hù)兩份數(shù)據(jù):MongoDB一份,Redis一份。
可能會(huì)存在數(shù)據(jù)不一致的問題。
5.4 再加一個(gè)本地緩存
在從數(shù)據(jù)庫獲取數(shù)據(jù)之后,再加一個(gè)本地緩存,保存默認(rèn)的數(shù)據(jù),即:北京市東城區(qū)的推薦商品數(shù)據(jù)。
這個(gè)本地緩存,只有在第一次訪問數(shù)據(jù)庫時(shí)寫入,并且有效期是24小時(shí)。
相當(dāng)于在MongoDB和數(shù)據(jù)庫之間,再加了一層默認(rèn)的本地緩存。
這樣就能解決數(shù)據(jù)庫訪問慢的問題。
6.最終方案
經(jīng)過激烈討論之后,我們最終選擇的方案是:本地緩存+MongoDB+本地默認(rèn)緩存+數(shù)據(jù)庫。
圖片
有時(shí)候選擇的某一個(gè)技術(shù)方案,是根據(jù)當(dāng)前的業(yè)務(wù)發(fā)展,或者公司現(xiàn)狀,資金,資源,人手,技術(shù)能力等多方面考慮的。
很多技術(shù)問題都沒有最完美的解決方案,只有最適合的方案。