java高並行系統設計之快取篇

2020-10-10 18:01:29

欄目今天介紹java高並行系統設計的快取篇。

常見硬體元件的延時情況如下圖:

從這些資料中,你可以看到,做一次記憶體定址大概需要 100ns,而做一次磁碟的查詢則需要 10ms。可見,我們使用記憶體作為快取的儲存媒介相比於以磁碟作為主要儲存媒介的資料庫來說,效能上會提高多個數量級。所以,記憶體是最常見的一種快取資料的媒介。

一、快取案例

1、TLB

Linux 記憶體管理是通過一個叫做 MMU(Memory Management Unit)的硬體,來實現從虛擬地址到實體地址的轉換的,但是如果每次轉換都要做這麼複雜計算的話,無疑會造成效能的損耗,所以我們會藉助一個叫做 TLB(Translation Lookaside Buffer)的元件來快取最近轉換過的虛擬地址,和實體地址的對映。TLB 就是一種快取元件。

2、抖音

平臺上的短視訊實際上是使用內建的網路播放器來完成的。網路播放器接收的是資料流,將資料下載下來之後經過分離音視訊流,解碼等流程後輸出到外設裝置上播放。播放器中通常會設計一些快取的元件,在未開啟視訊時快取一部分視訊資料,比如我們開啟抖音,伺服器端可能一次會返回三個視訊資訊,我們在播放第一個視訊的時候,播放器已經幫我們快取了第二、三個視訊的部分資料,這樣在看第二個視訊的時候就可以給使用者「秒開」的感覺。

3、HTTP協定快取

當我們第一次請求靜態的資源時,比如一張圖片,伺服器端除了返回圖片資訊,在響應頭裡面還有一個「Etag」的欄位。瀏覽器會快取圖片資訊以及這個欄位的值。當下一次再請求這個圖片的時候,瀏覽器發起的請求頭裡面會有一個「If-None-Match」的欄位,並且把快取的「Etag」的值寫進去發給伺服器端。伺服器端比對圖片資訊是否有變化,如果沒有,則返回瀏覽器一個 304 的狀態碼,瀏覽器會繼續使用快取的圖片資訊。通過這種快取協商的方式,可以減少網路傳輸的資料大小,從而提升頁面展示效能。

二、快取分類

1、靜態快取

靜態快取在 Web 1.0 時期是非常著名的,它一般通過生成 Velocity 模板或者靜態 HTML 檔案來實現靜態快取,在 Nginx 上部署靜態快取可以減少對於後臺應用伺服器的壓力

2、分散式快取

分散式快取的大名可謂是如雷貫耳了,我們平時耳熟能詳的 Memcached、Redis 就是分散式快取的典型例子。它們效能強勁,通過一些分散式的方案組成叢集可以突破單機的限制。所以在整體架構中,分散式快取承擔著非常重要的角色

3、本地快取

Guava Cache 或者是 Ehcache 等,它們和應用程式部署在同一個程序中,優勢是不需要跨網路排程,速度極快,所以可以用來阻擋短時間內的熱點查詢。

三、快取的讀寫策略

1、Cache Aside策略

在更新資料時不更新快取,而是刪除快取中的資料,在讀取資料時,發現快取中沒了資料之後,再從資料庫中讀取資料,更新到快取中。

這個策略就是我們使用快取最常見的策略,Cache Aside 策略(也叫旁路快取策略),這個策略資料以資料庫中的資料為準,快取中的資料是按需載入的。

Cache Aside 策略是我們日常開發中最經常使用的快取策略,不過我們在使用時也要學會依情況而變,並不是一成不變的。Cache Aside 存在的最大的問題是當寫入比較頻繁時,快取中的資料會被頻繁地清理,這樣會對快取的命中率有一些影響。如果你的業務對快取命中率有嚴格的要求,那麼可以考慮兩種解決方案:

一種做法是在更新資料時也更新快取,只是在更新快取前先加一個分散式鎖,因為這樣在同一時間只允許一個執行緒更新快取,就不會產生並行問題了。當然這麼做對於寫入的效能會有一些影響(推薦);

另一種做法同樣也是在更新資料時更新快取,只是給快取加一個較短的過期時間,這樣即使出現快取不一致的情況,快取的資料也會很快過期,對業務的影響也是可以接受。

2、Read/Write Through

這個策略的核心原則是使用者只與快取打交道,由快取和資料庫通訊,寫入或者讀取資料。

Write Through

的策略是這樣的:先查詢要寫入的資料在快取中是否已經存在,如果已經存在,則更新快取中的資料,並且由快取元件同步更新到資料庫中,如果快取中資料不存在,我們把這種情況叫做「Write Miss(寫失效)」。一般來說,我們可以選擇兩種「Write Miss」方式:一個是「Write Allocate(按寫分配)」,做法是寫入快取相應位置,再由快取元件同步更新到資料庫中;另一個是「No-write allocate(不按寫分配)」,做法是不寫入快取中,而是直接更新到資料庫中。 我們看到 Write Through 策略中寫資料庫是同步的,這對於效能來說會有比較大的影響,因為相比於寫快取,同步寫資料庫的延遲就要高很多了。通過Write Back策略非同步的更新資料庫。

Read Through

策略就簡單一些,它的步驟是這樣的:先查詢快取中資料是否存在,如果存在則直接返回,如果不存在,則由快取元件負責從資料庫中同步載入資料。

3、Write Back

這個策略的核心思想是在寫入資料時只寫入快取,並且把快取塊兒標記為「髒」的。而髒塊兒只有被再次使用時才會將其中的資料寫入到後端儲存中。 在「Write Miss」的情況下,我們採用的是「Write Allocate」的方式,也就是在寫入後端儲存的同時要寫入快取,這樣我們在之後的寫請求中都只需要更新快取即可,而無需更新後端儲存了。注意與上面的write through策略作區分。

我們在讀取快取時如果發現快取命中則直接返回快取資料。如果快取不命中則尋找一個可用的快取塊兒,如果這個快取塊兒是「髒」的,就把快取塊兒中之前的資料寫入到後端儲存中,並且從後端儲存載入資料到快取塊兒,如果不是髒的,則由快取元件將後端儲存中的資料載入到快取中,最後我們將快取設定為不是髒的,返回資料就好了。

write back策略多用於向磁碟中寫資料。例如:作業系統層面的 Page Cache、紀錄檔的非同步刷盤、訊息佇列中訊息的非同步寫入磁碟等。因為這個策略在效能上的優勢毋庸置疑,它避免了直接寫磁碟造成的隨機寫問題,畢竟寫記憶體和寫磁碟的隨機 I/O 的延遲相差了幾個數量級呢。

四、快取高可用

快取的命中率是快取需要監控的資料指標,快取的高可用可以一定程度上減少快取穿透的概率,提升系統的穩定性。快取的高可用方案主要包括使用者端方案、中間代理層方案和伺服器端方案三大類:

1、使用者端方案

在使用者端方案中,你需要關注快取的寫和讀兩個方面: 寫入資料時,需要把被寫入快取的資料分散到多個節點中,即進行資料分片; 讀資料時,可以利用多組的快取來做容錯,提升快取系統的可用性。關於讀資料,這裡可以使用主從和多副本兩種策略,兩種策略是為了解決不同的問題而提出的。 具體的實現細節包括:資料分片、主從、多副本

資料分片

一致性Hash演演算法。在這個演演算法中,我們將整個 Hash 值空間組織成一個虛擬的圓環,然後將快取節點的 IP 地址或者主機名做 Hash 取值後,放置在這個圓環上。當我們需要確定某一個 Key 需要存取到哪個節點上的時候,先對這個 Key 做同樣的 Hash 取值,確定在環上的位置,然後按照順時針方向在環上「行走」,遇到的第一個快取節點就是要存取的節點。

這時如果在 Node 1 和 Node 2 之間增加一個 Node 5,你可以看到原本命中 Node 2 的 Key 3 現在命中到 Node 5,而其它的 Key 都沒有變化;同樣的道理,如果我們把 Node 3 從叢集中移除,那麼只會影響到 Key 5 。所以你看,在增加和刪除節點時,只有少量的 Key 會「漂移」到其它節點上,而大部分的 Key 命中的節點還是會保持不變,從而可以保證命中率不會大幅下降。 【提示】一致性hash出現的快取雪崩現象使用虛擬節點解決。一致性hash分片與hash分片的區別在於,快取命中率的問題,hash分片在存在機器加入或是減少的情況時候,會導致快取失效,快取命中率下降。

主從

Redis 本身支援主從的部署方式,但是 Memcached 並不支援,Memcached 的主從機制是如何在使用者端實現的。為每一組 Master 設定一組 Slave,更新資料時主從同步更新。讀取時,優先從 Slave 中讀資料,如果讀取不到資料就穿透到 Master 讀取,並且將資料回種到 Slave 中以保持 Slave 資料的熱度。主從機制最大的優點就是當某一個 Slave 宕機時,還會有 Master 作為兜底,不會有大量請求穿透到資料庫的情況發生,提升了快取系統的高可用性。

多副本

主從方式已經能夠解決大部分場景的問題,但是對於極端流量的場景下,一組 Slave 通常來說並不能完全承擔所有流量,Slave 網路卡頻寬可能成為瓶頸。為了解決這個問題,我們考慮在 Master/Slave 之前增加一層副本層,整體架構是這樣的:

這個方案中,當用戶端發起查詢請求時,請求首先會先從多個副本組中選取一個副本組發起查詢,如果查詢失敗,就繼續查詢 Master/Slave,並且將查詢的結果回種到所有副本組中,避免副本組中髒資料的存在。基於成本的考慮,每一個副本組容量比 Master 和 Slave 要小,因此它只儲存了更加熱的資料。在這套架構中,Master 和 Slave 的請求量會大大減少,為了保證它們儲存資料的熱度,在實踐中我們會把 Master 和 Slave 作為一組副本組使用。

2、中間代理層

業界也有很多中間代理層方案,比如 Facebook 的Mcrouter,Twitter 的Twemproxy,豌豆莢的Codis。它們的原理基本上可以由一張圖來概括:

3、伺服器端方案

Redis 在 2.4 版本中提出了 Redis Sentinel 模式來解決主從 Redis 部署時的高可用問題,它可以在主節點掛了以後自動將從節點提升為主節點,保證整體叢集的可用性,整體的架構如下圖所示:

redis Sentinel 也是叢集部署的,這樣可以避免 Sentinel 節點掛掉造成無法自動故障恢復的問題,每一個 Sentinel 節點都是無狀態的。在 Sentinel 中會設定 Master 的地址,Sentinel 會時刻監控 Master 的狀態,當發現 Master 在設定的時間間隔內無響應,就認為 Master 已經掛了,Sentinel 會從從節點中選取一個提升為主節點,並且把所有其他的從節點作為新主的從節點。Sentinel 叢集內部在仲裁的時候,會根據設定的值來決定當有幾個 Sentinel 節點認為主掛掉可以做主從切換的操作,也就是叢集內部需要對快取節點的狀態達成一致才行。

【提示】上述使用者端到sentinel叢集的連線是虛線,因為對於快取的寫入和讀取請求不會經過 Sentinel 節點。

五、快取穿透

1、帕累託

網際網路系統的資料存取模型一般會遵從「80/20 原則」。「80/20 原則」又稱為帕累托法則,是義大利經濟學家帕累託提出的一個經濟學的理論。簡單來說,它是指在一組事物中,最重要的部分通常只佔 20%,而其他的 80% 並沒有那麼重要。把它應用到資料存取的領域,就是我們會經常存取 20% 的熱點資料,而另外的 80% 的資料則不會被經常存取。既然快取的容量有限,並且大部分的存取只會請求 20% 的熱點資料,那麼理論上說,我們只需要在有限的快取空間裡儲存 20% 的熱點資料就可以有效地保護脆弱的後端系統了,也就可以放棄快取另外 80% 的非熱點資料了。所以這種少量的快取穿透是不可避免的,但是對系統是沒有損害的。

2、回種空值

當我們從資料庫中查詢到空值或者發生異常時,我們可以向快取中回種一個空值。但是因為空值並不是準確的業務資料,並且會佔用快取的空間,所以我們會給這個空值加一個比較短的過期時間,讓空值在短時間之內能夠快速過期淘汰。回種空值雖然能夠阻擋大量穿透的請求,但如果有大量的空值快取,也就會浪費快取的儲存空間,如果快取空間被佔滿了,還會剔除掉一些已經被快取的使用者資訊反而會造成快取命中率的下降。所以這個方案,我建議你在使用的時候應該評估一下快取容量是否能夠支撐。如果需要大量的快取節點來支援,那麼就無法通過通過回種空值的方式來解決,這時你可以考慮使用布隆過濾器。

3、布隆過濾器

1970 年布隆提出了一種布隆過濾器的演演算法,用來判斷一個元素是否在一個集合中。這種演演算法由一個二進位制陣列和一個 Hash 演演算法組成。它的基本思路如下:我們把集合中的每一個值按照提供的 Hash 演演算法算出對應的 Hash 值,然後將 Hash 值對陣列長度取模後得到需要計入陣列的索引值,並且將陣列這個位置的值從 0 改成 1。在判斷一個元素是否存在於這個集合中時,你只需要將這個元素按照相同的演演算法計算出索引值,如果這個位置的值為 1 就認為這個元素在集合中,否則則認為不在集合中。

如何使用布隆過濾器解決快取穿透呢?

以儲存使用者資訊的表為例進行講解。首先我們初始化一個很大的陣列,比方說長度為 20 億的陣列,接下來我們選擇一個 Hash 演演算法,然後我們將目前現有的所有使用者的 ID 計算出 Hash 值並且對映到這個大陣列中,對映位置的值設定為 1,其它值設定為 0。新註冊的使用者除了需要寫入到資料庫中之外,它也需要依照同樣的演演算法更新布隆過濾器的陣列中相應位置的值。那麼當我們需要查詢某一個使用者的資訊時,先查詢這個 ID 在布隆過濾器中是否存在,如果不存在就直接返回空值,而不需要繼續查詢資料庫和快取,這樣就可以極大地減少異常查詢帶來的快取穿透。

布隆過濾器優點:

(1)效能高。無論是寫入操作還是讀取操作,時間複雜度都是 O(1) 是常數值

(2)節省空間。比如,20 億的陣列需要 2000000000/8/1024/1024 = 238M 的空間,而如果使用陣列來儲存,假設每個使用者 ID 佔用 4 個位元組的空間,那麼儲存 20 億使用者需要 2000000000 * 4 / 1024 / 1024 = 7600M 的空間,是布隆過濾器的 32 倍。

布隆過濾器缺點:

(1)它在判斷元素是否在集合中時是有一定錯誤機率的,比如它會把不是集合中的元素判斷為處在集合中。

原因:Hash演演算法本身的缺陷。

解決方案:使用多個 Hash 演演算法為元素計算出多個 Hash 值,只有所有 Hash 值對應的陣列中的值都為 1 時,才會認為這個元素在集合中。

(2)不支援刪除元素。布隆過濾器不支援刪除元素的缺陷也和 Hash 碰撞有關。舉一個例子,假如兩個元素 A 和 B 都是集合中的元素,它們有相同的 Hash 值,它們就會對映到陣列的同一個位置。這時我們刪除了 A,陣列中對應位置的值也從 1 變成 0,那麼在判斷 B 的時候發現值是 0,也會判斷 B 是不在集合中的元素,就會得到錯誤的結論。

解決方案:我會讓陣列中不再只有 0 和 1 兩個值,而是儲存一個計數。比如如果 A 和 B 同時命中了一個陣列的索引,那麼這個位置的值就是 2,如果 A 被刪除了就把這個值從 2 改為 1。這個方案中的陣列不再儲存 bit 位,而是儲存數值,也就會增加空間的消耗。

4、狗樁效應

比方說當有一個極熱點的快取項,它一旦失效會有大量請求穿透到資料庫,這會對資料庫造成瞬時極大的壓力,我們把這個場景叫做「dog-pile effect」(狗樁效應)。解決狗樁效應的思路是儘量地減少快取穿透後的並行,方案也比較簡單:

(1)在程式碼中控制在某一個熱點快取項失效之後啟動一個後臺執行緒,穿透到資料庫,將資料載入到快取中,在快取未載入之前,所有存取這個快取的請求都不再穿透而直接返回。

(2)通過在 Memcached 或者 Redis 中設定分散式鎖,只有獲取到鎖的請求才能夠穿透到資料庫

六、CDN

1、靜態資源加速的原因

在我們的系統中存在著大量的靜態資源請求:對於移動 APP 來說,這些靜態資源主要是圖片、視訊和串流媒體資訊;對於 Web 網站來說,則包括了 JavaScript 檔案、CSS 檔案、靜態 HTML 檔案等等。它們的讀請求量極大並且對存取速度的要求很高還佔據了很高的頻寬,這時會出現存取速度慢頻寬被佔滿影響動態請求的問題,那麼你就需要考慮如何針對這些靜態資源進行讀加速了。

2、CDN

靜態資源存取的關鍵點是就近存取,即北京使用者存取北京的資料,杭州使用者存取杭州的資料,這樣才可以達到效能的最優。我們考慮在業務伺服器的上層增加一層特殊的快取,用來承擔絕大部分對於靜態資源的存取,這一層特殊快取的節點需要遍佈在全國各地,這樣可以讓使用者選擇最近的節點存取。快取的命中率也需要一定的保證,儘量減少存取資源儲存源站的請求數量(回源請求)。這一層快取就是CDN。

CDN(Content Delivery Network/Content Distribution Network,內容分發網路)。簡單來說,CDN 就是將靜態的資源分發到位於多個地理位置機房中的伺服器上,因此它能很好地解決資料就近存取的問題,也就加快了靜態資源的存取速度。

3、搭建CDN系統

搭建一個 CDN 系統需要考慮哪兩點:

(1)如何將使用者的請求對映到 CDN 節點上

你可能會覺得這很簡單啊,只需要告訴使用者 CDN 節點的 IP 地址,然後請求這個 IP 地址上面部署的 CDN 服務就可以了啊。但是,並不是這樣,需要把ip替換為相應的域名。那麼如何做到這一點呢?這就需要依靠 DNS 來幫我們解決域名對映的問題了。DNS(Domain Name System,域名系統)實際上就是一個儲存域名和 IP 地址對應關係的分散式資料庫。而域名解析的結果一般有兩種,一種叫做「A 記錄」,返回的是域名對應的 IP 地址;另一種是「CNAME 記錄」,返回的是另一個域名,也就是說當前域名的解析要跳轉到另一個域名的解析上。

舉個例子:比如你的公司的一級域名叫做 example.com,那麼你可以把你的圖片服務的域名定義為「img.example.com」,然後將這個域名的解析結果的 CNAME 設定到 CDN 提供的域名上,比如 uclound 可能會提供一個域名是「80f21f91.cdn.ucloud.com.cn」這個域名。這樣你的電商系統使用的圖片地址可以是「img.example.com/1.jpg」。

使用者在請求這個地址時,DNS 伺服器會將域名解析到 80f21f91.cdn.ucloud.com.cn 域名上,然後再將這個域名解析為 CDN 的節點 IP,這樣就可以得到 CDN 上面的資源資料了。

域名層級解析優化

因為域名解析過程是分級的,每一級有專門的域名伺服器承擔解析的職責,所以域名的解析過程有可能需要跨越公網做多次 DNS 查詢,在效能上是比較差的。一個解決的思路是:在 APP 啟動時對需要解析的域名做預先解析,然後把解析的結果快取到原生的一個 LRU 快取裡面。這樣當我們要使用這個域名的時候,只需要從快取中直接拿到所需要的 IP 地址就好了,如果快取中不存在才會走整個 DNS 查詢的過程。同時為了避免 DNS 解析結果的變更造成快取內資料失效,我們可以啟動一個定時器定期地更新快取中的資料。

(2)如何根據使用者的地理位置資訊選擇到比較近的節點。

GSLB(Global Server Load Balance,全域性負載均衡)的含義是對於部署在不同地域的伺服器之間做負載均衡,下面可能管理了很多的本地負載均衡元件。它有兩方面的作用:一方面,它是一種負載均衡伺服器,負載均衡,顧名思義嘛,指的是讓流量平均分配使得下面管理的伺服器的負載更平均;另一方面,它還需要保證流量流經的伺服器與流量源頭在地緣上是比較接近的。

GSLB 可以通過多種策略來保證返回的 CDN 節點和使用者儘量保證在同一地緣區域,比如說可以將使用者的 IP 地址按照地理位置劃分為若干個區域,然後將 CDN 節點對應到一個區域上,根據使用者所在區域來返回合適的節點;也可以通過傳送封包測量 RTT 的方式來決定返回哪一個節點。

總結:DNS 技術是 CDN 實現中使用的核心技術,可以將使用者的請求對映到 CDN 節點上;DNS 解析結果需要做本地快取,降低 DNS 解析過程的響應時間;GSLB 可以給使用者返回一個離著他更近的節點,加快靜態資源的存取速度。

拓展

(1)百度域名的解析過程

一開始,域名解析請求先會檢查本機的 hosts 檔案,檢視是否有 www.baidu.com 對應的 IP;如果沒有的話,就請求 Local DNS 是否有域名解析結果的快取,如果有就返回標識是從非權威 DNS 返回的結果;如果沒有就開始 DNS 的迭代查詢。先請求根 DNS,根 DNS 返回頂級 DNS(.com)的地址;再請求.com 頂級 DNS 得到 baidu.com 的域名伺服器地址;再從 baidu.com 的域名伺服器中查詢到 www.baidu.com 對應的 IP 地址,返回這個 IP 地址的同時標記這個結果是來自於權威 DNS 的結果,同時寫入 Local DNS 的解析結果快取,這樣下一次的解析同一個域名就不需要做 DNS 的迭代查詢了。

(2)CDN延時

一般我們會通過 CDN 廠商的介面將靜態的資源寫入到某一個 CDN 節點上,再由 CDN 內部的同步機制將資源分散同步到每個 CDN 節點,即使 CDN 內部網路經過了優化,這個同步的過程是有延時的,一旦我們無法從選定的 CDN 節點上獲取到資料,我們就不得不從源站獲取資料,而使用者網路到源站的網路可能會跨越多個主幹網,這樣不僅效能上有損耗也會消耗源站的頻寬,帶來更高的研發成本。所以我們在使用 CDN 的時候需要關注 CDN 的命中率和源站的頻寬情況。

以上就是java高並行系統設計之快取篇的詳細內容,更多請關注TW511.COM其它相關文章!