京東技術架構(一)構建億級前端讀服務
從入職京東到現在,做讀服務已經一年多的時間了,經曆了各種億級到百億級的讀服務;這段時間也進行了一些新的讀服務架構嚐試,從架構到代碼的編寫,各個環節都進行了反複嚐試,壓測並進行調優,希望得到一個自己滿意的讀服務架構。
一些設計原則
- 無狀態
- 數據閉環
- 緩存銀彈
- 並發化
- 降級開關
- 限流
- 切流量
- 其他
無狀態
如果設計的應用是無狀態的,那麼應用就可以水平擴展,當然實際生產環境可能是這樣子的: 應用無狀態,配置文件有狀態。比如不同的機房需要讀取不同的數據源,此時就需要通過配置文件指定。
數據閉環
如果依賴的數據來源特別多,此時就可以考慮使用數據閉環,基本步驟:
1、數據異構:通過如MQ機製接收數據變更,然後原子化存儲到合適的存儲引擎,如redis或持久化KV存儲;
2、數據聚合:這步是可選的,數據異構的目的是把數據從多個數據源拿過來,數據聚合目的是把這些數據做個聚合,這樣前端就可以一個調用拿到所有數據,此步驟一般存儲到KV存儲中;
3、前端展示:前端通過一次或少量幾次調用拿到所需要的數據。
這種方式的好處就是數據的閉環,任何依賴係統出問題了,還是能正常工作,隻是更新會有積壓,但是不影響前端展示。
另外此處如果一次需要多個數據,可以考慮使用Hash Tag機製將相關的數據聚合到一個實例,如在展示商品詳情頁時需要:商品基本信息:p:123:, 商品規格參數:d:123:,此時就可以使用冒號中間的123作為數據分片key,這樣相同id的商品相關數據就在一個實例。
緩存銀彈
緩存對於讀服務來說可謂抗流量的銀彈。
瀏覽器端緩存
設置請求的過期時間,如響應頭Expires、Cache-control進行控製。這種機製適用於如對實時性不太敏感的數據,如商品詳情頁框架、商家評分、評價、廣告詞等;但對於如價格、庫存等實時要求比較高的,就不能做瀏覽器端緩存。
CDN緩存
有些頁麵/活動頁/圖片等服務可以考慮將頁麵/活動頁/圖片推送到離用戶最近的CDN節點讓用戶能在離他最近的節點找到想要的數據。一般有兩種機製:推送機製(當內容變更後主動推送到CDN邊緣節點),拉取機製(先訪問邊緣節點,當沒有內容時回源到源服務器拿到內容並存儲到節點上),兩種方式各有利弊。 使用CDN時要考慮URL的設計,比如URL中不能有隨機數,否則每次都穿透CDN,回源到源服務器,相當於CDN沒有任何效果。對於爬蟲可以返回過期數據而選擇不回源。
接入層緩存
對於沒有CDN緩存的應用來說,可以考慮使用如Nginx搭建一層接入層,該接入層可以考慮如下機製實現:
1、URL重寫:將URL按照指定的順序或者格式重寫,去除隨機數;
2、一致性哈希:按照指定的參數(如分類/商品編號)做一致性Hash,從而保證相同數據落到一台服務器上;
3、proxy_cache:使用內存級/SSD級代理緩存來緩存內容;
4、proxy_cache_lock:使用lock機製,將多個回源合並為一個,減少回源量,並設置相應的lock超時時間;
5、shared_dict:此處如果架構使用了nginx+lua實現,可以考慮使用lua shared_dict進行cache,最大的好處就是reload緩存不丟失。
此處要注意,對於托底/異常數據不應該讓其緩存,否則用戶會在很長一段時間看到這些數據。
應用層緩存
如我們使用Tomcat時可以使用堆內緩存/堆外緩存,堆內緩存的最大問題就是重啟時內存中的緩存丟失,如果此時流量風暴來臨可能衝垮應用;還可以考慮使用local redis cache來代替堆外內存;或者在接入層使用shared_dict來將緩存前置,減少風暴。
分布式緩存
一種機製就是廢棄分布式緩存,改成應用local redis cache,即在應用所在服務器中部署一個redis,然後使用主從機製同步數據。如果數據量不大這種架構是最優的;如果數據量太大,單服務器存儲不了,還可以考慮分片機製將流量分散到多台;或者直接就是分布式緩存實現。常見的分片規則就是一致性哈希了。
如上圖就是我們一個應用的架構:
1、首先接入層讀取本地proxy cache / local cache;
2、如果不命中,會讀取分布式redis集群;
3、如果還不命中,會回源到tomcat,然後讀取堆內cache;如果沒有,則直接調用依賴業務獲取數據;然後異步化寫到redis集群;
因為我們使用了nginx+lua,第二、三步可以使用lua-resty-lock非阻塞鎖減少峰值時的回源量;如果你的服務是用戶維度的,這種非阻塞鎖不會有什麼大作用。
並發化
假設一個讀服務是需要如下數據:
1、數據A 10ms
2、數據B 15ms
3、數據C 20ms
4、數據D 5ms
5、數據E 10ms
那麼如果串行獲取那麼需要:60ms;
而如果數據C依賴數據A和數據B、數據D誰也不依賴、數據E依賴數據C;那麼我們可以這樣子來獲取數據:
那麼如果並發化獲取那麼需要:30ms;能提升一倍的性能。
假設數據E還依賴數據F(5ms),而數據F是在數據E服務中獲取的,此時就可以考慮在此服務中在取數據A/B/D時預取數據F,那麼整體性能就變為了:25ms。
降級開關
對於一個讀服務,很重要的一個設計就是降級開關,在設計降級開關時主要如下思路:
1、開關集中化管理:通過推送機製把開關推送到各個應用;
2、可降級的多級讀服務:比如隻讀本地緩存、隻讀分布式緩存、或者隻讀一個默認的降級數據;
3、開關前置化:如架構是nginx—>tomcat,可以將開關前置到nginx接入層,在nginx層做開關,請求不打到後端應用。
限流
目的是防止惡意流量,惡意攻擊,可以考慮如下思路:
1、惡意流量隻訪問cache;
2、對於穿透到後端應用的可以考慮使用nginx的limit模塊處理;
3、對於惡意ip可以使用如nginx deny進行屏蔽。
大部分時候是不進行接入層限流的,而是限製流量穿透到後端薄弱的應用層。
切流量
對於一個大型應用,切流量是非常重要的,比如多機房有機房掛了、或者有機架掛了、或者有服務器掛了等都需要切流量,可以使用如下手段進行切換:
1、DNS:切換機房入口;
2、LVS/HaProxy:切換故障的nginx接入層;
3、Nginx:切換故障的應用層;
另外我們有些應用為了更方便切換,還可以在nginx接入層做切換,通過nginx進行一些流量切換,而沒有通過如LVS/HaProxy做切換。
其他
不需要cookie的應用使用無狀態域名,如3.cn;
接入層請求頭過濾,隻轉發有用的請求頭到後端應用;
數據過濾邏輯前置,比如在接入層進行請求參數的合法性過濾;
內網設置合理的連接、讀、寫超時時間;
根據需要開啟gzip壓縮減少流量;
使用unix domain socket減少本機連接數;
內網考慮使用http長連接;
響應請求時,考慮響應頭加上服務器ip等信息,方便調試。
我們處理的讀服務大部分都是KV的,因此抗流量的思路就是大量緩存;而且怎麼讓緩存怎麼更接近用戶,離用戶越近速度就越快。再一個點就是要考慮好降級方案,在異常情況下應用不被拖垮拖死。我們係統大量使用了如nginx+lua+redis技術,使用這些技術解決了我們很多讀服務問題。
最後更新:2017-05-22 14:34:01