中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當前位置: 首頁 > news >正文

怎么做營銷網(wǎng)站推廣搜索引擎優(yōu)化的實驗結果分析

怎么做營銷網(wǎng)站推廣,搜索引擎優(yōu)化的實驗結果分析,公眾號的關注怎么加微信,湖南手機網(wǎng)站制作公司開篇寄語 開篇寄語:緩存,你真的用對了嗎? 你好,我是你的緩存老師陳波,可能大家對我的網(wǎng)名 fishermen 會更熟悉。 我是資深老碼農(nóng)一枚,經(jīng)歷了新浪微博從起步到當前月活數(shù)億用戶的大型互聯(lián)網(wǎng)系統(tǒng)的技術演進…

開篇寄語

開篇寄語:緩存,你真的用對了嗎?

你好,我是你的緩存老師陳波,可能大家對我的網(wǎng)名 fishermen 會更熟悉。

我是資深老碼農(nóng)一枚,經(jīng)歷了新浪微博從起步到當前月活數(shù)億用戶的大型互聯(lián)網(wǎng)系統(tǒng)的技術演進過程,現(xiàn)任新浪微博技術專家。我于 2008 年加入新浪,最初從事新浪 IM 的后端研發(fā)。2009 年之后開始微博 Feed 平臺系統(tǒng)的的研發(fā)及架構工作,深度參與最初若干個版本幾乎所有業(yè)務的開發(fā)和架構改進,2013 年后開始從事微博平臺基礎架構相關的研發(fā)工作。目前主要從事微博 Feed 平臺的基礎設施、緩存中間件、分布式存儲等的研發(fā)及架構優(yōu)化工作。

那么,我們?yōu)槭裁匆獙W習緩存呢?有必要學習緩存嗎?

隨著互聯(lián)網(wǎng)從門戶/搜索時代進入移動社交時代,互聯(lián)網(wǎng)產(chǎn)品也從滿足用戶單向瀏覽的需求,發(fā)展為滿足用戶個性信息獲取及社交的需求。這就要求產(chǎn)品做到以用戶和關系為基礎,對海量數(shù)據(jù)進行實時分析計算。也就意味著,用戶的每次請求,服務后端都要查詢用戶的個人信息、社交關系圖譜,以及關系圖譜涉及到的大量關聯(lián)信息。還要將這些信息進行聚合、過濾、篩選和排序,最終響應給用戶。如果這些信息全部從 DB 中加載,將會是一個無法忍受的漫長等待過程。

而緩存的使用,是提升系統(tǒng)性能、改善用戶體驗的唯一解決之道。

以新浪微博為例,作為移動互聯(lián)網(wǎng)時代的一個開拓者和重量級社交分享平臺,自 2009 年上線后,用戶數(shù)量和微博數(shù)量都從 0 開啟并高速增長,到 2019 年,日活躍用戶已超 2億,每日新發(fā) Feed 1~2億,每日訪問量百億級,歷史數(shù)據(jù)高達千億級。同時,在微博的日常服務中,核心接口可用性要達到 99.99%,響應時間在 10~60ms 以內(nèi),核心單個業(yè)務的數(shù)據(jù)訪問量高達百萬級 QPS。

所有這些數(shù)據(jù)都是靠良好的架構和不斷改進的緩存體系來支撐的。

其實,作為互聯(lián)網(wǎng)公司,只要有直接面對用戶的業(yè)務,要想持續(xù)確保系統(tǒng)的訪問性能和可用性,都需要使用緩存。因此,緩存也是后端工程師面試中一個非常重要的考察點,面試官通常會通過應聘者對緩存相關知識的理解深入程度,來判斷其開發(fā)經(jīng)驗和學習能力??梢哉f,對緩存的掌握程度,在某種意義上決定了后端開發(fā)者的職業(yè)高度。

想學好緩存,需要掌握哪些知識呢?

可以看一下這張“緩存知識點全景圖”。

img

首先,要熟練掌握緩存的基礎知識,了解緩存常用的分類、讀寫模式,熟悉緩存的七大經(jīng)典問題及解決應對之策,同時要從緩存組件的訪問協(xié)議、Client 入手,熟練掌握如何訪問各種緩存組件,如 Memcached、Redis、Pika 等。其次,要盡可能深入理解緩存組件的實現(xiàn)方案、設計原理,了解緩存的各種特性、優(yōu)勢和不足,這樣在緩存數(shù)據(jù)與預期不一致時,能夠快速定位并解決問題。再次,還要多了解線上大中型系統(tǒng)是如何對緩存進行架構設計的。線上系統(tǒng),業(yè)務功能豐富多變,跨域部署環(huán)境復雜,而且熱點頻發(fā),用戶習慣迥異。因此,緩存系統(tǒng)在設計之初就要盡量進行良好設計,規(guī)劃好如何進行Hash及分布、如何保障數(shù)據(jù)的一致性、如何進行擴容和縮容。當然,緩存體系也需要伴隨業(yè)務發(fā)展持續(xù)演進,這就需要對緩存體系進行持續(xù)的狀態(tài)監(jiān)控、異常報警、故障演練,以確保在故障發(fā)生時能及時進行人肉或自動化運維處理,并根據(jù)線上狀況不斷進行優(yōu)化和改進。最后,了解緩存在各種場景下的最佳實踐,理解這些最佳實踐背后的 Tradeoff,做到知其然知其所以然,以便在實際工作中能舉一反三,把知識和經(jīng)驗更好的應用到工作實踐中來。

img

如何高效學習緩存呢?你能學到什么?

對于緩存,網(wǎng)上學習資料很多,但過于零散和重復,想要系統(tǒng)地學習還是需要通過閱讀緩存相關的書籍、論文和緩存源碼,或是學習一些來自實戰(zhàn)總結的網(wǎng)絡課程。但前面幾種形式目前都需要花費較多時間。為了學員既系統(tǒng)又快速地獲得所需知識,拉勾教育推出了“300 分鐘學會”系列技術課,其中“緩存“課由我來講。

在這 300 分鐘里,我將結合自己在微博平臺的緩存架構經(jīng)驗,用 10 課時來分享:

如何更好地引入和使用緩存,自系統(tǒng)設計之初,就把緩存設計的關鍵點對號入座。如何規(guī)避并解決緩存設計中的七大經(jīng)典問題。從協(xié)議、使用技巧、網(wǎng)絡模型、核心數(shù)據(jù)結構、存儲架構、數(shù)據(jù)處理模型、優(yōu)化及改進方案等,多角度全方位深入剖析互聯(lián)網(wǎng)企業(yè)大量使用的Memcached、Redis等開源緩存組件。教你如何利用它們構建一個分布式緩存服務體系。最后,我將結合諸如秒殺、海量計數(shù)、微博 Feed 聚合等經(jīng)典業(yè)務場景,分析如何構建相應的高可用、高性能、易擴展的緩存架構體系。

通過本課程,你可以:

系統(tǒng)地學習緩存之設計架構的關鍵知識點;學會如何更好地使用 Memcached、Redis 等緩存組件;對這些緩存組件的內(nèi)部架構、設計原理有一個較為深入的了解,真正做到知其然更知其所以然;學會如何根據(jù)業(yè)務需要對緩存組件進行二次開發(fā);搞懂如何構建一個大型的分布式緩存服務系統(tǒng);了解在當前多種熱門場景下緩存服務的最佳實踐;現(xiàn)學現(xiàn)用,針對互聯(lián)網(wǎng)大中型系統(tǒng),構建出一個更好的緩存架構體系,在大幅提升系統(tǒng)吞吐和響應性能的同時,達到高可用、高擴展,從而可以更從容地應對海量并發(fā)請求和極端熱點事件。

img

第一章:緩存的原理、引入及設計

第01講:業(yè)務數(shù)據(jù)訪問性能太低怎么辦?

你好,我是你的緩存老師陳波,歡迎進入第1課時“緩存的原理”。這節(jié)課主要講緩存的基本思想、緩存的優(yōu)點、緩存的代價三個部分。

緩存的定義

先來看下緩存的定義。

緩存最初的含義,是指用于加速 CPU 數(shù)據(jù)交換的 RAM,即隨機存取存儲器,通常這種存儲器使用更昂貴但快速的靜態(tài) RAM(SRAM)技術,用以對 DRAM進 行加速。這是一個狹義緩存的定義。而廣義緩存的定義則更寬泛,任何可以用于數(shù)據(jù)高速交換的存儲介質(zhì)都是緩存,可以是硬件也可以是軟件。

img

緩存存在的意義就是通過開辟一個新的數(shù)據(jù)交換緩沖區(qū),來解決原始數(shù)據(jù)獲取代價太大的問題,讓數(shù)據(jù)得到更快的訪問。本課主要聚焦于廣義緩存,特別是互聯(lián)網(wǎng)產(chǎn)品大量使用的各種緩存組件和技術。
緩存原理
緩存的基本思想

img

緩存構建的基本思想是利用時間局限性原理,通過空間換時間來達到加速數(shù)據(jù)獲取的目的,同時由于緩存空間的成本較高,在實際設計架構中還要考慮訪問延遲和成本的權衡問題。這里面有 3 個關鍵點。

一是時間局限性原理,即被獲取過一次的數(shù)據(jù)在未來會被多次引用,比如一條微博被一個人感興趣并閱讀后,它大概率還會被更多人閱讀,當然如果變成熱門微博后,會被數(shù)以百萬/千萬計算的更多用戶查看。二是以空間換時間,因為原始數(shù)據(jù)獲取太慢,所以我們開辟一塊高速獨立空間,提供高效訪問,來達到數(shù)據(jù)獲取加速的目的。三是性能成本 Tradeoff,構建系統(tǒng)時希望系統(tǒng)的訪問性能越高越好,訪問延遲越低小越好。但維持相同數(shù)據(jù)規(guī)模的存儲及訪問,性能越高延遲越小,成本也會越高,所以在系統(tǒng)架構設計時,你需要在系統(tǒng)性能和開發(fā)運行成本之間做取舍。比如左邊這張圖,相同成本的容量,SSD 硬盤容量會比內(nèi)存大 10~30 倍以上,但讀寫延遲卻高 50~100 倍。

緩存的優(yōu)勢

緩存的優(yōu)勢主要有以下幾點:

提升訪問性能降低網(wǎng)絡擁堵減輕服務負載增強可擴展性

通過前面的介紹,我們已經(jīng)知道緩存存儲原始數(shù)據(jù),可以大幅提升訪問性能。不過在實際業(yè)務場景中,緩存中存儲的往往是需要頻繁訪問的中間數(shù)據(jù)甚至最終結果,這些數(shù)據(jù)相比 DB 中的原始數(shù)據(jù)小很多,這樣就可以減少網(wǎng)絡流量,降低網(wǎng)絡擁堵,同時由于減少了解析和計算,調(diào)用方和存儲服務的負載也可以大幅降低。緩存的讀寫性能很高,預熱快,在數(shù)據(jù)訪問存在性能瓶頸或遇到突發(fā)流量,系統(tǒng)讀寫壓力大增時,可以快速部署上線,同時在流量穩(wěn)定后,也可以隨時下線,從而使系統(tǒng)的可擴展性大大增強。
緩存的代價

然而不幸的是,任何事情都有兩面性,緩存也不例外,我們在享受緩存帶來一系列好處的同時,也注定需要付出一定的代價。

首先,服務系統(tǒng)中引入緩存,會增加系統(tǒng)的復雜度。其次,由于緩存相比原始 DB 存儲的成本更高,所以系統(tǒng)部署及運行的費用也會更高。最后,由于一份數(shù)據(jù)同時存在緩存和 DB 中,甚至緩存內(nèi)部也會有多個數(shù)據(jù)副本,多份數(shù)據(jù)就會存在一致性問題,同時緩存體系本身也會存在可用性問題和分區(qū)的問題。這就需要我們加強對緩存原理、緩存組件以及優(yōu)秀緩存體系實踐的理解,從系統(tǒng)架構之初就對緩存進行良好設計,降低緩存引入的副作用,讓緩存體系成為服務系統(tǒng)高效穩(wěn)定運行的強力基石。

一般來講,服務系統(tǒng)的全量原始數(shù)據(jù)存儲在 DB 中(如 MySQL、HBase 等),所有數(shù)據(jù)的讀寫都可以通過 DB 操作來獲取。但 DB 讀寫性能低、延遲高,如 MySQL 單實例的讀寫 QPS 通常只有千級別(3000~6000),讀寫平均耗時 10~100ms 級別,如果一個用戶請求需要查 20 個不同的數(shù)據(jù)來聚合,僅僅 DB 請求就需要數(shù)百毫秒甚至數(shù)秒。而 cache 的讀寫性能正好可以彌補 DB 的不足,比如 Memcached 的讀寫 QPS 可以達到 10~100萬 級別,讀寫平均耗時在 1ms 以下,結合并發(fā)訪問技術,單個請求即便查上百條數(shù)據(jù),也可以輕松應對。

但 cache 容量小,只能存儲部分訪問頻繁的熱數(shù)據(jù),同時,同一份數(shù)據(jù)可能同時存在 cache 和 DB,如果處理不當,就會出現(xiàn)數(shù)據(jù)不一致的問題。所以服務系統(tǒng)在處理業(yè)務請求時,需要對 cache 的讀寫方式進行適當設計,既要保證數(shù)據(jù)高效返回,又要盡量避免數(shù)據(jù)不一致等各種問題。

好了,第 1 課時的內(nèi)容到這里就全部結束了,我們一起來做一個簡單的回顧。首先,這一課時,你先了解了緩存的定義以及基本思想。然后,又學習了緩存的優(yōu)點和代價。

第02講:如何根據(jù)業(yè)務來選擇緩存模式和組件?

你好,我是你的緩存老師陳波,歡迎進入第 2 課時“緩存的讀寫模式及分類”。這一課時我們主要學習緩存的讀寫模式以及緩存的分類。

緩存讀寫模式

如下圖,業(yè)務系統(tǒng)讀寫緩存有 3 種模式:

Cache Aside(旁路緩存)Read/Write Through(讀寫穿透)Write Behind Caching(異步緩存寫入)

imgCache Aside

img

如上圖所示,Cache Aside 模式中,業(yè)務應用方對于寫,是更新 DB 后,直接將 key 從 cache 中刪除,然后由 DB 驅動緩存數(shù)據(jù)的更新;而對于讀,是先讀 cache,如果 cache 沒有,則讀 DB,同時將從 DB 中讀取的數(shù)據(jù)回寫到 cache。

這種模式的特點是,業(yè)務端處理所有數(shù)據(jù)訪問細節(jié),同時利用 Lazy 計算的思想,更新 DB 后,直接刪除 cache 并通過 DB 更新,確保數(shù)據(jù)以 DB 結果為準,則可以大幅降低 cache 和 DB 中數(shù)據(jù)不一致的概率。

如果沒有專門的存儲服務,同時是對數(shù)據(jù)一致性要求比較高的業(yè)務,或者是緩存數(shù)據(jù)更新比較復雜的業(yè)務,這些情況都比較適合使用 Cache Aside 模式。如微博發(fā)展初期,不少業(yè)務采用這種模式,這些緩存數(shù)據(jù)需要通過多個原始數(shù)據(jù)進行計算后設置。在部分數(shù)據(jù)變更后,直接刪除緩存。同時,使用一個 Trigger 組件,實時讀取 DB 的變更日志,然后重新計算并更新緩存。如果讀緩存的時候,Trigger 還沒寫入 cache,則由調(diào)用方自行到 DB 加載計算并寫入 cache。
Read/Write Through
img

如上圖,對于 Cache Aside 模式,業(yè)務應用需要同時維護 cache 和 DB 兩個數(shù)據(jù)存儲方,過于繁瑣,于是就有了 Read/Write Through 模式。在這種模式下,業(yè)務應用只關注一個存儲服務即可,業(yè)務方的讀寫 cache 和 DB 的操作,都由存儲服務代理。存儲服務收到業(yè)務應用的寫請求時,會首先查 cache,如果數(shù)據(jù)在 cache 中不存在,則只更新 DB,如果數(shù)據(jù)在 cache 中存在,則先更新 cache,然后更新 DB。而存儲服務收到讀請求時,如果命中 cache 直接返回,否則先從 DB 加載,回種到 cache 后返回響應。

這種模式的特點是,存儲服務封裝了所有的數(shù)據(jù)處理細節(jié),業(yè)務應用端代碼只用關注業(yè)務邏輯本身,系統(tǒng)的隔離性更佳。另外,進行寫操作時,如果 cache 中沒有數(shù)據(jù)則不更新,有緩存數(shù)據(jù)才更新,內(nèi)存效率更高。

微博 Feed 的 Outbox Vector(即用戶最新微博列表)就采用這種模式。一些粉絲較少且不活躍的用戶發(fā)表微博后,Vector 服務會首先查詢 Vector Cache,如果 cache 中沒有該用戶的 Outbox 記錄,則不寫該用戶的 cache 數(shù)據(jù),直接更新 DB 后就返回,只有 cache 中存在才會通過 CAS 指令進行更新。
Write Behind Caching

img

Write Behind Caching 模式與 Read/Write Through 模式類似,也由數(shù)據(jù)存儲服務來管理 cache 和 DB 的讀寫。不同點是,數(shù)據(jù)更新時,Read/write Through 是同步更新 cache 和 DB,而 Write Behind Caching 則是只更新緩存,不直接更新 DB,而是改為異步批量的方式來更新 DB。該模式的特點是,數(shù)據(jù)存儲的寫性能最高,非常適合一些變更特別頻繁的業(yè)務,特別是可以合并寫請求的業(yè)務,比如對一些計數(shù)業(yè)務,一條 Feed 被點贊 1萬 次,如果更新 1萬 次 DB 代價很大,而合并成一次請求直接加 1萬,則是一個非常輕量的操作。但這種模型有個顯著的缺點,即數(shù)據(jù)的一致性變差,甚至在一些極端場景下可能會丟失數(shù)據(jù)。比如系統(tǒng) Crash、機器宕機時,如果有數(shù)據(jù)還沒保存到 DB,則會存在丟失的風險。所以這種讀寫模式適合變更頻率特別高,但對一致性要求不太高的業(yè)務,這樣寫操作可以異步批量寫入 DB,減小 DB 壓力。

講到這里,緩存的三種讀寫模式講完了,你可以看到三種模式各有優(yōu)劣,不存在最佳模式。實際上,我們也不可能設計出一個最佳的完美模式出來,如同前面講到的空間換時間、訪問延遲換低成本一樣,高性能和強一致性從來都是有沖突的,系統(tǒng)設計從來就是取舍,隨處需要 trade-off。這個思想會貫穿整個 cache 課程,這也許是我們學習這個課程的另外一個收獲,即如何根據(jù)業(yè)務場景,更好的做 trade-off,從而設計出更好的服務系統(tǒng)。
緩存分類及常用緩存介紹

前面介紹了緩存的基本思想、優(yōu)勢、代價以及讀寫模式,接下來一起看下互聯(lián)網(wǎng)企業(yè)常用的緩存有哪些分類。
按宿主層次分類

按宿主層次分類的話,緩存一般可以分為本地 Cache、進程間 Cache 和遠程 Cache。

本地 Cache 是指業(yè)務進程內(nèi)的緩存,這類緩存由于在業(yè)務系統(tǒng)進程內(nèi),所以讀寫性能超高且無任何網(wǎng)絡開銷,但不足是會隨著業(yè)務系統(tǒng)重啟而丟失。進程間 Cache 是本機獨立運行的緩存,這類緩存讀寫性能較高,不會隨著業(yè)務系統(tǒng)重啟丟數(shù)據(jù),并且可以大幅減少網(wǎng)絡開銷,但不足是業(yè)務系統(tǒng)和緩存都在相同宿主機,運維復雜,且存在資源競爭。遠程 Cache 是指跨機器部署的緩存,這類緩存因為獨立設備部署,容量大且易擴展,在互聯(lián)網(wǎng)企業(yè)使用最廣泛。不過遠程緩存需要跨機訪問,在高讀寫壓力下,帶寬容易成為瓶頸。

本地 Cache 的緩存組件有 Ehcache、Guava Cache 等,開發(fā)者自己也可以用 Map、Set 等輕松構建一個自己專用的本地 Cache。進程間 Cache 和遠程 Cache 的緩存組件相同,只是部署位置的差異罷了,這類緩存組件有 Memcached、Redis、Pika 等。
按存儲介質(zhì)分類

還有一種常見的分類方式是按存儲介質(zhì)來分,這樣可以分為內(nèi)存型緩存和持久化型緩存。

內(nèi)存型緩存將數(shù)據(jù)存儲在內(nèi)存,讀寫性能很高,但緩存系統(tǒng)重啟或 Crash 后,內(nèi)存數(shù)據(jù)會丟失。持久化型緩存將數(shù)據(jù)存儲到 SSD/Fusion-IO 硬盤中,相同成本下,這種緩存的容量會比內(nèi)存型緩存大 1 個數(shù)量級以上,而且數(shù)據(jù)會持久化落地,重啟不丟失,但讀寫性能相對低 1~2 個數(shù)量級。Memcached 是典型的內(nèi)存型緩存,而 Pika 以及其他基于 RocksDB 開發(fā)的緩存組件等則屬于持久化型緩存。

第03講:設計緩存架構時需要考量哪些因素?

你好,我是你的緩存老師陳波,歡迎進入第 3 課時“緩存的引入及架構設計”。

至此,緩存原理相關的主要知識點就講完了,接下來會講到如何引入緩存并進行設計架構,以及在緩存設計架構中的一些關鍵考量點。
緩存的引入及架構設計
緩存組件選擇

在設計架構緩存時,你首先要選定緩存組件,比如要用 Local-Cache,還是 Redis、Memcached、Pika 等開源緩存組件,如果業(yè)務緩存需求比較特殊,你還要考慮是直接定制開發(fā)一個新的緩存組件,還是對開源緩存進行二次開發(fā),來滿足業(yè)務需要。
緩存數(shù)據(jù)結構設計

確定好緩存組件后,你還要根據(jù)業(yè)務訪問的特點,進行緩存數(shù)據(jù)結構的設計。對于直接簡單 KV 讀寫的業(yè)務,你可以將這些業(yè)務數(shù)據(jù)封裝為 String、Json、Protocol Buffer 等格式,序列化成字節(jié)序列,然后直接寫入緩存中。讀取時,先從緩存組件獲取到數(shù)據(jù)的字節(jié)序列,再進行反序列化操作即可。對于只需要存取部分字段或需要在緩存端進行計算的業(yè)務,你可以把數(shù)據(jù)設計為 Hash、Set、List、Geo 等結構,存儲到支持復雜集合數(shù)據(jù)類型的緩存中,如 Redis、Pika 等。
緩存分布設計

確定了緩存組件,設計好了緩存數(shù)據(jù)結構,接下來就要設計緩存的分布??梢詮?3 個維度來進行緩存分布設計。

首先,要選擇分布式算法,是采用取模還是一致性 Hash 進行分布。取模分布的方案簡單,每個 key 只會存在確定的緩存節(jié)點,一致性 Hash 分布的方案相對復雜,一個 key 對應的緩存節(jié)點不確定。但一致性 Hash 分布,可以在部分緩存節(jié)點異常時,將失效節(jié)點的數(shù)據(jù)訪問均衡分散到其他正常存活的節(jié)點,從而更好地保證了緩存系統(tǒng)的穩(wěn)定性。其次,分布讀寫訪問如何進行實施,是由緩存 Client 直接進行 Hash 分布定位讀寫,還是通過 Proxy 代理來進行讀寫路由?Client 直接讀寫,讀寫性能最佳,但需要 Client 感知分布策略。在緩存部署發(fā)生在線變化時,也需要及時通知所有緩存 Client,避免讀寫異常,另外,Client 實現(xiàn)也較復雜。而通過 Proxy 路由,Client 只需直接訪問 Proxy,分布邏輯及部署變更都由 Proxy 來處理,對業(yè)務應用開發(fā)最友好,但業(yè)務訪問多一跳,訪問性能會有一定的損失。最后,緩存系統(tǒng)運行過程中,如果待緩存的數(shù)據(jù)量增長過快,會導致大量緩存數(shù)據(jù)被剔除,緩存命中率會下降,數(shù)據(jù)訪問性能會隨之降低,這樣就需要將數(shù)據(jù)從緩存節(jié)點進行動態(tài)拆分,把部分數(shù)據(jù)水平遷移到其他緩存節(jié)點。這個遷移過程需要考慮,是由 Proxy 進行遷移還是緩存 Server 自身進行遷移,甚至根本就不支持遷移。對于 Memcached,一般不支持遷移,對 Redis,社區(qū)版本是依靠緩存 Server 進行遷移,而對 Codis 則是通過 Admin、Proxy 配合后端緩存組件進行遷移。

緩存架構部署及運維管理

設計完畢緩存的分布策略后,接下來就要考慮緩存的架構部署及運維管理了。架構部署主要考慮如何對緩存進行分池、分層、分 IDC,以及是否需要進行異構處理。

核心的、高并發(fā)訪問的不同數(shù)據(jù),需要分別分拆到獨立的緩存池中,進行分別訪問,避免相互影響;訪問量較小、非核心的業(yè)務數(shù)據(jù),則可以混存。對海量數(shù)據(jù)、訪問超過 10~100萬 級的業(yè)務數(shù)據(jù),要考慮分層訪問,并且要分攤訪問量,避免緩存過載。如果業(yè)務系統(tǒng)需要多 IDC 部署甚至異地多活,則需要對緩存體系也進行多 IDC 部署,要考慮如何跨 IDC 對緩存數(shù)據(jù)進行更新,可以采用直接跨 IDC 讀寫,也可以采用 DataBus 配合隊列機進行不同 IDC 的消息同步,然后由消息處理機進行緩存更新,還可以由各個 IDC 的 DB Trigger 進行緩存更新。某些極端場景下,還需要把多種緩存組件進行組合使用,通過緩存異構達到最佳讀寫性能。站在系統(tǒng)層面,要想更好得管理緩存,還要考慮緩存的服務化,考慮緩存體系如何更好得進行集群管理、監(jiān)控運維等。

緩存設計架構的常見考量點

在緩存設計架構的過程中,有一些非常重要的考量點,如下圖所示,只有分析清楚了這些考量點,才能設計架構出更佳的緩存體系。

img
讀寫方式

首先是 value 的讀寫方式。是全部整體讀寫,還是只部分讀寫及變更?是否需要內(nèi)部計算?比如,用戶粉絲數(shù),很多普通用戶的粉絲有幾千到幾萬,而大 V 的粉絲更是高達幾千萬甚至過億,因此,獲取粉絲列表肯定不能采用整體讀寫的方式,只能部分獲取。另外在判斷某用戶是否關注了另外一個用戶時,也不需要拉取該用戶的全部關注列表,直接在關注列表上進行檢查判斷,然后返回 True/False 或 0/1 的方式更為高效。
KV size

然后是不同業(yè)務數(shù)據(jù)緩存 KV 的 size。如果單個業(yè)務的 KV size 過大,需要分拆成多個 KV 來緩存。但是,不同緩存數(shù)據(jù)的 KV size 如果差異過大,也不能緩存在一起,避免緩存效率的低下和相互影響。
key 的數(shù)量

key 的數(shù)量也是一個重要考慮因素。如果 key 數(shù)量不大,可以在緩存中存下全量數(shù)據(jù),把緩存當 DB 存儲來用,如果緩存讀取 miss,則表明數(shù)據(jù)不存在,根本不需要再去 DB 查詢。如果數(shù)據(jù)量巨大,則在緩存中盡可能只保留頻繁訪問的熱數(shù)據(jù),對于冷數(shù)據(jù)直接訪問 DB。
讀寫峰值

另外,對緩存數(shù)據(jù)的讀寫峰值,如果小于 10萬 級別,簡單分拆到獨立 Cache 池即可。而一旦數(shù)據(jù)的讀寫峰值超過 10萬 甚至到達 100萬 級的QPS,則需要對 Cache 進行分層處理,可以同時使用 Local-Cache 配合遠程 cache,甚至遠程緩存內(nèi)部繼續(xù)分層疊加分池進行處理。微博業(yè)務中,大多數(shù)核心業(yè)務的 Memcached 訪問都采用的這種處理方式。
命中率

緩存的命中率對整個服務體系的性能影響甚大。對于核心高并發(fā)訪問的業(yè)務,需要預留足夠的容量,確保核心業(yè)務緩存維持較高的命中率。比如微博中的 Feed Vector Cache,常年的命中率高達 99.5% 以上。為了持續(xù)保持緩存的命中率,緩存體系需要持續(xù)監(jiān)控,及時進行故障處理或故障轉移。同時在部分緩存節(jié)點異常、命中率下降時,故障轉移方案,需要考慮是采用一致性 Hash 分布的訪問漂移策略,還是采用數(shù)據(jù)多層備份策略。

過期策略

可以設置較短的過期時間,讓冷 key 自動過期;也可以讓 key 帶上時間戳,同時設置較長的過期時間,比如很多業(yè)務系統(tǒng)內(nèi)部有這樣一些 key:key_20190801。

平均緩存穿透加載時間

平均緩存穿透加載時間在某些業(yè)務場景下也很重要,對于一些緩存穿透后,加載時間特別長或者需要復雜計算的數(shù)據(jù),而且訪問量還比較大的業(yè)務數(shù)據(jù),要配置更多容量,維持更高的命中率,從而減少穿透到 DB 的概率,來確保整個系統(tǒng)的訪問性能。
緩存可運維性

對于緩存的可運維性考慮,則需要考慮緩存體系的集群管理,如何進行一鍵擴縮容,如何進行緩存組件的升級和變更,如何快速發(fā)現(xiàn)并定位問題,如何持續(xù)監(jiān)控報警,最好有一個完善的運維平臺,將各種運維工具進行集成。
緩存安全性

對于緩存的安全性考慮,一方面可以限制來源 IP,只允許內(nèi)網(wǎng)訪問,同時對于一些關鍵性指令,需要增加訪問權限,避免被攻擊或誤操作時,導致重大后果。

好了,第3課時的內(nèi)容到這里就全部結束了,我們一起來做一個簡單的回顧。首先,我們學習了在系統(tǒng)研發(fā)中,如何引入緩存,如何按照4步走對緩存進行設計架構及管理。最后,還熟悉了緩存設計架構中的考量點,這樣你在緩存設計架構時對號入座即可。

第二章:7大緩存經(jīng)典問題

第04講:緩存失效、穿透和雪崩問題怎么處理?

你好,我是你的緩存老師陳波,歡迎進入第 4 課時“緩存訪問相關的經(jīng)典問題”。

前面講解了緩存的原理、引入,以及設計架構,總結了緩存在使用及設計架構過程中的很多套路和關鍵考量點。實際上,在緩存系統(tǒng)的設計架構中,還有很多坑,很多的明槍暗箭,如果設計不當會導致很多嚴重的后果。設計不當,輕則請求變慢、性能降低,重則會數(shù)據(jù)不一致、系統(tǒng)可用性降低,甚至會導致緩存雪崩,整個系統(tǒng)無法對外提供服務。

接下來將對緩存設計中的 7 大經(jīng)典問題,如下圖,進行問題描述、原因分析,并給出日常研發(fā)中,可能會出現(xiàn)該問題的業(yè)務場景,最后給出這些經(jīng)典問題的解決方案。本課時首先學習緩存失效、緩存穿透與緩存雪崩。

img

緩存失效
問題描述

緩存第一個經(jīng)典問題是緩存失效。上一課時講到,服務系統(tǒng)查數(shù)據(jù),首先會查緩存,如果緩存數(shù)據(jù)不存在,就進一步查 DB,最后查到數(shù)據(jù)后回種到緩存并返回。緩存的性能比 DB 高 50~100 倍以上,所以我們希望數(shù)據(jù)查詢盡可能命中緩存,這樣系統(tǒng)負荷最小,性能最佳。緩存里的數(shù)據(jù)存儲基本上都是以 key 為索引進行存儲和獲取的。業(yè)務訪問時,如果大量的 key 同時過期,很多緩存數(shù)據(jù)訪問都會 miss,進而穿透到 DB,DB 的壓力就會明顯上升,由于 DB 的性能較差,只在緩存的 1%~2% 以下,這樣請求的慢查率會明顯上升。這就是緩存失效的問題。
原因分析

導致緩存失效,特別是很多 key 一起失效的原因,跟我們?nèi)粘懢彺娴倪^期時間息息相關。

在寫緩存時,我們一般會根據(jù)業(yè)務的訪問特點,給每種業(yè)務數(shù)據(jù)預置一個過期時間,在寫緩存時把這個過期時間帶上,讓緩存數(shù)據(jù)在這個固定的過期時間后被淘汰。一般情況下,因為緩存數(shù)據(jù)是逐步寫入的,所以也是逐步過期被淘汰的。但在某些場景,一大批數(shù)據(jù)會被系統(tǒng)主動或被動從 DB 批量加載,然后寫入緩存。這些數(shù)據(jù)寫入緩存時,由于使用相同的過期時間,在經(jīng)歷這個過期時間之后,這批數(shù)據(jù)就會一起到期,從而被緩存淘汰。此時,對這批數(shù)據(jù)的所有請求,都會出現(xiàn)緩存失效,從而都穿透到 DB,DB 由于查詢量太大,就很容易壓力大增,請求變慢。
業(yè)務場景

很多業(yè)務場景,稍不注意,就出現(xiàn)大量的緩存失效,進而導致系統(tǒng) DB 壓力大、請求變慢的情況。比如同一批火車票、飛機票,當可以售賣時,系統(tǒng)會一次性加載到緩存,如果緩存寫入時,過期時間按照預先設置的過期值,那過期時間到期后,系統(tǒng)就會因緩存失效出現(xiàn)變慢的問題。類似的業(yè)務場景還有很多,比如微博業(yè)務,會有后臺離線系統(tǒng),持續(xù)計算熱門微博,每當計算結束,會將這批熱門微博批量寫入對應的緩存。還比如,很多業(yè)務,在部署新 IDC 或新業(yè)務上線時,會進行緩存預熱,也會一次性加載大批熱數(shù)據(jù)。
解決方案

對于批量 key 緩存失效的問題,原因既然是預置的固定過期時間,那解決方案也從這里入手。設計緩存的過期時間時,使用公式:過期時間=baes 時間+隨機時間。即相同業(yè)務數(shù)據(jù)寫緩存時,在基礎過期時間之上,再加一個隨機的過期時間,讓數(shù)據(jù)在未來一段時間內(nèi)慢慢過期,避免瞬時全部過期,對 DB 造成過大壓力,如下圖所示。

img

緩存穿透
問題描述

第二個經(jīng)典問題是緩存穿透。緩存穿透是一個很有意思的問題。因為緩存穿透發(fā)生的概率很低,所以一般很難被發(fā)現(xiàn)。但是,一旦你發(fā)現(xiàn)了,而且量還不小,你可能立即就會經(jīng)歷一個忙碌的夜晚。因為對于正常訪問,訪問的數(shù)據(jù)即便不在緩存,也可以通過 DB 加載回種到緩存。而緩存穿透,則意味著有特殊訪客在查詢一個不存在的 key,導致每次查詢都會穿透到 DB,如果這個特殊訪客再控制一批肉雞機器,持續(xù)訪問你系統(tǒng)里不存在的 key,就會對 DB 產(chǎn)生很大的壓力,從而影響正常服務。
原因分析

緩存穿透存在的原因,就是因為我們在系統(tǒng)設計時,更多考慮的是正常訪問路徑,對特殊訪問路徑、異常訪問路徑考慮相對欠缺。

緩存訪問設計的正常路徑,是先訪問 cache,cache miss 后查 DB,DB 查詢到結果后,回種緩存返回。這對于正常的 key 訪問是沒有問題的,但是如果用戶訪問的是一個不存在的 key,查 DB 返回空(即一個 NULL),那就不會把這個空寫回cache。那以后不管查詢多少次這個不存在的 key,都會 cache miss,都會查詢 DB。整個系統(tǒng)就會退化成一個“前端+DB“的系統(tǒng),由于 DB 的吞吐只在 cache 的 1%~2% 以下,如果有特殊訪客,大量訪問這些不存在的 key,就會導致系統(tǒng)的性能嚴重退化,影響正常用戶的訪問。
業(yè)務場景

緩存穿透的業(yè)務場景很多,比如通過不存在的 UID 訪問用戶,通過不存在的車次 ID 查看購票信息。用戶輸入錯誤,偶爾幾個這種請求問題不大,但如果是大量這種請求,就會對系統(tǒng)影響非常大。
解決方案

那么如何解決這種問題呢?如下圖所示。

第一種方案就是,查詢這些不存在的數(shù)據(jù)時,第一次查 DB,雖然沒查到結果返回 NULL,仍然記錄這個 key 到緩存,只是這個 key 對應的 value 是一個特殊設置的值。第二種方案是,構建一個 BloomFilter 緩存過濾器,記錄全量數(shù)據(jù),這樣訪問數(shù)據(jù)時,可以直接通過 BloomFilter 判斷這個 key 是否存在,如果不存在直接返回即可,根本無需查緩存和 DB。

img

不過這兩種方案在設計時仍然有一些要注意的坑。

對于方案一,如果特殊訪客持續(xù)訪問大量的不存在的 key,這些 key 即便只存一個簡單的默認值,也會占用大量的緩存空間,導致正常 key 的命中率下降。所以進一步的改進措施是,對這些不存在的 key 只存較短的時間,讓它們盡快過期;或者將這些不存在的 key 存在一個獨立的公共緩存,從緩存查找時,先查正常的緩存組件,如果 miss,則查一下公共的非法 key 的緩存,如果后者命中,直接返回,否則穿透 DB,如果查出來是空,則回種到非法 key 緩存,否則回種到正常緩存。對于方案二,BloomFilter 要緩存全量的 key,這就要求全量的 key 數(shù)量不大,10億 條數(shù)據(jù)以內(nèi)最佳,因為 10億 條數(shù)據(jù)大概要占用 1.2GB 的內(nèi)存。也可以用 BloomFilter 緩存非法 key,每次發(fā)現(xiàn)一個 key 是不存在的非法 key,就記錄到 BloomFilter 中,這種記錄方案,會導致 BloomFilter 存儲的 key 持續(xù)高速增長,為了避免記錄 key 太多而導致誤判率增大,需要定期清零處理。

BloomFilter

BloomFilter 是一個非常有意思的數(shù)據(jù)結構,不僅僅可以擋住非法 key 攻擊,還可以低成本、高性能地對海量數(shù)據(jù)進行判斷,比如一個系統(tǒng)有數(shù)億用戶和百億級新聞 feed,就可以用 BloomFilter 來判斷某個用戶是否閱讀某條新聞 feed。下面來對 BloomFilter 數(shù)據(jù)結構做一個分析,如下圖所示。

img

BloomFilter 的目的是檢測一個元素是否存在于一個集合內(nèi)。它的原理,是用 bit 數(shù)據(jù)組來表示一個集合,對一個 key 進行多次不同的 Hash 檢測,如果所有 Hash 對應的 bit 位都是 1,則表明 key 非常大概率存在,平均單記錄占用 1.2 字節(jié)即可達到 99%,只要有一次 Hash 對應的 bit 位是 0,就說明這個 key 肯定不存在于這個集合內(nèi)。

BloomFilter 的算法是,首先分配一塊內(nèi)存空間做 bit 數(shù)組,數(shù)組的 bit 位初始值全部設為 0,加入元素時,采用 k 個相互獨立的 Hash 函數(shù)計算,然后將元素 Hash 映射的 K 個位置全部設置為 1。檢測 key 時,仍然用這 k 個 Hash 函數(shù)計算出 k 個位置,如果位置全部為 1,則表明 key 存在,否則不存在。

BloomFilter 的優(yōu)勢是,全內(nèi)存操作,性能很高。另外空間效率非常高,要達到 1% 的誤判率,平均單條記錄占用 1.2 字節(jié)即可。而且,平均單條記錄每增加 0.6 字節(jié),還可讓誤判率繼續(xù)變?yōu)橹暗?1/10,即平均單條記錄占用 1.8 字節(jié),誤判率可以達到 1/1000;平均單條記錄占用 2.4 字節(jié),誤判率可以到 1/10000,以此類推。這里的誤判率是指,BloomFilter 判斷某個 key 存在,但它實際不存在的概率,因為它存的是 key 的 Hash 值,而非 key 的值,所以有概率存在這樣的 key,它們內(nèi)容不同,但多次 Hash 后的 Hash 值都相同。對于 BloomFilter 判斷不存在的 key ,則是 100% 不存在的,反證法,如果這個 key 存在,那它每次 Hash 后對應的 Hash 值位置肯定是 1,而不會是 0。
緩存雪崩
問題描述

第三個經(jīng)典問題是緩存雪崩。系統(tǒng)運行過程中,緩存雪崩是一個非常嚴重的問題。緩存雪崩是指部分緩存節(jié)點不可用,導致整個緩存體系甚至甚至服務系統(tǒng)不可用的情況。緩存雪崩按照緩存是否 rehash(即是否漂移)分兩種情況:

緩存不支持 rehash 導致的系統(tǒng)雪崩不可用緩存支持 rehash 導致的緩存雪崩不可用

原因分析

在上述兩種情況中,緩存不進行 rehash 時產(chǎn)生的雪崩,一般是由于較多緩存節(jié)點不可用,請求穿透導致 DB 也過載不可用,最終整個系統(tǒng)雪崩不可用的。而緩存支持 rehash 時產(chǎn)生的雪崩,則大多跟流量洪峰有關,流量洪峰到達,引發(fā)部分緩存節(jié)點過載 Crash,然后因 rehash 擴散到其他緩存節(jié)點,最終整個緩存體系異常。

第一種情況比較容易理解,如下圖所示。緩存節(jié)點不支持 rehash,較多緩存節(jié)點不可用時,大量 Cache 訪問會失敗,根據(jù)緩存讀寫模型,這些請求會進一步訪問 DB,而且 DB 可承載的訪問量要遠比緩存小的多,請求量過大,就很容易造成 DB 過載,大量慢查詢,最終阻塞甚至 Crash,從而導致服務異常。

第二種情況是怎么回事呢?這是因為緩存分布設計時,很多同學會選擇一致性 Hash 分布方式,同時在部分節(jié)點異常時,采用 rehash 策略,即把異常節(jié)點請求平均分散到其他緩存節(jié)點。在一般情況下,一致性 Hash 分布+rehash 策略可以很好得運行,但在較大的流量洪峰到臨之時,如果大流量 key 比較集中,正好在某 1~2 個緩存節(jié)點,很容易將這些緩存節(jié)點的內(nèi)存、網(wǎng)卡過載,緩存節(jié)點異常 Crash,然后這些異常節(jié)點下線,這些大流量 key 請求又被 rehash 到其他緩存節(jié)點,進而導致其他緩存節(jié)點也被過載 Crash,緩存異常持續(xù)擴散,最終導致整個緩存體系異常,無法對外提供服務。
業(yè)務場景

緩存雪崩的業(yè)務場景并不少見,微博、Twitter 等系統(tǒng)在運行的最初若干年都遇到過很多次。比如,微博最初很多業(yè)務緩存采用一致性 Hash+rehash 策略,在突發(fā)洪水流量來臨時,部分緩存節(jié)點過載 Crash 甚至宕機,然后這些異常節(jié)點的請求轉到其他緩存節(jié)點,又導致其他緩存節(jié)點過載異常,最終整個緩存池過載。另外,機架斷電,導致業(yè)務緩存多個節(jié)點宕機,大量請求直接打到 DB,也導致 DB 過載而阻塞,整個系統(tǒng)異常。最后緩存機器復電后,DB 重啟,數(shù)據(jù)逐步加熱后,系統(tǒng)才逐步恢復正常。
解決方案

預防緩存雪崩,這里給出 3 個解決方案。

方案一,對業(yè)務 DB 的訪問增加讀寫開關,當發(fā)現(xiàn) DB 請求變慢、阻塞,慢請求超過閥值時,就會關閉讀開關,部分或所有讀 DB 的請求進行 failfast 立即返回,待 DB 恢復后再打開讀開關,如下圖。

img

方案二,對緩存增加多個副本,緩存異?;蛘埱?miss 后,再讀取其他緩存副本,而且多個緩存副本盡量部署在不同機架,從而確保在任何情況下,緩存系統(tǒng)都會正常對外提供服務。方案三,對緩存體系進行實時監(jiān)控,當請求訪問的慢速比超過閥值時,及時報警,通過機器替換、服務替換進行及時恢復;也可以通過各種自動故障轉移策略,自動關閉異常接口、停止邊緣服務、停止部分非核心功能措施,確保在極端場景下,核心功能的正常運行。

實際上,微博平臺系統(tǒng),這三種方案都采用了,通過三管齊下,規(guī)避緩存雪崩的發(fā)生。

第05講:緩存數(shù)據(jù)不一致和并發(fā)競爭怎么處理?

你好,我是你的緩存老師陳波,歡迎進入第5課時“緩存數(shù)據(jù)相關的經(jīng)典問題”。

數(shù)據(jù)不一致
問題描述

七大緩存經(jīng)典問題的第四個問題是數(shù)據(jù)不一致。同一份數(shù)據(jù),可能會同時存在 DB 和緩存之中。那就有可能發(fā)生,DB 和緩存的數(shù)據(jù)不一致。如果緩存有多個副本,多個緩存副本里的數(shù)據(jù)也可能會發(fā)生不一致現(xiàn)象。
原因分析

不一致的問題大多跟緩存更新異常有關。比如更新 DB 后,寫緩存失敗,從而導致緩存中存的是老數(shù)據(jù)。另外,如果系統(tǒng)采用一致性 Hash 分布,同時采用 rehash 自動漂移策略,在節(jié)點多次上下線之后,也會產(chǎn)生臟數(shù)據(jù)。緩存有多個副本時,更新某個副本失敗,也會導致這個副本的數(shù)據(jù)是老數(shù)據(jù)。
業(yè)務場景

導致數(shù)據(jù)不一致的場景也不少。如下圖所示,在緩存機器的帶寬被打滿,或者機房網(wǎng)絡出現(xiàn)波動時,緩存更新失敗,新數(shù)據(jù)沒有寫入緩存,就會導致緩存和 DB 的數(shù)據(jù)不一致。緩存 rehash 時,某個緩存機器反復異常,多次上下線,更新請求多次 rehash。這樣,一份數(shù)據(jù)存在多個節(jié)點,且每次 rehash 只更新某個節(jié)點,導致一些緩存節(jié)點產(chǎn)生臟數(shù)據(jù)。

img

解決方案

要盡量保證數(shù)據(jù)的一致性。這里也給出了 3 個方案,可以根據(jù)實際情況進行選擇。

第一個方案,cache 更新失敗后,可以進行重試,如果重試失敗,則將失敗的 key 寫入隊列機服務,待緩存訪問恢復后,將這些 key 從緩存刪除。這些 key 在再次被查詢時,重新從 DB 加載,從而保證數(shù)據(jù)的一致性。第二個方案,緩存時間適當調(diào)短,讓緩存數(shù)據(jù)及早過期后,然后從 DB 重新加載,確保數(shù)據(jù)的最終一致性。第三個方案,不采用 rehash 漂移策略,而采用緩存分層策略,盡量避免臟數(shù)據(jù)產(chǎn)生。

img

數(shù)據(jù)并發(fā)競爭
問題描述

第五個經(jīng)典問題是數(shù)據(jù)并發(fā)競爭?;ヂ?lián)網(wǎng)系統(tǒng),線上流量較大,緩存訪問中很容易出現(xiàn)數(shù)據(jù)并發(fā)競爭的現(xiàn)象。數(shù)據(jù)并發(fā)競爭,是指在高并發(fā)訪問場景,一旦緩存訪問沒有找到數(shù)據(jù),大量請求就會并發(fā)查詢 DB,導致 DB 壓力大增的現(xiàn)象。

img

數(shù)據(jù)并發(fā)競爭,主要是由于多個進程/線程中,有大量并發(fā)請求獲取相同的數(shù)據(jù),而這個數(shù)據(jù) key 因為正好過期、被剔除等各種原因在緩存中不存在,這些進程/線程之間沒有任何協(xié)調(diào),然后一起并發(fā)查詢 DB,請求那個相同的 key,最終導致 DB 壓力大增,如下圖。

業(yè)務場景

數(shù)據(jù)并發(fā)競爭在大流量系統(tǒng)也比較常見,比如車票系統(tǒng),如果某個火車車次緩存信息過期,但仍然有大量用戶在查詢該車次信息。又比如微博系統(tǒng)中,如果某條微博正好被緩存淘汰,但這條微博仍然有大量的轉發(fā)、評論、贊。上述情況都會造成該車次信息、該條微博存在并發(fā)競爭讀取的問題。
解決方案

要解決并發(fā)競爭,有 2 種方案。

方案一是使用全局鎖。如下圖所示,即當緩存請求 miss 后,先嘗試加全局鎖,只有加全局鎖成功的線程,才可以到 DB 去加載數(shù)據(jù)。其他進程/線程在讀取緩存數(shù)據(jù) miss 時,如果發(fā)現(xiàn)這個 key 有全局鎖,就進行等待,待之前的線程將數(shù)據(jù)從 DB 回種到緩存后,再從緩存獲取。

?
img

方案二是,對緩存數(shù)據(jù)保持多個備份,即便其中一個備份中的數(shù)據(jù)過期或被剔除了,還可以訪問其他備份,從而減少數(shù)據(jù)并發(fā)競爭的情況,如下圖。

img

第06講:Hot Key和Big Key引發(fā)的問題怎么應對?

你好,我是你的緩存老師陳波,歡迎進入第6課時“緩存特殊 key 相關的經(jīng)典問題”。

Hot key
問題描述

第六個經(jīng)典問題是 Hot key。對于大多數(shù)互聯(lián)網(wǎng)系統(tǒng),數(shù)據(jù)是分冷熱的。比如最近的新聞、新發(fā)表的微博被訪問的頻率最高,而比較久遠的之前的新聞、微博被訪問的頻率就會小很多。而在突發(fā)事件發(fā)生時,大量用戶同時去訪問這個突發(fā)熱點信息,訪問這個 Hot key,這個突發(fā)熱點信息所在的緩存節(jié)點就很容易出現(xiàn)過載和卡頓現(xiàn)象,甚至會被 Crash。
原因分析

Hot key 引發(fā)緩存系統(tǒng)異常,主要是因為突發(fā)熱門事件發(fā)生時,超大量的請求訪問熱點事件對應的 key,比如微博中數(shù)十萬、數(shù)百萬的用戶同時去吃一個新瓜。數(shù)十萬的訪問請求同一個 key,流量集中打在一個緩存節(jié)點機器,這個緩存機器很容易被打到物理網(wǎng)卡、帶寬、CPU 的極限,從而導致緩存訪問變慢、卡頓。
業(yè)務場景

引發(fā) Hot key 的業(yè)務場景很多,比如明星結婚、離婚、出軌這種特殊突發(fā)事件,比如奧運、春節(jié)這些重大活動或節(jié)日,還比如秒殺、雙12、618 等線上促銷活動,都很容易出現(xiàn) Hot key 的情況。
解決方案

要解決這種極熱 key 的問題,首先要找出這些 Hot key 來。對于重要節(jié)假日、線上促銷活動、集中推送這些提前已知的事情,可以提前評估出可能的熱 key 來。而對于突發(fā)事件,無法提前評估,可以通過 Spark,對應流任務進行實時分析,及時發(fā)現(xiàn)新發(fā)布的熱點 key。而對于之前已發(fā)出的事情,逐步發(fā)酵成為熱 key 的,則可以通過 Hadoop 對批處理任務離線計算,找出最近歷史數(shù)據(jù)中的高頻熱 key。

找到熱 key 后,就有很多解決辦法了。首先可以將這些熱 key 進行分散處理,比如一個熱 key 名字叫 hotkey,可以被分散為 hotkey#1、hotkey#2、hotkey#3,……h(huán)otkey#n,這 n 個 key 分散存在多個緩存節(jié)點,然后 client 端請求時,隨機訪問其中某個后綴的 hotkey,這樣就可以把熱 key 的請求打散,避免一個緩存節(jié)點過載,如下圖所示。

img

其次,也可以 key 的名字不變,對緩存提前進行多副本+多級結合的緩存架構設計。

再次,如果熱 key 較多,還可以通過監(jiān)控體系對緩存的 SLA 實時監(jiān)控,通過快速擴容來減少熱 key 的沖擊。

最后,業(yè)務端還可以使用本地緩存,將這些熱 key 記錄在本地緩存,來減少對遠程緩存的沖擊。
Big key
問題描述

最后一個經(jīng)典問題是 Big key,也就是大 Key 的問題。大 key,是指在緩存訪問時,部分 Key 的 Value 過大,讀寫、加載易超時的現(xiàn)象。
原因分析

造成這些大 key 慢查詢的原因很多。如果這些大 key 占總體數(shù)據(jù)的比例很小,存 Mc,對應的 slab 較少,導致很容易被頻繁剔除,DB 反復加載,從而導致查詢較慢。如果業(yè)務中這種大 key 很多,而這種 key 被大量訪問,緩存組件的網(wǎng)卡、帶寬很容易被打滿,也會導致較多的大 key 慢查詢。另外,如果大 key 緩存的字段較多,每個字段的變更都會引發(fā)對這個緩存數(shù)據(jù)的變更,同時這些 key 也會被頻繁地讀取,讀寫相互影響,也會導致慢查現(xiàn)象。最后,大 key 一旦被緩存淘汰,DB 加載可能需要花費很多時間,這也會導致大 key 查詢慢的問題。
業(yè)務場景

大 key 的業(yè)務場景也比較常見。比如互聯(lián)網(wǎng)系統(tǒng)中需要保存用戶最新 1萬 個粉絲的業(yè)務,比如一個用戶個人信息緩存,包括基本資料、關系圖譜計數(shù)、發(fā) feed 統(tǒng)計等。微博的 feed 內(nèi)容緩存也很容易出現(xiàn),一般用戶微博在 140 字以內(nèi),但很多用戶也會發(fā)表 1千 字甚至更長的微博內(nèi)容,這些長微博也就成了大 key,如下圖。

img

解決方案

對于大 key,給出 3 種解決方案。

第一種方案,如果數(shù)據(jù)存在 Mc 中,可以設計一個緩存閥值,當 value 的長度超過閥值,則對內(nèi)容啟用壓縮,讓 KV 盡量保持小的 size,其次評估大 key 所占的比例,在 Mc 啟動之初,就立即預寫足夠數(shù)據(jù)的大 key,讓 Mc 預先分配足夠多的 trunk size 較大的 slab。確保后面系統(tǒng)運行時,大 key 有足夠的空間來進行緩存。       

img

第二種方案,如果數(shù)據(jù)存在 Redis 中,比如業(yè)務數(shù)據(jù)存 set 格式,大 key 對應的 set 結構有幾千幾萬個元素,這種寫入 Redis 時會消耗很長的時間,導致 Redis 卡頓。此時,可以擴展新的數(shù)據(jù)結構,同時讓 client 在這些大 key 寫緩存之前,進行序列化構建,然后通過 restore 一次性寫入,如下圖所示。

img

第三種方案時,如下圖所示,將大 key 分拆為多個 key,盡量減少大 key 的存在。同時由于大 key 一旦穿透到 DB,加載耗時很大,所以可以對這些大 key 進行特殊照顧,比如設置較長的過期時間,比如緩存內(nèi)部在淘汰 key 時,同等條件下,盡量不淘汰這些大 key。

img

至此,本課時緩存的 7 大經(jīng)典問題全部講完。

我們要認識到,對于互聯(lián)網(wǎng)系統(tǒng),由于實際業(yè)務場景復雜,數(shù)據(jù)量、訪問量巨大,需要提前規(guī)避緩存使用中的各種坑。你可以通過提前熟悉 Cache 的經(jīng)典問題,提前構建防御措施, 避免大量 key 同時失效,避免不存在 key 訪問的穿透,減少大 key、熱 key 的緩存失效,對熱 key 進行分流。你可以采取一系列措施,讓訪問盡量命中緩存,同時保持數(shù)據(jù)的一致性。另外,你還可以結合業(yè)務模型,提前規(guī)劃 cache 系統(tǒng)的 SLA,如 QPS、響應分布、平均耗時等,實施監(jiān)控,以方便運維及時應對。在遇到部分節(jié)點異常,或者遇到突發(fā)流量、極端事件時,也能通過分池分層策略、key 分拆等策略,避免故障發(fā)生。

最終,你能在各種復雜場景下,面對高并發(fā)、海量訪問,面對突發(fā)事件和洪峰流量,面對各種網(wǎng)絡或機器硬件故障,都能保持服務的高性能和高可用。

第三章:Memcached的原理及架構剖析

第07講:MC為何是應用最廣泛的緩存組件?

你好,我是你的緩存老師陳波,歡迎你進入第 7 課時“Memcached 原理及特性”的學習。

眾所周知,用戶體驗可以說是互聯(lián)網(wǎng)企業(yè)最看重的指標,而在用戶體驗中,請求響應速度首當其沖。因此互聯(lián)網(wǎng)系統(tǒng)對性能的追求是永無止境的。性能爭霸,緩存為王,Memcached,作為互聯(lián)網(wǎng)系統(tǒng)使用最廣泛、影響最大的標配緩存組件,可以說的上是王中之王了。

本課時將講解 Memcached 的原理及特性,系統(tǒng)架構,還會重點講解 Memcached 的網(wǎng)絡模型、狀態(tài)機,最后還會涉及到 Memcached 命令處理的整個流程。
Memcached 原理及特性

首先來看 Memcached 的原理及特性。
原理

Memcached 是一個開源的、高性能的分布式 key/value 內(nèi)存緩存系統(tǒng)。它以 key/value 鍵值對的方式存儲數(shù)據(jù),是一個鍵值類型的 NoSQL 組件。

NoSQL 即 Not SQL,泛指非關系型數(shù)據(jù)存儲。NoSQL 是通過聚合模型來進行數(shù)據(jù)處理的。其聚合模型主要分為:key/value 鍵值對、列族、圖形等幾種方式。其中 key/value 鍵值類似我們平常使用的 map,只能通過 key 來進行查找和變更操作。我們使用的 Memcached、Redis 等都是 key/value 類型的 NoSQL 存儲組件。

Memcached 簡稱 Mc,是一個典型的內(nèi)存型緩存組件,這就意味著,Mc 一旦重啟就會丟失所有的數(shù)據(jù)。如下圖所示,Mc 組件之間相互不通信,完全由 client 對 key 進行 Hash 后分布和協(xié)同。Mc 采用多線程處理請求,由一個主線程和任意多個工作線程協(xié)作,從而充分利用多核,提升 IO 效率。

img

slab 機制

接下來介紹 Mc 的 slab 機制。

Mc 并不是將所有數(shù)據(jù)放在一起來進行管理的,而是將內(nèi)存劃分為一系列相同大小的 slab 空間后,每個 slab 只管理一定范圍內(nèi)的數(shù)據(jù)存儲。也就是說 Mc 內(nèi)部采用 slab 機制來管理內(nèi)存分配。Mc 內(nèi)的內(nèi)存分配以 slab 為單位,默認情況下一個 slab 是 1MB,可以通過 -I 參數(shù)在啟動時指定其他數(shù)值。

slab 空間內(nèi)部,會被進一步劃分為一系列固定大小的 chunk。每個 chunk 內(nèi)部存儲一個 Item,利用 Item 結構存儲數(shù)據(jù)。因為 chunk 大小固定,而 key/value 數(shù)據(jù)的大小隨機。所以,Item存儲完 key/value 數(shù)據(jù)后,一般還會有多余的空間,這個多余的空間就被浪費了。為了提升內(nèi)存的使用效率,chunk size 就不能太大,而要盡量選擇與 key/value size 接近的 ,從而減少 chunk 內(nèi)浪費的空間。

Mc 在分配內(nèi)存時,先將內(nèi)存按固定大小劃分成 slab,然后再將不同 slab 分拆出固定 size 的 chunk。雖然 slab 內(nèi)的 chunk 大小相同,但不同 slab 的 chunk size 并不同,Mc 會按照一個固定比例,使劃分的 chunk size 逐步增大,從而滿足不同大小 key/value 存儲的需要。

如下圖,一組具有相同 chunk size 的所有 slab,就組成一個 slabclass。不同 slabclass 的 chunk size 按遞增因子一次增加。Mc 就通過 slabclass 來管理一組 slab 內(nèi)的存儲空間的。每個 slabclass 內(nèi)部有一個 freelist ,包含這組 slab 里所有空閑的 chunk,當需要存儲數(shù)據(jù)時,從這個 freelist 里面快速分配一個 chunk 做存儲空間。當 Item 數(shù)據(jù)淘汰剔除時,這個 Item 所在的 chunk 又被回收至這個 freelist。

img

Mc 在通過 slab 機制管理內(nèi)存分配時,實際 key/value 是存在 Item 結構中,所以對 key/value 的存儲空間分配就轉換為對 Item 的分配。而 Item 空間的分配有 2 種方式,如果 Mc 有空閑空間,則從 slabclass 的 freelist 分配;如果沒有空閑空間,則從對應 slabclass id 對應的 LRU 中剔除一個 Item,來復用這個 Item 的空間。

在查找或變更一個 key 時,首先要定位這個 key 所在的存儲位置。Mc 是通過哈希表 Hashtable 來定位 key 的。Hashtable 可以看作是一個內(nèi)存空間連續(xù)的大數(shù)組,而這個大數(shù)據(jù)的每一個槽位對應一個 key 的 Hash 值,這個槽位也稱 bucket。由于不同 key 的 Hash 值可能相同,所以 Mc 在 Hashtable 的每個捅內(nèi)部再用一個單向鏈表,來解決 Hash 沖突的問題。

Mc 內(nèi)部是通過 LRU 來管理存儲 Item 數(shù)據(jù)的,當內(nèi)存不足時,會從 LRU 隊尾中剔除一個過期或最不活躍的 key,供新的 Item 使用。
特性

講完 slab 機制我們來學習 Mc 的特性。

Mc 最大的特性是高性能,單節(jié)點壓測性能能達到百萬級的 QPS。其次因為 Mc 的訪問協(xié)議很簡單,只有 get/set/cas/touch/gat/stats 等有限的幾個命令。Mc 的訪問協(xié)議簡單,跟它的存儲結構也有關系。Mc 存儲結構很簡單,只存儲簡單的 key/value 鍵值對,而且對 value 直接以二進制方式存儲,不識別內(nèi)部存儲結構,所以有限幾個指令就可以滿足操作需要。Mc 完全基于內(nèi)存操作,在系統(tǒng)運行期間,在有新 key 寫進來時,如果沒有空閑內(nèi)存分配,就會對最不活躍的 key 進行 eviction 剔除操作。最后,Mc 服務節(jié)點運行也特別簡單,不同 Mc 節(jié)點之間互不通信,由 client 自行負責管理數(shù)據(jù)分布。

第08講:MC系統(tǒng)架構是如何布局的?

你好,我是你的緩存老師陳波,歡迎你進入第 8 課時“Memcached 系統(tǒng)架構”的學習。
系統(tǒng)架構

我們來看一下 Mc 的系統(tǒng)架構。

如下圖所示,Mc 的系統(tǒng)架構主要包括網(wǎng)絡處理模塊、多線程處理模塊、哈希表、LRU、slab 內(nèi)存分配模塊 5 部分。Mc 基于 Libevent 實現(xiàn)了網(wǎng)絡處理模塊,通過多線程并發(fā)處理用戶請求;基于哈希表對 key 進行快速定位,基于 LRU 來管理冷數(shù)據(jù)的剔除淘汰,基于 slab 機制進行快速的內(nèi)存分配及存儲。

img

系統(tǒng)架構

Mc 基于 Libevent 開發(fā)實現(xiàn)了多線程網(wǎng)絡模型。Mc 的多線程網(wǎng)絡模型分為主線程、工作線程。這些線程通過多路復用 IO 來進行網(wǎng)絡 IO 接入以及讀寫處理。在 Linux 下,通常使用 epoll。通過多路復用 IO,特別是 epoll 的使用,Mc 線程無須遍歷整個被偵聽的描述符集,只要在被通知后遍歷 Ready 隊列的描述符集合就 OK 了。這些描述符是在各項準備工作完成之后,才被內(nèi)核 IO 事件異步通知。也就是說,只在連接做好準備后,系統(tǒng)才會進行事件通知,Mc 才會進行 I/O 操作。這樣就不會發(fā)生阻塞,使 Mc 在支持高并發(fā)的同時,擁有非常高的 IO 吞吐效率。

Mc 除了用于 IO 的主線程和工作線程外,還用于多個輔助線程,如 Item 爬蟲線程、LRU 維護線程、哈希表維護線程等,通過多線程并發(fā)工作,Mc 可以充分利用機器的多個核心,實現(xiàn)很好的網(wǎng)絡 IO 性能和數(shù)據(jù)處理能力。

Mc 通過哈希表即 Hashtable 來快速定位 key。數(shù)據(jù)存儲時,數(shù)據(jù) Item 結構在存入 slab 中的 chunk 后,也會被存放到 Hashtable 中。同時,Mc 的哈希表會在每個桶,通過 Item 記錄一個單向鏈表,以此來解決不同 key 在哈希表中的 Hash 沖突問題。 當需要查找給定 key 的 Item 時,首先計算 key 的 Hash 值,然后對哈希表中與 Hash 值對應的 bucket 中進行搜索,通過輪詢 bucket 里的單向鏈表,找到該 key 對應的 Item 指針,這樣就找到了 key 對應的存儲 Item,如下圖所示。

img

正常情況下,Mc 對哈希表的插入、查找操作都是在主表中進行的。當表中 Item 數(shù)量大于哈希表 bucket 節(jié)點數(shù)的 1.5 倍時,就對哈希表進行擴容。如下圖所示,擴容時,Mc 內(nèi)部使用兩張 Hashtable,一個主哈希表 primary_hashtable,一個是舊哈希表 old_hashtable。當擴容開始時,原來的主哈希表就成為舊哈希表,而新分配一個 2 倍容量的哈希表作為新的主表。擴容過程中,維護線程會將舊表的 Item 指針,逐步復制插入到新主哈希表。遷移過程中,根據(jù)遷移位置,用戶請求會同時查舊表和新的主表,當數(shù)據(jù)全部遷移完成,所有的操作就重新回到主表中進行。

img

LRU 機制

Mc 主要通過 LRU 機制,來進行冷數(shù)據(jù)淘汰的。自 1.4.24 版本之后,Mc 不斷優(yōu)化 LRU 算法,當前 Mc 版本已默認啟用分段 LRU 了。在啟用分段 LRU 之前,每個 slabclass id 只對應一個 COLD LRU,在內(nèi)存不足時,會直接從 COLD LRU 剔除數(shù)據(jù)。而在啟用分段 LRU 之后,每個 slabclass id 就有 TEMP、HOT、WARM 和 COLD 四個 LRU。

如下圖所示,TEMP LRU 中 Item 剩余過期時間通常很短,默認是 61 秒以內(nèi)。該列隊中的 Item 永遠不會發(fā)生在隊列內(nèi)搬運,也不會遷移到其他隊列。在插入新 key/value 時,如果 key 的剩余過期時間小于 61 秒,則直接進入 TEMP LRU。后面,在必要時直接進行過期即可。這樣避免了鎖競爭,性能也更高。

img

對于 HOT LRU,內(nèi)部不搬運,當隊列滿時,如果隊尾 Item 是 Active 狀態(tài),即被訪問過,那么會遷移到 WARM 隊列,否則遷移到 COLD 隊列。

對于 WARM LRU,如果隊列的 Item 被再次訪問,就搬到隊首,否則遷移到 COLD 隊列。

對于 COLD LRU,存放的是最不活躍的 Item,一旦內(nèi)存滿了,隊尾的 Item 會被剔除。如果 COLD LRU 里的 Item 被再次訪問,會遷移到 WARM LRU。
slab 分配機制

一般應用系統(tǒng)的內(nèi)存分配是直接采用 malloc 和 free 來進行分配及回收的。長時間運行后,內(nèi)存碎片越來越多,嚴重增加系統(tǒng)內(nèi)存管理器的負擔。碎片的不斷產(chǎn)生,不僅導致大量的內(nèi)存浪費,而且碎片整理越來越復雜,會導致內(nèi)存分配越來越慢,進而導致系統(tǒng)分配速度和存儲效率越來越差。Mc 的 slab 分配機制的出現(xiàn),碎片問題迎刃而解。下面我們來先簡單了解一下 Mc 的 slab 分配機制。

Mc 通過 slab 機制來分配管理內(nèi)存的,如下圖所示??梢哉f,slab 分配機制的使用,是 Mc 分配及存儲高性能的關鍵所在。在 Mc 啟動時,會創(chuàng)建 64 個 slabclass,但索引為 0 的 slabclass 做 slab 重新分配之用,基本不參與其他 slabclass 的日常分配活動。每個 slabclass 會根據(jù)需要不斷分配默認大小為 1MB 的 slab。

每個 slab 又被分為相同大小的 chunk。chunk 就是 Mc 存儲數(shù)據(jù)的基本存儲單位。slabclass 1 的 chunk size 最小,默認最小 chunk 的大小是 102 字節(jié),后續(xù)的 slabclass 會按照增長因子逐步增大 chunk size,具體數(shù)值會進一步對 8 取整。Mc 默認的增長因子是 1.25,啟動時可以通過 -f 將增長因子設為其他值。比如采用默認值,slabclass 1 的 chunk size 是 102,slabclass 2 的 chunk size 是 102×1.25,再對 8 取整后是 128。

img

Mc slab 中的 chunk 中通過 Item 結構存 key/value 鍵值對,Item 結構體的頭部存鏈表的指針、flag、過期時間等,然后存 key 及 value。一般情況下,Item 并不會將 chunk 填滿,但由于每個 key/value 在存儲時,都會根據(jù) kev/value size,選擇最接近的 slabclass,所以 chunk 浪費的字節(jié)非常有限,基本可以忽略。

每次新分配一個 slab 后,會將 slab 空間等分成相同 size 的 chunk,這些 chunk 會被加入到 slabclass 的 freelist 中,在需要時進行分配。分配出去的 chunk 存儲 Item 數(shù)據(jù),在過期被剔除后,會再次進入 freelist,供后續(xù)使用。

第09講:MC是如何使用多線程和狀態(tài)機來處理請求命令的?

你好,我是你的緩存老師陳波,歡迎你進入第 9 課時“Memcached 網(wǎng)絡模型及狀態(tài)機”的學習。

網(wǎng)絡模型

了解了 Mc 的系統(tǒng)架構之后,我們接下來可以逐一深入學習 Mc 的各個模塊了。首先,我們來學習 Mc 的網(wǎng)絡模型。
主線程

Mc 基于 Libevent 實現(xiàn)多線程網(wǎng)絡 IO 模型。Mc 的 IO 處理線程分主線程和工作線程,每個線程各有一個 event_base,來監(jiān)聽網(wǎng)絡事件。主線程負責監(jiān)聽及建立連接。工作線程負責對建立的連接進行網(wǎng)絡 IO 讀取、命令解析、處理及響應。

Mc 主線程在監(jiān)聽端口時,當有連接到來,主線程 accept 該連接,并將連接調(diào)度給工作線程。調(diào)度處理邏輯,主線程先將 fd 封裝成一個 CQ_ITEM 結構,并存入新連接隊列中,然后輪詢一個工作線程,并通過管道向該工作線程發(fā)送通知。工作線程監(jiān)聽到通知后,會從新連接隊列獲取一個連接,然后開始從這個連接讀取網(wǎng)絡 IO 并處理,如下圖所示。主線程的這個處理邏輯主要在狀態(tài)機中執(zhí)行,對應的連接狀態(tài)為 conn_listening。

img

工作線程

工作線程監(jiān)聽到主線程的管道通知后,會從連接隊列彈出一個新連接,然后就會創(chuàng)建一個 conn 結構體,注冊該 conn 讀事件,然后繼續(xù)監(jiān)聽該連接上的 IO 事件。后續(xù)這個連接有命令進來時,工作線程會讀取 client 發(fā)來的命令,進行解析并處理,最后返回響應。工作線程的主要處理邏輯也是在狀態(tài)機中,一個名叫 drive_machine 的函數(shù)。
狀態(tài)機

img

這個狀態(tài)機由主線程和工作線程共享,實際是采用 switch-case 來實現(xiàn)的。狀態(tài)機函數(shù)如下圖所示,switch 連接的 state,然后根據(jù)連接的不同狀態(tài),執(zhí)行不同的邏輯操作,并進行狀態(tài)轉換。接下來我們開始分析 Mc 的狀態(tài)機。

主線程狀態(tài)機

如下圖所示,主線程在狀態(tài)機中只處理 conn_listening 狀態(tài),負責 accept 新連接和調(diào)度新連接給工作線程。狀態(tài)機中其他狀態(tài)處理基本都在工作線程中進行。由于 Mc 同時支持 TCP、UDP 協(xié)議,而互聯(lián)網(wǎng)企業(yè)大多使用 TCP 協(xié)議,并且通過文本協(xié)議,來訪問 Mc,所以后面狀態(tài)機的介紹,將主要結合 TCP 文本協(xié)議來進行重點分析。

img

工作線程狀態(tài)機

工作線程的狀態(tài)機處理邏輯,如下圖所示,包括剛建立 conn 連接結構體時進行的一些重置操作,然后注冊讀事件,在有數(shù)據(jù)進來時,讀取網(wǎng)絡數(shù)據(jù),并進行解析并處理。如果是讀取指令或統(tǒng)計指令,至此就基本處理完畢,接下來將響應寫入連接緩沖。如果是更新指令,在進行初步處理后,還會繼續(xù)讀取 value 部分,再進行存儲或變更,待變更完畢后將響應寫入連接緩沖。最后再將響應寫給 client。響應 client 后,連接會再次重置連接狀態(tài),等待進入下一次的命令處理循環(huán)中。這個過程主要包含了 conn_new_cmd、conn_waiting、conn_read、conn_parse_cmd、conn_nread、conn_write、conn_mwrite、conn_closing 這 8 個狀態(tài)事件。

img

工作線程狀態(tài)事件及邏輯處理
conn_new_cmd

主線程通過調(diào)用 dispatch_conn_new,把新連接調(diào)度給工作線程后,worker 線程創(chuàng)建 conn 對象,這個連接初始狀態(tài)就是 conn_new_cmd。除了通過新建連接進入 conn_new_cmd 狀態(tài)之外,如果連接命令處理完畢,準備接受新指令時,也會將連接的狀態(tài)設置為 conn_new_cmd 狀態(tài)。

進入 conn_new_cmd 后,工作線程會調(diào)用 reset_cmd_handler 函數(shù),重置 conn 的 cmd 和 substate 字段,并在必要時對連接 buf 進行收縮。因為連接在處理 client 來的命令時,對于寫指令,需要分配較大的讀 buf 來存待更新的 key value,而對于讀指令,則需要分配較大的寫 buf 來緩沖待發(fā)送給 client 的 value 結果。持續(xù)運行中,隨著大 size value 的相關操作,這些緩沖會占用很多內(nèi)存,所以需要設置一個閥值,超過閥值后就進行緩沖內(nèi)存收縮,避免連接占用太多內(nèi)存。在后端服務以及中間件開發(fā)中,這個操作很重要,因為線上服務的連接很容易達到萬級別,如果一個連接占用幾十 KB 以上的內(nèi)存,后端系統(tǒng)僅連接就會占用數(shù)百 MB 甚至數(shù) GB 以上的內(nèi)存空間。
conn_parse_cmd

工作線程處理完 conn_new_cmd 狀態(tài)的主要邏輯后,如果讀緩沖區(qū)有數(shù)據(jù)可以讀取,則進入 conn_parse_cmd 狀態(tài),否則就會進入到 conn_waiting 狀態(tài),等待網(wǎng)絡數(shù)據(jù)進來。
conn_waiting

連接進入 conn_waiting 狀態(tài)后,處理邏輯很簡單,直接通過 update_event 函數(shù)注冊讀事件即可,之后會將連接狀態(tài)更新為 conn_read。
conn_read

當工作線程監(jiān)聽到網(wǎng)絡數(shù)據(jù)進來,連接就進入 conn_read 狀態(tài)。對 conn_read 的處理,是通過 try_read_network 從 socket 中讀取網(wǎng)絡數(shù)據(jù)。如果讀取失敗,則進入 conn_closing 狀態(tài),關閉連接。如果沒有讀取到任何數(shù)據(jù),則會返回 conn_waiting,繼續(xù)等待 client 端的數(shù)據(jù)到來。如果讀取數(shù)據(jù)成功,則會將讀取的數(shù)據(jù)存入 conn 的 rbuf 緩沖,并進入 conn_parse_cmd 狀態(tài),準備解析 cmd。
conn_parse_cmd

conn_parse_cmd 狀態(tài)的處理邏輯就是解析命令。工作線程首先通過 try_read_command 讀取連接的讀緩沖,并通過 \n 來分隔數(shù)據(jù)報文的命令。如果命令首行長度大于 1024,關閉連接,這就意味著 key 長度加上其他各項命令字段的總長度要小于 1024字節(jié)。當然對于 key,Mc 有個默認的最大長度,key_max_length,默認設置為 250字節(jié)。校驗完畢首行報文的長度,接下來會在 process_command 函數(shù)中對首行指令進行處理。

process_command 用來處理 Mc 的所有協(xié)議指令,所以這個函數(shù)非常重要。process_command 會首先按照空格分拆報文,確定命令協(xié)議類型,分派給 process_XX_command 函數(shù)處理。

Mc 的命令協(xié)議從直觀邏輯上可以分為獲取類型、變更類型、其他類型。但從實際處理層面區(qū)分,則可以細分為 get 類型、update 類型、delete 類型、算術類型、touch 類型、stats 類型,以及其他類型。對應的處理函數(shù)為,process_get_command, process_update_command, process_arithmetic_command, process_touch_command等。每個處理函數(shù)能夠處理不同的協(xié)議,具體參見下圖所示思維導圖。

img

conn_parse_cmd

注意 conn_parse_cmd 的狀態(tài)處理,只有讀取到 \n,有了完整的命令首行協(xié)議,才會進入 process_command,否則會跳轉到 conn_waiting,繼續(xù)等待客戶端的命令數(shù)據(jù)報文。在 process_command 處理中,如果是獲取類命令,在獲取到 key 對應的 value 后,則跳轉到 conn_mwrite,準備寫響應給連接緩沖。而對于 update 變更類型的指令,則需要繼續(xù)讀取 value 數(shù)據(jù),此時連接會跳轉到 conn_nread 狀態(tài)。在 conn_parse_cmd 處理過程中,如果遇到任何失敗,都會跳轉到 conn_closing 關閉連接。
complete_nread

對于 update 類型的協(xié)議指令,從 conn 繼續(xù)讀取 value 數(shù)據(jù)。讀取到 value 數(shù)據(jù)后,會調(diào)用 complete_nread,進行數(shù)據(jù)存儲處理;數(shù)據(jù)處理完畢后,向 conn 的 wbuf 寫響應結果。然后 update 類型處理的連接進入到 conn_write 狀態(tài)。
conn_write

連接 conn_write 狀態(tài)處理邏輯很簡單,直接進入 conn_mwrite 狀態(tài)?;蛘弋?conn 的 iovused 為 0 或對于 udp 協(xié)議,將響應寫入 conn 消息緩沖后,再進入 conn_mwrite 狀態(tài)。
conn_mwrite

進入 conn_mwrite 狀態(tài)后,工作線程將通過 transmit 來向客戶端寫數(shù)據(jù)。如果寫數(shù)據(jù)失敗,跳轉到 conn_closing,關閉連接退出狀態(tài)機。如果寫數(shù)據(jù)成功,則跳轉到 conn_new_cmd,準備下一次新指令的獲取。
conn_closing

最后一個 conn_closing 狀態(tài),前面提到過很多次,在任何狀態(tài)的處理過程中,如果出現(xiàn)異常,就會進入到這個狀態(tài),關閉連接,這個連接也就 Game Over了。
Mc 命令處理全流程

img

至此,Mc 的系統(tǒng)架構和狀態(tài)機的內(nèi)容就全部講完了,再梳理一遍 Mc 對命令的處理全過程,如下圖所示,從而加深對 Mc 的狀態(tài)機及命令處理流程的理解。

Mc 啟動后,主線程監(jiān)聽并準備接受新連接接入。當有新連接接入時,主線程進入 conn_listening 狀態(tài),accept 新連接,并將新連接調(diào)度給工作線程。Worker 線程監(jiān)聽管道,當收到主線程通過管道發(fā)送的消息后,工作線程中的連接進入 conn_new_cmd 狀態(tài),創(chuàng)建 conn 結構體,并做一些初始化重置操作,然后進入 conn_waiting 狀態(tài),注冊讀事件,并等待網(wǎng)絡 IO。有數(shù)據(jù)到來時,連接進入 conn_read 狀態(tài),讀取網(wǎng)絡數(shù)據(jù)。讀取成功后,就進入 conn_parse_cmd 狀態(tài),然后根據(jù) Mc 協(xié)議解析指令。對于讀取指令,獲取到 value 結果后,進入 conn_mwrite 狀態(tài)。對于變更指令,則進入 conn_nread,進行 value 的讀取,讀取到 value 后,對 key 進行變更,當變更完畢后,進入 conn_write,然后將結果寫入緩沖。然后和讀取指令一樣,也進入 conn_mwrite 狀態(tài)。進入到 conn_mwrite 狀態(tài)后,將結果響應發(fā)送給 client。發(fā)送響應完畢后,再次進入到 conn_new_cmd 狀態(tài),進行連接重置,準備下一次命令處理循環(huán)。在讀取、解析、處理、響應過程,遇到任何異常就進入 conn_closing,關閉連接。

總結下最近 3 個課時的內(nèi)容。首先講解了 Memcached 的原理及特性。然后結合 Memcached 的系統(tǒng)架構,學習了 Mc 基于 Libevent 的多線程網(wǎng)絡模型,知道了 Mc 的 IO 主線程負責接受連接及調(diào)度,工作線程負責讀取指令、處理并響應。本課時還有一個重點是 Memcached 狀態(tài)機,知道了主線程處理 conn_listening,工作線程處理其他 8 種重要狀態(tài)。每種狀態(tài)下對應不同的處理邏輯,從而將 Mc 整個冗長復雜的處理過程進行分階段的處理,每個階段只關注有限的邏輯,從而確保整個處理過程的清晰、簡潔。

最后通過梳理 Mc 命令處理的全過程,學習了 Mc 如何建立連接,如何進行命令讀取、處理及響應,從而把 Mc 的系統(tǒng)架構、多線程網(wǎng)絡模型、狀態(tài)機處理進行邏輯打通。

為了方便理解,提供本課時所有知識點的思維導圖,如下圖所示。

img

OK,這節(jié)課就講到這里,下一課時我會分享“Memcached 哈希表”,記得按時來聽課哈。好,下節(jié)課見,拜拜!

第四章:Memcached進階

第10講:MC是怎么定位key的?

你好,我是你的緩存課老師陳波,歡迎你進入第 10 課時“Memcached 哈希表”的學習。

我們在進行 Mc 架構剖析時,除了學習 Mc 的系統(tǒng)架構、網(wǎng)絡模型、狀態(tài)機外,還對 Mc 的 slab 分配、Hashtable、LRU 有了簡單的了解。本節(jié)課,將進一步深入學習這些知識點。

接下來,進入 Memcached 進階的學習。會講解 Mc 是如何進行 key 定位,如何淘汰回收過期失效 key 的,還將分析 Mc 的內(nèi)存管理 slab 機制,以及 Mc 進行數(shù)據(jù)存儲維護的關鍵機理,最后還會對 Mc 進行完整的協(xié)議分析,并以 Java 語言為例,介紹 Mc 常用的 client,以及如何進行調(diào)優(yōu)及改進。
key 定位
哈希表

Mc 將數(shù)據(jù)存儲在 Item 中,然后這些 Item 會被 slabclass 的 4 個 LRU 管理。這些 LRU 都是通過雙向鏈表實現(xiàn)數(shù)據(jù)記錄的。雙向鏈表在進行增加、刪除、修改位置時都非常高效,但其獲取定位 key 的性能非常低下,只能通過鏈表遍歷來實現(xiàn)。因此,Mc 還通過 Hashtable,也就是哈希表,來記錄管理這些 Item,通過對 key 進行哈希計算,從而快速定位和讀取這些 key/value 所在的 Item,如下圖所示。

img

哈希表也稱散列表,可以通過把 key 映射到哈希表中的一個位置來快速訪問記錄,定位 key 的時間復雜度只有 O(1)。Mc 的哈希表實際是一個一維指針數(shù)組,數(shù)組的每個位置稱作一個 bucket,即一個桶。性能考慮的需要,Mc 的哈希表的長度設置為 2 的 N 次方。Mc 啟動時,默認會構建一個擁有 6.4萬 個桶的哈希表,隨著新 key 的不斷插入,哈希表中的元素超過閥值后,會對哈希表進行擴容,最大可以構建 2 的 32 次方個桶的哈希表,也就是說 Mc 哈希表經(jīng)過多次擴容后,最多只能有不超過 43億 個桶。
哈希表設計

對于哈希表設計,有 2 個關鍵點,一個是哈希算法,一個是哈希沖突解決方案。Mc 使用的哈希算法有 2 種,分別是 Murmur3 Hash 和 Jenkins Hash。Mc 當前版本,默認使用 Murmur3 Hash 算法。不同的 key 通過 Hash 計算,被定位到了相同的桶,這就是哈希沖突。Mc 是通過對每個桶啟用一個單向鏈表,來解決哈希沖突問題的。
定位 key

Memcached 定位 key 時,首先根據(jù) key 采用 Murmur3 或者 Jenkins 算法進行哈希計算,得到一個 32 位的無符號整型輸出,存儲到變量 hv 中。因為哈希表一般沒有 2^32 那么大,所以需要將 key 的哈希值映射到哈希表的范圍內(nèi)。Mc 采用最簡單的取模算法作為映射函數(shù),即采用 hv%hashsize 進行計算。由于普通的取模運算比較耗時,所以 Mc 將哈希表的長度設置為 2 的 n 次方,采用位運算進行優(yōu)化,即采用 hv&hashmask 來計算。hashmask 即 2 的 n 次方 減 1。

定位到 key 所在的桶的位置后,如果是插入一個新數(shù)據(jù),則將數(shù)據(jù) Item 采用頭部插入法插入桶的單向鏈表中。如果是查找,則輪詢對應哈希桶中的那個單向鏈表,依次比對 key 字符串,key 相同則找到數(shù)據(jù) Item。

img

如果哈希表桶中元素太多,這個鏈表輪詢耗時會比較長,所以在哈希表中元素達到桶數(shù)的 1.5 倍之后,Mc 會對哈希表進行 2 倍擴容。由于哈希表最多只有 43 億左右個桶,所以性能考慮,單個 Mc 節(jié)點最多存儲 65億 個 key/value。如果要存更多 key,則需要修改 Mc 源碼,將最大哈希,即 HASHPOWER_MAX, 進行調(diào)大設置。

哈希表擴容

當 Mc 的哈希表中,Item 數(shù)量大于 1.5 倍的哈希桶數(shù)量后,Mc 就對哈希表進行擴容處理。如下圖所示,Mc 的哈希擴容是通過哈希維護線程進行處理的。準備開始擴容時,哈希維護線程會首先將所有 IO 工作線程和輔助線程進行暫停,其中輔助線程包括 LRU 維護線程、slab 維護線程、LRU 爬蟲線程。待這些線程暫停后,哈希維護線程會將當前的主哈希表設為舊哈希表,然后將新的主哈希表擴容之前的 2 倍容量。然后,工作線程及輔助線程繼續(xù)工作,同時哈希維護線程開始逐步將 Item 元素從舊哈希表遷移到主哈希表。

img

Mc 在啟動時,會根據(jù)設置的工作線程數(shù),來構建 一個 Item 鎖哈希表,線程越多,構建的鎖哈希表越大,對于 4 個線程,鎖哈希表有 4096 個桶,對于 10 個線程,鎖哈希表會有 8192 個桶,Item 鎖哈希表最多有 32k 個桶,1k 是 1024,即最多即 32768 個桶。Mc 的鎖哈希表中,每個桶對應一個 Item 鎖,所以 Mc 最多只有 32768 個 Item 鎖。

Mc 哈希表在讀取、變更以及擴容遷移過程中,先將 key hash 定位到 Item 鎖哈希表的鎖桶,然后對 Item 鎖進行加鎖,然后再進行實際操作。實際上,除了在哈希表,在其他任何時候,只要涉及到在對 Item 的操作,都會根據(jù) Item 中的 key,進行 Item 哈希鎖桶加鎖,以避免 Item 被同時讀寫而產(chǎn)生臟數(shù)據(jù)。Mc 默認有 4096 個鎖桶,所以對 key 加鎖時,沖突的概率較小,而且 Mc 全部是內(nèi)存操作,操作速度很快,即便申請時鎖被占用,也會很快被釋放。

Mc 哈希表在擴容時,哈希表維護線程,每次按 桶鏈表緯度 遷移,即一次遷移一個桶里單向鏈表的所有 Item 元素。在擴容過程中,如果要查找或插入 key,會參照遷移位置選擇哈希表。如果 key 對應的哈希桶在遷移位置之前,則到新的主哈希表進行查詢或插入,否則到舊哈希表進行查詢和插入。待全部擴容遷移完畢,所有的處理就會全部在新的主哈希表進行。

### 第11講:MC如何淘汰冷key和失效key?

你好,我是你的緩存課老師陳波,歡迎進入第 11 課時“Memcached 淘汰策略”的學習。
淘汰策略

Mc 作為緩存組件,意味著 Mc 中只能存儲訪問最頻繁的熱數(shù)據(jù),一旦存入數(shù)據(jù)超過內(nèi)存限制,就需要對 Mc 中的冷 key 進行淘汰工作。Mc 中的 key 基本都會有過期時間,在 key 過期后,出于性能考慮,Mc 并不會立即刪除過期的 key,而是由維護線程逐步清理,同時,只有這個失效的 key 被訪問時,才會進行刪除,從而回收存儲空間。所以 Mc 對 key 生命周期的管理,即 Mc 對 key 的淘汰,包括失效和刪除回收兩個緯度,知識結構如下圖所示。

img

key 的失效,包括 key 在 expire 時間之后的過期,以及用戶在 flush_all 之后對所有 key 的過期 2 種方式。

而 Mc 對 key/value 的刪除回收,則有 3 種方式。

第一種是獲取時的惰性刪除,即 key 在失效后,不立即刪除淘汰,而在獲取時,檢測 key 的狀態(tài),如果失效,才進行真正的刪除并回收存儲空間。第二種方式是在需要對 Item 進行內(nèi)存分配申請時,如果內(nèi)存已全部用完,且該 Item 對應的slabclass 沒有空閑的 chunk 可用,申請失敗,則會對 LRU 隊尾進行同步掃描,回收過期失效的 key,如果沒有失效的 key,則會強制刪除一個 key。第三種方式是 LRU 維護線程,不定期掃描 4 個 LRU 隊列,對過期 key/value 進行異步淘汰。

flush_all

Mc 中,key 失效除了常規(guī)的到達過期時間之外,還有一種用 flush_all 的方式進行全部過期。如果緩存數(shù)據(jù)寫入異常,出現(xiàn)大量臟數(shù)據(jù),而又沒有簡單的辦法快速找出所有的臟數(shù)據(jù),可以用 flush_all 立即讓所有數(shù)據(jù)失效,通過 key 重新從 DB 加載的方式來保證數(shù)據(jù)的正確性。flush_all 可以讓 Mc 節(jié)點的所有 key 立即失效,不過,在某些場景下,需要讓多個 Mc 節(jié)點的數(shù)據(jù)在某個時間同時失效,這時就可以用 flush_all 的延遲失效指令了。該指令通過 flush_all 指令后面加一個 expiretime 參數(shù),可以讓多個 Mc 在某個時間同時失效所有的 key。

img

flush_all 后面沒有任何參數(shù),等價于 flush_all 0,即立即失效所有的 key。當 Mc 收到 flush_all 指令后,如果是延遲失效,會將全局 setting 中的 oldest_live 設為指定 N 秒后的時間戳,即 N 秒后失效;如果是立即失效,則將全局 setting 中的 oldest_cas 設為當前最大的全局 cas 值。設置完這個全局變量值后,立即返回。因此,在 Mc 通過 flush_all 失效所有 key 時,實際不做任何 key 的刪除操作,這些 key ,后續(xù)會通過用戶請求同步刪除,或 LRU 維護線程的異步刪除,來完成真正的刪除動作。
惰性刪除

Mc 中,過期失效 key 的惰性主動刪除,是指在 touch、get、gets 等指令處理時,首先需要查詢 key,找到 key 所在的 Item,然后校驗 key 是否過期,是否被 flush,如果過期或被 flush,則直接進行真正的刪除回收操作。

對于校驗 key 過期很容易,直接判斷過期時間即可。對于檢查 key 是否被 flush,處理邏輯是首先檢查 key 的最近訪問時間是否小于全局設置中的 oldest_live,如果小于則說明 key 被 flush 了;否則,再檢查 key 的 cas 唯一 id 值,如果小于全局設置中的 oldest_cas,說明也被 flush 了。
內(nèi)存分配失敗,LRU 同步淘汰

Mc 在插入或變更 key 時,首先會在適合的 slabclass 為新的 key/value 分配一個空閑的 Item 空間,如果分配失敗,會同步對該 slabclass 的 COLD LRU 進行隊尾元素淘汰,如果淘汰回收成功,則 slabclass 會多一個空閑的 Item,這個 Item 就可以被前面那個 key 來使用。如果 COLD LRU 隊列沒有 Item 數(shù)據(jù),則淘汰失敗,此時會對 HOT LRU 進行隊尾輪詢,如果 key 過期失效則進行淘汰回收,否則進行遷移。
LRU 維護線程,異步淘汰

在 key 進行讀取、插入或變更時,同步進行 key 淘汰回收,并不是一種高效的辦法,因為淘汰回收操作相比請求處理,也是一個重量級操作,會導致 Mc 性能大幅下降。因此 Mc 額外增加了一個 LRU 維護線程,對過期失效 key 進行回收,在不增加請求負擔的情況下,盡快回收失效 key 鎖占用的空間。

前面講到,Mc 有 64 個 slabclass,其中 1~63 號 slabclass 用于存取 Item 數(shù)據(jù)。實際上,為了管理過期失效數(shù)據(jù),1~63 號 slabclass 還分別對應了 4 個 LRU,分布是 TEMP、HOT、WARM、COLD LRU。所以這就總共有 63*4 = 252 個 LRU。LRU 維護線程,會按策略間斷 sleep,待 sleep 結束,就開始對 4 個 LRU 進行隊尾清理工作。

Mc 在新寫入 key 時,如果 key 的過期時間小于 61s,就會直接插入到 TEMP LRU 中,如下圖所示。TEMP LRU 沒有長度限制,可以一直插入,同時因為過期時間短,TEMP LRU 不進行隊列內(nèi)部的搬運和隊列間的遷移,確保處理性能最佳。LRU 維護線程在 sleep 完畢后,首先會對 TEMP LRU 隊尾進行 500 次輪詢,然后在每次輪詢時,會進行 5 次小循環(huán)。小循環(huán)時,首先檢查 key是否過期失效,如果失效則進行回收淘汰,然后繼續(xù)小循環(huán);如果遇到一個沒失效的 key,則回收該 key 并退出 TEMP LRU 的清理工作。如果 TEMP LRU 隊尾 key 全部失效,維護線程一次可以回收 500*5 共 2500 個失效的 key。

如下圖,MC 在新寫入 key 時,如果 key 的過期時間超過 61s,就會直接插入到 HOT LRU。HOT LRU 會有內(nèi)存限制,每個 HOT LRU 所占內(nèi)存不得超過所在 slabclass 總實際使用內(nèi)存的 20%。LRU 維護線程在執(zhí)行日常維護工作時,首先對 TEMP LRU 進行清理,接下來就會對 HOT LRU 進行維護。HOT LRU 的維護,也是首先輪詢 500 次,每次輪詢進行 5 次小循環(huán),小循環(huán)時,首先檢查 key 是否過期失效,如果失效則進行回收淘汰,然后繼續(xù)小循環(huán)。直到遇到?jīng)]失效的 key。如果這個 key 的狀態(tài)是 ACTIVE,則遷移到 WARM LRU。對于非 ACTIVE 狀態(tài)的 key,如果 HOT LRU 內(nèi)存占用超過限制,則遷移到 COLD LRU,否則進行紓困性清理掉該 key,注意這種紓困性清理操作一般不會發(fā)生,一旦發(fā)生時,雖然會清理掉該 key,但操作函數(shù)此時也認定本次操作回收和清理 keys 數(shù)仍然為 0。

img

如下圖,如果 HOT LRU 中回收和遷移的 keys 數(shù)為 0,LRU 維護線程會對 WARM LRU 進行輪詢。WARM LRU 也有內(nèi)存限制,每個 WARM LRU 所占內(nèi)存不得超過所在 slabclass 總實際使用內(nèi)存的 40%。WARM LRU 的維護,也是首先輪詢 500 次,每次輪詢進行 5 次小循環(huán),小循環(huán)時,首先檢查 key 是否過期失效,如果失效則進行回收淘汰,然后繼續(xù)小循環(huán)。直到遇到?jīng)]失效的 key。如果這個 key 的狀態(tài)是 ACTIVE,則內(nèi)部搬運到 LRU 隊列頭部。對于非 ACTIVE 狀態(tài)的 key,如果 WARM LRU 內(nèi)存占用超過限制,則遷移到 COLD LRU,否則進行紓困性清理掉該 key。注意這種紓困性清理操作一般不會發(fā)生,一旦發(fā)生時,雖然會清理掉該 key,但操作函數(shù)此時也認定本次操作回收和清理 keys 數(shù)仍然為 0。

img

LRU 維護線程最后會對 COLD LRU 進行維護,如下圖。與 TEMP LRU 相同,COLD LRU 也沒有長度限制,可以持續(xù)存放數(shù)據(jù)。COLD LRU 的維護,也是首先輪詢 500 次,每次輪詢進行 5 次小循環(huán),小循環(huán)時,首先檢查 key 是否過期失效,如果失效則進行回收淘汰,然后繼續(xù)小循環(huán)。直到遇到?jīng)]失效的 key。如果這個 key 的狀態(tài)是 ACTIVE,則會遷移到 WARM LRU 隊列頭部,否則不處理直接返回。

img

LRU 維護線程處理時,TEMP LRU 是在獨立循環(huán)中進行,其他三個 LRU 在另外一個循環(huán)中進行,如果 HOT、WARM、COLD LRU 清理或移動的 keys 數(shù)為 0,則那個 500 次的大循環(huán)就立即停止。

第12講:為何MC能長期維持高性能讀寫?

你好,我是你的緩存課老師陳波,歡迎進入第 12 課時“Memcached 內(nèi)存管理 slab 機制”的學習。
內(nèi)存管理 slab 機制

講完淘汰策略,我們接下來學習內(nèi)存管理 slab 機制。

Mc 內(nèi)存分配采用 slab 機制,slab 機制可以規(guī)避內(nèi)存碎片,是 Mc 能持續(xù)高性能進行數(shù)據(jù)讀寫的關鍵。
slabclass

Mc 的 slab 機制是通過 slabclass 來進行運作的,如下圖所示。Mc 在啟動時,會構建長度為 64 的 slabclass 數(shù)組,其中 0 號 slabclass 用于 slab 的重新分配,1~63 號 slabclass 存儲數(shù)據(jù) Item。存儲數(shù)據(jù)的每個 slabclass,都會記錄本 slabclass 的 chunk size,同時不同 slabclass 的 chunk size 會按遞增因子增加,最后一個 slabclass(即 63 號 slabclass)的 chunk size 會直接設為最大的 chunk size,默認是 0.5MB。每個 slabclass 在沒有空閑的 chunk 時,Mc 就會為其分配一個默認大小為 1MB 的 slab,同時按照本 slabclass 的 chunk size 進行拆分,這些分拆出來的 chunk 會按 Item 結構體進行初始化,然后記錄到 slabclass 的 freelist 鏈表中。當有 key/value 要存儲在本 slabclass 時,就從 freelist 分配一個 Item,供其使用。同時,如果 Item 過期了,或被 flush_all 失效了,或在內(nèi)存不夠時被強項剔除了,也會在適當時刻,重新被回收到 freelist,以供后續(xù)分配使用。

img

存儲 slab 分配

如下圖所示,Mc 的存儲空間分配是以 slab 為單位的,每個 slab 的默認大小時 1MB。因此在存數(shù)據(jù)時,Mc 的內(nèi)存最小分配單位是 1MB,分配了這個 1MB 的 slab 后,才會進一步按所在 slabclass 的chunk size 進行細分,分拆出的相同 size 的 chunk。這個 chunk 用來存放 Item 數(shù)據(jù),Item 數(shù)據(jù)包括 Item 結構體字段,以及 key/value。

一般來講,Item 結構體及 key/value 不會填滿 chunk,會存在少量字節(jié)的浪費,但這個浪費的字節(jié)很少,基本可以忽略。Mc 中,slab 一旦分配,就不會再被回收,但會根據(jù)運行狀況,重新在不同 slabclass 之間進行分配。

img

當一個 slabclass 沒有空閑 chunk,而新數(shù)據(jù)插入時,就會對其嘗試增加一個新的 slab。slabclass 增加新 slab 時,首先會從 0 號全局 slabclass 中復用一個之前分配的 slab,如果 0 號 slabclass 沒有 slab,則會嘗試從內(nèi)存堆空間直接分配一個 slab。如果 0 號全局 slabclass 沒有空閑 slab,而且 Mc 內(nèi)存分配已經(jīng)達到 Mc 設定的上限值,就說明此時沒有可重新分配的 slab,分配新 slab 失敗,直接返回。

當然,雖然 slabclass 分配 slab 失敗,但并不意味著 Item分配會失敗,前面已經(jīng)講到,可以通過同步 LRU 淘汰,回收之前分配出去的 Item,供新的存儲請求使用。
Item

Mc 中,slabclass 中的 chunk 會首先用 Item 結構體進行初始化,然后存到 freelist 鏈表中,待需要分配給數(shù)據(jù)存儲時,再從 freelist 中取出,存入 key/value,以及各種輔助屬性,然后再存到 LRU 鏈表及 Hashtable 中,如下圖所示。Item 結構體,首先有兩個 prev、next 指針,在分配給待存儲數(shù)據(jù)之前,這兩個指針用來串聯(lián) freelist 鏈表,在分配之后,則用來串聯(lián)所在的 LRU 鏈表。接下來是一個 h_next 指針,用來在分配之后串聯(lián)哈希表的桶單向鏈表。Item 結構體還存儲了過期時間、所屬 slabclass id,key 長度、cas 唯一 id 值等,最后在 Item 結構體尾部,存儲了 key、flag、value 長度,以及 value block 數(shù)據(jù)。在 value 之后的 chunk 空間,就被浪費掉了。Item 在空閑期間,即初始分配時以及被回收后,都被 freelist 管理。在存儲期間,被哈希表、LRU 管理。

img

存儲 Item 分配

Mc 采用 slab 機制管理分配內(nèi)存,采用 Item 結構存儲 key/value,因此對存儲 key/value 的內(nèi)存分配,就轉換為對 Item 的分配。分配 Item 空間時,會進行 10 次大循環(huán),直到分配到 Item 空間才會提前返回。如果循環(huán)了 10 次,還沒有分配到 Item 空間,則存儲失敗,返回一個 SERVER_ERROR 響應。

在分配過程中,首先,如果 slabclass 的 freelist 有空間,則直接分配。否則,嘗試分配一個新的 slab,新 slab 依次嘗試從全局 slab 池(即 0 號 slabclass)中復用一個空閑 slab,如果全局 slab 池沒有 slab,則嘗試從內(nèi)存直接分配。分配新 slab 成功后,會按照 slabclass 記錄的 chunk size 對 slab 進行分拆,并將分拆出來的 chunk 按 Item 結構初始化后記錄到 freelist。如果全局 slab 池為空,且 Mc 內(nèi)存分配已經(jīng)達到設定的上限,則走新增 slab 的路徑失敗,轉而進行 5 次小循環(huán),嘗試從 COLD LRU 回收過期 key,如果沒有過期則直接強制剔除隊尾的一個正常 key。如果該 slabclass 的 COLD LRU 沒有 Item,則對其 HOT LRU 進行處理,對 HOT 鏈表隊尾 Item 進行回收或者遷移,以方便在下次循環(huán)中找到一個可用的 Item 空間。

數(shù)據(jù)存儲機理

講完 Mc 的哈希表定位、LRU 淘汰、slab 內(nèi)存分配,接下來我們來看看 Mc 中 key/value 數(shù)據(jù)的存儲機理,通過對數(shù)據(jù)存儲以及維護過程的分析,來把 Mc 的核心模塊進行打通和關聯(lián)。

首先來看 Mc 如何通過 slab 機制將數(shù)據(jù)寫入預分配的存儲空間。

如下圖所示,當需要存儲 key/value 數(shù)據(jù)時,首先根據(jù) key/value size,以及 Item 結構體的 size,計算出存儲這個 key/value 需要的字節(jié)數(shù),然后根據(jù)這個字節(jié)數(shù)選擇一個能存儲的 chunk size 最小的 slabclass。再從這個 slabclass 的 freelist 分配一個空閑的 chunk 給這個 key/value 使用。如果 freelist 為空,首先嘗試為該 slabclass 新分配一個 slab,如果 slab 分配成功,則將 slab 按 size 分拆出一些 chunk,通過 Item 結構初始化后填充到 freelist。如果 slab 分配失敗,則通過 LRU 淘汰失效的 Item 或強行剔除一個正常的 Item,然后這些 Item 也會填充到 freelist。當 freelist 有 Item 時,即可分配給 key/value。這個過程會重試 10 次,直到分配到 Item 位置。一般情況下,Item 分配總會成功,極小概率情況下也會分配失敗,如果分配失敗,則會回復一個 SERVER_ERROR 響應,通知 client 存儲失敗。分配到一個空閑的 Item 后,就會往這個 Item 空間寫入過期時間、flag、slabclass id、key,以及 value 等。對于 set 指令,如果這個 key 還有一個舊值,在存入新 value 之前,還會先將這個舊值刪除掉。

img

當對 key/value 分配 Item 成功,并寫入數(shù)據(jù)后,接下來就會將這個 Item 存入哈希表。因為Mc 哈希表存在遷移的情況,所以對于正常場景,直接存入主哈希表。在哈希表遷移期間,需要根據(jù)遷移位置,選擇存入主哈希表還是舊哈希表。存入哈希表之后,這個 key 就可以快速定位了。然后這個 Item 還會被存入 LRU,Mc 會根據(jù)這個 key 的過期時間進行判斷,如果過期時間小于 61s,則存入 TEMP LRU,否則存入 HOT LRU。

至此,這個 key/value 就被正確地存入 Mc 了,數(shù)據(jù)內(nèi)容寫入 slabclass 中某個 slab 的 chunk 位置,該 chunk 用 Item 結構填充,這個 Item 會被同時記錄到 Hashtable 和 LRU,如下圖所示。通過 Hashtable 可以快速定位查到這個 key,而 LRU 則用于 Item 生命周期的日常維護。

img

Mc 對 Item 生命周期的日常維護,包括異步維護和同步維護。異步維護是通過 LRU 維護線程來進行的,整個過程不影響 client 的正常請求,在 LRU 維護線程內(nèi),對過期、失效 key 進行回收,并對 4 個 LRU 進行鏈表內(nèi)搬運和鏈表間遷移。這是 Item 生命周期管理的主要形式。同步維護,由工作線程在處理請求命令時進行。工作線程在處理 delete 指令時,會直接將 key/value 進行刪除。在存儲新 key/value 時,如果分配失敗,會進行失效的 key 回收,或者強行剔除正常的 Item。這些 Item 被回收后,會進入到 slabclass 的 freelist 進行重復使用。

第13講:如何完整學習MC協(xié)議及優(yōu)化client訪問?

你好,我是你的緩存課老師陳波,歡迎進入第 13 課時“Memcached 協(xié)議分析”的學習。
協(xié)議分析
異常錯誤響應

接下來,我們來完整學習 Mc 協(xié)議。在學習 Mc 協(xié)議之前,首先來看看 Mc 處理協(xié)議指令,如果發(fā)現(xiàn)異常,如何進行異常錯誤響應的。Mc 在處理所有 client 端指令時,如果遇到錯誤,就會返回 3 種錯誤信息中的一種。

第一種錯誤是協(xié)議錯誤,一個"ERROR\r\n"的字符串。表明 client 發(fā)送了一個非法命令。第二種錯誤是 client 錯誤,格式為"CLIENT_ERROR <error-描述信息>\r\n"。這個錯誤信息表明 ,client 發(fā)送的協(xié)議命令格式有誤,比如少了字段、多了非法字段等。第三種錯誤是"SERVER_ERROR <error-描述信息>\r\n"。這個錯誤信息表明 Mc server 端,在處理命令時出現(xiàn)的錯誤。比如在給 key/value 分配 Item 空間失敗后,會返回"SERVER_ERROR out of memory storing object" 錯誤信息。

存儲協(xié)議命令

現(xiàn)在再來看看 Mc 的存儲協(xié)議。Mc 的存儲協(xié)議命令不多,只有 6 個。

Mc 存儲指令分 2 行。第一行是報文首部,第二行是 value 的 data block 塊。這兩部分用 \r\n 來進行分割和收尾。

存儲類指令的報文首行分 2 種格式,其中一種是在 cmd 存儲指令,后面跟 key、flags、expiretime、value 字節(jié)數(shù),以及一個可選的 noreply。

其中 flags 是用戶自己設計的一個特殊含義數(shù)字,Mc 對 flag 只存儲,而不進行任何額外解析處理,expiretime 是 key 的過期時間,value 字節(jié)數(shù)是 value block 塊的字節(jié)長度,而帶上 noreply 是指 Mc 處理完后靜默處理,不返回任何響應給 client。

這種 cmd 指令包括我們最常用的 set 指令,另外還包括 add、replace、append、reppend ,總共 5 個指令:

Set 命令用于存儲一個 key/value;Add 命令是在當 key 不存在時,才存儲這個 key/value;Replace 命令,是當 key 存在時,才存儲這個 key/value;Append 命令,是當 key 存在時,追加 data 到 value 的尾部;Prepend 命令,是當 key 存在時,將 data 加到 value 的頭部。

另外一種存儲協(xié)議指令,主要格式和字段與前一種基本相同,只是多了一個 cas unique id,這種格式只有 cas 指令使用。cas 指令是指只有當這個 key 存在,且從本 client 獲取以來,沒有其他任何人修改過時,才進行修改。cas 的英文含義是 compare and set,即比較成功后設置的意思。
存儲命令響應

Mc 在響應存儲協(xié)議時,如果遇到錯誤,就返回前面說的3種錯誤信息中的一種。否則就會返回如下 4 種正常的響應,"STORED\r\n”、"EXISTS\r\n”、“NOT_STORED\r\n”、"NOT_FOUND\r\n“。

其中,stored 表明存儲修改成功。NOT_STORED 表明數(shù)據(jù)沒有存儲成功,但并不是遇到錯誤或異常。這個響應一般表明 add 或 replace 等指令,前置條件不滿足時,比如 add,這個 key 已經(jīng)存在 Mc,就會 add 新 key 失敗。replace 時, key 不存在,也無法 replace 成功。EXISTS 表明待 cas 的key 已經(jīng)被修改過了,而 NOT_FOUND 是指待 cas 的 key 在 Mc 中不存在。

img

Mc 對存儲命令的請求及響應協(xié)議,可以參考下面的思維導圖來有一個完整的印象。

獲取命令

Mc 的獲取協(xié)議,只有 get、gets 兩種指令,如下圖所示。格式為 get/gets 后,跟隨若干個 key,然后 \r\n 結束請求命令。get 指令只獲取 key 的 flag 及 value,gets 會額外多獲取一個 cas unique id值。gets 主要是為 cas 指令服務的。

獲取命令的響應,就是 value 字串,后面跟上 key、flag、value 字節(jié)數(shù),以及 value 的 data block 塊。最后跟一個 END\r\n 表明所有存在的 key/value 已經(jīng)返回,如果沒有返回的 key,則表明這個 key 在 Mc 中不存在。

img

其他指令

Mc 的其他協(xié)議指令包括 delete、incr、decr、touch、gat、gats、slabs、lru、stats 這 9 種指令。

其中 delete 用于刪除一個 key。

incr/decr 用于對一個無符號長整型數(shù)字進行加或減。

touch、gat、gats 是 Mc 后來增加的指令,都可以用來修改 key 的過期時間。不同點是 touch 只修改 key 的過期時間,不獲取 key對應的value。

而 gat、gats 指令,不僅會修改 key 的過期時間,還會獲取 key 對應的 flag 和 value 數(shù)據(jù)。gats 同 gets,還會額外獲取 cas 唯一 id 值。

Slabs reassign 用于在 Mc 內(nèi)存達到設定上限后,將 slab 重新在不同的 slabclass 之間分配。這樣可以規(guī)避 Mc 啟動后自動分配而產(chǎn)生隨機性,使特殊 size 的數(shù)據(jù)也得到較好的命中率。Slabs automove 是一個開關指令,當打開時,就允許 Mc 后臺線程自行決定何時將 slab 在slabclass 之間重新分配。

lru 指令用于 Mc LRU 的設置和調(diào)優(yōu)。比如 LRU tune 用于設置 HOT、WARM LRU 的內(nèi)存占比。LRU mode 用來設置 Mc 只使用 COLD LRU,還是使用新版的 4 個 LRU 的新策略。LRU TEMP_TTL 用來設置 Mc 的 TEMP LRU 的TTL值,默認是 61s,小于這個 TMEP_TTL 的 key會被插入到 TEMP LRU。

Stats 用于獲取 Mc 的各種統(tǒng)計數(shù)據(jù)。Stats 后面可以跟 statistics、slabs、size 等參數(shù),來進一步獲取更多不同的詳細統(tǒng)計。
Client 使用

Mc 在互聯(lián)網(wǎng)企業(yè)應用廣泛,熱門語言基本都有 Mc client 的實現(xiàn)。以 Java 語言為例,互聯(lián)網(wǎng)業(yè)界廣泛使用的有 Memcached-Java-Client、SpyMemcached、Xmemcached 等。

Memcached-Java-Client 推出時間早,10 年前就被廣泛使用,這個 client 性能一般,但足夠穩(wěn)定,很多互聯(lián)網(wǎng)企業(yè)至今仍在使用。不過這個 client 幾年前就停止了更新。

SpyMemcached 出現(xiàn)的比較晚,性能較好,但高并發(fā)訪問場景,穩(wěn)定性欠缺。近幾年

變更很少,基本停止了更新。

Xmemcached 性能較好,綜合表現(xiàn)最佳。而且社區(qū)活躍度高,近些年也一直在持續(xù)更新中。Java 新項目啟動,推薦使用 Xmemcached。

在使用 Mc client 時,有一些通用性的調(diào)優(yōu)及改進方案。比如,如果讀寫的 key/value 較大,需要設置更大的緩沖 buf,以提高性能。在一些業(yè)務場景中,需要啟用 TCP_NODELAY,避免 40ms 的延遲問題。同時,如果存取的 key/value size 較大,可以設置一個壓縮閥值,超過閥值,就對value 進行壓縮算法,減少讀寫及存儲的空間。

為了避免緩存雪崩,并更好地應對極熱 key 及洪水流量的問題,還可以對 Mc client 進行封裝,加入多副本、多層級策略,使 Mc 緩存系統(tǒng)在任何場景下,都可做到高可用、高性能。

講到這里,Mc 的核心知識點就基本講完了,知識點結構圖如下所示。

img

回顧一下最近幾節(jié)課的內(nèi)容。首先,學習了 Mc 的系統(tǒng)架構,學習了 Mc 基于 libevent 的網(wǎng)絡模型,學習了 Mc 的多線程處理,包括主線程、工作線程如何進行網(wǎng)絡 IO 協(xié)調(diào)及處理,學習了 Mc 的狀態(tài)機。然后,繼續(xù)學習了 Mc 用于定位 key 的哈希表,學習了用于數(shù)據(jù)生命周期管理的 LRU,還學習 slab 分配機制,以及 Mc 數(shù)據(jù)的存儲機理。最后,還完整學習了 Mc的協(xié)議,了解了以 Java 語言為例的 3 種 Mc client,以及 Mc client 在線上使用過程中,如何進行調(diào)優(yōu)及改進。

根據(jù)下面 Mc 協(xié)議的思維導圖,查看自己是否對所有指令都有理解,可以結合 Mc 的協(xié)議文檔,啟動一個 Mc 實例,進行各個命令的實際操練。

img

OK,這節(jié)課就講到這里啦,下一課時我將分享“Memcached 經(jīng)典問題及解決方案”,記得按時來聽課哈。好,下節(jié)課見,拜拜!

第五章:分布式Memcached實戰(zhàn)

第14講:大數(shù)據(jù)時代,MC如何應對新的常見問題?

你好,我是你的緩存課老師陳波,歡迎進入第 14 課時“Memcached 經(jīng)典問題及解決方案”的學習。
大數(shù)據(jù)時代 Memcached 經(jīng)典問題

隨著互聯(lián)網(wǎng)的快速發(fā)展和普及,人類進入了大數(shù)據(jù)時代。在大數(shù)據(jù)時代,移動設備全面融入了人們的工作和生活,各種數(shù)據(jù)以前所未有的 速度被生產(chǎn)、挖掘和消費。移動互聯(lián)網(wǎng)系統(tǒng)也不斷演進和發(fā)展,存儲、計算和分析這些海量數(shù)據(jù),以滿足用戶的需要。在大數(shù)據(jù)時代,大中型互聯(lián)網(wǎng)系統(tǒng)具有如下特點。

首先,系統(tǒng)存儲的數(shù)據(jù)量巨大,比如微博系統(tǒng),每日有數(shù)億條記錄,歷史數(shù)據(jù)達百億甚至千億條記錄。其次,用戶多,訪問量巨大,每日峰值流量高達百萬級 QPS。要存儲百千億級的海量數(shù)據(jù),同時滿足大量用戶的高并發(fā)訪問,互聯(lián)網(wǎng)系統(tǒng)需要部署較多的服務實例,不少大中型互聯(lián)網(wǎng)系統(tǒng)需要部署萬級,甚至十萬級的服務實例。再次,由于大數(shù)據(jù)時代,社會信息獲取扁平化,熱點事件、突發(fā)事件很容易瞬間引爆,引來大量場外用戶集中關注,從而形成流量洪峰。最后,任何硬件資源都有發(fā)生故障的概率,而且存在 4 年故障效應,即服務資源在使用 4 年后,出現(xiàn)故障的概率會陡增;由于大中型互聯(lián)網(wǎng)系統(tǒng)的部署,需要使用大量的服務器、路由器和交換機,同時部署在多個地區(qū)的不同 IDC,很多服務資源的使用時間遠超 4 年,局部出現(xiàn)硬件故障障、網(wǎng)絡訪問異常就比較常見了。

由于互聯(lián)網(wǎng)系統(tǒng)會大量使用 Memcached 作為緩存,而在使用 Memcached 的過程中,同樣也會受到前面所說的系統(tǒng)特點的影響,從而產(chǎn)生特有的經(jīng)典問題。
容量問題

第一個問題是容量問題。Memcached 在使用中,除了存儲數(shù)據(jù)占用內(nèi)存外,連接的讀寫緩沖、哈希表分配、輔助線程處理、進程運行等都會占用內(nèi)存空間,而且操作系統(tǒng)本身也會占用不少內(nèi)存,為了確保 Mc 的穩(wěn)定運行,Mc 的內(nèi)存設置,一般設為物理內(nèi)存的 80%。另外,設置的內(nèi)存,也不完全是存儲有效數(shù)據(jù),我上一節(jié)課講到,每個 Item 數(shù)據(jù)存儲在 chunk 時,會有部分字節(jié)浪費,另外 key 在過期、失效后,不是立即刪除,而是采用延遲淘汰、異步 LRU 隊尾掃描的方式清理,這些暫時沒有淘汰的、過期失效的 key ,也會占用不少的存儲空間。當前大數(shù)據(jù)時代,互聯(lián)網(wǎng)系統(tǒng)中的很多核心業(yè)務,需要緩存的熱數(shù)據(jù)在 300~500GB 以上,遠遠超過單機物理內(nèi)存的容量。
性能瓶頸

第二個問題是性能瓶頸問題。出于系統(tǒng)穩(wěn)定性考慮,線上 Mc 的訪問,最大 QPS 要在 10~20w 以下,超過則可能會出現(xiàn)慢查的問題。而對中大型互聯(lián)網(wǎng)系統(tǒng),核心業(yè)務的緩存請求高達百萬級 QPS,僅僅靠簡單部署單個物理機、單個資源池很難達到線上的業(yè)務要求。
連接瓶頸

第三個問題是連接瓶頸的問題。出于穩(wěn)定性考慮,線上 Mc 的連接數(shù)要控制在 10w 以下。以避免連接數(shù)過多,導致連接占用大量內(nèi)存,從而出現(xiàn)命中率下降、甚至慢查超時的問題。對于大中型系統(tǒng),線上實例高達萬級、甚至十萬級,單個實例的最小、最大連接數(shù),一般設置在 5~60 個之間。業(yè)務實例的連接數(shù)遠超過單個機器的穩(wěn)定支撐范圍。
硬件資源局部故障

第四個問題是硬件資源局部故障,導致的緩存體系的可用性問題。由于任何硬件資源,都有一定故障概率,而且在使用 4 年后,故障率陡增。對于數(shù)以萬計的硬件設備,隨時都有可能出現(xiàn)機器故障,從而導致 Mc 節(jié)點訪問性能下降、宕機,海量訪問穿透到 DB,引發(fā) DB 過載,最終導致整個系統(tǒng)無法訪問,引發(fā)雪崩現(xiàn)象。
流量洪峰下快速擴展

第五個問題是在流量洪峰的場景下,如何快速擴展的問題。大數(shù)據(jù)時代,由于信息擴散的扁平化,突發(fā)事件、重大活動發(fā)生時,海量用戶同時蜂擁而至,短時間引發(fā)巨大流量。整個系統(tǒng)的訪問量相比日常峰值增大 70% 以上,同時出現(xiàn)大量的極熱 key 的訪問,這些極熱 key 所在的 Mc 節(jié)點,訪問量相比日常高峰,增大 2~3 倍以上,很容易出現(xiàn) CPU 飆升、帶寬打滿、機器負荷嚴重過載的現(xiàn)象。
Memchcaed 經(jīng)典問題及應對方案

為了解決大中型互聯(lián)網(wǎng)系統(tǒng)在使用 Mc 時的這些問題。我們可以使用下面的解決方案。
Memcached 分拆緩存池

首先對系統(tǒng)內(nèi)的核心業(yè)務數(shù)據(jù)進行分拆,讓訪問量大的數(shù)據(jù),使用獨立的緩存池。同時每個緩存池 4~8 個節(jié)點,這樣就可以支撐足夠大的容量,還避免單個緩存節(jié)點壓力過大。對于緩存池的分布策略,可以采用一致性哈希分布和哈希取模分布。

一致性哈希分布算法中,首先計算 Mc 服務節(jié)點的哈希值,然后將其持續(xù)分散配置在圓中,這樣每個緩存節(jié)點,實際包括大量大小各異的 N 個 hash 點。如下圖所示,在數(shù)據(jù)存儲或請求時,對 key 采用相同的 hash 算法,并映射到前面的那個圓中,從映射位置順時針查找,找到的第一個 Mc 節(jié)點,就是目標存取節(jié)點。

img

而哈希取模分布算法,則比較簡單,對 key 做 hash 后,對 Mc 節(jié)點數(shù)取模,即可找到待存取的目標 Mc 節(jié)點。

系統(tǒng)運行過程中,Mc 節(jié)點故障不可避免,有時候甚至短期內(nèi)出現(xiàn)多次故障。在 Mc 節(jié)點故障下線后,如果采用一致性 hash 分布,可以方便得通過 rehash 策略,將該 Mc 節(jié)點的 hash 點、訪問量,均勻分散到其他 Mc 節(jié)點。如果采用取模分布,則會直接導致 1/N 的訪問 miss,N 是 Mc 資源池的節(jié)點數(shù)。

因此,對于單層 Mc 緩存架構,一致性 hash 分布配合 rehash 策略,是一個更佳的方案。通過將業(yè)務數(shù)據(jù)分拆到獨立 Mc 資源池,同時在每個資源池采用合適的分布算法,可以很好的解決 Mc 使用中容量問題、性能瓶頸問題,以及連接瓶頸問題。
Master-Slave 兩級架構

在系統(tǒng)的訪問量比較大,比如峰值 QPS 達到 20w 以上時,如果緩存節(jié)點故障,即便采用一致性 hash,也會在一段時間內(nèi)給 DB 造成足夠大的壓力,導致大量慢查詢和訪問超時的問題。另外,如果某些緩存服務器短期多次故障,反復上下線,多次 rehash 還會產(chǎn)生臟數(shù)據(jù)。對此,可以采用 Master-Slave 的兩級架構方案。

在這種架構方案下,將業(yè)務正常訪問的 Memcached 緩存池作為 master,然后在 master 之后,再加一個slave 資源池作 master 的熱備份。slave 資源池也用 6~8 個節(jié)點,內(nèi)存設置只用 master 的 1/2~1/3 即可。因為 slave 的應用,主要是考慮在 master 訪問 miss 或異常時,Mc 緩存池整體的命中率不會過度下降,所以并不需要設置太大內(nèi)存。

日常訪問,對于讀操作,直接訪問 master,如果訪問 miss,再訪問 slave。如果 slave 命中,就將讀取到的 key 回寫到 master。對于寫操作,set、touch 等覆蓋類指令,直接更新master 和 slave;而 cas、append 等,以 master 為準,master 在 cas、add 成功后,再將 key 直接 set 到 slave,以保持 master、slave 的數(shù)據(jù)一致性。

如下圖,在 master 部分節(jié)點異常后,由 slave 層來承接。任何一層,部分節(jié)點的異常,不會影響整體緩存的命中率、請求耗時等 SLA 指標。同時分布方式采用哈希取模方案,mc 節(jié)點異常不rehash,直接穿透,方案簡潔,還可以避免一致性 hash 在 rehash 后產(chǎn)生的臟數(shù)據(jù)問題。

img

Master-Slave 架構,在訪問量比較大的場景下,可以很好得解決局部設備故障的問題。在部分節(jié)點異?;蛟L問 miss 時,多消耗 1ms 左右的時間,訪問 slave 資源,實現(xiàn)以時間換系統(tǒng)整體可用性的目的。
M-S-L1 架構

20世紀初,意大利統(tǒng)計學家帕累托提出來一個觀點:在任何特定群體中,重要的因子通常只占少數(shù),而不重要的因子則占多數(shù),因此只要能控制具有重要性的少數(shù)因子,即能控制全局。這個理論經(jīng)過多年演化,就成為當前大家所熟悉的 80/20 定律。80/20 定律在互聯(lián)網(wǎng)系統(tǒng)中也廣泛存在,如 80% 的用戶訪問會集中在系統(tǒng) 20% 的功能上,80% 的請求會集中在 20% 的數(shù)據(jù)上。因此,互聯(lián)網(wǎng)系統(tǒng)的數(shù)據(jù),有明顯的冷熱區(qū)分,而且這個冷熱程度往往比 80/20 更大,比如微博、微信最近一天的數(shù)據(jù),被訪問的特別頻繁,而一周前的數(shù)據(jù)就很少被訪問了。而且最近幾天的熱數(shù)據(jù)中,部分 feed 信息會被大量傳播和交互,比其他 大部分數(shù)據(jù)的訪問量要高很多倍,形成明顯的頭部請求。

頭部請求,會導致日常大量訪問,被集中在其中一小部分 key 上。同時,在突發(fā)新聞、重大事件發(fā)生時,請求量短期增加 50~70% 以上,而這些請求,又集中在 突發(fā)事件的關聯(lián) key 上,造就大量的熱 key 的出現(xiàn)。熱 key 具有隨機性,如果集中在某少數(shù)幾個節(jié)點,就會導致這 些節(jié)點的壓力陡增數(shù)倍,負荷嚴重過載,進而引發(fā)大量查詢變慢超時的問題。

為了應對日常峰值的熱數(shù)據(jù)訪問,特別是在應對突發(fā)事件時,洪峰流量帶來的極熱數(shù)據(jù)訪問,我們可以通過增加 L1 層來解決。如下圖所示,L1 層包含 2~6 組 L1 資源池,每個 L1 資源池,用 4~6 個節(jié)點,但內(nèi)存容量只要 Master 的 1/10 左右即可。

img

如圖,讀請求時,首先隨機選擇一個 L1 進行讀取,如果 miss 則訪問 master,如果 master 也 miss,最后訪問 slave。中途,只要任何一層命中,則對上一層資源池進行回寫。

寫請求時,同 Master-Slave 架構類似,對于 set 覆蓋類指令,直接 set 三層所有的資源池。對于 add/cas/append 等操作,以 master 為準,master 操作成功后,將最后的 key/value set 到 L1 和 slave 層所有資源池。

由于 L1 的內(nèi)存只有 master 的 1/10,且 L1 優(yōu)先被讀取,所以 L1 中 Memcached 只會保留最熱的 key,因為 key 一旦稍微變冷,就會排到 COLD LRU 隊尾,并最終被剔除。雖然 L1 的內(nèi)存小,但由于 L1 里,永遠只保存了 系統(tǒng)訪問量 最大最熱的數(shù)據(jù),根據(jù)我們的統(tǒng)計, L1 可以滿足整個系統(tǒng)的 60~80% 以上的請求數(shù)據(jù)。這也與 80/20 原則相符合。

master 存放全量的熱數(shù)據(jù),用于滿足 L1 讀取 miss 或異常后的訪問流量。slave 用來存放絕大部分的熱數(shù)據(jù),而且與 master 存在一定的差異,用來滿足 L1、master 讀取 miss 或異常的訪問流量。

這里面有個可以進一步優(yōu)化的地方,即為確保 master、slave 的熱度,讓 master、slave 也盡可能只保留最熱的那部分數(shù)據(jù),可以在讀取 L1 時,保留適當?shù)母怕?#xff0c;直接讀取 master 或slave,讓最熱的 key 被訪問到,從而不會被 master、slave 剔除。此時,訪問路徑需要稍做調(diào)整,即如果首先訪問了 master,如果 miss,接下來只訪問 slave。而如果首先訪問了 slave,如果 miss,接下來只訪問 master。

?

通過 Master-Slave-L1 架構,在流量洪峰到來之際,我們可以用很少的資源,快速部署多組L1資源池,然后加入 L1 層中,從而讓整個系統(tǒng)的抗峰能力達到 N 倍的提升。從而以最簡潔的辦法,快速應對流量洪峰,把極熱 key 分散到 N 組 L1 中,每個 L1 資源池只用負責 1/N 的請求。除了抗峰,另外,還可以輕松應對局部故障,避免雪崩的發(fā)生。

本課時,講解了大數(shù)據(jù)時代下大中型互聯(lián)網(wǎng)系統(tǒng)的特點,訪問 Memcached 緩存時的經(jīng)典問題及應對方案;還講解了如何通過分拆緩存池、Master-Slave 雙層架構,來解決 Memcached 的容量問題、性能瓶頸、連接瓶頸、局部故障的問題,以及 Master-Slave-L1 三層架構,通過多層、多副本 Memcached 體系,來更好得解決突發(fā)洪峰流量和局部故障的問題。

可以參考下面的思維導圖,對這些知識點進行回顧和梳理。

img

OK,這節(jié)課就講到這里啦,下一課時我將分享“Twemproxy 框架、應用及擴展 ”相關的知識,記得按時來聽課哈。好,下節(jié)課見,拜拜!

第15講:如何深入理解、應用及擴展 Twemproxy?

你好,我是你的緩存課老師陳波,歡迎進入第 15 課時“Twemproxy 框架、應用及擴展”的學習。
Twemproxy 架構及應用

Twemproxy 是 Twitter 的一個開源架構,它是一個分片資源訪問的代理組件。如下圖所示,它可以封裝資源池的分布及 hash 規(guī)則,解決后端部分節(jié)點異常后的探測和重連問題,讓 client 訪問盡可能簡單,同時資源變更時,只要在 Twemproxy 變更即可,不用更新數(shù)以萬計的 client,讓資源變更更輕量。最后,Twemproxy 跟后端通過單個長連接訪問,可以大大減少后端資源的連接壓力。
系統(tǒng)架構

接下來分析基于 Twemproxy 的應用系統(tǒng)架構,以及 Twemproxy 組件的內(nèi)部架構。

如下圖所示, 在應用系統(tǒng)中,Twemproxy 是一個介于 client 端和資源端的中間層。它的后端,支持Memcached 資源池和 Redis 資源池的分片訪問。Twemproxy 支持取模分布和一致性 hash 分布,還支持隨機分布,不過使用場景較少。

img

應用前端在請求緩存數(shù)據(jù)時,直接訪問 Twemproxy 的對應端口,然后 Twemproxy 解析命令得到 key,通過 hash 計算后,按照分布策略,將 key 路由到后端資源的分片。在后端資源響應后,再將響應結果返回給對應的 client。

在系統(tǒng)運行中,Twemproxy 會自動維護后端資源服務的狀態(tài)。如果后端資源服務異常,會自動進行剔除,并定期探測,在后端資源恢復后,再對緩存節(jié)點恢復正常使用。
組件架構

Twemproxy 是基于 epoll 事件驅動模型開發(fā)的,架構如下圖所示。它是一個單進程、單線程組件。核心進程處理所有的事件,包括網(wǎng)絡 IO,協(xié)議解析,消息路由等。Twemproxy 可以監(jiān)聽多個端口,每個端口接受并處理一個業(yè)務的緩存請求。Twemproxy 支持 Redis、Memcached 協(xié)議,支持一致性 hash 分布、取模分布、隨機分布三種分布方案。Twemproxy 通過 YAML 文件進行配置,簡單清晰,且便于人肉讀寫。

img

Twemproxy 與后端資源通過單個長連接訪問,在收到業(yè)務大量并發(fā)請求后,會通過 pipeline 的方式,將多個請求批量發(fā)到后端。在后端資源持續(xù)訪問異常時,Twemproxy 會將其從正常列表中剔除,并不斷探測,待其恢復后再進行請求的路由分發(fā)。

Twemproxy 運行中,會持續(xù)產(chǎn)生海量請求及響應的消息流,于是開發(fā)者精心設計了內(nèi)存管理機制,盡可能的減少內(nèi)存分配和復制,最大限度的提升系統(tǒng)性能。Twemproxy 內(nèi)部,請求和響應都是一個消息,而這個消息結構體,以及消息存放數(shù)據(jù)的緩沖都是重復使用的,避免反復分配和回收的開銷,提升消息處理的性能。為了解決短連接的問題,Twemproxy 的連接也是復用的,這樣在面對 PHP client 等短連接訪問時,也可以反復使用之前分配的 connection,提升連接性能。

另外,Twemproxy 對消息還采用了 zero copy(即零拷貝)方案。對于請求消息,只在client 接受時讀取一次,后續(xù)的解析、處理、轉發(fā)都不進行拷貝,全部共享最初的那個消息緩沖。對于后端的響應也采用類似方案,只在接受后端響應時,讀取到消息緩沖,后續(xù)的解析、處理及回復 client 都不進行拷貝。通過共享消息體及消息緩沖,雖然 Twemproxy 是單進程/單線程處理,仍然可以達到 6~8w 以上的 QPS。
Twemproxy 請求及響應

接下來看一下 Twemproxy 是如何進行請求路由及響應的。

Twemproxy 監(jiān)聽端口,當有 client 連接進來時,則 accept 新連接,并構建初始化一個 client_conn。當建連完畢,client 發(fā)送數(shù)據(jù)到來時,client_conn 收到網(wǎng)絡讀事件,則從網(wǎng)卡讀取數(shù)據(jù),并記入請求消息的緩沖中。讀取完畢,則開始按照配置的協(xié)議進行解析,解析成功后,就將請求 msg 放入到 client_conn 的 out 隊列中。接下來,就對解析的命令 key 進行 hash 計算,并根據(jù)分布算法,找到對應 server 分片的連接,即一個 server_conn 結構體,如下圖。

img

如果 server_conn的 in 隊列為空,首先對 server_conn 觸發(fā)一個寫事件。然后將 req msg 存入到 server_conn 的 in 隊列。Server_conn 在處理寫事件時,會對 in 隊列中的 req msg 進行聚合,按照 pipeline 的方式批量發(fā)送到后端資源。待發(fā)送完畢后,將該條請求 msg 從 server_conn 的 in 隊列刪除,并插入到 out 隊列中。

?

后端資源服務完成請求后,會將響應發(fā)送給 Twemproxy。當響應到 Twemproxy 后,對應的 server_conn 會收到 epoll 讀事件,則開始讀取響應 msg。響應讀取并解析后,會首先將server_conn 中,out 隊列的第一個 req msg 刪除,并將這個 req msg 和最新收到的 rsp msg 進行配對。在 req 和 rsp 匹配后,觸發(fā) client_conn 的寫事件,如下圖。

img

然后 client_conn 在處理 epoll 寫事件時,則按照請求順序,批量將響應發(fā)送給 client 端。發(fā)送完畢后,將 req msg 從 client 的 out 隊列刪除。最后,再回收消息緩沖,以及消息結構體,供后續(xù)請求處理的時候復用。至此一個請求的處理徹底完成。
Twemproxy 安裝和使用

Twemproxy 的安裝和使用比較簡單。首先通過 Git,將 Twemproxy 從 GitHub clone 到目標服務器,然后進入 Twemproxy 路徑,首先執(zhí)行 $ autoreconf -fvi,然后執(zhí)行 ./configure ,最后執(zhí)行 make(當然,也可以再執(zhí)行 make install),這樣就完成了 Temproxy 的編譯和安裝。然后就可以通過 src/nutcracker -c /xxx/conf/nutcracker.yml 來啟動 Twemproxy 了。

Twemproxy 代理后端資源訪問,這些后端資源的部署信息及訪問策略都是在 YAML 文件中配置。所以接下來,我們簡單看一下 Twemproxy 的配置。如圖所示,這個配置中代理了 2 個業(yè)務數(shù)據(jù)的緩存訪問。一個是 alpha,另一個是 beta。在每個業(yè)務的配置詳情里。首先是 listen 配置項,用于設置監(jiān)聽該業(yè)務的端口。然后是 hash 算法和分布算法。Auto_eject_hosts 用于設置在后端 server 異常時,是否將這個異常 server 剔除,然后進行 rehash,默認不剔除。Redis配置項用于指示后端資源類型,是 Redis 還是 Memcached。最后一個配置項 servers,用于設置資源池列表。

以 Memcached 訪問為例,將業(yè)務的 Memcached 資源部署好之后,然后將 Mc 資源列表、訪問方式等設到 YAML 文件的配置項,然后啟動 Twemproxy,業(yè)務端就可以通過訪問 Twemproxy ,來獲取后端資源的數(shù)據(jù)了。后續(xù),Mc 資源有任何變更,業(yè)務都不用做任何改變,運維直接修改 Twemproxy 的配置即可。

Twemproxy 在實際線的使用中,還是存在不少問題的。首先,它是單進程/單線程模型,一個 event_base 要處理所有的事件,這些事件包括 client 請求的讀入,轉發(fā)請求給后端 server,從 server 接受響應,以及將響應發(fā)送給 client。單個 Twemproxy 實例,壓測最大可以到 8w 左右的 QPS,出于線上穩(wěn)定性考慮,QPS 最多支撐到 3~4w。而 Memcached 的線上 QPS,一般可以達到 10~20w,一個 Mc 實例前面要掛 3~5 個 Twemproxy 實例。實例數(shù)太多,就會引發(fā)諸如管理復雜、成本過高等一系列問題。

其次,基于性能及預防單點故障的考慮,Twemproxy 需要進行多實例部署,而且還需要根據(jù)業(yè)務訪問量的變化,進行新實例的加入或冗余實例的下線。多個 Twemproxy 實例同時被訪問,如果 client 訪問策略不當,就會出現(xiàn)有些 Twemproxy 壓力過大,而有些卻很空閑,造成訪問不均的問題。

再次,后端資源在 Twemproxy 的 YAML 文件集中配置,資源變更的維護,比直接在所有業(yè)務 client 端維護,有了很大的簡化。但在多個 Twemproxy 修改配置,讓這些配置同時生效,也是一個復雜的工作。

最后,Twemproxy 也無法支持 Mc 多副本、多層次架構的訪問策略,無法支持 Redis 的Master-Slave 架構的讀寫分離訪問。

為此,你可以對 Twemproxy 進行擴展,以更好得滿足業(yè)務及運維的需要。
Twemproxy 擴展
多進程改造

性能首當其沖。首先可以對 Twemproxy 的單進程/單線程動刀,改為并行處理模型。并行方案可以用多線程方案,也可以采用多進程方案。由于 Twemproxy 只是一個消息路由中間件,不需要額外共享數(shù)據(jù),采用多進程方案會更簡潔,更適合。

多進程改造中,可以分別構建一個 master 進程和多個 worker 進程來進行任務處理,如下圖所示。每個進程維護自己獨立的 epoll 事件驅動。其中 master 進程,主要用于監(jiān)聽端口,accept 新連接,并將連接調(diào)度給 worker 進程。

img

而 worker 進程,基于自己獨立的 event_base,管理從 master 調(diào)度給自己的所有 client 連接。在 client 發(fā)送網(wǎng)絡請求到達時,進行命令讀取、解析,并在進程內(nèi)的 IO 隊列流轉,最后將請求打包,pipeline 給后端的 server。

在 server 處理完畢請求,發(fā)回響應時。對應 worker 進程,會讀取并解析響應,然后批量回復給 client。

通過多進程改造,Twemproxy 的 QPS 可以從 8w 提升到 40w+。業(yè)務訪問時,需要部署的Twemproxy 的實例數(shù)會大幅減少,運維會更加簡潔。
增加負載均衡

對于多個 Twemproxy 訪問,如何進行負載均衡的問題。一般有三種方案。

第一種方案,是在 Twemproxy 和業(yè)務訪問端之間,再增加一組 LVS,作為負載均衡層,通過 LVS 負載均衡層,你可以方便得增加或減少 Twemproxy 實例,由 LVS 負責負載均衡和請求分發(fā),如下圖。

img

第二種方案,是將 Twemproxy 的 IP 列表加入 DNS。業(yè)務 client 通過域名來訪問 Twemproxy,每次建連時,DNS 隨機返回一個 IP,讓連接盡可能均衡。

第三種方案,是業(yè)務 client 自定義均衡策略。業(yè)務 client 從配置中心或 DNS 獲取所有的Twemproxy 的 IP 列表,然后對這些 Twemproxy 進行均衡訪問,從而達到負載均衡。

方案一,可以通過成熟的 LVS 方案,高效穩(wěn)定的支持負載均衡策略,但多了一層,成本和運維的復雜度會有所增加。方案二,只能做到連接均衡,訪問請求是否均衡,無法保障。方案三,成本最低,性能也比前面 2 個方案更高效。推薦使用方案三,微博內(nèi)部也是采用第三種方案。
增加配置中心

對于 Twemproxy 配置的維護,可以通過增加一個配置中心服務來解決。將 YAML 配置文件中的所有配置信息,包括后端資源的部署信息、訪問信息,以配置的方式存儲到配置中心,如下圖。

img

Twemproxy 啟動時,首先到配置中心訂閱并拉取配置,然后解析并正常啟動。Twemproxy 將自己的 IP 和監(jiān)聽端口信息,也注冊到配置中心。業(yè)務 client 從配置中心,獲取Twemproxy 的部署信息,然后進行均衡訪問。

在后端資源變更時,直接更新配置中心的配置。配置中心會通知所有 Twemproxy 實例,收到事件通知,Twemproxy 即可拉取最新配置,并調(diào)整后端資源的訪問,實現(xiàn)在線變更。整個過程自動完成,更加高效和可靠。
支持 M-S-L1 多層訪問

前面提到,為了應對突發(fā)洪水流量,避免硬件局部故障的影響,對 Mc 訪問采用了Master-Slave-L1 架構??梢詫⒃摼彺婕軜嬻w系的訪問策略,封裝到 Twemproxy 內(nèi)部。實現(xiàn)方案也比較簡單。首先在 servers 配置中,增加 Master、Slave、L1 三層,如下圖。

img

Twemproxy 啟動時,每個 worker 進程預連所有的 Mc 后端,當收到 client 請求時,根據(jù)解析出來的指令,分別采用不同訪問策略即可。

對于 get 請求,首先隨機選擇一個 L1 來訪問,如果 miss,繼續(xù)訪問 Master 和 Slave。中間在任何一層命中,則回寫。對于 gets 請求,需要以 master 為準,從 master 讀取。如果 master 獲取失敗,則從 slave獲取,獲取后回種到 master,然后再次從 master 獲取,確保得到 cas unique id 來自 master。對于 add/cas 等請求,首先請求 master,成功后,再將 key/value 通過 set 指令,寫到 slave 和所有 L1。對于 set 請求,最簡單,直接 set 所有資源池即可。對于 stats 指令的響應,由 Twemproxy 自己統(tǒng)計,或者到后端 Mc 獲取后聚合獲得。

Redis 主從訪問

Redis 支持主從復制,為了支持更大并發(fā)訪問量,同時減少主庫的壓力,一般會部署多個從庫,寫操作直接請求 Redis 主庫,讀操作隨機選擇一個 Redis 從庫。這個邏輯同樣可以封裝在Twemproxy 中。如下圖所示,Redis 的主從配置信息,可以用域名的方式,也可以用 IP 端口的方式記錄在配置中心,由 Twemproxy 訂閱并實時更新,從而在 Redis 增減 slave、主從切換時,及時對后端進行訪問變更。

img

本課時,講解了大數(shù)據(jù)時代下大中型互聯(lián)網(wǎng)系統(tǒng)的特點,訪問 Memcached 緩存時的經(jīng)典問題及應對方案;還講解了如何通過分拆緩存池、Master-Slave 雙層架構,來解決 Memcached 的容量問題、性能瓶頸、連接瓶頸、局部故障的問題,以及 Master-Slave-L1 三層架構,通過多層、多副本 Memcached 體系,來更好得解決突發(fā)洪峰流量和局部故障的問題。

本節(jié)課重點學習了基于 Twemproxy 的應用系統(tǒng)架構方案,學習了 Twemproxy 的系統(tǒng)架構和關鍵技術,學習了 Twemproxy 的部署及配置信息。最后還學習了如何擴展 Twemproxy,從而使 Twemproxy 具有更好的性能、可用性和可運維性。

可以參考下面的思維導圖,對這些知識點進行回顧和梳理。

img

OK,這節(jié)課就講到這里啦,下一課時我將分享“Redis基本原理”,記得按時來聽課哈。好,下節(jié)課見,拜拜!

第六章:Redis原理、協(xié)議及使用

第16講:常用的緩存組件Redis是如何運行的?

你好,我是你的緩存課老師陳波,歡迎進入第 16 課時“Redis 基本原理”的學習。

Redis 基本原理
Redis 簡介

Redis 是一款基于 ANSI C 語言編寫的,BSD 許可的,日志型 key-value 存儲組件,它的所有數(shù)據(jù)結構都存在內(nèi)存中,可以用作緩存、數(shù)據(jù)庫和消息中間件。

Redis 是 Remote dictionary server 即遠程字典服務的縮寫,一個 Redis 實例可以有多個存儲數(shù)據(jù)的字典,客戶端可以通過 select 來選擇字典即 DB 進行數(shù)據(jù)存儲。
Redis 特性

同為 key-value 存儲組件,Memcached 只能支持二進制字節(jié)塊這一種數(shù)據(jù)類型。而 Redis 的數(shù)據(jù)類型卻豐富的多,它具有 8 種核心數(shù)據(jù)類型,每種數(shù)據(jù)類型都有一系列操作指令對應。Redis 性能很高,單線程壓測可以達到 10~11w 的 QPS。

雖然 Redis 所有數(shù)據(jù)的讀寫操作,都在內(nèi)存中進行,但也可以將所有數(shù)據(jù)進行落盤做持久化。Redis 提供了 2 種持久化方式。

快照方式,將某時刻所有數(shù)據(jù)都寫入硬盤的 RDB 文件;追加文件方式,即將所有寫命令都以追加的方式寫入硬盤的 AOF 文件中。

線上 Redis 一般會同時使用兩種方式,通過開啟 appendonly 及關聯(lián)配置項,將寫命令及時追加到 AOF 文件,同時在每日流量低峰時,通過 bgsave 保存當時所有內(nèi)存數(shù)據(jù)快照。

對于互聯(lián)網(wǎng)系統(tǒng)的線上流量,讀操作遠遠大于寫操作。以微博為例,讀請求占總體流量的 90%左右。大量的讀請求,通常會遠超 Redis 的可承載范圍。此時,可以使用 Redis 的復制特性,讓一個 Redis 實例作為 master,然后通過復制掛載多個不斷同步更新的副本,即多個 slave。通過讀寫分離,把所有寫操作落在 Redis 的 master,所有讀操作隨機落在 Redis 的多個 slave 中,從而大幅提升 Redis 的讀寫能力。

Lua 是一個高效、簡潔、易擴展的腳本語言,可以方便的嵌入其他語言中使用。Redis 自 2.6 版本開始支持 Lua。通過支持 client 端自定義的 Lua 腳本,Redis 可以減少網(wǎng)絡開銷,提升處理性能,還可以把腳本中的多個操作作為一個整體來操作,實現(xiàn)原子性更新。

Redis 還支持事務,在 multi 指令后,指定多個操作,然后通過 exec 指令一次性執(zhí)行,中途如果出現(xiàn)異常,則不執(zhí)行所有命令操作,否則,按順序一次性執(zhí)行所有操作,執(zhí)行過程中不會執(zhí)行任何其他指令。

Redis 還支持 Cluster 特性,可以通過自動或手動方式,將所有 key 按哈希分散到不同節(jié)點,在容量不足時,還可以通過 Redis 的遷移指令,把其中一部分 key 遷移到其他節(jié)點。

img

對于 Redis 的特性,可以通過這張思維導圖,做個初步了解。在后面的課程中,我會逐一進行詳細講解。

作為緩存組件,Redis 的最大優(yōu)勢是支持豐富的數(shù)據(jù)類型。目前,Redis 支持 8 種核心數(shù)據(jù)類型,包括 string、list、set、sorted set、hash、bitmap、geo、hyperloglog。

Redis 的所有內(nèi)存數(shù)據(jù)結構都存在全局的 dict 字典中,dict 類似 Memcached 的 hashtable。Redis 的 dict 也有 2 個哈希表,插入新 key 時,一般用 0 號哈希表,隨著 key 的插入或刪除,當 0 號哈希表的 keys 數(shù)大于哈希表桶數(shù),或 kyes 數(shù)小于哈希桶的 1/10 時,就對 hash 表進行擴縮。dict 中,哈希表解決沖突的方式,與 Memcached 相同,也是使用桶內(nèi)單鏈表,來指向多個 hash 相同的 key/value 數(shù)據(jù)。
Redis 高性能

Redis 一般被看作單進程/單線程組件,因為 Redis 的網(wǎng)絡 IO 和命令處理,都在核心進程中由單線程處理。Redis 基于 Epoll 事件模型開發(fā),可以進行非阻塞網(wǎng)絡 IO,同時由于單線程命令處理,整個處理過程不存在競爭,不需要加鎖,沒有上下文切換開銷,所有數(shù)據(jù)操作都是在內(nèi)存中操作,所以 Redis 的性能很高,單個實例即可以達到 10w 級的 QPS。核心線程除了負責網(wǎng)絡 IO 及命令處理外,還負責寫數(shù)據(jù)到緩沖,以方便將最新寫操作同步到 AOF、slave。

除了主進程,Redis 還會 fork 一個子進程,來進行重負荷任務的處理。Redis fork 子進程主要有 3 種場景。

收到 bgrewriteaof 命令時,Redis 調(diào)用 fork,構建一個子進程,子進程往臨時 AOF文件中,寫入重建數(shù)據(jù)庫狀態(tài)的所有命令,當寫入完畢,子進程則通知父進程,父進程把新增的寫操作也追加到臨時 AOF 文件,然后將臨時文件替換老的 AOF 文件,并重命名。收到 bgsave 命令時,Redis 構建子進程,子進程將內(nèi)存中的所有數(shù)據(jù)通過快照做一次持久化落地,寫入到 RDB 中。當需要進行全量復制時,master 也會啟動一個子進程,子進程將數(shù)據(jù)庫快照保存到 RDB 文件,在寫完 RDB 快照文件后,master 就會把 RDB 發(fā)給 slave,同時將后續(xù)新的寫指令都同步給 slave。

img

主進程中,除了主線程處理網(wǎng)絡 IO 和命令操作外,還有 3 個輔助 BIO 線程。這 3 個 BIO 線程分別負責處理,文件關閉、AOF 緩沖數(shù)據(jù)刷新到磁盤,以及清理對象這三個任務隊列。

Redis 在啟動時,會同時啟動這三個 BIO 線程,然后 BIO 線程休眠等待任務。當需要執(zhí)行相關類型的后臺任務時,就會構建一個 bio_job 結構,記錄任務參數(shù),然后將 bio_job 追加到任務隊列尾部。然后喚醒 BIO 線程,即可進行任務執(zhí)行。
Redis 持久化

Redis 的持久化是通過 RDB 和 AOF 文件進行的。RDB 只記錄某個時間點的快照,可以通過設置指定時間內(nèi)修改 keys 數(shù)的閥值,超過則自動構建 RDB 內(nèi)容快照,不過線上運維,一般會選擇在業(yè)務低峰期定期進行。RDB 存儲的是構建時刻的數(shù)據(jù)快照,內(nèi)存數(shù)據(jù)一旦落地,不會理會后續(xù)的變更。而 AOF,記錄是構建整個數(shù)據(jù)庫內(nèi)容的命令,它會隨著新的寫操作不斷進行追加操作。由于不斷追加,AOF 會記錄數(shù)據(jù)大量的中間狀態(tài),AOF 文件會變得非常大,此時,可以通過 bgrewriteaof 指令,對 AOF 進行重寫,只保留數(shù)據(jù)的最后內(nèi)容,來大大縮減 AOF 的內(nèi)容。

img

?

為了提升系統(tǒng)的可擴展性,提升讀操作的支撐能力,Redis 支持 master-slave 的復制功能。當 Redis 的 slave 部署并設置完畢后,slave 會和 master 建立連接,進行全量同步。

第一次建立連接,或者長時間斷開連接后,缺失的指令超過 master 復制緩沖區(qū)的大小,都需要先進行一次全量同步。全量同步時,master 會啟動一個子進程,將數(shù)據(jù)庫快照保存到文件中,然后將這個快照文件發(fā)給 slave,同時將快照之后的寫指令也同步給 slave。

全量同步完成后,如果 slave 短時間中斷,然后重連復制,缺少的寫指令長度小于 master 的復制緩沖大小,master 就會把 slave 缺失的內(nèi)容全部發(fā)送給 slave,進行增量復制。

Redis 的 master 可以掛載多個 slave,同時 slave 還可以繼續(xù)掛載 slave,通過這種方式,可以有效減輕 master 的壓力,同時在 master 掛掉后,可以在 slave 通過 slaveof no one 指令,使當前 slave 停止與 master 的同步,轉而成為新的 master。
Redis 集群管理

Redis 的集群管理有 3 種方式。

client 分片訪問,client 對 key 做 hash,然后按取?;蛞恢滦?hash,把 key 的讀寫分散到不同的 Redis 實例上。在 Redis 前加一個 proxy,把路由策略、后端 Redis 狀態(tài)維護的工作都放到 proxy 中進行,client 直接訪問 proxy,后端 Redis 變更,只需修改 proxy 配置即可。直接使用 Redis cluster。Redis 創(chuàng)建之初,使用方直接給 Redis 的節(jié)點分配 slot,后續(xù)訪問時,對 key 做 hash 找到對應的 slot,然后訪問 slot 所在的 Redis 實例。在需要擴容縮容時,可以在線通過 cluster setslot 指令,以及 migrate 指令,將 slot 下所有 key 遷移到目標節(jié)點,即可實現(xiàn)擴縮容的目的。

至此,Redis 的基本原理就講完了,相信你對 Redis 應該有了一個大概的了解。接下來,我將開始逐一深入分析 Redis 的各個技術細節(jié)。

OK,這節(jié)課就講到這里啦,下一課時我將分享“Redis 數(shù)據(jù)類型”,記得按時來聽課哈。好,下節(jié)課見,拜拜!

第17講:如何理解、選擇并使用Redis的核心數(shù)據(jù)類型?

你好,我是你的緩存課老師陳波,歡迎進入第 17 課時“Redis 數(shù)據(jù)類型”的學習。

Redis 數(shù)據(jù)類型

首先,來看一下 Redis 的核心數(shù)據(jù)類型。Redis 有 8 種核心數(shù)據(jù)類型,分別是 :

string 字符串類型;list 列表類型;set 集合類型;sorted set 有序集合類型;hash 類型;bitmap 位圖類型; geo 地理位置類型;HyperLogLog 基數(shù)統(tǒng)計類型。

string 字符串

string 是 Redis 的最基本數(shù)據(jù)類型??梢园阉斫鉃?Mc 中 key 對應的 value 類型。string 類型是二進制安全的,即 string 中可以包含任何數(shù)據(jù)。

Redis 中的普通 string 采用 raw encoding 即原始編碼方式,該編碼方式會動態(tài)擴容,并通過提前預分配冗余空間,來減少內(nèi)存頻繁分配的開銷。

在字符串長度小于 1MB 時,按所需長度的 2 倍來分配,超過 1MB,則按照每次額外增加 1MB 的容量來預分配。

Redis 中的數(shù)字也存為 string 類型,但編碼方式跟普通 string 不同,數(shù)字采用整型編碼,字符串內(nèi)容直接設為整數(shù)值的二進制字節(jié)序列。

在存儲普通字符串,序列化對象,以及計數(shù)器等場景時,都可以使用 Redis 的字符串類型,字符串數(shù)據(jù)類型對應使用的指令包括 set、get、mset、incr、decr 等。
list 列表

Redis 的 list 列表,是一個快速雙向鏈表,存儲了一系列的 string 類型的字串值。list 中的元素按照插入順序排列。插入元素的方式,可以通過 lpush 將一個或多個元素插入到列表的頭部,也可以通過 rpush 將一個或多個元素插入到隊列尾部,還可以通過 lset、linsert 將元素插入到指定位置或指定元素的前后。

list 列表的獲取,可以通過 lpop、rpop 從對頭或隊尾彈出元素,如果隊列為空,則返回 nil。還可以通過 Blpop、Brpop 從隊頭/隊尾阻塞式彈出元素,如果 list 列表為空,沒有元素可供彈出,則持續(xù)阻塞,直到有其他 client 插入新的元素。這里阻塞彈出元素,可以設置過期時間,避免無限期等待。最后,list 列表還可以通過 LrangeR 獲取隊列內(nèi)指定范圍內(nèi)的所有元素。Redis 中,list 列表的偏移位置都是基于 0 的下標,即列表第一個元素的下標是 0,第二個是 1。偏移量也可以是負數(shù),倒數(shù)第一個是 -1,倒數(shù)第二個是 -2,依次類推。

img

list 列表,對于常規(guī)的 pop、push 元素,性能很高,時間復雜度為 O(1),因為是列表直接追加或彈出。但對于通過隨機插入、隨機刪除,以及隨機范圍獲取,需要輪詢列表確定位置,性能就比較低下了。

feed timeline 存儲時,由于 feed id 一般是遞增的,可以直接存為 list,用戶發(fā)表新 feed,就直接追加到隊尾。另外消息隊列、熱門 feed 等業(yè)務場景,都可以使用 list 數(shù)據(jù)結構。

操作 list 列表時,可以用 lpush、lpop、rpush、rpop、lrange 來進行常規(guī)的隊列進出及范圍獲取操作,在某些特殊場景下,也可以用 lset、linsert 進行隨機插入操作,用 lrem 進行指定元素刪除操作;最后,在消息列表的消費時,還可以用 Blpop、Brpop 進行阻塞式獲取,從而在列表暫時沒有元素時,可以安靜的等待新元素的插入,而不需要額外持續(xù)的查詢。
set 集合

set 是 string 類型的無序集合,set 中的元素是唯一的,即 set 中不會出現(xiàn)重復的元素。Redis 中的集合一般是通過 dict 哈希表實現(xiàn)的,所以插入、刪除,以及查詢元素,可以根據(jù)元素 hash 值直接定位,時間復雜度為 O(1)。

對 set 類型數(shù)據(jù)的操作,除了常規(guī)的添加、刪除、查找元素外,還可以用以下指令對 set 進行操作。

sismember 指令判斷該 key 對應的 set 數(shù)據(jù)結構中,是否存在某個元素,如果存在返回 1,否則返回 0;sdiff 指令來對多個 set 集合執(zhí)行差集;sinter 指令對多個集合執(zhí)行交集;sunion 指令對多個集合執(zhí)行并集;spop 指令彈出一個隨機元素;srandmember 指令返回一個或多個隨機元素。

set 集合的特點是查找、插入、刪除特別高效,時間復雜度為 O(1),所以在社交系統(tǒng)中,可以用于存儲關注的好友列表,用來判斷是否關注,還可以用來做好友推薦使用。另外,還可以利用 set 的唯一性,來對服務的來源業(yè)務、來源 IP 進行精確統(tǒng)計。
sorted set 有序集合

Redis 中的 sorted set 有序集合也稱為 zset,有序集合同 set 集合類似,也是 string 類型元素的集合,且所有元素不允許重復。

但有序集合中,每個元素都會關聯(lián)一個 double 類型的 score 分數(shù)值。有序集合通過這個 score 值進行由小到大的排序。有序集合中,元素不允許重復,但 score 分數(shù)值卻允許重復。

有序集合除了常規(guī)的添加、刪除、查找元素外,還可以通過以下指令對 sorted set 進行操作。

zscan 指令:按順序獲取有序集合中的元素;zscore 指令:獲取元素的 score 值;zrange指令:通過指定 score 返回獲取 scpre 范圍內(nèi)的元素;在某個元素的 score 值發(fā)生變更時,還可以通過 zincrby 指令對該元素的 score 值進行加減。通過 zinterstore、zunionstore 指令對多個有序集合進行取交集和并集,然后將新的有序集合存到一個新的 key 中,如果有重復元素,重復元素的 score 進行相加,然后作為新集合中該元素的 score 值。

sorted set 有序集合的特點是:

所有元素按 score 排序,而且不重復;查找、插入、刪除非常高效,時間復雜度為 O(1)。

因此,可以用有序集合來統(tǒng)計排行榜,實時刷新榜單,還可以用來記錄學生成績,從而輕松獲取某個成績范圍內(nèi)的學生名單,還可以用來對系統(tǒng)統(tǒng)計增加權重值,從而在 dashboard 實時展示。
hash 哈希

Redis 中的哈希實際是 field 和 value 的一個映射表。

hash 數(shù)據(jù)結構的特點是在單個 key 對應的哈希結構內(nèi)部,可以記錄多個鍵值對,即 field 和 value 對,value 可以是任何字符串。而且這些鍵值對查詢和修改很高效。

所以可以用 hash 來存儲具有多個元素的復雜對象,然后分別修改或獲取這些元素。hash 結構中的一些重要指令,包括:hmset、hmget、hexists、hgetall、hincrby 等。

hmset 指令批量插入多個 field、value 映射;hmget 指令獲取多個 field 對應的 value 值;hexists 指令判斷某個 field 是否存在;如果 field 對應的 value 是整數(shù),還可以用 hincrby 來對該 value 進行修改。

bitmap 位圖

Redis 中的 bitmap 位圖是一串連續(xù)的二進制數(shù)字,底層實際是基于 string 進行封裝存儲的,按 bit 位進行指令操作的。bitmap 中每一 bit 位所在的位置就是 offset 偏移,可以用 setbit、bitfield 對 bitmap 中每個 bit 進行置 0 或置 1 操作,也可以用 bitcount 來統(tǒng)計 bitmap 中的被置 1 的 bit 數(shù),還可以用 bitop 來對多個 bitmap 進行求與、或、異或等操作。

img

bitmap 位圖的特點是按位設置、求與、求或等操作很高效,而且存儲成本非常低,用來存對象標簽屬性的話,一個 bit 即可存一個標簽??梢杂?bitmap,存用戶最近 N 天的登錄情況,每天用 1 bit,登錄則置 1。個性推薦在社交應用中非常重要,可以對新聞、feed 設置一系列標簽,如軍事、娛樂、視頻、圖片、文字等,用 bitmap 來存儲這些標簽,在對應標簽 bit 位上置 1。對用戶,也可以采用類似方式,記錄用戶的多種屬性,并可以很方便的根據(jù)標簽來進行多維度統(tǒng)計。bitmap 位圖的重要指令包括:setbit、 getbit、bitcount、bitfield、 bitop、bitpos 等。

在移動社交時代,LBS 應用越來越多,比如微信、陌陌中附近的人,美團、大眾點評中附近的美食、電影院,滴滴、優(yōu)步中附近的專車等。要實現(xiàn)這些功能,就得使用地理位置信息進行搜索。地球的地理位置是使用二維的經(jīng)緯度進行表示的,我們只要確定一個點的經(jīng)緯度,就可以確認它在地球的位置。

Redis 在 3.2 版本之后增加了對 GEO 地理位置的處理功能。Redis 的 GEO 地理位置本質(zhì)上是基于 sorted set 封裝實現(xiàn)的。在存儲分類 key 下的地理位置信息時,需要對該分類 key 構建一個 sorted set 作為內(nèi)部存儲結構,用于存儲一系列位置點。

在存儲某個位置點時,首先利用 Geohash 算法,將該位置二維的經(jīng)緯度,映射編碼成一維的 52 位整數(shù)值,將位置名稱、經(jīng)緯度編碼 score 作為鍵值對,存儲到分類 key 對應的 sorted set 中。

需要計算某個位置點 A 附近的人時,首先以指定位置 A 為中心點,以距離作為半徑,算出 GEO 哈希 8 個方位的范圍, 然后依次輪詢方位范圍內(nèi)的所有位置點,只要這些位置點到中心位置 A 的距離在要求距離范圍內(nèi),就是目標位置點。輪詢完所有范圍內(nèi)的位置點后,重新排序即得到位置點 A 附近的所有目標。

使用 geoadd,將位置名稱(如人、車輛、店名)與對應的地理位置信息添加到指定的位置分類 key 中;使用 geopos 方便地查詢某個名稱所在的位置信息;使用 georadius 獲取指定位置附近,不超過指定距離的所有元素;使用 geodist 來獲取指定的兩個位置之間的距離。

這樣,是不是就可以實現(xiàn),找到附近的餐廳,算出當前位置到對應餐廳的距離,這樣的功能了?

Redis GEO 地理位置,利用 Geohash 將大量的二維經(jīng)緯度轉一維的整數(shù)值,這樣可以方便的對地理位置進行查詢、距離測量、范圍搜索。但由于地理位置點非常多,一個地理分類 key 下可能會有大量元素,在 GEO 設計時,需要提前進行規(guī)劃,避免單 key 過度膨脹。

Redis 的 GEO 地理位置數(shù)據(jù)結構,應用場景很多,比如查詢某個地方的具體位置,查當前位置到目的地的距離,查附近的人、餐廳、電影院等。GEO 地理位置數(shù)據(jù)結構中,重要指令包括 geoadd、geopos、geodist、georadius、georadiusbymember 等。
hyperLogLog 基數(shù)統(tǒng)計

Redis 的 hyperLogLog 是用來做基數(shù)統(tǒng)計的數(shù)據(jù)類型,當輸入巨大數(shù)量的元素做統(tǒng)計時,只需要很小的內(nèi)存即可完成。HyperLogLog 不保存元數(shù)據(jù),只記錄待統(tǒng)計元素的估算數(shù)量,這個估算數(shù)量是一個帶有 0.81% 標準差的近似值,在大多數(shù)業(yè)務場景,對海量數(shù)據(jù),不足 1% 的誤差是可以接受的。

Redis 的 HyperLogLog 在統(tǒng)計時,如果計數(shù)數(shù)量不大,采用稀疏矩陣存儲,隨著計數(shù)的增加,稀疏矩陣占用的空間也會逐漸增加,當超過閥值后,則改為稠密矩陣,稠密矩陣占用的空間是固定的,約為12KB字節(jié)。

通過 hyperLoglog 數(shù)據(jù)類型,你可以利用 pfadd 向基數(shù)統(tǒng)計中增加新的元素,可以用 pfcount 獲得 hyperLogLog 結構中存儲的近似基數(shù)數(shù)量,還可以用 hypermerge 將多個 hyperLogLog 合并為一個 hyperLogLog 結構,從而可以方便的獲取合并后的基數(shù)數(shù)量。

hyperLogLog 的特點是統(tǒng)計過程不記錄獨立元素,占用內(nèi)存非常少,非常適合統(tǒng)計海量數(shù)據(jù)。在大中型系統(tǒng)中,統(tǒng)計每日、每月的 UV 即獨立訪客數(shù),或者統(tǒng)計海量用戶搜索的獨立詞條數(shù),都可以用 hyperLogLog 數(shù)據(jù)類型來進行處理。

OK,這節(jié)課就講到這里啦,下一課時我將分享“Redis 協(xié)議分析”,記得按時來聽課哈。好,下節(jié)課見,拜拜!

第18講:Redis協(xié)議的請求和響應有哪些“套路”可循?
2019/10/14 陳波
8.3M
00:00/12:01
看視頻

你好,我是你的緩存課老師陳波,歡迎進入第 18 課時“Redis 協(xié)議分析”的學習,本課時主要學習Redis的設計原則、三種響應模式、2種請求格式、5種響應格式。
Redis 協(xié)議

Redis 支持 8 種核心數(shù)據(jù)結構,每種數(shù)據(jù)結構都有一系列的操作指令,除此之外,Redis 還有事務、集群、發(fā)布訂閱、腳本等一系列相關的指令。為了方便以一種統(tǒng)一的風格和原則來設計和使用這些指令,Redis 設計了 RESP,即 Redis Serialization Protocol,中文意思是 Redis 序列化協(xié)議。RESP 是二進制安全協(xié)議,可以供 Redis 或其他任何 Client-Server 使用。在 Redis 內(nèi)部,還會基于 RESP 進一步擴展細節(jié)。
設計原則

Redis 序列化協(xié)議的設計原則有三個:

第一是實現(xiàn)簡單;第二是可快速解析;第三是便于閱讀。

Redis 協(xié)議的請求響應模型有三種,除了 2 種特殊模式,其他基本都是 ping-pong 模式,即 client 發(fā)送一個請求,server 回復一個響應,一問一答的訪問模式。

2 種特殊模式:

pipeline 模式,即 client 一次連續(xù)發(fā)送多個請求,然后等待 server 響應,server 處理完請求后,把響應返回給 client。pub/sub 模式。即發(fā)布訂閱模式,client 通過 subscribe 訂閱一個 channel,然后 client 進入訂閱狀態(tài),靜靜等待。當有消息產(chǎn)生時,server 會持續(xù)自動推送消息給 client,不需要 client 的額外請求。而且客戶端在進入訂閱狀態(tài)后,只可接受訂閱相關的命令如 SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE 和 PUNSUBSCRIBE,除了這些命令,其他命令一律失效。

Redis 協(xié)議的請求和響應也是有固定套路的。

對于請求指令,格式有 2 種類型。

當你沒有 redis-client,但希望可以用通用工具 telnet,直接與 Redis 交互時,Redis 協(xié)議雖然簡單易于閱讀,但在交互式會話中使用,并不容易拼寫,此時可以用第一種格式,即 inline cmd 內(nèi)聯(lián)命令格式。使用 inline cmd 內(nèi)聯(lián)格式,只需要用空格分隔請求指令及參數(shù),簡單快速,一個簡單的例子如 mget key1 key2\r\n。第二種格式是 Array 數(shù)組格式類型。請求指令用的數(shù)組類型,與 Redis 響應的數(shù)組類型相同,后面在介紹響應格式類型時會詳細介紹。

響應格式

Redis 協(xié)議的響應格式有 5 種,分別是:

simple strings 簡單字符串類型,以 + 開頭,后面跟字符串,以 CRLF(即 \r\n)結尾。這種類型不是二進制安全類型,字符串中不能包含 \r 或者 \n。比如許多響應回復以 OK 作為操作成功的標志,協(xié)議內(nèi)容就是 +OK\r\rn 。Redis 協(xié)議將錯誤作為一種專門的類型,格式同簡單字符串類型,唯一不同的是以 -(減號)開頭。Redis 內(nèi)部實現(xiàn)對 Redis 協(xié)議做了進一步規(guī)范,減號后面一般先跟 ERR 或者 WRONGTYPE,然后再跟其他簡單字符串,最后以 CRLF(回車換行)結束。這里給了兩個示例,client 在解析響應時,一旦發(fā)現(xiàn) - 開頭,就知道收到 Error 響應。Integer 整數(shù)類型。整數(shù)類型以 :開頭,后面跟字符串表示的數(shù)字,最后以回車換行結尾。Redis 中許多命令都返回整數(shù),但整數(shù)的含義要由具體命令來確定。比如,對于 incr 指令,:后的整數(shù)表示變更后的數(shù)值;對于 llen 表示 list 列表的長度,對于 exists 指令,1 表示 key 存在,0 表示 key 不存在。這里給個例子,:后面跟了個 1000,然后回車換行結束。bulk strings 字符串塊類型。字符串塊分頭部和真正字符串內(nèi)容兩部分。字符串塊類型的頭部, 為 $ 開頭,隨后跟真正字符串內(nèi)容的字節(jié)長度,然后以 CRLF 結尾。字符串塊的頭部之后,跟隨真正的字符串內(nèi)容,最后以 CRLF 結束字符串塊。字符串塊用于表示二進制安全的字符串,最大長度可以支持 512MB。一個常規(guī)的例子,“$6\r\nfoobar\r\n”,對于空字串,可以表示為 “$0\r\n\r\n”,NULL字串: “$-1\r\n”。Arrays 數(shù)組類型,如果一個命令需要返回多條數(shù)據(jù)就需要用數(shù)組格式類型,另外,前面提到 client 的請求命令也是主要采用這種格式。

Arrays 數(shù)組類型,以 * 開頭,隨后跟一個數(shù)組長度 N,然后以回車換行結尾;然后后面跟隨 N 個數(shù)組元素,每個數(shù)組元素的類型,可以是 Redis 協(xié)議中除內(nèi)聯(lián)格式外的任何一種類型。

比如一個字符串塊的數(shù)組實例,*2\r\n$3\r\nget\r\n$3\r\nkey\r\n。整數(shù)數(shù)組實例:”*3\r\n:1\r\n:2\r\n:3\r\n",混合數(shù)組實例:"*3\r\n :1\r\n-Bar\r\n$6\r\n foobar\r\n”,空數(shù)組:”0\r\n”,NULL數(shù)組:”-1\r\n”。
協(xié)議分類

Redis 協(xié)議主要分為 16 種,其中 8 種協(xié)議對應前面我們講到的 8 種數(shù)據(jù)類型,你選擇了使用什么數(shù)據(jù)類型,就使用對應的響應操作指令即可。剩下 8 種協(xié)議如下所示。

pub-sub 發(fā)布訂閱協(xié)議,client 可以訂閱 channel,持續(xù)等待 server 推送消息。事務協(xié)議,事務協(xié)議可以用 multi 和 exec 封裝一些列指令,來一次性執(zhí)行。腳本協(xié)議,關鍵指令是 eval、evalsha 和 script等。連接協(xié)議,主要包括權限控制,切換 DB,關閉連接等。復制協(xié)議,包括 slaveof、role、psync 等。配置協(xié)議,config set/get 等,可以在線修改/獲取配置。調(diào)試統(tǒng)計協(xié)議,如 slowlog,monitor,info 等。其他內(nèi)部命令,如 migrate,dump,restore 等。

Redis client 的使用及改進

由于 Redis 使用廣泛,幾乎所有主流語言都有對 Redis 開發(fā)了對應的 client。以 Java 語言為例,廣泛使用的有 Jedis、Redisson 等。對于 Jedis client,它的優(yōu)勢是輕量,簡潔,便于集成和改造,它支持連接池,提供指令維度的操作,幾乎支持 Redis 的所有指令,但它不支持讀寫分離。Redisson 基于 Netty 實現(xiàn),非阻塞 IO,性能較高,而且支持異步請求和連接池,還支持讀寫分離、讀負載均衡,它內(nèi)建了 tomcat Session ,支持 spring session 集成,但 redisson 實現(xiàn)相對復雜。

在新項目啟動時,如果只是簡單的 Redis 訪問業(yè)務場景,可以直接用 Jedis,甚至可以簡單封裝 Jedis,實現(xiàn) master-slave 的讀寫分離方案。如果想直接使用讀寫分離,想集成 spring session 等這些高級特性,也可以采用 redisson。

Redis client 在使用中,需要根據(jù)業(yè)務及運維的需要,進行相關改進。在 client 訪問異常時,可以增加重試策略,在訪問某個 slave 異常時,需要重試其他 slave 節(jié)點。需要增加對 Redis 主從切換、slave 擴展的支持,比如采用守護線程定期掃描 master、slave 域名,發(fā)現(xiàn) IP 變更,及時切換連接。對于多個 slave 的訪問,還需要增加負載均衡策略。最后,Redis client 還可以與配置中心、Redis 集群管理平臺整合,從而實時感知及協(xié)調(diào) Redis 服務的訪問。

至此,本節(jié)課的內(nèi)容就講完了。

在這幾節(jié)課中,你首先學習了 Redis 的特性及基本原理,初步了解了 Redis 的數(shù)據(jù)類型、主進程/子進程、BIO 線程、持久化、復制、集群等;這些內(nèi)容會在后續(xù)逐一深入學習。

然后,詳細學習了 Redis 的數(shù)據(jù)類型,了解了字符串、列表、集合、有序集合、哈希、位圖、GEO 地理位置、HyperLogLog 基數(shù)統(tǒng)計,這 8 種核心數(shù)據(jù)類型的功能、特點、主要操作指令及應用場景。

接下來,你還熟悉了 Redis 協(xié)議,包括 Redis 協(xié)議的設計原則、三種響應模型,2 種請求格式和 5 種響應格式。

最后,以 Java 語言為例,你還了解了 Redis client 的對比、選擇及改進。

你可以參考這個思維導圖,對這些知識點進行回顧和梳理。

img

OK,這節(jié)課就講到這里啦,下一課時我將分享“Redis 系統(tǒng)架構”,記得按時來聽課哈。好,下節(jié)課見,拜拜!

第七章:Redis進階(上)

第19講:Redis系統(tǒng)架構中各個處理模塊是干什么的?

你好,我是你的緩存課老師陳波,歡迎進入第 19 課時“Redis 系統(tǒng)架構”的學習。
Redis 系統(tǒng)架構

通過前面的學習,相信你已經(jīng)掌握了 Redis 的原理、數(shù)據(jù)類型及訪問協(xié)議等內(nèi)容。本課時,我將進一步分析 Redis 的系統(tǒng)架構,重點講解 Redis 系統(tǒng)架構的事件處理機制、數(shù)據(jù)管理、功能擴展、系統(tǒng)擴展等內(nèi)容。
事件處理機制

Redis 組件的系統(tǒng)架構如圖所示,主要包括事件處理、數(shù)據(jù)存儲及管理、用于系統(tǒng)擴展的主從復制/集群管理,以及為插件化功能擴展的 Module System 模塊。

img

Redis 中的事件處理模塊,采用的是作者自己開發(fā)的 ae 事件驅動模型,可以進行高效的網(wǎng)絡 IO 讀寫、命令執(zhí)行,以及時間事件處理。

其中,網(wǎng)絡 IO 讀寫處理采用的是 IO 多路復用技術,通過對 evport、epoll、kqueue、select 等進行封裝,同時監(jiān)聽多個 socket,并根據(jù) socket 目前執(zhí)行的任務,來為 socket 關聯(lián)不同的事件處理器。

當監(jiān)聽端口對應的 socket 收到連接請求后,就會創(chuàng)建一個 client 結構,通過 client 結構來對連接狀態(tài)進行管理。在請求進入時,將請求命令讀取緩沖并進行解析,并存入到 client 的參數(shù)列表。

然后根據(jù)請求命令找到 對應的redisCommand ,最后根據(jù)命令協(xié)議,對請求參數(shù)進一步的解析、校驗并執(zhí)行。Redis 中時間事件比較簡單,目前主要是執(zhí)行 serverCron,來做一些統(tǒng)計更新、過期 key 清理、AOF 及 RDB 持久化等輔助操作。
數(shù)據(jù)管理

Redis 的內(nèi)存數(shù)據(jù)都存在 redisDB 中。Redis 支持多 DB,每個 DB 都對應一個 redisDB 結構。Redis 的 8 種數(shù)據(jù)類型,每種數(shù)據(jù)類型都采用一種或多種內(nèi)部數(shù)據(jù)結構進行存儲。同時這些內(nèi)部數(shù)據(jù)結構及數(shù)據(jù)相關的輔助信息,都以 kye/value 的格式存在 redisDB 中的各個 dict 字典中。

數(shù)據(jù)在寫入 redisDB 后,這些執(zhí)行的寫指令還會及時追加到 AOF 中,追加的方式是先實時寫入AOF 緩沖,然后按策略刷緩沖數(shù)據(jù)到文件。由于 AOF 記錄每個寫操作,所以一個 key 的大量中間狀態(tài)也會呈現(xiàn)在 AOF 中,導致 AOF 冗余信息過多,因此 Redis 還設計了一個 RDB 快照操作,可以通過定期將內(nèi)存里所有的數(shù)據(jù)快照落地到 RDB 文件,來以最簡潔的方式記錄 Redis 的所有內(nèi)存數(shù)據(jù)。

Redis 進行數(shù)據(jù)讀寫的核心處理線程是單線程模型,為了保持整個系統(tǒng)的高性能,必須避免任何kennel 導致阻塞的操作。為此,Redis 增加了 BIO 線程,來處理容易導致阻塞的文件 close、fsync 等操作,確保系統(tǒng)處理的性能和穩(wěn)定性。

在 server 端,存儲內(nèi)存永遠是昂貴且短缺的,Redis 中,過期的 key 需要及時清理,不活躍的 key 在內(nèi)存不足時也可能需要進行淘汰。為此,Redis 設計了 8 種淘汰策略,借助新引入的 eviction pool,進行高效的 key 淘汰和內(nèi)存回收。
功能擴展

Redis 在 4.0 版本之后引入了 Module System 模塊,可以方便使用者,在不修改核心功能的同時,進行插件化功能開發(fā)。使用者可以將新的 feature 封裝成動態(tài)鏈接庫,Redis 可以在啟動時加載,也可以在運行過程中隨時按需加載和啟用。

在擴展模塊中,開發(fā)者可以通過 RedisModule_init 初始化新模塊,用 RedisModule_CreateCommand 擴展各種新模塊指令,以可插拔的方式為 Redis 引入新的數(shù)據(jù)結構和訪問命令。
系統(tǒng)擴展

Redis作者在架構設計中對系統(tǒng)的擴展也傾注了大量關注。在主從復制功能中,psyn 在不斷的優(yōu)化,不僅在 slave 閃斷重連后可以進行增量復制,而且在 slave 通過主從切換成為 master 后,其他 slave 仍然可以與新晉升的 master 進行增量復制,另外,其他一些場景,如 slave 重啟后,也可以進行增量復制,大大提升了主從復制的可用性。使用者可以更方便的使用主從復制,進行業(yè)務數(shù)據(jù)的讀寫分離,大幅提升 Redis 系統(tǒng)的穩(wěn)定讀寫能力。

通過主從復制可以較好的解決 Redis 的單機讀寫問題,但所有寫操作都集中在 master 服務器,很容易達到 Redis 的寫上限,同時 Redis 的主從節(jié)點都保存了業(yè)務的所有數(shù)據(jù),隨著業(yè)務發(fā)展,很容易出現(xiàn)內(nèi)存不夠用的問題。

為此,Redis 分區(qū)無法避免。雖然業(yè)界大多采用在 client 和 proxy 端分區(qū),但 Redis 自己也早早推出了 cluster 功能,并不斷進行優(yōu)化。Redis cluster 預先設定了 16384 個 slot 槽,在 Redis 集群啟動時,通過手動或自動將這些 slot 分配到不同服務節(jié)點上。在進行 key 讀寫定位時,首先對 key 做 hash,并將 hash 值對 16383 ,做 按位與運算,確認 slot,然后確認服務節(jié)點,最后再對 對應的 Redis 節(jié)點,進行常規(guī)讀寫。如果 client 發(fā)送到錯誤的 Redis 分片,Redis 會發(fā)送重定向回復。如果業(yè)務數(shù)據(jù)大量增加,Redis 集群可以通過數(shù)據(jù)遷移,來進行在線擴容。

OK,這節(jié)課就講到這里啦,下一課時我將重點講解“Redis 的事件驅動模型”,記得按時來聽課哈。好,下節(jié)課見,拜拜!

###?第20講:Redis如何處理文件事件和時間事件?

上一課時,我們學習了 Redis 的系統(tǒng)架構,接下來的幾個課時我將帶你一起對這些模塊和設計進行詳細分析。首先,我將分析 Redis 的事件驅動模型。
Redis 事件驅動模型
事件驅動模型

Redis 是一個事件驅動程序,但和 Memcached 不同的是,Redis 并沒有采用 libevent 或 libev 這些開源庫,而是直接開發(fā)了一個新的事件循環(huán)組件。Redis 作者給出的理由是,盡量減少外部依賴,而自己開發(fā)的事件模型也足夠簡潔、輕便、高效,也更易控制。Redis 的事件驅動模型機制封裝在 aeEventLoop 等相關的結構體中,網(wǎng)絡連接、命令讀取執(zhí)行回復,數(shù)據(jù)的持久化、淘汰回收 key 等,幾乎所有的核心操作都通過 ae 事件模型進行處理。

img

Redis 的事件驅動模型處理 2 類事件:

文件事件,如連接建立、接受請求命令、發(fā)送響應等;時間事件,如 Redis 中定期要執(zhí)行的統(tǒng)計、key 淘汰、緩沖數(shù)據(jù)寫出、rehash等。

文件事件處理

img

Redis 的文件事件采用典型的 Reactor 模式進行處理。Redis 文件事件處理機制分為 4 部分:

連接 socketIO 多路復用程序文件事件分派器事件處理器

文件事件是對連接 socket 操作的一個抽象。當端口監(jiān)聽 socket 準備 accept 新連接,或者連接 socket 準備好讀取請求、寫入響應、關閉時,就會產(chǎn)生一個文件事件。IO 多路復用程序負責同時監(jiān)聽多個 socket,當這些 socket 產(chǎn)生文件事件時,就會觸發(fā)事件通知,文件分派器就會感知并獲取到這些事件。

雖然多個文件事件可能會并發(fā)出現(xiàn),但 IO 多路復用程序總會將所有產(chǎn)生事件的 socket 放入一個隊列中,通過這個隊列,有序的把這些文件事件通知給文件分派器。
IO多路復用

Redis 封裝了 4 種多路復用程序,每種封裝實現(xiàn)都提供了相同的 API 實現(xiàn)。編譯時,會按照性能和系統(tǒng)平臺,選擇最佳的 IO 多路復用函數(shù)作為底層實現(xiàn),選擇順序是,首先嘗試選擇 Solaries 中的 evport,如果沒有,就嘗試選擇 Linux 中的 epoll,否則就選擇大多 UNIX 系統(tǒng)都支持的 kqueue,這 3 個多路復用函數(shù)都直接使用系統(tǒng)內(nèi)核內(nèi)部的結構,可以服務數(shù)十萬的文件描述符。

如果當前編譯環(huán)境沒有上述函數(shù),就會選擇 select 作為底層實現(xiàn)方案。select 方案的性能較差,事件發(fā)生時,會掃描全部監(jiān)聽的描述符,事件復雜度是 O(n),并且只能同時服務有限個文件描述符,32 位機默認是 1024 個,64 位機默認是 2048 個,所以一般情況下,并不會選擇 select 作為線上運行方案。Redis 的這 4 種實現(xiàn),分別在 ae_evport、ae_epoll、ae_kqueue 和 ae_select 這 4 個代碼文件中。
文件事件收集及派發(fā)器

Redis 中的文件事件分派器是 aeProcessEvents 函數(shù)。它會首先計算最大可以等待的時間,然后利用 aeApiPoll 等待文件事件的發(fā)生。如果在等待時間內(nèi),一旦 IO 多路復用程序產(chǎn)生了事件通知,則會立即輪詢所有已產(chǎn)生的文件事件,并將文件事件放入 aeEventLoop 中的 aeFiredEvents 結構數(shù)組中。每個 fired event 會記錄 socket 及 Redis 讀寫事件類型。

這里會涉及將多路復用中的事件類型,轉換為 Redis 的 ae 事件驅動模型中的事件類型。以采用 Linux 中的 epoll 為例,會將 epoll 中的 EPOLLIN 轉為 AE_READABLE 類型,將 epoll 中的 EPOLLOUT、EPOLLERR 和 EPOLLHUP 轉為 AE_WRITABLE 事件。

aeProcessEvents 在獲取到觸發(fā)的事件后,會根據(jù)事件類型,將文件事件 dispatch 派發(fā)給對應事件處理函數(shù)。如果同一個 socket,同時有讀事件和寫事件,Redis 派發(fā)器會首先派發(fā)處理讀事件,然后再派發(fā)處理寫事件。
文件事件處理函數(shù)分類

Redis 中文件事件函數(shù)的注冊和處理主要分為 3 種。

連接處理函數(shù) acceptTcpHandler

Redis 在啟動時,在 initServer 中對監(jiān)聽的 socket 注冊讀事件,事件處理器為 acceptTcpHandler,該函數(shù)在有新連接進入時,會被派發(fā)器派發(fā)讀任務。在處理該讀任務時,會 accept 新連接,獲取調(diào)用方的 IP 及端口,并對新連接創(chuàng)建一個 client 結構。如果同時有大量連接同時進入,Redis 一次最多處理 1000 個連接請求。

readQueryFromClient 請求處理函數(shù)

連接函數(shù)在創(chuàng)建 client 時,會對新連接 socket 注冊一個讀事件,該讀事件的事件處理器就是 readQueryFromClient。在連接 socket 有請求命令到達時,IO 多路復用程序會獲取并觸發(fā)文件事件,然后這個讀事件被派發(fā)器派發(fā)給本請求的處理函數(shù)。readQueryFromClient 會從連接 socket 讀取數(shù)據(jù),存入 client 的 query 緩沖,然后進行解析命令,按照 Redis 當前支持的 2 種請求格式,及 inline 內(nèi)聯(lián)格式和 multibulk 字符塊數(shù)組格式進行嘗試解析。解析完畢后,client 會根據(jù)請求命令從命令表中獲取到對應的 redisCommand,如果對應 cmd 存在。則開始校驗請求的參數(shù),以及當前 server 的內(nèi)存、磁盤及其他狀態(tài),完成校驗后,然后真正開始執(zhí)行 redisCommand 的處理函數(shù),進行具體命令的執(zhí)行,最后將執(zhí)行結果作為響應寫入 client 的寫緩沖中。

命令回復處理器 sendReplyToClient

當 redis需要發(fā)送響應給client時,Redis 事件循環(huán)中會對client的連接socket注冊寫事件,這個寫事件的處理函數(shù)就是sendReplyToClient。通過注冊寫事件,將 client 的socket與 AE_WRITABLE 進行間接關聯(lián)。當 Client fd 可進行寫操作時,就會觸發(fā)寫事件,該函數(shù)就會將寫緩沖中的數(shù)據(jù)發(fā)送給調(diào)用方。

img

Redis 中的時間事件是指需要在特定時間執(zhí)行的事件。多個 Redis 中的時間事件構成 aeEventLoop 中的一個鏈表,供 Redis 在 ae 事件循環(huán)中輪詢執(zhí)行。

Redis 當前的主要時間事件處理函數(shù)有 2 個:

serverCronmoduleTimerHandler

Redis 中的時間事件分為 2 類:

單次時間,即執(zhí)行完畢后,該時間事件就結束了。周期性事件,在事件執(zhí)行完畢后,會繼續(xù)設置下一次執(zhí)行的事件,從而在時間到達后繼續(xù)執(zhí)行,并不斷重復。

時間事件主要有 5 個屬性組成。

事件 ID:Redis 為時間事件創(chuàng)建全局唯一 ID,該 ID 按從小到大的順序進行遞增。執(zhí)行時間 when_sec 和 when_ms:精確到毫秒,記錄該事件的到達可執(zhí)行時間。時間事件處理器 timeProc:在時間事件到達時,Redis 會調(diào)用相應的 timeProc 處理事件。關聯(lián)數(shù)據(jù) clientData:在調(diào)用 timeProc 時,需要使用該關聯(lián)數(shù)據(jù)作為參數(shù)。鏈表指針 prev 和 next:它用來將時間事件維護為雙向鏈表,便于插入及查找所要執(zhí)行的時間事件。

時間事件的處理是在事件循環(huán)中的 aeProcessEvents 中進行。執(zhí)行過程是:

首先遍歷所有的時間事件。比較事件的時間和當前時間,找出可執(zhí)行的時間事件。然后執(zhí)行時間事件的 timeProc 函數(shù)。執(zhí)行完畢后,對于周期性時間,設置時間新的執(zhí)行時間;對于單次性時間,設置事件的 ID為 -1,后續(xù)在事件循環(huán)中,下一次執(zhí)行 aeProcessEvents 的時候從鏈表中刪除。

第21講:Redis讀取請求數(shù)據(jù)后,如何進行協(xié)議解析和處理?

你好,我是你的緩存課老師陳波,歡迎進入第 21 課時“Redis 協(xié)議解析及處理”的學習。上一課時,我們學習了 Redis 事件驅動模型,接下來,看一下 Redis 是如何進行協(xié)議解析及處理的。

Redis 協(xié)議解析及處理
協(xié)議解析

上一課時講到,請求命令進入,觸發(fā) IO 讀事件后。client 會從連接文件描述符讀取請求,并存入 client 的 query buffer 中。client 的讀緩沖默認是 16KB,讀取命令時,如果發(fā)現(xiàn)請求超過 1GB,則直接報異常,關閉連接。

img

client 讀取完請求命令后,則根據(jù) query buff 進行協(xié)議解析。協(xié)議解析時,首先查看協(xié)議的首字符。如果是 *,則解析為字符塊數(shù)組類型,即 MULTIBULK。否則請求解析為 INLINE 類型。

INLINE 類型是以 CRLF 結尾的單行字符串,協(xié)議命令及參數(shù)以空格分隔。解析過程參考之前課程里分析的對應協(xié)議格式。協(xié)議解析完畢后,將請求參數(shù)個數(shù)存入 client 的 argc 中,將請求的具體參數(shù)存入 client 的 argv 中。
協(xié)議執(zhí)行

請求命令解析完畢,則進入到協(xié)議執(zhí)行部分。協(xié)議執(zhí)行中,對于 quit 指令,直接返回 OK,設置 flag 為回復后關閉連接。

img

對于非 quit 指令,以 client 中 argv[0] 作為命令,從 server 中的命令表中找到對應的 redisCommand。如果沒有找到 redisCommand,則返回未知 cmd 異常。如果找到 cmd,則開始執(zhí)行 redisCommand 中的 proc 函數(shù),進行具體命令的執(zhí)行。在命令執(zhí)行完畢后,將響應寫入 client 的寫緩沖。并按配置和部署,將寫指令分發(fā)給 aof 和 slaves。同時更新相關的統(tǒng)計數(shù)值。

第22講:怎么認識和應用Redis內(nèi)部數(shù)據(jù)結構?

上一課時,我們學習了 Redis 協(xié)議解析及處理,接下來,看一下 Redis 的內(nèi)部數(shù)據(jù)結構是什么樣的?
Redis 內(nèi)部數(shù)據(jù)結構
RdeisDb

img

Redis 中所有數(shù)據(jù)都保存在 DB 中,一個 Redis 默認最多支持 16 個 DB。Redis 中的每個 DB 都對應一個 redisDb 結構,即每個 Redis 實例,默認有 16 個 redisDb。用戶訪問時,默認使用的是 0 號 DB,可以通過 select $dbID 在不同 DB 之間切換。

img

redisDb 主要包括 2 個核心 dict 字典、3 個非核心 dict 字典、dbID 和其他輔助屬性。2 個核心 dict 包括一個 dict 主字典和一個 expires 過期字典。主 dict 字典用來存儲當前 DB 中的所有數(shù)據(jù),它將 key 和各種數(shù)據(jù)類型的 value 關聯(lián)起來,該 dict 也稱 key space。過期字典用來存儲過期時間 key,存的是 key 與過期時間的映射。日常的數(shù)據(jù)存儲和訪問基本都會訪問到 redisDb 中的這兩個 dict。

3 個非核心 dict 包括一個字段名叫 blocking_keys 的阻塞 dict,一個字段名叫 ready_keys 的解除阻塞 dict,還有一個是字段名叫 watched_keys 的 watch 監(jiān)控 dict。

在執(zhí)行 Redis 中 list 的阻塞命令 blpop、brpop 或者 brpoplpush 時,如果對應的 list 列表為空,Redis 就會將對應的 client 設為阻塞狀態(tài),同時將該 client 添加到 DB 中 blocking_keys 這個阻塞 dict。所以該 dict 存儲的是處于阻塞狀態(tài)的 key 及 client 列表。

當有其他調(diào)用方在向某個 key 對應的 list 中增加元素時,Redis 會檢測是否有 client 阻塞在這個 key 上,即檢查 blocking_keys 中是否包含這個 key,如果有則會將這個 key 加入 read_keys 這個 dict 中。同時也會將這個 key 保存到 server 中的一個名叫 read_keys 的列表中。這樣可以高效、不重復的插入及輪詢。

當 client 使用 watch 指令來監(jiān)控 key 時,這個 key 和 client 就會被保存到 watched_keys 這個 dict 中。redisDb 中可以保存所有的數(shù)據(jù)類型,而 Redis 中所有數(shù)據(jù)類型都是存放在一個叫 redisObject 的結構中。
redisObject

img

redisObject 由 5 個字段組成。

type:即 Redis 對象的數(shù)據(jù)類型,目前支持 7 種 type 類型,分別為OBJ_STRINGOBJ_LISTOBJ_SETOBJ_ZSETOBJ_HASHOBJ_MODULEOBJ_STREAMencoding:Redis 對象的內(nèi)部編碼方式,即內(nèi)部數(shù)據(jù)結構類型,目前支持 10 種編碼方式包括OBJ_ENCODING_RAWOBJ_ENCODING_INTOBJ_ENCODING_HTOBJ_ENCODING_ZIPLIST 等。LRU:存儲的是淘汰數(shù)據(jù)用的 LRU 時間或 LFU 頻率及時間的數(shù)據(jù)。refcount:記錄 Redis 對象的引用計數(shù),用來表示對象被共享的次數(shù),共享使用時加 1,不再使用時減 1,當計數(shù)為 0 時表明該對象沒有被使用,就會被釋放,回收內(nèi)存。ptr:它指向對象的內(nèi)部數(shù)據(jù)結構。比如一個代表 string 的對象,它的 ptr 可能指向一個 sds 或者一個 long 型整數(shù)。

dict

img

前面講到,Redis 中的數(shù)據(jù)實際是存在 DB 中的 2 個核心 dict 字典中的。實際上 dict 也是 Redis 的一種使用廣泛的內(nèi)部數(shù)據(jù)結構。

?

Redis 中的 dict,類似于 Memcached 中 hashtable。都可以用于 key 或元素的快速插入、更新和定位。dict 字典中,有一個長度為 2 的哈希表數(shù)組,日常訪問用 0 號哈希表,如果 0 號哈希表元素過多,則分配一個 2 倍 0 號哈希表大小的空間給 1 號哈希表,然后進行逐步遷移,rehashidx 這個字段就是專門用來做標志遷移位置的。在哈希表操作中,采用單向鏈表來解決 hash 沖突問題。dict 中還有一個重要字段是 type,它用于保存 hash 函數(shù)及 key/value 賦值、比較函數(shù)。

dictht 中的 table 是一個 hash 表數(shù)組,每個桶指向一個 dictEntry 結構。dictht 采用 dictEntry 的單向鏈表來解決 hash 沖突問題。

img

dictht 是以 dictEntry 來存 key-value 映射的。其中 key 是 sds 字符串,value 為存儲各種數(shù)據(jù)類型的 redisObject 結構。

dict 可以被 redisDb 用來存儲數(shù)據(jù) key-value 及命令操作的輔助信息。還可以用來作為一些 Redis 數(shù)據(jù)類型的內(nèi)部數(shù)據(jù)結構。dict 可以作為 set 集合的內(nèi)部數(shù)據(jù)結構。在哈希的元素數(shù)超過 512 個,或者哈希中 value 大于 64 字節(jié),dict 還被用作為哈希類型的內(nèi)部數(shù)據(jù)結構。
sds

字符串是 Redis 中最常見的數(shù)據(jù)類型,其底層實現(xiàn)是簡單動態(tài)字符串即 sds。簡單動態(tài)字符串本質(zhì)是一個 char*,內(nèi)部通過 sdshdr 進行管理。sdshdr 有 4 個字段。len 為字符串實際長度,alloc 當前字節(jié)數(shù)組總共分配的內(nèi)存大小。flags 記錄當前字節(jié)數(shù)組的屬性;buf 是存儲字符串真正的值及末尾一個 \0。

img

sds 的存儲 buf 可以動態(tài)擴展或收縮,字符串長度不用遍歷,可直接獲得,修改和訪問都很方便。由于 sds 中字符串存在 buf 數(shù)組中,長度由 len 定義,而不像傳統(tǒng)字符串遇 0 停止,所以 sds 是二進制安全的,可以存放任何二進制的數(shù)據(jù)。

img

簡單動態(tài)字符串 sds 的獲取字符串長度很方便,通過 len 可以直接得到,而傳統(tǒng)字符串需要對字符串進行遍歷,時間復雜度為 O(n)。

sds 相比傳統(tǒng)字符串多了一個 sdshdr,對于大量很短的字符串,這個 sdshdr 還是一個不小的開銷。在 3.2 版本后,sds 會根據(jù)字符串實際的長度,選擇不同的數(shù)據(jù)結構,以更好的提升內(nèi)存效率。當前 sdshdr 結構分為 5 種子類型,分別為 sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。其中 sdshdr5 只有 flags 和 buf 字段,其他幾種類型的 len 和 alloc 采用從 uint8_t 到 uint64_t 的不同類型,以節(jié)省內(nèi)存空間。

sds 可以作為字符串的內(nèi)部數(shù)據(jù)結構,同時 sds 也是 hyperloglog、bitmap 類型的內(nèi)部數(shù)據(jù)結構。
ziplist

為了節(jié)約內(nèi)存,并減少內(nèi)存碎片,Redis 設計了 ziplist 壓縮列表內(nèi)部數(shù)據(jù)結構。壓縮列表是一塊連續(xù)的內(nèi)存空間,可以連續(xù)存儲多個元素,沒有冗余空間,是一種連續(xù)內(nèi)存數(shù)據(jù)塊組成的順序型內(nèi)存結構。

img

ziplist 的結構如圖所示,主要包括 5 個部分。

zlbytes 是壓縮列表所占用的總內(nèi)存字節(jié)數(shù)。Zltail 尾節(jié)點到起始位置的字節(jié)數(shù)。Zllen 總共包含的節(jié)點/內(nèi)存塊數(shù)。Entry 是 ziplist 保存的各個數(shù)據(jù)節(jié)點,這些數(shù)據(jù)點長度隨意。Zlend 是一個魔數(shù) 255,用來標記壓縮列表的結束。

如圖所示,一個包含 4 個元素的 ziplist,總占用字節(jié)是 100bytes,該 ziplist 的起始元素的指針是 p,zltail 是 80,則第 4 個元素的指針是 P+80。

img

壓縮列表 ziplist 的存儲節(jié)點 entry 的結構如圖,主要有 6 個字段。

prevRawLen 是前置節(jié)點的長度;preRawLenSize 編碼 preRawLen 需要的字節(jié)數(shù);len 當前節(jié)點的長度;lensize 編碼 len 所需要的字節(jié)數(shù);encoding  當前節(jié)點所用的編碼類型;entryData 當前節(jié)點數(shù)據(jù)。

img

由于 ziplist 是連續(xù)緊湊存儲,沒有冗余空間,所以插入新的元素需要 realloc 擴展內(nèi)存,所以如果 ziplist 占用空間太大,realloc 重新分配內(nèi)存和拷貝的開銷就會很大,所以 ziplist 不適合存儲過多元素,也不適合存儲過大的字符串。

因此只有在元素數(shù)和 value 數(shù)都不大的時候,ziplist 才作為 hash 和 zset 的內(nèi)部數(shù)據(jù)結構。其中 hash 使用 ziplist 作為內(nèi)部數(shù)據(jù)結構的限制時,元素數(shù)默認不超過 512 個,value 值默認不超過 64 字節(jié)??梢酝ㄟ^修改配置來調(diào)整 hash_max_ziplist_entries 、hash_max_ziplist_value 這兩個閥值的大小。

zset 有序集合,使用 ziplist 作為內(nèi)部數(shù)據(jù)結構的限制元素數(shù)默認不超過 128 個,value 值默認不超過 64 字節(jié)??梢酝ㄟ^修改配置來調(diào)整 zset_max_ziplist_entries 和 zset_max_ziplist_value 這兩個閥值的大小。
quicklist

Redis 在 3.2 版本之后引入 quicklist,用以替換 linkedlist。因為 linkedlist 每個節(jié)點有前后指針,要占用 16 字節(jié),而且每個節(jié)點獨立分配內(nèi)存,很容易加劇內(nèi)存的碎片化。而 ziplist 由于緊湊型存儲,增加元素需要 realloc,刪除元素需要內(nèi)存拷貝,天然不適合元素太多、value 太大的存儲。

img

而 quicklist 快速列表應運而生,它是一個基于 ziplist 的雙向鏈表。將數(shù)據(jù)分段存儲到 ziplist,然后將這些 ziplist 用雙向指針連接。快速列表的結構如圖所示。

head、tail 是兩個指向第一個和最后一個 ziplist 節(jié)點的指針。count 是 quicklist 中所有的元素個數(shù)。len 是 ziplist 節(jié)點的個數(shù)。compress 是 LZF 算法的壓縮深度。

快速列表中,管理 ziplist 的是 quicklistNode 結構。quicklistNode 主要包含一個 prev/next 雙向指針,以及一個 ziplist 節(jié)點。單個 ziplist 節(jié)點可以存放多個元素。

快速列表從頭尾讀寫數(shù)據(jù)很快,時間復雜度為 O(1)。也支持從中間任意位置插入或讀寫元素,但速度較慢,時間復雜度為 O(n)??焖倭斜懋斍爸饕鳛?list 列表的內(nèi)部數(shù)據(jù)結構。
zskiplist

跳躍表 zskiplist 是一種有序數(shù)據(jù)結構,它通過在每個節(jié)點維持多個指向其他節(jié)點的指針,從而可以加速訪問。跳躍表支持平均 O(logN) 和最差 O(n) 復雜度的節(jié)點查找。在大部分場景,跳躍表的效率和平衡樹接近,但跳躍表的實現(xiàn)比平衡樹要簡單,所以不少程序都用跳躍表來替換平衡樹。

img

如果 sorted set 類型的元素數(shù)比較多或者元素比較大,Redis 就會選擇跳躍表來作為 sorted set有序集合的內(nèi)部數(shù)據(jù)結構。

跳躍表主要由 zskipList 和節(jié)點 zskiplistNode 構成。zskiplist 結構如圖,header 指向跳躍表的表頭節(jié)點。tail 指向跳躍表的表尾節(jié)點。length 表示跳躍表的長度,它是跳躍表中不包含表頭節(jié)點的節(jié)點數(shù)量。level 是目前跳躍表內(nèi),除表頭節(jié)點外的所有節(jié)點中,層數(shù)最大的那個節(jié)點的層數(shù)。

跳躍表的節(jié)點 zskiplistNode 的結構如圖所示。ele 是節(jié)點對應的 sds 值,在 zset 有序集合中就是集合中的 field 元素。score 是節(jié)點的分數(shù),通過 score,跳躍表中的節(jié)點自小到大依次排列。backward 是指向當前節(jié)點的前一個節(jié)點的指針。level 是節(jié)點中的層,每個節(jié)點一般有多個層。每個 level 層都帶有兩個屬性,一個是 forwad 前進指針,它用于指向表尾方向的節(jié)點;另外一個是 span 跨度,它是指 forward 指向的節(jié)點到當前節(jié)點的距離。

img

如圖所示是一個跳躍表,它有 3 個節(jié)點。對應的元素值分別是 S1、S2 和 S3,分數(shù)值依次為 1.0、3.0 和 5.0。其中 S3 節(jié)點的 level 最大是 5,跳躍表的 level 是 5。header 指向表頭節(jié)點,tail 指向表尾節(jié)點。在查到元素時,累加路徑上的跨度即得到元素位置。在跳躍表中,元素必須是唯一的,但 score 可以相同。相同 score 的不同元素,按照字典序進行排序。

在 sorted set 數(shù)據(jù)類型中,如果元素數(shù)較多或元素長度較大,則使用跳躍表作為內(nèi)部數(shù)據(jù)結構。默認元素數(shù)超過 128 或者最大元素的長度超過 64,此時有序集合就采用 zskiplist 進行存儲。由于 geo 也采用有序集合類型來存儲地理位置名稱和位置 hash 值,所以在超過相同閥值后,也采用跳躍表進行存儲。

img

Redis 主要的內(nèi)部數(shù)據(jù)結構講完了,接下來整體看一下,之前講的 8 種數(shù)據(jù)類型,具體都是采用哪種內(nèi)部數(shù)據(jù)結構來存儲的。

首先,對于 string 字符串,Redis 主要采用 sds 來進行存儲。而對于 list 列表,Redis 采用 quicklist 進行存儲。對于 set 集合類型,Redis 采用 dict 來進行存儲。對于 sorted set 有序集合類型,如果元素數(shù)小于 128 且元素長度小于 64,則使用 ziplist 存儲,否則使用 zskiplist 存儲。對于哈希類型,如果元素數(shù)小于 512,并且元素長度小于 64,則用 ziplist 存儲,否則使用 dict 字典存儲。對于 hyperloglog,采用 sds 簡單動態(tài)字符串存儲。對于 geo,如果位置數(shù)小于 128,則使用 ziplist 存儲,否則使用 zskiplist 存儲。最后對于 bitmap,采用 sds 簡單動態(tài)字符串存儲。

除了這些主要的內(nèi)部數(shù)據(jù)結構,還有在特殊場景下也會采用一些其他內(nèi)部結構存儲,比如,如果操作的字符串都是整數(shù),同時指令是 incr、decr 等,會對字符串采用 long 型整數(shù)存儲,這些場景比較特殊,限于時間關系,這里不做進一步闡述。

第八章:Redis進階(下)

第23講:Redis是如何淘汰key的?

你好,我是你的緩存課老師陳波,歡迎進入第 23 課時“Redis 淘汰策略”的學習。本課時我們主要學習 Redis 淘汰原理、淘汰方式、以及 8 種淘汰策略等內(nèi)容。
淘汰原理

首先我們來學習 Redis 的淘汰原理。

系統(tǒng)線上運行中,內(nèi)存總是昂貴且有限的,在數(shù)據(jù)總量遠大于 Redis 可用的內(nèi)存總量時,為了最大限度的提升訪問性能,Redis 中只能存放最新最熱的有效數(shù)據(jù)。

當 key 過期后,或者 Redis 實際占用的內(nèi)存超過閥值后,Redis 就會對 key 進行淘汰,刪除過期的或者不活躍的 key,回收其內(nèi)存,供新的 key 使用。Redis 的內(nèi)存閥值是通過 maxmemory 設置的,而超過內(nèi)存閥值后的淘汰策略,是通過 maxmemory-policy 設置的,具體的淘汰策略后面會進行詳細介紹。Redis 會在 2 種場景下對 key 進行淘汰,第一種是在定期執(zhí)行 serverCron 時,檢查淘汰 key;第二種是在執(zhí)行命令時,檢查淘汰 key。

第一種場景,Redis 定期執(zhí)行 serverCron 時,會對 DB 進行檢測,清理過期 key。清理流程如下。首先輪詢每個 DB,檢查其 expire dict,即帶過期時間的過期 key 字典,從所有帶過期時間的 key 中,隨機選取 20 個樣本 key,檢查這些 key 是否過期,如果過期則清理刪除。如果 20 個樣本中,超過 5 個 key 都過期,即過期比例大于 25%,就繼續(xù)從該 DB 的 expire dict 過期字典中,再隨機取樣 20 個 key 進行過期清理,持續(xù)循環(huán),直到選擇的 20 個樣本 key 中,過期的 key 數(shù)小于等于 5,當前這個 DB 則清理完畢,然后繼續(xù)輪詢下一個 DB。

在執(zhí)行 serverCron 時,如果在某個 DB 中,過期 dict 的填充率低于 1%,則放棄對該 DB 的取樣檢查,因為效率太低。如果 DB 的過期 dict 中,過期 key 太多,一直持續(xù)循環(huán)回收,會占用大量主線程時間,所以 Redis 還設置了一個過期時間。這個過期時間根據(jù) serverCron 的執(zhí)行頻率來計算,5.0 版本及之前采用慢循環(huán)過期策略,默認是 25ms,如果回收超過 25ms 則停止,6.0 非穩(wěn)定版本采用快循環(huán)策略,過期時間為 1ms。

第二種場景,Redis 在執(zhí)行命令請求時。會檢查當前內(nèi)存占用是否超過 maxmemory 的數(shù)值,如果超過,則按照設置的淘汰策略,進行刪除淘汰 key 操作。
淘汰方式

Redis 中 key 的淘汰方式有兩種,分別是同步刪除淘汰和異步刪除淘汰。在 serverCron 定期清理過期 key 時,如果設置了延遲過期配置 lazyfree-lazy-expire,會檢查 key 對應的 value 是否為多元素的復合類型,即是否是 list 列表、set 集合、zset 有序集合和 hash 中的一種,并且 value 的元素數(shù)大于 64,則在將 key 從 DB 中 expire dict 過期字典和主 dict 中刪除后,value 存放到 BIO 任務隊列,由 BIO 延遲刪除線程異步回收;否則,直接從 DB 的 expire dict 和主 dict 中刪除,并回收 key、value 所占用的空間。在執(zhí)行命令時,如果設置了 lazyfree-lazy-eviction,在淘汰 key 時,也采用前面類似的檢測方法,對于元素數(shù)大于 64 的 4 種復合類型,使用 BIO 線程異步刪除,否則采用同步直接刪除。
淘汰策略

img

Redis 提供了 8 種淘汰策略對 key 進行管理,而且還引入基于樣本的 eviction pool,來提升剔除的準確性,確保 在保持最大性能 的前提下,剔除最不活躍的 key。eviction pool 主要對 LRU、LFU,以及過期 dict ttl 內(nèi)存管理策略 生效。處理流程為,當 Redis 內(nèi)存占用超過閥值后,按策略從主 dict 或者帶過期時間的 expire dict 中隨機選擇 N 個 key,N 默認是 5,計算每個 key 的 idle 值,按 idle 值從小到大的順序插入 evictionPool 中,然后選擇 idle 最大的那個 key,進行淘汰。

img

選擇淘汰策略時,可以通過配置 Redis 的 maxmemory 設置最大內(nèi)存,并通 maxmemory_policy 設置超過最大內(nèi)存后的處理策略。如果 maxmemory 設為 0,則表明對內(nèi)存使用沒有任何限制,可以持續(xù)存放數(shù)據(jù),適合作為存儲,來存放數(shù)據(jù)量較小的業(yè)務。如果數(shù)據(jù)量較大,就需要估算熱數(shù)據(jù)容量,設置一個適當?shù)闹?#xff0c;將 Redis 作為一個緩存而非存儲來使用。

Redis 提供了 8 種 maxmemory_policy 淘汰策略來應對內(nèi)存超過閥值的情況。

第一種淘汰策略是 noeviction,它是 Redis 的默認策略。在內(nèi)存超過閥值后,Redis 不做任何清理工作,然后對所有寫操作返回錯誤,但對讀請求正常處理。noeviction 適合數(shù)據(jù)量不大的業(yè)務場景,將關鍵數(shù)據(jù)存入 Redis 中,將 Redis 當作 DB 來使用。

第二種淘汰策略是 volatile-lru,它對帶過期時間的 key 采用最近最少訪問算法來淘汰。使用這種策略,Redis 會從 redisDb 的 expire dict 過期字典中,首先隨機選擇 N 個 key,計算 key 的空閑時間,然后插入 evictionPool 中,最后選擇空閑時間最久的 key 進行淘汰。這種策略適合的業(yè)務場景是,需要淘汰的key帶有過期時間,且有冷熱區(qū)分,從而可以淘汰最久沒有訪問的key。

第三種策略是 volatile-lfu,它對帶過期時間的 key 采用最近最不經(jīng)常使用的算法來淘汰。使用這種策略時,Redis 會從 redisDb 中的 expire dict 過期字典中,首先隨機選擇 N 個 key,然后根據(jù)其 value 的 lru 值,計算 key 在一段時間內(nèi)的使用頻率相對值。對于 lfu,要選擇使用頻率最小的 key,為了沿用 evictionPool 的 idle 概念,Redis 在計算 lfu 的 Idle 時,采用 255 減去使用頻率相對值,從而確保 Idle 最大的 key 是使用次數(shù)最小的 key,計算 N 個 key 的 Idle 值后,插入 evictionPool,最后選擇 Idle 最大,即使用頻率最小的 key,進行淘汰。這種策略也適合大多數(shù) key 帶過期時間且有冷熱區(qū)分的業(yè)務場景。

第四種策略是 volatile-ttl,它是對帶過期時間的 key 中選擇最早要過期的 key 進行淘汰。使用這種策略時,Redis 也會從 redisDb 的 expire dict 過期字典中,首先隨機選擇 N 個 key,然后用最大無符號 long 值減去 key 的過期時間來作為 Idle 值,計算 N 個 key 的 Idle 值后,插入evictionPool,最后選擇 Idle 最大,即最快就要過期的 key,進行淘汰。這種策略適合,需要淘汰的key帶過期時間,且有按時間冷熱區(qū)分的業(yè)務場景。

img

第五種策略是 volatile-random,它是對帶過期時間的 key 中隨機選擇 key 進行淘汰。使用這種策略時,Redis 從 redisDb 的 expire dict 過期字典中,隨機選擇一個 key,然后進行淘汰。如果需要淘汰的key有過期時間,沒有明顯熱點,主要被隨機訪問,那就適合選擇這種淘汰策略。

第六種策略是 allkey-lru,它是對所有 key,而非僅僅帶過期時間的 key,采用最近最久沒有使用的算法來淘汰。這種策略與 volatile-lru 類似,都是從隨機選擇的 key 中,選擇最長時間沒有被訪問的 key 進行淘汰。區(qū)別在于,volatile-lru 是從 redisDb 中的 expire dict 過期字典中選擇 key,而 allkey-lru 是從所有的 key 中選擇 key。這種策略適合,需要對所有 key 進行淘汰,且數(shù)據(jù)有冷熱讀寫區(qū)分的業(yè)務場景。

第七種策略是 allkeys-lfu,它也是針對所有 key 采用最近最不經(jīng)常使用的算法來淘汰。這種策略與 volatile-lfu 類似,都是在隨機選擇的 key 中,選擇訪問頻率最小的 key 進行淘汰。區(qū)別在于,volatile-flu從expire dict 過期字典中選擇 key,而 allkeys-lfu 是從主 dict 中選擇 key。這種策略適合的場景是,需要從所有的 key 中進行淘汰,但數(shù)據(jù)有冷熱區(qū)分,且越熱的數(shù)據(jù)訪問頻率越高。

最后一種策略是 allkeys-random,它是針對所有 key 進行隨機算法進行淘汰。它也是從主 dict 中隨機選擇 key,然后進行刪除回收。如果需要從所有的 key 中進行淘汰,并且 key 的訪問沒有明顯熱點,被隨機訪問,即可采用這種策略。

第24講:Redis崩潰后,如何進行數(shù)據(jù)恢復的?

你好,我是你的緩存課老師陳波,歡迎來到第 24 課時“Redis 崩潰后,如何進行數(shù)據(jù)恢復”的學習。本課時我們主要學習通過 RDB、AOF、混合存儲等數(shù)據(jù)持久化方案來解決如何進行數(shù)據(jù)恢復的問題。

img

Redis 持久化是一個將內(nèi)存數(shù)據(jù)轉儲到磁盤的過程。Redis 目前支持 RDB、AOF,以及混合存儲三種模式。
RDB

Redis 的 RDB 持久化是以快照的方式將內(nèi)存數(shù)據(jù)存儲到磁盤。在需要進行 RDB 持久化時,Redis 會將內(nèi)存中的所有數(shù)據(jù)以二進制的格式落地,每條數(shù)據(jù)存儲的內(nèi)容包括過期時間、數(shù)據(jù)類型、key,以及 value。當 Redis 重啟時,如果 appendonly 關閉,則會讀取 RDB 持久化生成的二進制文件進行數(shù)據(jù)恢復。

觸發(fā)構建 RDB 的場景主要有以下四種。

第一種場景是通過 save 或 bgsave 命令進行主動 RDB 快照構建。它是由調(diào)用方調(diào)用 save 或 bgsave 指令進行觸發(fā)的。第二種場景是利用配置 save m n 來進行自動快照生成。它是指在 m 秒中,如果插入或變更 n 個 key,則自動觸發(fā) bgsave。這個配置可以設置多個配置行,以便組合使用。由于峰值期間,Redis 的壓力大,變更的 key 也比較多,如果再進行構建 RDB 的操作,會進一步增加機器負擔,對調(diào)用方請求會有一定的影響,所以線上使用時需要謹慎。第三種場景是主從復制,如果從庫需要進行全量復制,此時主庫也會進行 bgsave 生成一個 RDB 快照。第四種場景是在運維執(zhí)行 flushall 清空所有數(shù)據(jù),或執(zhí)行 shutdown 關閉服務時,也會觸發(fā) Redis 自動構建 RDB 快照。

img

save 是在主進程中進行 RDB 持久化的,持久化期間 Redis 處于阻塞狀態(tài),不處理任何客戶請求,所以一般使用較少。而 bgsave 是 fork 一個子進程,然后在子進程中構建 RDB 快照,構建快照的過程不直接影響用戶的訪問,但仍然會增加機器負載。線上 Redis 快照備份,一般會選擇凌晨低峰時段,通過 bgsave 主動觸發(fā)進行備份。

RDB 快照文件主要由 3 部分組成。

第一部分是 RDB 頭部,主要包括 RDB 的版本,以及 Redis 版本、創(chuàng)建日期、占用內(nèi)存等輔助信息。第二部分是各個 RedisDB 的數(shù)據(jù)。存儲每個 RedisDB 時,會首先記錄當前 RedisDB 的DBID,然后記錄主 dict 和 expire dict 的記錄數(shù)量,最后再輪詢存儲每條數(shù)據(jù)記錄。存儲數(shù)據(jù)記錄時,如果數(shù)據(jù)有過期時間,首先記錄過期時間。如果 Redis 的 maxmemory_policy 過期策略采用 LRU 或者 LFU,還會將 key 對應的 LRU、LFU 值進行落地,最后記錄數(shù)據(jù)的類型、key,以及 value。第三部部分是 RDB 的尾部。RDB 尾部,首先存儲 Redis 中的 Lua 腳本等輔助信息。然后存儲 EOF 標記,即值為 255 的字符。最后存 RDB 的 cksum。

至此,RDB 就落地完畢。

RDB 采用二進制方式存儲內(nèi)存數(shù)據(jù),文件小,且啟動時恢復速度快。但構建 RDB 時,一個快照文件只能存儲,構建時刻的內(nèi)存數(shù)據(jù),無法記錄之后的數(shù)據(jù)變更。構建 RDB 的過程,即便在子進程中進行,但仍然屬于 CPU 密集型的操作,而且每次落地全量數(shù)據(jù),耗時也比較長,不能隨時進行,特別是不能在高峰期進行。由于 RDB 采用二進制存儲,可讀性差,而且由于格式固定,不同版本之間可能存在兼容性問題。
AOF

img

Redis 的 AOF 持久化是以命令追加的方式進行數(shù)據(jù)落地的。通過打開 appendonly 配置,Redis 將每一個寫指令追加到磁盤 AOF 文件,從而及時記錄內(nèi)存數(shù)據(jù)的最新狀態(tài)。這樣即便 Redis 被 crash 或異常關閉后,再次啟動,也可以通過加載 AOF,來恢復最新的全量數(shù)據(jù),基本不會丟失數(shù)據(jù)。

AOF 文件中存儲的協(xié)議是寫指令的 multibulk 格式,這是 Redis 的標準協(xié)議格式,所以不同的 Redis 版本均可解析并處理,兼容性很好。

但是,由于 Redis 會記錄所有寫指令操作到 AOF,大量的中間狀態(tài)數(shù)據(jù),甚至被刪除的過期數(shù)據(jù),都會存在 AOF 中,冗余度很大,而且每條指令還需通過加載和執(zhí)行來進行數(shù)據(jù)恢復,耗時會比較大。

AOF 數(shù)據(jù)的落地流程如下。Redis 在處理完寫指令后,首先將寫指令寫入 AOF 緩沖,然后通過 server_cron 定期將 AOF 緩沖寫入文件緩沖。最后按照配置策略進行 fsync,將文件緩沖的數(shù)據(jù)真正同步寫入磁盤。

img

Redis 通過 appendfsync 來設置三種不同的同步文件緩沖策略。

第一種配置策略是 no,即 Redis 不主動使用 fsync 進行文件數(shù)據(jù)同步落地,而是由操作系統(tǒng)的 write 函數(shù)去確認同步時間,在 Linux 系統(tǒng)中大概每 30 秒會進行一次同步,如果 Redis 發(fā)生 crash,就會造成大量的數(shù)據(jù)丟失。第二種配置策略是 always,即每次將 AOF 緩沖寫入文件,都會調(diào)用 fsync 強制將內(nèi)核數(shù)據(jù)寫入文件,安全性最高,但性能上會比較低效,而且由于頻繁的 IO 讀寫,磁盤的壽命會大大降低。第三種配置策略是 everysec。即每秒通過 BIO 線程進行一次 fsync。這種策略在安全性、性能,以及磁盤壽命之間做較好的權衡,可以較好的滿足線上業(yè)務需要。

img

隨著時間的推移,AOF 持續(xù)記錄所有的寫指令,AOF 會越來越大,而且會充斥大量的中間數(shù)據(jù)、過期數(shù)據(jù),為了減少無效數(shù)據(jù),提升恢復時間,可以定期對 AOF 進行 rewrite 操作。

AOF 的 rewrite 操作可以通過運維執(zhí)行 bgrewiretaof 命令來進行,也可以通過配置重寫策略進行,由 Redis 自動觸發(fā)進行。當對 AOF 進行 rewrite 時,首先會 fork 一個子進程。子進程輪詢所有 RedisDB 快照,將所有內(nèi)存數(shù)據(jù)轉為 cmd,并寫入臨時文件。在子進程 rewriteaof 時,主進程可以繼續(xù)執(zhí)行用戶請求,執(zhí)行完畢后將寫指令寫入舊的 AOF 文件和 rewrite 緩沖。子進程將 RedisDB 中數(shù)據(jù)落地完畢后,通知主進程。主進程從而將 AOF rewite 緩沖數(shù)據(jù)寫入 AOF 臨時文件,然后用新的 AOF 文件替換舊的 AOF 文件,最后通過 BIO 線程異步關閉舊的 AOF 文件。至此,AOF 的 rewrite 過程就全部完成了。

img

AOF 重寫的過程,是一個輪詢?nèi)?RedisDB 快照,逐一落地的過程。每個 DB,首先通過 select $db 來記錄待落的 DBID。然后通過命令記錄每個 key/value。對于數(shù)據(jù)類型為 SDS 的value,可以直接落地。但如果 value 是聚合類型,則會將所有元素設為批量添加指令,進行落地。

對于 list 列表類型,通過 RPUSH 指令落地所有列表元素。對于 set 集合,會用 SADD 落地所有集合元素。對于 Zset 有序集合,會用 Zadd 落地所有元素,而對于 Hash 會用 Hmset 落地所有哈希元素。如果數(shù)據(jù)帶過期時間,還會通過 pexpireat 來記錄數(shù)據(jù)的過期時間。

AOF 持久化的優(yōu)勢是可以記錄全部的最新內(nèi)存數(shù)據(jù),最多也就是 1-2 秒的數(shù)據(jù)丟失。同時 AOF 通過 Redis 協(xié)議來追加記錄數(shù)據(jù),兼容性高,而且可以持續(xù)輕量級的保存最新數(shù)據(jù)。最后因為是直接通過 Redis 協(xié)議存儲,可讀性也比較好。

AOF 持久化的不足是隨著時間的增加,冗余數(shù)據(jù)增多,文件會持續(xù)變大,而且數(shù)據(jù)恢復需要讀取所有命令并執(zhí)行,恢復速度相對較慢。
混合持久化

img

Redis 在 4.0 版本之后,引入了混合持久化方式,而且在 5.0 版本后默認開啟。前面講到 RDB 加載速度快,但構建慢,缺少最新數(shù)據(jù)。AOF 持續(xù)追加最新寫記錄,可以包含所有數(shù)據(jù),但冗余大,加載速度慢?;旌夏J揭惑w化使用 RDB 和 AOF,綜合 RDB 和 AOF 的好處。即可包含全量數(shù)據(jù),加載速度也比較快。可以使用 aof-use-rdb-preamble 配置來明確打開混合持久化模式。

混合持久化也是通過 bgrewriteaof 來實現(xiàn)的。當啟用混合存儲后,進行 bgrewriteaof 時,主進程首先依然是 fork 一個子進程,子進程首先將內(nèi)存數(shù)據(jù)以 RDB 的二進制格式寫入 AOF 臨時文件中。然后,再將落地期間緩沖的新增寫指令,以命令的方式追加到臨時文件。然后再通知主進程落地完畢。主進程將臨時文件修改為 AOF 文件,并關閉舊的 AOF 文件。這樣主體數(shù)據(jù)以 RDB 格式存儲,新增指令以命令方式追加的混合存儲方式進行持久化。后續(xù)執(zhí)行的任務,以正常的命令方式追加到新的 AOF 文件即可。

混合持久化綜合了 RDB 和 AOF 的優(yōu)缺點,優(yōu)勢是包含全量數(shù)據(jù),加載速度快。不足是頭部的 RDB 格式兼容性和可讀性較差。

第25講: Redis是如何處理容易超時的系統(tǒng)調(diào)用的?

本課時我們主要學習通過 BIO 線程解決處理容易超時的系統(tǒng)調(diào)用問題,以及 BIO 線程處理的任務與處理流程等內(nèi)容。

BIO 線程簡介

Redis 在運行過程中,不可避免的會產(chǎn)生一些運行慢的、容易引發(fā)阻塞的任務,如將內(nèi)核中的文件緩沖同步到磁盤中、關閉文件,都會引發(fā)短時阻塞,還有一些大 key,如一些元素數(shù)高達萬級或更多的聚合類元素,在刪除時,由于所有元素需要逐一釋放回收,整個過程耗時也會比較長。而 Redis 的核心處理線程是單進程單線程模型,所有命令的接受與處理、數(shù)據(jù)淘汰等都在主線程中進行,這些任務處理速度非???。如果核心單線程還要處理那些慢任務,在處理期間,勢必會阻塞用戶的正常請求,導致服務卡頓。為此,Redis 引入了 BIO 后臺線程,專門處理那些慢任務,從而保證和提升主線程的處理能力。

img

Redis 的 BIO 線程采用生產(chǎn)者-消費者模型。主線程是生產(chǎn)者,生產(chǎn)各種慢任務,然后存放到任務隊列中。BIO 線程是消費者,從隊列獲取任務并進行處理。如果生產(chǎn)者生產(chǎn)任務過快,隊列可用于緩沖這些任務,避免負荷過載或數(shù)據(jù)丟失。如果消費者處理速度很快,處理完畢后就可以安靜的等待,不增加額外的性能開銷。再次,有新任務時,主線程通過條件變量來通知 BIO 線程,這樣 BIO 線程就可以再次執(zhí)行任務。

BIO 處理任務

Redis 啟動時,會創(chuàng)建三個任務隊列,并對應構建 3 個 BIO 線程,三個 BIO 線程與 3 個任務隊列之間一一對應。BIO 線程分別處理如下 3 種任務。

close 關閉文件任務。rewriteaof 完成后,主線程需要關閉舊的 AOF 文件,就向 close 隊列插入一個舊 AOF 文件的關閉任務。由 close 線程來處理。fysnc 任務。Redis 將 AOF 數(shù)據(jù)緩沖寫入文件內(nèi)核緩沖后,需要定期將系統(tǒng)內(nèi)核緩沖數(shù)據(jù)寫入磁盤,此時可以向 fsync 隊列寫入一個同步文件緩沖的任務,由 fsync 線程來處理。lazyfree 任務。Redis 在需要淘汰元素數(shù)大于 64 的聚合類數(shù)據(jù)類型時,如列表、集合、哈希等,就往延遲清理隊列中寫入待回收的對象,由 lazyfree 線程后續(xù)進行異步回收。

BIO 處理流程

BIO 線程的整個處理流程如圖所示。當主線程有慢任務需要異步處理時。就會向對應的任務隊列提交任務。提交任務時,首先申請內(nèi)存空間,構建 BIO 任務。然后對隊列鎖進行加鎖,在隊列尾部追加新的 BIO 任務,最后嘗試喚醒正在等待任務的 BIO 線程。

img

BIO 線程啟動時或持續(xù)處理完所有任務,發(fā)現(xiàn)任務隊列為空后,就會阻塞,并等待新任務的到來。當主線程有新任務后,主線程會提交任務,并喚醒 BIO 線程。BIO 線程隨后開始輪詢獲取新任務,并進行處理。當處理完所有 BIO 任務后,則再次進入阻塞,等待下一輪喚醒。

第26講:如何大幅成倍提升Redis處理性能?

本課時我們主要學習如何通過 Redis 多線程來大幅提升性能,涉及主線程與 IO 線程、命令處理流程,以及多線程方案的優(yōu)劣等內(nèi)容。

主線程

Redis 自問世以來,廣受好評,應用廣泛。但相比, Memcached 單實例壓測 TPS 可以高達百萬,線上可以穩(wěn)定跑 20~40 萬而言,Redis 的單實例壓測 TPS 不過 10~12 萬,線上一般最高也就 2~4 萬,仍相差一個數(shù)量級。

Redis 慢的主要原因是單進程單線程模型。雖然一些重量級操作也進行了分拆,如 RDB 的構建在子進程中進行,文件關閉、文件緩沖同步,以及大 key 清理都放在 BIO 線程異步處理,但還遠遠不夠。線上 Redis 處理用戶請求時,十萬級的 client 掛在一個 Redis 實例上,所有的事件處理、讀請求、命令解析、命令執(zhí)行,以及最后的響應回復,都由主線程完成,縱然是 Redis 各種極端優(yōu)化,巧婦難為無米之炊,一個線程的處理能力始終是有上限的。當前服務器 CPU 大多是 16 核到 32 核以上,Redis 日常運行主要只使用 1 個核心,其他 CPU 核就沒有被很好的利用起來,Redis 的處理性能也就無法有效地提升。而 Memcached 則可以按照服務器的 CPU 核心數(shù),配置數(shù)十個線程,這些線程并發(fā)進行 IO 讀寫、任務處理,處理性能可以提高一個數(shù)量級以上。

IO 線程

面對性能提升困境,雖然 Redis 作者不以為然,認為可以通過多部署幾個 Redis 實例來達到類似多線程的效果。但多實例部署則帶來了運維復雜的問題,而且單機多實例部署,會相互影響,進一步增大運維的復雜度。為此,社區(qū)一直有種聲音,希望 Redis 能開發(fā)多線程版本。

因此,Redis 即將在 6.0 版本引入多線程模型,當前代碼在 unstable 版本中,6.0 版本預計在明年發(fā)版。Redis 的多線程模型,分為主線程和 IO 線程。

因為處理命令請求的幾個耗時點,分別是請求讀取、協(xié)議解析、協(xié)議執(zhí)行,以及響應回復等。所以 Redis 引入 IO 多線程,并發(fā)地進行請求命令的讀取、解析,以及響應的回復。而其他的所有任務,如事件觸發(fā)、命令執(zhí)行、IO 任務分發(fā),以及其他各種核心操作,仍然在主線程中進行,也就說這些任務仍然由單線程處理。這樣可以在最大程度不改變原處理流程的情況下,引入多線程。

命令處理流程

Redis 6.0 的多線程處理流程如圖所示。主線程負責監(jiān)聽端口,注冊連接讀事件。當有新連接進入時,主線程 accept 新連接,創(chuàng)建 client,并為新連接注冊請求讀事件。

img

當請求命令進入時,在主線程觸發(fā)讀事件,主線程此時并不進行網(wǎng)絡 IO 的讀取,而將該連接所在的 client 加入待讀取隊列中。Redis 的 Ae 事件模型在循環(huán)中,發(fā)現(xiàn)待讀取隊列不為空,則將所有待讀取請求的 client 依次分派給 IO 線程,并自旋檢查等待,等待 IO 線程讀取所有的網(wǎng)絡數(shù)據(jù)。所謂自旋檢查等待,也就是指主線程持續(xù)死循環(huán),并在循環(huán)中檢查 IO 線程是否讀完,不做其他任何任務。只有發(fā)現(xiàn) IO 線程讀完所有網(wǎng)絡數(shù)據(jù),才停止循環(huán),繼續(xù)后續(xù)的任務處理。

一般可以配置多個 IO 線程,比如配置 4~8 個,這些 IO 線程發(fā)現(xiàn)待讀取隊列中有任務時,則開始并發(fā)處理。每個 IO 線程從對應列表獲取一個任務,從里面的 client 連接中讀取請求數(shù)據(jù),并進行命令解析。當 IO 線程完成所有的請求讀取,并完成解析后,待讀取任務數(shù)變?yōu)?0。主線程就停止循環(huán)檢測,開始依次執(zhí)行 IO 線程已經(jīng)解析的所有命令,每執(zhí)行完畢一個命令,就將響應寫入 client 寫緩沖,這些 client 就變?yōu)榇貜?client,這些待回復 client 被加入待回復列表。然后主線程將這些待回復 client,輪詢分配給多個 IO 線程。然后再次自旋檢測等待。

然后 IO 線程再次開始并發(fā)執(zhí)行,將不同 client 的響應緩沖寫給 client。當所有響應全部處理完后,待回復的任務數(shù)變?yōu)?0,主線程結束自旋檢測,繼續(xù)處理后續(xù)的任務,以及新的讀請求。

Redis 6.0 版本中新引入的多線程模型,主要是指可配置多個 IO 線程,這些線程專門負責請求讀取、解析,以及響應的回復。通過 IO 多線程,Redis 的性能可以提升 1 倍以上。

多線程方案優(yōu)劣

雖然多線程方案能提升1倍以上的性能,但整個方案仍然比較粗糙。首先所有命令的執(zhí)行仍然在主線程中進行,存在性能瓶頸。然后所有的事件觸發(fā)也是在主線程中進行,也依然無法有效使用多核心。而且,IO 讀寫為批處理讀寫,即所有 IO 線程先一起讀完所有請求,待主線程解析處理完畢后,所有 IO 線程再一起回復所有響應,不同請求需要相互等待,效率不高。最后在 IO 批處理讀寫時,主線程自旋檢測等待,效率更是低下,即便任務很少,也很容易把 CPU 打滿。整個多線程方案比較粗糙,所以性能提升也很有限,也就 1~2 倍多一點而已。要想更大幅提升處理性能,命令的執(zhí)行、事件的觸發(fā)等都需要分拆到不同線程中進行,而且多線程處理模型也需要優(yōu)化,各個線程自行進行 IO 讀寫和執(zhí)行,互不干擾、等待與競爭,才能真正高效地利用服務器多核心,達到性能數(shù)量級的提升。

第九章:分布式Redis實戰(zhàn)

第27講:Redis是如何進行主從復制的?

本課時我們主要學習 Redis 復制原理,以及復制分析等內(nèi)容。

Redis 復制原理

為了避免單點故障,數(shù)據(jù)存儲需要進行多副本構建。同時由于 Redis 的核心操作是單線程模型的,單個 Redis 實例能處理的請求 TPS 有限。因此 Redis 自面世起,基本就提供了復制功能,而且對復制策略不斷進行優(yōu)化。

img

通過數(shù)據(jù)復制,Redis 的一個 master 可以掛載多個 slave,而 slave 下還可以掛載多個 slave,形成多層嵌套結構。所有寫操作都在 master 實例中進行,master 執(zhí)行完畢后,將寫指令分發(fā)給掛在自己下面的 slave 節(jié)點。slave 節(jié)點下如果有嵌套的 slave,會將收到的寫指令進一步分發(fā)給掛在自己下面的 slave。通過多個 slave,Redis 的節(jié)點數(shù)據(jù)就可以實現(xiàn)多副本保存,任何一個節(jié)點異常都不會導致數(shù)據(jù)丟失,同時多 slave 可以 N 倍提升讀性能。master 只寫不讀,這樣整個 master-slave 組合,讀寫能力都可以得到大幅提升。

master 在分發(fā)寫請求時,同時會將寫指令復制一份存入復制積壓緩沖,這樣當 slave 短時間斷開重連時,只要 slave 的復制位置點仍然在復制積壓緩沖,則可以從之前的復制位置點之后繼續(xù)進行復制,提升復制效率。

img

主庫 master 和從庫 slave 之間通過復制 id 進行匹配,避免 slave 掛到錯誤的 master。Redis 的復制分為全量同步和增量同步。Redis 在進行全量同步時,master 會將內(nèi)存數(shù)據(jù)通過 bgsave 落地到 rdb,同時,將構建 內(nèi)存快照期間 的寫指令,存放到復制緩沖中,當 rdb 快照構建完畢后,master 將 rdb 和復制緩沖隊列中的數(shù)據(jù)全部發(fā)送給 slave,slave 完全重新創(chuàng)建一份數(shù)據(jù)。這個過程,對 master 的性能損耗較大,slave 構建數(shù)據(jù)的時間也比較長,而且傳遞 rdb 時還會占用大量帶寬,對整個系統(tǒng)的性能和資源的訪問影響都比較大。而增量復制,master 只發(fā)送 slave 上次復制位置之后的寫指令,不用構建 rdb,而且傳輸內(nèi)容非常有限,對 master、slave 的負荷影響很小,對帶寬的影響可以忽略,整個系統(tǒng)受影響非常小。

在 Redis 2.8 之前,Redis 基本只支持全量復制。在 slave 與 master 斷開連接,或 slave 重啟后,都需要進行全量復制。在 2.8 版本之后,Redis 引入 psync,增加了一個復制積壓緩沖,在將寫指令同步給 slave 時,會同時在復制積壓緩沖中也寫一份。在 slave 短時斷開重連后,上報master runid 及復制偏移量。如果 runid 與 master 一致,且偏移量仍然在 master 的復制緩沖積壓中,則 master 進行增量同步。

但如果 slave 重啟后,master runid 會丟失,或者切換 master 后,runid 會變化,仍然需要全量同步。因此 Redis 自 4.0 強化了 psync,引入了 psync2。在 pysnc2 中,主從復制不再使用 runid,而使用 replid(即復制id) 來作為復制判斷依據(jù)。同時 Redis 實例在構建 rdb 時,會將 replid 作為 aux 輔助信息存入 rbd。重啟時,加載 rdb 時即可得到 master 的復制 id。從而在 slave 重啟后仍然可以增量同步。

在 psync2 中,Redis 每個實例除了會有一個復制 id 即 replid 外,還有一個 replid2。Redis 啟動后,會創(chuàng)建一個長度為 40 的隨機字符串,作為 replid 的初值,在建立主從連接后,會用 master的 replid 替換自己的 replid。同時會用 replid2 存儲上次 master 主庫的 replid。這樣切主時,即便 slave 匯報的復制 id 與新 master 的 replid 不同,但和新 master 的 replid2 相同,同時復制偏移仍然在復制積壓緩沖區(qū)內(nèi),仍然可以實現(xiàn)增量復制。

Redis 復制分析

在設置 master、slave 時,首先通過配置或者命令 slaveof no one 將節(jié)點設置為主庫。然后其他各個從庫節(jié)點,通過 slaveof $master_ip $master_port,將其他從庫掛在到 master 上。同樣方法,還可以將 slave 節(jié)點掛載到已有的 slave 節(jié)點上。在準備開始數(shù)據(jù)復制時,slave 首先會主動與 master 創(chuàng)建連接,并上報信息。具體流程如下。

img

slave 創(chuàng)建與 master 的連接后,首先發(fā)送 ping 指令,如果 master 沒有返回異常,而是返回 pong,則說明 master 可用。如果 Redis 設置了密碼,slave 會發(fā)送 auth $masterauth 指令,進行鑒權。當鑒權完畢,從庫就通過 replconf 發(fā)送自己的端口及 IP 給 master。接下來,slave 繼續(xù)通過 replconf 發(fā)送 capa eof capa psync2 進行復制版本校驗。如果 master 校驗成功。從庫接下來就通過 psync 將自己的復制 id、復制偏移發(fā)送給 master,正式開始準備數(shù)據(jù)同步。

主庫接收到從庫發(fā)來的 psync 指令后,則開始判斷可以進行數(shù)據(jù)同步的方式。前面講到,Redis 當前保存了復制 id,replid 和 replid2。如果從庫發(fā)來的復制 id,與 master 的復制 id(即 replid 和 replid2)相同,并且復制偏移在復制緩沖積壓中,則可以進行增量同步。master 發(fā)送 continue 響應,并返回 master 的 replid。slave 將 master 的 replid 替換為自己的 replid,并將之前的復制 id 設置為 replid2。之后,master 則可繼續(xù)發(fā)送,復制偏移位置 之后的指令,給 slave,完成數(shù)據(jù)同步。

如果主庫發(fā)現(xiàn)從庫傳來的復制 id 和自己的 replid、replid2 都不同,或者復制偏移不在復制積壓緩沖中,則判定需要進行全量復制。master 發(fā)送 fullresync 響應,附帶 replid 及復制偏移。然后, master 根據(jù)需要構建 rdb,并將 rdb 及復制緩沖發(fā)送給 slave。

對于增量復制,slave 接下來就等待接受 master 傳來的復制緩沖及新增的寫指令,進行數(shù)據(jù)同步。

而對于全量同步,slave 會首先進行,嵌套復制的清理工作,比如 slave 當前還有嵌套的 子slave,則該 slave 會關閉嵌套 子slave 的所有連接,并清理自己的復制積壓緩沖。然后,slave 會構建臨時 rdb 文件,并從 master 連接中讀取 rdb 的實際數(shù)據(jù),寫入 rdb 中。在寫 rdb 文件時,每寫 8M,就會做一個 fsync操作, 刷新文件緩沖。當接受 rdb 完畢則將 rdb 臨時文件改名為 rdb 的真正名字。

接下來,slave 會首先清空老數(shù)據(jù),即刪除本地所有 DB 中的數(shù)據(jù),并暫時停止從 master 繼續(xù)接受數(shù)據(jù)。然后,slave 就開始全力加載 rdb 恢復數(shù)據(jù),將數(shù)據(jù)從 rdb 加載到內(nèi)存。在 rdb 加載完畢后,slave 重新利用與 master 的連接 socket,創(chuàng)建與 master 連接的 client,并在此注冊讀事件,可以開始接受 master 的寫指令了。此時,slave 還會將 master 的 replid 和復制偏移設為自己的復制 id 和復制偏移 offset,并將自己的 replid2 清空,因為,slave 的所有嵌套 子slave 接下來也需要進行全量復制。最后,slave 就會打開 aof 文件,在接受 master 的寫指令后,執(zhí)行完畢并寫入到自己的 aof 中。

相比之前的 sync,psync2 優(yōu)化很明顯。在短時間斷開連接、slave 重啟、切主等多種場景,只要延遲不太久,復制偏移仍然在復制積壓緩沖,均可進行增量同步。master 不用構建并發(fā)送巨大的 rdb,可以大大減輕 master 的負荷和網(wǎng)絡帶寬的開銷。同時,slave 可以通過輕量的增量復制,實現(xiàn)數(shù)據(jù)同步,快速恢復服務,減少系統(tǒng)抖動。

但是,psync 依然嚴重依賴于復制緩沖積壓,太大會占用過多內(nèi)存,太小會導致頻繁的全量復制。而且,由于內(nèi)存限制,即便設置相對較大的復制緩沖區(qū),在 slave 斷開連接較久時,仍然很容易被復制緩沖積壓沖刷,從而導致全量復制。

第28講:如何構建一個高性能、易擴展的Redis集群?

通過上一課時的學習,我們知道復制功能可以 N 倍提升 Redis 節(jié)點的讀性能,而集群則可以通過分布式方案來 N 倍提升 Redis 的寫性能。除了提升性能之外,Redis 集群還可以提供更大的容量,提升資源系統(tǒng)的可用性。

?

Redis 集群的分布式方案主要有 3 種。分別是 Client 端分區(qū)方案,Proxy 分區(qū)方案,以及原生的 Redis Cluster 分區(qū)方案。

Client 端分區(qū)

img

Client 端分區(qū)方案就是由 Client 決定數(shù)據(jù)被存儲到哪個 Redis 分片,或者由哪個 Redis 分片來獲取數(shù)據(jù)。它的核心思想是通過哈希算法將不同的 key 映射到固定的 Redis 分片節(jié)點上。對于單個 key 請求,Client 直接對 key 進行哈希后,確定 Redis 分片,然后進行請求。而對于一個請求附帶多個 key 的場景,Client 會首先將這些 key 按哈希分片進行分類,從而將一個請求分拆為多個請求,然后再分別請求不同的哈希分片節(jié)點。

Client 通過哈希算法將數(shù)據(jù)進行分布,一般采用的哈希算法是取模哈希、一致性哈希和區(qū)間分布哈希。前兩種哈希算法之前的課程已有詳細分析,此處不在贅述。對于區(qū)間分布哈希,實際是一種取模哈希的變種,取模哈希是哈希并取模計算后,按哈希值來分配存儲節(jié)點,而區(qū)間哈希是在哈希計算后,將哈希劃分為多個區(qū)間,然后將這些區(qū)間分配給存儲節(jié)點。如哈希后分 1024 個哈希點,然后將 0~511 作為分片 1,將 512~1023 作為分片 2。

對于 Client 端分區(qū),由于 Redis 集群有多個 master 分片,同時每個 master 下掛載多個 slave,每個 Redis 節(jié)點都有獨立的 IP 和端口。如果 master 異常需要切換 master,或讀壓力過大需要擴展新的 slave,這些都會涉及集群存儲節(jié)點的變更,需要 Client 端做連接切換。

img

為了避免 Client 頻繁變更 IP 列表,可以采用 DNS 的方式來管理集群的主從。對 Redis 集群的每個分片的主和從均采用不同 DNS 域名。Client 通過域名解析的方式獲取域名下的所有 IP,然后來訪問集群節(jié)點。由于每個分片 master 下有多個 slave,Client 需要在多個 slave 之間做負載均衡??梢园凑諜嘀亟⑴c slave 之間的連接,然后訪問時,輪詢使用這些連接依次訪問,即可實現(xiàn)按權重訪問 slave 節(jié)點。

在 DNS 訪問模式下,Client 需要異步定時探測主從域名,如果發(fā)現(xiàn) IP 變更,及時與新節(jié)點建立連接,并關閉老連接。這樣在主庫故障需要切換時,或者從庫需要增加減少時,任何分片的主從變化,只需運維或管理進程改一下 DNS 下的 IP 列表,業(yè)務 Client 端不需要做任何配置變更,即可正常切換訪問。

?

Client 端分區(qū)方案的優(yōu)點在于分區(qū)邏輯簡單,配置簡單,Client 節(jié)點之間和 Redis 節(jié)點之間均無需協(xié)調(diào),靈活性強。而且 Client 直接訪問對應 Redis 節(jié)點,沒有額外環(huán)節(jié),性能高效。但該方案擴展不便。在 Redis 端,只能成倍擴展,或者預先分配足夠多的分片。在 Client 端,每次分片后,業(yè)務端需要修改分發(fā)邏輯,并進行重啟。

Proxy 端分區(qū)

Proxy 端分區(qū)方案是指 Client 發(fā)送請求給 Proxy 請求代理組件,Proxy 解析 Client 請求,并將請求分發(fā)到正確的 Redis 節(jié)點,然后等待 Redis 響應,最后再將結果返回給 Client 端。

img

如果一個請求包含多個 key,Proxy 需要將請求的多個 key,按分片邏輯分拆為多個請求,然后分別請求不同的 Redis 分片,接下來等待Redis響應,在所有的分拆響應到達后,再進行聚合組裝,最后返回給 Client。在整個處理過程中,Proxy 代理首先要負責接受請求并解析,然后還要對 key 進行哈希計算及請求路由,最后還要將結果進行讀取、解析及組裝。如果系統(tǒng)運行中,主從變更或發(fā)生擴縮容,也只需由 Proxy 變更完成,業(yè)務 Client 端基本不受影響。

?

常見的 Proxy 端分區(qū)方案有 2 種,第一種是基于 Twemproxy 的簡單分區(qū)方案,第二種是基于Codis 的可平滑數(shù)據(jù)遷移的分區(qū)方案。

Twemproxy 是 Twitter 開源的一個組件,支持 Redis 和 Memcached 協(xié)議訪問的代理組件。在講分布式 Memecached 實戰(zhàn)時,我曾經(jīng)詳細介紹了它的原理和實現(xiàn)架構,此處不再贅述。總體而言,Twemproxy 實現(xiàn)簡單、穩(wěn)定性高,在一些訪問量不大且很少發(fā)生擴縮的業(yè)務場景中,可以很好的滿足需要。但由于 Twemproxy 是單進程單線程模型的,對包含多個 key 的 mutli 請求,由于需要分拆請求,然后再等待聚合,處理性能較低。而且,在后端 Redis 資源擴縮容,即增加或減少分片時,需要修改配置并重啟,無法做到平滑擴縮。而且 Twemproxy 方案默認只有一個代理組件,無管理后端,各種運維變更不夠便利。

img

而 Codis 是一個較為成熟的分布式 Redis 解決方案。對于業(yè)務 Client 訪問,連接 Codis-proxy 和連接單個 Redis 幾乎沒有區(qū)別。Codis 底層除了會自動解析分發(fā)請求之外,還可以在線進行數(shù)據(jù)遷移,使用非常方便。

Codis 系統(tǒng)主要由 Codis-server、Codis-proxy、Codis-dashboard、Zookeeper 等組成。

Codis-server 是 Codis 的存儲組件,它是基于 Redis 的擴展,增加了 slot 支持和數(shù)據(jù)遷移功能,所有數(shù)據(jù)存儲在預分配的 1024 個 slot 中,可以按 slot 進行同步或異步數(shù)據(jù)遷移。Codis-proxy 處理 Client 請求,解析業(yè)務請求,并路由給后端的 Codis-server group。Codis 的每個 server group 相當于一個 Redis 分片,由 1 個 master 和 N 個從庫組成。Zookeeper 用于存儲元數(shù)據(jù),如 Proxy 的節(jié)點,以及數(shù)據(jù)訪問的路由表。除了 Zookeeper,Codis 也支持 etcd 等其他組件,用于元數(shù)據(jù)的存儲和通知。Codis-dashboard 是 Codis 的管理后臺,可用于管理數(shù)據(jù)節(jié)點、Proxy 節(jié)點的加入或刪除,還可用于執(zhí)行數(shù)據(jù)遷移等操作。Dashboard 的各項變更指令通過 Zookeeper 進行分發(fā)。Codis 提供了功能較為豐富的管理后臺,可以方便的對整個集群進行監(jiān)控及運維。      

Proxy 端分區(qū)方案的優(yōu)勢,是 Client 訪問邏輯和 Redis 分布邏輯解耦,業(yè)務訪問便捷簡單。在資源發(fā)生變更或擴縮容時,只用修改數(shù)量有限的 Proxy 即可,數(shù)量龐大的業(yè)務 Client 端不用做調(diào)整。

但 Proxy 端分區(qū)的方案,訪問時請求需要經(jīng)過 Proxy 中轉,訪問多跳了一級,性能會存在損耗,一般損耗會達到 5~15% 左右。另外多了一個代理層,整個系統(tǒng)架構也會更復雜。

Redis Cluster 分區(qū)

Redis 社區(qū)版在 3.0 后開始引入 Cluster 策略,一般稱之為 Redis-Cluster 方案。Redis-Cluster 按 slot 進行數(shù)據(jù)的讀寫和管理,一個 Redis-Cluster 集群包含 16384 個 slot。每個 Redis 分片負責其中一部分 slot。在集群啟動時,按需將所有 slot 分配到不同節(jié)點,在集群系統(tǒng)運行后,按 slot 分配策略,將 key 進行 hash 計算,并路由到對應節(jié)點 訪問。

img

隨著業(yè)務訪問模型的變化,Redis 部分節(jié)點可能會出現(xiàn)壓力過大、訪問不均衡的現(xiàn)象,此時可以將 slot 在 Redis 分片節(jié)點內(nèi)部進行遷移,以均衡訪問。如果業(yè)務不斷發(fā)展,數(shù)據(jù)量過大、TPS過高,還可以將 Redis 節(jié)點的部分 slot 遷移到新節(jié)點,增加 Redis-Cluster 的分片,對整個 Redis 資源進行擴容,已提升整個集群的容量及讀寫能力。

?

在啟動 Redis 集群時,在接入數(shù)據(jù)讀寫前,可以通過 Redis 的 Cluster addslots 將 16384 個 slot 分配給不同的 Redis 分片節(jié)點,同時可以用 Cluster delslots 去掉某個節(jié)點的 slot,用 Cluster flushslots 清空某個節(jié)點的所有 slot 信息,來完成 slot 的調(diào)整。

Redis Cluster 是一個去中心化架構,每個節(jié)點記錄全部 slot 的拓撲分布。這樣 Client 如果把 key 分發(fā)給了錯誤的 Redis 節(jié)點,Redis 會檢查請求 key 所屬的 slot,如果發(fā)現(xiàn) key 屬于其他節(jié)點的 slot,會通知 Client 重定向到正確的 Redis 節(jié)點訪問。

Redis Cluster 下的不同 Redis 分片節(jié)點通過 gossip 協(xié)議進行互聯(lián),使用 gossip 的優(yōu)勢在于,該方案無中心控制節(jié)點,這樣,更新不會受到中心節(jié)點的影響,可以通過通知任意一個節(jié)點來進行管理通知。不足就是元數(shù)據(jù)的更新會有延時,集群操作會在一定的時延后才會通知到所有Redis。由于 Redis Cluster 采用 gossip 協(xié)議進行服務節(jié)點通信,所以在進行擴縮容時,可以向集群內(nèi)任何一個節(jié)點,發(fā)送 Cluster meet 指令,將新節(jié)點加入集群,然后集群節(jié)點會立即擴散新節(jié)點,到整個集群。meet 新節(jié)點操作的擴散,只需要有一條節(jié)點鏈能到達集群各個節(jié)點即可,無需 meet 所有集群節(jié)點,操作起來比較便利。

?

在 Redis-Cluster 集群中,key 的訪問需要 smart client 配合。Client 首先發(fā)送請求給 Redis 節(jié)點,Redis 在接受并解析命令后,會對 key 進行 hash 計算以確定 slot 槽位。計算公式是對 key 做 crc16 哈希,然后對 16383 進行按位與操作。如果 Redis 發(fā)現(xiàn) key 對應的 slot 在本地,則直接執(zhí)行后返回結果。

img

如果 Redis 發(fā)現(xiàn) key 對應的 slot 不在本地,會返回 moved 異常響應,并附帶 key 的 slot,以及該 slot 對應的正確 Redis 節(jié)點的 host 和 port。Client 根據(jù)響應解析出正確的節(jié)點 IP 和端口,然后把請求重定向到正確的 Redis,即可完成請求。為了加速訪問,Client 需要緩存 slot 與 Redis 節(jié)點的對應關系,這樣可以直接訪問正確的節(jié)點,以加速訪問性能。

?

Redis-Cluster 提供了靈活的節(jié)點擴縮容方案,可以在不影響用戶訪問的情況下,動態(tài)為集群增加節(jié)點擴容,或下線節(jié)點為集群縮容。由于擴容在線上最為常見,我首先來分析一下 Redis-Cluster 如何進行擴容操作。

在準備對 Redis 擴容時,首先準備待添加的新節(jié)點,部署 Redis,配置 cluster-enable 為 true,并啟動。然后運維人員,通過client連接上一個集群內(nèi)的 Redis 節(jié)點,通過 cluster meet 命令將新節(jié)點加入到集群,該節(jié)點隨后會通知集群內(nèi)的其他節(jié)點,有新節(jié)點加入。因為新加入的節(jié)點還沒有設置任何 slot,所以不接受任何讀寫操作。

然后,將通過 cluster setslot $slot importing 指令,在新節(jié)點中,將目標 slot 設為 importing 導入狀態(tài)。再將 slot 對應的源節(jié)點,通過 cluster setslot $slot migrating 將源節(jié)點的 slot 設為 migrating 遷移導出狀態(tài)。

?

接下來,就從源節(jié)點獲取待遷移 slot 的 key,通過 cluster getkeysinslot $slot $count 命令,從 slot 中獲取 N 個待遷移的 key。然后通過 migrate 指令,將這些 key 依次逐個遷移或批量一次遷移到目標新節(jié)點。對于遷移單個 key,使用指令 migrate $host $port $key $dbid timeout,如果一次遷移多個 key,在指令結尾加上 keys 選項,同時將多個 key 放在指令結尾即可。持續(xù)循環(huán)前面 2 個步驟,不斷獲取 slot 里的 key,然后進行遷移,最終將 slot 下的所有數(shù)據(jù)都遷移到目標新節(jié)點。最后通過 cluster setslot 指令將這個 slot 指派給新增節(jié)點。setslot 指令可以發(fā)給集群內(nèi)的任意一個節(jié)點,這個節(jié)點會將這個指派信息擴散到整個集群。至此,slot 就遷移到了新節(jié)點。如果要遷移多個 slot,可以繼續(xù)前面的遷移步驟,最終將所有需要遷移的 slot 數(shù)據(jù)搬到新節(jié)點。

這個新遷移 slot 的節(jié)點屬于主庫,對于線上應用,還需要增加從庫,以增加讀寫能力及可用性,否則一旦主庫崩潰,整個分片的數(shù)據(jù)就無法訪問。在節(jié)點上增加從庫,需要注意的是,不能使用非集群模式下的 slaveof 指令,而要使用 cluster replication,才能完成集群分片節(jié)點下的 slave 添加。另外,對于集群模式,slave 只能掛在分片 master 上,slave 節(jié)點自身不能再掛載 slave。

img

縮容流程與擴容流程類似,只是把部分節(jié)點的 slot 全部遷移走,然后把這些沒有 slot 的節(jié)點進行下線處理。在下線老節(jié)點之前,需要注意,要用 cluster forget 通知集群,集群節(jié)點要,從節(jié)點信息列表中,將目標節(jié)點移除,同時會將該節(jié)點加入到禁止列表,1 分鐘之內(nèi)不允許再加入集群。以防止在擴散下線節(jié)點時,又被誤加入集群。

Redis 社區(qū)官方在源代碼中也提供了 redis-trib.rb,作為 Redis Cluster 的管理工具。該工具用 Ruby 開發(fā),所以在使用前,需要安裝相關的依賴環(huán)境。redis-trib 工具通過封裝前面所述的 Redis 指令,從而支持創(chuàng)建集群、檢查集群、添加刪除節(jié)點、在線遷移 slot 等各種功能。

?

Redis Cluster 在 slot 遷移過程中,獲取key指令以及遷移指令逐一發(fā)送并執(zhí)行,不影響 Client 的正常訪問。但在遷移單條或多條 key 時,Redis 節(jié)點是在阻塞狀態(tài)下進行的,也就是說,Redis 在遷移 key 時,一旦開始執(zhí)行遷移指令,就會阻塞,直到遷移成功或確認失敗后,才會停止該 key 的遷移,從而繼續(xù)處理其他請求。slot 內(nèi)的 key 遷移是通過 migrate 指令進行的。

在源節(jié)點接收到 migrate $host $port $key $destination-db 的指令后,首先 slot 遷移的源節(jié)點會與遷移的目標節(jié)點建立 socket 連接,第一次遷移,或者遷移過程中,當前待遷移的 DB 與前一次遷移的 DB 不同,在遷移數(shù)據(jù)前,還需要發(fā)送 select $dbid 進行切換到正確的 DB。

img

然后,源節(jié)點會輪詢所有待遷移的 key/value。獲取 key 的過期時間,并將 value 進行序列化,序列化過程就是將 value 進行 dump,轉換為類 rdb 存儲的二進制格式。這個二進制格式分 3 部分。第一部分是 value 對象的 type。第二部分是 value 實際的二進制數(shù)據(jù);第三部分是當前 rdb 格式的版本,以及該 value 的 CRC64 校驗碼。至此,待遷移發(fā)送的數(shù)據(jù)準備完畢,源節(jié)點向目標節(jié)點,發(fā)送 restore-asking 指令,將過期時間、key、value 的二進制數(shù)據(jù)發(fā)送給目標節(jié)點。然后同步等待目標節(jié)點的響應結果。

?

目標節(jié)點對應的client,收到指令后,如果有 select 指令,就首先切換到正確的 DB。接下來讀取并處理 resotre-asking 指令,處理 restore-asking 指令時,首先對收到的數(shù)據(jù)進行解析校驗,獲取 key 的 ttl,校驗 rdb 版本及 value 數(shù)據(jù) cc64 校驗碼,確認無誤后,將數(shù)據(jù)存入 redisDb,設置過期時間,并返回響應。

源節(jié)點收到目標節(jié)點處理成功的響應后。對于非 copy 類型的 migrate,會刪除已遷移的 key。至此,key 的遷移就完成了。migrate 遷移指令,可以一次遷移一個或多個 key。注意,整個遷移過程中,源節(jié)點在發(fā)送 restore-asking 指令后,同步阻塞,等待目標節(jié)點完成數(shù)據(jù)處理,直到超時或者目標節(jié)點返回響應結果,收到結果后在本地處理完畢后序事件,才會停止阻塞,才能繼續(xù)處理其他事件。所以,單次遷移的 key 不能太多,否則阻塞時間會較長,導致 Redis 卡頓。同時,即便單次只遷移一個 key,如果對應的 value 太大,也可能導致 Redis 短暫卡頓。

?

在 slot 遷移過程中,不僅其他非遷移 slot 的 key 可以正常訪問,即便正在遷移的 slot,它里面的 key 也可以正常讀寫,不影響業(yè)務訪問。但由于 key 的遷移是阻塞模式,即在遷移 key 的過程中,源節(jié)點并不會處理任何請求,所以在 slot 遷移過程中,待讀寫的 key 只有三種存在狀態(tài)。

尚未被遷移,后續(xù)會被遷走;已經(jīng)被遷移;這個 key 之前并不存在集群中,是一個新 key。   

slot 遷移過程中,對節(jié)點里的 key 處理方式如下。

對于尚未被遷移的 key,即從 DB 中能找到該 key,不管這個 key 所屬的 slot 是否正在被遷移,都直接在本地進行讀寫處理。對于無法從 DB 中找到 value 的 key,但key所屬slot正在被遷移,包括已遷走或者本來不存在的 key 兩種狀態(tài),Redis 返回 ask 錯誤響應,并附帶 slot 遷移目標節(jié)點的 host 和 port。Client 收到 ask 響應后,將請求重定向到 slot 遷移的新節(jié)點,完成響應處理。對于無法從 DB 中找到 value 的 key,且 key 所在的 slot 不屬于本節(jié)點,說明 Client 發(fā)送節(jié)點有誤,直接返回 moved 錯誤響應,也附帶上 key 對應節(jié)點的 host 和 port,由 Client 重定向請求。     對于 Redis Cluster 集群方案,由社區(qū)官方實現(xiàn),并有 Redis-trib 集群工具,上線和使用起來比較便捷。同時它支持在線擴縮,可以隨時通過工具查看集群的狀態(tài)。但這種方案也存在不少弊端。首先,數(shù)據(jù)存儲和集群邏輯耦合,代碼邏輯復雜,容易出錯。

其次,Redis 節(jié)點要存儲 slot 和 key 的映射關系,需要額外占用較多內(nèi)存,特別是對 value size 比較小、而key相對較大的業(yè)務,影響更是明顯。

再次,key 遷移過程是阻塞模式,遷移大 value 會導致服務卡頓。而且,遷移過程,先獲取 key,再遷移,效率低。最后,Cluster 模式下,集群復制的 slave 只能掛載到 master,不支持 slave 嵌套,會導致 master 的壓力過大,無法支持那些,需要特別多 slave、讀 TPS 特別大的業(yè)務場景。

第29講:從容應對億級QPS訪問,Redis還缺少什么?

眾所周知,Redis 在線上實際運行時,面對海量數(shù)據(jù)、高并發(fā)訪問,會遇到不少問題,需要進行針對性擴展及優(yōu)化。本課時,我會結合微博在使用 Redis 中遇到的問題,來分析如何在生產(chǎn)環(huán)境下對 Redis 進行擴展改造,以應對百萬級 QPS。

功能擴展

對于線上較大流量的業(yè)務,單個 Redis 實例的內(nèi)存占用很容易達到數(shù) G 的容量,對應的 aof 會占用數(shù)十 G 的空間。即便每天流量低峰時間,對 Redis 進行 rewriteaof,減少數(shù)據(jù)冗余,但由于業(yè)務數(shù)據(jù)多,寫操作多,aof 文件仍然會達到 10G 以上。

此時,在 Redis 需要升級版本或修復 bug 時,如果直接重啟變更,由于需要數(shù)據(jù)恢復,這個過程需要近 10 分鐘的時間,時間過長,會嚴重影響系統(tǒng)的可用性。面對這種問題,可以對 Redis 擴展熱升級功能,從而在毫秒級完成升級操作,完全不影響業(yè)務訪問。

img

熱升級方案如下,首先構建一個 Redis 殼程序,將 redisServer 的所有屬性(包括redisDb、client等)保存為全局變量。然后將 Redis 的處理邏輯代碼全部封裝到動態(tài)連接庫 so 文件中。Redis 第一次啟動,從磁盤加載恢復數(shù)據(jù),在后續(xù)升級時,通過指令,殼程序重新加載 Redis 新的 so 文件,即可完成功能升級,毫秒級完成 Redis 的版本升級。而且整個過程中,所有 Client 連接仍然保留,在升級成功后,原有 Client 可以繼續(xù)進行讀寫操作,整個過程對業(yè)務完全透明。

?

在 Redis 使用中,也經(jīng)常會遇到一些特殊業(yè)務場景,是當前 Redis 的數(shù)據(jù)結構無法很好滿足的。此時可以對 Redis 進行定制化擴展??梢愿鶕?jù)業(yè)務數(shù)據(jù)特點,擴展新的數(shù)據(jù)結構,甚至擴展新的 Redis 存儲模型,來提升 Redis 的內(nèi)存效率和處理性能。

img

在微博中,有個業(yè)務類型是關注列表。關注列表存儲的是一個用戶所有關注的用戶 uid。關注列表可以用來驗證關注關系,也可以用關注列表,進一步獲取所有關注人的微博列表等。由于用戶數(shù)量過于龐大,存儲關注列表的 Redis 是作為一個緩存使用的,即不活躍的關注列表會很快被踢出 Redis。在再次需要這個用戶的關注列表時,重新從 DB 加載,并寫回 Redis。關注列表的元素全部 long,最初使用 set 存儲,回種 set 時,使用 sadd 進行批量添加。線上發(fā)現(xiàn),對于關注數(shù)比較多的關注列表,比如關注數(shù)有數(shù)千上萬個用戶,需要 sadd 上成千上萬個 uid,即便分幾次進行批量添加,每次也會消耗較多時間,數(shù)據(jù)回種效率較低,而且會導致 Redis 卡頓。另外,用 set 存關注列表,內(nèi)存效率也比較低。

于是,我們對 Redis 擴展了 longset 數(shù)據(jù)結構。longset 本質(zhì)上是一個 long 型的一維開放數(shù)組??梢圆捎?double-hash 進行尋址。

img

從 DB 加載到用戶的關注列表,準備寫入 Redis 前。Client 首先根據(jù)關注的 uid 列表,構建成 long 數(shù)組的二進制格式,然后通過擴展的 lsset 指令寫入 Redis。Redis 接收到指令后,直接將 Client 發(fā)來的二進制格式的 long 數(shù)組作為 value 值進行存儲。

longset 中的 long 數(shù)組,采用 double-hash 進行尋址,即對每個 long 值采用 2 個哈希函數(shù)計算,然后按 (h1 + n*h2)% 數(shù)組長度 的方式,確定 long 值的位置。n 從 0 開始計算,如果出現(xiàn)哈希沖突,即計算的哈希位置,已經(jīng)有其他元素,則 n 加 1,繼續(xù)向前推進計算,最大計算次數(shù)是數(shù)組的長度。

在向 longset 數(shù)據(jù)結構不斷增加 long 值元素的過程中,當數(shù)組的填充率超過閥值,Redis 則返回 longset 過滿的異常。此時 Client 會根據(jù)最新全量數(shù)據(jù),構建一個容量加倍的一維 long 數(shù)組,再次 lsset 回 Redis 中。

img

在移動社交平臺中,龐大的用戶群體,相互之間會關注、訂閱,用戶自己會持續(xù)分享各種狀態(tài),另外這些狀體數(shù)據(jù)會被其他用戶閱讀、評論、擴散及點贊。這樣,在用戶維度,就有關注數(shù)、粉絲數(shù)、各種狀態(tài)行為數(shù),然后用戶每發(fā)表的一條 feed、狀態(tài),還有閱讀數(shù)、評論數(shù)、轉發(fā)數(shù)、表態(tài)數(shù)等。一方面會有海量 key 需要進行計數(shù),另外一方面,一個 key 會有 N 個計數(shù)。在日常訪問中,一次查詢,不僅需要查詢大量的 key,而且對每個 key 需要查詢多個計數(shù)。

以微博為例,歷史計數(shù)高達千億級,而且隨著每日新增數(shù)億條 feed 記錄,每條記錄會產(chǎn)生 4~8 種計數(shù),如果采用 Redis 的計數(shù),僅僅單副本存儲,歷史數(shù)據(jù)需要占用 5~6T 以上的內(nèi)存,每日新增 50G 以上,如果再考慮多 IDC、每個 IDC 部署 1 主多從,占用內(nèi)存還要再提升一個數(shù)量級。由于微博計數(shù),所有的 key 都是隨時間遞增的 long 型值,于是我們改造了 Redis 的存儲結構。

首先采用 cdb 分段存儲計數(shù)器,通過預先分配的內(nèi)存數(shù)組 Table 存儲計數(shù),并且采用 double hash 解決沖突,避免 Redis 實現(xiàn)中的大量指針開銷。 然后,通過 Schema 策略支持多列,一個 key id 對應的多個計數(shù)可以作為一條計數(shù)記錄,還支持動態(tài)增減計數(shù)列,每列的計數(shù)內(nèi)存使用精簡到 bit。而且,由于 feed 計數(shù)冷熱區(qū)分明顯,我們進行冷熱數(shù)據(jù)分離存儲方案,根據(jù)時間維度,近期的熱數(shù)據(jù)放在內(nèi)存,之前的冷數(shù)據(jù)放在磁盤, 降低機器成本。

關于計數(shù)器服務的擴展,后面的案例分析課時,我會進一步深入介紹改造方案。

img

線上 Redis 使用,不管是最初的 sync 機制,還是后來的 psync 和 psync2,主從復制都會受限于復制積壓緩沖。如果 slave 斷開復制連接的時間較長,或者 master 某段時間寫入量過大,而 slave 的復制延遲較大,slave 的復制偏移量落在 master 的復制積壓緩沖之外,則會導致全量復制。

完全增量復制

于是,微博整合 Redis 的 rdb 和 aof 策略,構建了完全增量復制方案。

img

在完全增量方案中,aof 文件不再只有一個,而是按后綴 id 進行遞增,如 aof.00001、aof.00002,當 aof 文件超過閥值,則創(chuàng)建下一個 id 加 1 的文件,從而滾動存儲最新的寫指令。在 bgsave 構建 rdb 時,rdb 文件除了記錄當前的內(nèi)存數(shù)據(jù)快照,還會記錄 rdb 構建時間,對應 aof 文件的 id 及位置。這樣 rdb 文件和其記錄 aof 文件位置之后的寫指令,就構成一份完整的最新數(shù)據(jù)記錄。

主從復制時,master 通過獨立的復制線程向 slave 同步數(shù)據(jù)。每個 slave 會創(chuàng)建一個復制線程。第一次復制是全量復制,之后的復制,不管 slave 斷開復制連接有多久,只要 aof 文件沒有被刪除,都是增量復制。

第一次全量復制時,復制線程首先將 rdb 發(fā)給 slave,然后再將 rdb 記錄的 aof 文件位置之后的所有數(shù)據(jù),也發(fā)送給 slave,即可完成。整個過程不用重新構建 rdb。

img

后續(xù)同步時,slave 首先傳遞之前復制的 aof 文件的 id 及位置。master 的復制線程根據(jù)這個信息,讀取對應 aof 文件位置之后的所有內(nèi)容,發(fā)送給 slave,即可完成數(shù)據(jù)同步。

由于整個復制過程,master 在獨立復制線程中進行,所以復制過程不影響用戶的正常請求。為了減輕 master 的復制壓力,全增量復制方案仍然支持 slave 嵌套,即可以在 slave 后繼續(xù)掛載多個 slave,從而把復制壓力分散到多個不同的 Redis 實例。

集群管理

img

前面講到,Redis-Cluster 的數(shù)據(jù)存儲和集群邏輯耦合,代碼邏輯復雜易錯,存儲 slot 和 key 的映射需要額外占用較多內(nèi)存,對小 value 業(yè)務影響特別明顯,而且遷移效率低,遷移大 value 容易導致阻塞,另外,Cluster 復制只支持 slave 掛在 master 下,無法支持 需要較多slave、讀 TPS 特別大的業(yè)務場景。除此之外,Redis 當前還只是個存儲組件,線上運行中,集群管理、日常維護、狀態(tài)監(jiān)控報警等這些功能,要么沒有支持,要么支持不便。

因此我們也基于 Redis 構建了集群存儲體系。首先將 Redis 的集群功能剝離到獨立系統(tǒng),Redis 只關注存儲,不再維護 slot 等相關的信息。通過新構建的 clusterManager 組件,負責 slot 維護,數(shù)據(jù)遷移,服務狀態(tài)管理。

Redis 集群訪問可以由 proxy 或 smart client 進行。對性能特別敏感的業(yè)務,可以通過 smart client 訪問,避免訪問多一跳。而一般業(yè)務,可以通過 Proxy 訪問 Redis。

業(yè)務資源的部署、Proxy 的訪問,都通過配置中心進行獲取及協(xié)調(diào)。clusterManager 向配置中心注冊業(yè)務資源部署,并持續(xù)探測服務狀態(tài),根據(jù)服務狀態(tài)進行故障轉移,切主、上下線 slave 等。proxy 和 smart client 從配置中心獲取配置信息,并持續(xù)訂閱服務狀態(tài)的變化。

第十章:深入分布式緩存

第30講:面對海量數(shù)據(jù),為什么無法設計出完美的分布式緩存體系?

隨著互聯(lián)網(wǎng)的發(fā)展,分布式系統(tǒng)變得越來越重要,當前的大中型互聯(lián)網(wǎng)系統(tǒng)幾乎都向著分布式方向發(fā)展。分布式系統(tǒng)簡單說就是一個軟硬件分布在不同機房、不同區(qū)域的網(wǎng)絡計算機上,彼此之間僅僅通過消息傳遞進行通信及協(xié)調(diào)的系統(tǒng)。分布式系統(tǒng)需要利用分布的服務,在確保數(shù)據(jù)一致的基礎上,對外提供穩(wěn)定的服務。

CAP 定理的誕生

在分布式系統(tǒng)的發(fā)展中,影響最大最廣泛的莫過于 CAP 理論了,可以說 CAP 理論是分布式系統(tǒng)發(fā)展的理論基石。早在 1998 年,加州大學的計算機科學家 Eric Brewer ,就提出分布式系統(tǒng)的三個指標。在此基礎上,2 年后,Eric Brewer 進一步提出了 CAP 猜想。又過了 2 年,到了 2002 年,麻省理工學院的 Seth Gilbert 和 Nancy Lynch 從理論上證明了 CAP 猜想。CAP 猜想成為了 CAP 定理,也稱為布魯爾定理。從此,CAP 定理成為分布式系統(tǒng)發(fā)展的理論基石,廣泛而深遠的影響著分布式系統(tǒng)的發(fā)展。

CAP 定理指標

img

CAP 定理,簡單的說就是分布式系統(tǒng)不可能同時滿足 Consistency 一致性、Availability 可用性、Partition Tolerance 分區(qū)容錯性三個要素。因為 Consistency、Availability 、Partition Tolerance 這三個單詞的首字母分別是 C、A、P,所以這個結論被稱為 CAP 定理。

Consistency 一致性

img

CAP 定理的第一個要素是 Consistency 一致性。一致性的英文含義是指“all nodes see the same data at the same time”。即所有節(jié)點在任意時間,被訪問返回的數(shù)據(jù)完全一致。CAP 作者 Brewer 的另外一種解釋是在寫操作之后的讀指令,必須得到的是寫操作寫入的值,或者寫操作之后新更新的值。從服務端的視角來看,就是在 Client 寫入一個更新后,Server 端如何同步這個新值到整個系統(tǒng),從而保證整個系統(tǒng)的這個數(shù)據(jù)都相同。而從客戶端的視角來看,則是并發(fā)訪問時,在變更數(shù)據(jù)后,如何獲取到最新值。

Availability 可用性

img

CAP 定理的第二個要素是 Availability 可用性??捎眯缘挠⑽暮x是指“Reads and writes always succeed”。即服務集群總能夠對用戶的請求給予響應。Brewer 的另外一個種解釋是對于一個沒有宕機或異常的節(jié)點,總能響應用戶的請求。也就是說當用戶訪問一個正常工作的節(jié)點時,系統(tǒng)保證該節(jié)點必須給用戶一個響應,可以是正確的響應,也可以是一個老的甚至錯誤的響應,但是不能沒有響應。從服務端的視角來看,就是服務節(jié)點總能響應用戶請求,不會吞噬、阻塞請求。而從客戶端視角來看,發(fā)出的請求總有響應,不會出現(xiàn)整個服務集群無法連接、超時、無響應的情況。

Partition Tolerance 分區(qū)容錯性

img

第三個要素是 Partition Tolerance 分區(qū)容錯性。分區(qū)容錯的英文含義是指“The system continues to operate despite arbitrary message loss or failure of part of the system”。即出現(xiàn)分區(qū)故障或分區(qū)間通信異常時,系統(tǒng)仍然要對外提供服務。在分布式環(huán)境,每個服務節(jié)點都不是可靠的,不同服務節(jié)點之間的通信有可能出現(xiàn)問題。當某些節(jié)點出現(xiàn)異常,或者某些節(jié)點與其他節(jié)點之間的通信出現(xiàn)異常時,整個系統(tǒng)就產(chǎn)生了分區(qū)問題。從服務端的視角來看,出現(xiàn)節(jié)點故障、網(wǎng)絡異常時,服務集群仍然能對外提供穩(wěn)定服務,就是具有較好的分區(qū)容錯性。從客戶端視角來看,就是服務端的各種故障對自己透明。

正常服務場景

img

根據(jù)CAP定理,在分布式系統(tǒng)中這三個要素不可能三者兼顧,最多只能同時滿足兩點。接下來,我們用 最簡單的2 個服務節(jié)點場景,簡要證明一下 CAP 定理。

如圖所示,網(wǎng)絡上有 2 個服務節(jié)點 Node1 和 Node2,它們之間通過網(wǎng)絡連通組成一個分布式系統(tǒng)。在正常工作的業(yè)務場景,Node1 和 Node2 始終正常運行,且網(wǎng)絡一直良好連通。

假設某初始時刻,兩個節(jié)點中的數(shù)據(jù)相同,都是 V0,用戶訪問 Nodel 和 Node2 都會立即得到 V0 的響應。當用戶向 Node1 更新數(shù)據(jù),將 V0 修改為 V1時,分布式系統(tǒng)會構建一個數(shù)據(jù)同步操作 M,將 V1 同步給 Node2,由于 Node1 和 Node2 都正常工作,且相互之間通信良好,Node2 中的 V0 也會被修改為 V1。此時,用戶分別請求 Node1 和 Node2,得到的都是 V1,數(shù)據(jù)保持一致性,且總可以都得到響應。

網(wǎng)絡異常場景

img

作為一個分布式系統(tǒng),總是有多個分布的、需要網(wǎng)絡連接的節(jié)點,節(jié)點越多、網(wǎng)絡連接越復雜,節(jié)點故障、網(wǎng)絡異常的情況出現(xiàn)的概率就會越大。要完全滿足 CAP 三個元素。就意味著,如果節(jié)點之間出現(xiàn)了網(wǎng)絡異常時,需要支持網(wǎng)絡異常,即支持分區(qū)容錯性,同時分布式系統(tǒng)還需要滿足一致性和可用性。我們接下來看是否可行。

現(xiàn)在繼續(xù)假設,初始時刻,Node1 和 Node2 的數(shù)據(jù)都是 V0,然后此時 Node1 和 Node2 之間的網(wǎng)絡斷開。用戶向 Node1 發(fā)起變更請求,將 V0 變更為 V1,分布式系統(tǒng)準備發(fā)起同步操作 M,但由于 Node1 和 Node2 之間網(wǎng)絡斷開,同步操作 M 無法及時同步到 Node2,所以 Node2 中的數(shù)據(jù)仍然是 V0。

此時,有用戶向 Node2 發(fā)起請求,由于 Node2 與 Node1 斷開連接,數(shù)據(jù)沒有同步,Node2 無法立即向用戶返回正確的結果 V1。那怎么辦呢?有兩種方案。

第一種方案,是犧牲一致性,Node2 向請求用戶返回老數(shù)據(jù) V0 的響應。第二種方案,是犧牲可用性,Node2 持續(xù)阻塞請求,直到 Node1 和 Node2 之間的網(wǎng)絡連接恢復,并且數(shù)據(jù)更新操作 M 在 Node2 上執(zhí)行完畢,Node2 再給用戶返回正確的 V1 操作。

至此,簡要證明過程完畢。整個分析過程也就說明了,分布式系統(tǒng)滿足分區(qū)容錯性時,就無法同時滿足一致性和可用性,只能二選一,也就進一步證明了分布式系統(tǒng)無法同時滿足一致性、可用性、分區(qū)容錯性這三個要素。

CAP 權衡

CA

img

根據(jù) CAP 理論和前面的分析,我們知道分布式系統(tǒng)無法同時滿足一致性、可用性、分區(qū)容錯性三個要素,那我們在構建分布式系統(tǒng)時,應該如何選擇呢?

由于這三個要素對分布式系統(tǒng)都非常重要,既然三個不能同時滿足,那就先盡量滿足兩個,只舍棄其中的一個元素。

第一種方案選擇是 CA,即不支持分區(qū)容錯,只支持一致性和可用性。不支持分區(qū)容錯性,也就意味著不允許分區(qū)異常,設備、網(wǎng)絡永遠處于理想的可用狀態(tài),從而讓整個分布式系統(tǒng)滿足一致性和可用性。

但由于分布式系統(tǒng)是由眾多節(jié)點通過網(wǎng)絡通信連接構建的,設備故障、網(wǎng)絡異常是客觀存在的,而且分布的節(jié)點越多,范圍越廣,出現(xiàn)故障和異常的概率也越大,因此,對于分布式系統(tǒng)而言,分區(qū)容錯 P 是無法避免的,如果避免了 P,只能把分布式系統(tǒng)回退到單機單實例系統(tǒng)。

CP

img

第二種方案選擇是 CP,因為分區(qū)容錯 P 客觀存在,即相當于放棄系統(tǒng)的可用性,換取一致性。那么系統(tǒng)在遇到分區(qū)異常時,會持續(xù)阻塞整個服務,直到分區(qū)問題解決,才恢復對外服務,這樣可以保證數(shù)據(jù)的一致性。選擇 CP 的業(yè)務場景比較多,特別是對數(shù)據(jù)一致性特別敏感的業(yè)務最為普遍。比如在支付交易領域,Hbase 等分布式數(shù)據(jù)庫領域,都要優(yōu)先保證數(shù)據(jù)的一致性,在出現(xiàn)網(wǎng)絡異常時,系統(tǒng)就會暫停服務處理。分布式系統(tǒng)中,用來分發(fā)及訂閱元數(shù)據(jù)的 Zookeeper,也是選擇優(yōu)先保證 CP 的。因為數(shù)據(jù)的一致性是這些系統(tǒng)的基本要求,否則,銀行系統(tǒng)0 余額大量取現(xiàn),數(shù)據(jù)庫系統(tǒng)訪問,隨機返回新老數(shù)據(jù)都會引發(fā)一系列的嚴重問題。

AP

img

第三種方案選擇是 AP,由于分區(qū)容錯 P 客觀存在,即相當于放棄系統(tǒng)數(shù)據(jù)的一致性,換取可用性。這樣,在系統(tǒng)遇到分區(qū)異常時,節(jié)點之間無法通信,數(shù)據(jù)處于不一致的狀態(tài),為了保證可用性,服務節(jié)點在收到用戶請求后立即響應,那只能返回各自新老不同的數(shù)據(jù)。這種舍棄一致性,而保證系統(tǒng)在分區(qū)異常下的可用性,在互聯(lián)網(wǎng)系統(tǒng)中非常常見。比如微博多地部署,如果不同區(qū)域的網(wǎng)絡中斷,區(qū)域內(nèi)的用戶仍然發(fā)微博、相互評論和點贊,但暫時無法看到其他區(qū)域用戶發(fā)布的新微博和互動狀態(tài)。對于微信朋友圈也是類似。還有如 12306 的火車購票系統(tǒng),在節(jié)假日高峰期搶票時,偶爾也會遇到,反復看到某車次有余票,但每次真正點擊購買時,卻提示說沒有余票。這樣,雖然很小一部分功能受限,但系統(tǒng)整體服務穩(wěn)定,影響非常有限,相比 CP,用戶體驗會更佳。

CAP 問題及誤區(qū)

img

CAP 理論極大的促進了分布式系統(tǒng)的發(fā)展,但隨著分布式系統(tǒng)的演進,大家發(fā)現(xiàn),其實 CAP 經(jīng)典理論其實過于理想化,存在不少問題和誤區(qū)。

首先,以互聯(lián)網(wǎng)場景為例,大中型互聯(lián)網(wǎng)系統(tǒng),主機數(shù)量眾多,而且多區(qū)域部署,每個區(qū)域有多個 IDC。節(jié)點故障、網(wǎng)絡異常,出現(xiàn)分區(qū)問題很常見,要保證用戶體驗,理論上必須保證服務的可用性,選擇 AP,暫時犧牲數(shù)據(jù)的一致性,這是最佳的選擇。

但是,當分區(qū)異常發(fā)生時,如果系統(tǒng)設計的不夠良好,并不能簡單的選擇可用性或者一致性。例如,當分區(qū)發(fā)生時,如果一個區(qū)域的系統(tǒng)必須要訪問另外一個區(qū)域的依賴子服務,才可以正常提供服務,而此時網(wǎng)絡異常,無法訪問異地的依賴子服務,這樣就會導致服務的不可用,無法支持可用性。同時,對于數(shù)據(jù)的一致性,由于網(wǎng)絡異常,無法保證數(shù)據(jù)的一致性,各區(qū)域數(shù)據(jù)暫時處于不一致的狀態(tài)。在網(wǎng)絡恢復后,由于待同步的數(shù)據(jù)眾多且復雜,很容易出現(xiàn)不一致的問題,同時某些業(yè)務操作可能跟執(zhí)行順序有關,即便全部數(shù)據(jù)在不同區(qū)域間完成同步,但由于執(zhí)行順序不同,導致最后結果也會不一致。長期多次分區(qū)異常后,會累積導致大量的數(shù)據(jù)不一致,從而持續(xù)影響用戶體驗。

?

其次,在分布式系統(tǒng)中,分區(qū)問題肯定會發(fā)生,但卻很少發(fā)生,或者說相對于穩(wěn)定工作的時間,會很短且很小概率。當不存在分區(qū)時,不應該只選擇 C 或者 A,而是可以同時提供一致性和可用性。

再次,同一個系統(tǒng)內(nèi),不同業(yè)務,同一個業(yè)務處理的不同階段,在分區(qū)發(fā)生時,選擇一致性和可用性的策略可能都不同。比如前面講的 12306 購票系統(tǒng),車次查詢功能會選擇 AP,購票功能在查詢階段也選擇 AP,但購票功能在支付階段,則會選擇 CP。因此,在系統(tǒng)架構或功能設計時,并不能簡單選擇 AP 或者 CP。

而且,系統(tǒng)實際運行中,對于 CAP 理論中的每個元素,實際并不都是非黑即白的。比如一致性,有強一致性,也有弱一致性,即便暫時大量數(shù)據(jù)不一致,在經(jīng)歷一段時間后,不一致數(shù)據(jù)會減少,不一致率會降低。又如可用性,系統(tǒng)可能會出現(xiàn)部分功能異常,其他功能正常,或者壓力過大,只能支持部分用戶的請求的情況。甚至分區(qū)也可以有一系列中間狀態(tài),區(qū)域網(wǎng)絡完全中斷的情況較少,但網(wǎng)絡通信條件卻可以在 0~100% 之間連續(xù)變化,而且系統(tǒng)內(nèi)不同業(yè)務、不同功能、不同組件對分區(qū)還可以有不同的認知和設置。

最后,CAP 經(jīng)典理論,沒有考慮實際業(yè)務中網(wǎng)絡延遲問題,延遲自始到終都存在,甚至分區(qū)異常P都可以看作一種延遲,而且這種延遲可以是任意時間,1 秒、1 分鐘、1 小時、1 天都有可能,此時系統(tǒng)架構和功能設計時就要考慮,如何進行定義區(qū)分及如何應對。

這些問題,傳統(tǒng)的 CAP 經(jīng)典理論并沒有給出解決方案,開發(fā)者如果簡單進行三選二,就會進入誤區(qū),導致系統(tǒng)在運行中問題連連。

第31講:如何設計足夠可靠的分布式緩存體系,以滿足大中型移動互聯(lián)網(wǎng)系統(tǒng)的需要?

上一課時我們了解了為什么不能設計出同時滿足一致性、可用性、分區(qū)容錯性的分布式系統(tǒng),本課時我們來具體看下,工作中應該如何設計分布式系統(tǒng),以滿足大中型互聯(lián)網(wǎng)系統(tǒng)的需求。

傳統(tǒng) CAP 的突破

隨著分布式系統(tǒng)的不斷演進,會不斷遇到各種問題,特別是當前,在大中型互聯(lián)網(wǎng)系統(tǒng)的演進中,私有云、公有云并行發(fā)展且相互融合,互聯(lián)網(wǎng)系統(tǒng)的部署早已突破單個區(qū)域,系統(tǒng)拓撲走向全國乃至全球的多區(qū)域部署。在踐行傳統(tǒng)的經(jīng)典 CAP 理論的同時,需要認識到 CAP 三要素的復雜性,不能簡單的對 CAP 理論進行三選二,需要根據(jù)業(yè)務特點、部署特點,對 CAP 理論進行創(chuàng)新、修正及突破。

img

甚至 CAP 理論的提出者 Eric Brewer 自己也在 CAP 理論提出的 12 年后,即在 2012 年,對 CAP 理論,特別是 CAP 使用中的一些誤區(qū),進一步進行修正、拓展及演進說明。Brewer 指出,CAP 理論中經(jīng)典的三選二公式存在誤導性,CAP 理論的經(jīng)典實踐存在過于簡化三種要素,以及三要素之間的相互關系的問題。他同時把 CAP 與 ACID、BASE 進行比較,分析了 CAP 與延遲的關系,最后還重點分析了分布式系統(tǒng)如何應對分區(qū)異常的問題。

要突破經(jīng)典的 CAP 理論和實踐,要認識到 CAP 三要素都不是非黑即白,而是存在一系列的可能性,要在實際業(yè)務場景中對分布式系統(tǒng),進行良好的架構設計,這是一個很大的挑戰(zhàn)。

在系統(tǒng)實際運行過程中,大部分時間,分區(qū)異常不會發(fā)生,此時可以提供良好的一致性和可用性。同時,我們需要在系統(tǒng)架構設計中,在分析如何實現(xiàn)業(yè)務功能、系統(tǒng) SLA 指標實現(xiàn)等之外,還要考慮整個系統(tǒng)架構中,各個業(yè)務、模塊、功能、系統(tǒng)部署如何處理潛在的分區(qū)問題。

img

要良好處理潛在的分區(qū)問題,可以采用如下步驟。

首先,要考慮如何感知分區(qū)的發(fā)生,可以通過主動探測、狀態(tài)匯報、特殊時間/特殊事件預警、歷史數(shù)據(jù)預測等方式及時發(fā)現(xiàn)分區(qū)。

其次,如果發(fā)現(xiàn)分區(qū),如何在分區(qū)模式下進行業(yè)務處理??梢圆捎脙?nèi)存緩沖、隊列服務保存數(shù)據(jù)后,繼續(xù)服務,也可以對敏感功能直接停止服務,還可以對分區(qū)進行進一步細分,如果是短時間延遲,可以部分功能或請求阻塞等待結果,其他功能和請求快速返回本地老數(shù)據(jù);如果分區(qū)時長超過一定閥值,進行部分功能下線,只提供部分核心功能。

最后,在分區(qū)異?;謴秃?#xff0c;如何同步及修復數(shù)據(jù),建立補償機制應對分區(qū)模式期間的錯誤。如系統(tǒng)設計中引入消息隊列,在分區(qū)模式期間,變更的數(shù)據(jù)用消息隊列進行保存,分區(qū)恢復后,消息處理機從消息隊列中進行數(shù)據(jù)讀取及修復。也可以設計為同步機制,分區(qū)異常時,記錄最后同步的位置點,分區(qū)恢復后,從記錄的位置點繼續(xù)同步數(shù)據(jù)。還可以在分區(qū)時,分布式系統(tǒng)的各區(qū)記錄自己沒有同步出去的數(shù)據(jù),然后在分區(qū)恢復后,主動進行異地數(shù)據(jù)比較及合并。最后,還可以在故障恢復后通過數(shù)據(jù)掃描,對比分區(qū)數(shù)據(jù),進行比較及修復。

BASE 理論

BASE 理論最初由 Brewer 及他的同事們提出。雖然比較久遠,但在當前的互聯(lián)網(wǎng)界活力更盛。各大互聯(lián)網(wǎng)企業(yè),在構建大中型規(guī)模的分布式互聯(lián)網(wǎng)系統(tǒng),包括各種基于私有云、公有云及多云結合的分布式系統(tǒng)時,在盡力借鑒 CAP 理論與實踐的同時,還充分驗證和實踐了 BASE 理論,并將其作為 CAP 理論的一種延伸,很好的應用在互聯(lián)網(wǎng)各種系統(tǒng)中。

BASE 理論及實踐是分布式系統(tǒng)對一致性和可用性權衡后的結果。其基本思想是分布式系統(tǒng)各個功能要適當權衡,盡力保持整個系統(tǒng)穩(wěn)定可用,即便在出現(xiàn)局部故障和異常時,也確保系統(tǒng)的主體功能可用,確保系統(tǒng)的最終一致性。

img

BASE 理論也包括三要素,即 Basically Availabe 基本可用、Soft state 軟狀態(tài)和 Eventual Consistency 最終一致性。

Basically Availabe 基本可用

基本可用是指分布式系統(tǒng)在出現(xiàn)故障時,允許損失部分可用性。比如可以損失部分 SLA,如響應時間適當增加、處理性能適當下降,也可以損失部分周邊功能、甚至部分核心功能。最終保證系統(tǒng)的主體基本穩(wěn)定,核心功能基本可用的狀態(tài)。如淘寶、京東在雙十一峰值期間,請求會出現(xiàn)變慢,但少許延遲后,仍然會返回正確結果,同時還會將部分請求導流到降級頁面等。又如微博在突發(fā)故障時,會下線部分周邊功能,將資源集中用于保障首頁 feed 刷新、發(fā)博等核心功能。

Soft state 軟狀態(tài)

軟狀態(tài)是指允許系統(tǒng)存在中間狀態(tài)。故障發(fā)生時,各分區(qū)之間的數(shù)據(jù)同步出現(xiàn)延時或暫停,各區(qū)域的數(shù)據(jù)處于不一致的狀態(tài),這種狀態(tài)的出現(xiàn),并不影響系統(tǒng)繼續(xù)對外提供服務。這種節(jié)點不一致的狀態(tài)和現(xiàn)象就是軟狀態(tài)。

Eventual Consistency 最終一致性

最終一致性,是指分布式系統(tǒng)不需要實時保持強一致狀態(tài),在系統(tǒng)故障發(fā)生時,可以容忍數(shù)據(jù)的不一致,在系統(tǒng)故障恢復后,數(shù)據(jù)進行同步,最終再次達到一致的狀態(tài)。

BASE 理論是面向大中型分布式系統(tǒng)提出的,它更適合當前的大中型互聯(lián)網(wǎng)分布式系統(tǒng)。

首先用戶體驗第一,系統(tǒng)設計時要優(yōu)先考慮可用性。其次,在故障發(fā)生時,可以犧牲部分功能的可用性,犧牲數(shù)據(jù)的強一致性,來保持系統(tǒng)核心功能的可用性。最后,在系統(tǒng)故障恢復后,通過各種策略,確保系統(tǒng)最終再次達到一致。

一致性問題及應對

分布式系統(tǒng)中,為了保持系統(tǒng)的可用性和性能,系統(tǒng)中的數(shù)據(jù)需要存儲多個副本,這些副本分布在不同的物理機上,如果服務器、網(wǎng)絡出現(xiàn)故障,就會導致部分數(shù)據(jù)副本寫入成功,部分數(shù)據(jù)副本寫入失敗,這就會導致各個副本之間數(shù)據(jù)不一致,數(shù)據(jù)內(nèi)容沖突,也就造成了數(shù)據(jù)的不一致。因此,為了保持分布式系統(tǒng)的一致性,核心就是如何解決分布式系統(tǒng)中的數(shù)據(jù)一致性。

保持數(shù)據(jù)一致性的方案比較多,比較常見的方案有,分布式事務,主從復制,業(yè)務層消息總線等。

分布式事務

分布式事務在各節(jié)點均能正常執(zhí)行事務內(nèi)一系列操作才會提交,否則就進行回滾,可以保持系統(tǒng)內(nèi)數(shù)據(jù)的強一致。分布式事務應用比較廣泛,比如跨行轉賬,用戶甲向用戶乙轉賬,甲賬戶需要減少,乙賬戶需要增加對應金額,這兩個操作就必須構成一個分布式事務。還有其他場景,比如 12306 中支付出票、支付寶買入基金等,都需要保持對應操作的事務性。

img

分布式事務的具體方案較多,典型有 2PC 兩階段提交、3PC 三階段提交、Paxos、Zab、Raft等。

兩階段提交方案中,系統(tǒng)包括兩類節(jié)點,一類是協(xié)調(diào)者,一類是事務參與者。協(xié)調(diào)者一般只有一個,參與者可以理解為數(shù)據(jù)副本的數(shù)量,一般有多個。

兩階段提交的執(zhí)行分為請求階段和提交階段兩部分。在請求階段,協(xié)調(diào)者將通知事務參與者準備提交或取消事務,通知完畢后,事務參與者就開始進行表決。在表決中,參與者如果本地作業(yè)執(zhí)行成功,則表決同意,如果執(zhí)行失敗,則表決取消,然后把表決回復給協(xié)調(diào)者。然后進入提交階段。

在提交階段,協(xié)調(diào)者將基于第一階段的表決結果進行決策是提交事務還是取消事務。決策方式是所有參與者表決同意則決策提交,否則決策取消。然后協(xié)調(diào)者把決策結果分發(fā)給所有事務參與者。事務參與者接受到協(xié)調(diào)者的決策后,執(zhí)行對應的操作。

三階段提交與兩階段提交類似,只是在協(xié)調(diào)者、參與者都引入了超時機制,而且把兩階段提交中的第一階段分拆成了 2 步,即先詢問再鎖資源。

分布式事務中 Paxos、Zab、Raft 等方案的基本思想類似。在每個數(shù)據(jù)副本附帶版本信息,每次寫操作保證寫入大于 N/2 個節(jié)點,同時每次讀操作也保證從大于 N/2 個節(jié)點讀,以多數(shù)派作為最終決策。這種仲裁方式在業(yè)界使用比較廣泛,比如亞馬遜的 Dynamo 存儲也是類似,Dynamo 的決策更簡潔,只要寫操作數(shù) + 讀操作數(shù)大于節(jié)點數(shù)即可。一般整個仲裁過程由協(xié)調(diào)者進行,當然也可以像 Dynamo那樣,支持由業(yè)務 Client 決策也沒問題,更有彈性,因為可以由業(yè)務按各種策略選擇。在仲裁后,仲裁者可以選擇正確的版本數(shù)據(jù),甚至在某些場景下可以將不同版本的數(shù)據(jù)合并成一個新數(shù)據(jù)。

主從復制

主從復制也是一種使用較為廣泛的一致性方案。在 Mysql 等各種 DB 中廣泛使用,之前課程中講到的 Redis 也是采用主從復制來保持主從數(shù)據(jù)一致的。

除了從數(shù)據(jù)層保證一致性,還可以在上層業(yè)務層,通過消息總線分發(fā),來更新緩存及存儲體系,這也是互聯(lián)網(wǎng)企業(yè)在進行異地多活方案設計時經(jīng)常會考慮到的方案。

消息總線在各區(qū)域相互分發(fā)消息,有 push 推和 pull 拉兩種方案。一般來講,pull 拉的方式,由于拉取及拉取后的執(zhí)行過程對分發(fā)是可以感知,在網(wǎng)絡異常時,更容易保障數(shù)據(jù)的一致性。

分布式系統(tǒng)多區(qū)數(shù)據(jù)一致性案例

img

如圖所示,是微博進行多區(qū)數(shù)據(jù)一致性保障案例。消息是通過消息中間件 wmb 進行分發(fā)的。wmb 兩邊分別為分布式系統(tǒng)的 2 個區(qū)域。每個區(qū)域所有的用戶寫操作,都會封裝成一條消息,業(yè)務消息會首先寫入消息隊列服務,然后消息隊列處理機讀取消息隊列,并進行緩存和 DB 的更新。在業(yè)務消息寫入消息隊列服務時,wmb 會同時將這條消息分發(fā)給其他所有異地區(qū)子系統(tǒng)。分發(fā)的方式是,wmb 本地組件先將消息寫入本地隊列,然后 wmb 異地組件 Client 再讀取。當分區(qū)故障發(fā)生時,異地讀取失敗,消息仍然在各區(qū)的消息隊列中,不會丟失。分區(qū)故障過程中,系統(tǒng)的各區(qū)子系統(tǒng)只處理本地事件。在分區(qū)故障排除后,wmb Client 繼續(xù)讀取異地消息,然后由消息處理機執(zhí)行,最終實現(xiàn)數(shù)據(jù)的一致性。

由于 wmb 通過消息隊列機方式從業(yè)務層面進行同步,分區(qū)故障發(fā)生時,各區(qū)都是先執(zhí)行本地,分區(qū)恢復后再執(zhí)行異地,所有事件在各區(qū)的執(zhí)行順序可能會有差異,在某些極端場景下,可能會導致數(shù)據(jù)不一致。所以,微博只用 wmb 來更新緩存,DB 層仍然采用主從復制的方式進行強一致保障。這樣即便故障恢復期間,可能存在少量緩存數(shù)據(jù)暫時不一致,由于恢復數(shù)據(jù)時采用了更短的過期時間,這部分數(shù)據(jù)在從 DB 重新加載后,仍然能保持數(shù)據(jù)的最終一致性。同時,微博不用 DB 數(shù)據(jù)更新緩存,是由于緩存數(shù)據(jù)結構過于復雜,而且經(jīng)常需要根據(jù)業(yè)務需要進行擴展,一條緩存記錄會涉及眾多 DB,以及 Redis 中多項紀錄,通過 DB 同步數(shù)據(jù)觸發(fā)更新緩存涉及因素太多,不可控。所以微博在嘗試 DB 驅動緩存更新方案失敗后,就改為 wmb 消息隊列方式進行緩存更新。

第32講:一個典型的分布式緩存系統(tǒng)是什么樣的?

本課時我們具體看下一個典型的分布式緩存系統(tǒng)是什么樣的。

分布式 Redis 服務

由于本課程聚焦于緩存,接下來,我將以微博內(nèi)的 分布式 Redis 服務系統(tǒng)為例,介紹一個典型的分布式緩存系統(tǒng)的組成。

img

微博的 Redis 服務內(nèi)部也稱為 RedisService。RedisService 的整體架構如圖所示。主要分為Proxy、存儲、集群管理、配置中心、Graphite,5 個部分。

RedisService 中的 Proxy 是無狀態(tài)多租戶模型,每個 Proxy 下可以掛載不同的業(yè)務存儲,通過端口進行業(yè)務區(qū)分。存儲基于 Redis 開發(fā),但在集群數(shù)據(jù)存儲時,只保留了基本的存儲功能,支持定制的遷移功能,但存儲內(nèi)部無狀態(tài),不存儲 key-slot 映射關系。配置中心用于記錄及分發(fā)各種元數(shù)據(jù),如存儲 Proxy 的 IP、端口、配置等,在發(fā)生變化時,訂閱者可以及時感知。Graphite 系統(tǒng)用于記錄并展現(xiàn)系統(tǒng)、業(yè)務,組件以及實例等的狀態(tài)數(shù)據(jù)。ClusterManager 用于日常運維管理,業(yè)務 SLA 監(jiān)控,報警等。同時 ClusterManager 會整合 Proxy、Redis 后端存儲以及配置中心,對業(yè)務數(shù)據(jù)進行集群管理

多租戶 Proxy

img

RedisService 中的 Proxy 無任何狀態(tài),所有 Proxy 實例的啟動參數(shù)相同。但 Proxy 啟動前,clusterManager 會在配置中心設置該實例的業(yè)務及存儲配置信息,Proxy 啟動后,到配置中心通過自己的 IP 來獲取并訂閱配置,然后進行初始化。Proxy 與后端 Redis 存儲采用長連接,當 Client 并發(fā)發(fā)送請求到 Proxy 后,Proxy 會將請求進行打包,并發(fā)地以 pipeline 的方式批量訪問后端,以提升請求效率。對于多租戶 Proxy,由于不同業(yè)務的存儲位置可能不同,因此對每個請求需要進行業(yè)務區(qū)分,一般有 2 種方式進行區(qū)分。

方案 1,按照 key 的 namespace 前綴進行業(yè)務區(qū)分,比如 Client 分別請求 user、graph、feed 業(yè)務下的 key k1,業(yè)務 Client 分別構建 {user}k1、{graph}k1、{feed}k1,然后發(fā)送給 Proxy,Proxy 解析 key 前綴確定 key 對應的業(yè)務。

方案 2,對每個業(yè)務分配一個業(yè)務端口,不同業(yè)務訪問自己的端口,Proxy 會根據(jù)端口確定業(yè)務類型。這種類型不需要解析 key 前綴,不需要重構請求,性能更為高效。但需要為業(yè)務配置端口,增加管理成本,實踐上,由于業(yè)務 Redis 資源一般會采用不同端口,所以業(yè)務 Proxy 可以采用業(yè)務資源分片的最小端口來作為業(yè)務端口標志。

Redis 數(shù)據(jù)存儲

img

RedisService 中的 Redis 存儲基于 Redis 5.0 擴展,內(nèi)部稱 wredis,wredis 不存儲 key-slot 映射,只記錄當前實例中存儲的 slot 的 key 計數(shù)。wredis 處理任何收到的操作命令,而數(shù)據(jù)分片訪問的正確性由訪問端確保。在每日低峰時段,clusterManager 對 Redis 存儲進行掃描,發(fā)現(xiàn) slot 存儲是否存在異常。因為微博中有大量的小 value key,如果集群中增加 key-slot 映射,會大大增大存儲成本,通過消除 key-slot 映射等相關優(yōu)化,部分業(yè)務可以減少 20% 以上的存儲容量。

wredis 支持 slot 的同步遷移及異步遷移。同時支持熱升級,可以毫秒級完成組件升級。wredis 也支持全增量復制,支持微博內(nèi)部擴展的多種數(shù)據(jù)結構。熱升級、全增量復制、數(shù)據(jù)結構擴展等,在之前的課時中有介紹,具體可以參考之前講的“Redis 功能擴展”課時的內(nèi)容。

配置中心 configService

img

微博的配置中心,內(nèi)部稱為 configService,是微博內(nèi)部配置元數(shù)據(jù)管理的基礎組件。configService 自身也是多 IDC 部署的,配置信息通過多版本數(shù)據(jù)結構存儲,支持版本回溯。同時配置數(shù)據(jù)可以通過 merkle hash 樹進行快速一致性驗證。RedisService 中的所有業(yè)務、資源、Proxy 的配置都存儲在 configService 中,由 cluster 寫入并變更,Proxy、業(yè)務 Client 獲取并訂閱所需的配置數(shù)據(jù)。configService 在配置節(jié)點發(fā)生變更時,會只對節(jié)點進行事件通知,訂閱者無需獲取全量數(shù)據(jù),可以大大減輕配置變更后的獲取開銷。

ClusterManager 是一個運維后臺。主要用于運維工作,如后端資源、Proxy 的實例部署,配置變更,版本升級等。也用于數(shù)據(jù)的集群管理,clusterManager 內(nèi)部會存儲業(yè)務數(shù)據(jù)的集群映射,并在必要時進行數(shù)據(jù)遷移和故障轉移。遷移采用 slot 方式,可以根據(jù)負載進行遷移流量控制,同時會探測集群內(nèi)的節(jié)點狀態(tài),如在 wredis 的 master 異常后,從 slave 中選擇一個新的master,并重建主從關系。clusterManager 還支持業(yè)務訪問的 Proxy 域名管理,監(jiān)控集群節(jié)點的實例狀態(tài),監(jiān)控業(yè)務的 SLA 指標,對異常進行報警,以便運維及時進行處理。

集群數(shù)據(jù)同步

img

RedisService 中的數(shù)據(jù)存儲在多個區(qū)域,每個區(qū)域都有多個 IDC。部署方式是核心內(nèi)網(wǎng)加公有云的方式。使用公有云,主要是由微博的業(yè)務特點決定的,在突發(fā)事件或熱點事件發(fā)生時,很容易形成流量洪峰,讀寫 TPS 大幅增加,利用公有云可以快速、低成本的擴展系統(tǒng),大幅增加系統(tǒng)處理能力。根據(jù)業(yè)務特點,wredis 被分為緩存和存儲類型。對于 Redis 緩存主要通過消息總線進行驅動更新,而對于 Redis 存儲則采用主從復制更新。更新方式不同,主要是因為 Redis 作為緩存類型的業(yè)務數(shù)據(jù),在不同區(qū)或者不同 IDC 的熱點數(shù)據(jù)不同,如果采用主從復制,部署從庫的 IDC,會出現(xiàn)熱數(shù)據(jù)無法進入緩存,同時冷數(shù)據(jù)無法淘汰的問題,因為從庫的淘汰也要依賴主庫進行。而對于 Redis 作存儲的業(yè)務場景,由于緩存存放全量數(shù)據(jù),直接采用主從復制進行數(shù)據(jù)一致性保障,這樣最便捷。

第十一章:應用場景案例解析

第33講:如何為秒殺系統(tǒng)設計緩存體系?

本課時我們具體講解如何為秒殺系統(tǒng)設計緩存體系。

秒殺系統(tǒng)分析

互聯(lián)網(wǎng)電商為了吸引人氣,經(jīng)常會對一些商品進行低價秒殺售賣活動。比如幾年前小米的不定期新品發(fā)售,又如當前每年定期舉行雙11、雙12中的特價商品售賣。秒殺售賣時,大量消費者蜂擁而至,給電商帶來了極大的人氣,也給電商背后的服務系統(tǒng)帶來了超高的并發(fā)訪問負荷。

在不同電商、不同的秒殺活動,秒殺系統(tǒng)售賣的商品、銷售策略大不相同,但秒殺背后的秒殺系統(tǒng)卻有很大的相似性,基本都有以下這些共同特點。

首先,秒殺業(yè)務簡單,每個秒殺活動售賣的商品是事先定義好的,這些商品有明確的類型和數(shù)量,賣完即止。

其次,秒殺活動定時上架,而且會提供一個秒殺入口,消費者可以在活動開始后,通過這個入口進行搶購秒殺活動。

再次,秒殺活動由于商品售價低廉,廣泛宣傳,購買者遠大于商品數(shù),開始售賣后,會被快速搶購一空。

最后,由于秒殺活動的參與者眾多,遠超日常訪客數(shù)量,大量消費者涌入秒殺系統(tǒng),還不停的刷新訪問,短時間內(nèi)給系統(tǒng)帶來超高的并發(fā)流量,直到活動結束,流量消失。

分析了秒殺系統(tǒng)的特點,很容易發(fā)現(xiàn),秒殺系統(tǒng)實際就是一個有計劃的低價售賣活動,活動期間會帶來 N 倍爆發(fā)性增長的瞬時流量,活動后,流量會快速消失。因此,秒殺活動會給后端服務帶來如下的技術挑戰(zhàn)。

首先,秒殺活動持續(xù)時間短,但訪問沖擊量大,秒殺系統(tǒng)需要能夠應對這種爆發(fā)性的類似攻擊的訪問模型。

其次,業(yè)務的請求量遠遠大于售賣量,大部分是最終無法購買成功的請求,秒殺系統(tǒng)需要提前規(guī)劃好處理策略;

而且,由于業(yè)務前端訪問量巨大,系統(tǒng)對后端數(shù)據(jù)的訪問量也會短時間爆增,需要對數(shù)據(jù)存儲資源進行良好設計。

另外,秒殺活動雖然持續(xù)時間短,但活動期間會給整個業(yè)務系統(tǒng)帶來超大負荷,業(yè)務系統(tǒng)需要制定各種策略,避免系統(tǒng)過載而宕機。

最后,由于售賣活動商品價格低廉,存在套利空間,各種非法作弊手段層出,需要提前規(guī)劃預防策略。

秒殺系統(tǒng)設計

在設計秒殺系統(tǒng)時,有兩個設計原則。

首先,要盡力將請求攔截在系統(tǒng)上游,層層設阻攔截,過濾掉無效或超量的請求。因為訪問量遠遠大于商品數(shù)量,所有的請求打到后端服務的最后一步,其實并沒有必要,反而會嚴重拖慢真正能成交的請求,降低用戶體驗。

其次,要充分利用緩存,提升系統(tǒng)的性能和可用性。

img

秒殺系統(tǒng)專為秒殺活動服務,售賣商品確定,因此可以在設計秒殺商品頁面時,將商品信息提前設計為靜態(tài)信息,將靜態(tài)的商品信息以及常規(guī)的 CSS、JS、宣傳圖片等靜態(tài)資源,一起獨立存放到 CDN 節(jié)點,加速訪問,且降低系統(tǒng)訪問壓力。

在訪問前端也可以制定種種限制策略,比如活動沒開始時,搶購按鈕置灰,避免搶先訪問,用戶搶購一次后,也將按鈕置灰,讓用戶排隊等待,避免反復刷新。

用戶所有的請求進入秒殺系統(tǒng)前,通過負載均衡策略均勻分發(fā)到不同 Web 服務器,避免節(jié)點過載。在 Web 服務器中,首先進行各種服務預處理,檢查用戶的訪問權限,識別并發(fā)刷訂單的行為。同時在真正服務前,也要進行服務前置檢查,避免超售發(fā)生。如果發(fā)現(xiàn)售出數(shù)量已經(jīng)達到秒殺數(shù)量,則直接返回結束。

秒殺系統(tǒng)在處理搶購業(yè)務邏輯時,除了對用戶進行權限校驗,還需要訪問商品服務,對庫存進行修改,訪問訂單服務進行訂單創(chuàng)建,最后再進行支付、物流等后續(xù)服務。這些依賴服務,可以專門為秒殺業(yè)務設計排隊策略,或者額外部署實例,對秒殺系統(tǒng)進行專門服務,避免影響其他常規(guī)業(yè)務系統(tǒng)。

img

在秒殺系統(tǒng)設計中,最重要的是在系統(tǒng)開發(fā)之初就進行有效分拆。首先分拆秒殺活動頁面的內(nèi)容,將靜態(tài)內(nèi)容分拆到 CDN,動態(tài)內(nèi)容才通過接口訪問。其次,要將秒殺業(yè)務系統(tǒng)和其他業(yè)務系統(tǒng)進行功能分拆,盡量將秒殺系統(tǒng)及依賴服務獨立分拆部署,避免影響其他核心業(yè)務系統(tǒng)。

由于秒殺的參與者遠大于商品數(shù),為了提高搶購的概率,時常會出現(xiàn)一些利用腳本和僵尸賬戶并發(fā)頻繁調(diào)用接口進行強刷的行為,秒殺系統(tǒng)需要構建訪問記錄緩存,記錄訪問 IP、用戶的訪問行為,發(fā)現(xiàn)異常訪問,提前進行阻斷及返回。同時還需要構建用戶緩存,并針對歷史數(shù)據(jù)分析,提前緩存僵尸強刷專業(yè)戶,方便在秒殺期間對其進行策略限制。這些訪問記錄、用戶數(shù)據(jù),通過緩存進行存儲,可以加速訪問,另外,對用戶數(shù)據(jù)還進行緩存預熱,避免活動期間大量穿透。

在業(yè)務請求處理時,所有操作盡可能由緩存交互完成。由于秒殺商品較少,相關信息全部加載到內(nèi)存,把緩存暫時當作存儲用,并不會帶來過大成本負擔。

為秒殺商品構建商品信息緩存,并對全部目標商品進行預熱加載。同時對秒殺商品構建獨立的庫存緩存,加速庫存檢測。這樣通過秒殺商品列表緩存,進行快速商品信息查詢,通過庫存緩存,可以快速確定秒殺活動進程,方便高效成交或無可售商品后的快速檢測及返回。在用戶搶購到商品后,要進行庫存事務變更,進行庫存、訂單、支付等相關的構建和修改,這些操作可以盡量由系統(tǒng)只與緩存組件交互完成初步處理。后續(xù)落地等操作,必須要入DB庫的操作,可以先利用消息隊列機,記錄成交事件信息,然后再逐步分批執(zhí)行,避免對 DB 造成過大壓力。

總之,在秒殺系統(tǒng)中,除了常規(guī)的分拆訪問內(nèi)容和服務,最重要的是盡量將所有數(shù)據(jù)訪問進行緩存化,盡量減少 DB 的訪問,在大幅提升系統(tǒng)性能的同時,提升用戶體驗。

第34講:如何為海量計數(shù)場景設計緩存體系?

在上一課時我們講解了如何為秒殺系統(tǒng)進行緩存設計,在本課時我們將具體講解如何為海量計數(shù)場景設計緩存服務。

計數(shù)常規(guī)方案

img

計數(shù)服務在互聯(lián)網(wǎng)系統(tǒng)中非常常見,用戶的關注粉絲數(shù)、帖子數(shù)、評論數(shù)等都需要進行計數(shù)存儲。計數(shù)的存儲格式也很簡單,key 一般是用戶 uid 或者帖子 id 加上后綴,value 一般是 8 字節(jié)的 long 型整數(shù)。

最常見的計數(shù)方案是采用緩存 + DB 的存儲方案。當計數(shù)變更時,先變更計數(shù) DB,計數(shù)加 1,然后再變更計數(shù)緩存,修改計數(shù)存儲的 Memcached 或 Redis。這種方案比較通用且成熟,但在高并發(fā)訪問場景,支持不夠友好。在互聯(lián)網(wǎng)社交系統(tǒng)中,有些業(yè)務的計數(shù)變更特別頻繁,比如微博 feed 的閱讀數(shù),計數(shù)的變更次數(shù)和訪問次數(shù)相當,每秒十萬到百萬級以上的更新量,如果用 DB 存儲,會給 DB 帶來巨大的壓力,DB 就會成為整個計數(shù)服務的瓶頸所在。即便采用聚合延遲更新 DB 的方案,由于總量特別大,同時請求均衡分散在大量不同的業(yè)務端,巨大的寫壓力仍然是 DB 的不可承受之重。因此這種方案只適合中小規(guī)模的計數(shù)服務使用。

img

在 Redis 問世并越來越成熟后,很多互聯(lián)網(wǎng)系統(tǒng)會直接把計數(shù)全部存儲在 Redis 中。通過 hash 分拆的方式,可以大幅提升計數(shù)服務在 Redis 集群的寫性能,通過主從復制,在 master 后掛載多個從庫,利用讀寫分離,可以大幅提升計數(shù)服務在 Redis 集群的讀性能。而且 Redis 有持久化機制,不會丟數(shù)據(jù),在很多大中型互聯(lián)網(wǎng)場景,這都是一個比較適合的計數(shù)服務方案。

在互聯(lián)網(wǎng)移動社交領域,由于用戶基數(shù)巨大,每日發(fā)表大量狀態(tài)數(shù)據(jù),且相互之間有大量的交互動作,從而產(chǎn)生了海量計數(shù)和超高并發(fā)訪問,如果直接用 Redis 進行存儲,會帶來巨大的成本和性能問題。

海量計數(shù)場景

以微博為例,系統(tǒng)內(nèi)有大量的待計數(shù)對象。如從用戶維度,日活躍用戶 2 億+,月活躍用戶接近 5 億。從 Feed 維度,微博歷史 Feed 有數(shù)千億條,而且每日新增數(shù)億條的新 Feed。這些用戶和 Feed 不但需要進行計數(shù),而且需要進行多個計數(shù)。比如,用戶維度,每個用戶需要記錄關注數(shù)、粉絲數(shù)、發(fā)表 Feed 數(shù)等。而從 Feed 維度,每條 Feed 需要記錄轉發(fā)數(shù)、評論數(shù)、贊、閱讀等計數(shù)。

而且,在微博業(yè)務場景下,每次請求都會請求多個對象的多個計數(shù)。比如查看用戶時,除了獲取該用戶的基本信息,還需要同時獲取用戶的關注數(shù)、粉絲數(shù)、發(fā)表 Feed 數(shù)。獲取微博列表時,除了獲取 Feed 內(nèi)容,還需要同時獲取 Feed 的轉發(fā)數(shù)、評論數(shù)、贊數(shù),以及閱讀數(shù)。因此,微博計數(shù)服務的總訪問量特別大,很容易達到百萬級以上的 QPS。

因此,在海量計數(shù)高并發(fā)訪問場景,如果采用緩存 + DB 的架構,首先 DB 在計數(shù)更新就會存在瓶頸,其次,單個請求一次請求數(shù)十個計數(shù),一旦緩存 miss,穿透到 DB,DB 的讀也會成為瓶頸。因為 DB 能支撐的 TPS 不過 3000~6000 之間,遠遠無法滿足高并發(fā)計數(shù)訪問場景的需要。

采用 Redis 全量存儲方案,通過分片和主從復制,讀寫性能不會成為主要問題,但容量成本卻會帶來巨大開銷。

因為,一方面 Redis 作為通用型存儲來存儲計數(shù),內(nèi)存存儲效率低。以存儲一個 key 為 long 型 id、value 為 4 字節(jié)的計數(shù)為例,Redis 至少需要 65 個字節(jié)左右,不同版本略有差異。但這個計數(shù)理論只需要占用 12 個字節(jié)即可。內(nèi)存有效負荷只有 12/65=18.5%。如果再考慮一個 long 型 id 需要存 4 個不同類型的 4 字節(jié)計數(shù),內(nèi)存有效負荷只有 (8+16)/(65*4)= 9.2%。

另一方面,Redis 所有數(shù)據(jù)均存在內(nèi)存,單存儲歷史千億級記錄,單份數(shù)據(jù)拷貝需要 10T 以上,要考慮核心業(yè)務上 1 主 3 從,需要 40T 以上的內(nèi)存,再考慮多 IDC 部署,輕松占用上百 T 內(nèi)存。就按單機 100G 內(nèi)存來算,計數(shù)服務就要占用上千臺大內(nèi)存服務器。存儲成本太高。

海量計數(shù)服務架構

img

為了解決海量計數(shù)的存儲及訪問的問題,微博基于 Redis 定制開發(fā)了計數(shù)服務系統(tǒng),該計數(shù)服務兼容 Redis 協(xié)議,將所有數(shù)據(jù)分別存儲在內(nèi)存和磁盤 2 個區(qū)域。首先,內(nèi)存會預分配 N 塊大小相同的 Table 空間,線上一般每個 Table 占用 1G 字節(jié),最大分配 10 個左右的 Table 空間。首先使用 Table0,當存儲填充率超過閥值,就使用 Table1,依次類推。每個 Table 中,key 是微博 id,value 是自定義的多個計數(shù)。

微博的 id 按時間遞增,因此每個內(nèi)存 Table 只用存儲一定范圍內(nèi)的 id 即可。內(nèi)存 Table 預先按設置分配為相同 size 大小的 key-value 槽空間。每插入一個新 key,就占用一個槽空間,當槽位填充率超過閥值,就滾動使用下一個 Table,當所有預分配的 Table 使用完畢,還可以根據(jù)配置,繼續(xù)從內(nèi)存分配更多新的 Table 空間。當內(nèi)存占用達到閥值,就會把內(nèi)存中 id 范圍最小的 Table 落盤到 SSD 磁盤。落盤的 Table 文件稱為 DDB。每個內(nèi)存 Table 對應落盤為 1 個 DDB 文件。

計數(shù)服務會將落盤 DDB 文件的索引記錄在內(nèi)存,這樣當查詢需要從內(nèi)存穿透到磁盤時,可以直接定位到磁盤文件,加快查詢速度。

計數(shù)服務可以設置 Schema 策略,使一個 key 的 value 對應存儲多個計數(shù)。每個計數(shù)占用空間根據(jù) Schema 確定,可以精確到 bit。key 中的各個計數(shù),設置了最大存儲空間,所以只能支持有限范圍內(nèi)的計數(shù)。如果計數(shù)超過設置的閥值,則需要將這個 key 從 Table 中刪除,轉儲到 aux dict 輔助詞典中。

同時每個 Table 負責一定范圍的 id,由于微博 id 隨時間增長,而非逐一遞增,Table 滾動是按照填充率達到閥值來進行的。當系統(tǒng)發(fā)生異常時,或者不同區(qū)域網(wǎng)絡長時間斷開重連后,在老數(shù)據(jù)修復期間,可能在之前的 Table 中插入較多的計數(shù) key。如果舊 Table 插入數(shù)據(jù)量過大,超過容量限制,或者持續(xù)搜索存儲位置而不得,查找次數(shù)超過閥值,則將新 key 插入到 extend dict 擴展詞典中。

微博中的 feed 一般具有明顯的冷熱區(qū)分,并且越新的 feed 越熱,訪問量越大,越久遠的 feed 越冷。新的熱 key 存放內(nèi)存 Table,老的冷 key 隨所在的 Table 被置換到 DDB 文件。當查詢 DDB 文件中的冷 key 時,會采用多線程異步并行查詢,基本不影響業(yè)務的正常訪問。同時,這些冷 key 從 DDB 中查詢后,會被存放到 LRU 中,從而方便后續(xù)的再次訪問。

計數(shù)服務的內(nèi)存數(shù)據(jù)快照仍然采用前面講的 RDB + 滾動 AOF 策略。RDB 記錄構建時刻對應的 AOF 文件 id 及 pos 位置。全量復制時,master 會將磁盤中的 DDB 文件,以及內(nèi)存數(shù)據(jù)快照對應的 RDB 和 AOF 全部傳送給 slave。

在之后的所有復制就是全增量復制,slave 在斷開連接,再次重連 master 時,匯報自己同步的 AOF 文件 id 及位置,master 將對應文件位置之后的內(nèi)容全部發(fā)送給 slave,即可完成同步。

img

計數(shù)服務中的內(nèi)存 Table 是一個一維開放數(shù)據(jù),每個 key-value 按照 Schema 策略占用相同的內(nèi)存。每個 key-value 內(nèi)部,key 和多個計數(shù)緊湊部署。首先 8 字節(jié)放置 long 型 key,然后按Schema 設置依次存放各個計數(shù)。

key 在插入及查詢時,流程如下。

首先根據(jù)所有 Table 的 id 范圍,確定 key 所在的內(nèi)存 Table。

然后再根據(jù) double-hash 算法計算 hash,用 2 個 hash 函數(shù)分別計算出 2 個 hash 值,采用公示 h1+N*h2 來定位查找。

在對計數(shù)插入或變更時,如果查詢位置為空,則立即作為新值插入 key/value,否則對比 key,如果 key 相同,則進行計數(shù)增減;如果 key 不同,則將 N 加 1,然后進入到下一個位置,繼續(xù)進行前面的判斷。如果查詢的位置一直不為空,且 key 不同,則最多查詢設置的閥值次數(shù),如果仍然沒查到,則不再進行查詢。將該 key 記錄到 extend dict 擴展詞典中。

在對計數(shù) key 查找時,如果查詢的位置為空,說明 key 不存在,立即停止。如果 key 相同,返回計數(shù),否則 N 加 1,繼續(xù)向后查詢,如果查詢達到閥值次數(shù),沒有遇到空,且 key 不同,再查詢 aux dict 輔助字典 和 extend dict 擴展字典,如果也沒找到該 key,則說明該 key 不存在,即計數(shù)為 0。

海量計數(shù)服務收益

微博計數(shù)服務,多個計數(shù)按 Schema 進行緊湊存儲,共享同一個 key,每個計數(shù)的 size 按 bit 設計大小,沒有額外的指針開銷,內(nèi)存占用只有 Redis 的 10% 以下。同時,由于 key 的計數(shù) size 固定,如果計數(shù)超過閥值,則獨立存儲 aux dict 輔助字典中。

同時由于一個 key 存儲多個計數(shù),同時這些計數(shù)一般都需要返回,這樣一次查詢即可同時獲取多個計數(shù),查詢性能相比每個計數(shù)獨立存儲的方式提升 3~5 倍。

###?第35講:如何為社交feed場景設計緩存體系?

在上一課時我們講解了如何為海量計數(shù)場景進行緩存設計,本課時中我將講解如何為社交 Feed 場景設計緩存體系。

Feed 流場景分析

img

Feed 流是很多移動互聯(lián)網(wǎng)系統(tǒng)的重要一環(huán),如微博、微信朋友圈、QQ 好友動態(tài)、頭條/抖音信息流等。雖然這些產(chǎn)品形態(tài)各不相同,但業(yè)務處理邏輯卻大體相同。用戶日常的“刷刷刷”,就是在獲取 Feed 流,這也是 Feed 流的一個最重要應用場景。用戶刷新獲取 Feed 流的過程,對于服務后端,就是一個獲取用戶感興趣的 Feed,并對 Feed 進行過濾、動態(tài)組裝的過程。

接下來,我將以微博為例,介紹用戶在發(fā)出刷新 Feed 流的請求后,服務后端是如何進行處理的。

獲取 Feed 流操作是一個重操作,后端數(shù)據(jù)處理存在 100 ~ 1000 倍以上的讀放大。也就是說,前端用戶發(fā)出一個接口請求,服務后端需要請求數(shù)百甚至數(shù)千條數(shù)據(jù),然后進行組裝處理并返回響應。因此,為了提升處理性能、快速響應用戶,微博 Feed 平臺重度依賴緩存,幾乎所有的數(shù)據(jù)都從緩存獲取。如用戶的關注關系從 Redis 緩存中獲取,用戶發(fā)出的 Feed 或收到特殊 Feed 從 Memcached 中獲取,用戶及 Feed 的各種計數(shù)從計數(shù)服務中獲取。

Feed 流流程分析

Feed 流業(yè)務作為微博系統(tǒng)的核心業(yè)務,為了保障用戶體驗,SLA 要求較高,核心接口的可用性要達到 4 個 9,接口耗時要在 50~100ms 以內(nèi),后端數(shù)據(jù)請求平均耗時要在 3~5ms 以內(nèi),因此為了滿足億級龐大用戶群的海量并發(fā)訪問需求,需要對緩存體系進行良好架構且不斷改進。

在 Feed 流業(yè)務中,核心業(yè)務數(shù)據(jù)的緩存命中率基本都在 99% 以上,這些緩存數(shù)據(jù),由 Feed 系統(tǒng)進行多線程并發(fā)獲取及組裝,從而及時發(fā)送響應給用戶。

Feed 流獲取的處理流程如下。

首先,根據(jù)用戶信息,獲取用戶的關注關系,一般會得到 300~2000 個關注用戶的 UID。

然后,再獲取用戶自己的 Feed inbox 收件箱。收件箱主要存放其他用戶發(fā)表的供部分特定用戶可見的微博 ID 列表。

接下來,再獲取所有關注列表用戶的微博 ID 列表,即關注者發(fā)表的所有用戶或者大部分用戶可見的 Feed ID 列表。這些 Feed ID 列表都以 vector 數(shù)組的形式存儲在緩存。由于一般用戶的關注數(shù)會達到數(shù)百甚至數(shù)千,因此這一步需要獲取數(shù)百或數(shù)千個 Feed vector。

然后,Feed 系統(tǒng)將 inbox 和關注用戶的所有 Feed vector 進行合并,并排序、分頁,即得到目標 Feed 的 ID 列表。

接下來,再根據(jù) Feed ID 列表獲取對應的 Feed 內(nèi)容,如微博的文字、視頻、發(fā)表時間、源微博 ID 等。

然后,再進一步獲取所有微博的發(fā)表者 user 詳細信息、源微博內(nèi)容等信息,并進行內(nèi)容組裝。

之后,如果用戶設置的過濾詞,還要將這些 Feed 進行過濾篩選,剔除用戶不感興趣的 Feed。

接下來,再獲取用戶對這些 Feed 的收藏、贊等狀態(tài),并設置到對應微博中。

最后,獲取這些 Feed 的轉發(fā)數(shù)、評論數(shù)、贊數(shù)等,并進行計數(shù)組裝。至此,Feed 流獲取處理完畢,Feed 列表以 JSON 形式返回給前端,用戶刷新微博首頁成功完成。

Feed 流緩存架構

img

Feed 流處理中,緩存核心業(yè)務數(shù)據(jù)主要分為 6 大類。

第一類是用戶的 inbox 收件箱,在用戶發(fā)表僅供少量用戶可見的 Feed 時,為了提升訪問效率,這些 Feed ID 并不會進入公共可見的 outbox 發(fā)件箱,而會直接推送到目標客戶的收件箱。

第二類是用戶的 outbox 發(fā)件箱。用戶發(fā)表的普通微博都進入 outbox,這些微博幾乎所有人都可見,由粉絲在刷新 Feed 列表首頁時,系統(tǒng)直接拉取組裝。

第三類是 Social Graph 即用戶的關注關系,如各種關注列表、粉絲列表。

第四類是 Feed Content 即 Feed 的內(nèi)容,包括 Feed 的文字、視頻、發(fā)表時間、源微博 ID 等。

第五類是 Existence 存在性判斷緩存,用來判斷用戶是否閱讀了某條 Feed,是否贊了某條 Feed 等。對于存在性判斷,微博是采用自研的 phantom 系統(tǒng),通過 bloomfilter 算法進行存儲的。

第六類是 Counter 計數(shù)服務,用來存儲諸如關注數(shù)、粉絲數(shù),Feed 的轉發(fā)、評論、贊、閱讀等各種計數(shù)。

對于 Feed 的 inbox 收件箱、outbox 發(fā)件箱,Feed 系統(tǒng)通過 Memcached 進行緩存,以 feed id的一維數(shù)組格式進行存儲。

對于關注列表,Feed 系統(tǒng)采用 Redis 進行緩存,存儲格式為 longset。longset 在之前的課時介紹過,是微博擴展的一種數(shù)據(jù)結構,它是一個采用 double-hash 尋址的一維數(shù)組。當緩存 miss 后,業(yè)務 client 可以從 DB 加載,并直接構建 longset 的二進制格式數(shù)據(jù)作為 value寫入Redis,Redis 收到后直接 restore 到內(nèi)存,而不用逐條加入。這樣,即便用戶有成千上萬個關注,也不會引發(fā)阻塞。

Feed content 即 Feed 內(nèi)容,采用 Memcached 存儲。由于 Feed 內(nèi)容有眾多的屬性,且時常需要根據(jù)業(yè)務需要進行擴展,Feed 系統(tǒng)采用 Google 的 protocol bufers 的格式進行存放。protocol buffers 序列化后的所生成的二進制消息非常緊湊,二進制存儲空間比 XML 小 3~10 倍,而序列化及反序列化的性能卻高 10 倍以上,而且擴展及變更字段也很方便。微博的 Feed content 最初采用 XML 和 JSON 存儲,在 2011 年之后逐漸全部改為 protocol buffers 存儲。

對于存在性判斷,微博 Feed 系統(tǒng)采用自研的 phantom 進行存儲。數(shù)據(jù)存儲采用 bloom filter 存儲結構。實際上 phantom 本身就是一個分段存儲的 bloomfilter 結構。bloomFilter 采用 bit 數(shù)組來表示一個集合,整個數(shù)組最初所有 bit 位都是 0,插入 key 時,采用 k 個相互獨立的 hash 函數(shù)計算,將對應 hash 位置置 1。而檢測某個 key 是否存在時,通過對 key 進行多次 hash,檢查對應 hash 位置是否為 1 即可,如果有一個為 0,則可以確定該 key 肯定不存在,但如果全部為 1,大概率說明該 key 存在,但該 key 也有可能不存在,即存在一定的誤判率,不過這個誤判率很低,一般平均每條記錄占用 1.2 字節(jié)時,誤判率即可降低到 1%,1.8 字節(jié),誤判率可以降到千分之一。基本可以滿足大多數(shù)業(yè)務場景的需要。

對于計數(shù)服務,微博就是用前面講到的 CounterService。CounterService 采用 schema 策略,支持一個 key 對應多個計數(shù),只用 5~10% 的空間,卻提升 3~5 倍的讀取性能。

Feed 流 Mc 架構

img

Feed 流的緩存體系中,對于 Memcached 存儲采用 L1-Main-Backup 架構。這個架構前面在講分布式 Memcached 實踐中也有介紹。微博 Feed 流的 Memcached 存儲架構體系中,L1 單池容量一般為 Main 池的 1/10,有 4~6 組 L1,用于存放最熱的數(shù)據(jù),可以很好的解決熱點事件或節(jié)假日的流量洪峰問題。Main 池容量最大,保存了最近一段時間的幾乎所有較熱的數(shù)據(jù)。Backup 池的容量一般在 Main 池的 1/2 以下,主要解決 Main 池異常發(fā)生或者 miss 后的 key 訪問。

L1-Main-Bakcup 三層 Memcached 架構,可以很好抵御突發(fā)洪峰流量、局部故障等。實踐中,如果業(yè)務流量不大,還可以配置成兩層 Main-Bakckup。對于 2 層或 3 層 Mc 架構,處理 Mc 指令需要各種穿透、回種,需要保持數(shù)據(jù)的一致性,這些策略相對比較復雜。因此微博構建了 proxy,封裝 Mc 多層的讀寫邏輯,簡化業(yè)務的訪問。部分業(yè)務由于對響應時間很敏感,不希望因為增加 proxy 一跳而增加時間開銷,因此微博也提供了對應的 client,由 client 獲取并訂閱 Mc 部署,對三層 Mc 架構進行直接訪問。

在突發(fā)熱點事件發(fā)生,大量用戶上線并集中訪問、發(fā)表 Feed,并且會對部分 Feed 進行超高并發(fā)的訪問,總體流量增加 1 倍以上,熱點數(shù)據(jù)所在的緩存節(jié)點流量增加數(shù)倍,此時需要能夠快速增加多組 L1,從而快速分散這個節(jié)點數(shù)據(jù)的訪問。另外在任何一層,如果有節(jié)點機器故障,也需要使用其他機器替代。這樣三層 Mc 架構,時常需要進行一些變更。微博的 Mc 架構配置存放在配置中心 config-server 中,由 captain 進行管理。proxy、client 啟動時讀取并訂閱這些配置,在 Mc 部署變更時,可以及時自動切換連接。

Feed 流處理程序訪問 Mc 架構時,對于讀請求,首先會隨機選擇一組 L1,如果 L1 命中則直接返回,否則讀取 Main 層,如果 Main 命中,則首先將 value 回種到 L1,然后返回。如果 Main 層也 miss,就再讀取 slave,如果 slave 命中,則回種 Main 和最初選擇的那組 L1,然后返回。如果 slave 也 miss,就從 DB 加載后,回種到各層。這里有一個例外,就是 gets 請求,因為 gets 是為了接下來的 cas 更新服務,而三層 Mc 緩存是以 Main、Backup 為基準,所以 gets 請求直接訪問 Main 層,如果 Main 層失敗就訪問 Backup,只要有一層訪問獲得數(shù)據(jù)則請求成功。后續(xù) cas 時,將數(shù)據(jù)更新到對應 Main 或 Backup,如果 cas 成功,就把這個 key/value set 到其他各層。

對于數(shù)據(jù)更新,三層 Mc 緩存架構以 Main-Backup 為基準,即首先更新 Main 層,如果 Main 更新成功,則再寫其他三層所有 Mc pool 池。如果 Main 層更新失敗,再嘗試更新 Backup 池,如果 Backup 池更新成功,再更新其他各層。如果 Main、Backup 都更新失敗,則直接返回失敗,不更新 L1 層。在數(shù)據(jù)回種,或者 Main 層更新成功后再更新其他各層時,Mc 指令的執(zhí)行一般采用 noreply 方式,可以更高效的完成多池寫操作。

三層 Mc 架構,可以支撐百萬級的 QPS 訪問,各種場景下命中率高達 99% 以上,是 Feed 流處理程序穩(wěn)定運行的重要支撐。

img

對于 Feed 流中的 Redis 存儲訪問,業(yè)務的 Redis 部署基本都采用 1 主多從的方式。同時多個子業(yè)務按類型分為 cluster 集群,通過多租戶 proxy 進行訪問。對于一些數(shù)據(jù)量很小的業(yè)務,還可以共享 Redis 存儲,進行混合讀寫。對于一些響應時間敏感的業(yè)務,基于性能考慮,也支持smart client 直接訪問 Redis 集群。整個 Redis 集群,由 clusterManager 進行運維、slot 維護及遷移。配置中心記錄集群相關的 proxy 部署及 Redis 配置及部署等。這個架構在之前的經(jīng)典分布式緩存系統(tǒng)課程中有詳細介紹,此處不再贅述。

http://www.risenshineclean.com/news/5818.html

相關文章:

  • 紀檢監(jiān)察工作 網(wǎng)站建設軟文寫作營銷
  • 做58同城網(wǎng)站需要多少錢企業(yè)網(wǎng)站seo排名優(yōu)化
  • 中國建設銀行陜西分行官方網(wǎng)站騰訊云域名注冊
  • 網(wǎng)站建設近五年出版的書籍微信上如何投放廣告
  • 網(wǎng)站的滾屏切換是怎么做的網(wǎng)絡推廣具體內(nèi)容
  • 公司網(wǎng)站應該包括哪些內(nèi)容求職seo
  • 成品網(wǎng)站開發(fā)手機百度提交入口
  • wordpress5.0后臺慢seo引流什么意思
  • 無錫網(wǎng)站建設楚天軟件seo對網(wǎng)站優(yōu)化
  • 網(wǎng)站設計拓撲圖做網(wǎng)站需要多少錢 都包括什么
  • 網(wǎng)站頭部優(yōu)化文字怎么做今天重大新聞事件
  • wordpress html壓縮寧波核心關鍵詞seo收費
  • 教做幼兒菜譜菜的網(wǎng)站2024年度關鍵詞
  • 網(wǎng)站如何調(diào)用手機淘寶做淘寶客最近新聞摘抄50字
  • 常州市中大建設工程有限公司網(wǎng)站百度應用商店app下載安裝
  • 班服定制網(wǎng)站百度發(fā)布信息的免費平臺
  • 網(wǎng)站建設高端crm網(wǎng)站
  • 網(wǎng)站素材免費網(wǎng)站seo優(yōu)化效果
  • 申請網(wǎng)頁空間的網(wǎng)站長沙本地推廣平臺
  • 網(wǎng)站設計公司 上seo賺錢方法大揭秘
  • 裝飾公司怎樣做網(wǎng)站交換友情鏈接的渠道
  • 通達oa 做網(wǎng)站企業(yè)培訓權威機構
  • 恐怖小說網(wǎng)站怎么做小廣告模板
  • 新媒體公司網(wǎng)站怎么做2345網(wǎng)址導航瀏覽器下載
  • 仿網(wǎng)站建設免費b2b網(wǎng)站有哪些
  • 汕頭人才招聘網(wǎng)最新招聘信息北京seo全網(wǎng)營銷
  • 用c 做的網(wǎng)站怎么打開域名免費注冊0元注冊
  • 攝影師簽約有哪些網(wǎng)站線上宣傳渠道
  • 成都娛樂場所關閉最新消息武漢建站優(yōu)化廠家
  • 中糧網(wǎng)站是哪個公司做的上海高端seo公司