緩存中間件-緩存架構的實現(上)
前言
一眨眼,2019年就過去了。我希望從按照中間件,分別闡述一些常見的架構問題,以及解決方案。一方面這些問題與解決方案具備一定通用性 。另一方面,也算是面試中常見的問題。
我希望根據自己待過各種規模公司的經驗來談一些看法。
- 如果是針對大部分小公司的工作或面試,這些問題都稍微留下個印象即可。因為小公司的技術對這些問題並不是很看重,或者說機會用不到(小型公司往往追求產品功能的實現,業務的推進等)。
- 如果是針對大部分中型公司的工作或面試,希望可以完整地知道這些問題與解決方案。因為在中型公司中,這些問題都或多或少遇到,甚至是需要迫切解決的。
- 如果是大型公司的話,那么不僅僅需要知道這些問題與解決方案。還需要從中理解為什么會有這樣的問題,為什么這樣解決,在現有的項目中應該如何應用,是否提升空間等。因為在大公司中,一方面其內部往往采用自研框架,其它框架能夠借鑒的只有方案,思想等精髓;另一方面大公司不缺乏那些應用開源框架的人,缺的是把握方案通用思想的人。
如果上述無法理解的話,大家可以從功能性追求與非功能性追求兩個方面去思考。就像寫一個簡單的方法一樣,最基本的要求是實現其功能,緊接着就是不斷追求其非功能性(如性能,擴展性,安全性等)。放大來看,對於公司的技術發展也是如此,或者說更為嚴格。
之后找個機會,專門寫個博客,來談談我對公司技術與公司的看法。
話題收回來,接下來,讓我開始有關中間件問題與解決方案的闡述吧。
概述
緩存的認識
既然提及緩存中間件相關的問題及方案,首先就要談談這個緩存。
原本我想通過高速緩存舉例,但是想了想還是用內存舉例子吧。
比如我們現在玩的單機游戲,往往都容量都非常大(幾十G,乃至上百G),輕輕松松都超過了電腦內存(16G)。那么很明顯電腦在運行游戲時,是不可能將整個游戲文件都放入內存的。但是如果文件都在硬盤里,需要的時候再讀取,顯然硬盤的讀寫速度時不夠的(由於游戲文件類別很多,所以硬盤不可能一直順序讀寫),那游戲也會經常卡頓,加載緩慢等。那么該如何解決這個問題呢?
其實這個問題和我們業務中遇到的一些問題是很類似的。一方面我們希望用戶可以在保證用戶體驗的前提下查詢數據(如設備列表,訂單列表等),另一方面我們不可能將所有數據都放在內存(內存的讀寫速度比硬盤快,所以就不解釋為什么用硬盤了)中。那么到底該怎么解決這個問題呢?
這里就需要說到局部性原理了。局部性原理指的是數據的訪問往往趨向於聚集在較小的連續區域。這里的連續區域包含兩個方面:
- 時間維度:一個被使用的數據,在接下來較短的時間內,往往會被再次使用。
- 空間維度:一個被使用的數據,其關聯的數據,往往也會被使用。
局部性原理是在內存,高速緩存部分,提出來用於解決問題的。
其實,我與朋友交流分布式的一些想法時,經常說:分布式系統和單機內部是非常相似的,很多理念都是相通的。當想通了這點后,就可以去思考兩者的區別的。
緩存中間件其實就是利用了局部性原理,不過緩存中間件本身只實現了局部性原理的時間維度。這也是為什么很多人都說緩存中間件是用來保存熱點數據,符合二八定律。不過我們可以在應用部分實現局部性原理的空間維度。
緩存的定位
五六年前,有人就提出一個有關緩存的問題,那就是緩存作為一個非持久化數據,我們該怎么划分它。是否需要保證它的可用性。其中就有一位阿里的前輩在他的書中提到,他更傾向於認為緩存並不是一種持久化數據,不該將緩存作為一種可靠數據源。但是這位前輩也表示現有的框架中對緩存依賴較重,應該在一定程度上保護它們,避免緩存雪崩等情況。
我的看法是,在現有的技術體系中,緩存中間件等已經不再只是一個緩存了。一方面我們已經將Session等重要數據放在了緩存中,並且目前沒有一個更合適的對應存儲(我認為暫時也不需要一個新的存儲方式。但是如果需要的話,可以將緩存中間件實例等按照內容的生命周期等進行分組)。另一方面,我們會需要明確緩存在系統中職責,它只是用來作為緩存,以及一些分布式內存。但是諸如單機所有的內部調用,應該通過消息中間件或RPC等來實現。並且明確不同緩存的職責,如Session不該放在Cookie中等。
緩存的分類
緩存框架大致可以從客戶端到數據源,分成以下分類。
- 瀏覽器緩存
- Cookie
- LocalStorage
- SessionStorage
- CDN緩存
- 負載層緩存
- Nginx緩存模塊
- Squid緩存服務器
- Lua擴展
- 應用層緩存
- Etag
- ThreadLocal
- Guava
- 外部緩存
- Redis
- 數據庫緩存
- MySql緩存
我特意查詢了一下百度,首頁上的有關緩存架構的博客,一半都只是在圍繞着緩存中間件闡述緩存架構,剩下的一般也往往在大分類上有所遺漏(如瀏覽器緩存,數據庫緩存)。當然也有一些博客在專門的領域闡述得較為深入,或者層次的划分比較不錯。故本博客只是在闡述現階段我對緩存架構的認識(也借鑒了一些書籍,課程的緩存體系)。
瀏覽器緩存
瀏覽器緩存,也是很多時候被后端所遺忘的部分。因為這已經不屬於后端的工作了,但這一定屬於架構師或者相關技術負責的職責。當然還有一個原因是我做過專門的前端開發。
說白了,就是在瀏覽器保存一部分數據,當然這需要前端進行開發。
這里直接上圖,大家可以看一下Cookie,LocalStorage,SessionStorage:
PS:圖片來自網絡
優勢
由於是瀏覽器緩存,位於整個web請求相應框架的client端,所以對業務提供方沒有任何負載壓力與影響。只是客戶端的瀏覽器存在些許的存儲占據與計算負載。
注意
- Cookie等的存儲容量是有限的,需要注意分配。
- Cookie等的存儲是明文的,不可以存儲敏感數據,否則會存在安全隱患。
- Cookie等需要注意存儲時間時間的有效設置。
- Cookie等存在一定的學習成本,與相關特性(如Cookie的域名設置問題,父域名無法讀子域名的Cookie數據)。
- Cookie等需要明確業務中有哪些數據適合放在這里,如域名等。
實際應用
在我之前負責的IOT項目中,頁面往往存在大量的數據,如終端列表,傳感器列表,監測點列表等。並且數據間存在一定數據關系,如需要通過現存的終端列表來獲取對應傳感器列表,又如通過傳感器列表來獲取對應報警列表等。
為了避免頁面切換時,為了獲取一個列表而需要多次請求(如為了獲得已選定的終端列表的傳感器列表,需要先請求終端列表),所以通過LocalStorage來存儲終端列表。
CDN緩存
CDN,Content Delivery Network,即內容分發網絡。
- CDN是構建網絡上的內容分發網絡
- CDN可以使得用戶就近獲取所需內容,避免網絡擁塞,提高用戶訪問速度
- CDN依靠部署在各地的服務器,通過鏡像服務器實現內容同步,其包括負載均衡,內容分發,調度等模塊。
優勢
- 降低訪問延遲。使得用戶就近獲取所需內容,避免過多路由造成用戶訪問延遲問題。
- 降低服務器壓力。畢竟放在CDN服務器的內容,就不用到應用服務器獲取了。
- 消除運營商差別。消除運營商之家互聯的瓶頸造成的影響,使得所有用戶獲得同樣的訪問質量
- 集群抗攻擊。廣泛分布的CDN節點,可有有效避免DDOS等攻擊。
缺點
- 同步緩慢。由於CDN是大量且分層的節點分布,所以數據的下發與同步會比較緩慢。
- 如果是使用收費服務,則需要一定支出。如果是自建CDN,則需要技術付出。個人推薦,不必要的話,還是直接采用CDN收費服務吧,性價比更高一些。
- 自身Web體系需要進行相應的調整。如CDN文件更新與服務器文件更新(版本號等手段)等問題。
關鍵技術
該部分內容,引自網易雲課堂。
- 緩存
- 緩存代理軟件:Squid
- 緩存算法決定命中率,源服務器壓力,FTP節點存儲能力
- 分發能力
- 分發能力取決於IDC(網絡數據中西)能力和IDC策略性分布
- 負載均衡
- 負載均衡軟件:Nginx
- 負載均衡(智能調度)決定最佳路由,響應時間,可用性,服務質量
- 基於DNS
- DNS服務器軟件:BIND
- 基於DNS的負載均衡以CNAME實現域名中專,智取最優節點服務
- 緩存點有客戶端瀏覽器緩存,本地DNS服務器緩存
- 緩存內哦讓那個有DNS地址緩存,客戶請求內容緩存,動態內容緩存
- 支持協議
- 靜動態加速(圖片加速,https帶證書加速)
- 下載加速
- 流媒體加速
- 企業應用加速
- 手機應用加速
就當擴展一下見識吧(囧)
實際應用
如果寫過前端代碼,會知道有的時候,我們采用的jQuery等通用JS,CSS等大多是使用公共的cdn地址。
有的公司,會將公司的一些公共JS,圖片等靜態資源(尤其是公司Logo等),放在CDN上。進行網頁開發時,直接引用對應的CDN地址。
負載層緩存
負載層緩存一般是與負載均衡器相關的緩存,這里我就拿Nginx舉例。
Nginx可以通過以下三種手段,實現緩存:
- 本身的緩存模塊
- 轉發請求至對應緩存服務器
- 可以通過lua模塊,直接從外部緩存(如Redis等)獲取緩存數據
接下來一一闡述
Nginx緩存模塊
Nginx的http_proxy模塊,可以實現類似於Squid的緩存功能.
Nginx對客戶端已經訪問的內容在Nginx服務器本地建立緩存副本,那么在一定時間內再次訪問這些內容時,就不需要請求后面的應用服務器了。
與此同時,當后面的應用服務器無法提供服務時(如宕機),Nginx服務器上的緩存資源還能夠回應相關的用戶請求,提高了后面應用服務器的魯棒性(健壯性)。
優勢
- 商業成本無。Nginx是開源的,無需商業付費。
- 技術迭代成本低。現有的Web體系大多采用Nginx,進行技術迭代時,在Nginx只需要增加一個新的模塊即可。
- 可定制。可以根據需要,對指定路徑,指定資源等進行定制化的緩存策略。
缺點
- 需要對Nginx的緩存模塊進行一定的認識與學習。畢竟很多人使用Nginx都只是CV一下配置。
- 需要根據業務需要與技術特點,進行緩存策略的調整。如果缺乏經驗與足夠的認識,可能會指定出不恰當的緩存技術規范(如哪些數據該走Nginx緩存模塊等)。
基本認識
緩存文件位置設置
通過proxy_cache_path參數指定。proxy_cache_path有兩個必填參數:
- 第一個參數為緩存目錄。
- 第二個keys_zone參數指定緩存名稱和占用內存空間的大小。
指定特定請求被緩存
- Nginx默認會緩存所有get和head方法的請求結果,緩存的key默認使用請求字符串
- 自定義key。如proxy_cache_key "$host$request_uri$cookie_user";
- 指定請求至少被發送了多少次以上才被緩存,從而避免低頻請求被緩存。如proxy_cache_min_uses 5;
- 指定哪些方法的請求被緩存。如proxy_cache_methods GET HEAD POST;
緩存有效期
默認情況下,緩存內容是長期留存,除非緩存的容量超出誰知的限制。也可以自定義設置有效時間。如:
- 響應狀態碼為200 302時,10分鍾有效期限:proxy_cache_valid 200 302 10m;
- 對任何狀態碼,5分鍾有效期限:proxy_cache_valid any 5m;
部分請求跳過緩存
通過proxy_cache_bypass指令,明確請求對應的響應來自原始數據,而不是緩存。
例如(該示例來自網易雲課堂) proxy_cache_bypass $cookie_nocache $arg_nocache$arg_comment;
表示:如果任何一個參數不為空,或者不等於0,nginx就不會查找緩存,直接進行代理轉發。
擴展
網頁的緩存是由HTTP消息頭中的“Cache-control”來控制的,常見的取值有private,no-cache,max-age,must-revalidate等,默認為private。詳見下表:
Squid緩存服務器
其實Squid緩存服務器與Nginx緩存十分類似(畢竟Nginx的緩存就是仿照Squid的),所以這里只是表示有這么個選擇,不做深入。
Lua擴展
Nginx是C語言開發(這也是Nginx高性能的根本原因之一),並且Nginx模塊需要用C開發,並且需要符合一系列復雜的規則,還需要熟悉Nginx源碼。
ngx_lua模塊
所以Nginx提供了ngx_lua模塊,通過lua解釋器集成進Nginx。而ngx_lua模塊具備以下特性:
- 高並發,非阻塞地處理各種請求。
- Lua內建協程(可對比golang),從而將異步回調轉換成順序調用的形式。
- 每個協程都有一個獨立的全局環境(變量空間),繼承於全局共享的,只讀的“comman data”。
上述只是簡單提一下Lua擴展,感興趣的可以查詢相關資料。
這里繼續闡述Lua擴展,實現緩存功能。
實際應用
為了幫助大家理解,先說一下實際應用。
Nginx針對HTTP請求處理,有十一個階段。與之相對的,ngx_lua模塊的執行指令都包含在了上述的十一個階段。這里只說一下其中的content_by_lua指令,針對的是Nginx的content階段,可以在location,location if范圍內使用,主要作為內容處理器,接收請求處理並輸出響應。
具體配置如下:
這樣配置后,直接瀏覽器訪問本地ip(或者通過curl命令),可以看到“Hello,world”。
當然,這種用法相對比較初級。在OpenResty中存在一些組件,可以幫助ngx_lua模塊直接訪問Redis這樣的數據源。這樣就可以將一些簡單的數據通過這種方式來進行訪問,降低應用服務器壓力。
優勢
- 降低應用服務器壓力
- 門檻較低。可以按照一些配置模板,直接進行使用
- 擴展性較強。ngx_lua模塊的應用上限還是比較高的
- 靈活性強。ngx_lua模塊的靈活性,表示其在緩存方面具有較高的靈活性
缺點
- 精通難。想要精通這部分的話,需要了解lua腳本,以及Nginx的HTTP請求階段等。
- 額外的開發任務。除了應用開發外,還需要專門的lua開發。
- 耦合性較高。一個頁面,一個功能,卻往往需要進行Nginx與后端聯合開發。
- 任務難以界定。在業務上難以界定一些功能的開發該歸於哪個模塊(Nginx,后端)。
總結
至此,我們已經了解了緩存架構中最靠近用戶的三層緩存:瀏覽器緩存,CDN緩存,負載層緩存。
如果存在什么問題,或者疑惑,可以私信或@我。