標籤:

【iOS學習】大型高並發與高可用的三層緩存架構總結

nginx

對於中間件nginx常用來做流量的分發,同時nginx本身也有自己的緩存(容量有限),我們可以用來緩存熱點數據,讓用戶的請求直接走緩存並返回,減少流向伺服器的流量

一.模板引擎

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

  1. 小型系統可能直接在伺服器端渲染出所有的頁面並放入緩存,之後的相同頁面請求就可以直接返回,不用去查詢數據源或者做數據邏輯處理
  2. 對於頁面非常之多的系統,當模板有改變,上述方法就需要重新渲染所有的頁面模板,毫無疑問是不可取的。因此配合nginx+lua(OpenResty),將模板單獨保存在nginx緩存中,同時對於用來渲染的數據也存在nginx緩存中,但是需要設置一個緩存過期的時間,以儘可能保證模板的實時性

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

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

  1. 分發層nginx負責流量分發的邏輯和策略,根據自己定義的一些規則,比如根據productId進行hash,然後對後端nginx數量取模將某一個商品的訪問請求固定路由到一個nginx後端伺服器上去
  2. 後端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配置,再次啟動數據就正常啟動

  1. RDB

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

  1. AOF

以append-only的模式寫入一個日誌文件中,在redis重啟的時候可以通過回放AOF日誌中的寫入指令來重新構建整個數據集。(實際上每次寫的日誌數據會先到linux os cache,然 後redis每隔一秒調用操作系統fsync將os cache中的數據寫入磁碟)。對redis有一定的性能影響,能夠盡量保證數據的完整性。redis通過rewrite機制來保障AOF文件不會太龐大,基於當前內存數據並可以做適當的指令重構。

二.redis集群

  1. replication

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

  1. sentinal哨兵

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

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

  1. 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。

緩存數據更新策略

  • 對時效性要求高的緩存數據,當發生變更的時候,直接採取資料庫和redis緩存雙寫的方案,讓緩存時效性最高。
  • 對時效性不高的數據,當發生變更之後,採取MQ非同步通知的方式,通過數據生產服務來監聽MQ消息,然後非同步去拉取服務的數據更新tomcat jvm緩存和redis緩存,對於nginx本地緩存過期之後就可以從redis中拉取新的數據並更新到nginx本地。

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

  • 讀的時候,先讀緩存,緩存沒有的話,那麼就讀資料庫,然後取出數據後放入緩存,同時返迴響應
  • 更新的時候,先刪除緩存,然後再更新資料庫

之所以更新的時候只是刪除緩存,因為對於一些複雜有邏輯的緩存數據,每次數據變更都更新一次緩存會造成額外的負擔,只是刪除緩存,讓該數據下一次被使用的時候再去執行讀的操作來重新緩存,這裡採用的是懶載入的策略。舉個例子,一個緩存涉及的表的欄位,在1分鐘內就修改了20次,或者是100次,那麼緩存跟新20次,100次;但是這個緩存在1分鐘內就被讀取了1次,因此每次更新緩存就會有大量的冷數據,對於緩存符合28黃金法則,20%的數據,佔用了80%的訪問量

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

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

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

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

  1. 比較複雜的數據不一致問題分析

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

解決方案:將資料庫與緩存更新與讀取操作進行非同步串列化。當更新數據的時候,根據數據的唯一標識,將更新數據操作路由到一個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

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


推薦閱讀:

TAG:iOS開發 |