漫談Web快取架構

木可大大發表於2019-02-28

計算機領域多處地方用到快取,比如說為了緩解CPU和記憶體之間的速度不匹配問題,我們往往通過增加一級、二級、三級快取,CPU先從快取中取指令,如果取不到,再從記憶體中取,並更新快取,同時,根據程式的區域性性原理,使得大部分情況下快取都會命中。

目前,Web應用的核心資料通常存放在資料庫中,比如說使用者資訊、訂單資訊、交易資訊等,同時,資料庫和程式語言是無關的,通過SQL互動,Java、Php等語言寫的程式需要訪問資料庫,執行業務邏輯,展示結果給使用者。但是資料庫有一定的侷限性,譬如:1.資料庫連線是非常 "昂貴 "的資源,為了複用這些資源,目前採用連線池技術,2. 連線池的連線數是有限的,如果使用者過多,勢必要等待,3. 讀寫資料時需要加鎖。

通過上述介紹,我們知道一個大型系統中資料庫往往會成為瓶頸,我們不能每次訪問都訪問資料庫,尤其是在多使用者,大併發的情況下。面對這種情況,我們通常採用何種方法呢?在計算機行業中的所有問題,都可以通過增加一個抽象層來解決。那麼,針對資料庫這個瓶頸,我們可以在應用層和資料庫層增加一層,即快取層

如何實現快取

如果你是某某大型公司的首席架構師,現在公司需要自研一套快取系統,你應該怎麼設計呢?我想在設計之前應該想好以下幾個問題:

  1. 快取裡放什麼格式的資料?
  2. 應用程式(客戶端)如何訪問快取?
  3. 快取空間被應用程式用完了怎麼辦?
  4. 要不要支援分散式儲存(資料的分片),怎麼實現?

1. 快取裡放什麼格式的資料?

目前常見的資料格式有序列化物件、XML、JSON、字串(key,value)和基本的資料結構,其中針對Java語言的序列化物件有序列化和反序列化,而Google研發的protobuf是和語言無關的,比如說Python將某物件序列化,Java能將這個物件進行反序列化。

2. 應用程式如何訪問快取

考慮到公司有很多後端小組,並且使用不同的程式語言,這就要求我們自研的快取系統應該和程式語言無關,基於此,我們需要制定一套協議來支援各種語言。客戶端如何使用這套協議?最常見的就是客戶端/伺服器模型。首先,伺服器監聽請求;接著,客戶端傳送請求,獲得響應,其中客戶端傳送的請求就是協議;最後,基於Socket通訊。比如說:set 'name' 'mukedada'get name

3. 快取空間被用完了怎麼辦?

快取伺服器端在啟動的時候,應該設定快取大小,當快取被沾滿時,採用LRU演算法。

4. 實現分散式儲存

對於大型應用伺服器,單機的快取伺服器是支撐不了的。那麼,就需要對快取伺服器進行水平擴充套件(即增刪伺服器,當活動結束後,就需要考慮刪減伺服器),那麼用什麼演算法讓資料相對平均的分配到每臺伺服器?同時,這個演算法應該放在客戶端還是服務端呢?

  • 客戶端實現 注意這裡的客戶端指的是Web應用服務,伺服器列表資訊通過配置檔案獲得。當節點數發生變化時,需要讓客戶端知曉。
    image.png

它的典型應用是Memcached,Memcached使用的是一致性Hash演算法,在介紹它之前,我們先來看一下餘數演算法。對於使用者要儲存的(key,value),計算key的整數雜湊值,然後對伺服器的數目求餘,這樣來確定儲存伺服器。這個方法存在一個致命的問題:當伺服器個數傳送變化時,餘數會發生變化,這樣一來需要重新到資料庫獲取資料。 為了加深大家的理解,舉個具體的例項:假設有3臺伺服器0、1、2,key1、key2的hash值分別是100,99,對應的餘數分別是1和0,也就是說它們分別存放於編號為1和0的伺服器中,現在如果增加一臺伺服器3,那麼它們的餘數也會隨之發生變化,100%4 = 0,99%4 = 3,但是它們在0、3號伺服器卻找不到對應的資料。

image.png

為了解決餘數演算法存在的問題,我們的先輩們提出了分散式一致性hash演算法。它思路就是當伺服器個數發生變化時,儘可能的減少影響。譬如:當我們新增node5,隻影響區域性範圍內的key,而餘數演算法則影響全域性。

image.png

但是它也存在分佈不均勻的問題,導致有的伺服器上快取的資料多,有的少。一種方法就是虛擬節點,也就是說讓一個伺服器化身為多個虛擬節點,分佈到環上。Memcache採用的就是這種方法。

image.png
另一種方法就是Hash槽。Redis採用的就是這種方法,在介紹路由實現時會詳細介紹該方法。

  • 代理實現 代理程式放在伺服器端,它的典型案例有Twemproxy和Codis。它的基本思想:應用程式向Proxy傳送請求,Proxy通過一定演算法計算得到資料應該從哪個節點獲取,並且返回響應給客戶端。為了防止Proxy出現單點故障,可以通過叢集等方式實現Proxy高可用。

    image.png

  • 路由實現 它的典型案例就是Redis。它的基本思想是應用程式可以將請求傳送到任意一個節點,當節點包含該請求資料,則直接返回響應給應用程式,當節點不包含該請求資料時,則告訴它跳轉到其他節點中取資料,其中,客戶端程式庫需要解析相應的指令。例如:當node1中沒有資料,會讓客戶端程式訪問node3,這類似於web中的重定向,缺點: node1需要知道其他節點的資料,即node1和其他節點是相互通訊的。

    image.png
    首先它有16284個槽,每個node節點管理一段Hash槽,每當新來一個請求,都對它的key值進行CRC16(key)%16384求餘,最終會落到0~16383這個區間的槽中。
    image.png

但是,每當新增一個節點時,需要從原先的每個節點中獲取hash槽,這時需要涉及資料遷移的過程。如果在資料遷移的過程中有一個使用者請求,這個時候該怎麼辦?目前一種解決方法是讓node1和node4的持有相同的槽,但是設定不同的狀態,例如node1的槽的狀態設定為正在遷移,而node4的狀態是正在匯入,首先將請求交給node1,如果node1中有資料則直接返回,如果沒有則交給ndoe4。如下圖所示。

image.png

同時,我們注意到node1、node2等存在單點故障,為增加可用性,我們對每個node使用主從模式。資料首先寫入到master節點,之後有兩種方式,方式一,直接將結果返回給客戶端,然後將master節點資料同步到slave從節點中,這樣做的好處就是響應週期短,缺點是可能存在資料不一致的情況,即master節點將結果返回給客戶端之後,還沒來得及將資料同步到slave節點中就發生故障,那麼這部分資料就會丟失。方式二,資料寫入到master節點之後,需要將資料同步到slave節點成功之後,再將結果返回給客戶端,這種方式保證了資料強一致性,但是使用者需要更長的時間來等待。

image.png

快取擊穿問題

使用者每次訪問快取都沒有命中,導致每次請求都要訪問資料庫,這就是快取擊穿問題,出現這種情況導致快取沒起效果,反而增加了系統消耗。針對這個問題,一般諸如雙十一等活動都會在活動開始之前將使用者資訊預先存放到快取中。


image

歡迎關注微信公眾號:木可大大,所有文章都將同步在公眾號上。

相關文章