從入職京東到現(xiàn)在,做讀服務(wù)已經(jīng)一年多的時(shí)間了骗炉,經(jīng)歷了各種億級(jí)到百億級(jí)的讀服務(wù)照宝;這段時(shí)間也進(jìn)行了一些新的讀服務(wù)架構(gòu)嘗試,從架構(gòu)到代碼的編寫痕鳍,各個(gè)環(huán)節(jié)都進(jìn)行了反復(fù)嘗試硫豆,壓測(cè)并進(jìn)行調(diào)優(yōu)龙巨,希望得到一個(gè)自己滿意的讀服務(wù)架構(gòu)笼呆。
一些設(shè)計(jì)原則
無(wú)狀態(tài)
數(shù)據(jù)閉環(huán)
緩存銀彈
并發(fā)化
降級(jí)開關(guān)
限流
切流量
其他
無(wú)狀態(tài)
如果設(shè)計(jì)的應(yīng)用是無(wú)狀態(tài)的始腾,那么應(yīng)用就可以水平擴(kuò)展疮茄,當(dāng)然實(shí)際生產(chǎn)環(huán)境可能是這樣子的: 應(yīng)用無(wú)狀態(tài),配置文件有狀態(tài)。比如不同的機(jī)房需要讀取不同的數(shù)據(jù)源答倡,此時(shí)就需要通過(guò)配置文件指定。
數(shù)據(jù)閉環(huán)
如果依賴的數(shù)據(jù)來(lái)源特別多枫匾,此時(shí)就可以考慮使用數(shù)據(jù)閉環(huán)狭握,基本步驟:
1、數(shù)據(jù)異構(gòu):通過(guò)如MQ機(jī)制接收數(shù)據(jù)變更叼屠,然后原子化存儲(chǔ)到合適的存儲(chǔ)引擎瞳腌,如redis或持久化KV存儲(chǔ);
2镜雨、數(shù)據(jù)聚合:這步是可選的嫂侍,數(shù)據(jù)異構(gòu)的目的是把數(shù)據(jù)從多個(gè)數(shù)據(jù)源拿過(guò)來(lái),數(shù)據(jù)聚合目的是把這些數(shù)據(jù)做個(gè)聚合荚坞,這樣前端就可以一個(gè)調(diào)用拿到所有數(shù)據(jù)挑宠,此步驟一般存儲(chǔ)到KV存儲(chǔ)中;
3颓影、前端展示:前端通過(guò)一次或少量幾次調(diào)用拿到所需要的數(shù)據(jù)各淀。
這種方式的好處就是數(shù)據(jù)的閉環(huán),任何依賴系統(tǒng)出問(wèn)題了诡挂,還是能正常工作碎浇,只是更新會(huì)有積壓,但是不影響前端展示璃俗。
另外此處如果一次需要多個(gè)數(shù)據(jù)南捂,可以考慮使用Hash Tag機(jī)制將相關(guān)的數(shù)據(jù)聚合到一個(gè)實(shí)例,如在展示商品詳情頁(yè)時(shí)需要:商品基本信息:p:123:旧找, 商品規(guī)格參數(shù):d:123:溺健,此時(shí)就可以使用冒號(hào)中間的123作為數(shù)據(jù)分片key,這樣相同id的商品相關(guān)數(shù)據(jù)就在一個(gè)實(shí)例钮蛛。
緩存銀彈
緩存對(duì)于讀服務(wù)來(lái)說(shuō)可謂抗流量的銀彈鞭缭。
瀏覽器端緩存
設(shè)置請(qǐng)求的過(guò)期時(shí)間,如響應(yīng)頭Expires魏颓、Cache-control進(jìn)行控制岭辣。這種機(jī)制適用于如對(duì)實(shí)時(shí)性不太敏感的數(shù)據(jù),如商品詳情頁(yè)框架甸饱、商家評(píng)分沦童、評(píng)價(jià)仑濒、廣告詞等;但對(duì)于如價(jià)格偷遗、庫(kù)存等實(shí)時(shí)要求比較高的墩瞳,就不能做瀏覽器端緩存。
CDN緩存
有些頁(yè)面/活動(dòng)頁(yè)/圖片等服務(wù)可以考慮將頁(yè)面/活動(dòng)頁(yè)/圖片推送到離用戶最近的CDN節(jié)點(diǎn)讓用戶能在離他最近的節(jié)點(diǎn)找到想要的數(shù)據(jù)氏豌。一般有兩種機(jī)制:推送機(jī)制(當(dāng)內(nèi)容變更后主動(dòng)推送到CDN邊緣節(jié)點(diǎn))喉酌,拉取機(jī)制(先訪問(wèn)邊緣節(jié)點(diǎn),當(dāng)沒(méi)有內(nèi)容時(shí)回源到源服務(wù)器拿到內(nèi)容并存儲(chǔ)到節(jié)點(diǎn)上)泵喘,兩種方式各有利弊泪电。 使用CDN時(shí)要考慮URL的設(shè)計(jì),比如URL中不能有隨機(jī)數(shù)纪铺,否則每次都穿透CDN相速,回源到源服務(wù)器,相當(dāng)于CDN沒(méi)有任何效果鲜锚。對(duì)于爬蟲可以返回過(guò)期數(shù)據(jù)而選擇不回源突诬。
接入層緩存
對(duì)于沒(méi)有CDN緩存的應(yīng)用來(lái)說(shuō),可以考慮使用如Nginx搭建一層接入層烹棉,該接入層可以考慮如下機(jī)制實(shí)現(xiàn):
1攒霹、URL重寫:將URL按照指定的順序或者格式重寫,去除隨機(jī)數(shù)浆洗;
2催束、一致性哈希:按照指定的參數(shù)(如分類/商品編號(hào))做一致性Hash,從而保證相同數(shù)據(jù)落到一臺(tái)服務(wù)器上伏社;
3抠刺、proxy_cache:使用內(nèi)存級(jí)/SSD級(jí)代理緩存來(lái)緩存內(nèi)容;
4摘昌、proxy_cache_lock:使用lock機(jī)制速妖,將多個(gè)回源合并為一個(gè),減少回源量聪黎,并設(shè)置相應(yīng)的lock超時(shí)時(shí)間罕容;
5、shared_dict:此處如果架構(gòu)使用了nginx+lua實(shí)現(xiàn)稿饰,可以考慮使用lua shared_dict進(jìn)行cache锦秒,最大的好處就是reload緩存不丟失。
此處要注意喉镰,對(duì)于托底/異常數(shù)據(jù)不應(yīng)該讓其緩存旅择,否則用戶會(huì)在很長(zhǎng)一段時(shí)間看到這些數(shù)據(jù)。
應(yīng)用層緩存
如我們使用Tomcat時(shí)可以使用堆內(nèi)緩存/堆外緩存侣姆,堆內(nèi)緩存的最大問(wèn)題就是重啟時(shí)內(nèi)存中的緩存丟失生真,如果此時(shí)流量風(fēng)暴來(lái)臨可能沖垮應(yīng)用沉噩;還可以考慮使用local redis cache來(lái)代替堆外內(nèi)存;或者在接入層使用shared_dict來(lái)將緩存前置柱蟀,減少風(fēng)暴川蒙。
分布式緩存
一種機(jī)制就是廢棄分布式緩存,改成應(yīng)用local redis cache产弹,即在應(yīng)用所在服務(wù)器中部署一個(gè)redis派歌,然后使用主從機(jī)制同步數(shù)據(jù)弯囊。如果數(shù)據(jù)量不大這種架構(gòu)是最優(yōu)的痰哨;如果數(shù)據(jù)量太大,單服務(wù)器存儲(chǔ)不了匾嘱,還可以考慮分片機(jī)制將流量分散到多臺(tái)斤斧;或者直接就是分布式緩存實(shí)現(xiàn)。常見(jiàn)的分片規(guī)則就是一致性哈希了霎烙。
如上圖就是我們一個(gè)應(yīng)用的架構(gòu):
1撬讽、首先接入層讀取本地proxy cache / local cache;
2悬垃、如果不命中游昼,會(huì)讀取分布式redis集群;
3尝蠕、如果還不命中烘豌,會(huì)回源到tomcat,然后讀取堆內(nèi)cache看彼;如果沒(méi)有廊佩,則直接調(diào)用依賴業(yè)務(wù)獲取數(shù)據(jù);然后異步化寫到redis集群靖榕;
因?yàn)槲覀兪褂昧薾ginx+lua标锄,第二、三步可以使用lua-resty-lock非阻塞鎖減少峰值時(shí)的回源量茁计;如果你的服務(wù)是用戶維度的料皇,這種非阻塞鎖不會(huì)有什么大作用。
并發(fā)化
假設(shè)一個(gè)讀服務(wù)是需要如下數(shù)據(jù):
1星压、數(shù)據(jù)A ?10ms
2践剂、數(shù)據(jù)B ?15ms
3、數(shù)據(jù)C ? 20ms
4租幕、數(shù)據(jù)D ? 5ms
5舷手、數(shù)據(jù)E ? 10ms
那么如果串行獲取那么需要:60ms;
而如果數(shù)據(jù)C依賴數(shù)據(jù)A和數(shù)據(jù)B劲绪、數(shù)據(jù)D誰(shuí)也不依賴男窟、數(shù)據(jù)E依賴數(shù)據(jù)C盆赤;那么我們可以這樣子來(lái)獲取數(shù)據(jù):
那么如果并發(fā)化獲取那么需要:30ms;能提升一倍的性能歉眷。
假設(shè)數(shù)據(jù)E還依賴數(shù)據(jù)F(5ms)牺六,而數(shù)據(jù)F是在數(shù)據(jù)E服務(wù)中獲取的,此時(shí)就可以考慮在此服務(wù)中在取數(shù)據(jù)A/B/D時(shí)預(yù)取數(shù)據(jù)F汗捡,那么整體性能就變?yōu)榱耍?5ms淑际。
降級(jí)開關(guān)
對(duì)于一個(gè)讀服務(wù),很重要的一個(gè)設(shè)計(jì)就是降級(jí)開關(guān)扇住,在設(shè)計(jì)降級(jí)開關(guān)時(shí)主要如下思路:
1春缕、開關(guān)集中化管理:通過(guò)推送機(jī)制把開關(guān)推送到各個(gè)應(yīng)用;
2艘蹋、可降級(jí)的多級(jí)讀服務(wù):比如只讀本地緩存锄贼、只讀分布式緩存、或者只讀一個(gè)默認(rèn)的降級(jí)數(shù)據(jù)女阀;
3宅荤、開關(guān)前置化:如架構(gòu)是nginx--->tomcat,可以將開關(guān)前置到nginx接入層浸策,在nginx層做開關(guān)冯键,請(qǐng)求不打到后端應(yīng)用。
限流
目的是防止惡意流量庸汗,惡意攻擊惫确,可以考慮如下思路:
1、惡意流量只訪問(wèn)cache夫晌;
2雕薪、對(duì)于穿透到后端應(yīng)用的可以考慮使用nginx的limit模塊處理;
3晓淀、對(duì)于惡意ip可以使用如nginx deny進(jìn)行屏蔽所袁。
大部分時(shí)候是不進(jìn)行接入層限流的,而是限制流量穿透到后端薄弱的應(yīng)用層凶掰。
切流量
對(duì)于一個(gè)大型應(yīng)用燥爷,切流量是非常重要的,比如多機(jī)房有機(jī)房掛了懦窘、或者有機(jī)架掛了前翎、或者有服務(wù)器掛了等都需要切流量,可以使用如下手段進(jìn)行切換:
1畅涂、DNS:切換機(jī)房入口港华;
2、LVS/HaProxy:切換故障的nginx接入層午衰;
3立宜、Nginx:切換故障的應(yīng)用層冒萄;
另外我們有些應(yīng)用為了更方便切換,還可以在nginx接入層做切換橙数,通過(guò)nginx進(jìn)行一些流量切換尊流,而沒(méi)有通過(guò)如LVS/HaProxy做切換。
其他
不需要cookie的應(yīng)用使用無(wú)狀態(tài)域名灯帮,如3.cn崖技;
接入層請(qǐng)求頭過(guò)濾,只轉(zhuǎn)發(fā)有用的請(qǐng)求頭到后端應(yīng)用钟哥;
數(shù)據(jù)過(guò)濾邏輯前置迎献,比如在接入層進(jìn)行請(qǐng)求參數(shù)的合法性過(guò)濾;
內(nèi)網(wǎng)設(shè)置合理的連接瞪醋、讀忿晕、寫超時(shí)時(shí)間装诡;
根據(jù)需要開啟gzip壓縮減少流量银受;
使用unix domain socket減少本機(jī)連接數(shù);
內(nèi)網(wǎng)考慮使用http長(zhǎng)連接鸦采;
響應(yīng)請(qǐng)求時(shí)宾巍,考慮響應(yīng)頭加上服務(wù)器ip等信息,方便調(diào)試渔伯。
我們處理的讀服務(wù)大部分都是KV的顶霞,因此抗流量的思路就是大量緩存;而且怎么讓緩存怎么更接近用戶锣吼,離用戶越近速度就越快选浑。再一個(gè)點(diǎn)就是要考慮好降級(jí)方案,在異常情況下應(yīng)用不被拖垮拖死玄叠。我們系統(tǒng)大量使用了如nginx+lua+redis技術(shù)古徒,使用這些技術(shù)解決了我們很多讀服務(wù)問(wèn)題。