天天看點

Nginx+Redis+Ehcache:大型高并發與高可用的三層緩存架構總結

摘要: 對于高并發架構,毫無疑問緩存是最重要的一環,對于大量的高并發,可以采用三層緩存架構來實作,nginx+redis+ehcache

歡迎工作一到五年的Java工程師朋友們加入Java架構開發:860113481

群内提供免費的Java架構學習資料(裡面有高可用、高并發、高性能及分布式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

Nginx

對于中間件nginx常用來做流量的分發,同時nginx本身也有自己的緩存(容量有限),我們可以用來緩存熱點資料,讓使用者的請求直接走緩存并傳回,減少流向伺服器的流量

一、模闆引擎

通常我們可以配合使用freemaker/velocity等模闆引擎來抗住大量的請求

小型系統可能直接在伺服器端渲染出所有的頁面并放入緩存,之後的相同頁面請求就可以直接傳回,不用去查詢資料源或者做資料邏輯處理

對于頁面非常之多的系統,當模闆有改變,上述方法就需要重新渲染所有的頁面模闆,毫無疑問是不可取的。是以配合nginx+lua(OpenResty),将模闆單獨儲存在nginx緩存中,同時對于用來渲染的資料也存在nginx緩存中,但是需要設定一個緩存過期的時間,以盡可能保證模闆的實時性

二、雙層nginx來提升緩存命中率

對于部署多個nginx而言,如果不加入一些資料的路由政策,那麼可能導緻每個nginx的緩存命中率很低。是以可以部署雙層nginx

分發層nginx負責流量分發的邏輯和政策,根據自己定義的一些規則,比如根據productId進行hash,然後對後端nginx數量取模将某一個商品的通路請求固定路由到一個nginx後端伺服器上去

後端nginx用來緩存一些熱點資料到自己的緩存區(分發層隻能配置1個嗎)

Redis

使用者的請求,在nginx沒有緩存相應的資料,那麼會進入到redis緩存中,redis可以做到全量資料的緩存,通過水準擴充能夠提升并發、高可用的能力

一、持久化機制

持久化機制:将redis記憶體中的資料持久化到磁盤中,然後可以定期将磁盤檔案上傳至S3(AWS)或者ODPS(阿裡雲)等一些雲存儲服務上去。

如果同時使用RDB和AOF兩種持久化機制,那麼在redis重新開機的時候,會使用AOF來重新建構資料,因為AOF中的資料更加完整,建議将兩種持久化機制都開啟,用AO F來保證資料不丢失,作為資料恢複的第一選擇;用RDB來作不同程度的冷備,在AOF檔案都丢失或損壞不可用的時候來快速進行資料的恢複。

實戰踩坑:對于想從RDB恢複資料,同時AOF開關也是打開的,一直無法正常恢複,因為每次都會優先從AOF擷取資料(如果臨時關閉AOF,就可以正常恢複)。此時首先停止redis,然後關閉AOF,拷貝RDB到相應目錄,啟動redis之後熱修改配置參數redis config set appendonly yes,此時會自動生成一個目前記憶體資料的AOF檔案,然後再次停止redis,打開AOF配置,再次啟動資料就正常啟動

RDB

對redis中的資料執行周期性的持久化,每一刻持久化的都是全量資料的一個快照。對redis性能影響較小,基于RDB能夠快速異常恢複

AOF

以append-only的模式寫入一個日志檔案中,在redis重新開機的時候可以通過回放AOF日志中的寫入指令來重新建構整個資料集。(實際上每次寫的日志資料會先到linux os cache,然後redis每隔一秒調用作業系統fsync将os cache中的資料寫入磁盤)。對redis有一定的性能影響,能夠盡量保證資料的完整性。redis通過rewrite機制來保障AOF檔案不會太龐大,基于目前記憶體資料并可以做适當的指令重構。

二、redis叢集

replication

一主多從架構,主節點負責寫,并且将資料同步到其他salve節點(異步執行),從節點負責讀,主要就是用來做讀寫分離的橫向擴容架構。這種架構的master節點資料一定要做持久化,否則,當master當機重新開機之後記憶體資料清空,那麼就會将空資料複制到slave,導緻所有資料消失

sentinal哨兵

哨兵是redis叢集架構中很重要的一個元件,負責監控redis master和slave程序是否正常工作,當某個redis執行個體故障時,能夠發送消息報警通知給管理者,當master node當機能夠自動轉移到slave node上,如果故障轉移發生來,會通知client用戶端新的master位址。sentinal至少需要3個執行個體來保證自己的健壯性,并且能夠更好地進行quorum投票以達到majority來執行故障轉移。

前兩種架構方式最大的特點是,每個節點的資料是相同的,無法存取海量的資料。是以哨兵叢集的方式使用與資料量不大的情況

redis cluster

redis cluster支撐多master node,每個master node可以挂載多個slave node,如果mastre挂掉會自動将對應的某個slave切換成master。需要注意的是redis cluster架構下slave節點主要是用來做高可用、故障主備切換的,如果一定需要slave能夠提供讀的能力,修改配置也可以實作(同時也需要修改jedis源碼來支援該情況下的讀寫分離操作)。redis cluster架構下,master就是可以任意擴充的,直接橫向擴充master即可提高讀寫吞吐量。slave節點能夠自動遷移(讓master節點盡量平均擁有slave節點),對整個架構過載備援的slave就可以保障系統更高的可用性。

ehcache

tomcat jvm堆記憶體緩存,主要是抗redis出現大規模災難。如果redis出現了大規模的當機,導緻nginx大量流量直接湧入資料生産服務,那麼最後的tomcat堆記憶體緩存也可以處理部分請求,避免所有請求都直接流向DB

針對上面的技術我特意整理了一下,有很多技術不是靠幾句話能講清楚,是以幹脆找朋友錄制了一些視訊,很多問題其實答案很簡單,但是背後的思考和邏輯不簡單,要做到知其然還要知其是以然。如果想學習Java工程化、高性能及分布式、深入淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友可以加我的Java進階群:694549689,群裡有阿裡大牛直播講解技術,以及Java大型網際網路技術的視訊免費分享給大家。

緩存資料更新政策

對時效性要求高的緩存資料,當發生變更的時候,直接采取資料庫和redis緩存雙寫的方案,讓緩存時效性最高。

對時效性不高的資料,當發生變更之後,采取MQ異步通知的方式,通過資料生産服務來監聽MQ消息,然後異步去拉取服務的資料更新tomcat jvm緩存和redis緩存,對于nginx本地緩存過期之後就可以從redis中拉取新的資料并更新到nginx本地。

經典的緩存+資料庫讀寫的模式,cache aside pattern

讀的時候,先讀緩存,緩存沒有的話,那麼就讀資料庫,然後取出資料後放入緩存,同時傳回響應

更新的時候,先删除緩存,然後再更新資料庫

之是以更新的時候隻是删除緩存,因為對于一些複雜有邏輯的緩存資料,每次資料變更都更新一次緩存會造成額外的負擔,隻是删除緩存,讓該資料下一次被使用的時候再去執行讀的操作來重新緩存,這裡采用的是懶加載的政策。舉個例子,一個緩存涉及的表的字段,在1分鐘内就修改了20次,或者是100次,那麼緩存跟新20次,100次;但是這個緩存在1分鐘内就被讀取了1次,是以每次更新緩存就會有大量的冷資料,對于緩存符合28黃金法則,20%的資料,占用了80%的通路量

資料庫和redis緩存雙寫不一緻的問題

最初級的緩存不一緻問題以及解決方案

問題:如果先修改資料庫再删除緩存,那麼當緩存删除失敗來,那麼會導緻資料庫中是最新資料,緩存中依舊是舊資料,造成資料不一緻。

解決方案:可以先删除緩存,再修改資料庫,如果删除緩存成功但是資料庫修改失敗,那麼資料庫中是舊資料,緩存是空不會出現不一緻

比較複雜的資料不一緻問題分析

問題:對于資料發生來變更,先删除緩存,然後去修改資料庫,此時資料庫中的資料還沒有修改成功,并發的讀請求到來去讀緩存發現是空,進而去資料庫查詢到此時的舊資料放到緩存中,然後之前對資料庫資料的修改成功來,就會造成資料不一緻

解決方案:将資料庫與緩存更新與讀取操作進行異步串行化。當更新資料的時候,根據資料的唯一辨別,将更新資料操作路由到一個jvm内部的隊列中,一個隊列對應一個工作線程,線程串行拿到隊列中的操作一條一條地執行。當執行隊列中的更新資料操作,删除緩存,然後去更新資料庫,此時還沒有完成更新的時候過來一個讀請求,讀到了空的緩存那麼可以先将緩存更新的請求發送至路由之後的隊列中,此時會在隊列積壓,然後同步等待緩存更新完成,一個隊列中多個相同資料緩存更新請求串在一起是沒有意義的,是以可以做過濾處理。等待前面的更新資料操作完成資料庫操作之後,才會去執行下一個緩存更新的操作,此時會從資料庫中讀取最新的資料,然後寫入緩存中,如果請求還在等待時間範圍内,不斷輪詢發現可以取到緩存中值就可以直接傳回(此時可能會有對這個緩存資料的多個請求正在這樣處理);如果請求等待事件超過一定時長,那麼這一次的請求直接讀取資料庫中的舊值

對于這種處理方式需要注意一些問題:

讀請求長時阻塞:由于讀請求進行來非常輕度的異步化,是以對逾時的問題需要格外注意,超過逾時時間會直接查詢DB,處理不好會對DB造成壓力,是以需要測試系統高峰期QPS來調整機器數以及對應機器上的隊列數最終決定合理的請求等待逾時時間

多執行個體部署的請求路由:可能這個服務會部署多個執行個體,那麼必須保證對應的請求都通過nginx伺服器路由到相同的服務執行個體上

熱點資料的路由導師請求的傾斜:因為隻有在商品資料更新的時候才會清空緩存,然後才會導緻讀寫并發,是以更新頻率不是太高的話,這個問題的影響并不是特别大,但是的确可能某些機器的負載會高一些

分布式緩存重建并發沖突解決方案

對于緩存生産服務,可能部署在多台機器,當redis和ehcache對應的緩存資料都過期不存在時,此時可能nginx過來的請求和kafka監聽的請求同時到達,導緻兩者最終都去拉取資料并且存入redis中,是以可能産生并發沖突的問題,可以采用redis或者zookeeper類似的分布式鎖來解決,讓請求的被動緩存重建與監聽主動的緩存重建操作避免并發的沖突,當存入緩存的時候通過對比時間字段廢棄掉舊的資料,儲存最新的資料到緩存

緩存冷啟動以及緩存預熱解決方案

當系統第一次啟動,大量請求湧入,此時的緩存為空,可能會導緻DB崩潰,進而讓系統不可用,同樣當redis所有緩存資料異常丢失,也會導緻該問題。是以,可以提前放入資料到redis避免上述冷啟動的問題,當然也不可能是全量資料,可以根據類似于當天的具體通路情況,實時統計出通路頻率較高的熱資料,這裡熱資料也比較多,需要多個服務并行的分布式去讀寫到redis中(是以要基于zk分布式鎖)

通過nginx+lua将通路流量上報至kafka中,storm從kafka中消費資料,實時統計處每個商品的通路次數,通路次數基于LRU(apache commons collections LRUMap)記憶體資料結構的存儲方案,使用LRUMap去存放是因為記憶體中的性能高,沒有外部依賴,每個storm task啟動的時候基于zk分布式鎖将自己的id寫入zk同一個節點中,每個storm task負責完成自己這裡的熱資料的統計,每隔一段時間就周遊一下這個map,然後維護一個前1000的資料list,然後去更新這個list,最後開啟一個背景線程,每隔一段時間比如一分鐘都将排名的前1000的熱資料list同步到zk中去,存儲到這個storm task對應的一個znode中去

部署多個執行個體的服務,每次啟動的時候就會去拿到上述維護的storm task id清單的節點資料,然後根據taskid,一個一個去嘗試擷取taskid對應的znode的zk分布式鎖,如果能夠擷取到分布式鎖,再去擷取taskid status的鎖進而查詢預熱狀态,如果沒有被預熱過,那麼就将這個taskid對應的熱資料list取出來,進而從DB中查詢出來寫入緩存中,如果taskid分布式鎖擷取失敗,快速抛錯進行下一次循環擷取下一個taskid的分布式鎖即可,此時就是多個服務執行個體基于zk分布式鎖做協調并行的進行緩存的預熱

緩存熱點導緻系統不可用解決方案

對于瞬間大量的相同資料的請求湧入,可能導緻該資料經過hash政策之後對應的應用層nginx被壓垮,如果請求繼續就會影響至其他的nginx,最終導緻所有nginx出現異常整個系統變得不可用。

基于nginx+lua+storm的熱點緩存的流量分發政策自動降級來解決上述問題的出現,可以設定通路次數大于後95%平均值n倍的資料為熱點,在storm中直接發送http請求到流量分發的nginx上去,使其存入本地緩存,然後storm還會将熱點對應的完整緩存資料沒發送到所有的應用nginx伺服器上去,并直接存放到本地緩存。

對于流量分發nginx,通路對應的資料,如果發現是熱點辨別就立即做流量分發政策的降級,對同一個資料的通路從hash到一台應用層nginx降級成為分發至所有的應用層nginx。storm需要儲存上一次識别出來的熱點List,并同目前計算出來的熱點list做對比,如果已經不是熱點資料,則發送對應的http請求至流量分發nginx中來取消對應資料的熱點辨別

緩存雪崩解決方案

redis叢集徹底崩潰,緩存服務大量對redis的請求等待,占用資源,随後緩存服務大量的請求進入源頭服務去查詢DB,使DB壓力過大崩潰,此時對源頭服務的請求也大量等待占用資源,緩存服務大量的資源全部耗費在通路redis和源服務無果,最後使自身無法提供服務,最終會導緻整個網站崩潰。

事前的解決方案,搭建一套高可用架構的redis cluster叢集,主從架構、一主多從,一旦主節點當機,從節點自動跟上,并且最好使用雙機房部署叢集。

事中的解決方案,部署一層ehcache緩存,在redis全部實作情況下能夠抗住部分壓力;對redis cluster的通路做資源隔離,避免所有資源都等待,對redis cluster的通路失敗時的情況去部署對應的熔斷政策,部署redis cluster的降級政策;對源服務通路的限流以及資源隔離

事後的解決方案:redis資料做了備份可以直接恢複,重新開機redis即可;redis資料徹底失敗來或者資料過舊,可以快速緩存預熱,然後讓redis重新啟動。然後由于資源隔離的half-open政策發現redis已經能夠正常通路,那麼所有的請求将自動恢複

緩存穿透解決方案

對于在多級緩存中都沒有對應的資料,并且DB也沒有查詢到資料,此時大量的請求都會直接到達DB,導緻DB承載高并發的問題。解決緩存穿透的問題可以對DB也沒有的資料傳回一個空辨別的資料,進而儲存到各級緩存中,因為有對資料修改的異步監聽,是以當資料有更新,新的資料會被更新到緩存彙中。

Nginx緩存失效導緻redis壓力倍增

可以在nginx本地,設定緩存資料的時候随機緩存的有效期,避免同一時刻緩存都失效而大量請求直接進入redis

這個過程值得我們去深入學習和思考。