1. 程式人生 > >京東技術架構(一)構建億級前端讀服務

京東技術架構(一)構建億級前端讀服務

作者:張開濤

從入職京東到現在,做讀服務已經一年多的時間了,經歷了各種億級到百億級的讀服務;這段時間也進行了一些新的讀服務架構嘗試,從架構到程式碼的編寫,各個環節都進行了反覆嘗試,壓測並進行調優,希望得到一個自己滿意的讀服務架構。

一些設計原則

  • 無狀態
  • 資料閉環
  • 快取銀彈
  • 併發化
  • 降級開關
  • 限流
  • 切流量
  • 其他

無狀態

如果設計的應用是無狀態的,那麼應用就可以水平擴充套件,當然實際生產環境可能是這樣子的: 應用無狀態,配置檔案有狀態。比如不同的機房需要讀取不同的資料來源,此時就需要通過配置檔案指定。

資料閉環

如果依賴的資料來源特別多,此時就可以考慮使用資料閉環,基本步驟:

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技術,使用這些技術解決了我們很多讀服務問題。