天天看點

Redis分享 - 理論基礎Redis

Redis

1、通路架構

動态庫通路

  • libsimplekv.so

網絡通路架構

  • Socket Server

2、操作子產品+索引子產品

資料結構

  • 簡單動态字元串
  • 雙向連結清單
  • 壓縮清單
  • 哈希表
  • 跳表
  • 整數數組

組成(key-value)

  • 為了實作從鍵到值的快速通路,Redis 使用了一個哈希表來儲存所有鍵值對,key用的字元串,value用的是指向具體值的指針,是以不管值是 String,還是集合類型,哈希桶中的元素都是指向它們的指針。
    • 哈希桶中的 entry 元素中儲存了key和value指針,分别指向了實際的鍵和值,這樣一來,即使值是一個集合,也可以通過*value指針被查找到
    • 哈希表儲存了所有的鍵值對,是以,我也把它稱為全局哈希表。哈希表的最大好處很明顯,就是讓我們可以用 O(1) 的時間複雜度來快速查找到鍵值對——我們隻需要計算鍵的哈希值,就可以知道它所對應的哈希桶位置,然後就可以通路相應的 entry 元素
    • 問題:寫入大量的資料?
      • 産生
        • 哈希表的沖突問題(碰撞)
        • rehash 可能帶來的操作阻塞(擴容)
      • 解決
        • Redis 解決哈希沖突的方式,就是鍊式哈希。鍊式哈希也很容易了解,就是指同一個哈希桶中的多個元素用一個連結清單來儲存,它們之間依次用指針連接配接。
  • value
    • String
    • List
    • Hash
    • Set
    • Zset

3、存儲子產品

記憶體配置設定器

  • redis
    • tcmalloc
    • libc
    • jemalloc(預設)
      • 需要存儲大小為130位元組的對象,jemalloc會将其放入160位元組的記憶體單元
      • 優點
        • 1、采用多個 arena 來避免線程同步
        • 2、細粒度的鎖,比如每一個 bin 以及每一個 extents 都有自己的鎖
        • 3、Memory Order 的使用,比如 rtree 的讀寫通路有不同的原子語義(relaxed, acquire,release)
        • 4、結構體以及記憶體配置設定時保證對齊,以獲得更好的 cache locality
        • 5、cache_bin 配置設定記憶體時會通過棧變量來判斷是否成功以避免 cache miss
        • 6、dirty extent 的 delay coalesce 來獲得更好的 cache locality;extent 的 lazy purge 來保證更平滑的 gc 機制
        • 7、緊湊的結構體記憶體布局來減少占用空間,比如 extent.e_bits
        • 8、rtree 引入 rtree_ctx 的兩級 cache 機制,提升 extent 資訊擷取速度的同時減少 cache miss
        • 9、tcache gc 時對緩存容量的動态調整
      • 缺點
        • 1、arena 之間的記憶體不可見
          • 某個線程在這個 arena 使用了很多記憶體,之後這個 arena 并沒有其他線程使用,導緻這個 arena 的記憶體無法被 gc,占用過多
          • 兩個位于不同 arena 的線程頻繁進行記憶體申請,導緻兩個 arena 的記憶體出現大量交叉,但是連續的記憶體由于在不同 arena 而無法進行合并
  • 其他方式
    • glibc
      • malloc
      • free
        • 鍵值資料庫的鍵值對通常大小不一,glibc 的配置設定器在處理随機的大小記憶體塊配置設定時,表現并不好。一旦儲存的鍵值對資料規模過大,就可能會造成較嚴重的記憶體碎片問題
  • 記憶體配置設定算法
    • FF(首次适應算法)
      • 從空閑分區表的第一個表目起查找該表,把最先能夠滿足要求的空閑區配置設定給作業,這種方法的目的在于減少查找時間。為适應這種算法,空閑分區表(空閑區鍊)中的空閑分區要按位址由低到高進行排序。該算法優先使用低址部分空閑區,在低址空間造成許多小的空閑區,在高位址空間保留大的空閑區。
    • BF(最佳适應算法)
      • 從全部空閑區中找出能滿足作業要求的、且大小最小的空閑分區,這種方法能使碎片盡量小。為适應此算法,空閑分區表(空閑區鍊)中的空閑分區要按從小到大進行排序,自表頭開始查找到第一個滿足要求的自由分區配置設定。該算法保留大的空閑區,但造成許多小的空閑區。
    • 最差适應算法
      • 從全部空閑區中找出能滿足作業要求的、且大小最大的空閑分區,進而使連結清單中的結點大小趨于均勻,适用于請求配置設定的記憶體大小範圍較窄的系統。為适應此算法,空閑分區表(空閑區鍊)中的空閑分區按大小從大到小進行排序,自表頭開始查找到第一個滿足要求的自由分區配置設定。該算法保留小的空閑區,盡量減少小的碎片産生。

鍵值對儲存在記憶體還是外存

  • 外存
    • 雖然可以避免資料丢失,但是受限于磁盤的慢速讀寫(通常在幾 ms 級别),鍵值資料庫的整體性能會被拉低
    • 記憶體
      • 是讀寫很快,畢竟記憶體的通路速度一般都在百 ns 級别。但是,潛在的風險是一旦掉電,所有的資料都會丢失。

持久化

    • 檔案
      • AOF
        • 操作過程
        • 底層指令怎麼記錄的
          • 如set testkey testvalue
          • 解釋:
            • “*3”表示目前指令有三個部分,每部分都是由“$+數字”開頭,後面緊跟着具體的指令、鍵或值。這裡,“數字”表示這部分中的指令、鍵或值一共有多少位元組。例如,“$3 set”表示這部分有 3 個位元組,也就是“set”指令
        • 特點
          • 先執行指令後記錄日志
            • 為什麼要這樣?
              • 先讓系統執行指令,隻有指令能執行成功,才會被記錄到日志中,否則,系統就會直接向用戶端報錯(避免出現錯誤指令)
            • 好處
              • 指令執行後才記錄日志,是以不會阻塞目前的寫操作。
        • 問題
          • 潛在風險
            • 1、如果剛執行完一個指令,還沒有來得及記日志就當機了,那麼這個指令和相應的資料就有丢失的風險。
              • Redis 是用作緩存,還可以從後端資料庫重新讀入資料進行恢複
              • Redis 是直接用作資料庫的話,此時,因為指令沒有記入日志,是以就無法用日志進行恢複了
            • 2、AOF 雖然避免了對目前指令的阻塞,但可能會給下一個操作帶來阻塞風險。
              • AOF 日志也是在主線程中執行的,如果在把日志檔案寫入磁盤時,磁盤寫壓力大,就會導緻寫盤很慢,進而導緻後續的操作也無法執行了
            • 解決方式
              • Always(同步寫回)
                • 每個寫指令執行完,立馬同步地将日志寫回磁盤
              • Everysec(每秒寫回)
                • 每個寫指令執行完,隻是先把日志寫到 AOF 檔案的記憶體緩沖區,每隔一秒把緩沖區中的内容寫入磁盤
              • No(作業系統控制的寫回)
                • 每個寫指令執行完,隻是先把日志寫到 AOF 檔案的記憶體緩沖區,由作業系統決定何時将緩沖區内容寫回磁盤
              • 想要獲得高性能,就選擇 No 政策;如果想要得到高可靠性保證,就選擇Always 政策;如果允許資料有一點丢失,又希望性能别受太大影響的話,那麼就選擇Everysec 政策
          • 性能問題
            • 1、檔案系統本身對檔案大小有限制,無法儲存過大的檔案
            • 2、如果檔案太大,之後再往裡面追加指令記錄的話,效率也會變低
            • 3、如果發生當機,AOF 中記錄的指令要一個個被重新執行,用于故障恢複,如果日志檔案太大,整個恢複過程就會非常緩慢,這就會影響到 Redis 的正常使用
              • AOF 重寫機制
                • 多變一:舊日志檔案中的多條指令,在重寫後的新日志中變成了一條指令。
                • 簡單點來說,就是我們在同一個鍵中操作多次,日志隻會存最終結果,過程資料會删除
                • 産生的潛在問題
                  • 1、fork子程序
                    • 1、fork子程序,fork這個瞬間一定是會阻塞主線程的(注意,fork時并不會一次性拷貝所有記憶體資料給子程序),fork采用作業系統提供的寫實複制(Copy On Write)機制,就是為了避免一次性拷貝大量記憶體資料給子程序造成的長時間阻塞問題,但fork子程序需要拷貝程序必要的資料結構,其中有一項就是拷貝記憶體頁表(虛拟記憶體和實體記憶體的映射索引表),這個拷貝過程會消耗大量CPU資源,拷貝完成之前整個程序是會阻塞的,阻塞時間取決于整個執行個體的記憶體大小,執行個體越大,記憶體頁表越大,fork阻塞時間越久。拷貝記憶體頁表完成後,子程序與父程序指向相同的記憶體位址空間,也就是說此時雖然産生了子程序,但是并沒有申請與父程序相同的記憶體大小。
                    • 2、fork出的子程序指向與父程序相同的記憶體位址空間,此時子程序就可以執行AOF重寫,把記憶體中的所有資料寫入到AOF檔案中。但是此時父程序依舊是會有流量寫入的,如果父程序操作的是一個已經存在的key,那麼這個時候父程序就會真正拷貝這個key對應的記憶體資料,申請新的記憶體空間,這樣逐漸地,父子程序記憶體資料開始分離,父子程序逐漸擁有各自獨立的記憶體空間。因為記憶體配置設定是以頁為機關進行配置設定的,預設4k,如果父程序此時操作的是一個bigkey,重新申請大塊記憶體耗時會變長,可能會産阻塞風險。另外,如果作業系統開啟了記憶體大頁機制(Huge Page,頁面大小2M),那麼父程序申請記憶體時阻塞的機率将會大大提高,是以在Redis機器上需要關閉Huge Page機制。Redis每次fork生成RDB或AOF重寫完成後,都可以在Redis log中看到父程序重新申請了多大的記憶體空間。
                  • 2、AOF重寫過程中父程序産生寫入(AOF重寫不複用AOF本身的日志)
                    • 1、父子程序寫同一個檔案必然會産生競争問題,控制競争就意味着會影響父程序的性能
                    • 2、如果AOF重寫過程中失敗了,那麼原本的AOF檔案相當于被污染了,無法做恢複使用。是以Redis AOF重寫一個新檔案,重寫失敗的話,直接删除這個檔案就好了,不會對原先的AOF檔案産生影響。等重寫完成之後,直接替換舊檔案即可
              • 阻塞
                • 一個拷貝,兩處日志
                  • 一個拷貝
                    • 1、每次執行重寫時,主線程 fork 出背景的 bgrewriteaof 子程序
                    • 2、fork 會把主線程的記憶體拷貝一份給bgrewriteaof 子程序,這裡面就包含了資料庫的最新資料。
                    • 3、bgrewriteaof 子程序就可以在不影響主線程的情況下,逐一把拷貝的資料寫成操作,記入重寫日志
                  • 兩處日志
                    • 如果有寫操作,第一處日志就是指正在使用的 AOF 日志,Redis 會把這個操作寫到它的緩沖區
                    • 第二處日志,就是指新的 AOF 重寫日志(也會被寫到重寫日志的緩沖區)
                      • 1、等到拷貝資料的所有操作記錄重寫完成後,重寫日志記錄的這些最新操作也會寫入新的 AOF 檔案,以保證資料庫最新狀态的記錄
                      • 2、用新的 AOF 檔案替代舊檔案
                  • 每次 AOF 重寫時,Redis 會先執行一個記憶體拷貝,用于重寫;然後,使用兩個日志保證在重寫過程中,新寫入的資料不會丢失。而且,因為 Redis 采用額外的線程進行資料重寫,是以,這個過程并不會阻塞主線程。
      • RDB(類比現實生活的拍照)
        • 意思
          • 記憶體快照
            • 記憶體中的資料在某一刻的狀态記錄
            • 對 Redis 來說,它實作類似照片記錄效果的方式,就是把某一時刻的狀态以檔案的形式寫到磁盤上,也就是快照。這樣一來,即使當機,快照檔案也不會丢失,資料的可靠性也就得到了保證。這個快照檔案就稱為 RDB 檔案,其中,RDB 就是 Redis DataBase 的縮寫。
        • 考慮
          • 對哪些資料做快照?
            • 全量快照(把記憶體中的所有資料都記錄到磁盤中)
              • 産生問題
                • 需要協調資料位置
                • 寫資料的時間開銷大
              • 生成RDB檔案
                • save
                  • 在主線程中執行,會導緻阻塞
                • bgsave
                  • 建立一個子程序,專門用于寫入 RDB 檔案,避免了主線程的阻塞,這也是Redis RDB 檔案生成的預設配置
                    • bgsave執行全量快照
                    • 這既提供了資料的可靠性保證,也避免了對 Redis 的性能影響
          • 做快照時,資料還能被增删改嗎?
            • 正常來說是不能增删改的,因為一旦有操作,就會可能導緻快照不完整,資料不對
            • 但是,redis采用了COW(寫時複制技術),在執行快照的同時,正常處理寫操作
              • 簡單來說,bgsave 子程序是由主線程 fork 生成的,可以共享主線程的所有記憶體資料。bgsave 子程序運作後,開始讀取主線程的記憶體資料,并把它們寫入 RDB 檔案。
              • 如果主線程對這些資料也都是讀操作(例如圖中的鍵值對 A),那麼,主線程和bgsave 子程序互相不影響。但是,如果主線程要修改一塊資料(例如圖中的鍵值對 C),那麼,這塊資料就會被複制一份,生成該資料的副本。然後bgsave 子程序會把這個副本資料寫入 RDB 檔案,而在這個過程中,主線程仍然可以直接修改原來的資料。
            • Redis 會使用 bgsave 對目前記憶體中的所有資料做快照,這個操作是子程序在背景完成的,這就允許主線程同時可以修改資料
          • 可以每秒做一次快照嗎?
              • 頻繁将全量資料寫入磁盤,會給磁盤帶來很大壓力,多個快照競争有限的磁盤帶寬,前一個快照還沒有做完,後一個又開始做了,容易造成惡性循環
              • bgsave 子程序需要通過 fork 操作從主線程建立出來。雖然,子程序在建立後不會再阻塞主線程,但是,fork 這個建立過程本身會阻塞主線程,而且主線程的記憶體越大,阻塞時間越長。如果頻繁 fork 出 bgsave 子程序,這就會頻繁阻塞主線程了
              • 增量快照
              • 但是也會有問題,要修改的資料多
          • 丢失資料比較多,難把控兩次快照的時間
      • AOF+RDB
        • 記憶體快照以一定的頻率執行,在兩次快照之間,使用 AOF 日志記錄這期間的所有指令操作。
        • 在 Redis 重新開機的時候,可以先加載 RDB 的内容,然後再重放增量 AOF 日志就可以完全替代之前的AOF 全量檔案重放,是以重新開機效率大幅得到提升。
      • 選擇
        • 1、資料不能丢失時,記憶體快照和 AOF 的混合使用是一個很好的選擇
        • 2、如果允許分鐘級别的資料丢失,可以隻使用 RDB
        • 3、如果隻用 AOF,優先使用 everysec 的配置選項,因為它在可靠性和性能之間取了一個平衡
    • 資料庫恢複
      • 1、需要頻繁通路資料庫,會給資料庫帶來巨大的壓力
      • 2、,這些資料是從慢速資料庫中讀取出來的,性能肯定比不上從 Redis 中讀取,導緻使用這些資料的應用程式響應變慢
  • 磁盤

問題:為了保證資料的可靠性,Redis 需要在磁盤上讀寫 AOF 和 RDB,但在高并發場景裡,這就會直接帶來兩個新問題:一個是寫 AOF 和RDB 會造成 Redis 性能抖動,另一個是 Redis 叢集資料同步和執行個體恢複時,讀 RDB 比較慢,限制了同步和恢複速度。

  • NVM(非易失記憶體)

6、高可用擴充叢集支撐子產品

資料分片

  • 定義
    • 啟動多個 Redis 執行個體組成一個叢集,然後按照一定的規則,把收到的資料劃分成多份,每一份用一個執行個體來儲存
  • 如何儲存更多資料?
    • 縱向擴充
      • 更新單個 Redis 執行個體的資源配置,包括增加記憶體容量、增加磁盤容量、使用更高配置的 CPU。就像下圖中,原來的執行個體記憶體是 8GB,硬碟是 50GB,縱向擴充後,記憶體增加到 24GB,磁盤增加到 150GB。
        • 實施起來簡單、直接
      • 潛在問題
        • 當使用 RDB 對資料進行持久化時,如果資料量增加,需要的記憶體也會增加,主線程 fork 子程序時就可能會阻塞(不要求持久化儲存redis)
        • 縱向擴充會受到硬體和成本的限制
    • 橫向擴充
      • 橫向增加目前 Redis 執行個體的個數,就像下圖中,原來使用 1 個 8GB 記憶體、50GB 磁盤的執行個體,現在使用三個相同配置的執行個體
      • 在面向百萬、千萬級别的使用者規模時,橫向擴充的 Redis 切片叢集會是一個非常好的選擇。
    • 資料切片和執行個體的對應分布關系
      • Redis Cluster1、Redis Cluster 方案采用哈希槽(Hash Slot),來處理資料和執行個體之間的映射關系2、在 Redis Cluster 方案中,一個切片叢集共有 16384個哈希槽,這些哈希槽類似于資料分區,每個鍵值對都會根據它的key,被映射到一個哈希槽中
        • 過程1、我們在部署 Redis Cluster 方案時,可以使用 cluster create 指令建立叢集,此時,Redis 會自動把這些槽平均分布在叢集執行個體上。例如,如果叢集中有 N 個執行個體,那麼,每個執行個體 上的槽個數為 16384/N 個。 2、當然, 我們也可以使用 cluster meet 指令手動建立執行個體間的連接配接,形成叢集,再使用 cluster addslots 指令,指定每個執行個體上的哈希槽個數。 3、舉個例子,假設叢集中不同 Redis 執行個體的記憶體大小配置不一,如果把哈希槽均分在各個實 例上,在儲存相同數量的鍵值對時,和記憶體大的執行個體相比,記憶體小的執行個體就會有更大的容 量壓力。遇到這種情況時,你可以根據不同執行個體的資源配置情況,使用 cluster addslots 指令手動配置設定哈希槽。
          • 首先根據鍵值對的 key,按照 CRC16 算法計算一個 16 bit的值;然後,再用這個 16bit 值對 16384 取模,得到 0~16383 範圍内的模數,每個模數代表一個相應編号的哈希槽
        • 通過哈希槽,切片叢集就實作了資料到哈希槽、哈希槽再到執行個體的配置設定
        • 建議
          • 在手動配置設定哈希槽時,需要把 16384 個槽都配置設定完,否則Redis 叢集無法正常工作。
        • 用戶端如何定位資料?
          • 1、Redis 執行個體會把自己的哈希槽資訊發給和它相連接配接的其它執行個體,來完成哈希槽配置設定資訊的擴散。當執行個體之間互相連接配接後,每個執行個體就有所有哈希槽的映射關系了
          • 2、用戶端收到哈希槽資訊後,會把哈希槽資訊緩存在本地。當用戶端請求鍵值對時,會先計算鍵所對應的哈希槽,然後就可以給相應的執行個體發送請求了。

5、高可用叢集支撐子產品

高可用

  • 資料盡量少丢失
    • AOF和RDB解決
  • 服務盡量少中斷
    • 增加副本備援量
      • 增加執行個體
      • 多執行個體儲存同一份資料
        • 資料如何保持一緻?
        • 資料讀寫操作可以發給所有執行個體嗎
          • 主從庫叢集模式(讀寫分 離)保證資料副本的一緻,主從庫之間采用的是讀寫分 離的方式。
            • 讀操作

              主庫、從庫都可以接收

            • 寫操作

              首先到主庫執行,然後,主庫将寫操作同步給從庫

            • 為什麼要采用讀寫分離?
              • 沒有采用讀寫分離
                • 1、如果所有機器都能寫,相當于用戶端一個請求,可能會修改多次,導緻資料不一緻,這個時候讀就可能讀到老資料
                • 2、如果要保持所有執行個體資料儲存一緻,就要加鎖,執行個體間協商是否完成修改等操作
              • 采用讀寫分離
                • 1、所有的資料的修改都會在主庫上進行,不需要協商多個執行個體,隻需要把主庫的最新資料同步過去就行了。
                • 如何同步?
                  • 全量複制1、主從庫間建立連接配接、協商同步的過程,主要是為全量複制做準備(從庫和主庫建立起連接配接,并告訴主庫即将進行同步,主庫确認回複後,主從庫間就可以開始同步了)從庫給主庫發送 psync 指令,表示要進行資料同步,主庫根據這個指令的參數 來啟動複制。psync 指令包含了主庫的 runID 和複制進度 offset 兩個參數。runID,是每個 Redis 執行個體啟動時都會自動生成的一個随機 ID,用來唯一标記這個實 例。當從庫和主庫第一次複制時,因為不知道主庫的 runID,是以将 runID 設 為“?”。 offset,此時設為 -1,表示第一次複制。主庫收到 psync 指令後,會用 FULLRESYNC 響應指令帶上兩個參數:主庫 runID 和主庫 目前的複制進度 offset,傳回給從庫。從庫收到響應後,會記錄下這兩個參數。 這裡有個地方需要注意,FULLRESYNC 響應表示第一次複制采用的全量複制,也就是說, 主庫會把目前所有的資料都複制給從庫。2、主庫将所有資料同步給從庫。從庫收到資料後,在本地完成資料加載。(依賴于RDB)主庫執行 bgsave 指令,生成 RDB 檔案,接着将檔案發給從庫。從庫接收到 RDB 檔案後,會先清空目前資料庫,然後加載 RDB 檔案。這是因為從庫在通過 replicaof 指令開始和主庫同步前,可能儲存了其他資料。為了避免之前資料的影響,從庫需要先把 目前資料庫清空。 在主庫将資料同步給從庫的過程中,主庫不會被阻塞,仍然可以正常接收請求。否則, Redis 的服務就被中斷了。但是,這些請求中的寫操作并沒有記錄到剛剛生成的 RDB 檔案 中。為了保證主從庫的資料一緻性,主庫會在記憶體中用專門的 replication buffer,記錄 RDB 檔案生成後收到的所有寫操作。 3、主庫會把第二階段執行過程中新收到的寫指令,再發送給從 庫。具體的操作是,當主庫完成 RDB 檔案發送後,就會把此時 replication buffer 中的修改操作發給從庫,從庫再重新執行這些操作。這樣一來,主從庫就實作同步了。
                    • 簡單點來說就是,先把已有的發給從庫,如果在發的過程中修改了,那就把修改的放入一個緩存池中,然後等待前面的發完了,再把緩存的發出去。
                    • 問題:(耗時操作)
                      • 生成 RDB 檔案
                      • 傳輸 RDB 檔案
                  • 基于長連接配接的指令傳播
                    • 風險點
                      • 網絡斷連或阻塞

                        主庫與從庫斷了或阻塞,會導緻從庫資料是舊資料

                  • 增量複制當主從庫斷連後,主庫會把斷連期間收到的寫操作指令,寫入 replication buffer,同時也會把這些操作指令也寫入 repl_backlog_buffer 這個緩沖區。 repl_backlog_buffer 是一個環形緩沖區,主庫會記錄自己寫到的位置,從庫則會記錄自己 已經讀到的位置。 剛開始的時候,主庫和從庫的寫讀位置在一起,這算是它們的起始位置。随着主庫不斷接 收新的寫操作,它在緩沖區中的寫位置會逐漸偏離起始位置,我們通常用偏移量來衡量這 個偏移距離的大小,對主庫來說,對應的偏移量就是master_repl_offset。主庫接收的新 寫操作越多,這個值就會越大。 同樣,從庫在複制完寫操作指令後,它在緩沖區中的讀位置也開始逐漸偏移剛才的起始位 置,此時,從庫已複制的偏移量 slave_repl_offset 也在不斷增加。正常情況下,這兩個偏 移量基本相等。主從庫的連接配接恢複之後,從庫首先會給主庫發送 psync 指令,并把自己目前的 slave_repl_offset 發給主庫,主庫會判斷自己的 master_repl_offset 和 slave_repl_offset 之間的差距。在網絡斷連階段,主庫可能會收到新的寫操作指令,是以,一般來說,master_repl_offset 會大于 slave_repl_offset。此時,主庫隻用把 master_repl_offset 和 slave_repl_offset 之間的指令操作同步給從庫就行。
                    • Redis repl_backlog_buffer的使用
                    • Redis 增量複制流程
                    • 總結
                      • 1、就是相當于網絡等問題之後,此時會有一個repl_backlog_buffer(環形隊列)去記錄值,有兩個偏移量,一個是主庫的 master_repl_offse,一個是從庫的 slave_repl_offset2、主庫的 master_repl_offse會一直往環形隊列中加資料,然後從庫在同步完rdb之後會讀環形隊列的主庫的 master_repl_offse的 初始值,并用從庫的slave_repl_offset從這讀起,一直讀到兩個偏移量相等就可以了
                        • 如果從庫的讀取速度比較慢,就有可能導緻從庫還未讀取的操作被主庫新寫的操作覆寫了,這會導緻主從庫間的資料不一緻。
                        • 調整 repl_backlog_size 這個參數
                        • 緩沖空間的計算公式

                          一般情況:緩沖空間大小 = 主庫寫入指令速度 * 操作大小 - 主從庫間網絡傳輸指令速度 * 操作大小實際工作:repl_backlog_size = 緩沖空間大小 * 2,這也就是 repl_backlog_size 的最終值。

              • 一個 Redis 執行個體的資料庫不要太大,一個執行個體大小在幾 GB 級别比較合适,這樣可以減少 RDB 檔案生成、傳輸和重新加載的開銷
          • 哨兵叢集
            • 哨兵機制
              • 監控監控是指哨兵程序在運作時,周期性地給所有的主從庫發送 PING 指令,檢測它們是否仍然線上運作。如果從庫沒有在規定時間内響應哨兵的 PING 指令,哨兵就會把它标記為“下線狀态”;同樣,如果主庫也沒有在規定時間内響應哨兵的 PING 指令,哨兵就會判定主庫下線,然後開始自動切換主庫的流程。
                • 在監控任務中,哨兵需要判斷主庫是否處于下線狀态
                  • 主觀下線
                    • 哨兵程序會使用 PING 指令檢測它自己和主、從庫的網絡連接配接情況,用來判斷執行個體的狀态

                      誤判:主從切換,導緻哨兵認為主庫下線,然後進行選舉,資料同步,造成一系列的開銷解決:通常會采用多執行個體組成的叢集模式進行部署,這也被稱為哨兵叢集

                  • 客觀下線“客觀下線”的标準就是,當有 N 個哨兵執行個體時,最好要有 N/2 + 1 個執行個體判 斷主庫為“主觀下線”,才能最終判定主庫為“客觀下線”。這樣一來,就可以減少誤判 的機率,也能避免誤判帶來的無謂的主從庫切換。(當然,有多少個執行個體做出“主觀下 線”的判斷才可以,可以由 Redis 管理者自行設定)。
                    • 針對主庫(哨兵叢集的少數服從多數)
              • 選主(選擇主庫)主庫挂了以後,哨兵就需要從很多個從庫裡,按照一定的規則選擇一個從庫執行個體,把它作為新的主庫。這一步完成後,現在的叢集裡就有了新主庫。
                • 在選主任務中,哨兵也要決定選擇哪個從庫執行個體作為主庫
                  • 過程我們在多個從庫中,先按照一定的篩選條件,把不符合條件的從庫去掉。然後,我們再按照一定的規則,給剩下的從庫逐個打分,将得分最高的從庫選為新主庫
                    • 篩選1、如果從庫總是和主庫斷連,而且斷連次數超出了一定的門檻值,我們就有理由相信,這個從庫 的網絡狀況并不是太好,就可以把這個從庫篩掉了。 2、你使用配置項 down-after-milliseconds *10。其中,down-after-milliseconds 是我們認定主從庫斷連的最大連接配接逾時時間。如果在 down-after-milliseconds 毫秒内,主從節點都沒有通過網絡聯系上,我們就可以認為主從節點斷連 了。如果發生斷連的次數超過了 10 次,就說明這個從庫的網絡狀況不好,不适合作為新主 庫。
                      • 檢查從庫的目前線上狀态
                      • 判斷它之前的網絡連接配接狀态
                    • 打分
                      • 1、優先級最高的從庫得分高

                        使用者可以通過 slave-priority 配置項,給不同的從庫設定不同優先級。比如,你有兩個從庫,它們的記憶體大小不一樣,你可以手動給記憶體大的執行個體設定一個高優先級。在選主時, 哨兵會給優先級高的從庫打高分,如果有一個從庫優先級最高,那麼它就是新主庫了。如 果從庫的優先級都一樣,那麼哨兵開始第二輪打分

                      • 2、和舊主庫同步程度最接近的從庫得分高
                      • 3、ID 号小的從庫得分高
                        • 在優先級和複制進度都相同的情況下,ID 号最小的從庫得分最高,會被選為新主庫
                      • 主從庫同步時有個指令傳播的過程。在這個過程中,主庫會用 master_repl_offset 記錄目前的最新寫操作在repl_backlog_buffer 中的位置,而從庫會 用slave_repl_offset 這個值記錄目前的複制進度。此時,我們想要找的從庫,它的 slave_repl_offset 需要最接近 master_repl_offset。如果 在所有從庫中,有從庫的 slave_repl_offset 最接近 master_repl_offset,那麼它的得分就 最高,可以作為新主庫。 就像如圖所示,舊主庫的 master_repl_offset 是 1000,從庫 1、2 和 3 的 slave_repl_offset 分别是 950、990 和 900,那麼,從庫 2 就應該被選為新主庫。當然,如果有兩個從庫的 slave_repl_offset 值大小是一樣的(例如,從庫 1 和從庫 2 的 slave_repl_offset 值都是 990),我們就需要給它們進行第三輪打分了
              • 通知

                在執行通知任務時,哨兵會把新主庫的連接配接資訊發給其他從庫,讓它們執行 replicaof 指令,和新主庫建立連接配接,并進行資料複制。同時,哨兵會把新主庫的連接配接資訊通知給用戶端,讓它們把請求操作發到新主庫上(把新主庫資訊發給從庫和用戶端)

              • 哨兵機制的三項任務與目标
            • 關鍵機制
              • 1、基于 pub/sub 機制的哨兵叢集組成1、在主從叢集中,主庫上有一個名為“sentinel:hello”的頻道,不同哨兵就是通過 它來互相發現,實作互相通信的。 2、我來舉個例子,具體說明一下。在下圖中,哨兵 1 把自己的 IP(172.16.19.3)和端口 (26579)釋出到“sentinel:hello”頻道上,哨兵 2 和 3 訂閱了該頻道。那麼此時,哨兵 2 和 3 就可以從這個頻道直接擷取哨兵 1 的 IP 位址和端口号。 3、然後,哨兵 2、3 可以和哨兵 1 建立網絡連接配接。通過這個方式,哨兵 2 和 3 也可以建立網 絡連接配接,這樣一來,哨兵叢集就形成了。它們互相間可以通過網絡連接配接進行通信,比如說對主庫有沒有下線這件事兒進行判斷和協商。4、哨兵除了彼此之間建立起連接配接形成叢集外,還需要和從庫建立連接配接。這是因為,在哨兵的 監控任務中,它需要對主從庫都進行心跳判斷,而且在主從庫切換完成後,它還需要通知 從庫,讓它們和新主庫進行同步。
                • 隻有訂閱了同一個頻道的應用,才能通過釋出的消息進行資訊交換。
              • 2、基于 INFO 指令的從庫清單,這可以幫助哨兵和從庫建立連接配接1、這是由哨兵向主庫發送 INFO 指令來完成的。就像下圖所示,哨兵 2 給主庫發送 INFO 命 令,主庫接受到這個指令後,就會把從庫清單傳回給哨兵。接着,哨兵就可以根據從庫列 表中的連接配接資訊,和每個從庫建立連接配接,并在這個連接配接上持續地對從庫進行監控。哨兵 1 和 3 可以通過相同的方法和從庫建立連接配接。2、通過 pub/sub 機制,哨兵之間可以組成叢集,同時,哨兵又通過 INFO 指令,獲得 了從庫連接配接資訊,也能和從庫建立連接配接,并進行監控了。 3、但是,哨兵不能隻和主、從庫連接配接。因為,主從庫切換後,用戶端也需要知道新主庫的連 接資訊,才能向新主庫發送請求操作。是以,哨兵還需要完成把新主庫的資訊告訴用戶端 這個任務。
                • 哨兵是如何知道從庫的 IP 位址和端口的呢?
              • 3、基于哨兵自身的 pub/sub 功能,這實作了用戶端和哨兵之間的事件通知哨兵就是一個運作在特定模式下的 Redis 執行個體,隻不過它并不服務請求操 作,隻是完成監控、選主和通知的任務。是以,每個哨兵執行個體也提供 pub/sub 機制,客戶 端可以從哨兵訂閱消息。哨兵提供的消息訂閱頻道有很多,不同頻道包含了主從庫切換過 程中的不同關鍵事件。
                • 括主庫下線判斷、新主庫標明、從庫重新配置
                • 用戶端從哨兵這裡訂閱消息
                  • 步驟
                    • 1、用戶端讀取哨兵的配置檔案
                    • 2、可以獲得哨兵的位址和端口
                    • 3、哨兵建立網絡連接配接
                    • 4、用戶端執行訂閱指令,來擷取不同的事件消息
            • 由哪個哨兵執行主從切換?
              • 1、一個哨兵獲得了仲裁所需的贊成票數後,就可以标記主庫為“客觀下線”。這個所需的贊 成票數是通過哨兵配置檔案中的 quorum 配置項設定的。例如,現在有 5 個哨兵, quorum 配置的是 3,那麼,一個哨兵需要 3 張贊成票,就可以标記主庫為“客觀下 線”了。這 3 張贊成票包括哨兵自己的一張贊成票和另外兩個哨兵的贊成票。 2、此時,這個哨兵就可以再給其他哨兵發送指令,表明希望由自己來執行主從切換,并讓所 有其他哨兵進行投票。這個投票過程稱為“Leader 選舉”。因為最終執行主從切換的哨兵 稱為 Leader,投票過程就是确定 Leader。 3、在投票過程中,任何一個想成為 Leader 的哨兵,要滿足兩個條件:第一,拿到半數以上的 贊成票;第二,拿到的票數同時還需要大于等于哨兵配置檔案中的 quorum 值。以 3 個哨 兵為例,假設此時的 quorum 設定為 2,那麼,任何一個想成為 Leader 的哨兵隻要拿到 2 張贊成票,就可以了。
              • 1、在 T1 時刻,S1 判斷主庫為“客觀下線”,它想成為 Leader,就先給自己投一張贊成票, 然後分别向 S2 和 S3 發送指令,表示要成為 Leader。 2、在 T2 時刻,S3 判斷主庫為“客觀下線”,它也想成為 Leader,是以也先給自己投一張贊 成票,再分别向 S1 和 S2 發送指令,表示要成為 Leader。 3、在 T3 時刻,S1 收到了 S3 的 Leader 投票請求。因為 S1 已經給自己投了一票 Y,是以它 不能再給其他哨兵投贊成票了,是以 S1 回複 N 表示不同意。同時,S2 收到了 T2 時 S3 發送的 Leader 投票請求。因為 S2 之前沒有投過票,它會給第一個向它發送投票請求的哨 兵回複 Y,給後續再發送投票請求的哨兵回複 N,是以,在 T3 時,S2 回複 S3,同意 S3 成為 Leader。 4、在 T4 時刻,S2 才收到 T1 時 S1 發送的投票指令。因為 S2 已經在 T3 時同意了 S3 的投 票請求,此時,S2 給 S1 回複 N,表示不同意 S1 成為 Leader。發生這種情況,是因為 S3 和 S2 之間的網絡傳輸正常,而 S1 和 S2 之間的網絡傳輸可能正好擁塞了,導緻投票請 求傳輸慢了。5、在 T5 時刻,S1 得到的票數是來自它自己的一票 Y 和來自 S2 的一票 N。而 S3 除 了自己的贊成票 Y 以外,還收到了來自 S2 的一票 Y。此時,S3 不僅獲得了半數以上的 Leader 贊成票,也達到預設的 quorum 值(quorum 為 2),是以它最終成為了 Leader。接着,S3 會開始執行選主操作,而且在標明新主庫後,會給其他從庫和用戶端通 知新主庫的資訊。 6、如果 S3 沒有拿到 2 票 Y,那麼這輪投票就不會産生 Leader。哨兵叢集會等待一段時間 (也就是哨兵故障轉移逾時時間的 2 倍),再重新選舉。這是因為,哨兵叢集能夠進行成 功投票,很大程度上依賴于選舉指令的正常網絡傳播。如果網絡壓力較大或有短時堵塞, 就可能導緻沒有一個哨兵能拿到半數以上的贊成票。是以,等到網絡擁塞好轉之後,再進 行投票選舉,成功的機率就會增加。 7、需要注意的是,如果哨兵叢集隻有 2 個執行個體,此時,一個哨兵要想成為 Leader,必須獲得2 票,而不是 1 票。是以,如果有個哨兵挂掉了,那麼,此時的叢集是無法進行主從庫切 換的。是以,通常我們至少會配置 3 個哨兵執行個體。這一點很重要,你在實際應用時可不能 忽略了。
              • 選leader,由leader執行
              • 要保證所有哨兵執行個體的配置是一緻的,尤其是主觀下線的判斷值 down-after-milliseconds。

4、IO模型

  • 單線程高性能
    • Redis 的網絡 IO和鍵值對讀寫是由一個線程來完成的,這也是 Redis 對外提供鍵值存儲服務的主要流程
  • 額外線程
    • 持久化、異步删除、叢集資料同步
  • 采用高效的資料結構,比如哈希表和跳表
  • 采用IO多路複用機制,使其在網絡 IO 操作中能并發處理大量的用戶端請求,實作高吞吐率
    • 基本 IO 模型與阻塞點
      • 以 Get 請求為例,為了處理一個 Get 請求,需要監聽用戶端請求(bind/listen),和用戶端建立連接配接(accept),服務端從 socket 中讀取請求(recv),解析用戶端發送請求(parse),根據請求類型讀取鍵值資料(get),最後給用戶端傳回結果,即向 socket 中寫回資料(send)。
        • bind/listen、accept、recv、parse 和 send 屬于網絡 IO 處理,而 get 屬于鍵值資料操作
        • 潛在點: accept() 和 recv()
          • 當Redis監聽到一個用戶端有請求連接配接時,但一直未能成功建立起連接配接時,會阻塞在 accept() 函數這裡,導緻其他用戶端無法和 Redis 建立連接配接
          • 當 Redis 通過 recv() 從一個用戶端讀取資料時,如果資料一直沒有到達,Redis 也會一直阻塞在 recv()
    • 非阻塞IO模式
      • Redis套接字類型與非阻塞設定
        • 1、socket() 方法會傳回主動套接字
        • 2、調用 listen() 方法,将主動套接字轉化為監聽套接字,此時,可以監聽來自用戶端的連接配接請求。
        • 3、調用 accept() 方法接收到達的用戶端連接配接,并傳回已連接配接套接字
      • 連接配接
        • listen()連接配接
          • 當 Redis 調用 accept() 但一直未有連接配接請求到達時,Redis 線程可以傳回處理其他操作,而不用一直等待(調用 accept() 時,已經存在監聽套接字)
        • accept()連接配接
          • Redis 調用 recv() 後,如果已連接配接套接字上一直沒有資料到達,Redis 線程同樣可以傳回處理其他操作。我們也需要有機制繼續監聽該已連接配接套接字,并在有資料達到時通知 Redis
        • 一個監聽套接字和一個已連接配接套接字
    • 基于多路複用的高性能 I/O 模型
      • 一個線程處理多個IO流(Linux的select,epoll)
        • 回調機制
          • select/epoll 提供了基于事件的回調機制,即針對不同僚件的發生,調用相應的處理函數。
          • 這些事件會被放進一個事件隊列,Redis 單線程對該事件隊列不斷進行處理。這樣一來,Redis 無需一直輪詢是否有請求實際發生,這就可以避免造成 CPU 資源浪費。同時,Redis 在對事件隊列中的事件進行處理時,會調用相應的處理函數,這就實作了基于事件的回調。因為 Redis 一直在對事件隊列進行處理,是以能及時響應用戶端請求,提升Redis 的響應性能。
          • 了解例子
            • 以連接配接請求和讀資料請求為例:這兩個請求分别對應 Accept 事件和 Read 事件,Redis 分别對這兩個事件注冊 accept 和get 回調函數。當 Linux 核心監聽到有連接配接請求或讀資料請求時,就會觸發 Accept 事件和 Read 事件,此時,核心就會回調 Redis 相應的 accept 和 get 函數進行處理。
            • 這就像病人去醫院瞧病。在醫生實際診斷前,每個病人(等同于請求)都需要先分診、測體溫、登記等。如果這些工作都由醫生來完成,醫生的工作效率就會很低。是以,醫院都設定了分診台,分診台會一直處理這些診斷前的工作(類似于 Linux 核心監聽請求),然後再轉交給醫生做實際診斷。這樣即使一個醫生(相當于 Redis 單線程),效率也能提升。
          • FreeBSD 的 kqueue
          • Solaris 的 evport
      • 多個監聽套接字和多個已連接配接套接字
      • 核心會一直監聽這些套接字上的連接配接請求或資料請求。一旦有請求到達,就會交給 Redis 線程處理,這就實作了一個 Redis 線程處理多個IO 流的效果。
      • Redis 線程不會阻塞在某一個特定的監聽或已連接配接套接字上,也就是說,不會阻塞在某一個特定的用戶端請求處理上
      • Redis 可以同時和多個用戶端連接配接并處理請求,進而提升并發性
      • 多個 FD 就是剛才所說的多個套接字。Redis 網絡架構調用 epoll 機制,讓核心監聽這些套接字。
  • 單線程處理IO請求性能的瓶頸
    • 任意一個請求在server中一旦發生耗時,都會影響整個server的性能,也就是說後面的請求都要等前面這個耗時請求處理完成,自己才能被處理到。
      • 1、操作bigkey:寫入一個bigkey在配置設定記憶體時需要消耗更多的時間,同樣,删除bigkey釋放記憶體同樣會産生耗時
      • 2、使用複雜度過高的指令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)指令,但是N很大,例如lrange key 0 -1一次查詢全量資料;
      • 3、大量key集中過期:Redis的過期機制也是在主線程中執行的,大量key集中過期會導緻處理一個請求時,耗時都在删除過期key,耗時變長
      • 4、AOF刷盤開啟always機制:每次寫入都需要把這個操作刷到磁盤,寫磁盤的速度遠比寫記憶體慢,會拖慢Redis的性能
      • 6、主從全量同步生成RDB:雖然采用fork子程序生成資料快照,但fork這一瞬間也是會阻塞整個線程的,執行個體越大,阻塞時間越久
        • lazy-free機制,把bigkey釋放記憶體的耗時操作放在了異步線程中執行,降低對主線程的影響。
    • 并發量非常大時,單線程讀寫用戶端IO資料存在性能瓶頸,雖然采用IO多路複用機制,但是讀寫用戶端資料依舊是同步IO,隻能單線程依次讀取用戶端的資料,無法利用到CPU多核。
        • 多線程,可以在高并發場景下利用CPU多核多線程讀寫用戶端資料,進一步提升server性能,當然,隻是針對用戶端的讀寫是并行的,每個指令的真正操作依舊是單線程的
        • 充分利用伺服器的多核資源
        • 多線程分攤 Redis 同步 IO 讀寫負荷

其他操作

  • 多線程
    • 1、線程數增加,系統吞吐量短期上升,長期增長緩慢,有時還有可能下降
      • 當有多個線程要修改這個共享資源時,為了保證共享資源的正确性,就需要有額外的機制進行保證,而這個額外的機制,就會帶來額外的開銷。
      • 多線程程式設計模式面臨的共享資源的并發通路控制問題。
      • 即使增加了線程,大部分線程也在等待擷取通路共享資源的互斥鎖,并行變串行,系統吞吐率并沒有随着線程的增加而增加。
      • 多線程開發可以引入同步原語(volatile)來保護共享資源的并發通路,但是會降低系統代碼的易調試性和可維護性。

XMind: ZEN - Trial Version