引子
現代網際網路系統,架構設計時避不開的一點就是流量規劃、負載均衡。期望做到透明、多級的分流系統。「多級」就是在各個層面的技術元件來分流,「透明」就是業務無感知(甚至是技術無感知)。本文期望能夠給各位架構師作為掃盲貼使用。
兩條普適性原則:
- 1.盡最大限度減少到達單點部件的流量。引導請求分流至最合適的元件中,避免絕大多數流量彙集到單點部件(如資料庫),同時依然能夠在絕大多數時候保證處理結果的準確性,使單點系統在出現故障時自動而迅速地實施補救措施,這便是系統架構中多級分流的意義。
- 2.奧卡姆剃刀原則。作為一名架構設計者,應對多級分流的手段有全面的理解與充分的準備,同時清晰地意識到這些設施並不是越多越好。在能滿足需求的前提下,最簡單的系統就是最好的系統。
作為架構師,想一想應對大流量網際網路系統,我們有哪些可以優化的地方?擴頻寬?伺服器/元件橫向擴容?做快取?基本思路如下圖所示:
一、使用者端快取
使用者端快取(Client Cache):HTTP 協定的無狀態性決定了它必須依靠使用者端快取來解決網路傳輸效率上的缺陷。包含三種快取機制:「狀態快取」、「強制快取」(「強快取」)和「協商快取」。
1.1 狀態快取
是指不經過伺服器,使用者端直接根據快取資訊對目標網站的狀態判斷。例如:301/Moved Permanently(永久重定向)
1.2 強制快取
使用者端可以無須經過任何請求,在指定時點前一直持有和使用該資源的本地快取副本。
Expires 是 HTTP/1.0 協定中開始提供的 Header,後面跟隨一個截至時間引數。當伺服器返回某個資源時帶有該 Header 的話,意味著伺服器承諾截止時間之前資源不會發生變動,瀏覽器可直接快取該資料,不再重新發請求,範例:
HTTP/1.1 200 OK
Expires: Wed, 8 Apr 2020 07:28:00 GMT
缺點:
受限於使用者端的本地時間。
無法處理涉及到使用者身份的私有資源。
無法描述「不快取」的語意。
- Cache-Control是 HTTP/1.1 協定中定義的強制快取 Header,它的語意比起 Expires 來說就豐富了很多,如果 Cache-Control 和 Expires 同時存在,並且語意存在衝突(譬如 Expires 與 max-age / s-maxage 衝突)的話,規定必須以 Cache-Control 為準。Cache-Control 的使用範例如下:
HTTP/1.1 200 OK
Cache-Control: max-age=600
1.3 協商快取
基於檢測的快取機制,通常被稱為「協商快取」。協商快取有兩種變動檢查機制,分別是根據資源的修改時間進行檢查,以及根據資源唯一標識是否發生變化來進行檢查,它們都是靠一組成對出現的請求、響應 Header 來實現的:
- Last-Modified 和 If-Modified-Since:Last-Modified 是伺服器的響應 Header,用於告訴使用者端這個資源的最後修改時間。對於帶有這個 Header 的資源,當用戶端需要再次請求時,會通過 If-Modified-Since 把之前收到的資源最後修改時間傳送回伺服器端。
如果此時伺服器端發現資源在該時間後沒有被修改過,就只要返回一個 304/Not Modified 的響應即可,無須附帶訊息體,達到節省流量的目的,如下所示:
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=600
Last-Modified: Wed, 8 Apr 2020 15:31:30 GMT
如果此時伺服器端發現資源在該時間之後有變動,就會返回 200/OK 的完整響應,在訊息體中包含最新的資源,如下所示:
HTTP/1.1 200 OK
Cache-Control: public, max-age=600
Last-Modified: Wed, 8 Apr 2020 15:31:30 GMT
Content
- Etag 和 If-None-Match:Etag 是伺服器的響應 Header,用於告訴使用者端這個資源的唯一標識。HTTP 伺服器可以根據自己的意願來選擇如何生成這個標識,譬如 Apache 伺服器的 Etag 值預設是對檔案的索引節點(INode),大小和最後修改時間進行雜湊計算後得到的。對於帶有這個 Header 的資源,當用戶端需要再次請求時,會通過 If-None-Match 把之前收到的資源唯一標識傳送回伺服器端。
如果此時伺服器端計算後發現資源的唯一標識與上傳回來的一致,說明資源沒有被修改過,就只要返回一個 304/Not Modified 的響應即可,無須附帶訊息體,達到節省流量的目的,如下所示:
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=600
ETag: "28c3f612-ceb0-4ddc-ae35-791ca840c5fa"
如果此時伺服器端發現資源的唯一標識有變動,就會返回 200/OK 的完整響應,在訊息體中包含最新的資源,如下所示:
HTTP/1.1 200 OK
Cache-Control: public, max-age=600
ETag: "28c3f612-ceb0-4ddc-ae35-791ca840c5fa"
Content
Etag是 HTTP 中一致性最強的快取機制,又是 HTTP 中效能最差的快取機制。
二、域名解析
2.1 概念
域名快取(DNS Lookup):DNS 也許是全世界最大、使用最頻繁的資訊查詢系統,如果沒有適當的分流機制,DNS 將會成為整個網路的瓶頸。DNS的核心作用就是把域名解析成IP地址。典型的DNS域名解析流程如下圖:
2.2 缺點
- DNS 系統多級分流的設計本生沒啥問題,但在極限情況(各級伺服器均無快取)下,效能很差。--->專門有一種被稱為「DNS 預取」(DNS Prefetching)的前端優化手段用來避免這類問題。
- DNS 的分級查詢意味著每一級都有可能受到中間人攻擊的威脅,流量被劫持。--->解決方案:最近幾年出現了另一種新的 DNS 工作模式:HTTPDNS(也稱為 DNS over HTTPS,DoH)。它將原本的 DNS 解析服務開放為一個基於 HTTPS 協定的查詢服務,替代基於 UDP 傳輸協定的 DNS 域名解析。
三、傳輸鏈路
流量從使用者端往伺服器傳輸,這個過程就是「傳輸鏈路」。程式發出的請求能否與應用層、傳輸層協定提倡的方式相匹配,對傳輸的效率也會有極大影響。由於HTTP 協定還在持續發展,從 20 世紀 90 年代的 HTTP/1.0 和 HTTP/1.1,到 2015 年釋出的 HTTP/2,再到 2019 年的 HTTP/3,由於 HTTP 協定本身的變化,使得「適合 HTTP 傳輸的請求」的特徵也在不斷變化。變化如下圖:
3.1 連線數優化
HTTP( HTTP/3 以前)是以 TCP 為傳輸層的應用層協定。減少請求數量和擴大並行請求數成為了優化的主流思想。
- 原理是讓使用者端對同一個域名長期持有一個或多個不會用完即斷的 TCP 連線。
- 典型做法是在使用者端維護一個 FIFO 佇列,每次取完資料之後一段時間內不自動斷開連線,以便獲取下一個資源時直接複用,避免建立 TCP 連線的成本。
- HTTP/2 的最重要的技術特徵一,被稱為 HTTP/2 多路複用(HTTP/2 Multiplexing)技術。如下圖所示:
核心:
- 流(Stream)作為資料通道。
- 幀(Frame)作為最小資訊單位,且附帶一個流 ID 以標識這個幀屬於哪個流。
- 傳輸時按照幀打散資料到不同流上傳輸。接收時根據ID,將不同流中的資料段重組。
- 在 HTTP/2 中 Header 壓縮的原理是基於字典編碼的資訊複用,是單域名單連線的機制(對每個域名只維持一個 TCP 連線(One Connection Per Origin)來以任意順序傳輸任意數量的資源),更適合小檔案傳輸。
3.2 傳輸壓縮
- 「靜態預壓縮」(Static Precompression):在網路時代的早期,伺服器處理能力還很薄弱,為了啟用壓縮,把靜態資源先預先壓縮為.gz 檔案的形式存放起來,當用戶端可以接受壓縮版本的資源時(請求的 Header 中包含 Accept-Encoding: gzip)就返回壓縮後的版本(響應的 Header 中包含 Content-Encoding: gzip),否則就返回未壓縮的原版。
- 「即時壓縮」(On-The-Fly Compression):現代伺服器處理能力大幅提升,廢棄預壓縮,使用即時壓縮。整個壓縮過程全部在記憶體的資料流中完成,不必等資源壓縮完成再返回響應,這樣可以顯著提高「首位元組時間」(Time To First Byte,TTFB),改善 Web 效能體驗。缺點是不知道壓縮後資源的確切大小(Content-Length 這個響應 Header)。
3.3 快速 UDP 網路連線
「快速 UDP 網路連線」(Quick UDP Internet Connections,QUIC):以 UDP 協定為基礎,由自己來實現可靠傳輸能力,並專門支援移動裝置的網路切換場景。QUIC 提出了連線識別符號的概念,該識別符號可以唯一地標識使用者端與伺服器之間的連線,而無須依靠 IP 地址。這樣,切換網路後,只需向伺服器端傳送一個包含此識別符號的封包即可重用既有的連線。2018 年末,IETF 正式批准了 HTTP over QUIC 使用 HTTP/3 的版本號,將其確立為最新一代的網際網路標準。
截止到2023.2.21,根據W3Techs的資料顯示,全球網站中,支援HTTP/2 協定佔 39.8%,HTTP/3協定佔25.2%。
四、內容分發網路
內容分發網路(Content Distribution Network),即CDN是一種十分古老而又十分透明,沒什麼存在感的分流系統,許多人都說聽過它,但真正瞭解過它的人卻很少。一個運作良好的內容分發網路,能為網際網路系統解決跨運營商、跨地域物理距離所導致的時延問題,能為網站流量頻寬起到分流、減負的作用。舉個例子,如果不是有遍佈全國乃至全世界的阿里雲CDN網路支援,哪怕把整個杭州所有市民上網的權力都剝奪了,把頻寬全部讓給淘寶的機房,恐怕也撐不住全國乃至全球使用者在雙十一期間的瘋狂「圍毆」。內容分發網路的工作過程,主要涉及路由解析、內容分發、負載均衡和所能支援的 CDN 應用內容四個方面,我們來逐一瞭解。
4.1 路由解析
前面第二節講解了DNS的路由解析,這裡擴充套件一下有CDN參與的過程,核心就是生成了CNAME(Canonical Name規範名)記錄。實際上有2條記錄:
- 一條是A記錄,代表Address,域名->IP對映關係。
- 一條是CNAME記錄,代表別名->域名對映關係。
如下圖:
CDN 路由解析的具體工作過程是:
-
架設好「hello.com
」的伺服器後,將伺服器的 IP 地址在CDN服務商上註冊為「源站」,註冊後你會得到一個 CNAME(Canonical Name規範名),DNS 服務商會註冊一條 CNAME 記錄。
-
當第一位使用者來訪你的站點時,將首先發生一次未命中快取的 DNS 查詢,域名服務商解析出 CNAME(「hello
.cdn.com
」) 後,返回給本地 DNS。
-
本地 DNS 查詢 CNAME 時,由於能解析該 CNAME 的權威伺服器只有 CDN 服務商所架設的權威 DNS,這個 DNS 服務將根據一定的均衡策略和引數,如拓撲結構、容量、時延等,在全國的 CDN 快取節點中挑選一個最適合的,將 IP 給本地 DNS。
-
瀏覽器從本地 DNS 拿到 IP 地址,存取CDN伺服器。有快取直接返回,沒快取存取源站再快取返回。
4.2 內容分發
CDN 獲取源站資源的過程被稱為「內容分發」,這是 CDN 的核心價值。目前主要有以下兩種主流的內容分發方式:
- 主動分發(Push):分發由源站主動發起,將內容從源站或者其他資源庫推播到使用者邊緣的各個 CDN 快取節點上。一般用於網站要預載大量資源的場景。譬如雙十一之前一段時間內,淘寶、京東等各個網路商城就會開始把未來活動中所需用到的資源推播到 CDN 快取節點中,特別常用的資源甚至會直接快取到你的手機 APP 的儲存空間或者瀏覽器的localStorage上。
- 被動回源(Pull):被動回源由使用者存取所觸發全自動、雙向透明的資源快取過程。CDN 快取節點發現沒有該資源,就會實時從源站中獲取(源站->CDN->使用者)。不適合應用於資料量較大的資源。但使用起來非常方便。這種分發方式是小型站點使用 CDN 服務的主流選擇,如果不是自建 CDN,而是購買阿里雲、騰訊雲的 CDN 服務的站點,多數採用的就是這種方式。
4.3 CDN 應用
內容分發網路最初是為了快速分發靜態資源而設計的,但今天的 CDN 所能做的事情已經遠遠超越了開始建設時的目標,列舉如下:
- 加速靜態資源:這是 CDN 本職工作。
- 安全防禦:CDN 在廣義上可以視作網站的堡壘機,源站只對 CDN 提供服務,由 CDN 來對外界其他使用者服務,這樣惡意攻擊者就不容易直接威脅源站。CDN 對某些攻擊手段的防禦,如對DDoS 攻擊的防禦尤其有效。但需注意,將安全都寄託在 CDN 上本身是不安全的,一旦源站真實 IP 被洩漏,就會面臨很高的風險。
- 協定升級:不少 CDN 提供商都同時對接(代售 CA 的)SSL 證書服務,可以實現源站是 HTTP 協定的,而對外開放的網站是基於 HTTPS 的。同理,可以實現源站到 CDN 是 HTTP/1.x 協定,CDN 提供的外部服務是 HTTP/2 或 HTTP/3 協定、實現源站是基於 IPv4 網路的,CDN 提供的外部服務支援 IPv6 網路,等等。
- 狀態快取:第一節介紹使用者端快取時簡要提到了狀態快取,CDN 不僅可以快取源站的資源,還可以快取源站的狀態,譬如源站的 301/302 轉向就可以快取起來讓使用者端直接跳轉、還可以通過 CDN 開啟HSTS、可以通過 CDN 進行加速 SSL 證書存取,等等。
- 修改資源:CDN 可以在返回資源給使用者的時候修改它的任何內容,以實現不同的目的。譬如,可以對源站未壓縮的資源自動壓縮並修改 Content-Encoding,以節省使用者的網路頻寬消耗、可以對源站未啟用使用者端快取的內容加上快取 Header,自動啟用使用者端快取,可以修改CORS的相關 Header,將源站不支援跨域的資源提供跨域能力,等等。
- 存取控制:CDN 可以實現 IP 黑/白名單功能,根據不同的來訪 IP 提供不同的響應結果,根據 IP 的存取流量來實現 QoS 控制、根據 HTTP 的 Referer 來實現防盜鏈,等等。
五、負載均衡
負載均衡(Load Balancing):排程後方的多臺機器,以統一的介面對外提供服務,承擔此職責的技術元件被稱為「負載均衡」。從形式上來說都可以分為兩種:四層負載均衡和七層負載均衡。維基百科上對 OSI 七層模型的介紹如下圖:
Layer層級 |
Protocol data unit (PDU) 協定資料單元 |
功能 |
流量在哪負載均衡 |
主機層(Host Layers) |
7 |
應用層 Application Layer |
資料 Data |
提供為應用軟體提供服務的介面,用於與其他應用軟體之間的通訊。典型協定:HTTP、HTTPS、FTP、Telnet、SSH、SMTP、POP3 等 |
Nginx、Kong等。 |
6 |
表達層 Presentation Layer |
把資料轉換為能與接收者的系統格式相容並適合傳輸的格式。 |
主機。 |
5 |
對談層 Session Layer |
負責在資料傳輸中設定和維護計算機網路中兩臺計算機之間的通訊連線。 |
主機。 |
4 |
傳輸層 Transport Layer |
Segment 資料段, Datagram資料包 |
把傳輸表頭加至資料以形成封包。傳輸表頭包含了所使用的協定等傳送資訊。典型協定:TCP、UDP、RDP、SCTP、FCP 等 |
主機。協定裡包含了主機的port埠號,如果說IP確定了網際網路上主機的位置,那麼port指向該主機上監聽該埠的程式。
|
媒體層(Media Layers) |
3 |
網路層 Network Layer |
封包 Packet |
決定資料的傳輸路徑選擇和轉發,將網路表頭附加至資料段後以形成報文(即封包)。典型協定:IPv4/IPv6、IGMP、ICMP、EGP、RIP 等 |
路由器,裝置之間依靠 IPv4/IPv6地址定址。網路包可以跨越LAN子網,在整個WAN廣域網上通訊了(比如 internet)。 |
2 |
資料鏈路層 Data Link Layer |
資料框 Frame |
負責對等的網路定址、錯誤偵測和糾錯。當表頭和表尾被附加至封包後,就形成資料框(Frame)。典型協定:WiFi(802.11)、Ethernet(802.3)、PPP 等。 |
交換機,裝置之間依靠MAC地址定址。同一個LAN裡面的裝置可以相互通訊。 |
1 |
物理層 Physical Layer |
位元流 Bit |
在物理網路上傳送資料框,它負責管理電腦通訊裝置和網路媒體之間的互通。包括了針腳、電壓、線纜規範、集線器、中繼器、網路卡、主機介面卡等。 |
網路卡、電纜光纖,裝置之間需要靠交叉線進行直連通訊。 |
看了上圖後,就可以清晰理解L4/L7負載均衡的概念:
- L4四層負載均衡:基於OSI模型的下四層(上圖1-4)的負載均衡。簡單理解:根據IP+TCP埠,主要依靠2、3層交換器、路由器裝置轉發流量。特點是效能高。
- L7七層負載均衡:基於OSI模型的下七層(上圖1-7)的負載均衡。簡單理解:基於URL等應用層資訊的負載均衡,類似一層代理。特點是功能強,效能低。
下面我們來看看最常用負載均衡方案:L4負載均衡的資料鏈路層(5.1)、網路層(5.2)和L7負載均衡的應用層(5.3)。
5.1 資料鏈路層負載均衡
負載均衡器在轉發請求過程中直接修改幀的 MAC 目標地址。
如上圖,整個請求、轉發、響應的鏈路形成一個「三角關係」,所以這種負載均衡模式也常被很形象地稱為「三角傳輸模式」(Direct Server Return,DSR),也有叫「單臂模式」(Single Legged Mode)或者「直接路由」(Direct Routing),筆者覺得最後這個最貼切,一刀見血。
- 優點:直接路由,響應由真實伺服器直接返回,負載均衡器不會成為網路瓶頸,效能高。
- 缺點:負載均衡器必須與真實伺服器之間的通訊必須是二層可達的,即在一個子網當中,無法跨 VLAN。
5.2 網路層負載均衡
兩種模式:
1.IP 隧道模式
保持原來的封包不變,新建立一個封包,把原來封包的 Headers 和 Payload 整體作為另一個新的封包的 Payload,在這個新封包的 Headers 中寫入真實伺服器的 IP 作為目標地址,然後把它傳送出去。設計者給這種「套娃式」的傳輸起名叫做「IP 隧道」(IP Tunnel)傳輸,也還是相當的形象。
- 優點:可跨越VLAN。
- 缺點:需要封包拆包,效能損失。
2.NAT 模式
即網路地址轉換(Network Address Translation)。直接把封包 Headers 中的目標地址改掉,請求轉發給真實伺服器,並響應給均衡器,由均衡器把應答包的源 IP 改回自己的 IP,再發給使用者端,這樣才能保證使用者端與真實伺服器之間的正常通訊。
- 優點:可跨越VLAN。
- 缺點:流量大時,網路成為瓶頸。家用路由器基本就是這個原理。
5.3 應用層負載均衡
1.概念
四層負載均衡(L4)工作模式都屬於「轉發」,即直接將承載著 TCP 報文的底層資料格式(IP 封包或乙太網幀)轉發到真實伺服器上,此時使用者端到響應請求的真實伺服器維持著同一條 TCP 通道。但工作在四層之後(L7)的負載均衡模式就無法再進行轉發了,只能進行代理,此時真實伺服器、負載均衡器、使用者端三者之間由兩條獨立的 TCP 通道來維持通訊,轉發與代理的區別如下圖所示。
- 正向代理:最基本的代理,指在使用者端設定的(多個client->proxy->伺服器)、代表使用者端與伺服器通訊的代理服務,它是使用者端可知,而對伺服器透明的(無感知)。
- 反向代理:在伺服器設定(proxy->多個伺服器),代表真實伺服器來與使用者端通訊的代理服務,此時它對使用者端來說是透明的(無感知)。
- 透明代理:指對雙方都透明的,設定在網路中間裝置上的代理服務,譬如,架設在路由器上的透明FQ代理。
七層負載均衡器它就屬於反向代理中的一種。網路效能不如四層負載均衡器,但它工作在應用層,可以感知應用層通訊的具體內容,能夠做出更智慧的決策。
2. 可實現功能
- CDN 可以做的快取方面的工作(就是除去 CDN 根據物理位置就近返回這種優化鏈路的工作外),七層均衡器全都可以實現,譬如靜態資源快取、協定升級、安全防護、存取控制,等等。
- 七層均衡器可以實現更智慧化的路由。譬如,根據 Session 路由,以實現親和性的叢集;根據 URL 路由,實現專職化服務(此時就相當於閘道器的職責);甚至根據使用者身份路由,實現對部分使用者的特殊服務(如某些站點的貴賓伺服器),等等。
- 某些安全攻擊可以由七層均衡器來抵禦,譬如一種常見的 DDoS 手段是 SYN Flood 攻擊。
- 很多微服務架構的系統中,鏈路治理措施都需要在七層中進行,譬如服務降級、熔斷、異常注入,等等。
5.4 均衡策略與實現
常見的均衡策略如下:
- 輪循均衡(Round Robin):每一次來自網路的請求輪流分配給內部中的伺服器,從 1 至 N 然後重新開始。此種均衡演演算法適合於叢集中的所有伺服器都有相同的軟硬體設定並且平均服務請求相對均衡的情況。
- 權重輪循均衡(Weighted Round Robin):根據伺服器的不同處理能力,給每個伺服器分配不同的權值。能確保高效能的伺服器得到更多的使用率,避免低效能的伺服器負載過重。
- 隨機均衡(Random):把來自使用者端的請求隨機分配給內部中的多個伺服器,在資料足夠大的場景下能達到相對均衡的分佈。
- 權重隨機均衡(Weighted Random):此種均衡演演算法類似於權重輪循演演算法,不過在分配處理請求時是個隨機選擇的過程。
- 一致性雜湊均衡(Consistency Hash):根據請求中某一些資料作為特徵值來計算需要落在的節點上,保證同一個特徵值每次都一定落在相同的伺服器上。當服務叢集某個伺服器出現故障,不會導致整個服務叢集的雜湊鍵值重新分佈。
- 響應速度均衡(Response Time):負載均衡裝置對內部各伺服器發出一個探測請求(例如 Ping),根據最快響應時間來決定哪一臺伺服器來響應使用者端的服務請求。此種均衡演演算法能較好的反映伺服器的當前執行狀態。
- 最少連線數均衡(Least Connection):當有新的服務連線請求時,將把當前請求分配給連線數最少的伺服器,使均衡更加符合實際情況,負載更加均衡。此種均衡策略適合長時處理的請求服務,如 FTP 傳輸。
六、伺服器端快取
快取(Cache):軟體開發中的快取並非多多益善,它有收益,也有風險。伺服器端快取大體可分為兩類:
- 為緩解 CPU 壓力:譬如把方法執行結果儲存起來、把原本要實時計算的內容提前算好、把一些公用的資料進行復用,這可以節省 CPU 算力,順帶提升響應效能。
- 為緩解 I/O 壓力:譬如把原本對網路、磁碟等較慢媒介的讀寫存取變為對記憶體等較快媒介的存取,將原本對單點部件(如資料庫)的讀寫存取變為到可擴縮部件(如快取中介軟體)的存取,順帶提升響應效能。
tip:總監級崗位需要關注的東西:硬體提升和快取帶來的風險,需要做一個均衡考量。
6.1 快取屬性
通常,我們設計或者選擇快取至少會考慮以下四個維度的屬性:
- 吞吐量:快取的吞吐量使用 OPS 值(每秒運算元,Operations per Second,ops/s)來衡量,反映了對快取進行並行讀、寫操作的效率,即快取本身的工作效率高低。
- 命中率:快取的命中率即成功從快取中返回結果次數與總請求次數的比值,反映了引入快取的價值高低,命中率越低,引入快取的收益越小,價值越低。
- 擴充套件功能:快取除了基本讀寫功能外,還提供哪些額外的管理功能,譬如最大容量、失效時間、失效事件、命中率統計,等等。
- 分散式支援:快取可分為「程序內快取」和「分散式快取」兩大類,前者只為節點本身提供服務,無網路存取操作,速度快但快取的資料不能在各個服務節點中共用,後者則相反。
1.吞吐量
根據 Caffeine 給出的一組目前業界主流程序內快取實現方案,包括有 Caffeine、ConcurrentLinkedHashMap、LinkedHashMap、Guava Cache、Ehcache 和 Infinispan Embedded 的對比,從Benchmarks中體現出的它們在 8 執行緒、75%讀操作、25%寫操作下的吞吐量來看,各種快取元件庫的效能差異還是十分明顯的,最高與最低的相差了足有一個數量級,如下圖所示。
在 Caffeine 的實現中,設有專門的環形快取區(Ring Buffer,也常稱作 Circular Buffer)來記錄由於資料讀取而產生的狀態變動紀錄檔。為進一步減少競爭,Caffeine 給每條執行緒(對執行緒取 Hash,雜湊值相同的使用同一個緩衝區)都設定一個專用的環形緩衝。所謂環形緩衝,並非 Caffeine 的專有概念,它是一種擁有讀、寫兩個指標的資料複用結構,在電腦科學中有非常廣泛的應用。Ring Buffer原理如下圖:
2.命中率與淘汰策略
最基礎的淘汰策略實現方案有以下三種:
- FIFO(First In First Out):優先淘汰最早進入被快取的資料。採用這種淘汰策略,很可能會大幅降低快取的命中率。
- LRU(Least Recent Used):優先淘汰最久未被使用存取過的資料。LRU 通常會採用 HashMap 加 LinkedList 雙重結構(如 LinkedHashMap)來實現,以 HashMap 來提供存取介面,保證常數時間複雜度的讀取效能,以 LinkedList 的連結串列元素順序來表示資料的時間順序,每次快取命中時把返回物件調整到 LinkedList 開頭,每次快取淘汰時從連結串列末端開始清理資料。LRU 尤其適合用來處理短時間內頻繁存取的熱點物件。但如果短時間內因為某種原因未被存取過,此時這些熱點資料依然要面臨淘汰的命運。
- LFU(Least Frequently Used):優先淘汰存取次數最少的資料。LFU 會給每個資料新增一個存取計數器,每存取一次就加 1,需要淘汰時就清理計數器數值最小的那批資料。LFU 可以解決上面 LRU 中熱點資料間隔一段時間不存取就被淘汰的問題,但同時它又引入了兩個新的問題,首先是需要對每個快取的資料專門去維護一個計數器,每次存取都要更新,在上一節「吞吐量」裡解釋了這樣做會帶來高昂的維護開銷;另一個問題是不便於處理隨時間變化的熱度變化,譬如某個曾經頻繁存取的資料現在不需要了,它也很難自動被清理出快取。
近年來提出的 TinyLFU 和 W-TinyLFU 演演算法:
- TinyLFU(Tiny Least Frequently Used):TinyLFU 是 LFU 的改進版本。為了緩解 LFU 每次存取都要修改計數器所帶來的效能負擔,TinyLFU 會首先採用 Sketch 對存取資料進行分析,所謂 Sketch 是統計學上的概念,指用少量的樣本資料來估計全體資料的特徵,這種做法顯然犧牲了一定程度的準確性,但是隻要樣本資料與全體資料具有相同的概率分佈,Sketch 得出的結論仍不失為一種高效與準確之間權衡的有效結論。藉助Count–Min Sketch演演算法(可視為布隆過濾器的一種等價變種結構),TinyLFU 可以用相對小得多的記錄頻率和空間來近似地找出快取中的低價值資料。為了解決 LFU 不便於處理隨時間變化的熱度變化問題,TinyLFU 採用了基於「滑動時間窗」(在「流量控制」中我們會更詳細地分析這種演演算法)的熱度衰減演演算法,簡單理解就是每隔一段時間,便會把計數器的數值減半,以此解決「舊熱點」資料難以清除的問題。
- W-TinyLFU(Windows-TinyLFU):W-TinyLFU 又是 TinyLFU 的改進版本。為了應對稀疏突發存取(絕對頻率較小,但突發存取頻率很高的資料)的問題,W-TinyLFU 就結合了 LRU 和 LFU 兩者的優點,從整體上看是它是 LFU 策略,從區域性實現上看又是 LRU 策略。具體做法是將新記錄暫時放入一個名為 Window Cache 的前端 LRU 快取裡面,讓這些物件可以在 Window Cache 中累積熱度,如果能通過 TinyLFU 的過濾器,再進入名為 Main Cache 的主快取中儲存,主快取根據資料的存取頻繁程度分為不同的段(LFU 策略,實際上 W-TinyLFU 只分了兩段),但單獨某一段區域性來看又是基於 LRU 策略去實現的(稱為 Segmented LRU)。每當前一段快取滿了之後,會將低價值資料淘汰到後一段中去儲存,直至最後一段也滿了之後,該資料就徹底清理出快取。
Caffeine 官方給出的 W-TinyLFU 以及另外兩種高階淘汰策略ARC(Adaptive Replacement Cache)、LIRS(Low Inter-Reference Recency Set)與基礎的 LFU 策略之間的對比,如下圖所示。
3.擴充套件功能
一般來說,一套標準的 Map 介面(或者來自JSR 107的 javax.cache.Cache 介面)就可以滿足快取存取的基本需要,不過在「存取」之外,專業的快取往往還會提供很多額外的功能。筆者簡要列舉如下:
- 載入器:許多快取都有「CacheLoader」之類的設計,載入器可以讓快取從只能被動儲存外部放入的資料,變為能夠主動通過載入器去載入指定 Key 值的資料,載入器也是實現自動重新整理功能的基礎前提。
- 淘汰策略:有的快取淘汰策略是固定的,也有一些快取能夠支援使用者自己根據需要選擇不同的淘汰策略。
- 失效策略:要求快取的資料在一定時間後自動失效(移除出快取)或者自動重新整理(使用載入器重新載入)。
- 事件通知:快取可能會提供一些事件監聽器,讓你在資料狀態變動(如失效、重新整理、移除)時進行一些額外操作。有的快取還提供了對快取資料本身的監視能力(Watch 功能)。
- 並行級別:對於通過分段加鎖來實現的快取(以 Guava Cache 為代表),往往會提供並行級別的設定。可以簡單將其理解為快取內部是使用多個 Map 來分段儲存資料的,並行級別就用於計算出使用 Map 的數量。如果將這個引數設定過大,會引入更多的 Map,需要額外維護這些 Map 而導致更大的時間和空間上的開銷;如果設定過小,又會導致在存取時產生執行緒阻塞,因為多個執行緒更新同一個 ConcurrentMap 的同一個值時會產生鎖競爭。
- 容量控制:快取通常都支援指定初始容量和最大容量,初始容量目的是減少擴容頻率,這與 Map 介面本身的初始容量含義是一致的。最大容量類似於控制 Java 堆的-Xmx 引數,當快取接近最大容量時,會自動清理掉低價值的資料。
- 參照方式:支援將資料設定為軟參照或者弱參照,提供參照方式的設定是為了將快取與 Java 虛擬機器器的垃圾收集機制聯絡起來。
- 統計資訊:提供諸如快取命中率、平均載入時間、自動回收計數等統計。
- 持久化:支援將快取的內容儲存到資料庫或者磁碟中,程序內快取提供持久化功能的作用不是太大,但分散式快取大多都會考慮提供持久化功能。
幾款主流程序內快取方案對比結果如下圖:
4.分散式快取
複製式快取:複製式快取可以看作是「能夠支援分散式的程序內快取」,讀取資料時直接從當前節點的程序記憶體中返回,理論上可以做到與程序內快取一樣高的讀取效能;當資料發生變化時,就必須遵循複製協定,將變更同步到叢集的每個節點中,複製效能隨著節點的增加呈現平方級下降。
集中式快取:集中式快取是目前分散式快取的主流形式,集中式快取的讀、寫都需要網路存取,其好處是不會隨著叢集節點數量的增加而產生額外的負擔,其壞處是讀、寫都不再可能達到程序內快取那樣的高效能。典型案例就是Redis。
分散式快取與程序內快取各有所長,也有各有侷限,它們是互補而非競爭的關係,如有需要,完全可以同時把程序內快取和分散式快取互相搭配,構成透明多級快取(Transparent Multilevel Cache,TMC),如下圖所示:
6.2 快取風險
1.快取穿透
概念:快取中沒有,資料庫中也沒有,這種查詢不存在資料的現象被稱為快取穿透。
解決方案:
- 對於業務邏輯本身就不能避免的快取穿透,可以約定在一定時間內對返回為空的 Key 值依然進行快取,使得在一段時間內快取最多被穿透一次。
- 對於惡意攻擊導致的快取穿透,通常會在快取之前設定一個布隆過濾器來解決。
2.快取擊穿
概念:如果快取中某些熱點資料忽然因某種原因失效了,譬如典型地由於超期而失效,請求未能命中快取,都到達真實資料來源中去,導致其壓力劇增,這種現象被稱為快取擊穿。
解決方案:
- 加鎖同步,以請求該資料的 Key 值為鎖,使得只有第一個請求可以流入到真實的資料來源中,其他執行緒採取阻塞或重試策略。如果是程序內快取出現問題,施加普通互斥鎖即可,如果是分散式快取中出現的問題,就施加分散式鎖,這樣資料來源就不會同時收到大量針對同一個資料的請求了。
- 熱點資料由程式碼來手動管理,例如分散快取失效時間等(設定不同失效時間+亂數)。
3.快取雪崩
概念:快取擊穿是針對單個熱點資料失效,快取雪崩是大批次快取資料失效給資料來源帶來壓力。
解決方案:
- 提升快取系統可用性,建設分散式快取的叢集。
- 啟用透明多級快取,各個服務節點一級快取中的資料通常會具有不一樣的載入時間,也就分散了它們的過期時間。
- 將快取的生存期從固定時間改為一個時間段內的隨機時間,譬如原本是一個小時過期,那可以快取不同資料時,設定生存期為 55 分鐘到 65 分鐘之間的某個隨機時間。
4.快取汙染
概念:存汙染是指快取中的資料與真實資料來源中的資料不一致的現象。
解決方案:
了儘可能的提高使用快取時的一致性,已經總結不少更新快取可以遵循設計模式,譬如 Cache Aside、Read/Write Through、Write Behind Caching 等。其中最簡單、成本最低的 Cache Aside 模式是指:
- 讀資料時,先讀快取,快取沒有的話,再讀資料來源,然後將資料放入快取,再響應請求。
- 寫資料時,先寫資料來源,再刪(而不是更新)快取。(如先刪cache,資料還沒寫入DB,有查詢來了,就會返回舊資料;)
Cache Aside 模式依然是不能保證在一致性上絕對不出問題的。先寫資料來源再刪快取,也可能有問題:如果寫入成功,快取未刪除,導致DB和cache不一致。第二種機率很小,可以使用MQ重試機制解決,實現最終一致性。
=====參考=========
http://icyfenix.cn/architect-perspective/general-architecture/diversion-system/