redis 問答

問題:和跟 Redis 相比,SimpleKV 還缺少什麼?

@曾軾麟同學:

數據結構:缺乏廣泛的數據結構支持,比如支持範圍查詢的 SkipList 和 Stream 等數據結構。

高可用:缺乏哨兵或者 master-slave 模式的高可用設計;橫向擴展:缺乏集羣和分片功能;

內存安全性:缺乏內存過載時的 key 淘汰算法的支持;

內存利用率:沒有充分對數據結構進行優化,提高內存利用率,例如使用壓縮性的數據結構;

功能擴展:需要具備後續功能的拓展;

不具備事務性:無法保證多個操作的原子性。

@Kaito 同學:SimpleKV 所缺少的有:豐富的數據類型、支持數據壓縮、過期機制、數據淘汰策略、主從複製、集羣化、高可用集羣等,另外,還可以增加統計模塊、通知模塊、調試模塊、元數據查詢等輔助功能。

 

問題:整數數組和壓縮列表作爲底層數據結構的優勢是什麼?

整數數組和壓縮列表的設計,充分體現了 Redis“又快又省”特點中的“省”,也就是節省內存空間。整數數組和壓縮列表都是在內存中分配一塊地址連續的空間,然後把集合中的元素一個接一個地放在這塊空間內,非常緊湊。因爲元素是挨個連續放置的,我們不用再通過額外的指針把元素串接起來,這就避免了額外指針帶來的空間開銷。我畫一張圖,展示下這兩個結構的內存佈局。整數數組和壓縮列表中的 entry 都是實際的集合元素,它們一個挨一個保存,非常節省內存空間。

Redis 之所以採用不同的數據結構,其實是在性能和內存使用效率之間進行的平衡。

 

問題:Redis 基本 IO 模型中還有哪些潛在的性能瓶頸?

這個問題是希望你能進一步理解阻塞操作對 Redis 單線程性能的影響。在 Redis 基本 IO 模型中,主要是主線程在執行操作,任何耗時的操作,例如 bigkey、全量返回等操作,都是潛在的性能瓶頸。

 

問題 1:AOF 重寫過程中有沒有其他潛在的阻塞風險?

風險一:Redis 主線程 fork 創建 bgrewriteaof 子進程時,內核需要創建用於管理子進程的相關數據結構,這些數據結構在操作系統中通常叫作進程控制塊(Process Control Block,簡稱爲 PCB)。內核要把主線程的 PCB 內容拷貝給子進程。這個創建和拷貝過程由內核執行,是會阻塞主線程的。而且,在拷貝過程中,子進程要拷貝父進程的頁表,這個過程的耗時和 Redis 實例的內存大小有關。如果 Redis 實例內存大,頁表就會大,fork 執行時間就會長,這就會給主線程帶來阻塞風險。

風險二:bgrewriteaof 子進程會和主線程共享內存。當主線程收到新寫或修改的操作時,主線程會申請新的內存空間,用來保存新寫或修改的數據,如果操作的是 bigkey,也就是數據量大的集合類型數據,那麼,主線程會因爲申請大空間而面臨阻塞風險。因爲操作系統在分配內存空間時,有查找和鎖的開銷,這就會導致阻塞。

問題 2:AOF 重寫爲什麼不共享使用 AOF 本身的日誌?

如果都用 AOF 日誌的話,主線程要寫,bgrewriteaof 子進程也要寫,這兩者會競爭文件系統的鎖,這就會對 Redis 主線程的性能造成影響。

問題:使用一個 2 核 CPU、4GB 內存、500GB 磁盤的雲主機運行 Redis,Redis 數據庫的數據量大小差不多是 2GB。當時 Redis 主要以修改操作爲主,寫讀比例差不多在 8:2 左右,也就是說,如果有 100 個請求,80 個請求執行的是修改操作。在這個場景下,用 RDB 做持久化有什麼風險嗎?

@Kaito 同學的回答從內存資源和 CPU 資源兩方面分析了風險,非常棒。我稍微做了些完善和精簡,你可以參考一下。

內存不足的風險:Redis fork 一個 bgsave 子進程進行 RDB 寫入,如果主線程再接收到寫操作,就會採用寫時複製。寫時複製需要給寫操作的數據分配新的內存空間。本問題中寫的比例爲 80%,那麼,在持久化過程中,爲了保存 80% 寫操作涉及的數據,寫時複製機制會在實例內存中,爲這些數據再分配新內存空間,分配的內存量相當於整個實例數據量的 80%,大約是 1.6GB,這樣一來,整個系統內存的使用量就接近飽和了。此時,如果實例還有大量的新 key 寫入或 key 修改,雲主機內存很快就會被喫光。如果雲主機開啓了 Swap 機制,就會有一部分數據被換到磁盤上,當訪問磁盤上的這部分數據時,性能會急劇下降。如果雲主機沒有開啓 Swap,會直接觸發 OOM,整個 Redis 實例會面臨被系統 kill 掉的風險

。主線程和子進程競爭使用 CPU 的風險:生成 RDB 的子進程需要 CPU 核運行,主線程本身也需要 CPU 核運行,而且,如果 Redis 還啓用了後臺線程,此時,主線程、子進程和後臺線程都會競爭 CPU 資源。由於雲主機只有 2 核 CPU,這就會影響到主線程處理請求的速度。

 

問題:爲什麼主從庫間的複製不使用 AOF?

RDB 文件是二進制文件,無論是要把 RDB 寫入磁盤,還是要通過網絡傳輸 RDB,IO 效率都比記錄和傳輸 AOF 的高。

在從庫端進行恢復時,用 RDB 的恢復效率要高於用 AOF。

問題 1:在主從切換過程中,客戶端能否正常地進行請求操作呢?

主從集羣一般是採用讀寫分離模式,當主庫故障後,客戶端仍然可以把讀請求發送給從庫,讓從庫服務。但是,對於寫請求操作,客戶端就無法執行了。

問題 2:如果想要應用程序不感知服務的中斷,還需要哨兵或客戶端再做些什麼嗎?

一方面,客戶端需要能緩存應用發送的寫請求。只要不是同步寫操作(Redis 應用場景一般也沒有同步寫),寫請求通常不會在應用程序的關鍵路徑上,所以,客戶端緩存寫請求後,給應用程序返回一個確認就行。

另一方面,主從切換完成後,客戶端要能和新主庫重新建立連接,哨兵需要提供訂閱頻道,讓客戶端能夠訂閱到新主庫的信息。同時,客戶端也需要能主動和哨兵通信,詢問新主庫的信息。

 

問題 1:5 個哨兵實例的集羣,quorum 值設爲 2。在運行過程中,如果有 3 個哨兵實例都發生故障了,此時,Redis 主庫如果有故障,還能正確地判斷主庫“客觀下線”嗎?如果可以的話,還能進行主從庫自動切換嗎?

因爲判定主庫“客觀下線”的依據是,認爲主庫“主觀下線”的哨兵個數要大於等於 quorum 值,現在還剩 2 個哨兵實例,個數正好等於 quorum 值,所以還能正常判斷主庫是否處於“客觀下線”狀態。如果一個哨兵想要執行主從切換,就要獲到半數以上的哨兵投票贊成,也就是至少需要 3 個哨兵投票贊成。但是,現在只有 2 個哨兵了,所以就無法進行主從切換了。

問題 2:哨兵實例是不是越多越好呢?如果同時調大 down-after-milliseconds 值,對減少誤判是不是也有好處?

哨兵實例越多,誤判率會越低,但是在判定主庫下線和選舉 Leader 時,實例需要拿到的贊成票數也越多,等待所有哨兵投完票的時間可能也會相應增加,主從庫切換的時間也會變長,客戶端容易堆積較多的請求操作,可能會導致客戶端請求溢出,從而造成請求丟失。如果業務層對 Redis 的操作有響應時間要求,就可能會因爲新主庫一直沒有選定,新操作無法執行而發生超時報警。調大 down-after-milliseconds 後,可能會導致這樣的情況:主庫實際已經發生故障了,但是哨兵過了很長時間才判斷出來,這就會影響到 Redis 對業務的可用性。

問題:爲什麼 Redis 不直接用一個表,把鍵值對和實例的對應關係記錄下來?

如果使用表記錄鍵值對和實例的對應關係,一旦鍵值對和實例的對應關係發生了變化(例如實例有增減或者數據重新分佈),就要修改表。如果是單線程操作表,那麼所有操作都要串行執行,性能慢;如果是多線程操作表,就涉及到加鎖開銷。此外,如果數據量非常大,使用表記錄鍵值對和實例的對應關係,需要的額外存儲空間也會增加。基於哈希槽計算時,雖然也要記錄哈希槽和實例的對應關係,但是哈希槽的個數要比鍵值對的個數少很多,無論是修改哈希槽和實例的對應關係,還是使用額外空間存儲哈希槽和實例的對應關係,都比直接記錄鍵值對和實例的關係的開銷小得多。

 

問題 1:rehash 的觸發時機和漸進式執行機制

1.Redis 什麼時候做 rehash?

Redis 會使用裝載因子(load factor)來判斷是否需要做 rehash。裝載因子的計算方式是,哈希表中所有 entry 的個數除以哈希表的哈希桶個數。Redis 會根據裝載因子的兩種情況,來觸發 rehash 操作:

裝載因子≥1,

同時,哈希表被允許進行 rehash;裝載因子≥5

在第一種情況下,如果裝載因子等於 1,同時我們假設,所有鍵值對是平均分佈在哈希表的各個桶中的,那麼,此時,哈希表可以不用鏈式哈希,因爲一個哈希桶正好保存了一個鍵值對。

但是,如果此時再有新的數據寫入,哈希表就要使用鏈式哈希了,這會對查詢性能產生影響。在進行 RDB 生成和 AOF 重寫時,哈希表的 rehash 是被禁止的,這是爲了避免對 RDB 和 AOF 重寫造成影響。如果此時,Redis 沒有在生成 RDB 和重寫 AOF,那麼,就可以進行 rehash。否則的話,再有數據寫入時,哈希表就要開始使用查詢較慢的鏈式哈希了。

在第二種情況下,也就是裝載因子大於等於 5 時,就表明當前保存的數據量已經遠遠大於哈希桶的個數,哈希桶裏會有大量的鏈式哈希存在,性能會受到嚴重影響,此時,就立馬開始做 rehash。

剛剛說的是觸發 rehash 的情況,如果裝載因子小於 1,或者裝載因子大於 1 但是小於 5,同時哈希表暫時不被允許進行 rehash(例如,實例正在生成 RDB 或者重寫 AOF),此時,哈希表是不會進行 rehash 操作的。

 

2. 採用漸進式 hash 時,如果實例暫時沒有收到新請求,是不是就不做 rehash 了?

其實不是的。Redis 會執行定時任務,定時任務中就包含了 rehash 操作。所謂的定時任務,就是按照一定頻率(例如每 100ms/ 次)執行的任務。在 rehash 被觸發後,即使沒有收到新請求,Redis 也會定時執行一次 rehash 操作,而且,每次執行時長不會超過 1ms,以免對其他任務造成影響

 

問題 2:主線程、子進程和後臺線程的聯繫與區別

從操作系統的角度來看,進程一般是指資源分配單元,例如一個進程擁有自己的堆、棧、虛存空間(頁表)、文件描述符等;而線程一般是指 CPU 進行調度和執行的實體。

Redis 啓動以後,本身就是一個進程,它會接收客戶端發送的請求,並處理讀寫操作請求。而且,接收請求和處理請求操作是 Redis 的主要工作,Redis 沒有再依賴於其他線程,所以,我一般把完成這個主要工作的 Redis 進程,稱爲主進程或主線程

在主線程中,我們還可以使用 fork 創建子進程,或是使用 pthread_create 創建線程。下面我先介紹下 Redis 中用 fork 創建的子進程有哪些。

創建 RDB 的後臺子進程,同時由它負責在主從同步時傳輸 RDB 給從庫;

通過無盤複製方式傳輸 RDB 的子進程;

bgrewriteaof 子進程。

Redis 使用的線程。從 4.0 版本開始,Redis 也開始使用 pthread_create 創建線程,這些線程在創建後,一般會自行執行一些任務,例如執行異步刪除任務。相對於完成主要工作的主線程來說,我們一般可以稱這些線程爲後臺線程。

 

問題 3:寫時複製的底層實現機制

Redis 在使用 RDB 方式進行持久化時,會用到寫時複製機制。寫時複製的效果:bgsave 子進程相當於複製了原始數據,而主線程仍然可以修改原來的數據。

對 Redis 來說,主線程 fork 出 bgsave 子進程後,bgsave 子進程實際是複製了主線程的頁表。這些頁表中,就保存了在執行 bgsave 命令時,主線程的所有數據塊在內存中的物理地址。這樣一來,bgsave 子進程生成 RDB 時,就可以根據頁表讀取這些數據,再寫入磁盤中。如果此時,主線程接收到了新寫或修改操作,那麼,主線程會使用寫時複製機制。具體來說,寫時複製就是指,主線程在有寫操作時,纔會把這個新寫或修改後的數據寫入到一個新的物理地址中,並修改自己的頁表映射。

bgsave 子進程複製主線程的頁表以後,假如主線程需要修改虛頁 7 裏的數據,那麼,主線程就需要新分配一個物理頁(假設是物理頁 53),然後把修改後的虛頁 7 裏的數據寫到物理頁 53 上,而虛頁 7 裏原來的數據仍然保存在物理頁 33 上。這個時候,虛頁 7 到物理頁 33 的映射關係,仍然保留在 bgsave 子進程中。所以,bgsave 子進程可以無誤地把虛頁 7 的原始數據寫入 RDB 文件。

問題 4:replication buffer 和 repl_backlog_buffer 的區別

在進行主從複製時,Redis 會使用 replication buffer 和 repl_backlog_buffer,有些同學可能不太清楚它們的區別,我再解釋下。總的來說,replication buffer 是主從庫在進行全量複製時,主庫上用於和從庫連接的客戶端的 buffer,而 repl_backlog_buffer 是爲了支持從庫增量複製,主庫上用於持續保存寫操作的一塊專用 buffer。

Redis 主從庫在進行復制時,當主庫要把全量複製期間的寫操作命令發給從庫時,主庫會先創建一個客戶端,用來連接從庫,然後通過這個客戶端,把寫操作命令發給從庫。在內存中,主庫上的客戶端就會對應一個 buffer,這個 buffer 就被稱爲 replication buffer。Redis 通過 client_buffer 配置項來控制這個 buffer 的大小。主庫會給每個從庫建立一個客戶端,所以 replication buffer 不是共享的,而是每個從庫都有一個對應的客戶端。

repl_backlog_buffer 是一塊專用 buffer,在 Redis 服務器啓動後,開始一直接收寫操作命令,這是所有從庫共享的。主庫和從庫會各自記錄自己的複製進度,所以,不同的從庫在進行恢復時,會把自己的複製進度(slave_repl_offset)發給主庫,主庫就可以和它獨立同步。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章