天天看點

Redis精華

Redis

版本:V3.0 2013-6-29 (江南白衣版權所有,轉載請保留出處)

1. Overview

1.1 資料

  • ,最好的入門小冊子,可以先于一切文檔之前看,免費。
  • 作者Antirez的部落格
  • Redis 指令中文版, 

    huangz同學的翻譯。

  • Redis設計與實作 

    ,又是huangz同學的巨作,深入了解内部實作機制。

  • Redis 2.6源碼中文注釋版 

    ,繼續是huangz同學的大功德。

  • NoSQL Fan裡的Redis分類
  • 《Redis in Action》 

    (Manning, 2013) MEAP版,看目錄挺實戰,亞馬遜中國預售250元人民币。

  • Redis的幾個認識誤區 

    by Tim yang。

1.2 優缺點

非常非常的快,有測評說比Memcached還快(當大家都是單CPU的時候),而且是無短闆的快,讀寫都一般的快,所有API都差不多快,也沒有MySQL Cluster、MongoDB那樣更新同一條記錄如Counter時慢下去的毛病。

豐富的資料結構,超越了一般的Key-Value資料庫而被認為是一個資料結構伺服器。組合各種結構,限制Redis用途的是你自己的想象力。

因為是個人作品,Redis目前隻有2.3萬行代碼,Keep it simple的死硬做法,使得普通公司而不需淘寶那個級别的文藝公司也可以吃透它。Redis宣言就是作者的自白,我最喜歡其中的“代碼像首詩”,”設計是一場與複雜性的戰鬥“,“Coding是一件艱苦的事情,唯一的辦法是享受它。如果它已不能帶來快樂就停止它。為了防止這一天的出現,我們要盡量避免把Redis往乏味的路上帶。”

讓人又愛又恨的單線程架構,使得代碼不用處理平時最讓人頭痛的并發而大幅簡化,但也帶來CPU的瓶頸,而且單線程被慢操作所阻塞時,其他請求的延時變得不确定。

那Redis不是什麼?

  • Redis 不是Big Data,資料都在記憶體中,無法以T為機關。
  • 在Redis-Cluster釋出并被穩定使用之前,Redis沒有真正的平滑水準擴充能力。
  • Redis 不支援Ad-Hoc Query,提供的隻是資料結構的API,沒有SQL一樣的查詢能力。

1.3 Feature速覽

  • 所有資料都在記憶體中。
  • 五種資料結構:String / Hash / List / Set / Ordered Set。
  • 資料過期時間支援。
  • 不完全的事務支援。
  • 服務端腳本:使用Lua Script編寫,類似存儲過程的作用。
  • PubSub:撈過界的消息一對多釋出訂閱功能,起碼Redis-Sentinel使用了它。
  • 持久化:支援定期導出記憶體的Snapshot 與 記錄寫記錄檔的Append Only File兩種模式。
  • Replication:Master-Slave模式,Master可連接配接多個隻讀Slave,暫無專門的Geographic Replication支援。
  • Fail-Over:Redis-Sentinel節點負責監控Master節點,在master失效時提升slave,獨立的仲裁節點模式有效防止腦裂。
  • Sharding:開發中的Redis-Cluser。
  • 動态配置:所有參數可用指令行動态配置不需重新開機,并重新寫回配置檔案中,對雲上的大規模部署非常合适。

1.4 八卦

  • 作者是意大利的Salvatore Sanfilippo(antirez),又是VMWare大善人聘請了他專心寫Redis。
  • antirez和我一樣不喜歡搞什麼咨詢服務,不過最近VMWare旗下的Pivotal公司開始招聘Redis Commericial Engineer。
  • 預設端口6379,是手機按鍵上MERZ對應的号碼,意大利歌女Alessia Merz是antirez和朋友們認為愚蠢的代名詞。

2. 資料結構

2.1 Key

  • Key 不能太長,比如1024位元組,但antirez也不喜歡太短如"u:1000:pwd",要表達清楚意思才好。他私人建議用":"分隔域,用"."作為單詞間的連接配接,如"comment:1234:reply.to"。
  • Keys,傳回比對的key,支援通配符如 "keys a*" 、 "keys a?c",但不建議在生産環境大資料量下使用。
  • Sort,對集合按數字或字母順序排序後傳回或另存為list,還可以關聯到外部key等。因為複雜度是最高的O(N+M*log(M))(N是集合大小,M 為傳回元素的數量),有時會安排到slave上執行。
  • Expire/ExpireAt/Persist/TTL,關于Key逾時的操作。預設以秒為機關,也有p字頭的以毫秒為機關的版本, Redis的内部實作見2.9 過期資料清除。

2.2 String

最普通的key-value類型,說是String,其實是任意的byte[],比如圖檔,最大512M。 所有常用指令的複雜度都是O(1),普通的Get/Set方法,可以用來做Cache,存Session。

Incr/IncrBy/IncrByFloat/Decr/DecrBy,可以用來做計數器,做自增序列。key不存在時會建立并貼心的設原值為0。IncrByFloat專門針對float,沒有對應的decrByFloat版本?用負數啊。

SetNx, 僅當key不存在時才Set。可以用來選舉Master或做分布式鎖:所有Client不斷嘗試使用SetNx master myName搶注Master,成功的那位不斷使用Expire重新整理它的過期時間。如果Master倒掉了key就會失效,剩下的節點又會發生新一輪搶 奪。

其他Set指令:

  • SetEx, Set + Expire 的簡便寫法,p字頭版本以毫秒為機關。
  • GetSet, 設定新值,傳回舊值。比如一個按小時計算的計數器,可以用GetSet擷取計數并重置為0。這種指令在服務端做起來是舉手之勞,用戶端便友善很多。
  • MGet/MSet/MSetNx, 一次get/set多個key。
  • 2.6.12版開始,Set指令已融合了Set/SetNx/SetEx三者,SetNx與SetEx可能會被廢棄。

GetBit/SetBit/BitOp,與或非/BitCount, BitMap的玩法,比如統計今天的獨立通路使用者數時,每個注冊使用者都有一個offset,他今天進來的話就把他那個位設為1,用BitCount就可以得出今天的總人數。

Append/SetRange/GetRange/StrLen,對文本進行擴充、替換、截取和求長度,隻對特定資料格式如字段定長的有用,json就沒什麼用。

2.3 Hash

Key-HashMap結構,相比String類型将這整個對象持久化成JSON格式,Hash将對象的各個屬性存入Map裡,可以隻讀取/更新對象的某些屬性。這樣有些屬性超長就讓它一邊呆着不動,另外不同的子產品可以隻更新自己關心的屬性而不會互相并發覆寫沖突。

另一個用法是土法建索引。比如User對象,除了id有時還要按name來查詢。可以有如下的資料記錄:

  • (String) user:101 -> {"id":101,"name":"calvin"...}
  • (String) user:102 -> {"id":102,"name":"kevin"...}
  • (Hash) user:index-> "calvin"->101, "kevin" -> 102

底層實作是hash table,一般操作複雜度是O(1),要同時操作多個field時就是O(N),N是field的數量。

2.4 List

List是一個雙向連結清單,支援雙向的Pop/Push,江湖規矩一般從左端Push,右端Pop——LPush/RPop,而且還有Blocking的版本BLPop/BRPop,用戶端可以阻塞在那直到有消息到來,所有操作都是O(1)的好孩子,可以當Message Queue來用。當多個Client并發阻塞等待,有消息入列時誰先被阻塞誰先被服務。

還有RPopLPush/ BRPopLPush,彈出來傳回給client的同時,把自己又推入另一個list,LLen擷取清單的長度。

還有按值進行的操作:LRem(按值删除元素)、LInsert(插在某個值的元素的前後),複雜度是O(N),N是List長度,因為List的值不唯一,是以要周遊全部元素,而Set隻要O(log(N))。

按下标進行的操作:下标從0開始,隊列從左到右算,下标為負數時則從右到左。

  • LSet 

    ,按下标設定元素值。

  • LIndex,按下标傳回元素。
  • LRange,不同于POP直接彈走元素,隻是傳回清單内一段下标的元素,是分頁的最愛。
  • LTrim,限制List的大小,比如隻保留最新的20條消息。

複雜度也是O(N),其中LSet的N是List長度,LIndex的N是下标的值,LRange的N是start的值+列出元素的個數,因為是連結清單而不是數組,是以按下标通路其實要周遊連結清單,除非下标正好是隊頭和隊尾。LTrim的N是移除元素的個數。

在消息隊列中,并沒有JMS的ack機制,如果消費者把job給Pop走了又沒處理完就當機了怎麼辦?

  • 解 決方法之一是加多一個sorted set,分發的時候同時發到list與sorted set,以分發時間為score,使用者把job做完了之後要用ZREM消掉sorted set裡的job,并且定時從sorted set中取出逾時沒有完成的任務,重新放回list。
  • 另一個做法是為每個worker多加一個的list,彈出任務時改用 RPushLPop,将job同時放到worker自己的list中,完成時用LREM消掉。如果叢集管理(如zookeeper)發現worker已經 挂掉,就将worker的list内容重新放回主list。

2.5 Set

Set就是Set,以将可能重複的元素随便放入而Set會自動去重,底層實作也是hash table。

  • SAdd/SRem/SIsMember/SCard/SMove/SMembers,各種标準操作。除了SMembers都是O(1)。
  • SInter/SInterStore/SUnion/SUnionStore/SDiff/SDiffStore,各種集合操作。交集運算可以用來顯示線上好友(線上使用者 交集 好友清單),共同關注(兩個使用者的關注清單的交集)。O(N),并集和差集的N是集合大小之和,交集的N是小的那個集合的大小*2。

2.6 Sorted Set

有序集,元素放入集合時還要提供該元素的分數。

  • ZRange/ZRevRange,按排名的上下限傳回元素,正數與倒數。
  • ZRangeByScore/ZRevRangeByScore,按分數的上下限傳回元素,正數與倒數。
  • ZRemRangeByRank/ZRemRangeByScore,按排名/按分數的上下限删除元素。
  • ZCount,統計分數上下限之間的元素個數。
  • ZRank/ZRevRank 

    ,顯示某個元素的正倒序的排名。

  • ZScore/ZIncrby,顯示元素的分數/增加元素的分數。
  • ZAdd(Add)/ZRem(Remove)/ZCard(Count),ZInsertStore(交集)/ZUnionStore(并集),Set操作,與正牌Set相比,少了IsMember和差集運算。

Sorted Set的實作是hash table(element->score, 用于實作ZScore及判斷element是否在集合内),和skip list(score->element,按score排序)的混合體。 skip list有點像平衡二叉樹那樣,不同範圍的score被分成一層一層,每層是一個按score排序的連結清單。

ZAdd/ZRem 是O(log(N)),ZRangeByScore/ZRemRangeByScore是O(log(N)+M),N是Set大小,M是結果/操作元素的 個數。可見,原本可能很大的N被很關鍵的Log了一下,1000萬大小的Set,複雜度也隻是幾十不到。當然,如果一次命中很多元素M很大那誰也沒辦法 了。

2.7 事務

用Multi(Start Transaction)、Exec(Commit)、Discard(Rollback)實作。 在事務送出前,不會執行任何指令,隻會把它們存到一個隊列裡,不影響其他用戶端的操作。在事務送出時,批量執行所有指令。《Redis設計與實作》中的詳述。

注意,Redis裡的事務,與我們平時的事務概念很不一樣:

  • 它僅僅是保證事務裡的操作會被連續獨占的執行。因為是單線程架構,在執行完事務内所有指令前是不可能再去同時執行其他用戶端的請求的。
  • 它沒有隔離級别的概念,因為事務送出前任何指令都不會被實際執行,也就不存在"事務内的查詢要看到事務裡的更新,在事務外查詢不能看到"這個讓人萬分頭痛的問題。
  • 它 不保證原子性——所有指令同時成功或同時失敗,隻有決定是否開始執行全部指令的能力,沒有執行到一半進行復原的能力。在redis裡失敗分兩種,一種是明 顯的指令錯誤,比如指令名拼錯,指令參數個數不對,在2.6版中全部指令都不會執行。另一種是隐含的,比如在事務裡,第一句是SET foo bar, 第二句是LLEN foo,對第一句産生的String類型的key執行LLEN會失敗,但這種錯誤隻有在指令運作後才能發現,這時候第一句成功,第二句失敗。還有,如果事 務執行到一半redis被KILL,已經執行的指令同樣也不會被復原。

Watch指令,類似樂觀鎖,事務送出時,如果Key的值已被别的用戶端改變,比如某個list已被别的用戶端push/pop過了,整個事務隊列都不會被執行。

2.8 Lua Script

Redis2.6内置的Lua Script支援,可以在Redis的Server端一次過運作大量邏輯,就像存儲過程一樣,避免了海量中間資料在網路上的傳輸。

  • Lua自稱是在Script語言裡關于快的标準,Redis選擇了它而不是流行的JavaScript。
  • 因為Redis的單線程架構,整個Script預設是在一個事務裡的。
  • Script裡涉及的所有Key盡量用變量,從外面傳入,使Redis一開始就知道你要改變哪些key。(but why?)
  • Eval每次傳輸一整段Script比較費帶寬,可以先用Script Load載入script,傳回哈希值。然後用EvalHash執行。因為就是SHA-1,是以任何時候執行傳回的哈希值都是一樣的。
  • 内置的Lua庫裡還很貼心的帶了CJSON,可以處理json字元串。
  • 一段用Redis做Timer的示例代碼,下面的script被定期調用,從以觸發時間為score的sorted set中取出已到期的Job,放到list中給Client們blocking popup。
    -- KEYS: [1]job:sleeping, [2]job:ready -- ARGS: [1]currentTime -- Comments: result is the jobid local jobs=redis.call('zrangebyscore', KEYS[1], '-inf', ARGV[1]) local count =table.maxn(jobs) if count>0 then -- Comments: remove from Sleeping Job sorted setredis.call('zremrangebyscore', KEYS[1], '-inf', ARGV[1]) -- Comments: add to the Ready Joblist -- Comments: can optimize to use lpush id1,id2,... for better performance for i=1,countdo redis.call('lpush', KEYS[2], jobs[i]) end end      

2.9 過期資料清除

官方文檔 與 《Redis設計與實作》中的詳述,過期資料的清除從來不容易,為每一條key設定一個timer,到點立刻删除的消耗太大,每秒周遊所有資料消耗也大,Redis使用了一種相對務實的做法:

當client主動通路key會先對key進行逾時判斷,過時的key會立刻删除。

如 果clien永遠都不再get那條key呢? 它會在Master的背景,每秒10次的執行如下操作: 随機選取100個key校驗是否過期,如果有25個以上的key過期了,立刻額外随機選取下100個key(不計算在10次之内)。可見,如果過期的 key不多,它最多每秒回收200條左右,如果有超過25%的key過期了,它就會做得更多,但隻要key不被主動get,它占用的記憶體什麼時候最終被清 理掉隻有天知道。

3. 性能

3.1 測試結果

  • 測 試環境: RHEL 6.3 / HP Gen8 Server/ 2 * Intel Xeon 2.00GHz(6 core) / 64G DDR3 memory / 300G RAID-1 SATA / 1 master(writ AOF), 1 slave(write AOF & RDB)
  • 資料準備: 預加載兩千萬條資料,占用10G記憶體。
  • 測試工具:自帶的redis-benchmark,預設隻是基于一個很小的資料集進行測試,調整指令行參數如下,就可以開100條線程(預設50),SET 1千萬次(key在0-1千萬間随機),key長21位元組,value長256位元組的資料。
    redis-benchmark -t SET -c 100 -n 10000000 -r 10000000 -d 256      
  • 測試結果(TPS): 1.SET:4.5萬, 2.GET:6萬 ,3.INCR:6萬,4.真實混合場景: 2.5萬SET & 3萬GET
  • 單條用戶端線程時6千TPS,50與100條用戶端線程差别不大,200條時會略多。
  • Get/Set操作,經過了LAN,延時也隻有1毫秒左右,可以反複放心調用,不用像調用REST接口和通路資料庫那樣,每多一次外部通路都心痛。
  • 資源監控:

    1.CPU: 占了一個處理器的100%,總CPU是4%(因為總共有2CPU*6核*超線程 = 24個處理器),可見單線程下單處理器的能力是瓶頸。 AOF rewrite時另一個處理器占用50-70%。

    2. 網卡:15-20 MB/s receive, 3Mb/s send(no slave) or 15-20 MB/s send (with slave) 。當把value長度加到4K時,receive 99MB/s,已經到達千兆網卡的瓶頸,TPS降到2萬。

    3.硬碟:15MB/s(AOF append), 100MB/s(AOF rewrite/AOF load,普通硬碟的瓶頸),

3.2 為什麼快

  • 純ANSI C編寫。
  • 不依賴第三方類庫,沒有像memcached那樣使用libevent,因為libevent迎合通用性而造成代碼龐大,是以作者用libevent中兩個檔案修改實作了自己的epoll event loop。微軟的相容Windows更新檔也因為同樣原因被拒了。
  • 快,原因之一是Redis多樣的資料結構,每種結構隻做自己愛做的事,當然比資料庫隻有Table,MongogoDB隻有JSON一種結構快了。
  • 可惜單線程架構,雖然作者認為CPU不是瓶頸,記憶體與網絡帶寬才是。但實際測試時并非如此,見上。

3.3 性能調優

  • 官方文檔關于各種産生Latency的原因的詳細分析, 

    中文版

  • 正視網絡往返時間:

    1.MSet/LPush/ZAdd等都支援一次輸入多個Key。

    2.PipeLining模式 

    可以一次輸入多個指令。

    3.更快的是Lua Script模式,還可以包含邏輯,直接在服務端又get又set的,見2.8 Lua Script。

  • 發現執行緩慢的指令,可配置執行超過多少時間的指令算是緩慢指令(預設10毫秒,不含IO時間),可以用slowlog get 指令檢視(預設隻保留最後的128條)。單線程的模型下,一個請求占掉10毫秒是件大事情。
  • 持久化對性能的影響很大,見5.1持久化。
  • 要熟悉各指令的複雜度,不過隻要不是O(N)一個超大集合,都不用太擔心。

4. 容量

4.1 最大記憶體

  • 所有的資料都必須在記憶體中,原來2.0版的VM政策(将Value放到磁盤,Key仍然放在記憶體),2.4版後嫌麻煩又不支援了。
  • 一定要設定最大記憶體,否則實體記憶體用爆了就會大量使用Swap,寫RDB檔案時的速度慢得你想死。
  • 多 留一倍記憶體是最安全的。重寫AOF檔案和RDB檔案的程序(即使不做持久化,複制到Slave的時候也要寫RDB)會fork出一條新程序來,采用了操作 系統的Copy-On-Write政策(子程序與父程序共享Page。如果父程序的Page-每頁4K有修改,父程序自己建立那個Page的副本,不會影 響到子程序,父愛如山)。留意Console打出來的報告,如"RDB: 1215 MB of memory used by copy-on-write"。在系統極度繁忙時,如果父程序的所有Page在子程序寫RDB過程中都被修改過了,就需要兩倍記憶體。
  • 按照Redis啟動時的提醒,設定 vm.overcommit_memory = 1 ,使得fork()一條10G的程序時,因為COW政策而不一定需要有10G的free memory。
  • 其他需要考慮的記憶體包括:

    1.AOF rewrite過程中對新寫入指令的緩存(rewrite結束後會merge到新的aof檔案),留意"Background AOF buffer size: 80 MB"的字樣。

    2.負責與Slave同步的Client的緩存,預設設定master需要為每個slave預留不高于256M的緩存(見5.1持久化)。

  • 當 最大記憶體到達時,按照配置的Policy進行處理, 預設政策為volatile-lru,對設定了expire time的key進行LRU清除(不是按實際expire time)。如果沒有資料設定了expire time或者policy為noeviction,則直接報錯,但此時系統仍支援get之類的讀操作。 另外還有幾種policy,比如volatile-ttl按最接近expire time的,allkeys-lru對所有key都做LRU。

4.2 記憶體占用

  • 測試表明,string類型需要90位元組的額外代價,就是說key 1個位元組,value 1個位元組時,還是需要占用92位元組的長度,而上面的benchmark的記錄就占用了367個位元組。其他類型可根據文檔自行計算或實際測試一下。
  • 使用jemalloc配置設定記憶體,删除資料後,記憶體并不會乖乖還給作業系統而是被Redis截留下來重用到新的資料上,直到Redis重新開機。是以程序實際占用記憶體是看INFO裡傳回的used_memory_peak_human。
  • Redis内部用了ziplist/intset這樣的壓縮結構來減少hash/list/set/zset的存儲,預設當集合的元素少于512個且最長那個值不超過64位元組時使用,可配置。
  • 用make 32bit可以編譯出32位的版本,每個指針占用的記憶體更小,但隻支援最大4GB記憶體。

4.4 水準分區,Sharding

  • 其實,大記憶體加上垂直分區也夠了,不一定非要沙丁一把。
  • Jedis支援在用戶端做分區,局限是不能動态re-sharding, 有分區的master倒了,不能減少分區必須用slave頂上。要增加分區的話,呃.....
  • antire在部落格裡提到了Twemproxy,一個Twitter寫的Proxy,但它在發現節點倒掉後,隻會重新計算一緻性哈希環,把資料存到别的master去,而不是內建Sentinel指向新由slave更新的master,像Memcached一樣的做法也隻适合做Cache的場景。

Redis-Cluster是今年工作重點,支援automatic re-sharding, 采用和Hazelcast類似的算法,總共有N個分區(eg.N=1024),每台Server負責若幹個分區。

  • 在用戶端先hash出key 屬于哪個分區,随便發給一台server,server會告訴它真正哪個Server負責這個分區,緩存下來,下次還有該分區的請求就直接發到地兒了。
  • Re- sharding時,會将某些分區的資料移到新的Server上,完成後各Server周知分區<->Server映射的變化,因為分區數量 有限,是以通訊量不大。 在遷移過程中,用戶端緩存的依然是舊的分區映射資訊,原server對于已經遷移走的資料的get請求,會傳回一個臨時轉向的應答,用戶端先不會更新 Cache。等遷移完成了,就會像前面那樣傳回一條永久轉向資訊,用戶端更新Cache,以後就都去新server了。

5. 高可用性

高可用性關乎系統出錯時到底會丢失多少資料,多久不能服務。要綜合考慮持久化,Master-Slave複制及Fail-Over配置,以及具體Crash情形,比如Master死了,但Slave沒死。或者隻是Redis死了,作業系統沒死等等。

5.1 持久化

  • 綜述: 

    解密Redis持久化(中文概括版), 

    英文原版,《Redis設計與實作》: 

    RDB 

    與 

    AOF。

  • 很多人開始會想象兩者是互相結合的,即dump出一個snapshot到RDB檔案,然後在此基礎上記錄變化日志到AOF檔案。實際上兩者毫無關系,完全獨立運作,因為作者認為簡單才不會出錯。如果使用了AOF,重新開機時隻會從AOF檔案載入資料,不會再管RDB檔案。
  • 正确關閉伺服器:redis-cli shutdown 或者 kill,都會graceful shutdown,保證寫RDB檔案以及将AOF檔案fsync到磁盤,不會丢失資料。 如果是粗暴的Ctrl+C,或者kill -9 就可能丢失。

5.1.1 RDB檔案

  • RDB是整個記憶體的壓縮過的Snapshot,RDB的資料結構,可以配置複合的快照觸發條件,預設是1分鐘内改了1萬次,或5分鐘内改了10次,或15分鐘内改了1次。
  • RDB寫入時,會連記憶體一起Fork出一個新程序,周遊新程序記憶體中的資料寫檔案,這樣就解決了些Snapshot過程中又有新的寫入請求進來的問題。 Fork的細節見4.1最大記憶體。
  • RDB會先寫到臨時檔案,完了再Rename成,這樣外部程式對RDB檔案的備份和傳輸過程是安全的。而且即使寫新快照的過程中Server被強制關掉了,舊的RDB檔案還在。
  • 可配置是否進行壓縮,壓縮方法是字元串的LZF算法,以及将string形式的數字變回int形式存儲。
  • 動态所有停止RDB儲存規則的方法:redis-cli config set save ""

5.1.2 AOF檔案

  • 記錄檔,記錄所有有效的寫操作,等于mysql的binlog,格式就是明文的Redis協定的純文字檔案。
  • 一 般配置成每秒調用一次fdatasync将kernel的檔案緩存刷到磁盤。當作業系統非正常關機時,檔案可能會丢失不超過2秒的資料(更嚴謹的定義見 後)。 如果設為fsync always,性能隻剩幾百TPS,不用考慮。如果設為no,靠作業系統自己的sync,Linux系統一般30秒一次。
  • AOF檔案持續增長而過大時,會fork出一條新程序來将檔案重寫(也是先寫臨時檔案,最後再rename,), 周遊新程序的記憶體中資料,每條記錄有一條的Set語句。預設配置是當AOF檔案大小是上次rewrite後大小的一倍,且檔案大于64M時觸發。
  • Redis協定, 如set mykey hello, 将持久化成*3 $3 set $5 mykey $5 hello, 第一個數字代表這條語句有多少元,其他的數字代表後面字元串的長度。這樣的設計,使得即使在寫檔案過程中突然關機導緻檔案不完整,也能自我修複,執行 redis-check-aof即可。

綜上所述,RDB的資料不實時,同時使用兩者時伺服器重新開機也隻會找AOF檔案。那要不要隻使用AOF呢?作者建議不要,因為RDB更适合用于備份資料庫(AOF在不斷變化不好備份),快速重新開機,而且不會有AOF可能潛在的bug,留着作為一個萬一的手段。

5.1.3 讀寫性能

  • AOF重寫和RDB寫入都是在fork出新程序後,周遊新程序的記憶體順序寫的,既不阻塞主程序繼續處理用戶端請求,順序寫的速度也比随機寫快。
  • 測試把剛才benchmark的11G資料寫成一個1.3的RDB檔案,或者等大的AOF檔案rewrite,需要80秒,在redis-cli info中可檢視。啟動時載入一個AOF或RDB檔案的速度與上面寫入時相同,在log中可檢視。
  • Fork一個使用了大量記憶體的程序也要時間,大約10ms per GB的樣子,但Xen在EC2上是讓人郁悶的239ms,各種系統的對比,Info指令裡的latest_fork_usec顯示上次花費的時間。
  • 在 bgrewriteaof過程中,所有新來的寫入請求依然會被寫入舊的AOF檔案,同時放到buffer中,當rewrite完成後,會在主線程把這部分 内容合并到臨時檔案中之後才rename成新的AOF檔案,是以rewrite過程中會不斷列印"Background AOF buffer size: 80 MB, Background AOF buffer size: 180 MB",計算系統容量時要留意這部分的記憶體消耗。注意,這個合并的過程是阻塞的,如果你産生了280MB的buffer,在100MB/s的傳統硬碟 上,Redis就要阻塞2.8秒!!!
  • NFS或者Amazon上的EBS都不推薦,因為它們也要消耗帶寬。
  • bgsave和bgaofrewrite不會被同時執行,如果bgsave正在執行,bgaofrewrite會自動延後。
  • 2.4版以後,寫入AOF時的fdatasync由另一條線程來執行,不會再阻塞主線程。
  • 2.4版以後,lpush/zadd可以輸入一次多個值了,使得AOF重寫時可以将舊版本中的多個lpush/zadd指令合成一個,每64個key串一串。

5.1.4 性能調整

因為RDB檔案隻用作後備用途,建議隻在Slave上持久化RDB檔案,而且隻要15分鐘備份一次就夠了,隻保留save 900 1這條規則。

如 果Enalbe AOF,好處是在最惡劣情況下也隻會丢失不超過兩秒資料,啟動腳本較簡單隻load自己的AOF檔案就可以了。代價一是帶來了持續的IO,二是AOF rewrite的最後将rewrite過程中産生的新資料寫到新檔案造成的阻塞幾乎是不可避免的。隻要硬碟許可,應該盡量減少AOF rewrite的頻率,AOF重寫的基礎大小預設值64M太小了,可以設到5G以上。預設超過原大小100%大小時重寫可以改到适當的數值,比如之前的 benchmark每個小時會産生40G大小的AOF檔案,如果硬碟能撐到半夜系統閑時才用cron排程bgaofrewrite就好了。

如 果不Enable AOF ,僅靠Master-Slave Replication 實作高可用性也可以。能省掉一大筆IO也減少了rewrite時帶來的系統波動。代價是如果Master/Slave同時倒掉,會丢失十幾分鐘的資料,啟 動腳本也要比較兩個Master/Slave中的RDB檔案,載入較新的那個。新浪微網誌就選用了這種架構,見Tim的部落格

5.1.5 Trouble Shooting —— Enable AOF可能導緻整個Redis被Block住,在2.6.12版之前

現象描述:當AOF rewrite 15G大小的記憶體時,Redis整個死掉的樣子,所有指令甚至包括slave發到master的ping,redis-cli info都不能被執行。

原因分析:

  • 官方文檔,由IO産生的Latency詳細分析, 已經預言了悲劇的發生,但一開始沒留意。
  • Redis為求簡單,采用了單請求處理線程結構。
  • 打開AOF持久化功能後, Redis處理完每個事件後會調用write(2)将變化寫入kernel的buffer,如果此時write(2)被阻塞,Redis就不能處理下一個事件。
  • Linux規定執行write(2)時,如果對同一個檔案正在執行fdatasync(2)将kernel buffer寫入實體磁盤,或者有system wide sync在執行,write(2)會被block住,整個Redis被block住。
  • 如 果系統IO繁忙,比如有别的應用在寫盤,或者Redis自己在AOF rewrite或RDB snapshot(雖然此時寫入的是另一個臨時檔案,雖然各自都在連續寫,但兩個檔案間的切換使得磁盤磁頭的尋道時間加長),就可能導緻 fdatasync(2)遲遲未能完成進而block住write(2),block住整個Redis。
  • 為了更清晰的看到fdatasync(2)的執行時長,可以使用"strace -p (pid of redis server) -T -e -f trace=fdatasync",但會影響系統性能。
  • Redis 提供了一個自救的方式,當發現檔案有在執行fdatasync(2)時,就先不調用write(2),隻存在cache裡,免得被block。但如果已經 超過兩秒都還是這個樣子,則會硬着頭皮執行write(2),即使redis會被block住。此時那句要命的log會列印:“Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.” 之後用redis-cli INFO可以看到aof_delayed_fsync的值被加1。
  • 是以,對于fsync設為 everysec時丢失資料的可能性的最嚴謹說法是:如果有fdatasync在長時間的執行,此時redis意外關閉會造成檔案裡不多于兩秒的資料丢 失。如果fdatasync運作正常,redis意外關閉沒有影響,隻有當作業系統crash時才會造成少于1秒的資料丢失。

解決方法:

最 後發現,原來是AOF rewrite時一直埋頭的調用write(2),由系統自己去觸發sync。在RedHat Enterprise 6裡,預設配置vm.dirty_background_ratio=10,也就是占用了10%的可用記憶體才會開始背景flush,而我的伺服器有64G 記憶體。很明顯一次flush太多資料會造成阻塞,是以最後果斷設定了sysctl vm.dirty_bytes=33554432(32M),問題解決。

然後提了個issue,AOF rewrite時定時也執行一下fdatasync嘛, antirez三分鐘後就回複了,新版中,AOF rewrite時32M就會重寫主動調用fdatasync。

5.2 Master-Slave複制

5.2.1 概述

  • slave可以在配置檔案、啟動指令行參數、以及redis-cli執行SlaveOf指令來設定自己是奴隸。
  • 測試表明同步延時非常小,指令一旦執行完畢就會立刻寫AOF檔案和向Slave轉發,除非Slave自己被阻塞住了。
  • 比較蠢的是,即使在配置檔案裡設了slavof,slave啟動時依然會先從資料檔案載入一堆沒用的資料,再去執行slaveof。
  • "Slaveof no one",立馬變身master。
  • 2.8版本将支援PSYNC部分同步,master會撥出一小段記憶體來存放要發給slave的指令,如果slave短暫的斷開了,重連時會從記憶體中讀取需要補讀的指令,這樣就不需要斷開兩秒也搞一次全同步了。但如果斷開時間較長,已經超過了記憶體中儲存的資料,就還是要全同步。
  • Slave也可以接收Read-Only的請求。

5.2.2 slaveof執行過程,完全重用已有功能,非常經濟

  • 先執行一次全同步 -- 請求master BgSave出自己的一個RDB Snapshot檔案發給slave,slave接收完畢後,清除掉自己的舊資料,然後将RDB載入記憶體。
  • 再進行增量同步 -- master作為一個普通的client連入slave,将所有寫操作轉發給slave,沒有特殊的同步協定。

5.2.3 Trouble Shooting again

有時候明明master/slave都活得好好的,突然間就說要重新進行全同步了:

1.Slave顯示:# MASTER time out: no data nor PING received...

slave 會每隔repl-ping-slave-period(預設10秒)ping一次master,如果超過repl-timeout(預設60秒)都沒有收 到響應,就會認為Master挂了。如果Master明明沒挂但被阻塞住了也會報這個錯。可以适當調大repl-timeout。

2.Master 顯示:# Client addr=10.175.162.123:44670 flags=S oll=104654 omem=2147487792 events=rw cmd=sync scheduled to be closed ASAP for overcoming of output buffer limits.

當slave沒挂但被阻塞住了,比如正在loading Master發過來的RDB, Master的指令不能立刻發送給slave,就會放在output buffer中(見oll是指令數量,omem是大小),在配置檔案中有如下配置:client-output-buffer-limit slave 256mb 64mb 60, 這是說負責發資料給slave的client,如果buffer超過256m或者連續60秒超過64m,就會被立刻強行關閉!!! Traffic大的話一定要設大一點。否則就會出現一個很悲劇的循環,Master傳輸一個大的RDB給Slave,Slave努力的裝載,但還沒裝載 完,Master對client的緩存滿了,再來一次。

平時可以在master執行 redis-cli client list 找那個cmd=sync,flag=S的client,注意OMem的變化。

5.3 Fail-Over

Redis-sentinel是2.6版開始加入的另一組獨立運作的節點,提供自動Fail Over的支援。

  • 官方文檔 

    Redis核心解讀–叢集管理工具(Redis-sentinel)

  • antirez 對 Sentinel的反駁,與下篇

5.3.1 主要執行過程

  • Sentinel每秒鐘對所有master,slave和其他sentinel執行Ping,redis-server節點要應答+PONG或-LOADING或-MASTERDOWN.
  • 如果某一台Sentinel沒有在30秒内(可配置得短一些哦)收到上述正确應答,它就會認為master處于sdown狀态(主觀Down)
  • 它向其他sentinel詢問是否也認為該master倒了(SENTINEL is-master-down-by-addr ), 如果quonum台(預設是2)sentinel在5秒鐘内都這樣認為,就會認為master真是odown了(客觀Down)。
  • 此時會選出一台sentinel作為Leader執行fail-over, Leader會從slave中選出一個提升為master(執行slaveof no one),然後讓其他slave指向它(執行slaveof new master)。

5.3.2 master/slave 及 其他sentinel的發現

master 位址在sentinel.conf裡, sentinel會每10秒一次向master發送INFO,知道master的slave有哪些。 如果master已經變為slave,sentinel會分析INFO的應答指向新的master。以前,sentinel重新開機時,如果master已經 切換過了,但sentinel.conf裡master的位址并沒有變,很可能有悲劇發生。另外master重新開機後如果沒有切換成slave,也可能有悲 劇發生。新版好像修複了一點這個問題,待研究。

另外,sentinel會在master上建一個pub/sub channel,名為"sentinel:hello",通告各種資訊,sentinel們也是通過接收pub/sub channel上的+sentinel的資訊發現彼此,因為每台sentinel每5秒會發送一次自己的host資訊,宣告自己的存在。

5.3.3 自定義reconfig腳本

  • sentinel在failover時還會執行配置檔案裡指定的使用者自定義reconfig腳本,做使用者自己想做的事情,比如讓master變為slave并指向新的master。
  • 腳 本的将會在指令行按順序傳入如下參數:
  • 腳本傳回0是正常,如果傳回1會被重新執行,如果傳回2或以上不會。 如果超過60秒沒傳回會被強制終止。

覺得Sentinel至少有兩個可提升的地方:

  • 一是如果master 主動shutdown,比如系統更新,有辦法主動通知sentinel提升新的master,減少服務中斷時間。
  • 二是比起redis-server太原始了,要自己醜陋的以nohup sentinel > logfile 2>&1 & 啟動,也不支援shutdown指令,要自己kill pid。

5.4 Client的高可用性

基 于Sentinel的方案,client需要執行語句SENTINEL get-master-addr-by-name mymaster 可獲得目前master的位址。 Jedis正在內建sentinel,已經支援了sentinel的一些指令,但還沒釋出,但sentinel版的連接配接池則暫時完全沒有,在公司的項目裡 我參考網友的項目自己寫了一個。

淘寶的Tedis driver,使用了完全不同的思路,不基于Sentinel,而是多寫随機讀, 一開始就同步寫入到所有節點,讀的話随便讀一個還活着的節點就行了。但有些節點成功有些節點失敗如何處理? 節點死掉重新起來後怎麼重新同步?什麼時候可以重新Ready? 是以不是很敢用。

另外如Ruby寫的redis_failover,也是抛開了Redis Sentinel,基于ZooKeeper的臨時方案。

Redis作者也在部落格裡抱怨怎麼沒有人做Dynamo-style 的client。

6. 運維

6.1 安裝

  • 安裝包制作:沒有現成,需要自己編譯,自己寫rpm包的腳本,可參考utils中的install_server.sh與redis_init_script。

    但 RHEL下設定script runlevel的方式不一樣,redis_init_script中要增加一句 "# chkconfig: 345 90 10" ,而install_server.sh可以删掉後面的那句“chkconfig --level 345 reis"

  • 雲服務:http://garantiadata.com/ 

    和 

    http://redistogo.com/ 

    在Amazon和Heroku上提供雲服務,供同樣部署在此兩者上的應用使用。

  • Chef Recipes:brianbianco/redisio,活躍,同步更新版本。

6.2 部署模型

  • Redis隻能使用單線程,為了提高CPU使用率,有提議在同一台伺服器上啟動多個Redis執行個體,但這會帶來嚴重的IO争用,除非Redis不需要持久化,或者有某種方式保證多個執行個體不會在同一個時間重寫AOF。
  • 一組sentinel能同時監控多個Master。
  • 有提議說環形的slave結構,即master隻連一個slave,然後slave再連slave,此部署有兩個前提,一是有大量的隻讀需求需要在slave完成,二是對slave傳遞時的資料不一緻性不敏感。

6.3 配置

對redif.conf預設配置的修改見附錄1。

6.3.1 三條路

  • 可以配置檔案中編寫。
  • 可以在啟動時的指令行配置,redis-server --port 7777 --slaveof 127.0.0.1 8888。
  • 雲時代大規模部署,把配置檔案滿街傳顯然不是好的做法, 可以用redis-cli執行Config Set指令, 修改所有的參數,達到維護人員最愛的不重新開機服務而修改參數的效果,而且在新版本裡還可以執行 

    Config Rewrite 

    将改動寫回到檔案中,不過全部預設值都會列印出來,可能會破壞掉原來的檔案的排版,注釋。

6.3.2 安全保護

  • 在配置檔案裡設定密碼:requirepass foobar。
  • 禁止某些危險指令,比如殘暴的FlushDB,将它rename成"":rename-command FLUSHDB ""。

6.4 監控與維護

綜述: Redis監控技巧

6.4.1 監控指令

Info指 令将傳回非常豐富的資訊。 着重監控檢查記憶體使用,是否已接近上限,used_memory是Redis申請的記憶體,used_memory_rss是作業系統配置設定給Redis的物 理記憶體,兩者之間隔着碎片,隔着Swap。 還有重點監控 AOF與RDB檔案的儲存情況,以及master-slave的關系。Statistic 資訊還包括key命中率,所有指令的執行次數,所有client連接配接數量等, CONFIG RESETSTAT 可重置為0。

Monitor指令可以顯示Server收到的所有指令,主要用于debug,影響性能,生産環境慎用。

SlowLog 檢查慢操作(見2.性能)。

6.4.2 Trouble Shooting支援

  • 日志可以動态的設定成verbose/debug模式,但不見得有更多有用的log可看,verbose還會很煩的每5秒列印目前的key情況和client情況。指令為config set loglevel verbose。
  • 最愛Redis的地方是代碼隻有2.3萬行,而且編碼優美,而且huangz同學還在原來的注釋上再加上了中文注釋——Redis 2.6源碼中文注釋版 

    ,是以雖然是C寫的代碼,雖然有十年沒看過C代碼,但這幾天trouble shooting毫無難度,一看就懂。

  • Trobule shotting的經曆證明antirez處理issue的速度非常快(如果你的issue言之有物的話),比Weblogic之類的商業支援還好。

6.4.3 三方工具

官網列出了如下工具,但暫時沒發現會直接拿來用的:

  • Redis Live,基于Python的web應用,使用Info和Monitor獲得系統情況和指令統計分析。 因為Monitor指令影響性能,是以建議用cron定期運作,每次偷偷采樣兩分鐘的樣子。
  • phpRedisAdmin,基于php的Web應用,目标是MysqlAdmin那樣的管理工具,可以管理每一條Key的情況,但它的界面應該隻适用于Key的數量不太多的情況,Demo。
  • Redis Faina,基于Python的指令行,Instagram出品,使用者自行獲得Monitor的輸出後發給它進行統計分析。由于Monitor輸出的格式在Redis版本間不一樣,要去github下最新版。
  • Redis-rdb-tools 

    基于Python的指令行,可以分析RDB檔案每條Key對應value所占的大小,還可以将RDB dump成普通文本檔案然後比較兩個庫是否一緻,還可以将RDB輸出成JSON格式,可能是最有用的一個了。

  • Redis Sampler,基于Ruby的指令行,antirez自己寫的,統計資料分布情況。

7. Java Driver

7.1 Driver選擇

各個Driver好像隻有Jedis比較活躍,但也5個月沒送出了,也是Java裡唯一的Redis官方推薦。

Spring Data Redis的 封裝并不太必要,因為Jedis已足夠簡單,沒有像Spring Data MongoDB對MongoDB java driver的封裝那樣大幅簡化代碼,頂多就是加強了一點點點pipeline和transaction狀态下的coding,禁止了一些此狀态下不能用 的指令。而所謂屏蔽各種底層driver的差異并不太吸引人,因為我就沒打算選其他幾種driver。有興趣的可以翻翻它的JedisConnection代碼。

是以,SpringSide直接在Jedis的基礎上,按Spring的風格封裝了一個JedisTemplate,負責從池中擷取與歸還Jedis執行個體,處理異常。

7.2 Jedis的細節

Jedis基于Apache Commons Pool做的連接配接池,預設最大連接配接數隻有8,必須自行重新設定。

Jedis的blocking pop函數,應用執行ExecutorService.shutdownNow()中斷線程時并不能把它中斷,見讨論組。兩個解決方法:

  • 不要用不限時的blocking popup,傳多一個逾時時間參數,如5秒。
  • 找地方将調用blocking popup的jedis儲存起來,shutdown時主動調用它的close。

8. Windows的版本

Windows版本友善對應用的本地開發調試,但Redis并沒有提供,好在微軟提供了一個依賴LibUV實作相容的更新檔,https://github.com/MSOpenTech/redis,但redis作者拒絕合并到master中,微軟隻好苦憋的時時人工同步。 目前的穩定版是2.6版本,支援Lua腳本。

因為github現在已經沒有Download服務了,是以編譯好的可執行檔案藏在這裡:

  • https://github.com/MSOpenTech/redis/tree/2.6/bin/release

9. 成功案例

注:下文中的連結都是網站的架構描述文檔。

Twitter和新浪微網誌, 都屬于将Redis各種資料結構用得出神入化的那種,如何釋出大V如奧巴馬的消息是它們最頭痛的問題。

Tumblr: 11億美刀賣給Yahoo的圖檔日志網站,22 台Redis server,每台運作8 - 32個執行個體,總共100多個Redis執行個體在跑。有着Redis has been completely problem free and the community is great的崇高評價。Redis在裡面扮演了八爪魚多面手的角色:

  • Dashboard的海量通知的存儲。
  • Dashboard的二級索引。
  • 存儲海量短連結的HBase前面的緩存。
  • Gearman Job Queue的存儲。
  • 正在替換另外30台memcached。

Instagram ,曾經,Redis powers their main feed, activity feed, sessions system, and other services。但可惜目前已遷往Cassandra,說新架構隻需1/4的硬體費用,是的,就是那個導緻Digg CTO辭職的Canssandra。

Flickr , 依然是asynchronous task system and rudimentary queueing system。之前Task system放在mysql innodb,根本,撐不住。

The Others:

  • Pinterest,混合使用MySQL、Membase與Redis作為存儲。
  • StackOverflow 

    ,2 Dell R610 Redis servers for distribute caching layer,好窮好輕量。

  • Youporn.com,100%的Redis,MySQL隻用于建立新需求用到的sorted set,300K QPS的大壓力。
  • 日本微信 

    ,Redis在前負責異步Job Queue和O(n)的資料,且作為O(n*t)資料的cache,HBase在後,負責O(n*t)資料, n是使用者,t是時間。

  • Discourge,号稱是為下一個十年打造的論壇系統, We use Redis for our job queue, rate limiting, as a cache and for transient data,剛好和我司的用法一樣。

附錄

附錄1: 對redis.conf預設配置的修改

Master上

  • daemonize no -> yes ,啟動daemonize模式,注意如果用daemon工具啟動redis-server時設回false。
  • logfile stdout -> /var/log/redis/redis.log ,指定日志檔案
  • 注釋掉RDB的所有觸發規則,在Master不儲存RDB檔案。
  • dir ./ -> /var/data/redis,指定持久化檔案及臨時檔案目錄.
  • maxmemory,設定為可用記憶體/2.
  • (可選)appendonly no->yes,打開AOF檔案.
  • auto-aof-rewrite-percentage 100, 綜合考慮硬碟大小,可接受重新開機加載延時等盡量的大,減少AOF rewrite頻率.
  • auto-aof-rewrite-min-size 64mb,同上,起碼設為5G.
  • client-output-buffer-limit slave 256mb 64mb 60. 考慮Traffic及Slave同步是RDB加載所需時間,正确設定避免buffer撐爆client被關掉後又要重新進行全同步。
  • 安全配置,可選。

Slave上

  • 設定RDB儲存頻率,因為RDB隻作為Backup工具,隻保留15分鐘的規則,設定為15分鐘儲存一次就夠了save 900 1。
  • (可選)slaveof 設定master位址,也可動态設定。

附錄2:版本變更曆史

繼續閱讀