Nginx+Redis+Ehcache:大型高併發與高可用的三層快取架構總結

java邵先生發表於2018-09-14

對於高併發架構,毫無疑問快取是最重要的一環,對於大量的高併發,可以採用三層快取架構來實現nginx+redis+ehcache。


Nginx

Nginx+Redis+Ehcache:大型高併發與高可用的三層快取架構總結

對於中介軟體nginx常用來做流量的分發,同時nginx本身也有自己的快取(容量有限),我們可以用來快取熱點資料,讓使用者的請求直接走快取並返回,減少流向伺服器的流量。


1.模板引擎

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


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

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


2.雙層nginx來提升快取命中率

對於部署多個nginx而言,如果不加入一些資料的路由策略,那麼可能導致每個nginx的快取命中率很低。因此可以部署雙層nginx:


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

  2. 後端nginx用來快取一些熱點資料到自己的快取區(分發層只能配置1個嗎)。


Redis

Nginx+Redis+Ehcache:大型高併發與高可用的三層快取架構總結

使用者的請求,在nginx沒有快取相應的資料,那麼會進入到redis快取中,redis可以做到全量資料的快取,通過水平擴充套件能夠提升併發、高可用的能力。


1.持久化機制

將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配置,再次啟動資料就正常啟動。


  1. RDB

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


  2. AOF

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


2.redis叢集

  1. replication

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


  2. sentinal哨兵

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

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


  3. 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

Nginx+Redis+Ehcache:大型高併發與高可用的三層快取架構總結

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

快取資料更新策略

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


對時效性不高的資料,當發生變更之後,採取MQ非同步通知的方式,通過資料生產服務來監聽MQ訊息,然後非同步去拉取服務的資料更新tomcat jvm快取和redis快取,對於nginx本地快取過期之後就可以從redis中拉取新的資料並更新到nginx本地。


經典的快取+資料庫讀寫的模式

cache aside pattern


  1. 讀的時候,先讀快取,快取沒有的話,那麼就讀資料庫,然後取出資料後放入快取,同時返回響應。


  2. 更新的時候,先刪除快取,然後再更新資料庫

    之所以更新的時候只是刪除快取,因為對於一些複雜有邏輯的快取資料,每次資料變更都更新一次快取會造成額外的負擔,只是刪除快取,讓該資料下一次被使用的時候再去執行讀的操作來重新快取,這裡採用的是懶載入的策略。

    舉個例子,一個快取涉及的表的欄位,在1分鐘內就修改了20次,或者是100次,那麼快取跟新20次,100次;但是這個快取在1分鐘內就被讀取了1次,因此每次更新快取就會有大量的冷資料,對於快取符合28黃金法則,20%的資料,佔用了80%的訪問量。


資料庫和redis快取雙寫不一致的問題


1.最初級的快取不一致問題以及解決方案

問題:

如果先修改資料庫再刪除快取,那麼當快取刪除失敗來,那麼會導致資料庫中是最新資料,快取中依舊是舊資料,造成資料不一致。

解決方案:

可以先刪除快取,再修改資料庫,如果刪除快取成功但是資料庫修改失敗,那麼資料庫中是舊資料,快取是空不會出現不一致。


2.比較複雜的資料不一致問題分析

問題:

問題:對於資料發生來變更,先刪除快取,然後去修改資料庫,此時資料庫中的資料還沒有修改成功,併發的讀請求到來去讀快取發現是空,進而去資料庫查詢到此時的舊資料放到快取中,然後之前對資料庫資料的修改成功來,就會造成資料不一致。

解決方案:

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

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


1.讀請求長時阻塞

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

2.多例項部署的請求路由

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

3.熱點資料的路由導師請求的傾斜

因為只有在商品資料更新的時候才會清空快取,然後才會導致讀寫併發,所以更新頻率不是太高的話,這個問題的影響並不是特別大,但是的確可能某些機器的負載會高一些


分散式快取重建併發衝突解決方案


對於快取生產服務,可能部署在多臺機器,當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

這個過程值得我們去深入學習和思考。如果對你有幫助請動動小手關注下吧!


相關文章